-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
268 lines (241 loc) · 9.42 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import "dotenv/config";
import express from "express";
import helmet from "helmet";
import knex from "knex";
import { point as turf_point, polygon as turf_polygon } from "@turf/helpers"
import booleanPointInPolygon from "@turf/boolean-point-in-polygon"
import cachedFeed from "./cachedFeed.js";
import padLeft from "./padLeft.js";
const DIRECTION_LOOKUP = { 0: "in", 1: "out" };
const ROUTE_TYPE_LOOKUP = { 0: "tram", 2: "rail", 3: "bus", 4: "ferry" };
// Period after the expected arrival of the vehicle to the stop to keep showing on the feed
const DEFAULT_FEED_FILTER_TIME = 2; // minutes
const knex_db = knex({
client: process.env.DB_CLIENT,
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
}
});
const app = express();
app.use(helmet());
app.use(express.static("public"));
app.get("/feed", async (req, res) => {
try {
let routes = [];
if (req.query.routes) {
routes = req.query.routes.split(",");
}
if (!req.query.neLat || !req.query.neLng || !req.query.swLat || !req.query.swLng) {
res.status(400);
return res.send("Unspecified boundary parameters");
}
const bounds = turf_polygon([[
[req.query.neLng, req.query.neLat],
[req.query.neLng, req.query.swLat],
[req.query.swLng, req.query.swLat],
[req.query.swLng, req.query.neLat],
[req.query.neLng, req.query.neLat],
]]);
const feed = await cachedFeed()
let vehicles = feed.vehicles;
// Keep vehicles on specified routes
if (routes.length) {
vehicles = vehicles.filter(v => routes.includes(v.route));
}
// Keep vehicles within specified map bounds
vehicles = vehicles.filter(v => {
const point = turf_point([v.longitude, v.latitude]);
return booleanPointInPolygon(point, bounds);
});
// Get direction and type of vehicle
// Wrap in try catch in case we can't contact DB, but we can still return GTFS data
let trips = [];
try {
trips = await knex_db
.select("trip_id", "direction_id", "shape_id", "route_short_name", "route_type")
.from("trips")
.innerJoin("routes", "routes.route_id", "trips.route_id")
.whereIn("trip_id", vehicles.map(v => v.tripId));
} catch (err) {
console.error(err);
}
vehicles = vehicles.map(v => {
const trip = trips.find(t => t.trip_id === v.tripId) || {};
return {
id: v.id,
route: v.route,
latitude: v.latitude,
longitude: v.longitude,
route: trip.route_short_name || v.route, // if there's no name, probably an unplanned trip
routeType: ROUTE_TYPE_LOOKUP[trip.route_type] || "rail", // Schedules seem to be missing train routes
direction: DIRECTION_LOOKUP[trip.direction_id],
delay: v.delay
}
});
res.json(vehicles);
} catch (err) {
console.error(err);
res.status(500);
res.json(err);
}
});
app.get("/feed-stops", async (req, res) => {
try {
if (!req.query.from) {
res.status(400);
return res.send("Need 'from' stop code");
}
if (!req.query.to || !req.query.to.length) {
res.status(400);
return res.send("Need 'to' stop code");
}
const feed = await cachedFeed();
let vehicles = feed.vehicles;
const from = req.query.from;
let to;
if (req.query.to.includes(",")) {
to = req.query.to.split(",")
} else {
to = [req.query.to];
}
const trips = await knex_db
.select("t.trip_id", "r.route_short_name", "st1.departure_time", "s2.stop_name", "st2.arrival_time")
.from("trips as t")
.innerJoin("routes as r", "r.route_id", "t.route_id")
.innerJoin("stop_times as st1", "st1.trip_id", "t.trip_id")
.innerJoin("stops as s1", "s1.stop_id", "st1.stop_id")
.innerJoin("stop_times as st2", "st2.trip_id", "t.trip_id")
.innerJoin("stops as s2", "s2.stop_id", "st2.stop_id")
.where("s1.stop_code", from)
.whereIn("s2.stop_code", to);
vehicles = vehicles
// Remove vehicles that aren't in the scheduled trips since we can't tell when they'll depart/arrive
.filter(v => trips.find(t => t.trip_id === v.tripId))
.map(v => {
const trip = trips.find(t => t.trip_id === v.tripId) || {};
return {
route: trip.route_short_name,
departs: trip.departure_time && trip.departure_time.substring(0, 5),
delay: v.delay,
to: trip.stop_name
.replace("Elizabeth Street Stop 81 near George St", "Elizabeth St")
.replace("Cultural Centre, platform 1", "Cultural Centre"),
arrives: trip.arrival_time && trip.arrival_time.substring(0, 5)
}
});
// Max Delay is used to conservatively filter out vehicles that have already "arrived" at the "from" stop
const maxDelay = vehicles.reduce((prev, curr) => curr.delay > prev.delay ? curr : prev, 0).delay / 60 + DEFAULT_FEED_FILTER_TIME;
let earliestArrival = "99:99";
vehicles = vehicles
.map(v => {
let expectDepart, expectArrive, hasDeparted, exclude;
if (v.departs) {
// Calculate expected departure based on delay
const delayInMins = Math.round((v.delay || 0) / 60);
const departsHr = Number.parseInt(v.departs.substring(0, 2));
const departsMin = Number.parseInt(v.departs.substring(3, 5));
const departsDelay = departsMin + Number.parseInt(delayInMins);
const expectDepartHr = departsHr + Math.floor(departsDelay / 60);
const expectDepartMin = departsDelay < 0 ? 60 + departsDelay : departsDelay % 60;
expectDepart = `${padLeft(expectDepartHr, "0", 2)}:${padLeft(expectDepartMin, "0", 2)}`;
const arrivesHr = Number.parseInt(v.arrives.substring(0, 2));
const arrivesMin = Number.parseInt(v.arrives.substring(3, 5));
const arrivesDelay = arrivesMin + Number.parseInt(delayInMins);
const expectArriveHr = arrivesHr + Math.floor(arrivesDelay / 60);
const expectArriveMin = arrivesDelay < 0 ? 60 + arrivesDelay : arrivesDelay % 60;
expectArrive = `${padLeft(expectArriveHr, "0", 2)}:${padLeft(expectArriveMin, "0", 2)}`;
const now = feed.timestamp;
const nowAbs = now.getHours() * 60 + now.getMinutes();
const departsAbs = departsHr * 60 + departsMin;
const expectDepartAbs = expectDepartHr * 60 + expectDepartMin;
// Use the greater of departs or expected to determine whether the vehicle has departed
hasDeparted = Math.max(departsAbs, expectDepartAbs) < nowAbs;
exclude = departsAbs + maxDelay < nowAbs;
if (!exclude && !hasDeparted && expectArrive < earliestArrival) {
earliestArrival = expectArrive;
}
}
return {
route: v.route,
departs: v.departs,
expectDepart: expectDepart,
departed: hasDeparted,
to: v.to,
arrives: v.arrives,
expectArrive: expectArrive,
exclude: exclude
};
})
// Exclude vehicles that have already departed from the "from" stop
.filter(v => !v.exclude)
.map(v => ({
route: v.route,
departs: v.departs,
expectDepart: v.expectDepart,
departed: v.departed,
to: v.to,
arrives: v.arrives,
expectArrive: v.expectArrive,
// Return undefined so the earliestArrival key is not included in the JSON object sent to client
earliestArrival : v.expectArrive === earliestArrival ? true : undefined
}));
vehicles.sort((a, b) => {
if (!a.departs || !b.departs) {
return 0;
}
return a.departs.localeCompare(b.departs);
});
res.json(vehicles);
} catch (err) {
console.error(err);
res.status(500);
res.json(err);
}
});
app.get("/debug", async (req, res) => {
try {
let vehicles = await fileFeed.get();
const trips = await knex_db
.select("t.trip_id", "r.route_short_name", "st1.departure_time", "s2.stop_name", "st2.arrival_time")
.from("trips as t")
.innerJoin("routes as r", "r.route_id", "t.route_id")
.innerJoin("stop_times as st1", "st1.trip_id", "t.trip_id")
.innerJoin("stops as s1", "s1.stop_id", "st1.stop_id")
.innerJoin("stop_times as st2", "st2.trip_id", "t.trip_id")
.innerJoin("stops as s2", "s2.stop_id", "st2.stop_id")
.where("s1.stop_code", "005840");
vehicles = vehicles
.map(v => {
const trip = trips.find(t => t.trip_id === v.tripId) || {};
let status;
if (trip.trip_id) {
status = "1 Match";
} else if (["P129", "P137", "P141", "P151"].includes(v.route)) {
status = "2 Missing match";
} else {
status = "3 No match"
}
return {
route: v.route,
tripId: v.tripId,
status
}
});
vehicles.sort((a, b) => a.tripId.localeCompare(b.tripId));
vehicles.sort((a, b) => a.status.localeCompare(b.status));
vehicles = vehicles
.map(v => {
return `<tr><td>${v.route}</td><td>${v.tripId}</td><td>${v.status}</td></tr>`
});
res.send("<table><thead><th>Route</th><th>Trip Id</th><th>Match Status</th></thead><tbody>" + vehicles.join("") + "</tbody></table>");
} catch (err) {
console.error(err);
res.status(500);
res.json(err);
}
});
const port = parseInt(process.env.PORT, 10) || 3000;
app.listen(port, () => console.log(`Listening on port ${port}`));