├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── images └── ScreenShot.png ├── pom.xml └── src └── main ├── java └── org │ └── onebusaway │ └── gtfs_realtime │ └── visualizer │ ├── DataServlet.java │ ├── Vehicle.java │ ├── VehicleListener.java │ ├── VisualizerMain.java │ ├── VisualizerModule.java │ ├── VisualizerServer.java │ └── VisualizerService.java └── resources ├── log4j.properties └── org └── onebusaway └── gtfs_realtime └── visualizer ├── WhiteCircle8.png ├── index.css ├── index.html ├── index.js └── usage.txt /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | cache: maven 25 | - name: Build with Maven 26 | run: mvn -B package --file pom.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | release.properties 3 | pom.xml.releaseBackup 4 | .settings 5 | .project 6 | .classpath -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | COPYRIGHT_SECTION 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | onebusaway-gtfs-realtime-visualizer [![Java CI with Maven](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/actions/workflows/maven.yml/badge.svg)](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/actions/workflows/maven.yml) 2 | ====================== 3 | 4 | A demo application to visualize GTFS Realtime feeds. 5 | 6 | ## Introduction 7 | 8 | The goal of the [GTFS Realtime](https://developers.google.com/transit/gtfs-realtime/) specification is to provide both transit agencies and developers with a consistent way to exchange real-time public transit data. A specific feature of GTFS Realtime is support for [vehicle positions](https://developers.google.com/transit/gtfs-realtime/vehicle-positions), which allow an agency to specify information about vehicle locations. 9 | 10 | We want to make it easy for developers to work with vehicle position using the GTFS Realtime format, so we've put together a quick demo project that shows how to consume a GTFS Realtime vehicle positions feed. 11 | 12 | ![Screen Shot](/images/ScreenShot.png) 13 | 14 | In the demo project, we'll walk you through a simple Java-based program that consumes a GTFS Realtime feed and visualizes it on a map. 15 | 16 | If you want to follow along at home, you can [download a ZIP of the source-code for the demo project](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/zipball/master) or [import the code from the Git repository directly](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer). The project is designed to be built with [Apache Maven](http://maven.apache.org/), so download that if you are interested in building and running the project. 17 | 18 | ## Running the Demo Project 19 | 20 | Before we dig into how the demo application the works, let's see it in action! After you've downloaded the code for the project, open up a terminal and change to the root directory of the project. From there, run Maven to compile and package the application: 21 | 22 | mvn package 23 | 24 | Now that the project has been built, you should be able to run the resulting application bundle: 25 | 26 | java -jar target/onebusaway-GTFS Realtime-visualizer-0.0.1-SNAPSHOT.jar \ 27 | --vehiclePositionsUrl=https://cdn.mbta.com/realtime/VehiclePositions.pb 28 | 29 | This will start the application up using [MBTA's GTFS Realtime feeds](http://mbta.com/rider_tools/developers/default.asp?id=22393). When the application starts, you should see a message like: 30 | 31 | ======================================================= 32 | 33 | The GTFS-realtime visualizer server has 34 | started. Check it out at: 35 | 36 | http://localhost:8080/ 37 | 38 | ======================================================= 39 | 40 | Note: In Java 9 and later versions, you may get an error like: `java.lang.ClassNotFoundException: javax.annotation.PostConstruct`. Use this command: 41 | ``` 42 | java --add-modules java.xml.ws.annotation -jar target/onebusaway-gtfs-realtime-visualizer-0.0.1-SNAPSHOT.jar\ 43 | --vehiclePositionsUrl=https://cdn.mbta.com/realtime/VehiclePositions.pb 44 | ``` 45 | Refer [this issue](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/issues/10) for details. 46 | 47 | So browse to [http://localhost:8080/](http://localhost:8080/) and take a look! The map update uses a number of HTML5 technologies, so be sure to use a modern browser like [Google Chrome](https://www.google.com/chrome) for the best experience. If all goes well, you should see a map of real-time vehicle positions that update over time. 48 | 49 | Please note that if you use this application with your own GTFS-rt vehicle positions feed, the `VehicleDescriptor` must contain the `id` field, such as: 50 | 51 | ~~~ 52 | entity { 53 | id: "vehicle_position_2403" 54 | vehicle { 55 | position { 56 | latitude: 28.06235 57 | longitude: -82.45927 58 | bearing: 360.0 59 | speed: 0.0 60 | } 61 | vehicle { 62 | id: "2403" // This is required for this application 63 | } 64 | } 65 | } 66 | ~~~ 67 | 68 | The `id` field is required so that the vehicle path history can be drawn on the map over multiple refreshes of the feed. 69 | 70 | ## Digging into the Code 71 | 72 | So how does this all work? Let's look at the code! Most of the work with GTFS Realtime data is done in the following class: 73 | 74 | [org.onebusaway.gtfs_realtime.visualizer.VisualizerService](https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/blob/master/src/main/java/org/onebusaway/gtfs_realtime/visualizer/VisualizerService.java) 75 | 76 | The class does the following: 77 | 78 | * Periodically downloads vehicle data from the GTFS Realtime feeds. 79 | * Extracts relevant position data. 80 | * Notifies listeners of updated data. 81 | 82 | Let's step through the code for each of these steps. 83 | 84 | We setup a recurring task that downloads data from the GTFS Realtime feeds. The data comes in the form of a [Protocol Buffer](http://code.google.com/p/protobuf/) stream, which we can parse to create a [FeedMessage](https://developers.google.com/transit/gtfs-realtime/reference#FeedMessage): 85 | 86 | FeedMessage feed = FeedMessage.parseFrom(_vehiclePositionsUrl.openStream()); 87 | 88 | The feed message contains a series of [FeedEntity](https://developers.google.com/transit/gtfs-realtime/reference#FeedEntity) objects that contains information about a vehicle, a trip, or an alert. In our case, we are interested in vehicle position data, so we will look for a [VehiclePosition](https://developers.google.com/transit/gtfs-realtime/reference#VehiclePosition) for each entity, along with a [Position](https://developers.google.com/transit/gtfs-realtime/reference#Position) for the vehicle. 89 | 90 | for (FeedEntity entity : feed.getEntityList()) { 91 | if (!entity.hasVehicle()) { 92 | continue; 93 | } 94 | VehiclePosition vehicle = entity.getVehicle(); 95 | if (vehicle.hasPosition()) { 96 | continue; 97 | } 98 | Position position = vehicle.getPosition(); 99 | 100 | Getting access to a vehicle location is that simple! 101 | 102 | Of course, we've left out a few details. We take advantage of a couple of OneBusAway libraries to simplify our application: 103 | 104 | * The [onebusaway-gtfs-realtime-api](https://github.com/OneBusAway/onebusaway-gtfs-realtime-api/wiki) module provides pre-packaged Java classes generated from the [GTFS-realtime protocol buffer definition](https://developers.google.com/transit/gtfs-realtime/gtfs-realtime-proto). 105 | 106 | ## Next Steps 107 | 108 | We provided this demo application as an example that you can build on when working with GTFS Realtime feeds. Integrating GTFS Realtime feeds with alerts, trip updates, and vehicle positions along with existing GTFS schedule data can be complex, but it can also allow you to build powerful applications. 109 | 110 | Also be sure to check out [all our GTFS Realtime resources](https://github.com/OneBusAway/onebusaway/wiki/GTFS-Realtime-Resources). -------------------------------------------------------------------------------- /images/ScreenShot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/85da7a331f18ac34065feca471367724c3de19cd/images/ScreenShot.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | onebusaway 6 | org.onebusaway 7 | 1.1.9 8 | 9 | onebusaway-gtfs-realtime-visualizer 10 | 0.0.1-SNAPSHOT 11 | onebusaway-gtfs-realtime-visualizer 12 | A visualizer for GTFS-realtime transit data. 13 | https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/wiki/ 14 | 15 | 16 | 17 | central 18 | https://repo1.maven.org/maven2/ 19 | 20 | 21 | repo.camsys-apps.com 22 | https://repo.camsys-apps.com/third-party/ 23 | 24 | 25 | releases-camsys-public-repo 26 | https://repo.camsys-apps.com/releases/ 27 | 28 | true 29 | 30 | 31 | false 32 | 33 | 34 | 35 | snapshots-camsys-public-repo 36 | https://repo.camsys-apps.com/snapshots/ 37 | 38 | false 39 | 40 | 41 | true 42 | 43 | 44 | 45 | 46 | 47 | scm:git:https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer.git 48 | scm:git:ssh://git@github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer.git 49 | https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer 50 | 51 | 52 | 53 | GitHub 54 | https://github.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/issues 55 | 56 | 57 | 58 | 8.1.11.v20130520 59 | 60 | 9900 61 | 62 | 63 | 64 | org.onebusaway 65 | onebusaway-gtfs-realtime-api 66 | 1.0.2 67 | 68 | 69 | org.onebusaway 70 | onebusaway-guice-jsr250 71 | 1.0.1 72 | 73 | 74 | org.eclipse.jetty 75 | jetty-webapp 76 | ${jetty.version} 77 | 78 | 79 | org.eclipse.jetty 80 | jetty-servlets 81 | ${jetty.version} 82 | 83 | 84 | org.eclipse.jetty 85 | jetty-websocket 86 | ${jetty.version} 87 | 88 | 89 | org.json 90 | json 91 | 20090211 92 | 93 | 94 | org.onebusaway 95 | onebusaway-cli 96 | 1.0.2 97 | 98 | 99 | org.slf4j 100 | slf4j-api 101 | 1.6.4 102 | 103 | 104 | org.slf4j 105 | slf4j-log4j12 106 | 1.6.4 107 | 108 | 109 | 110 | javax.annotation 111 | javax.annotation-api 112 | 1.3.2 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-shade-plugin 121 | 1.5 122 | 123 | 124 | package 125 | 126 | shade 127 | 128 | 129 | withAllDependencies 130 | 131 | 132 | org.onebusaway.gtfs_realtime.visualizer.VisualizerMain 133 | 134 | 135 | 136 | 137 | *:* 138 | 139 | META-INF/*.SF 140 | META-INF/*.DSA 141 | META-INF/*.RSA 142 | META-INF/DEPENDENCIES 143 | META-INF/LICENSE* 144 | META-INF/NOTICE* 145 | META-INF/eclipse.inf 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/DataServlet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import java.io.IOException; 19 | import java.io.PrintWriter; 20 | import java.util.List; 21 | import java.util.Set; 22 | 23 | import javax.annotation.PostConstruct; 24 | import javax.annotation.PreDestroy; 25 | import javax.inject.Inject; 26 | import javax.inject.Singleton; 27 | import javax.servlet.ServletException; 28 | import javax.servlet.http.HttpServletRequest; 29 | import javax.servlet.http.HttpServletResponse; 30 | 31 | import org.eclipse.jetty.util.ConcurrentHashSet; 32 | import org.eclipse.jetty.websocket.WebSocket; 33 | import org.eclipse.jetty.websocket.WebSocketServlet; 34 | import org.json.JSONArray; 35 | import org.json.JSONException; 36 | import org.json.JSONObject; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | @Singleton 41 | public class DataServlet extends WebSocketServlet implements VehicleListener { 42 | 43 | private static final long serialVersionUID = 1L; 44 | 45 | private static final Logger _log = LoggerFactory.getLogger(DataServlet.class); 46 | 47 | private static final int WEB_SOCKET_IDLE_TIMEOUT_MS = 120 * 1000; 48 | 49 | private VisualizerService _visualierService; 50 | 51 | private Set _sockets = new ConcurrentHashSet(); 52 | 53 | private volatile String _vehicles; 54 | 55 | @Inject 56 | public void setVisualizerService(VisualizerService visualizerService) { 57 | _visualierService = visualizerService; 58 | } 59 | 60 | @PostConstruct 61 | public void start() { 62 | _visualierService.addListener(this); 63 | } 64 | 65 | @PreDestroy 66 | public void stop() { 67 | _visualierService.removeListener(this); 68 | } 69 | 70 | @Override 71 | public void handleVehicles(List vehicles) { 72 | String vehiclesAsJsonString = getVehiclesAsString(vehicles); 73 | for (DataWebSocket socket : _sockets) { 74 | socket.sendVehicles(vehiclesAsJsonString); 75 | } 76 | } 77 | 78 | @Override 79 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) 80 | throws ServletException, IOException { 81 | resp.setContentType("application/json"); 82 | PrintWriter writer = resp.getWriter(); 83 | writer.write(_vehicles); 84 | } 85 | 86 | @Override 87 | public WebSocket doWebSocketConnect(HttpServletRequest request, 88 | String protocol) { 89 | return new DataWebSocket(); 90 | } 91 | 92 | public void addSocket(DataWebSocket dataWebSocket) { 93 | String vehiclesAsJsonString = getVehiclesAsString(_visualierService.getAllVehicles()); 94 | dataWebSocket.sendVehicles(vehiclesAsJsonString); 95 | _sockets.add(dataWebSocket); 96 | } 97 | 98 | public void removeSocket(DataWebSocket dataWebSocket) { 99 | _sockets.remove(dataWebSocket); 100 | } 101 | 102 | private String getVehiclesAsString(List vehicles) { 103 | try { 104 | JSONArray array = new JSONArray(); 105 | for (Vehicle vehicle : vehicles) { 106 | JSONObject obj = new JSONObject(); 107 | obj.put("id", vehicle.getId()); 108 | obj.put("lat", vehicle.getLat()); 109 | obj.put("lon", vehicle.getLon()); 110 | obj.put("lastUpdate", vehicle.getLastUpdate()); 111 | array.put(obj); 112 | } 113 | return array.toString(); 114 | } catch (JSONException ex) { 115 | throw new IllegalStateException(ex); 116 | } 117 | } 118 | 119 | class DataWebSocket implements WebSocket { 120 | 121 | private Connection _connection; 122 | 123 | @Override 124 | public void onOpen(Connection connection) { 125 | _connection = connection; 126 | _connection.setMaxIdleTime(WEB_SOCKET_IDLE_TIMEOUT_MS); 127 | addSocket(this); 128 | } 129 | 130 | @Override 131 | public void onClose(int closeCode, String message) { 132 | removeSocket(this); 133 | } 134 | 135 | public void sendVehicles(String vehiclesAsJsonString) { 136 | try { 137 | _connection.sendMessage(vehiclesAsJsonString); 138 | } catch (IOException ex) { 139 | _log.warn("error sending WebSocket message", ex); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/Vehicle.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | public class Vehicle { 19 | private String id; 20 | 21 | private double lat; 22 | 23 | private double lon; 24 | 25 | private long lastUpdate; 26 | 27 | public String getId() { 28 | return id; 29 | } 30 | 31 | public void setId(String id) { 32 | this.id = id; 33 | } 34 | 35 | public double getLat() { 36 | return lat; 37 | } 38 | 39 | public void setLat(double lat) { 40 | this.lat = lat; 41 | } 42 | 43 | public double getLon() { 44 | return lon; 45 | } 46 | 47 | public void setLon(double lon) { 48 | this.lon = lon; 49 | } 50 | 51 | public long getLastUpdate() { 52 | return lastUpdate; 53 | } 54 | 55 | public void setLastUpdate(long lastUpdate) { 56 | this.lastUpdate = lastUpdate; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/VehicleListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import java.util.List; 19 | 20 | public interface VehicleListener { 21 | public void handleVehicles(List vehicles); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/VisualizerMain.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import java.net.URI; 19 | import java.util.HashSet; 20 | import java.util.Set; 21 | 22 | import org.apache.commons.cli.CommandLine; 23 | import org.apache.commons.cli.GnuParser; 24 | import org.apache.commons.cli.Options; 25 | import org.apache.commons.cli.Parser; 26 | import org.onebusaway.cli.CommandLineInterfaceLibrary; 27 | import org.onebusaway.guice.jsr250.LifecycleService; 28 | 29 | import com.google.inject.Guice; 30 | import com.google.inject.Injector; 31 | import com.google.inject.Module; 32 | 33 | public class VisualizerMain { 34 | 35 | private static final String ARG_VEHICLE_POSITIONS_URL = "vehiclePositionsUrl"; 36 | 37 | public static void main(String[] args) throws Exception { 38 | VisualizerMain m = new VisualizerMain(); 39 | m.run(args); 40 | } 41 | 42 | private void run(String[] args) throws Exception { 43 | 44 | if (args.length == 0 || CommandLineInterfaceLibrary.wantsHelp(args)) { 45 | printUsage(); 46 | System.exit(-1); 47 | } 48 | 49 | Options options = new Options(); 50 | buildOptions(options); 51 | Parser parser = new GnuParser(); 52 | CommandLine cli = parser.parse(options, args); 53 | 54 | Set modules = new HashSet(); 55 | VisualizerModule.addModuleAndDependencies(modules); 56 | 57 | Injector injector = Guice.createInjector(modules); 58 | injector.injectMembers(this); 59 | 60 | VisualizerService service = injector.getInstance(VisualizerService.class); 61 | service.setVehiclePositionsUri(new URI( 62 | cli.getOptionValue(ARG_VEHICLE_POSITIONS_URL))); 63 | injector.getInstance(VisualizerServer.class); 64 | 65 | LifecycleService lifecycleService = injector.getInstance(LifecycleService.class); 66 | lifecycleService.start(); 67 | } 68 | 69 | private void printUsage() { 70 | CommandLineInterfaceLibrary.printUsage(getClass()); 71 | } 72 | 73 | private void buildOptions(Options options) { 74 | options.addOption(ARG_VEHICLE_POSITIONS_URL, true, ""); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/VisualizerModule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import java.util.Set; 19 | 20 | import org.onebusaway.guice.jsr250.JSR250Module; 21 | 22 | import com.google.inject.AbstractModule; 23 | import com.google.inject.Module; 24 | 25 | public class VisualizerModule extends AbstractModule { 26 | 27 | public static void addModuleAndDependencies(Set modules) { 28 | modules.add(new VisualizerModule()); 29 | JSR250Module.addModuleAndDependencies(modules); 30 | } 31 | 32 | @Override 33 | protected void configure() { 34 | 35 | } 36 | 37 | /** 38 | * Implement hashCode() and equals() such that two instances of the module 39 | * will be equal. 40 | */ 41 | @Override 42 | public int hashCode() { 43 | return this.getClass().hashCode(); 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) 49 | return true; 50 | if (o == null) 51 | return false; 52 | return this.getClass().equals(o.getClass()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/VisualizerServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import javax.annotation.PostConstruct; 19 | import javax.annotation.PreDestroy; 20 | import javax.inject.Inject; 21 | 22 | import org.eclipse.jetty.server.Handler; 23 | import org.eclipse.jetty.server.Server; 24 | import org.eclipse.jetty.server.handler.HandlerList; 25 | import org.eclipse.jetty.server.handler.ResourceHandler; 26 | import org.eclipse.jetty.servlet.ServletHandler; 27 | import org.eclipse.jetty.servlet.ServletHolder; 28 | import org.eclipse.jetty.util.resource.Resource; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | public class VisualizerServer { 33 | 34 | private static final Logger _log = LoggerFactory.getLogger(VisualizerServer.class); 35 | 36 | private DataServlet _dataServlet; 37 | 38 | private int _port = 8080; 39 | 40 | private Server _server; 41 | 42 | @Inject 43 | public void setDataServlet(DataServlet dataServlet) { 44 | _dataServlet = dataServlet; 45 | } 46 | 47 | public void setPort(int port) { 48 | _port = port; 49 | } 50 | 51 | @PostConstruct 52 | public void start() throws Exception { 53 | _server = new Server(_port); 54 | 55 | ResourceHandler resourceHandler = new ResourceHandler(); 56 | resourceHandler.setWelcomeFiles(new String[] {"index.html"}); 57 | resourceHandler.setBaseResource(Resource.newClassPathResource("org/onebusaway/gtfs_realtime/visualizer")); 58 | 59 | ServletHandler servletHandler = new ServletHandler(); 60 | servletHandler.addServletWithMapping(new ServletHolder(_dataServlet), 61 | "/data.json"); 62 | 63 | HandlerList handlers = new HandlerList(); 64 | handlers.setHandlers(new Handler[] {resourceHandler, servletHandler}); 65 | 66 | _server.setHandler(handlers); 67 | 68 | _server.start(); 69 | 70 | StringBuilder b = new StringBuilder(); 71 | b.append("\n"); 72 | b.append("=======================================================\n"); 73 | b.append("\n"); 74 | b.append(" The GTFS-realtime visualizer server has\n"); 75 | b.append(" started. Check it out at:\n"); 76 | b.append("\n"); 77 | b.append(" http://localhost:" + _port + "/\n"); 78 | b.append("\n"); 79 | b.append("=======================================================\n"); 80 | 81 | _log.info(b.toString()); 82 | } 83 | 84 | @PreDestroy 85 | public void stop() throws Exception { 86 | _server.stop(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/visualizer/VisualizerService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.visualizer; 17 | 18 | import java.io.IOException; 19 | import java.net.URI; 20 | import java.net.URL; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.concurrent.ConcurrentHashMap; 26 | import java.util.concurrent.CopyOnWriteArrayList; 27 | import java.util.concurrent.Executors; 28 | import java.util.concurrent.Future; 29 | import java.util.concurrent.ScheduledExecutorService; 30 | import java.util.concurrent.TimeUnit; 31 | 32 | import javax.annotation.PostConstruct; 33 | import javax.annotation.PreDestroy; 34 | import javax.inject.Singleton; 35 | 36 | import org.eclipse.jetty.websocket.WebSocket.Connection; 37 | import org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage; 38 | import org.eclipse.jetty.websocket.WebSocketClient; 39 | import org.eclipse.jetty.websocket.WebSocketClientFactory; 40 | import org.slf4j.Logger; 41 | import org.slf4j.LoggerFactory; 42 | 43 | import com.google.protobuf.InvalidProtocolBufferException; 44 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 45 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 46 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 47 | import com.google.transit.realtime.GtfsRealtime.Position; 48 | import com.google.transit.realtime.GtfsRealtime.VehicleDescriptor; 49 | import com.google.transit.realtime.GtfsRealtime.VehiclePosition; 50 | 51 | @Singleton 52 | public class VisualizerService { 53 | 54 | private static final Logger _log = LoggerFactory.getLogger(VisualizerService.class); 55 | 56 | private URI _vehiclePositionsUri; 57 | 58 | private ScheduledExecutorService _executor; 59 | 60 | private WebSocketClientFactory _webSocketFactory; 61 | 62 | private WebSocketClient _webSocketClient; 63 | 64 | private IncrementalWebSocket _incrementalWebSocket; 65 | 66 | private Future _webSocketConnection; 67 | 68 | private Map _vehicleIdsByEntityIds = new HashMap(); 69 | 70 | private Map _vehiclesById = new ConcurrentHashMap(); 71 | 72 | private List _listeners = new CopyOnWriteArrayList(); 73 | 74 | private final RefreshTask _refreshTask = new RefreshTask(); 75 | 76 | private int _refreshInterval = 20; 77 | 78 | private boolean _dynamicRefreshInterval = true; 79 | 80 | private long _mostRecentRefresh = -1; 81 | 82 | public void setVehiclePositionsUri(URI uri) { 83 | _vehiclePositionsUri = uri; 84 | } 85 | 86 | @PostConstruct 87 | public void start() throws Exception { 88 | String scheme = _vehiclePositionsUri.getScheme(); 89 | if (scheme.equals("ws") || scheme.equals("wss")) { 90 | _webSocketFactory = new WebSocketClientFactory(); 91 | _webSocketFactory.start(); 92 | _webSocketClient = _webSocketFactory.newWebSocketClient(); 93 | _webSocketClient.setMaxBinaryMessageSize(16384000); 94 | _incrementalWebSocket = new IncrementalWebSocket(); 95 | _webSocketConnection = _webSocketClient.open(_vehiclePositionsUri, 96 | _incrementalWebSocket); 97 | } else { 98 | _executor = Executors.newSingleThreadScheduledExecutor(); 99 | _executor.schedule(_refreshTask, 0, TimeUnit.SECONDS); 100 | } 101 | } 102 | 103 | @PreDestroy 104 | public void stop() throws Exception { 105 | if (_webSocketConnection != null) { 106 | _webSocketConnection.cancel(false); 107 | } 108 | if (_webSocketClient != null) { 109 | _webSocketClient = null; 110 | } 111 | if (_webSocketFactory != null) { 112 | _webSocketFactory.stop(); 113 | _webSocketFactory = null; 114 | } 115 | if (_executor != null) { 116 | _executor.shutdownNow(); 117 | } 118 | } 119 | 120 | public List getAllVehicles() { 121 | return new ArrayList(_vehiclesById.values()); 122 | } 123 | 124 | public void addListener(VehicleListener listener) { 125 | _listeners.add(listener); 126 | } 127 | 128 | public void removeListener(VehicleListener listener) { 129 | _listeners.remove(listener); 130 | } 131 | 132 | private void refresh() throws IOException { 133 | 134 | _log.info("refreshing vehicle positions"); 135 | 136 | URL url = _vehiclePositionsUri.toURL(); 137 | FeedMessage feed = FeedMessage.parseFrom(url.openStream()); 138 | 139 | boolean hadUpdate = processDataset(feed); 140 | 141 | if (hadUpdate) { 142 | if (_dynamicRefreshInterval) { 143 | updateRefreshInterval(); 144 | } 145 | } 146 | 147 | _executor.schedule(_refreshTask, _refreshInterval, TimeUnit.SECONDS); 148 | } 149 | 150 | private boolean processDataset(FeedMessage feed) { 151 | 152 | List vehicles = new ArrayList(); 153 | boolean update = false; 154 | 155 | for (FeedEntity entity : feed.getEntityList()) { 156 | if (entity.hasIsDeleted() && entity.getIsDeleted()) { 157 | String vehicleId = _vehicleIdsByEntityIds.get(entity.getId()); 158 | if (vehicleId == null) { 159 | _log.warn("unknown entity id in deletion request: " + entity.getId()); 160 | continue; 161 | } 162 | _vehiclesById.remove(vehicleId); 163 | continue; 164 | } 165 | if (!entity.hasVehicle()) { 166 | continue; 167 | } 168 | VehiclePosition vehicle = entity.getVehicle(); 169 | String vehicleId = getVehicleId(vehicle); 170 | if (vehicleId == null) { 171 | continue; 172 | } 173 | _vehicleIdsByEntityIds.put(entity.getId(), vehicleId); 174 | if (!vehicle.hasPosition()) { 175 | continue; 176 | } 177 | Position position = vehicle.getPosition(); 178 | Vehicle v = new Vehicle(); 179 | v.setId(vehicleId); 180 | v.setLat(position.getLatitude()); 181 | v.setLon(position.getLongitude()); 182 | v.setLastUpdate(System.currentTimeMillis()); 183 | 184 | Vehicle existing = _vehiclesById.get(vehicleId); 185 | if (existing == null || existing.getLat() != v.getLat() 186 | || existing.getLon() != v.getLon()) { 187 | _vehiclesById.put(vehicleId, v); 188 | update = true; 189 | } else { 190 | v.setLastUpdate(existing.getLastUpdate()); 191 | } 192 | 193 | vehicles.add(v); 194 | } 195 | 196 | if (update) { 197 | _log.info("vehicles updated: " + vehicles.size()); 198 | } 199 | 200 | for (VehicleListener listener : _listeners) { 201 | listener.handleVehicles(vehicles); 202 | } 203 | 204 | return update; 205 | } 206 | 207 | /** 208 | * @param vehicle 209 | * @return 210 | */ 211 | private String getVehicleId(VehiclePosition vehicle) { 212 | if (!vehicle.hasVehicle()) { 213 | return null; 214 | } 215 | VehicleDescriptor desc = vehicle.getVehicle(); 216 | if (!desc.hasId()) { 217 | return null; 218 | } 219 | return desc.getId(); 220 | } 221 | 222 | private void updateRefreshInterval() { 223 | long t = System.currentTimeMillis(); 224 | if (_mostRecentRefresh != -1) { 225 | int refreshInterval = (int) ((t - _mostRecentRefresh) / (2 * 1000)); 226 | _refreshInterval = Math.max(10, refreshInterval); 227 | _log.info("refresh interval: " + _refreshInterval); 228 | } 229 | _mostRecentRefresh = t; 230 | } 231 | 232 | private class RefreshTask implements Runnable { 233 | @Override 234 | public void run() { 235 | try { 236 | refresh(); 237 | } catch (Exception ex) { 238 | _log.error("error refreshing GTFS-realtime data", ex); 239 | } 240 | } 241 | } 242 | 243 | private class IncrementalWebSocket implements OnBinaryMessage { 244 | 245 | @Override 246 | public void onOpen(Connection connection) { 247 | 248 | } 249 | 250 | @Override 251 | public void onMessage(byte[] buf, int offset, int length) { 252 | if (offset != 0 || buf.length != length) { 253 | byte trimmed[] = new byte[length]; 254 | System.arraycopy(buf, offset, trimmed, 0, length); 255 | buf = trimmed; 256 | } 257 | FeedMessage message = parseMessage(buf); 258 | FeedHeader header = message.getHeader(); 259 | switch (header.getIncrementality()) { 260 | case FULL_DATASET: 261 | processDataset(message); 262 | break; 263 | case DIFFERENTIAL: 264 | processDataset(message); 265 | break; 266 | default: 267 | _log.warn("unknown incrementality: " + header.getIncrementality()); 268 | } 269 | } 270 | 271 | @Override 272 | public void onClose(int closeCode, String message) { 273 | 274 | } 275 | 276 | private FeedMessage parseMessage(byte[] buf) { 277 | try { 278 | return FeedMessage.parseFrom(buf); 279 | } catch (InvalidProtocolBufferException ex) { 280 | throw new IllegalStateException(ex); 281 | } 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger = INFO, stderr 2 | 3 | log4j.appender.stderr = org.apache.log4j.ConsoleAppender 4 | # Follow must be set to true, so that when stderr is closed and reopened in daemonization, we'll continue to log 5 | log4j.appender.stderr.Follow = TRUE 6 | log4j.appender.stderr.Threshold = DEBUG 7 | log4j.appender.stderr.Target = System.err 8 | log4j.appender.stderr.layout = org.apache.log4j.PatternLayout 9 | log4j.appender.stderr.layout.ConversionPattern = %d{ISO8601} %-5p [%F:%L] : %m%n -------------------------------------------------------------------------------- /src/main/resources/org/onebusaway/gtfs_realtime/visualizer/WhiteCircle8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneBusAway/onebusaway-gtfs-realtime-visualizer/85da7a331f18ac34065feca471367724c3de19cd/src/main/resources/org/onebusaway/gtfs_realtime/visualizer/WhiteCircle8.png -------------------------------------------------------------------------------- /src/main/resources/org/onebusaway/gtfs_realtime/visualizer/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Brian Ferris 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | html { height: 100% } 17 | body { height: 100%; margin: 0; padding: 0 } 18 | #map_canvas { height: 100% } 19 | -------------------------------------------------------------------------------- /src/main/resources/org/onebusaway/gtfs_realtime/visualizer/index.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | GTFS-realtime Visualizer 22 | 23 | 26 | 29 | 30 | 31 | 32 | 33 |
If you can still read this, something went wrong with the Javascript.
34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/org/onebusaway/gtfs_realtime/visualizer/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 Brian Ferris 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function Init() { 18 | 19 | var hostandport = window.location.hostname + ':' + window.location.port; 20 | 21 | /** 22 | * Create a custom-styled Google Map with no labels and custom color scheme. 23 | */ 24 | var CreateMap = function() { 25 | var map_style = [ { 26 | elementType : "labels", 27 | stylers : [ { 28 | visibility : "off" 29 | } ] 30 | }, { 31 | stylers : [ { 32 | saturation : -69 33 | } ] 34 | } ]; 35 | var map_canvas = document.getElementById("map_canvas"); 36 | var myOptions = { 37 | center : new google.maps.LatLng(42.349, -71.059), 38 | zoom : 8, 39 | styles : map_style, 40 | mapTypeId : google.maps.MapTypeId.ROADMAP 41 | }; 42 | return new google.maps.Map(map_canvas, myOptions); 43 | }; 44 | 45 | var map = CreateMap(); 46 | 47 | /** 48 | * We want to assign a random color to each bus in our visualization. We 49 | * pick from the HSV color-space since it gives more natural colors. 50 | */ 51 | var HsvToRgb = function(h, s, v) { 52 | h_int = parseInt(h * 6); 53 | f = h * 6 - h_int; 54 | var a = v * (1 - s); 55 | var b = v * (1 - f * s); 56 | var c = v * (1 - (1 - f) * s); 57 | switch (h_int) { 58 | case 0: 59 | return [ v, c, a ]; 60 | case 1: 61 | return [ b, v, a ]; 62 | case 2: 63 | return [ a, v, c ]; 64 | case 3: 65 | return [ a, b, v ]; 66 | case 4: 67 | return [ c, a, v ]; 68 | case 5: 69 | return [ v, a, b ]; 70 | } 71 | }; 72 | 73 | var HsvToRgbString = function(h, s, v) { 74 | var rgb = HsvToRgb(h, s, v); 75 | for ( var i = 0; i < rgb.length; ++i) { 76 | rgb[i] = parseInt(rgb[i] * 256) 77 | } 78 | return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; 79 | }; 80 | 81 | var h = Math.random(); 82 | var golden_ratio_conjugate = 0.618033988749895; 83 | 84 | var NextRandomColor = function() { 85 | h = (h + golden_ratio_conjugate) % 1; 86 | return HsvToRgbString(h, 0.90, 0.90) 87 | }; 88 | 89 | var icon = new google.maps.MarkerImage( 90 | 'http://' + hostandport + '/WhiteCircle8.png', null, null, 91 | new google.maps.Point(4, 4)); 92 | 93 | var CreateVehicle = function(v_data) { 94 | var point = new google.maps.LatLng(v_data.lat, v_data.lon); 95 | var path = new google.maps.MVCArray(); 96 | path.push(point); 97 | var marker_opts = { 98 | clickable : true, 99 | draggable : false, 100 | flat : false, 101 | icon : icon, 102 | map : map, 103 | position : point, 104 | title : 'id=' + v_data.id 105 | }; 106 | var polyline_opts = { 107 | clickable : false, 108 | editable : false, 109 | map : map, 110 | path : path, 111 | strokeColor : NextRandomColor(), 112 | strokeOpacity : 0.8, 113 | strokeWeight : 4 114 | }; 115 | return { 116 | id : v_data.id, 117 | marker : new google.maps.Marker(marker_opts), 118 | polyline : new google.maps.Polyline(polyline_opts), 119 | path : path, 120 | lastUpdate : v_data.lastUpdate 121 | }; 122 | }; 123 | 124 | function CreateVehicleUpdateOperation(vehicle, lat, lon) { 125 | return function() { 126 | var point = new google.maps.LatLng(lat, lon); 127 | vehicle.marker.setPosition(point); 128 | var path = vehicle.path; 129 | var index = path.getLength() - 1; 130 | path.setAt(index, point); 131 | }; 132 | }; 133 | 134 | var vehicles_by_id = {}; 135 | var animation_steps = 20; 136 | 137 | function UpdateVehicle(v_data, updates) { 138 | var id = v_data.id; 139 | if (!(id in vehicles_by_id)) { 140 | vehicles_by_id[id] = CreateVehicle(v_data); 141 | } 142 | var vehicle = vehicles_by_id[id]; 143 | if (vehicle.lastUpdate >= v_data.lastUpdate) { 144 | return; 145 | } 146 | vehicle.lastUpdate = v_data.lastUpdate 147 | 148 | var path = vehicle.path; 149 | var last = path.getAt(path.getLength() - 1); 150 | path.push(last); 151 | 152 | var lat_delta = (v_data.lat - last.lat()) / animation_steps; 153 | var lon_delta = (v_data.lon - last.lng()) / animation_steps; 154 | 155 | if (lat_delta != 0 && lon_delta != 0) { 156 | for ( var i = 0; i < animation_steps; ++i) { 157 | var lat = last.lat() + lat_delta * (i + 1); 158 | var lon = last.lng() + lon_delta * (i + 1); 159 | var op = CreateVehicleUpdateOperation(vehicle, lat, lon); 160 | updates[i].push(op); 161 | } 162 | } 163 | }; 164 | 165 | var first_update = true; 166 | 167 | var ProcessVehicleData = function(data) { 168 | var vehicles = jQuery.parseJSON(data); 169 | var updates = []; 170 | var bounds = new google.maps.LatLngBounds(); 171 | for ( var i = 0; i < animation_steps; ++i) { 172 | updates.push(new Array()); 173 | } 174 | jQuery.each(vehicles, function() { 175 | UpdateVehicle(this, updates); 176 | bounds.extend(new google.maps.LatLng(this.lat, this.lon)); 177 | }); 178 | if (first_update && ! bounds.isEmpty()) { 179 | map.fitBounds(bounds); 180 | first_update = false; 181 | } 182 | var applyUpdates = function() { 183 | if (updates.length == 0) { 184 | return; 185 | } 186 | var fs = updates.shift(); 187 | for ( var i = 0; i < fs.length; i++) { 188 | fs[i](); 189 | } 190 | setTimeout(applyUpdates, 1); 191 | }; 192 | setTimeout(applyUpdates, 1); 193 | }; 194 | 195 | /** 196 | * We create a WebSocket to listen for vehicle position updates from our 197 | * webserver. 198 | */ 199 | if ("WebSocket" in window) { 200 | var ws = new WebSocket("ws://" + hostandport + "/data.json"); 201 | ws.onopen = function() { 202 | console.log("WebSockets connection opened"); 203 | } 204 | ws.onmessage = function(e) { 205 | console.log("Got WebSockets message"); 206 | ProcessVehicleData(e.data); 207 | } 208 | ws.onclose = function() { 209 | console.log("WebSockets connection closed"); 210 | } 211 | } else { 212 | alert("No WebSockets support"); 213 | } 214 | } -------------------------------------------------------------------------------- /src/main/resources/org/onebusaway/gtfs_realtime/visualizer/usage.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Visualize GTFS-realtime vehicle position data. 3 | 4 | Usage: 5 | java -jar demo.jar --vehiclePositionsUrl=url 6 | 7 | Args: 8 | --vehiclePositionsUrl=url GTFS-realtime vehicle positions url 9 | --------------------------------------------------------------------------------