├── .github
└── workflows
│ └── release.yaml
├── .gitignore
├── .tool-versions
├── .vscode
└── launch.json
├── LICENSE.txt
├── Makefile
├── README.md
├── app
├── components
│ ├── location.jsx
│ └── modal.jsx
├── main.css
└── main.jsx
├── data
├── README.md
├── csv2json.jq
├── make_routes.sh
└── routes.json
├── fly.toml
├── go.mod
├── go.sum
├── img
├── arrow.png
├── arrow.svg
├── icon_arrow_offset.png
├── icon_bus_fill.png
├── icon_bus_fill_black.png
├── icon_bus_fill_circle.png
├── icon_mock_ferry.png
├── icon_streetcar_fill.png
├── icon_streetcar_fill_black.png
├── icon_streetcar_fill_circle.png
└── icon_vehicle_error.png
├── main.go
├── main_test.go
├── mock_bustime_server
├── go.mod
├── main.go
├── mock_vehicles.json
└── mock_vehicles_jp.protobuf
├── package-lock.json
├── package.json
└── public
├── app.css
├── app.js
└── index.html
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | deploy:
8 | name: Deploy app
9 | runs-on: ubuntu-latest
10 | concurrency: deploy-group # optional: ensure only one action runs at a time
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: superfly/flyctl-actions/setup-flyctl@master
14 | - run: flyctl deploy --remote-only
15 | env:
16 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # Go workspace file
18 | go.work
19 |
20 | node_modules
21 |
22 | # Binaries
23 | nola-transit-map
24 | server
25 | __debug_*
26 |
27 | .vscode/*
28 | # Track vscode debugger config
29 | !.vscode/launch.json
30 |
31 | .DS_Store
32 |
33 | #generated temp route files
34 | data/route_*
35 | data/*.txt
36 | data/*.zip
37 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 19.0.1
2 | golang 1.19.3
3 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 |
5 | {
6 | "name": "Dev Mode",
7 | "type": "go",
8 | "request": "launch",
9 | "mode": "auto",
10 | "program": "./",
11 | "env": {
12 | "DEV": "1",
13 | "CLEVER_DEVICES_KEY": "DEV",
14 | "CLEVER_DEVICES_IP": "DEV"
15 | }
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Benjamin Eckel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | npm install
3 | npm run build
4 | go build
5 |
6 | run:
7 | ./nola-transit-map
8 |
9 | show:
10 | open http://localhost:8080
11 |
12 | ### DEV ###
13 |
14 | mock:
15 | cd mock_bustime_server && go build && ./server
16 |
17 | dev:
18 | npm install
19 | go build
20 | sh -c 'DEV=1 CLEVER_DEVICES_KEY=1 CLEVER_DEVICES_IP=1 ./nola-transit-map' || echo "Couldn't run the binary."
21 |
22 | watch:
23 | npm run watch
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## NOLA Transit Map
2 |
3 | Realtime map of all New Orleans public transit vehicles (streetcars and busses). You can view the map here for the time being: [https://nolatransit.fly.dev/](https://nolatransit.fly.dev/)
4 |
5 | For some reason, going from the old RTA app to the new Le Pass app has resulted in the loss of realtime functionality. Not having realtime makes
6 | getting around the city extremely frustrating. This map is just a stop gap to get the data out to people again.
7 |
8 | ### Needs to be Done
9 |
10 | * somehow communicate the staleness of the data to the user (we have a status indicator for the connection but could use the vehicle timestamps)
11 | * nice icons and popups for the vehicles
12 | * show the user's location on the map
13 |
14 | ### Contributing
15 |
16 | Join #civic-hacking in the Nola Devs Slack channel, where this project is discussed: https://nola.slack.com/join/shared_invite/zt-4882ja82-iGm2yO6KCxsi2aGJ9vnsUQ.
17 |
18 | The API key is a protected secret. Only a few have access, hence the included mock server that is used in DEV mode.
19 |
20 | You need a few things on your machine to build the project. If you are an `asdf` user there is a .tool-versions file with acceptable versions of node, npm, and go, but not make to keep from conflicting with system build tools.
21 |
22 | * node and npm
23 | * go
24 | * make
25 |
26 | ### To Run in Development:
27 |
28 | 1. Run the mock bustime server in a terminal. The mock server serves fake vehicle data. Vehicles will appear stationary.
29 | ```
30 | # terminal tab 1 - Mock bustime server
31 | make mock
32 | ```
33 |
34 | 2. Run the main server _in another terminal_.
35 | ```
36 | # terminal tab 2 - Go backend
37 | make dev
38 | ```
39 |
40 | 3. (Optional) If working on the frontend, you probably want changes to trigger a JS build automatically. You'll still have to refresh the page to see changes.
41 | _In a 3rd terminal_:
42 | ```
43 | # terminal tab 3 - React frontend
44 | make watch
45 | ```
46 |
47 | 4. Open the frontend [http://localhost:8080](http://localhost:8080)
48 |
49 | You may need to refresh the page after the browser window is automatically opened by the `make` command.
50 |
51 | ### To Run in Production:
52 |
53 | Add the API and IP env vars to `make run`:
54 | ```
55 | make build && make run CLEVER_DEVICES_KEY=thekey CLEVER_DEVICES_IP=ipaddr
56 | ```
57 |
58 | ### To Refresh Route Data:
59 | When transit agencies modify their routes, you may need to refresh the route geometry data from the agency's [GTFS](https://en.wikipedia.org/wiki/GTFS) feed.
60 | Run the make_routes.sh script to download this data and convert it into the geojson format used by the frontend.
61 | ```
62 | cd data
63 | ./make_routes.sh
64 | ```
65 |
--------------------------------------------------------------------------------
/app/components/location.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import L from 'leaflet';
3 | import { useMap } from 'react-leaflet';
4 | import { CircleMarker, Popup } from 'react-leaflet';
5 |
6 | export default function LocationMarker() {
7 | const [position, setPosition] = useState(null);
8 | const [bbox, setBbox] = useState([]);
9 |
10 | const map = useMap();
11 |
12 | useEffect(() => {
13 | map.locate().on("locationfound", function (e) {
14 | setPosition(e.latlng);
15 | //map.flyTo(e.latlng, map.getZoom());
16 | const radius = e.accuracy;
17 | const circle = L.circle(e.latlng, radius);
18 | circle.addTo(map);
19 | setBbox(e.bounds.toBBoxString().split(","));
20 | });
21 | }, [map]);
22 |
23 | return position === null ? null : (
24 |
25 |
26 | You are here.
27 |
28 |
29 | );
30 | }
--------------------------------------------------------------------------------
/app/components/modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Button from 'react-bootstrap/Button';
3 |
4 |
5 | function CustomModal(props) {
6 | const [show, setShow] = useState(false);
7 |
8 | const handleClose = () => setShow(false);
9 | const handleShow = () => setShow(true);
10 |
11 | return(
12 |
13 |
{props.buttonText}
14 | {show ? (
15 |
16 |
17 |
18 |
{props.title}
19 |
{props.subtitle}
20 |
21 |
22 | {props.content}
23 |
24 |
25 |
26 | Close
27 |
28 |
29 |
30 |
31 | ) : (
32 | null
33 | )}
34 |
35 | )
36 | }
37 |
38 | export default CustomModal;
--------------------------------------------------------------------------------
/app/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | html,
7 | body,
8 | #main,
9 | main,
10 | .App,
11 | .leaflet-container {
12 | height: 100%;
13 | width: 100vw;
14 | }
15 |
16 | :root {
17 | --primary: #7A278D;
18 | --primary_darker: #610C75;
19 | --secondary_lighter: #02205C91;
20 | --secondary: #02205c;
21 | --secondary_darker: #01153c;
22 | --secondary_darkest: #00040b;
23 | --black: #111;
24 | --black_lighter: #00000094;
25 | --success: #72B01D;
26 | --error: #EA3546;
27 | --alert: #FFC100;
28 | }
29 |
30 | .control-bar {
31 | position: absolute;
32 | min-width: 35vw;
33 | border-radius: 0 0 10px 0px;
34 |
35 | display: flex;
36 | justify-content: space-between;
37 | align-items: center;
38 |
39 | background: var(--primary);
40 | color: white;
41 | z-index: 1000;
42 |
43 | filter: drop-shadow(0.2rem 0.2rem 0.5rem #00000077)
44 | }
45 |
46 |
47 | .control-bar svg {
48 | margin-right: 0.5rem;
49 | }
50 |
51 | /* .control-bar__filter-label { */
52 | /* align-items: center; */
53 | /* display: flex; */
54 | /* padding: 0.75rem 1rem; */
55 | /* } */
56 |
57 | .control-bar__connection-container {
58 | border-left: solid 1px var(--primary_darker);
59 | padding: 0.75rem 1rem;
60 | font-weight: 600;
61 | }
62 |
63 | .control-bar__connection-container.connected svg {
64 | color: var(--success);
65 | }
66 |
67 | .control-bar__connection-container.not-connected svg {
68 | color: var(--error);
69 | }
70 |
71 | .control-bar__connection-container.trouble-connecting svg {
72 | color: var(--alert);
73 | }
74 |
75 | .route-filter {
76 | /* margin-left: 1rem; */
77 | color: black;
78 |
79 | min-width: 300px;
80 |
81 | margin: 0.5rem;
82 | }
83 |
84 | .about-button {
85 | bottom: 0;
86 | color: white;
87 | position: absolute;
88 | left: 0;
89 | z-index: 1000;
90 | background: var(--secondary);
91 | border-radius: 0 10px 0 0;
92 | }
93 |
94 | .about-button:hover {
95 | background: var(--secondary_darker);
96 | }
97 |
98 | .about-button:active {
99 | transform: scale(99%);
100 | }
101 |
102 | .about-button svg {
103 | margin-right: 0.5rem;
104 | }
105 |
106 | button {
107 | display: flex;
108 | align-items: center;
109 | border: none;
110 | padding: 0.5rem 1rem;
111 | margin: 0;
112 | text-decoration: none;
113 | background: transparent;
114 | font-family: sans-serif;
115 | font-size: 1rem;
116 | cursor: pointer;
117 | text-align: center;
118 | transition: background 250ms ease-in-out, transform 150ms ease;
119 | }
120 |
121 | .Modal {
122 | position: fixed;
123 | z-index: 2000;
124 | inset: 0;
125 | background: var(--black_lighter);
126 | }
127 |
128 | .Modal__content {
129 | background: var(--secondary_darkest);
130 | color: white;
131 | min-height: 20%;
132 | padding: 2rem;
133 | border-radius: 10px;
134 | max-width: 60%;
135 | box-shadow: 2px 2px 8px #0009;
136 | margin-right: auto;
137 | margin-left: auto;
138 | margin-top: 10%;
139 | }
140 |
141 | .Modal__content--header {
142 | text-align: center;
143 | }
144 |
145 | .Modal__content--footer {
146 | margin-top: 2rem;
147 | }
148 |
149 | @media (min-width: 1400px) {
150 | .control-bar {
151 | min-width: 30vw;
152 | }
153 | }
154 |
155 | @media (max-width: 1100px) {
156 | .control-bar {
157 | min-width: 45vw;
158 | }
159 |
160 | .Modal__content {
161 | max-width: 75%;
162 | }
163 | }
164 |
165 | @media (max-width: 500px) {
166 | body {
167 | font-size: 0.9rem;
168 | }
169 |
170 | /* .control-bar__filter-label { */
171 | /* width: 100%; */
172 | /* padding: 0.5rem 0; */
173 | /* } */
174 |
175 | .route-filter {
176 | width: 100%;
177 | }
178 |
179 | .route-select-option__wrapper .route-and-icon span {
180 | font-size: 1.5rem;
181 | }
182 |
183 | .control-bar__label-text {
184 | display: none;
185 | }
186 |
187 | .control-bar {
188 | min-width: 100vw;
189 | font-size: 0.9rem;
190 | }
191 |
192 | .Modal__content {
193 | max-width: 90%;
194 | margin-top: 15%;
195 | font-size: 0.9rem;
196 | }
197 |
198 | .about-button {
199 | font-size: 0.7rem;
200 | padding: 0.5rem 0.75rem;
201 | }
202 | }
203 |
204 | .route-select-option__wrapper {
205 | display: grid;
206 | grid-template-columns: min-content auto;
207 | align-items: center;
208 | gap: 1rem;
209 | }
210 |
211 | .route-select-option__wrapper .route-and-icon {
212 | display: grid;
213 | grid-template-columns: 1fr 1fr;
214 | align-items: center;
215 | gap: 0.5rem;
216 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
217 | font-size: 1.8rem;
218 | }
219 |
220 | .route-select-option__wrapper img {
221 | width: 20px;
222 | height: 20px;
223 | }
--------------------------------------------------------------------------------
/app/main.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, forwardRef } from 'react'
2 | import { createRoot } from 'react-dom/client';
3 | import { MapContainer, TileLayer, Marker, Popup, GeoJSON } from 'react-leaflet'
4 | import { BsInfoLg, BsFillCircleFill, BsFillCloudSlashFill, BsFillExclamationTriangleFill } from 'react-icons/bs'
5 | import L from 'leaflet';
6 | import "leaflet-rotatedmarker";
7 | import 'bootstrap/dist/css/bootstrap.min.css';
8 | import NortaGeoJson from '../data/routes.json';
9 | import Row from 'react-bootstrap/Row';
10 | import Col from 'react-bootstrap/Col';
11 | import Select, { components as SelectComponents } from 'react-select'
12 | import makeAnimated from 'react-select/animated';
13 | import CustomModal from './components/modal';
14 | import LocationMarker from './components/location';
15 | import './main.css';
16 |
17 | import busIconMap from '../img/icon_bus_fill_circle.png'
18 | import busIconSelect from '../img/icon_bus_fill_black.png'
19 | import streetcarIconMap from '../img/icon_streetcar_fill_circle.png'
20 | import streetcarIconSelect from '../img/icon_streetcar_fill_black.png'
21 | // TODO: awaiting real ferry icon
22 | import ferryIcon from '../img/icon_mock_ferry.png'
23 | import errorIcon from '../img/icon_vehicle_error.png'
24 | import arrowIcon from '../img/icon_arrow_offset.png'
25 |
26 | import basicArrow from '../img/arrow.png'
27 |
28 | const VALID_ROUTES = NortaGeoJson
29 | .features
30 | .filter(f => f.geometry.type === "GeometryCollection" && f.properties.route_id)
31 |
32 | const ROUTE_ELEMS = VALID_ROUTES
33 | .reduce((acc, f) => {
34 | return {
35 | ...acc,
36 | [f.properties.route_id]:
37 | }
38 | }, {})
39 |
40 | // Used in creation of options for dropdown & map markers
41 | const ROUTE_INFO = VALID_ROUTES
42 | .reduce((acc, f) => {
43 | const { route_long_name, route_type, route_color: color } = f.properties
44 | let name = route_long_name ?? ''
45 | let type = 'error'
46 | if (route_type == 3) {
47 | type = 'bus'
48 | if (!name.toLowerCase().endsWith('bus')) name += ' Bus'
49 | }
50 | if (route_type == 0) {
51 | type = 'streetcar'
52 | if (!name.toLowerCase().endsWith('streetcar')) name += ' Streetcar'
53 | }
54 | if (route_type == 4) {
55 | type = 'ferry'
56 | if (!name.toLowerCase().endsWith('ferry')) name += ' Ferry'
57 | }
58 | return {
59 | ...acc,
60 | [f.properties.route_id]: { name, type, color }
61 | }
62 | }, {})
63 |
64 | const iconVehicle = new L.Icon({
65 | iconUrl: basicArrow,
66 | iconRetinaUrl: basicArrow,
67 | iconAnchor: null,
68 | shadowUrl: null,
69 | shadowSize: null,
70 | shadowAnchor: null,
71 | iconSize: new L.Point(24, 24),
72 | className: 'leaflet-marker-icon'
73 | });
74 |
75 | const RotatedMarker = forwardRef(({ children, ...props }, forwardRef) => {
76 | const markerRef = useRef();
77 |
78 | const { rotationAngle, rotationOrigin } = props;
79 | useEffect(() => {
80 | const marker = markerRef.current;
81 | if (marker) {
82 | marker.setRotationAngle(rotationAngle);
83 | marker.setRotationOrigin(rotationOrigin);
84 | }
85 | }, [rotationAngle, rotationOrigin]);
86 |
87 | return (
88 | {
90 | markerRef.current = ref;
91 | if (forwardRef) {
92 | forwardRef.current = ref;
93 | }
94 | }}
95 | {...props}
96 | >
97 | {children}
98 |
99 | );
100 | });
101 |
102 | /*
103 | These routes don't exist at NORTA.com
104 | When a vehicle enters its garage, its route becomes 'U'
105 | The definition of PO and PI routes is unknown -> filter out for now
106 | Note: The U route designation applies to both vehicles in the garage
107 | (not in service) and a selection of 24-hour routes that are not in
108 | the garage and running their normal route between 12:30am-ish and
109 | 4:00am-sh.
110 | */
111 | const NOT_IN_SERVICE_ROUTES = ['PO', 'PI']
112 |
113 | const MARKER_ICON_SIZE = 24 // ? pt or px
114 |
115 | const DROPDOWN_ICON_IMG = Object.freeze({
116 | ferry: ferryIcon,
117 | streetcar: streetcarIconSelect,
118 | bus: busIconSelect,
119 | error: errorIcon,
120 | })
121 |
122 | const ICON_ARROW = new L.Icon({
123 | iconUrl: arrowIcon,
124 | iconRetinaUrl: arrowIcon,
125 | // Tall so arrow doesn't intersect vehicle (&& match aspect ratio of graphic)
126 | iconSize: [MARKER_ICON_SIZE, MARKER_ICON_SIZE * 2],
127 | className: 'leaflet-marker-icon'
128 | });
129 |
130 | function ArrowMarker(props) {
131 | const { rotationAngle } = props;
132 | const markerRef = useRef();
133 |
134 | useEffect(() => {
135 | markerRef.current?.setRotationAngle(rotationAngle);
136 | }, [rotationAngle]);
137 | return ;
138 | }
139 |
140 | function newVehicleMapIcon(image) {
141 | return new L.Icon({
142 | iconUrl: image,
143 | iconRetinaUrl: image,
144 | iconSize: [MARKER_ICON_SIZE, MARKER_ICON_SIZE],
145 | className: 'leaflet-marker-icon'
146 | });
147 | }
148 |
149 | const VEHICLE_MARKER_ICONS = Object.freeze({
150 | ferry: newVehicleMapIcon(ferryIcon),
151 | streetcar: newVehicleMapIcon(streetcarIconMap),
152 | bus: newVehicleMapIcon(busIconMap),
153 | error: newVehicleMapIcon(errorIcon),
154 | })
155 |
156 | function VehicleMarker({ children, ...props }) {
157 | const { type } = props
158 | return (
159 |
160 | {children}
161 |
162 | )
163 | }
164 |
165 | // React Select animations
166 | const selectAnimatedComponents = makeAnimated()
167 |
168 | const { Option: SelectOption } = SelectComponents
169 |
170 | // custom React Select option - add icons and route colors to route options
171 | function RouteSelectOption(props) {
172 | const { data: { value, name, icon, color } } = props
173 | return (
174 |
175 |
176 |
177 |
{value}
178 |
179 |
180 |
{name}
181 |
182 |
183 | )
184 | }
185 |
186 | function timestampDisplay(timestamp) {
187 | const relativeTimestamp = new Date() - new Date(timestamp);
188 | if (relativeTimestamp < 60000) { return 'less than a minute ago'; }
189 | const minutes = Math.round(relativeTimestamp / 60000);
190 | if (minutes === 1) { return '1 minute ago'}
191 | return minutes + ' minutes ago';
192 | }
193 |
194 |
195 | class App extends React.Component {
196 | constructor(props) {
197 | super(props)
198 | const routes = localStorage.getItem("routes") || "[]"
199 | this.state = {
200 | vehicles: [],
201 | routes: JSON.parse(routes),
202 | connected: false,
203 | lastUpdate: new Date(),
204 | now: new Date(),
205 | sse: null,
206 | }
207 | this.handleRouteChange = this.handleRouteChange.bind(this)
208 | }
209 |
210 | componentWillMount() {
211 | this.connectSSE();
212 | this.interval = setInterval(() => this.setState({ now: Date.now() }), 1000);
213 | }
214 |
215 | componentWillUnmount() {
216 | this.closeSSE();
217 | clearInterval(this.interval)
218 | }
219 |
220 | connectSSE = () => {
221 | if (!this.state.sse || (this.state.sse && this.state.sse.readyState == EventSource.CLOSED)) {
222 | this.setState({ connected: false });
223 |
224 | const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/sse`
225 | const sse = new EventSource(url);
226 |
227 | sse.onmessage = (evt) => {
228 | console.log('SSE message');
229 | if (!this.state.connected) this.setState({ connected: true })
230 | const vehicles = JSON.parse(evt.data)
231 | const lastUpdate = new Date()
232 | this.setState({
233 | vehicles,
234 | lastUpdate,
235 | })
236 | console.dir(vehicles)
237 | };
238 |
239 | sse.onclose = () => {
240 | console.log('SSE closed');
241 | this.setState({ connected: false });
242 | };
243 |
244 | sse.onerror = (error) => {
245 | console.error('SSE error:', error);
246 | this.setState({ connected: false });
247 | };
248 |
249 | this.setState({ sse });
250 | }
251 |
252 | //check connection for reconnect every 5s
253 | setTimeout(this.connectSSE, 5000);
254 | };
255 |
256 | closeSSE = () => {
257 | if (this.state.sse) {
258 | this.state.sse.close();
259 | }
260 | };
261 |
262 | routeComponents() {
263 | if (this.state.routes.length === 0) return Object.values(ROUTE_ELEMS)
264 |
265 | return this.state.routes
266 | .map(r => r.value)
267 | .map(rid => ROUTE_ELEMS[rid])
268 | .filter(r => r !== null)
269 | }
270 |
271 | markerComponents() {
272 | let query = (_v) => true
273 | if (this.state.routes.length > 0) {
274 | const values = this.state.routes.map(r => r.value)
275 | query = (v) => values.includes(v.rt)
276 | console.log("query filter on routes: " + values)
277 | }
278 |
279 | return this.state.vehicles
280 | .filter(query)
281 | .map(v => {
282 | const coords = [v.lat, v.lon].map(parseFloat)
283 | const rotAng = parseInt(v.hdg, 10)
284 | const relTime = timestampDisplay(v.tmstmp)
285 | /*
286 | const type = ROUTE_INFO[v.rt]?.type ?? 'error'
287 | return (
288 |
289 |
290 |
291 |
292 | {v.rt}{v.des ? ' - ' + v.des.replace('>>', 'to') : ''}
293 |
294 | {relTime}
295 |
296 |
297 |
298 | )
299 | */
300 | return
301 |
302 | {v.rt}{v.des ? ' - ' + v.des : ''}
303 | {relTime}
304 |
305 |
306 | })
307 | }
308 |
309 | mapContainer() {
310 | return
311 |
315 | {this.markerComponents()}
316 | {this.routeComponents()}
317 |
318 |
319 | }
320 |
321 | notConnectedScreen() {
322 | return
323 |
324 | Connection broken. Attempting to reconnect...
325 |
326 |
327 | }
328 |
329 | buildControlBar() {
330 | let connectionStatus = this.state.connected
331 | ?
332 | Connected
333 |
334 | :
335 | Not Connected
336 |
337 |
338 | if (this.state.connected && this.lagging()) connectionStatus =
339 |
340 | Trouble Connecting...
341 |
342 |
343 | if (!this.state.connected) return this.notConnectedScreen()
344 |
345 | if (this.state.vehicles.length === 0) {
346 | return
347 |
348 | No Vehicles found yet. Are you connected?
349 |
350 |
351 | }
352 |
353 | function compare(a, b) {
354 | return ('' + a.label).localeCompare(b.label, 'en', { numeric: true });
355 | }
356 | const routes = [...new Set(this.state.vehicles.map(v => v.rt))]
357 | const routeOptions = routes.map(rt => {
358 | // route name is route id if routes.json is unaware of the route
359 | let { name, type, color } = ROUTE_INFO[rt]
360 | ?? { name: rt, type: 'error', color: '#000000'}
361 | if (color == "#ffffff") color = "#000000"
362 | if (rt == "PI" || rt == "PO" || rt == "U") name = "Unknown"
363 | const icon = DROPDOWN_ICON_IMG[type] ?? DROPDOWN_ICON_IMG['bus']
364 |
365 | return { value: rt, label: rt, name, icon, color }
366 | }).sort(compare)
367 |
368 | return
369 | {/* */}
370 |
381 | {/* */}
382 | {connectionStatus}
383 |
384 | }
385 |
386 | handleRouteChange(routes) {
387 | this.setState({ routes })
388 | localStorage.setItem("routes", JSON.stringify(routes))
389 | }
390 |
391 | lagging() {
392 | // lagging by over 13 seconds
393 | return Math.floor((this.state.now - this.state.lastUpdate) / 1000) > 13
394 | }
395 |
396 | render() {
397 | return
398 |
399 | {this.buildControlBar()}
400 | {this.mapContainer()}
401 | Code For New Orleans]}
404 | buttonText={[ , 'About this project']}
405 | content={
406 | [
407 | 'When the RTA switched to the new LePass app, all of the realtime data stopped working. Relying on public transportation in New Orleans without this data is extremely challenging. We made this map as a stop gap until realtime starts working again.',
408 |
409 | , , 'If you find a problem, or have a feature request, consider ', filing an issue here. ,
410 | ' You can also join us on slack in the #civic-hacking channel of the ', Nola Devs slack. ,
411 | ' Take a look at ', the README on GitHub , ' to learn more about how it works.'
412 | ]
413 | }
414 | />
415 |
416 |
417 | }
418 | }
419 |
420 | window.initApp = function () {
421 | console.log("initApp()")
422 |
423 | const root = createRoot(
424 | document.getElementById('main')
425 | )
426 |
427 | root.render( )
428 | }
429 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 | # routes
2 |
3 | Derived from GTFS trip data.
4 |
5 | We used the "dissolve" process in QGIS to combine all trip data per route id. This still leaves a lot of unnecessary segments at bus stops.
6 |
7 | One way to improve the geometry of the routes is replace the feature geometries with centerlines from this dataset in data.nola.gov ([Centerline](https://data.nola.gov/Transportation-and-Infrastructure/Centerline/hp2r-gr3h)). I did this in QGIS for the General Meyer Local route (103) just to try it out, and these are the steps I used:
8 |
9 | 1. Select each centerline segment that lies along a single route (segments are split at all intersections)
10 | 2. Export these selected segments to a new layer
11 | 3. Touch up vertices as needed and then dissolve the layer to a single feature
12 | 4. Copy this dissolved feature into the main `routes.geojson` file
13 | 5. Copy over attributes from original route feature
14 | 6. Delete the original feature for that route.
15 | 7. Leave an comment in the edit_comment field
16 |
17 | Leaving these notes here for future reference, as the features can be slowly fixed up over time.
--------------------------------------------------------------------------------
/data/csv2json.jq:
--------------------------------------------------------------------------------
1 | #CREDIT: csv2json.jq from https://stackoverflow.com/questions/29663187/csv-to-json-using-jq
2 |
3 | def objectify(headers):
4 | def tonumberq: tonumber? // .;
5 | . as $in
6 | | reduce range(0; headers|length) as $i ({}; .[headers[$i]] = ($in[$i]) );
7 |
8 | def csv2table:
9 | # For jq 1.4, replace the following line by: def trim: .;
10 | def trim: sub("^ +";"") | sub(" +$";"");
11 | split("\n") | map( split(",") | map(trim) );
12 |
13 | def csv2json:
14 | csv2table
15 | | .[0] as $headers
16 | | reduce (.[1:][] | select(length > 0) ) as $row
17 | ( []; . + [ $row|objectify($headers) ]);
18 |
--------------------------------------------------------------------------------
/data/make_routes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #init list of routes
4 | cat > routes.json << EOF
5 | {
6 | "type": "FeatureCollection",
7 | "name": "routes",
8 | "crs": {
9 | "type": "name",
10 | "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" }
11 | },
12 | "features": []
13 | }
14 | EOF
15 |
16 | add_features_from_GTFS () {
17 | url=$1
18 |
19 | #get GTFS data
20 | wget -O GTFS.zip "$url"
21 | unzip -o GTFS.zip
22 |
23 | routes=$(tail -n +2 routes.txt | cut -d "," -f 1)
24 |
25 | #routes.txt lists as '53-O', but shapes.txt has it as just 53. fix it here
26 | routes=${routes/53-O/53}
27 |
28 | for route in $routes; do
29 | echo "ROUTE $route"
30 |
31 | #get geometry/stops for every direction
32 | dirs=$(grep "\-${route}-" shapes.txt | cut -d ',' -f 1 | sort | uniq | cut -d '-' -f 3)
33 | dir_index=0
34 | for dir in $dirs; do
35 | echo " DIR $dir_index: $dir"
36 |
37 | #convert shapes.txt lat/lons into geojson LineString
38 | (head -1 shapes.txt; grep "shp-$route-$dir" shapes.txt) |
39 | jq -c -R -s 'include "csv2json"; csv2json | {type: "LineString", coordinates: [.[] | [.shape_pt_lon, .shape_pt_lat]]}' \
40 | > route_${route}_dir${dir_index}_lines
41 |
42 | #convert stops.txt lat/lons into geojson MutliPoint
43 | #NOTE: since stops.txt doesnt have route info, correlate by matching route lat/lon with stops lat/lon
44 | (echo "stop_lat,stop_lon"; cat stops.txt | cut -d "," -f 5-6 | grep -f <(grep "shp-$route-$dir" shapes.txt | cut -d "," -f 2-3)) |
45 | jq -c -R -s 'include "csv2json"; csv2json | {type: "MultiPoint", coordinates: [.[] | [.stop_lon, .stop_lat]]}' \
46 | > route_${route}_dir${dir_index}_stops
47 |
48 | dir_index=$(($dir_index+1))
49 | done
50 |
51 | #convert routes.txt entry to json route header
52 | (head -1 routes.txt; grep -e "^${route}," routes.txt ) |
53 | jq -c -R -s -f <(cat << EOF
54 | include "csv2json"; csv2json | .[0] |
55 | {
56 | type: "Feature",
57 | properties: {
58 | route_id: .route_short_name,
59 | agency_id: .agency_id,
60 | route_short_name: .route_short_name,
61 | route_long_name: .route_long_name,
62 | route_type: .route_type,
63 | route_color: "\("#")\(.route_color)",
64 | route_text_color: "\("#")\(.route_text_color)",
65 | },
66 | geometry: { type: "GeometryCollection", geometries: [] }
67 | }
68 | EOF
69 | ) > route_${route}_header
70 |
71 | #combine header/stops/lines into single geometrycollection for route
72 | filter="'.geometry.geometries += "
73 | spacer=""
74 | slurps=""
75 | dir_index=0
76 | for dir in $dirs; do
77 | filter="${filter}${spacer}\$stops${dir_index} + \$lines${dir_index}"
78 | slurps="$slurps --slurpfile lines${dir_index} route_${route}_dir${dir_index}_lines"
79 | slurps="$slurps --slurpfile stops${dir_index} route_${route}_dir${dir_index}_stops"
80 |
81 | dir_index=$(($dir_index+1))
82 | spacer=" + "
83 | done
84 | filter="$filter'"
85 | cmd="jq $filter route_${route}_header "$slurps" > route_${route}_feature"
86 | bash -c "$cmd"
87 |
88 | #add to list of routes
89 | jq -c '.features += $feature' routes.json --slurpfile feature route_${route}_feature > routes.json.tmp
90 | mv routes.json.tmp routes.json
91 |
92 | done
93 | }
94 |
95 | echo "=== RTA ==="
96 | add_features_from_GTFS https://www.norta.com/RTA/media/GTFS/GTFS.zip
97 |
98 | echo "=== JP TRANSIT ==="
99 | add_features_from_GTFS https://rideneworleans.org/wp/wp-content/uploads/GTFS-JET-20240913.zip
100 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for nolatransit on 2022-10-29T20:52:29-05:00
2 |
3 | app = "nolatransit"
4 | kill_signal = "SIGINT"
5 | kill_timeout = 5
6 | processes = []
7 |
8 | [build]
9 | builder = "paketobuildpacks/builder:base"
10 | buildpacks = ["gcr.io/paketo-buildpacks/go"]
11 |
12 | [build.args]
13 | BP_KEEP_FILES = "public/*"
14 |
15 | [env]
16 | PORT = "8080"
17 |
18 | [experimental]
19 | allowed_public_ports = []
20 | auto_rollback = true
21 |
22 | [[services]]
23 | http_checks = []
24 | internal_port = 8080
25 | processes = ["app"]
26 | protocol = "tcp"
27 | script_checks = []
28 | [services.concurrency]
29 | hard_limit = 25
30 | soft_limit = 20
31 | type = "connections"
32 |
33 | [[services.ports]]
34 | force_https = true
35 | handlers = ["http"]
36 | port = 80
37 |
38 | [[services.ports]]
39 | handlers = ["tls", "http"]
40 | port = 443
41 |
42 | [[services.tcp_checks]]
43 | grace_period = "1s"
44 | interval = "15s"
45 | restart_limit = 0
46 | timeout = "2s"
47 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/codefornola/nola-transit-map
2 |
3 | go 1.21
4 |
5 | toolchain go1.22.6
6 |
7 | require (
8 | github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs v1.0.0
9 | github.com/gorilla/websocket v1.5.0
10 | github.com/stretchr/testify v1.8.1
11 | google.golang.org/protobuf v1.35.1
12 | )
13 |
14 | require (
15 | github.com/davecgh/go-spew v1.1.1 // indirect
16 | github.com/pmezard/go-difflib v1.0.0 // indirect
17 | gopkg.in/yaml.v3 v3.0.1 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs v1.0.0 h1:f4P+fVYmSIWj4b/jvbMdmrmsx/Xb+5xCpYYtVXOdKoc=
2 | github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs v1.0.0/go.mod h1:nSmbVVQSM4lp9gYvVaaTotnRxSwZXEdFnJARofg5V4g=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
7 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
9 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
14 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
17 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
18 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
19 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
21 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
22 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 |
--------------------------------------------------------------------------------
/img/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/arrow.png
--------------------------------------------------------------------------------
/img/arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
10 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/img/icon_arrow_offset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_arrow_offset.png
--------------------------------------------------------------------------------
/img/icon_bus_fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_bus_fill.png
--------------------------------------------------------------------------------
/img/icon_bus_fill_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_bus_fill_black.png
--------------------------------------------------------------------------------
/img/icon_bus_fill_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_bus_fill_circle.png
--------------------------------------------------------------------------------
/img/icon_mock_ferry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_mock_ferry.png
--------------------------------------------------------------------------------
/img/icon_streetcar_fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_streetcar_fill.png
--------------------------------------------------------------------------------
/img/icon_streetcar_fill_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_streetcar_fill_black.png
--------------------------------------------------------------------------------
/img/icon_streetcar_fill_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_streetcar_fill_circle.png
--------------------------------------------------------------------------------
/img/icon_vehicle_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/img/icon_vehicle_error.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "os"
11 | "time"
12 | "strconv"
13 |
14 | "github.com/gorilla/websocket"
15 | "google.golang.org/protobuf/proto"
16 | "github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs"
17 | )
18 |
19 | const (
20 | // Time allowed to write the file to the client.
21 | writeWait = 10 * time.Second
22 |
23 | // Time allowed to read the next pong message from the client.
24 | pongWait = 60 * time.Second
25 |
26 | // Send pings to client with this period. Must be less than pongWait.
27 | pingPeriod = (pongWait * 9) / 10
28 |
29 | // Time between fetches to Clever Devices Bustime server.
30 | scraperFetchInterval = 10 * time.Second
31 |
32 | // Use in place of Clever Devices URL when in DEV mode.
33 | mockCleverDevicesUrl = "http://localhost:8081/getvehicles"
34 |
35 | // Clever Devices API URL: http://[host:port]/bustime/api/v3/getvehicles
36 | // http://ride.smtd.org/bustime/apidoc/docs/DeveloperAPIGuide3_0.pdf
37 | cleverDevicesUrlFormatter = "https://%s/bustime/api/v3/getvehicles"
38 |
39 | // Append to Clever Devices base url (above).
40 | // tmres=m -> time resolution: minute.
41 | // rtpidatafeed=bustime -> specify the Bustime data feed.
42 | // format=json -> respond with json (as opposed to XML).
43 | cleverDevicesVehicleQueryFormatter = "%s?key=%s&tmres=m&rtpidatafeed=bustime&format=json"
44 |
45 | // JP Transit GTFS URL
46 | // returns GTFS data as a protobuf (unless debug http param is set, in that case json)
47 | jpTransitUrl = "https://jetapp.jptransit.org/gtfsrt/vehicles"
48 |
49 | // Use in place of JP Transit URL when in DEV mode.
50 | mockJpTransitUrl = "http://localhost:8081/vehicles_jp"
51 | )
52 |
53 | var (
54 | addr = flag.String("addr", ":8080", "http service address")
55 | upgrader = websocket.Upgrader{
56 | ReadBufferSize: 1024,
57 | WriteBufferSize: 1024,
58 | }
59 |
60 | DEV = false
61 | )
62 |
63 | type VehicleTimestamp struct {
64 | time.Time
65 | }
66 |
67 | // UnmarshalJSON
68 | // We need a special unmarshal method for this string timestamp. It's of the
69 | // form "YYYYMMDD hh:mm"
70 | func (t *VehicleTimestamp) UnmarshalJSON(data []byte) error {
71 | var s string
72 | if err := json.Unmarshal(data, &s); err != nil {
73 | return err
74 | }
75 |
76 | loc, err := time.LoadLocation("America/Chicago")
77 | if err != nil {
78 | return err
79 | }
80 |
81 | // "YYYYMMDD hh:mm" https://pkg.go.dev/time#pkg-constants
82 | format := "20060102 15:04"
83 | time, err := time.ParseInLocation(format, s, loc)
84 | if err != nil {
85 | return err
86 | }
87 |
88 | t.Time = time
89 | return nil
90 | }
91 |
92 | // Vehicle represents an individual reading of a vehicle and it's location
93 | // at that point in time
94 | // Example:
95 | //
96 | // {
97 | // "vid": "155",
98 | // "tmstmp": "20200827 11:51",
99 | // "lat": "29.962149326173048",
100 | // "lon": "-90.05214051918121",
101 | // "hdg": "357",
102 | // "pid": 275,
103 | // "rt": "5",
104 | // "des": "Saratoga at Canal",
105 | // "pdist": 10122,
106 | // "dly": false,
107 | // "spd": 20,
108 | // "tatripid": "3130339",
109 | // "tablockid": "15",
110 | // "zone": "",
111 | // "srvtmstmp": "20200827 11:51",
112 | // "oid": "445",
113 | // "or": true,
114 | // "rid": "501",
115 | // "blk": 2102,
116 | // "tripid": 982856020
117 | // }
118 | type Vehicle struct {
119 | Vid string `json:"vid"`
120 | Tmstmp VehicleTimestamp `json:"tmstmp"`
121 | SrvTimstmp VehicleTimestamp `json:"srvtmstmp"`
122 | Lat float64 `json:"lat,string"`
123 | Lon float64 `json:"lon,string"`
124 | Hdg string `json:"hdg"`
125 | Rt string `json:"rt"`
126 | Tatripid string `json:"tatripid"`
127 | Tablockid string `json:"tablockid"`
128 | Zone string `json:"zone"`
129 | Oid string `json:"oid"`
130 | Rid string `json:"rid"`
131 | Des string `json:"des"`
132 | Pdist int `json:"pdist"`
133 | Pid int `json:"pid"`
134 | Spd int `json:"spd"`
135 | Blk int `json:"blk"`
136 | Tripid int `json:"tripid"`
137 | Dly bool `json:"dly"`
138 | Or bool `json:"or"`
139 | }
140 |
141 | type BusErr struct {
142 | Rt string `json:"rt"`
143 | Msg string `json:"msg"`
144 | }
145 |
146 | type BustimeData struct {
147 | Vehicles []Vehicle `json:"vehicle"`
148 | Errors []BusErr `json:"error"`
149 | }
150 |
151 | type BustimeResponse struct {
152 | Data BustimeData `json:"bustime-response"`
153 | }
154 |
155 | type Config struct {
156 | Key string `yaml:"key"`
157 | Interval time.Duration `yaml:"interval"`
158 | BaseUrlRTA string `yaml:"url"`
159 | BaseUrlJP string `yaml:"url"`
160 | }
161 |
162 | type Scraper struct {
163 | client *http.Client
164 | client_jp *http.Client
165 | config *Config
166 | }
167 |
168 | func NewScraper() *Scraper {
169 | api_key, ok := os.LookupEnv("CLEVER_DEVICES_KEY")
170 | if !ok {
171 | panic("Need to set environment variable CLEVER_DEVICES_KEY. Try `make run CLEVER_DEVICES_KEY=thekey`. Get key from Ben on slack")
172 | }
173 | ip, ok := os.LookupEnv("CLEVER_DEVICES_IP")
174 | if !ok {
175 | panic("Need to set environment variable CLEVER_DEVICES_KEY. Try `make run CLEVER_DEVICES_KEY=thekey`. Get key from Ben on slack")
176 | }
177 |
178 | baseUrlRTA := fmt.Sprintf(cleverDevicesUrlFormatter, ip)
179 | baseUrlJP := jpTransitUrl
180 | if DEV {
181 | baseUrlRTA = mockCleverDevicesUrl
182 | baseUrlJP = mockJpTransitUrl
183 | }
184 | config := &Config{
185 | BaseUrlRTA: baseUrlRTA,
186 | BaseUrlJP: baseUrlJP,
187 | Interval: scraperFetchInterval,
188 | Key: api_key,
189 | }
190 |
191 | tr := &http.Transport{
192 | MaxIdleConnsPerHost: 1024,
193 | TLSHandshakeTimeout: 0 * time.Second,
194 | }
195 | client := &http.Client{Transport: tr}
196 | client_jp := &http.Client{Transport: tr}
197 | return &Scraper{
198 | client,
199 | client_jp,
200 | config,
201 | }
202 | }
203 |
204 | func (s *Scraper) Start(vs chan []Vehicle) {
205 | for {
206 | result := s.fetch()
207 | log.Printf("Found %d RTA vehicles\n", len(result.Vehicles))
208 |
209 | result_jp := s.fetch_jp()
210 | log.Printf("Found %d JP vehicles\n", len(result_jp.Vehicles))
211 |
212 | vs <- append(result.Vehicles, result_jp.Vehicles...)
213 |
214 | time.Sleep(s.config.Interval)
215 | }
216 | }
217 |
218 | func (v *Scraper) fetch() (*BustimeData) {
219 | key := v.config.Key
220 | baseURL := v.config.BaseUrlRTA
221 | url := fmt.Sprintf(cleverDevicesVehicleQueryFormatter, baseURL, key)
222 | log.Println("Scraper URL:", url)
223 | resp, err := v.client.Get(url)
224 | if err != nil {
225 | log.Println("ERROR: Scraper response:", err)
226 | return &BustimeData{}
227 | }
228 | if resp.Body != nil {
229 | defer resp.Body.Close()
230 | }
231 | body, readErr := ioutil.ReadAll(resp.Body)
232 | if readErr != nil {
233 | log.Println("ERROR: Scraper response reader:", readErr)
234 | return &BustimeData{}
235 | }
236 |
237 | result := &BustimeResponse{}
238 | json.Unmarshal(body, result)
239 |
240 | return &result.Data
241 | }
242 |
243 | func (v *Scraper) fetch_jp() (*BustimeData) {
244 | url := v.config.BaseUrlJP
245 | log.Println("Scraper URL:", url)
246 | resp, err := v.client_jp.Get(url)
247 | if err != nil {
248 | log.Printf("ERROR: while fetching jptransit data: %s [firewall blocking VPN address?]", err)
249 | return &BustimeData{}
250 | }
251 | if resp.Body != nil {
252 | defer resp.Body.Close()
253 | }
254 | body, err := ioutil.ReadAll(resp.Body)
255 | if err != nil {
256 | log.Println("ERROR: Scraper response reader:", err)
257 | return &BustimeData{}
258 | }
259 |
260 | feed := gtfs.FeedMessage{}
261 | err = proto.Unmarshal(body, &feed)
262 | if err != nil {
263 | log.Println("ERROR: Scraper response parsing:", err)
264 | return &BustimeData{}
265 | }
266 |
267 | result := &BustimeData{}
268 | result.Vehicles = []Vehicle{}
269 |
270 | for _, entity := range feed.Entity {
271 | vehicle := entity.GetVehicle()
272 | position := vehicle.GetPosition()
273 | route := vehicle.GetTrip().GetRouteId()
274 | if (len(route) == 0) {
275 | route = "U"
276 | }
277 |
278 | v := Vehicle{}
279 | v.Vid = vehicle.GetVehicle().GetId()
280 | v.Tmstmp = VehicleTimestamp{}
281 | v.Tmstmp.Time = time.Unix(int64(vehicle.GetTimestamp()), 0)
282 | v.Lat = float64(position.GetLatitude())
283 | v.Lon = float64(position.GetLongitude())
284 | v.Hdg = strconv.FormatFloat(float64(position.GetBearing()), 'f', -1, 64)
285 | v.Rt = route
286 | result.Vehicles = append(result.Vehicles, v)
287 | }
288 |
289 | return result
290 | }
291 |
292 | func (v *Scraper) Close() {
293 | v.client.CloseIdleConnections()
294 | v.client_jp.CloseIdleConnections()
295 | }
296 |
297 | type VehicleChannel = chan []Vehicle
298 |
299 | type VehicleBroadcaster struct {
300 | incoming VehicleChannel
301 | vehicles []Vehicle
302 | receivers map[VehicleChannel]bool
303 | }
304 |
305 | func NewVehicleBroadCaster() *VehicleBroadcaster {
306 | return &VehicleBroadcaster{
307 | incoming: make(VehicleChannel),
308 | receivers: make(map[VehicleChannel]bool),
309 | }
310 | }
311 |
312 | func (b *VehicleBroadcaster) Register(c VehicleChannel) {
313 | b.receivers[c] = true
314 | }
315 |
316 | func (b *VehicleBroadcaster) Unregister(c VehicleChannel) {
317 | delete(b.receivers, c)
318 | }
319 |
320 | func (b *VehicleBroadcaster) Start() {
321 | scraper := NewScraper()
322 | defer scraper.Close()
323 | log.Println("Starting scraper")
324 | go scraper.Start(b.incoming)
325 | b.broadcast()
326 | }
327 |
328 | func (b *VehicleBroadcaster) broadcast() {
329 | for vs := range b.incoming {
330 | log.Println("Caching Vehicles")
331 | b.vehicles = vs
332 | log.Printf("%d listeners \n", len(b.receivers))
333 | for r, _ := range b.receivers {
334 | log.Println("Broadcasting Vehicles")
335 | select {
336 | case r <- vs:
337 | default:
338 | log.Println("Closing")
339 | close(r)
340 | b.Unregister(r)
341 | }
342 | }
343 | log.Println("Done Broadcasting")
344 | }
345 | }
346 |
347 | type Server struct {
348 | broadcaster *VehicleBroadcaster
349 | }
350 |
351 | func NewServer() *Server {
352 | return &Server{
353 | broadcaster: NewVehicleBroadCaster(),
354 | }
355 | }
356 |
357 | func (s *Server) Start() {
358 | go s.broadcaster.Start()
359 |
360 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
361 | http.ServeFile(w, r, "./public/index.html")
362 | })
363 |
364 | fs := http.FileServer(http.Dir("./public"))
365 | http.Handle("/public/", http.StripPrefix("/public/", fs))
366 |
367 | // Handle websocket connection
368 | http.HandleFunc("/sse", s.serveSSE)
369 |
370 | log.Println("Starting server")
371 | if err := http.ListenAndServe(*addr, nil); err != nil {
372 | log.Fatal(err)
373 | }
374 | }
375 |
376 | func (s *Server) writeVehicles(w http.ResponseWriter, vehicles []Vehicle) error {
377 | if len(vehicles) > 0 {
378 | payload, err := json.Marshal(vehicles)
379 | if err != nil {
380 | return err
381 | }
382 | fmt.Fprintf(w, "data: %s\n\n", payload)
383 | } else {
384 | log.Println("No Vehicles to write")
385 | }
386 | return nil
387 | }
388 |
389 | func (s *Server) serveSSE(w http.ResponseWriter, r *http.Request) {
390 | w.Header().Set("Content-Type", "text/event-stream")
391 | w.Header().Set("Cache-Control", "no-cache")
392 | w.Header().Set("Connection", "keep-alive")
393 |
394 | vehicleChan := make(VehicleChannel)
395 | s.broadcaster.Register(vehicleChan)
396 | log.Println("Sending cached vehicles")
397 | s.writeVehicles(w, s.broadcaster.vehicles)
398 |
399 | for vehicles := range vehicleChan {
400 | err := s.writeVehicles(w, vehicles)
401 | if err != nil {
402 | break
403 | }
404 | }
405 | log.Println("SSE connection closed")
406 | }
407 |
408 | func main() {
409 | if _, exists := os.LookupEnv("DEV"); exists {
410 | DEV = true
411 | log.Println("Set to DEV mode.")
412 | }
413 |
414 | server := NewServer()
415 | server.Start()
416 | }
417 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestUnmarshalVehicleTimestamp(t *testing.T) {
13 | testCases := []struct {
14 | vehicleJSON string
15 | wantTimeStr string
16 | }{
17 | {vehicleJSON: `{"tmstmp": "20200827 11:51"}`, wantTimeStr: "27 Aug 20 11:51 CDT"},
18 | {vehicleJSON: `{"tmstmp": "20230221 11:51"}`, wantTimeStr: "21 Feb 23 11:51 CST"},
19 | }
20 |
21 | for _, tc := range testCases {
22 | d := json.NewDecoder(strings.NewReader(tc.vehicleJSON))
23 | var gotVehicle Vehicle
24 | err := d.Decode(&gotVehicle)
25 | require.NoError(t, err)
26 |
27 | require.Equal(t, tc.wantTimeStr, gotVehicle.Tmstmp.Format(time.RFC822))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/mock_bustime_server/go.mod:
--------------------------------------------------------------------------------
1 | module server
2 |
3 | go 1.23.3
4 |
--------------------------------------------------------------------------------
/mock_bustime_server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | )
7 |
8 | const port = ":8081"
9 |
10 | func main() {
11 | http.HandleFunc(
12 | "/getvehicles", // ignores query params
13 | func(w http.ResponseWriter, r *http.Request) {
14 | http.ServeFile(w, r, "./mock_vehicles.json")
15 | log.Println("GET /getvehicles :: Served Vehicles")
16 | },
17 | )
18 | http.HandleFunc(
19 | "/vehicles_jp",
20 | func(w http.ResponseWriter, r *http.Request) {
21 | http.ServeFile(w, r, "./mock_vehicles_jp.protobuf")
22 | log.Println("GET /vehicles_jp :: Served Vehicles")
23 | },
24 | )
25 | log.Printf("\n\nMock Clever Devices Bustime server is running at: http://localhost%s \n", port)
26 | log.Fatal(http.ListenAndServe(port, nil))
27 | }
28 |
--------------------------------------------------------------------------------
/mock_bustime_server/mock_vehicles.json:
--------------------------------------------------------------------------------
1 | {
2 | "bustime-response": {
3 | "vehicle": [
4 | {
5 | "vid": "2409",
6 | "tmstmp": "20241126 20:55",
7 | "lat": "29.96765833333333",
8 | "lon": "-90.08832333333334",
9 | "hdg": "110",
10 | "pid": 677,
11 | "rt": "PI",
12 | "des": "",
13 | "pdist": 43618,
14 | "dly": false,
15 | "spd": 0,
16 | "tatripid": "3668461",
17 | "origtatripno": "3668461",
18 | "tablockid": "19",
19 | "zone": "",
20 | "mode": 1,
21 | "psgld": "N/A",
22 | "srvtmstmp": "20241126 20:55",
23 | "oid": "1829",
24 | "or": false,
25 | "rid": "913",
26 | "lwid1": "N/A",
27 | "lwid2": "000913",
28 | "blk": 3102,
29 | "tripid": 7417024,
30 | "tripdyn": 0,
31 | "stst": 70920,
32 | "stsd": "2024-11-26"
33 | },
34 | {
35 | "vid": "2414",
36 | "tmstmp": "20241126 20:55",
37 | "lat": "29.968194999999998",
38 | "lon": "-90.08832",
39 | "hdg": "227",
40 | "pid": 677,
41 | "rt": "PI",
42 | "des": "",
43 | "pdist": 43675,
44 | "dly": false,
45 | "spd": 10,
46 | "tatripid": "3667507",
47 | "origtatripno": "3667507",
48 | "tablockid": "262",
49 | "zone": "",
50 | "mode": 1,
51 | "psgld": "N/A",
52 | "srvtmstmp": "20241126 20:55",
53 | "oid": "1798",
54 | "or": false,
55 | "rid": "6205",
56 | "lwid1": "N/A",
57 | "lwid2": "006205",
58 | "blk": 5502,
59 | "tripid": 7366024,
60 | "tripdyn": 0,
61 | "stst": 73320,
62 | "stsd": "2024-11-26"
63 | },
64 | {
65 | "vid": "338",
66 | "tmstmp": "20241126 20:55",
67 | "lat": "29.958843333333334",
68 | "lon": "-90.084045",
69 | "hdg": "298",
70 | "pid": 686,
71 | "rt": "PI",
72 | "des": "",
73 | "pdist": 3120,
74 | "dly": false,
75 | "spd": 28,
76 | "tatripid": "3651193",
77 | "origtatripno": "3651193",
78 | "tablockid": "461",
79 | "zone": "",
80 | "mode": 1,
81 | "psgld": "N/A",
82 | "srvtmstmp": "20241126 20:55",
83 | "oid": "1504",
84 | "or": false,
85 | "rid": "6107",
86 | "lwid1": "N/A",
87 | "lwid2": "006107",
88 | "blk": 9402,
89 | "tripid": 7320024,
90 | "tripdyn": 0,
91 | "stst": 75240,
92 | "stsd": "2024-11-26"
93 | },
94 | {
95 | "vid": "347",
96 | "tmstmp": "20241126 20:55",
97 | "lat": "29.967608333333335",
98 | "lon": "-90.08825",
99 | "hdg": "32",
100 | "pid": 686,
101 | "rt": "PI",
102 | "des": "",
103 | "pdist": 6144,
104 | "dly": true,
105 | "spd": 0,
106 | "tatripid": "3668925",
107 | "origtatripno": "3668925",
108 | "tablockid": "33",
109 | "zone": "",
110 | "mode": 1,
111 | "psgld": "N/A",
112 | "srvtmstmp": "20241126 20:55",
113 | "oid": "1918",
114 | "or": false,
115 | "rid": "310",
116 | "lwid1": "N/A",
117 | "lwid2": "000310",
118 | "blk": 6802,
119 | "tripid": 7272024,
120 | "tripdyn": 0,
121 | "stst": 74700,
122 | "stsd": "2024-11-26"
123 | },
124 | {
125 | "vid": "2401",
126 | "tmstmp": "20241126 20:55",
127 | "lat": "29.954868198102766",
128 | "lon": "-90.0753552843829",
129 | "hdg": "117",
130 | "pid": 691,
131 | "rt": "PI",
132 | "des": "",
133 | "pdist": 81,
134 | "dly": false,
135 | "spd": 0,
136 | "tatripid": "3667074",
137 | "origtatripno": "3667074",
138 | "tablockid": "2103",
139 | "zone": "",
140 | "mode": 1,
141 | "psgld": "N/A",
142 | "srvtmstmp": "20241126 20:55",
143 | "oid": "1711",
144 | "or": false,
145 | "rid": "10306",
146 | "lwid1": "N/A",
147 | "lwid2": "010306",
148 | "blk": 3802,
149 | "tripid": 7301024,
150 | "tripdyn": 0,
151 | "stst": 74880,
152 | "stsd": "2024-11-26"
153 | },
154 | {
155 | "vid": "288",
156 | "tmstmp": "20241126 20:55",
157 | "lat": "29.967736666666664",
158 | "lon": "-90.08841499999998",
159 | "hdg": "129",
160 | "pid": 697,
161 | "rt": "PI",
162 | "des": "Not In Service",
163 | "pdist": 15833,
164 | "dly": true,
165 | "spd": 0,
166 | "tatripid": "3652459",
167 | "origtatripno": "3652459",
168 | "tablockid": "491",
169 | "zone": "",
170 | "mode": 1,
171 | "psgld": "N/A",
172 | "srvtmstmp": "20241126 20:55",
173 | "oid": "22",
174 | "or": false,
175 | "rid": "9105",
176 | "lwid1": "N/A",
177 | "lwid2": "009105",
178 | "blk": 9802,
179 | "tripid": 7267024,
180 | "tripdyn": 0,
181 | "stst": 72120,
182 | "stsd": "2024-11-26"
183 | },
184 | {
185 | "vid": "350",
186 | "tmstmp": "20241126 20:53",
187 | "lat": "29.977351666666664",
188 | "lon": "-90.03861666666666",
189 | "hdg": "284",
190 | "pid": 682,
191 | "rt": "PO",
192 | "des": "",
193 | "pdist": 6233,
194 | "dly": true,
195 | "spd": 20,
196 | "tatripid": "3652203",
197 | "origtatripno": "3652203",
198 | "tablockid": "384",
199 | "zone": "",
200 | "mode": 1,
201 | "psgld": "N/A",
202 | "srvtmstmp": "20241126 20:53",
203 | "oid": "1522",
204 | "or": false,
205 | "rid": "8405",
206 | "lwid1": "N/A",
207 | "lwid2": "008405",
208 | "blk": 7802,
209 | "tripid": 7147023,
210 | "tripdyn": 0,
211 | "stst": 73200,
212 | "stsd": "2024-11-26"
213 | },
214 | {
215 | "vid": "81713",
216 | "tmstmp": "20241126 20:55",
217 | "lat": "29.95286666666667",
218 | "lon": "-90.05624166666666",
219 | "hdg": "102",
220 | "pid": -1,
221 | "rt": "U",
222 | "des": "",
223 | "pdist": 0,
224 | "dly": false,
225 | "spd": 0,
226 | "tatripid": "N/A",
227 | "origtatripno": "",
228 | "tablockid": "N/A",
229 | "zone": "",
230 | "mode": 1,
231 | "psgld": "N/A",
232 | "srvtmstmp": "20241126 20:55",
233 | "oid": "N/A",
234 | "or": true,
235 | "rid": "N/A",
236 | "lwid1": "N/A",
237 | "lwid2": "N/A",
238 | "tripdyn": 0
239 | },
240 | {
241 | "vid": "306",
242 | "tmstmp": "20241126 20:55",
243 | "lat": "29.967660000000002",
244 | "lon": "-90.08857000000002",
245 | "hdg": "212",
246 | "pid": -1,
247 | "rt": "U",
248 | "des": "",
249 | "pdist": 0,
250 | "dly": false,
251 | "spd": 0,
252 | "tatripid": "N/A",
253 | "origtatripno": "",
254 | "tablockid": "N/A",
255 | "zone": "",
256 | "mode": 1,
257 | "psgld": "N/A",
258 | "srvtmstmp": "20241126 20:55",
259 | "oid": "N/A",
260 | "or": true,
261 | "rid": "N/A",
262 | "lwid1": "N/A",
263 | "lwid2": "N/A",
264 | "tripdyn": 0
265 | },
266 | {
267 | "vid": "948",
268 | "tmstmp": "20241126 20:55",
269 | "lat": "29.95009166666667",
270 | "lon": "-90.12898333333332",
271 | "hdg": "339",
272 | "pid": -1,
273 | "rt": "U",
274 | "des": "",
275 | "pdist": 0,
276 | "dly": false,
277 | "spd": 0,
278 | "tatripid": "N/A",
279 | "origtatripno": "",
280 | "tablockid": "N/A",
281 | "zone": "",
282 | "mode": 1,
283 | "psgld": "N/A",
284 | "srvtmstmp": "20241126 20:55",
285 | "oid": "N/A",
286 | "or": true,
287 | "rid": "N/A",
288 | "lwid1": "N/A",
289 | "lwid2": "N/A",
290 | "tripdyn": 0
291 | },
292 | {
293 | "vid": "951",
294 | "tmstmp": "20241126 20:55",
295 | "lat": "29.949756666666666",
296 | "lon": "-90.12951166666666",
297 | "hdg": "227",
298 | "pid": -1,
299 | "rt": "U",
300 | "des": "",
301 | "pdist": 0,
302 | "dly": false,
303 | "spd": 0,
304 | "tatripid": "N/A",
305 | "origtatripno": "",
306 | "tablockid": "N/A",
307 | "zone": "",
308 | "mode": 1,
309 | "psgld": "N/A",
310 | "srvtmstmp": "20241126 20:55",
311 | "oid": "N/A",
312 | "or": true,
313 | "rid": "N/A",
314 | "lwid1": "N/A",
315 | "lwid2": "N/A",
316 | "tripdyn": 0
317 | },
318 | {
319 | "vid": "315",
320 | "tmstmp": "20241126 20:55",
321 | "lat": "29.96755333333333",
322 | "lon": "-90.08871333333333",
323 | "hdg": "215",
324 | "pid": -1,
325 | "rt": "U",
326 | "des": "",
327 | "pdist": 0,
328 | "dly": false,
329 | "spd": 0,
330 | "tatripid": "N/A",
331 | "origtatripno": "",
332 | "tablockid": "N/A",
333 | "zone": "",
334 | "mode": 1,
335 | "psgld": "N/A",
336 | "srvtmstmp": "20241126 20:55",
337 | "oid": "N/A",
338 | "or": true,
339 | "rid": "N/A",
340 | "lwid1": "N/A",
341 | "lwid2": "N/A",
342 | "tripdyn": 0
343 | },
344 | {
345 | "vid": "317",
346 | "tmstmp": "20241126 20:55",
347 | "lat": "29.968276666666664",
348 | "lon": "-90.08911166666667",
349 | "hdg": "359",
350 | "pid": -1,
351 | "rt": "U",
352 | "des": "",
353 | "pdist": 0,
354 | "dly": false,
355 | "spd": 0,
356 | "tatripid": "N/A",
357 | "origtatripno": "",
358 | "tablockid": "N/A",
359 | "zone": "",
360 | "mode": 1,
361 | "psgld": "N/A",
362 | "srvtmstmp": "20241126 20:55",
363 | "oid": "N/A",
364 | "or": true,
365 | "rid": "N/A",
366 | "lwid1": "N/A",
367 | "lwid2": "N/A",
368 | "tripdyn": 0
369 | },
370 | {
371 | "vid": "280",
372 | "tmstmp": "20241126 20:55",
373 | "lat": "29.96795666666667",
374 | "lon": "-90.08980166666667",
375 | "hdg": "50",
376 | "pid": -1,
377 | "rt": "U",
378 | "des": "",
379 | "pdist": 0,
380 | "dly": false,
381 | "spd": 0,
382 | "tatripid": "N/A",
383 | "origtatripno": "",
384 | "tablockid": "N/A",
385 | "zone": "",
386 | "mode": 1,
387 | "psgld": "N/A",
388 | "srvtmstmp": "20241126 20:55",
389 | "oid": "N/A",
390 | "or": true,
391 | "rid": "N/A",
392 | "lwid1": "N/A",
393 | "lwid2": "N/A",
394 | "tripdyn": 0
395 | },
396 | {
397 | "vid": "285",
398 | "tmstmp": "20241126 20:55",
399 | "lat": "29.967476666666666",
400 | "lon": "-90.08837833333334",
401 | "hdg": "58",
402 | "pid": -1,
403 | "rt": "U",
404 | "des": "",
405 | "pdist": 0,
406 | "dly": false,
407 | "spd": 0,
408 | "tatripid": "N/A",
409 | "origtatripno": "",
410 | "tablockid": "N/A",
411 | "zone": "",
412 | "mode": 1,
413 | "psgld": "N/A",
414 | "srvtmstmp": "20241126 20:55",
415 | "oid": "N/A",
416 | "or": true,
417 | "rid": "N/A",
418 | "lwid1": "N/A",
419 | "lwid2": "N/A",
420 | "tripdyn": 0
421 | },
422 | {
423 | "vid": "961",
424 | "tmstmp": "20241126 20:55",
425 | "lat": "29.949813333333335",
426 | "lon": "-90.12931833333332",
427 | "hdg": "287",
428 | "pid": -1,
429 | "rt": "U",
430 | "des": "",
431 | "pdist": 0,
432 | "dly": false,
433 | "spd": 0,
434 | "tatripid": "N/A",
435 | "origtatripno": "",
436 | "tablockid": "N/A",
437 | "zone": "",
438 | "mode": 1,
439 | "psgld": "N/A",
440 | "srvtmstmp": "20241126 20:55",
441 | "oid": "N/A",
442 | "or": true,
443 | "rid": "N/A",
444 | "lwid1": "N/A",
445 | "lwid2": "N/A",
446 | "tripdyn": 0
447 | },
448 | {
449 | "vid": "334",
450 | "tmstmp": "20241126 20:55",
451 | "lat": "29.96776333333333",
452 | "lon": "-90.08840333333335",
453 | "hdg": "124",
454 | "pid": -1,
455 | "rt": "U",
456 | "des": "",
457 | "pdist": 0,
458 | "dly": false,
459 | "spd": 0,
460 | "tatripid": "N/A",
461 | "origtatripno": "",
462 | "tablockid": "N/A",
463 | "zone": "",
464 | "mode": 1,
465 | "psgld": "N/A",
466 | "srvtmstmp": "20241126 20:55",
467 | "oid": "N/A",
468 | "or": true,
469 | "rid": "N/A",
470 | "lwid1": "N/A",
471 | "lwid2": "N/A",
472 | "tripdyn": 0
473 | },
474 | {
475 | "vid": "2413",
476 | "tmstmp": "20241126 20:55",
477 | "lat": "29.967816666666668",
478 | "lon": "-90.08943666666667",
479 | "hdg": "121",
480 | "pid": -1,
481 | "rt": "U",
482 | "des": "",
483 | "pdist": 0,
484 | "dly": false,
485 | "spd": 0,
486 | "tatripid": "N/A",
487 | "origtatripno": "",
488 | "tablockid": "N/A",
489 | "zone": "",
490 | "mode": 1,
491 | "psgld": "N/A",
492 | "srvtmstmp": "20241126 20:55",
493 | "oid": "N/A",
494 | "or": true,
495 | "rid": "N/A",
496 | "lwid1": "N/A",
497 | "lwid2": "N/A",
498 | "tripdyn": 0
499 | },
500 | {
501 | "vid": "2411",
502 | "tmstmp": "20241126 20:55",
503 | "lat": "29.95640443650701",
504 | "lon": "-90.07356802737672",
505 | "hdg": "217",
506 | "pid": -1,
507 | "rt": "U",
508 | "des": "",
509 | "pdist": 0,
510 | "dly": false,
511 | "spd": 0,
512 | "tatripid": "N/A",
513 | "origtatripno": "",
514 | "tablockid": "N/A",
515 | "zone": "",
516 | "mode": 1,
517 | "psgld": "N/A",
518 | "srvtmstmp": "20241126 20:55",
519 | "oid": "N/A",
520 | "or": true,
521 | "rid": "N/A",
522 | "lwid1": "N/A",
523 | "lwid2": "N/A",
524 | "tripdyn": 0
525 | },
526 | {
527 | "vid": "228",
528 | "tmstmp": "20241126 20:55",
529 | "lat": "29.968101666666666",
530 | "lon": "-90.08957333333335",
531 | "hdg": "359",
532 | "pid": -1,
533 | "rt": "U",
534 | "des": "",
535 | "pdist": 0,
536 | "dly": false,
537 | "spd": 0,
538 | "tatripid": "N/A",
539 | "origtatripno": "",
540 | "tablockid": "N/A",
541 | "zone": "",
542 | "mode": 1,
543 | "psgld": "N/A",
544 | "srvtmstmp": "20241126 20:55",
545 | "oid": "N/A",
546 | "or": true,
547 | "rid": "N/A",
548 | "lwid1": "N/A",
549 | "lwid2": "N/A",
550 | "tripdyn": 0
551 | },
552 | {
553 | "vid": "922",
554 | "tmstmp": "20241126 20:55",
555 | "lat": "29.950098333333337",
556 | "lon": "-90.12895833333332",
557 | "hdg": "187",
558 | "pid": 0,
559 | "rt": "U",
560 | "des": "",
561 | "pdist": 0,
562 | "dly": false,
563 | "spd": 0,
564 | "tatripid": "null",
565 | "origtatripno": "",
566 | "tablockid": "412",
567 | "zone": "",
568 | "mode": 1,
569 | "psgld": "N/A",
570 | "srvtmstmp": "20241126 20:55",
571 | "oid": "787",
572 | "or": true,
573 | "rid": "N/A",
574 | "lwid1": "N/A",
575 | "lwid2": "001204",
576 | "blk": 8802,
577 | "tripdyn": 0
578 | },
579 | {
580 | "vid": "333",
581 | "tmstmp": "20241126 20:55",
582 | "lat": "29.96495614534672",
583 | "lon": "-90.11105632380969",
584 | "hdg": "222",
585 | "pid": 568,
586 | "rt": "3",
587 | "des": "Jefferson Hwy. at Causeway Blvd.",
588 | "pdist": 13711,
589 | "dly": false,
590 | "spd": 16,
591 | "tatripid": "3669000",
592 | "origtatripno": "3669000",
593 | "tablockid": "73",
594 | "zone": "",
595 | "mode": 1,
596 | "psgld": "N/A",
597 | "srvtmstmp": "20241126 20:55",
598 | "oid": "1902",
599 | "or": false,
600 | "rid": "313",
601 | "lwid1": "N/A",
602 | "lwid2": "000313",
603 | "blk": 11802,
604 | "tripid": 18020,
605 | "tripdyn": 0,
606 | "stst": 74400,
607 | "stsd": "2024-11-26"
608 | },
609 | {
610 | "vid": "323",
611 | "tmstmp": "20241126 20:55",
612 | "lat": "29.96049496084649",
613 | "lon": "-90.17014471488912",
614 | "hdg": "55",
615 | "pid": 571,
616 | "rt": "3",
617 | "des": "Main Library-CBD via Ochsner",
618 | "pdist": 6157,
619 | "dly": false,
620 | "spd": 26,
621 | "tatripid": "3668997",
622 | "origtatripno": "3668997",
623 | "tablockid": "43",
624 | "zone": "",
625 | "mode": 1,
626 | "psgld": "N/A",
627 | "srvtmstmp": "20241126 20:55",
628 | "oid": "1415",
629 | "or": false,
630 | "rid": "311",
631 | "lwid1": "N/A",
632 | "lwid2": "000311",
633 | "blk": 8902,
634 | "tripid": 5086020,
635 | "tripdyn": 0,
636 | "stst": 75060,
637 | "stsd": "2024-11-26"
638 | },
639 | {
640 | "vid": "224",
641 | "tmstmp": "20241126 20:55",
642 | "lat": "29.955519255361367",
643 | "lon": "-90.12067931342773",
644 | "hdg": "41",
645 | "pid": 571,
646 | "rt": "3",
647 | "des": "Main Library-CBD via Ochsner",
648 | "pdist": 24727,
649 | "dly": false,
650 | "spd": 3,
651 | "tatripid": "3668926",
652 | "origtatripno": "3668926",
653 | "tablockid": "83",
654 | "zone": "",
655 | "mode": 1,
656 | "psgld": "N/A",
657 | "srvtmstmp": "20241126 20:55",
658 | "oid": "1967",
659 | "or": false,
660 | "rid": "314",
661 | "lwid1": "N/A",
662 | "lwid2": "000314",
663 | "blk": 12202,
664 | "tripid": 3033020,
665 | "tripdyn": 0,
666 | "stst": 73800,
667 | "stsd": "2024-11-26"
668 | },
669 | {
670 | "vid": "37449",
671 | "tmstmp": "20241126 20:55",
672 | "lat": "29.922166666666666",
673 | "lon": "-89.97329833333333",
674 | "hdg": "228",
675 | "pid": 533,
676 | "rt": "4",
677 | "des": "Algiers",
678 | "pdist": 2363,
679 | "dly": false,
680 | "spd": 0,
681 | "tatripid": "3647265",
682 | "origtatripno": "3647265",
683 | "tablockid": "14",
684 | "zone": "",
685 | "mode": 1,
686 | "psgld": "N/A",
687 | "srvtmstmp": "20241126 20:55",
688 | "oid": "71003",
689 | "or": false,
690 | "rid": "401",
691 | "lwid1": "N/A",
692 | "lwid2": "000401",
693 | "blk": 1302,
694 | "tripid": 3675020,
695 | "tripdyn": 0,
696 | "stst": 74700,
697 | "stsd": "2024-11-26"
698 | },
699 | {
700 | "vid": "290",
701 | "tmstmp": "20241126 20:55",
702 | "lat": "29.958801666666666",
703 | "lon": "-90.00768000000001",
704 | "hdg": "293",
705 | "pid": 538,
706 | "rt": "8",
707 | "des": "Main Library-CBD via Arabi",
708 | "pdist": 11,
709 | "dly": false,
710 | "spd": 0,
711 | "tatripid": "3667230",
712 | "origtatripno": "3667230",
713 | "tablockid": "58",
714 | "zone": "",
715 | "mode": 1,
716 | "psgld": "N/A",
717 | "srvtmstmp": "20241126 20:55",
718 | "oid": "1952",
719 | "or": false,
720 | "rid": "809",
721 | "lwid1": "N/A",
722 | "lwid2": "000809",
723 | "blk": 10702,
724 | "tripid": 5909020,
725 | "tripdyn": 0,
726 | "stst": 75600,
727 | "stsd": "2024-11-26"
728 | },
729 | {
730 | "vid": "293",
731 | "tmstmp": "20241126 20:55",
732 | "lat": "29.9690613614157",
733 | "lon": "-90.05011640818577",
734 | "hdg": "284",
735 | "pid": 538,
736 | "rt": "8",
737 | "des": "Main Library-CBD via Arabi",
738 | "pdist": 14070,
739 | "dly": false,
740 | "spd": 0,
741 | "tatripid": "3667229",
742 | "origtatripno": "3667229",
743 | "tablockid": "38",
744 | "zone": "",
745 | "mode": 1,
746 | "psgld": "N/A",
747 | "srvtmstmp": "20241126 20:55",
748 | "oid": "1712",
749 | "or": false,
750 | "rid": "807",
751 | "lwid1": "N/A",
752 | "lwid2": "000807",
753 | "blk": 7702,
754 | "tripid": 5657020,
755 | "tripdyn": 0,
756 | "stst": 74520,
757 | "stsd": "2024-11-26"
758 | },
759 | {
760 | "vid": "335",
761 | "tmstmp": "20241126 20:55",
762 | "lat": "29.95571557538049",
763 | "lon": "-90.07344868833226",
764 | "hdg": "37",
765 | "pid": 561,
766 | "rt": "8",
767 | "des": "Arabi",
768 | "pdist": 4800,
769 | "dly": false,
770 | "spd": 0,
771 | "tatripid": "3667299",
772 | "origtatripno": "3667299",
773 | "tablockid": "68",
774 | "zone": "",
775 | "mode": 1,
776 | "psgld": "N/A",
777 | "srvtmstmp": "20241126 20:55",
778 | "oid": "1873",
779 | "or": false,
780 | "rid": "810",
781 | "lwid1": "N/A",
782 | "lwid2": "000810",
783 | "blk": 11402,
784 | "tripid": 3400020,
785 | "tripdyn": 0,
786 | "stst": 75060,
787 | "stsd": "2024-11-26"
788 | },
789 | {
790 | "vid": "275",
791 | "tmstmp": "20241126 20:55",
792 | "lat": "29.95962186168207",
793 | "lon": "-90.01024887220316",
794 | "hdg": "111",
795 | "pid": 561,
796 | "rt": "8",
797 | "des": "Arabi",
798 | "pdist": 27476,
799 | "dly": false,
800 | "spd": 24,
801 | "tatripid": "3667298",
802 | "origtatripno": "3667298",
803 | "tablockid": "48",
804 | "zone": "",
805 | "mode": 1,
806 | "psgld": "N/A",
807 | "srvtmstmp": "20241126 20:55",
808 | "oid": "1859",
809 | "or": false,
810 | "rid": "808",
811 | "lwid1": "N/A",
812 | "lwid2": "000808",
813 | "blk": 9602,
814 | "tripid": 169020,
815 | "tripdyn": 0,
816 | "stst": 73980,
817 | "stsd": "2024-11-26"
818 | },
819 | {
820 | "vid": "341",
821 | "tmstmp": "20241126 20:55",
822 | "lat": "30.029876178782757",
823 | "lon": "-89.9749707305829",
824 | "hdg": "149",
825 | "pid": 553,
826 | "rt": "9",
827 | "des": "Tchoupitoulas St. via Ochsner Baptist",
828 | "pdist": 2130,
829 | "dly": false,
830 | "spd": 28,
831 | "tatripid": "3668387",
832 | "origtatripno": "3668387",
833 | "tablockid": "89",
834 | "zone": "",
835 | "mode": 1,
836 | "psgld": "N/A",
837 | "srvtmstmp": "20241126 20:55",
838 | "oid": "1977",
839 | "or": false,
840 | "rid": "916",
841 | "lwid1": "N/A",
842 | "lwid2": "000916",
843 | "blk": 12302,
844 | "tripid": 547020,
845 | "tripdyn": 0,
846 | "stst": 74340,
847 | "stsd": "2024-11-26"
848 | },
849 | {
850 | "vid": "340",
851 | "tmstmp": "20241126 20:55",
852 | "lat": "29.925446932914742",
853 | "lon": "-90.1020563268553",
854 | "hdg": "174",
855 | "pid": 553,
856 | "rt": "9",
857 | "des": "Tchoupitoulas St. via Ochsner Baptist",
858 | "pdist": 72212,
859 | "dly": false,
860 | "spd": 27,
861 | "tatripid": "3668386",
862 | "origtatripno": "3668386",
863 | "tablockid": "109",
864 | "zone": "",
865 | "mode": 1,
866 | "psgld": "N/A",
867 | "srvtmstmp": "20241126 20:55",
868 | "oid": "1776",
869 | "or": false,
870 | "rid": "917",
871 | "lwid1": "N/A",
872 | "lwid2": "000917",
873 | "blk": 3402,
874 | "tripid": 6153020,
875 | "tripdyn": 0,
876 | "stst": 72540,
877 | "stsd": "2024-11-26"
878 | },
879 | {
880 | "vid": "354",
881 | "tmstmp": "20241126 20:55",
882 | "lat": "0.0",
883 | "lon": "0.0",
884 | "hdg": "0",
885 | "pid": 593,
886 | "rt": "9",
887 | "des": "N.O. East Hub via Ochsner Baptist",
888 | "pdist": 3430,
889 | "dly": false,
890 | "spd": 0,
891 | "tatripid": "3668459",
892 | "origtatripno": "3668459",
893 | "tablockid": "49",
894 | "zone": "",
895 | "mode": 1,
896 | "psgld": "N/A",
897 | "srvtmstmp": "20241126 20:55",
898 | "oid": "1650",
899 | "or": true,
900 | "rid": "911",
901 | "lwid1": "N/A",
902 | "lwid2": "000911",
903 | "blk": 9702,
904 | "tripid": 1352020,
905 | "tripdyn": 0,
906 | "stst": 65160,
907 | "stsd": "2024-11-26"
908 | },
909 | {
910 | "vid": "320",
911 | "tmstmp": "20241126 20:55",
912 | "lat": "29.96352663358928",
913 | "lon": "-90.0889249708319",
914 | "hdg": "37",
915 | "pid": 593,
916 | "rt": "9",
917 | "des": "N.O. East Hub via Ochsner Baptist",
918 | "pdist": 19068,
919 | "dly": false,
920 | "spd": 23,
921 | "tatripid": "3668442",
922 | "origtatripno": "3668442",
923 | "tablockid": "59",
924 | "zone": "",
925 | "mode": 1,
926 | "psgld": "N/A",
927 | "srvtmstmp": "20241126 20:55",
928 | "oid": "1545",
929 | "or": false,
930 | "rid": "915",
931 | "lwid1": "N/A",
932 | "lwid2": "000915",
933 | "blk": 10802,
934 | "tripid": 1695020,
935 | "tripdyn": 0,
936 | "stst": 74520,
937 | "stsd": "2024-11-26"
938 | },
939 | {
940 | "vid": "269",
941 | "tmstmp": "20241126 20:55",
942 | "lat": "30.013222",
943 | "lon": "-89.993124",
944 | "hdg": "343",
945 | "pid": 593,
946 | "rt": "9",
947 | "des": "N.O. East Hub via Ochsner Baptist",
948 | "pdist": 57464,
949 | "dly": false,
950 | "spd": 34,
951 | "tatripid": "3668441",
952 | "origtatripno": "3668441",
953 | "tablockid": "99",
954 | "zone": "",
955 | "mode": 1,
956 | "psgld": "N/A",
957 | "srvtmstmp": "20241126 20:55",
958 | "oid": "1995",
959 | "or": false,
960 | "rid": "918",
961 | "lwid1": "N/A",
962 | "lwid2": "000918",
963 | "blk": 12502,
964 | "tripid": 1097020,
965 | "tripdyn": 0,
966 | "stst": 72720,
967 | "stsd": "2024-11-26"
968 | },
969 | {
970 | "vid": "282",
971 | "tmstmp": "20241126 20:55",
972 | "lat": "29.939653121555104",
973 | "lon": "-90.07216345687748",
974 | "hdg": "13",
975 | "pid": 555,
976 | "rt": "11",
977 | "des": "Children's Hospital \u003E\u003E Canal at Magazine",
978 | "pdist": 22941,
979 | "dly": false,
980 | "spd": 17,
981 | "tatripid": "3648067",
982 | "origtatripno": "3648067",
983 | "tablockid": "211",
984 | "zone": "",
985 | "mode": 1,
986 | "psgld": "N/A",
987 | "srvtmstmp": "20241126 20:55",
988 | "oid": "235",
989 | "or": false,
990 | "rid": "1104",
991 | "lwid1": "N/A",
992 | "lwid2": "001104",
993 | "blk": 3902,
994 | "tripid": 1314020,
995 | "tripdyn": 0,
996 | "stst": 73680,
997 | "stsd": "2024-11-26"
998 | },
999 | {
1000 | "vid": "278",
1001 | "tmstmp": "20241126 20:55",
1002 | "lat": "29.9386040009454",
1003 | "lon": "-90.07130309175989",
1004 | "hdg": "197",
1005 | "pid": 596,
1006 | "rt": "11",
1007 | "des": "Canal at Magazine \u003E\u003E Children's Hospital",
1008 | "pdist": 5040,
1009 | "dly": false,
1010 | "spd": 15,
1011 | "tatripid": "3648105",
1012 | "origtatripno": "3648105",
1013 | "tablockid": "511",
1014 | "zone": "",
1015 | "mode": 1,
1016 | "psgld": "N/A",
1017 | "srvtmstmp": "20241126 20:55",
1018 | "oid": "1683",
1019 | "or": false,
1020 | "rid": "1106",
1021 | "lwid1": "N/A",
1022 | "lwid2": "001106",
1023 | "blk": 10002,
1024 | "tripid": 1687020,
1025 | "tripdyn": 0,
1026 | "stst": 74880,
1027 | "stsd": "2024-11-26"
1028 | },
1029 | {
1030 | "vid": "345",
1031 | "tmstmp": "20241126 20:55",
1032 | "lat": "29.916518333333336",
1033 | "lon": "-90.12737833333334",
1034 | "hdg": "289",
1035 | "pid": 596,
1036 | "rt": "11",
1037 | "des": "Canal at Magazine \u003E\u003E Children's Hospital",
1038 | "pdist": 27849,
1039 | "dly": false,
1040 | "spd": 0,
1041 | "tatripid": "3648104",
1042 | "origtatripno": "3648104",
1043 | "tablockid": "411",
1044 | "zone": "",
1045 | "mode": 1,
1046 | "psgld": "N/A",
1047 | "srvtmstmp": "20241126 20:55",
1048 | "oid": "1894",
1049 | "or": false,
1050 | "rid": "1105",
1051 | "lwid1": "N/A",
1052 | "lwid2": "001105",
1053 | "blk": 8602,
1054 | "tripid": 4413020,
1055 | "tripdyn": 0,
1056 | "stst": 73080,
1057 | "stsd": "2024-11-26"
1058 | },
1059 | {
1060 | "vid": "920",
1061 | "tmstmp": "20241126 20:55",
1062 | "lat": "29.951078140017177",
1063 | "lon": "-90.07017651986784",
1064 | "hdg": "193",
1065 | "pid": 558,
1066 | "rt": "12",
1067 | "des": "Canal St. \u003E\u003E S. Carrollton at S. Claiborne",
1068 | "pdist": 1200,
1069 | "dly": false,
1070 | "spd": 0,
1071 | "tatripid": "3670137",
1072 | "origtatripno": "3670137",
1073 | "tablockid": "1012",
1074 | "zone": "",
1075 | "mode": 1,
1076 | "psgld": "N/A",
1077 | "srvtmstmp": "20241126 20:55",
1078 | "oid": "724",
1079 | "or": false,
1080 | "rid": "1218",
1081 | "lwid1": "N/A",
1082 | "lwid2": "001218",
1083 | "blk": 3302,
1084 | "tripid": 6576020,
1085 | "tripdyn": 0,
1086 | "stst": 73980,
1087 | "stsd": "2024-11-26"
1088 | },
1089 | {
1090 | "vid": "930",
1091 | "tmstmp": "20241126 20:55",
1092 | "lat": "29.95117448002808",
1093 | "lon": "-90.12519229272161",
1094 | "hdg": "42",
1095 | "pid": 558,
1096 | "rt": "12",
1097 | "des": "Canal St. \u003E\u003E S. Carrollton at S. Claiborne",
1098 | "pdist": 31787,
1099 | "dly": false,
1100 | "spd": 16,
1101 | "tatripid": "3670136",
1102 | "origtatripno": "3670136",
1103 | "tablockid": "212",
1104 | "zone": "",
1105 | "mode": 1,
1106 | "psgld": "N/A",
1107 | "srvtmstmp": "20241126 20:55",
1108 | "oid": "787",
1109 | "or": false,
1110 | "rid": "1216",
1111 | "lwid1": "N/A",
1112 | "lwid2": "001204",
1113 | "blk": 4202,
1114 | "tripid": 4747020,
1115 | "tripdyn": 0,
1116 | "stst": 72780,
1117 | "stsd": "2024-11-26"
1118 | },
1119 | {
1120 | "vid": "460",
1121 | "tmstmp": "20241126 20:55",
1122 | "lat": "29.936777908299298",
1123 | "lon": "-90.12620085453253",
1124 | "hdg": "132",
1125 | "pid": 588,
1126 | "rt": "12",
1127 | "des": "Canal St. via Garden District",
1128 | "pdist": 9426,
1129 | "dly": false,
1130 | "spd": 19,
1131 | "tatripid": "3670055",
1132 | "origtatripno": "3670055",
1133 | "tablockid": "1112",
1134 | "zone": "",
1135 | "mode": 1,
1136 | "psgld": "N/A",
1137 | "srvtmstmp": "20241126 20:55",
1138 | "oid": "609",
1139 | "or": false,
1140 | "rid": "1217",
1141 | "lwid1": "N/A",
1142 | "lwid2": "001217",
1143 | "blk": 3502,
1144 | "tripid": 2916020,
1145 | "tripdyn": 0,
1146 | "stst": 74400,
1147 | "stsd": "2024-11-26"
1148 | },
1149 | {
1150 | "vid": "934",
1151 | "tmstmp": "20241126 20:55",
1152 | "lat": "29.94574836065188",
1153 | "lon": "-90.07298438715135",
1154 | "hdg": "14",
1155 | "pid": 588,
1156 | "rt": "12",
1157 | "des": "Canal St. via Garden District",
1158 | "pdist": 31124,
1159 | "dly": false,
1160 | "spd": 9,
1161 | "tatripid": "3670054",
1162 | "origtatripno": "3670054",
1163 | "tablockid": "1212",
1164 | "zone": "",
1165 | "mode": 1,
1166 | "psgld": "N/A",
1167 | "srvtmstmp": "20241126 20:55",
1168 | "oid": "767",
1169 | "or": false,
1170 | "rid": "1219",
1171 | "lwid1": "N/A",
1172 | "lwid2": "001219",
1173 | "blk": 3602,
1174 | "tripid": 4086020,
1175 | "tripdyn": 0,
1176 | "stst": 73200,
1177 | "stsd": "2024-11-26"
1178 | },
1179 | {
1180 | "vid": "249",
1181 | "tmstmp": "20241126 20:55",
1182 | "lat": "29.93166200844894",
1183 | "lon": "-90.0930946865058",
1184 | "hdg": "337",
1185 | "pid": 536,
1186 | "rt": "27",
1187 | "des": "Delgado CC via Xavier",
1188 | "pdist": 14626,
1189 | "dly": false,
1190 | "spd": 9,
1191 | "tatripid": "3648850",
1192 | "origtatripno": "3648850",
1193 | "tablockid": "227",
1194 | "zone": "",
1195 | "mode": 1,
1196 | "psgld": "N/A",
1197 | "srvtmstmp": "20241126 20:55",
1198 | "oid": "1646",
1199 | "or": false,
1200 | "rid": "2704",
1201 | "lwid1": "N/A",
1202 | "lwid2": "002704",
1203 | "blk": 4302,
1204 | "tripid": 1970020,
1205 | "tripdyn": 0,
1206 | "stst": 74640,
1207 | "stsd": "2024-11-26"
1208 | },
1209 | {
1210 | "vid": "328",
1211 | "tmstmp": "20241126 20:55",
1212 | "lat": "29.971497550552115",
1213 | "lon": "-90.11341482284713",
1214 | "hdg": "219",
1215 | "pid": 562,
1216 | "rt": "27",
1217 | "des": "Tchoup. Wal-Mart via Xavier",
1218 | "pdist": 9756,
1219 | "dly": false,
1220 | "spd": 4,
1221 | "tatripid": "3648827",
1222 | "origtatripno": "3648827",
1223 | "tablockid": "127",
1224 | "zone": "",
1225 | "mode": 1,
1226 | "psgld": "N/A",
1227 | "srvtmstmp": "20241126 20:55",
1228 | "oid": "1134",
1229 | "or": false,
1230 | "rid": "2703",
1231 | "lwid1": "N/A",
1232 | "lwid2": "002703",
1233 | "blk": 902,
1234 | "tripid": 3371020,
1235 | "tripdyn": 0,
1236 | "stst": 74700,
1237 | "stsd": "2024-11-26"
1238 | },
1239 | {
1240 | "vid": "348",
1241 | "tmstmp": "20241126 20:55",
1242 | "lat": "29.961017147326707",
1243 | "lon": "-90.12308224299912",
1244 | "hdg": "244",
1245 | "pid": 606,
1246 | "rt": "31",
1247 | "des": "Children's Hospital",
1248 | "pdist": 36499,
1249 | "dly": false,
1250 | "spd": 26,
1251 | "tatripid": "3648986",
1252 | "origtatripno": "3648986",
1253 | "tablockid": "131",
1254 | "zone": "",
1255 | "mode": 1,
1256 | "psgld": "N/A",
1257 | "srvtmstmp": "20241126 20:55",
1258 | "oid": "1855",
1259 | "or": false,
1260 | "rid": "3103",
1261 | "lwid1": "N/A",
1262 | "lwid2": "003103",
1263 | "blk": 1102,
1264 | "tripid": 4957020,
1265 | "tripdyn": 0,
1266 | "stst": 73320,
1267 | "stsd": "2024-11-26"
1268 | },
1269 | {
1270 | "vid": "308",
1271 | "tmstmp": "20241126 20:55",
1272 | "lat": "29.970454422278035",
1273 | "lon": "-90.10471897154947",
1274 | "hdg": "42",
1275 | "pid": 608,
1276 | "rt": "31",
1277 | "des": "Gentilly Woods Hub",
1278 | "pdist": 37939,
1279 | "dly": false,
1280 | "spd": 22,
1281 | "tatripid": "3648970",
1282 | "origtatripno": "3648970",
1283 | "tablockid": "231",
1284 | "zone": "",
1285 | "mode": 1,
1286 | "psgld": "N/A",
1287 | "srvtmstmp": "20241126 20:55",
1288 | "oid": "1626",
1289 | "or": false,
1290 | "rid": "3104",
1291 | "lwid1": "N/A",
1292 | "lwid2": "003104",
1293 | "blk": 4502,
1294 | "tripid": 3715020,
1295 | "tripdyn": 0,
1296 | "stst": 73200,
1297 | "stsd": "2024-11-26"
1298 | },
1299 | {
1300 | "vid": "307",
1301 | "tmstmp": "20241126 20:55",
1302 | "lat": "29.978298004404774",
1303 | "lon": "-90.09355230254683",
1304 | "hdg": "313",
1305 | "pid": 612,
1306 | "rt": "32",
1307 | "des": "Children's Hospital",
1308 | "pdist": 16990,
1309 | "dly": false,
1310 | "spd": 22,
1311 | "tatripid": "3649088",
1312 | "origtatripno": "3649088",
1313 | "tablockid": "232",
1314 | "zone": "",
1315 | "mode": 1,
1316 | "psgld": "N/A",
1317 | "srvtmstmp": "20241126 20:55",
1318 | "oid": "47",
1319 | "or": false,
1320 | "rid": "3203",
1321 | "lwid1": "N/A",
1322 | "lwid2": "003203",
1323 | "blk": 4602,
1324 | "tripid": 787020,
1325 | "tripdyn": 0,
1326 | "stst": 74400,
1327 | "stsd": "2024-11-26"
1328 | },
1329 | {
1330 | "vid": "327",
1331 | "tmstmp": "20241126 20:55",
1332 | "lat": "29.928361666666667",
1333 | "lon": "-90.11708500000002",
1334 | "hdg": "16",
1335 | "pid": 616,
1336 | "rt": "32",
1337 | "des": "Main Library-CBD",
1338 | "pdist": 7219,
1339 | "dly": false,
1340 | "spd": 24,
1341 | "tatripid": "3649071",
1342 | "origtatripno": "3649071",
1343 | "tablockid": "132",
1344 | "zone": "",
1345 | "mode": 1,
1346 | "psgld": "N/A",
1347 | "srvtmstmp": "20241126 20:55",
1348 | "oid": "1117",
1349 | "or": true,
1350 | "rid": "3204",
1351 | "lwid1": "N/A",
1352 | "lwid2": "003204",
1353 | "blk": 1202,
1354 | "tripid": 2568020,
1355 | "tripdyn": 0,
1356 | "stst": 74700,
1357 | "stsd": "2024-11-26"
1358 | },
1359 | {
1360 | "vid": "2022",
1361 | "tmstmp": "20241126 20:55",
1362 | "lat": "29.96988577193718",
1363 | "lon": "-90.09371087428566",
1364 | "hdg": "128",
1365 | "pid": 48,
1366 | "rt": "47",
1367 | "des": "Canal St. Ferry Terminal",
1368 | "pdist": 7531,
1369 | "dly": false,
1370 | "spd": 0,
1371 | "tatripid": "3649500",
1372 | "origtatripno": "3649500",
1373 | "tablockid": "247",
1374 | "zone": "",
1375 | "mode": 1,
1376 | "psgld": "N/A",
1377 | "srvtmstmp": "20241126 20:55",
1378 | "oid": "1867",
1379 | "or": false,
1380 | "rid": "4710",
1381 | "lwid1": "N/A",
1382 | "lwid2": "004710",
1383 | "blk": 4802,
1384 | "tripid": 6145020,
1385 | "tripdyn": 0,
1386 | "stst": 74760,
1387 | "stsd": "2024-11-26"
1388 | },
1389 | {
1390 | "vid": "2019",
1391 | "tmstmp": "20241126 20:55",
1392 | "lat": "29.95580311922909",
1393 | "lon": "-90.07337871384308",
1394 | "hdg": "307",
1395 | "pid": 438,
1396 | "rt": "47",
1397 | "des": "Canal Ferry Terminal \u003E\u003E Cemeteries Transit Ctr.",
1398 | "pdist": 3334,
1399 | "dly": false,
1400 | "spd": 0,
1401 | "tatripid": "3649561",
1402 | "origtatripno": "3649561",
1403 | "tablockid": "447",
1404 | "zone": "",
1405 | "mode": 1,
1406 | "psgld": "N/A",
1407 | "srvtmstmp": "20241126 20:55",
1408 | "oid": "722",
1409 | "or": false,
1410 | "rid": "4709",
1411 | "lwid1": "N/A",
1412 | "lwid2": "004709",
1413 | "blk": 9002,
1414 | "tripid": 3745020,
1415 | "tripdyn": 0,
1416 | "stst": 74760,
1417 | "stsd": "2024-11-26"
1418 | },
1419 | {
1420 | "vid": "2012",
1421 | "tmstmp": "20241126 20:55",
1422 | "lat": "29.98270166666667",
1423 | "lon": "-90.11064999999999",
1424 | "hdg": "247",
1425 | "pid": 438,
1426 | "rt": "47",
1427 | "des": "Canal Ferry Terminal \u003E\u003E Cemeteries Transit Ctr.",
1428 | "pdist": 18907,
1429 | "dly": false,
1430 | "spd": 0,
1431 | "tatripid": "3649560",
1432 | "origtatripno": "3649560",
1433 | "tablockid": "347",
1434 | "zone": "",
1435 | "mode": 1,
1436 | "psgld": "N/A",
1437 | "srvtmstmp": "20241126 20:55",
1438 | "oid": "706",
1439 | "or": false,
1440 | "rid": "4708",
1441 | "lwid1": "N/A",
1442 | "lwid2": "004708",
1443 | "blk": 6902,
1444 | "tripid": 1706020,
1445 | "tripdyn": 0,
1446 | "stst": 73440,
1447 | "stsd": "2024-11-26"
1448 | },
1449 | {
1450 | "vid": "2006",
1451 | "tmstmp": "20241126 20:55",
1452 | "lat": "29.950294999999997",
1453 | "lon": "-90.06479499999999",
1454 | "hdg": "136",
1455 | "pid": 436,
1456 | "rt": "48",
1457 | "des": "City Park/Art Museum",
1458 | "pdist": 0,
1459 | "dly": false,
1460 | "spd": 0,
1461 | "tatripid": "3649914",
1462 | "origtatripno": "3649914",
1463 | "tablockid": "3448",
1464 | "zone": "",
1465 | "mode": 1,
1466 | "psgld": "N/A",
1467 | "srvtmstmp": "20241126 20:55",
1468 | "oid": "668",
1469 | "or": false,
1470 | "rid": "4807",
1471 | "lwid1": "N/A",
1472 | "lwid2": "004807",
1473 | "blk": 8402,
1474 | "tripid": 3891020,
1475 | "tripdyn": 0,
1476 | "stst": 75420,
1477 | "stsd": "2024-11-26"
1478 | },
1479 | {
1480 | "vid": "2004",
1481 | "tmstmp": "20241126 20:55",
1482 | "lat": "29.965764664828132",
1483 | "lon": "-90.08765649697565",
1484 | "hdg": "307",
1485 | "pid": 436,
1486 | "rt": "48",
1487 | "des": "City Park/Art Museum",
1488 | "pdist": 9056,
1489 | "dly": false,
1490 | "spd": 0,
1491 | "tatripid": "3649913",
1492 | "origtatripno": "3649913",
1493 | "tablockid": "3348",
1494 | "zone": "",
1495 | "mode": 1,
1496 | "psgld": "N/A",
1497 | "srvtmstmp": "20241126 20:55",
1498 | "oid": "669",
1499 | "or": false,
1500 | "rid": "4806",
1501 | "lwid1": "N/A",
1502 | "lwid2": "004806",
1503 | "blk": 8302,
1504 | "tripid": 2607020,
1505 | "tripdyn": 0,
1506 | "stst": 74100,
1507 | "stsd": "2024-11-26"
1508 | },
1509 | {
1510 | "vid": "2024",
1511 | "tmstmp": "20241126 20:55",
1512 | "lat": "29.983766666666664",
1513 | "lon": "-90.09023166666665",
1514 | "hdg": "51",
1515 | "pid": 436,
1516 | "rt": "48",
1517 | "des": "City Park/Art Museum",
1518 | "pdist": 18781,
1519 | "dly": true,
1520 | "spd": 0,
1521 | "tatripid": "3649912",
1522 | "origtatripno": "3649912",
1523 | "tablockid": "3248",
1524 | "zone": "",
1525 | "mode": 1,
1526 | "psgld": "N/A",
1527 | "srvtmstmp": "20241126 20:55",
1528 | "oid": "619",
1529 | "or": false,
1530 | "rid": "4808",
1531 | "lwid1": "N/A",
1532 | "lwid2": "004808",
1533 | "blk": 8202,
1534 | "tripid": 6010020,
1535 | "tripdyn": 0,
1536 | "stst": 72780,
1537 | "stsd": "2024-11-26"
1538 | },
1539 | {
1540 | "vid": "2015",
1541 | "tmstmp": "20241126 20:55",
1542 | "lat": "29.95759350944716",
1543 | "lon": "-90.07530815622025",
1544 | "hdg": "128",
1545 | "pid": 590,
1546 | "rt": "48",
1547 | "des": "Canal St. Ferry Terminal",
1548 | "pdist": 14322,
1549 | "dly": false,
1550 | "spd": 3,
1551 | "tatripid": "3649859",
1552 | "origtatripno": "3649859",
1553 | "tablockid": "3148",
1554 | "zone": "",
1555 | "mode": 1,
1556 | "psgld": "N/A",
1557 | "srvtmstmp": "20241126 20:55",
1558 | "oid": "644",
1559 | "or": false,
1560 | "rid": "4805",
1561 | "lwid1": "N/A",
1562 | "lwid2": "004805",
1563 | "blk": 8102,
1564 | "tripid": 2839020,
1565 | "tripdyn": 0,
1566 | "stst": 74100,
1567 | "stsd": "2024-11-26"
1568 | },
1569 | {
1570 | "vid": "2023",
1571 | "tmstmp": "20241126 20:55",
1572 | "lat": "29.945491666666666",
1573 | "lon": "-90.07821833333334",
1574 | "hdg": "300",
1575 | "pid": 639,
1576 | "rt": "49",
1577 | "des": "UPT \u003E\u003E French Market Station",
1578 | "pdist": 0,
1579 | "dly": false,
1580 | "spd": 0,
1581 | "tatripid": "3650155",
1582 | "origtatripno": "3650155",
1583 | "tablockid": "149",
1584 | "zone": "",
1585 | "mode": 1,
1586 | "psgld": "N/A",
1587 | "srvtmstmp": "20241126 20:55",
1588 | "oid": "663",
1589 | "or": false,
1590 | "rid": "4905",
1591 | "lwid1": "N/A",
1592 | "lwid2": "004905",
1593 | "blk": 1702,
1594 | "tripid": 65020,
1595 | "tripdyn": 0,
1596 | "stst": 75600,
1597 | "stsd": "2024-11-26"
1598 | },
1599 | {
1600 | "vid": "2007",
1601 | "tmstmp": "20241126 20:55",
1602 | "lat": "29.960833333333333",
1603 | "lon": "-90.05713666666666",
1604 | "hdg": "44",
1605 | "pid": 639,
1606 | "rt": "49",
1607 | "des": "UPT \u003E\u003E French Market Station",
1608 | "pdist": 13138,
1609 | "dly": false,
1610 | "spd": 0,
1611 | "tatripid": "3650154",
1612 | "origtatripno": "3650154",
1613 | "tablockid": "349",
1614 | "zone": "",
1615 | "mode": 1,
1616 | "psgld": "N/A",
1617 | "srvtmstmp": "20241126 20:55",
1618 | "oid": "628",
1619 | "or": false,
1620 | "rid": "4904",
1621 | "lwid1": "N/A",
1622 | "lwid2": "004904",
1623 | "blk": 7002,
1624 | "tripid": 4674020,
1625 | "tripdyn": 0,
1626 | "stst": 73800,
1627 | "stsd": "2024-11-26"
1628 | },
1629 | {
1630 | "vid": "2020",
1631 | "tmstmp": "20241126 20:55",
1632 | "lat": "29.951106914101587",
1633 | "lon": "-90.06599034199965",
1634 | "hdg": "301",
1635 | "pid": 642,
1636 | "rt": "49",
1637 | "des": "French Market Station \u003E\u003E UPT",
1638 | "pdist": 5739,
1639 | "dly": false,
1640 | "spd": 11,
1641 | "tatripid": "3650191",
1642 | "origtatripno": "3650191",
1643 | "tablockid": "249",
1644 | "zone": "",
1645 | "mode": 1,
1646 | "psgld": "N/A",
1647 | "srvtmstmp": "20241126 20:55",
1648 | "oid": "759",
1649 | "or": false,
1650 | "rid": "4906",
1651 | "lwid1": "N/A",
1652 | "lwid2": "004902",
1653 | "blk": 4902,
1654 | "tripid": 5267020,
1655 | "tripdyn": 0,
1656 | "stst": 74700,
1657 | "stsd": "2024-11-26"
1658 | },
1659 | {
1660 | "vid": "314",
1661 | "tmstmp": "20241126 20:55",
1662 | "lat": "29.955693333333336",
1663 | "lon": "-90.12033166666667",
1664 | "hdg": "250",
1665 | "pid": 594,
1666 | "rt": "51",
1667 | "des": "S. Carrollton via Main Library-CBD",
1668 | "pdist": 40204,
1669 | "dly": false,
1670 | "spd": 0,
1671 | "tatripid": "3650379",
1672 | "origtatripno": "3650379",
1673 | "tablockid": "15353",
1674 | "zone": "",
1675 | "mode": 1,
1676 | "psgld": "N/A",
1677 | "srvtmstmp": "20241126 20:55",
1678 | "oid": "1851",
1679 | "or": true,
1680 | "rid": "5302",
1681 | "lwid1": "N/A",
1682 | "lwid2": "005302",
1683 | "blk": 3702,
1684 | "tripid": 2274020,
1685 | "tripdyn": 0,
1686 | "stst": 73080,
1687 | "stsd": "2024-11-26"
1688 | },
1689 | {
1690 | "vid": "252",
1691 | "tmstmp": "20241126 20:55",
1692 | "lat": "29.951851666666666",
1693 | "lon": "-90.10101833333334",
1694 | "hdg": "338",
1695 | "pid": 579,
1696 | "rt": "52",
1697 | "des": "UNO Via Main Library-CBD",
1698 | "pdist": 6395,
1699 | "dly": false,
1700 | "spd": 19,
1701 | "tatripid": "3650594",
1702 | "origtatripno": "3650594",
1703 | "tablockid": "25353",
1704 | "zone": "",
1705 | "mode": 1,
1706 | "psgld": "N/A",
1707 | "srvtmstmp": "20241126 20:55",
1708 | "oid": "1631",
1709 | "or": false,
1710 | "rid": "5301",
1711 | "lwid1": "N/A",
1712 | "lwid2": "005301",
1713 | "blk": 6202,
1714 | "tripid": 4344020,
1715 | "tripdyn": 0,
1716 | "stst": 74880,
1717 | "stsd": "2024-11-26"
1718 | },
1719 | {
1720 | "vid": "284",
1721 | "tmstmp": "20241126 20:55",
1722 | "lat": "30.02845541261545",
1723 | "lon": "-90.06444001940392",
1724 | "hdg": "252",
1725 | "pid": 579,
1726 | "rt": "52",
1727 | "des": "UNO Via Main Library-CBD",
1728 | "pdist": 54265,
1729 | "dly": true,
1730 | "spd": 0,
1731 | "tatripid": "3650593",
1732 | "origtatripno": "3650593",
1733 | "tablockid": "152",
1734 | "zone": "",
1735 | "mode": 1,
1736 | "psgld": "N/A",
1737 | "srvtmstmp": "20241126 20:55",
1738 | "oid": "1968",
1739 | "or": false,
1740 | "rid": "5206",
1741 | "lwid1": "N/A",
1742 | "lwid2": "005206",
1743 | "blk": 1902,
1744 | "tripid": 4237020,
1745 | "tripdyn": 0,
1746 | "stst": 72120,
1747 | "stsd": "2024-11-26"
1748 | },
1749 | {
1750 | "vid": "286",
1751 | "tmstmp": "20241126 20:55",
1752 | "lat": "29.954107013240797",
1753 | "lon": "-90.07526832127708",
1754 | "hdg": "206",
1755 | "pid": 589,
1756 | "rt": "52",
1757 | "des": "Hollygrove/Xavier via Main Library-CBD",
1758 | "pdist": 34279,
1759 | "dly": false,
1760 | "spd": 0,
1761 | "tatripid": "3650569",
1762 | "origtatripno": "3650569",
1763 | "tablockid": "352",
1764 | "zone": "",
1765 | "mode": 1,
1766 | "psgld": "N/A",
1767 | "srvtmstmp": "20241126 20:55",
1768 | "oid": "1783",
1769 | "or": false,
1770 | "rid": "5205",
1771 | "lwid1": "N/A",
1772 | "lwid2": "005205",
1773 | "blk": 7202,
1774 | "tripid": 374020,
1775 | "tripdyn": 0,
1776 | "stst": 73740,
1777 | "stsd": "2024-11-26"
1778 | },
1779 | {
1780 | "vid": "279",
1781 | "tmstmp": "20241126 20:55",
1782 | "lat": "30.027778105150947",
1783 | "lon": "-90.06171135036386",
1784 | "hdg": "176",
1785 | "pid": 539,
1786 | "rt": "55",
1787 | "des": "Main Library-CBD via UNO/SUNO",
1788 | "pdist": 20014,
1789 | "dly": false,
1790 | "spd": 31,
1791 | "tatripid": "3650759",
1792 | "origtatripno": "3650759",
1793 | "tablockid": "555",
1794 | "zone": "",
1795 | "mode": 1,
1796 | "psgld": "N/A",
1797 | "srvtmstmp": "20241126 20:55",
1798 | "oid": "1455",
1799 | "or": false,
1800 | "rid": "5509",
1801 | "lwid1": "N/A",
1802 | "lwid2": "005509",
1803 | "blk": 10402,
1804 | "tripid": 813020,
1805 | "tripdyn": 0,
1806 | "stst": 74340,
1807 | "stsd": "2024-11-26"
1808 | },
1809 | {
1810 | "vid": "297",
1811 | "tmstmp": "20241126 20:55",
1812 | "lat": "29.95375333333333",
1813 | "lon": "-90.07541833333335",
1814 | "hdg": "190",
1815 | "pid": 539,
1816 | "rt": "55",
1817 | "des": "Main Library-CBD via UNO/SUNO",
1818 | "pdist": 50099,
1819 | "dly": false,
1820 | "spd": 0,
1821 | "tatripid": "3650758",
1822 | "origtatripno": "3650758",
1823 | "tablockid": "255",
1824 | "zone": "",
1825 | "mode": 1,
1826 | "psgld": "N/A",
1827 | "srvtmstmp": "20241126 20:55",
1828 | "oid": "1934",
1829 | "or": true,
1830 | "rid": "5507",
1831 | "lwid1": "N/A",
1832 | "lwid2": "005507",
1833 | "blk": 5202,
1834 | "tripid": 1469020,
1835 | "tripdyn": 0,
1836 | "stst": 72660,
1837 | "stsd": "2024-11-26"
1838 | },
1839 | {
1840 | "vid": "299",
1841 | "tmstmp": "20241126 20:55",
1842 | "lat": "29.990388212030087",
1843 | "lon": "-90.05840865461258",
1844 | "hdg": "355",
1845 | "pid": 564,
1846 | "rt": "55",
1847 | "des": "Gentilly Woods Hub via UNO/SUNO",
1848 | "pdist": 20901,
1849 | "dly": false,
1850 | "spd": 0,
1851 | "tatripid": "3650801",
1852 | "origtatripno": "3650801",
1853 | "tablockid": "455",
1854 | "zone": "",
1855 | "mode": 1,
1856 | "psgld": "N/A",
1857 | "srvtmstmp": "20241126 20:55",
1858 | "oid": "1986",
1859 | "or": false,
1860 | "rid": "5508",
1861 | "lwid1": "N/A",
1862 | "lwid2": "005508",
1863 | "blk": 9202,
1864 | "tripid": 1080020,
1865 | "tripdyn": 0,
1866 | "stst": 74280,
1867 | "stsd": "2024-11-26"
1868 | },
1869 | {
1870 | "vid": "283",
1871 | "tmstmp": "20241126 20:55",
1872 | "lat": "30.018353330554962",
1873 | "lon": "-90.03567592813238",
1874 | "hdg": "159",
1875 | "pid": 564,
1876 | "rt": "55",
1877 | "des": "Gentilly Woods Hub via UNO/SUNO",
1878 | "pdist": 51550,
1879 | "dly": false,
1880 | "spd": 30,
1881 | "tatripid": "3650800",
1882 | "origtatripno": "3650800",
1883 | "tablockid": "155",
1884 | "zone": "",
1885 | "mode": 1,
1886 | "psgld": "N/A",
1887 | "srvtmstmp": "20241126 20:55",
1888 | "oid": "1862",
1889 | "or": false,
1890 | "rid": "5506",
1891 | "lwid1": "N/A",
1892 | "lwid2": "005506",
1893 | "blk": 2002,
1894 | "tripid": 6412020,
1895 | "tripdyn": 0,
1896 | "stst": 72600,
1897 | "stsd": "2024-11-26"
1898 | },
1899 | {
1900 | "vid": "300",
1901 | "tmstmp": "20241126 20:55",
1902 | "lat": "29.954300626331747",
1903 | "lon": "-90.07474316149634",
1904 | "hdg": "48",
1905 | "pid": 628,
1906 | "rt": "57",
1907 | "des": "SUNO",
1908 | "pdist": 33120,
1909 | "dly": false,
1910 | "spd": 0,
1911 | "tatripid": "3651031",
1912 | "origtatripno": "3651031",
1913 | "tablockid": "357",
1914 | "zone": "",
1915 | "mode": 1,
1916 | "psgld": "N/A",
1917 | "srvtmstmp": "20241126 20:55",
1918 | "oid": "1703",
1919 | "or": false,
1920 | "rid": "5707",
1921 | "lwid1": "N/A",
1922 | "lwid2": "005707",
1923 | "blk": 7402,
1924 | "tripid": 149020,
1925 | "tripdyn": 0,
1926 | "stst": 73440,
1927 | "stsd": "2024-11-26"
1928 | },
1929 | {
1930 | "vid": "303",
1931 | "tmstmp": "20241126 20:55",
1932 | "lat": "30.028188460208792",
1933 | "lon": "-90.05242139812444",
1934 | "hdg": "176",
1935 | "pid": 629,
1936 | "rt": "57",
1937 | "des": "Audubon Zoo via Tulane Univ.",
1938 | "pdist": 14518,
1939 | "dly": false,
1940 | "spd": 0,
1941 | "tatripid": "3651001",
1942 | "origtatripno": "3651001",
1943 | "tablockid": "457",
1944 | "zone": "",
1945 | "mode": 1,
1946 | "psgld": "N/A",
1947 | "srvtmstmp": "20241126 20:55",
1948 | "oid": "1906",
1949 | "or": true,
1950 | "rid": "MAN",
1951 | "lwid1": "0",
1952 | "lwid2": "0",
1953 | "blk": -1900558988,
1954 | "tripid": -730565389,
1955 | "tripdyn": 0,
1956 | "stst": 73500,
1957 | "stsd": "2024-11-26"
1958 | },
1959 | {
1960 | "vid": "319",
1961 | "tmstmp": "20241126 20:55",
1962 | "lat": "29.953902613547275",
1963 | "lon": "-90.07531660878341",
1964 | "hdg": "194",
1965 | "pid": 629,
1966 | "rt": "57",
1967 | "des": "Audubon Zoo via Tulane Univ.",
1968 | "pdist": 40823,
1969 | "dly": false,
1970 | "spd": 0,
1971 | "tatripid": "3651001",
1972 | "origtatripno": "3651001",
1973 | "tablockid": "457",
1974 | "zone": "",
1975 | "mode": 1,
1976 | "psgld": "N/A",
1977 | "srvtmstmp": "20241126 20:55",
1978 | "oid": "1909",
1979 | "or": false,
1980 | "rid": "5708",
1981 | "lwid1": "N/A",
1982 | "lwid2": "005708",
1983 | "blk": 9302,
1984 | "tripid": 4192020,
1985 | "tripdyn": 0,
1986 | "stst": 73500,
1987 | "stsd": "2024-11-26"
1988 | },
1989 | {
1990 | "vid": "301",
1991 | "tmstmp": "20241126 20:55",
1992 | "lat": "29.92646",
1993 | "lon": "-90.13189166666668",
1994 | "hdg": "18",
1995 | "pid": 629,
1996 | "rt": "57",
1997 | "des": "Audubon Zoo via Tulane Univ.",
1998 | "pdist": 67429,
1999 | "dly": true,
2000 | "spd": 0,
2001 | "tatripid": "3651000",
2002 | "origtatripno": "3651000",
2003 | "tablockid": "257",
2004 | "zone": "",
2005 | "mode": 1,
2006 | "psgld": "N/A",
2007 | "srvtmstmp": "20241126 20:55",
2008 | "oid": "1982",
2009 | "or": false,
2010 | "rid": "MAN",
2011 | "lwid1": "0",
2012 | "lwid2": "0",
2013 | "blk": -1108372680,
2014 | "tripid": -1277710698,
2015 | "tripdyn": 0,
2016 | "stst": 71280,
2017 | "stsd": "2024-11-26"
2018 | },
2019 | {
2020 | "vid": "337",
2021 | "tmstmp": "20241126 20:55",
2022 | "lat": "30.01007326632373",
2023 | "lon": "-90.02084029256878",
2024 | "hdg": "351",
2025 | "pid": 578,
2026 | "rt": "61",
2027 | "des": "Village De'Lest via N.O. East Hub",
2028 | "pdist": 30024,
2029 | "dly": false,
2030 | "spd": 11,
2031 | "tatripid": "3651236",
2032 | "origtatripno": "3651236",
2033 | "tablockid": "561",
2034 | "zone": "",
2035 | "mode": 1,
2036 | "psgld": "N/A",
2037 | "srvtmstmp": "20241126 20:55",
2038 | "oid": "1766",
2039 | "or": false,
2040 | "rid": "6109",
2041 | "lwid1": "N/A",
2042 | "lwid2": "006109",
2043 | "blk": 10502,
2044 | "tripid": 5303020,
2045 | "tripdyn": 0,
2046 | "stst": 74100,
2047 | "stsd": "2024-11-26"
2048 | },
2049 | {
2050 | "vid": "329",
2051 | "tmstmp": "20241126 20:55",
2052 | "lat": "30.03702579929512",
2053 | "lon": "-89.90975020601103",
2054 | "hdg": "249",
2055 | "pid": 578,
2056 | "rt": "61",
2057 | "des": "Village De'Lest via N.O. East Hub",
2058 | "pdist": 86409,
2059 | "dly": false,
2060 | "spd": 0,
2061 | "tatripid": "3651235",
2062 | "origtatripno": "3651235",
2063 | "tablockid": "361",
2064 | "zone": "",
2065 | "mode": 1,
2066 | "psgld": "N/A",
2067 | "srvtmstmp": "20241126 20:55",
2068 | "oid": "1861",
2069 | "or": false,
2070 | "rid": "6108",
2071 | "lwid1": "N/A",
2072 | "lwid2": "006108",
2073 | "blk": 7502,
2074 | "tripid": 4378020,
2075 | "tripdyn": 0,
2076 | "stst": 71940,
2077 | "stsd": "2024-11-26"
2078 | },
2079 | {
2080 | "vid": "321",
2081 | "tmstmp": "20241126 20:55",
2082 | "lat": "30.029062724984435",
2083 | "lon": "-89.98264835072486",
2084 | "hdg": "333",
2085 | "pid": 580,
2086 | "rt": "61",
2087 | "des": "Main Library-CBD via N.O. East Hub",
2088 | "pdist": 25414,
2089 | "dly": false,
2090 | "spd": 11,
2091 | "tatripid": "3651194",
2092 | "origtatripno": "3651194",
2093 | "tablockid": "661",
2094 | "zone": "",
2095 | "mode": 1,
2096 | "psgld": "N/A",
2097 | "srvtmstmp": "20241126 20:55",
2098 | "oid": "1864",
2099 | "or": false,
2100 | "rid": "6110",
2101 | "lwid1": "N/A",
2102 | "lwid2": "006110",
2103 | "blk": 11202,
2104 | "tripid": 6246020,
2105 | "tripdyn": 0,
2106 | "stst": 74280,
2107 | "stsd": "2024-11-26"
2108 | },
2109 | {
2110 | "vid": "325",
2111 | "tmstmp": "20241126 20:55",
2112 | "lat": "30.002497601643842",
2113 | "lon": "-90.03587942030947",
2114 | "hdg": "69",
2115 | "pid": 592,
2116 | "rt": "62",
2117 | "des": "N.O. East Hub via Wal-Mart",
2118 | "pdist": 23533,
2119 | "dly": false,
2120 | "spd": 0,
2121 | "tatripid": "3667509",
2122 | "origtatripno": "3667509",
2123 | "tablockid": "462",
2124 | "zone": "",
2125 | "mode": 1,
2126 | "psgld": "N/A",
2127 | "srvtmstmp": "20241126 20:55",
2128 | "oid": "1710",
2129 | "or": false,
2130 | "rid": "6209",
2131 | "lwid1": "N/A",
2132 | "lwid2": "006209",
2133 | "blk": 9502,
2134 | "tripid": 432020,
2135 | "tripdyn": 0,
2136 | "stst": 74880,
2137 | "stsd": "2024-11-26"
2138 | },
2139 | {
2140 | "vid": "324",
2141 | "tmstmp": "20241126 20:55",
2142 | "lat": "30.042689493697996",
2143 | "lon": "-89.9801450006687",
2144 | "hdg": "67",
2145 | "pid": 592,
2146 | "rt": "62",
2147 | "des": "N.O. East Hub via Wal-Mart",
2148 | "pdist": 55465,
2149 | "dly": false,
2150 | "spd": 0,
2151 | "tatripid": "3667508",
2152 | "origtatripno": "3667508",
2153 | "tablockid": "362",
2154 | "zone": "",
2155 | "mode": 1,
2156 | "psgld": "N/A",
2157 | "srvtmstmp": "20241126 20:55",
2158 | "oid": "1505",
2159 | "or": false,
2160 | "rid": "6208",
2161 | "lwid1": "N/A",
2162 | "lwid2": "006208",
2163 | "blk": 7602,
2164 | "tripid": 4670020,
2165 | "tripdyn": 0,
2166 | "stst": 73380,
2167 | "stsd": "2024-11-26"
2168 | },
2169 | {
2170 | "vid": "304",
2171 | "tmstmp": "20241126 20:55",
2172 | "lat": "30.0257650626177",
2173 | "lon": "-90.05613354141889",
2174 | "hdg": "260",
2175 | "pid": 655,
2176 | "rt": "66",
2177 | "des": "N.O. East Hub via UNO/SUNO",
2178 | "pdist": 32943,
2179 | "dly": false,
2180 | "spd": 28,
2181 | "tatripid": "3667858",
2182 | "origtatripno": "3667858",
2183 | "tablockid": "166",
2184 | "zone": "",
2185 | "mode": 1,
2186 | "psgld": "N/A",
2187 | "srvtmstmp": "20241126 20:55",
2188 | "oid": "1750",
2189 | "or": false,
2190 | "rid": "6602",
2191 | "lwid1": "N/A",
2192 | "lwid2": "006602",
2193 | "blk": 2402,
2194 | "tripid": 3977020,
2195 | "tripdyn": 0,
2196 | "stst": 74100,
2197 | "stsd": "2024-11-26"
2198 | },
2199 | {
2200 | "vid": "346",
2201 | "tmstmp": "20241126 20:55",
2202 | "lat": "30.053354346622218",
2203 | "lon": "-89.93088979329276",
2204 | "hdg": "177",
2205 | "pid": 576,
2206 | "rt": "67",
2207 | "des": "N. O. East Hub via Michoud/NASA",
2208 | "pdist": 24054,
2209 | "dly": false,
2210 | "spd": 28,
2211 | "tatripid": "3651743",
2212 | "origtatripno": "3651743",
2213 | "tablockid": "167",
2214 | "zone": "",
2215 | "mode": 1,
2216 | "psgld": "N/A",
2217 | "srvtmstmp": "20241126 20:55",
2218 | "oid": "1465",
2219 | "or": false,
2220 | "rid": "6702",
2221 | "lwid1": "N/A",
2222 | "lwid2": "006702",
2223 | "blk": 2502,
2224 | "tripid": 4986020,
2225 | "tripdyn": 0,
2226 | "stst": 74580,
2227 | "stsd": "2024-11-26"
2228 | },
2229 | {
2230 | "vid": "309",
2231 | "tmstmp": "20241126 20:55",
2232 | "lat": "30.067876073180805",
2233 | "lon": "-89.95351063008734",
2234 | "hdg": "49",
2235 | "pid": 546,
2236 | "rt": "68",
2237 | "des": "N.O. East Hub via Wal-Mart and Little Woods",
2238 | "pdist": 19628,
2239 | "dly": false,
2240 | "spd": 22,
2241 | "tatripid": "3651877",
2242 | "origtatripno": "3651877",
2243 | "tablockid": "268",
2244 | "zone": "",
2245 | "mode": 1,
2246 | "psgld": "N/A",
2247 | "srvtmstmp": "20241126 20:55",
2248 | "oid": "1530",
2249 | "or": false,
2250 | "rid": "6803",
2251 | "lwid1": "N/A",
2252 | "lwid2": "006803",
2253 | "blk": 5602,
2254 | "tripid": 4303020,
2255 | "tripdyn": 0,
2256 | "stst": 74700,
2257 | "stsd": "2024-11-26"
2258 | },
2259 | {
2260 | "vid": "311",
2261 | "tmstmp": "20241126 20:55",
2262 | "lat": "30.038374943034942",
2263 | "lon": "-89.97677664946251",
2264 | "hdg": "235",
2265 | "pid": 546,
2266 | "rt": "68",
2267 | "des": "N.O. East Hub via Wal-Mart and Little Woods",
2268 | "pdist": 48761,
2269 | "dly": false,
2270 | "spd": 0,
2271 | "tatripid": "3651876",
2272 | "origtatripno": "3651876",
2273 | "tablockid": "168",
2274 | "zone": "",
2275 | "mode": 1,
2276 | "psgld": "N/A",
2277 | "srvtmstmp": "20241126 20:55",
2278 | "oid": "1437",
2279 | "or": false,
2280 | "rid": "6804",
2281 | "lwid1": "N/A",
2282 | "lwid2": "006804",
2283 | "blk": 2602,
2284 | "tripid": 3604020,
2285 | "tripdyn": 0,
2286 | "stst": 73260,
2287 | "stsd": "2024-11-26"
2288 | },
2289 | {
2290 | "vid": "305",
2291 | "tmstmp": "20241126 20:55",
2292 | "lat": "29.976894332822788",
2293 | "lon": "-90.03748682663085",
2294 | "hdg": "194",
2295 | "pid": 583,
2296 | "rt": "80",
2297 | "des": "Elysian Fields via Delgado-Sidney Collier",
2298 | "pdist": 17658,
2299 | "dly": false,
2300 | "spd": 9,
2301 | "tatripid": "3651992",
2302 | "origtatripno": "3651992",
2303 | "tablockid": "280",
2304 | "zone": "",
2305 | "mode": 1,
2306 | "psgld": "N/A",
2307 | "srvtmstmp": "20241126 20:55",
2308 | "oid": "1816",
2309 | "or": false,
2310 | "rid": "8004",
2311 | "lwid1": "N/A",
2312 | "lwid2": "008004",
2313 | "blk": 5802,
2314 | "tripid": 439020,
2315 | "tripdyn": 0,
2316 | "stst": 74100,
2317 | "stsd": "2024-11-26"
2318 | },
2319 | {
2320 | "vid": "316",
2321 | "tmstmp": "20241126 20:55",
2322 | "lat": "29.979644473058173",
2323 | "lon": "-90.05832951435622",
2324 | "hdg": "267",
2325 | "pid": 648,
2326 | "rt": "84",
2327 | "des": "Main Library-CBD",
2328 | "pdist": 21953,
2329 | "dly": false,
2330 | "spd": 26,
2331 | "tatripid": "3652173",
2332 | "origtatripno": "3652173",
2333 | "tablockid": "284",
2334 | "zone": "",
2335 | "mode": 1,
2336 | "psgld": "N/A",
2337 | "srvtmstmp": "20241126 20:55",
2338 | "oid": "1890",
2339 | "or": false,
2340 | "rid": "8404",
2341 | "lwid1": "N/A",
2342 | "lwid2": "008404",
2343 | "blk": 5902,
2344 | "tripid": 6396020,
2345 | "tripdyn": 0,
2346 | "stst": 74160,
2347 | "stsd": "2024-11-26"
2348 | },
2349 | {
2350 | "vid": "302",
2351 | "tmstmp": "20241126 20:55",
2352 | "lat": "29.954668489795566",
2353 | "lon": "-90.01241414815708",
2354 | "hdg": "109",
2355 | "pid": 635,
2356 | "rt": "86",
2357 | "des": "Nunez CC \u003E\u003E Arabi",
2358 | "pdist": 43809,
2359 | "dly": false,
2360 | "spd": 2,
2361 | "tatripid": "3652335",
2362 | "origtatripno": "3652335",
2363 | "tablockid": "186",
2364 | "zone": "",
2365 | "mode": 1,
2366 | "psgld": "N/A",
2367 | "srvtmstmp": "20241126 20:55",
2368 | "oid": "1922",
2369 | "or": false,
2370 | "rid": "8602",
2371 | "lwid1": "N/A",
2372 | "lwid2": "008602",
2373 | "blk": 3002,
2374 | "tripid": 3485020,
2375 | "tripdyn": 0,
2376 | "stst": 73800,
2377 | "stsd": "2024-11-26"
2378 | },
2379 | {
2380 | "vid": "2405",
2381 | "tmstmp": "20241126 20:55",
2382 | "lat": "29.93707051085529",
2383 | "lon": "-90.0830733985952",
2384 | "hdg": "253",
2385 | "pid": 577,
2386 | "rt": "91",
2387 | "des": "Tchoup. Wal-Mart",
2388 | "pdist": 32292,
2389 | "dly": false,
2390 | "spd": 4,
2391 | "tatripid": "3652461",
2392 | "origtatripno": "3652461",
2393 | "tablockid": "591",
2394 | "zone": "",
2395 | "mode": 1,
2396 | "psgld": "N/A",
2397 | "srvtmstmp": "20241126 20:55",
2398 | "oid": "1828",
2399 | "or": false,
2400 | "rid": "9107",
2401 | "lwid1": "N/A",
2402 | "lwid2": "009107",
2403 | "blk": 10902,
2404 | "tripid": 345020,
2405 | "tripdyn": 0,
2406 | "stst": 73500,
2407 | "stsd": "2024-11-26"
2408 | },
2409 | {
2410 | "vid": "217",
2411 | "tmstmp": "20241126 20:55",
2412 | "lat": "29.954304113588865",
2413 | "lon": "-90.0748296915116",
2414 | "hdg": "75",
2415 | "pid": 597,
2416 | "rt": "91",
2417 | "des": "Cemeteries via Art Museum/Delgado CC",
2418 | "pdist": 16966,
2419 | "dly": false,
2420 | "spd": 0,
2421 | "tatripid": "3652500",
2422 | "origtatripno": "3652500",
2423 | "tablockid": "391",
2424 | "zone": "",
2425 | "mode": 1,
2426 | "psgld": "N/A",
2427 | "srvtmstmp": "20241126 20:55",
2428 | "oid": "1970",
2429 | "or": false,
2430 | "rid": "9106",
2431 | "lwid1": "N/A",
2432 | "lwid2": "009106",
2433 | "blk": 8002,
2434 | "tripid": 6379020,
2435 | "tripdyn": 0,
2436 | "stst": 74220,
2437 | "stsd": "2024-11-26"
2438 | },
2439 | {
2440 | "vid": "339",
2441 | "tmstmp": "20241126 20:55",
2442 | "lat": "29.98354080795059",
2443 | "lon": "-90.11032712900641",
2444 | "hdg": "3",
2445 | "pid": 597,
2446 | "rt": "91",
2447 | "des": "Cemeteries via Art Museum/Delgado CC",
2448 | "pdist": 41807,
2449 | "dly": false,
2450 | "spd": 0,
2451 | "tatripid": "3652499",
2452 | "origtatripno": "3652499",
2453 | "tablockid": "691",
2454 | "zone": "",
2455 | "mode": 1,
2456 | "psgld": "N/A",
2457 | "srvtmstmp": "20241126 20:55",
2458 | "oid": "1865",
2459 | "or": false,
2460 | "rid": "9108",
2461 | "lwid1": "N/A",
2462 | "lwid2": "009108",
2463 | "blk": 11602,
2464 | "tripid": 5433020,
2465 | "tripdyn": 0,
2466 | "stst": 72420,
2467 | "stsd": "2024-11-26"
2468 | },
2469 | {
2470 | "vid": "326",
2471 | "tmstmp": "20241126 20:55",
2472 | "lat": "29.930992600049347",
2473 | "lon": "-90.00115659289597",
2474 | "hdg": "297",
2475 | "pid": 550,
2476 | "rt": "103",
2477 | "des": "Main Library-CBD via Wilty Terminal",
2478 | "pdist": 6568,
2479 | "dly": false,
2480 | "spd": 41,
2481 | "tatripid": "3667075",
2482 | "origtatripno": "3667075",
2483 | "tablockid": "4103",
2484 | "zone": "",
2485 | "mode": 1,
2486 | "psgld": "N/A",
2487 | "srvtmstmp": "20241126 20:55",
2488 | "oid": "1931",
2489 | "or": false,
2490 | "rid": "10307",
2491 | "lwid1": "N/A",
2492 | "lwid2": "010307",
2493 | "blk": 8502,
2494 | "tripid": 1775020,
2495 | "tripdyn": 0,
2496 | "stst": 74400,
2497 | "stsd": "2024-11-26"
2498 | },
2499 | {
2500 | "vid": "2407",
2501 | "tmstmp": "20241126 20:55",
2502 | "lat": "29.942661068733933",
2503 | "lon": "-90.04081479655825",
2504 | "hdg": "6",
2505 | "pid": 652,
2506 | "rt": "103",
2507 | "des": "Cutoff Rec Ctr. via Algiers Ferry",
2508 | "pdist": 38170,
2509 | "dly": false,
2510 | "spd": 21,
2511 | "tatripid": "3667105",
2512 | "origtatripno": "3667105",
2513 | "tablockid": "5103",
2514 | "zone": "",
2515 | "mode": 1,
2516 | "psgld": "N/A",
2517 | "srvtmstmp": "20241126 20:55",
2518 | "oid": "1951",
2519 | "or": false,
2520 | "rid": "10308",
2521 | "lwid1": "N/A",
2522 | "lwid2": "010308",
2523 | "blk": 9902,
2524 | "tripid": 173020,
2525 | "tripdyn": 0,
2526 | "stst": 73800,
2527 | "stsd": "2024-11-26"
2528 | },
2529 | {
2530 | "vid": "313",
2531 | "tmstmp": "20241126 20:55",
2532 | "lat": "29.92066031552237",
2533 | "lon": "-90.04322029522534",
2534 | "hdg": "210",
2535 | "pid": 598,
2536 | "rt": "105",
2537 | "des": "Wilty Terminal via Wal-Mart",
2538 | "pdist": 45460,
2539 | "dly": false,
2540 | "spd": 0,
2541 | "tatripid": "3652852",
2542 | "origtatripno": "3652852",
2543 | "tablockid": "1105",
2544 | "zone": "",
2545 | "mode": 1,
2546 | "psgld": "N/A",
2547 | "srvtmstmp": "20241126 20:55",
2548 | "oid": "1419",
2549 | "or": false,
2550 | "rid": "10502",
2551 | "lwid1": "N/A",
2552 | "lwid2": "010502",
2553 | "blk": 302,
2554 | "tripid": 5156020,
2555 | "tripdyn": 0,
2556 | "stst": 73380,
2557 | "stsd": "2024-11-26"
2558 | },
2559 | {
2560 | "vid": "322",
2561 | "tmstmp": "20241126 20:55",
2562 | "lat": "29.95472",
2563 | "lon": "-90.07530833333334",
2564 | "hdg": "120",
2565 | "pid": 547,
2566 | "rt": "114A",
2567 | "des": "Main Library-CBD via Wilty Terminal",
2568 | "pdist": 67673,
2569 | "dly": false,
2570 | "spd": 0,
2571 | "tatripid": "3652949",
2572 | "origtatripno": "3652949",
2573 | "tablockid": "4114A",
2574 | "zone": "",
2575 | "mode": 1,
2576 | "psgld": "N/A",
2577 | "srvtmstmp": "20241126 20:55",
2578 | "oid": "1581",
2579 | "or": false,
2580 | "rid": "11413",
2581 | "lwid1": "N/A",
2582 | "lwid2": "011413",
2583 | "blk": 8702,
2584 | "tripid": 1844020,
2585 | "tripdyn": 0,
2586 | "stst": 72300,
2587 | "stsd": "2024-11-26"
2588 | },
2589 | {
2590 | "vid": "2400",
2591 | "tmstmp": "20241126 20:55",
2592 | "lat": "29.92279856247605",
2593 | "lon": "-90.01808423325369",
2594 | "hdg": "134",
2595 | "pid": 563,
2596 | "rt": "114A",
2597 | "des": "Cutoff Rec. Ctr. via Wilty Terminal",
2598 | "pdist": 49329,
2599 | "dly": false,
2600 | "spd": 27,
2601 | "tatripid": "3652988",
2602 | "origtatripno": "3652988",
2603 | "tablockid": "2114A",
2604 | "zone": "",
2605 | "mode": 1,
2606 | "psgld": "N/A",
2607 | "srvtmstmp": "20241126 20:55",
2608 | "oid": "1729",
2609 | "or": false,
2610 | "rid": "11409",
2611 | "lwid1": "N/A",
2612 | "lwid2": "011409",
2613 | "blk": 4002,
2614 | "tripid": 1509020,
2615 | "tripdyn": 0,
2616 | "stst": 73320,
2617 | "stsd": "2024-11-26"
2618 | },
2619 | {
2620 | "vid": "332",
2621 | "tmstmp": "20241126 20:55",
2622 | "lat": "29.923143333333336",
2623 | "lon": "-89.98292333333332",
2624 | "hdg": "294",
2625 | "pid": 563,
2626 | "rt": "114A",
2627 | "des": "Cutoff Rec. Ctr. via Wilty Terminal",
2628 | "pdist": 74101,
2629 | "dly": true,
2630 | "spd": 0,
2631 | "tatripid": "3652980",
2632 | "origtatripno": "3652980",
2633 | "tablockid": "5114A",
2634 | "zone": "",
2635 | "mode": 1,
2636 | "psgld": "N/A",
2637 | "srvtmstmp": "20241126 20:55",
2638 | "oid": "1698",
2639 | "or": false,
2640 | "rid": "11414",
2641 | "lwid1": "N/A",
2642 | "lwid2": "011414",
2643 | "blk": 10102,
2644 | "tripid": 6397020,
2645 | "tripdyn": 0,
2646 | "stst": 71520,
2647 | "stsd": "2024-11-26"
2648 | },
2649 | {
2650 | "vid": "2412",
2651 | "tmstmp": "20241126 20:55",
2652 | "lat": "29.93871204571504",
2653 | "lon": "-90.06237216182666",
2654 | "hdg": "85",
2655 | "pid": 543,
2656 | "rt": "114B",
2657 | "des": "Tall Timbers via Wilty Terminal",
2658 | "pdist": 14066,
2659 | "dly": false,
2660 | "spd": 42,
2661 | "tatripid": "3653168",
2662 | "origtatripno": "3653168",
2663 | "tablockid": "3114B",
2664 | "zone": "",
2665 | "mode": 1,
2666 | "psgld": "N/A",
2667 | "srvtmstmp": "20241126 20:55",
2668 | "oid": "290",
2669 | "or": false,
2670 | "rid": "11410",
2671 | "lwid1": "N/A",
2672 | "lwid2": "011410",
2673 | "blk": 6602,
2674 | "tripid": 1961020,
2675 | "tripdyn": 0,
2676 | "stst": 74700,
2677 | "stsd": "2024-11-26"
2678 | },
2679 | {
2680 | "vid": "343",
2681 | "tmstmp": "20241126 20:55",
2682 | "lat": "29.90433477070026",
2683 | "lon": "-89.99196455719535",
2684 | "hdg": "102",
2685 | "pid": 543,
2686 | "rt": "114B",
2687 | "des": "Tall Timbers via Wilty Terminal",
2688 | "pdist": 71900,
2689 | "dly": false,
2690 | "spd": 0,
2691 | "tatripid": "3653167",
2692 | "origtatripno": "3653167",
2693 | "tablockid": "1114B",
2694 | "zone": "",
2695 | "mode": 1,
2696 | "psgld": "N/A",
2697 | "srvtmstmp": "20241126 20:55",
2698 | "oid": "1787",
2699 | "or": false,
2700 | "rid": "11412",
2701 | "lwid1": "N/A",
2702 | "lwid2": "011412",
2703 | "blk": 602,
2704 | "tripid": 2005020,
2705 | "tripdyn": 0,
2706 | "stst": 72300,
2707 | "stsd": "2024-11-26"
2708 | },
2709 | {
2710 | "vid": "2410",
2711 | "tmstmp": "20241126 20:55",
2712 | "lat": "29.904452639188765",
2713 | "lon": "-89.99192333138541",
2714 | "hdg": "51",
2715 | "pid": 543,
2716 | "rt": "114B",
2717 | "des": "Tall Timbers via Wilty Terminal",
2718 | "pdist": 71924,
2719 | "dly": true,
2720 | "spd": 0,
2721 | "tatripid": "3653166",
2722 | "origtatripno": "3653166",
2723 | "tablockid": "2114B",
2724 | "zone": "",
2725 | "mode": 1,
2726 | "psgld": "N/A",
2727 | "srvtmstmp": "20241126 20:55",
2728 | "oid": "1519",
2729 | "or": false,
2730 | "rid": "11411",
2731 | "lwid1": "N/A",
2732 | "lwid2": "011411",
2733 | "blk": 4102,
2734 | "tripid": 3553020,
2735 | "tripdyn": 0,
2736 | "stst": 69900,
2737 | "stsd": "2024-11-26"
2738 | },
2739 | {
2740 | "vid": "349",
2741 | "tmstmp": "20241126 20:55",
2742 | "lat": "29.954794214461696",
2743 | "lon": "-90.07481128729921",
2744 | "hdg": "209",
2745 | "pid": 471,
2746 | "rt": "202",
2747 | "des": "Main Library-CBD",
2748 | "pdist": 81700,
2749 | "dly": false,
2750 | "spd": 0,
2751 | "tatripid": "3653375",
2752 | "origtatripno": "3653375",
2753 | "tablockid": "1202",
2754 | "zone": "",
2755 | "mode": 1,
2756 | "psgld": "N/A",
2757 | "srvtmstmp": "20241126 20:55",
2758 | "oid": "1972",
2759 | "or": false,
2760 | "rid": "20202",
2761 | "lwid1": "N/A",
2762 | "lwid2": "020202",
2763 | "blk": 802,
2764 | "tripid": 5200020,
2765 | "tripdyn": 0,
2766 | "stst": 73800,
2767 | "stsd": "2024-11-26"
2768 | }
2769 | ]
2770 | }
2771 | }
--------------------------------------------------------------------------------
/mock_bustime_server/mock_vehicles_jp.protobuf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefornola/nola-transit-map/63e81ad453832a55f01719f4c7a85ced7f6338a2/mock_bustime_server/mock_vehicles_jp.protobuf
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nola-transit-map",
3 | "version": "1.0.0",
4 | "description": "This program scrapes vehicle locations from Clever Devices' bustime product https://www.cleverdevices.com/products/bustime/ which the New Orleans RTA uses to track their bus and streetcar fleet. It scrapes all the locations in the fleet and puts them into a sqlite database.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "esbuild app/main.jsx --minify --loader:.png=dataurl --loader:.svg=dataurl --bundle --outfile=public/app.js",
9 | "watch": "esbuild app/main.jsx --loader:.png=dataurl --loader:.svg=dataurl --bundle --outfile=public/app.js --watch"
10 | },
11 | "private": true,
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "bootstrap": "^5.2.2",
16 | "esbuild": "^0.15.12",
17 | "leaflet": "^1.9.2",
18 | "leaflet-rotatedmarker": "^0.2.0",
19 | "react": "^18.2.0",
20 | "react-bootstrap": "^2.5.0",
21 | "react-dom": "^18.2.0",
22 | "react-icons": "^4.6.0",
23 | "react-leaflet": "^4.1.0",
24 | "react-select": "^5.5.9"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | NOLA Transit Map
5 |
6 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------