gtfs-sqljs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1263 -0
- package/dist/index.d.ts +1089 -0
- package/dist/index.js +3154 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3154 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/cache/utils.ts
|
|
12
|
+
var utils_exports = {};
|
|
13
|
+
__export(utils_exports, {
|
|
14
|
+
DEFAULT_CACHE_EXPIRATION_MS: () => DEFAULT_CACHE_EXPIRATION_MS,
|
|
15
|
+
filterExpiredEntries: () => filterExpiredEntries,
|
|
16
|
+
getCacheStats: () => getCacheStats,
|
|
17
|
+
isCacheExpired: () => isCacheExpired
|
|
18
|
+
});
|
|
19
|
+
function isCacheExpired(metadata, expirationMs = DEFAULT_CACHE_EXPIRATION_MS) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const age = now - metadata.timestamp;
|
|
22
|
+
return age > expirationMs;
|
|
23
|
+
}
|
|
24
|
+
function filterExpiredEntries(entries, expirationMs = DEFAULT_CACHE_EXPIRATION_MS) {
|
|
25
|
+
return entries.filter((entry) => !isCacheExpired(entry.metadata, expirationMs));
|
|
26
|
+
}
|
|
27
|
+
function getCacheStats(entries) {
|
|
28
|
+
const totalSize = entries.reduce((sum, entry) => sum + entry.metadata.size, 0);
|
|
29
|
+
const expiredEntries = entries.filter((entry) => isCacheExpired(entry.metadata));
|
|
30
|
+
const activeEntries = entries.filter((entry) => !isCacheExpired(entry.metadata));
|
|
31
|
+
return {
|
|
32
|
+
totalEntries: entries.length,
|
|
33
|
+
activeEntries: activeEntries.length,
|
|
34
|
+
expiredEntries: expiredEntries.length,
|
|
35
|
+
totalSize,
|
|
36
|
+
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
|
37
|
+
oldestEntry: entries.length > 0 ? Math.min(...entries.map((e) => e.metadata.timestamp)) : null,
|
|
38
|
+
newestEntry: entries.length > 0 ? Math.max(...entries.map((e) => e.metadata.timestamp)) : null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
var DEFAULT_CACHE_EXPIRATION_MS;
|
|
42
|
+
var init_utils = __esm({
|
|
43
|
+
"src/cache/utils.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
DEFAULT_CACHE_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/gtfs-sqljs.ts
|
|
50
|
+
import initSqlJs from "sql.js";
|
|
51
|
+
|
|
52
|
+
// src/schema/schema.ts
|
|
53
|
+
var GTFS_SCHEMA = [
|
|
54
|
+
{
|
|
55
|
+
name: "agency",
|
|
56
|
+
columns: [
|
|
57
|
+
{ name: "agency_id", type: "TEXT", required: true, primaryKey: true },
|
|
58
|
+
{ name: "agency_name", type: "TEXT", required: true },
|
|
59
|
+
{ name: "agency_url", type: "TEXT", required: true },
|
|
60
|
+
{ name: "agency_timezone", type: "TEXT", required: true },
|
|
61
|
+
{ name: "agency_lang", type: "TEXT", required: false },
|
|
62
|
+
{ name: "agency_phone", type: "TEXT", required: false },
|
|
63
|
+
{ name: "agency_fare_url", type: "TEXT", required: false },
|
|
64
|
+
{ name: "agency_email", type: "TEXT", required: false }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "stops",
|
|
69
|
+
columns: [
|
|
70
|
+
{ name: "stop_id", type: "TEXT", required: true, primaryKey: true },
|
|
71
|
+
{ name: "stop_name", type: "TEXT", required: true },
|
|
72
|
+
{ name: "stop_lat", type: "REAL", required: true },
|
|
73
|
+
{ name: "stop_lon", type: "REAL", required: true },
|
|
74
|
+
{ name: "stop_code", type: "TEXT", required: false },
|
|
75
|
+
{ name: "stop_desc", type: "TEXT", required: false },
|
|
76
|
+
{ name: "zone_id", type: "TEXT", required: false },
|
|
77
|
+
{ name: "stop_url", type: "TEXT", required: false },
|
|
78
|
+
{ name: "location_type", type: "INTEGER", required: false },
|
|
79
|
+
{ name: "parent_station", type: "TEXT", required: false },
|
|
80
|
+
{ name: "stop_timezone", type: "TEXT", required: false },
|
|
81
|
+
{ name: "wheelchair_boarding", type: "INTEGER", required: false },
|
|
82
|
+
{ name: "level_id", type: "TEXT", required: false },
|
|
83
|
+
{ name: "platform_code", type: "TEXT", required: false }
|
|
84
|
+
],
|
|
85
|
+
indexes: [
|
|
86
|
+
{ name: "idx_stops_stop_code", columns: ["stop_code"] },
|
|
87
|
+
{ name: "idx_stops_stop_name", columns: ["stop_name"] },
|
|
88
|
+
{ name: "idx_stops_parent_station", columns: ["parent_station"] }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "routes",
|
|
93
|
+
columns: [
|
|
94
|
+
{ name: "route_id", type: "TEXT", required: true, primaryKey: true },
|
|
95
|
+
{ name: "route_short_name", type: "TEXT", required: true },
|
|
96
|
+
{ name: "route_long_name", type: "TEXT", required: true },
|
|
97
|
+
{ name: "route_type", type: "INTEGER", required: true },
|
|
98
|
+
{ name: "agency_id", type: "TEXT", required: false },
|
|
99
|
+
{ name: "route_desc", type: "TEXT", required: false },
|
|
100
|
+
{ name: "route_url", type: "TEXT", required: false },
|
|
101
|
+
{ name: "route_color", type: "TEXT", required: false },
|
|
102
|
+
{ name: "route_text_color", type: "TEXT", required: false },
|
|
103
|
+
{ name: "route_sort_order", type: "INTEGER", required: false },
|
|
104
|
+
{ name: "continuous_pickup", type: "INTEGER", required: false },
|
|
105
|
+
{ name: "continuous_drop_off", type: "INTEGER", required: false }
|
|
106
|
+
],
|
|
107
|
+
indexes: [
|
|
108
|
+
{ name: "idx_routes_agency_id", columns: ["agency_id"] }
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "trips",
|
|
113
|
+
columns: [
|
|
114
|
+
{ name: "trip_id", type: "TEXT", required: true, primaryKey: true },
|
|
115
|
+
{ name: "route_id", type: "TEXT", required: true },
|
|
116
|
+
{ name: "service_id", type: "TEXT", required: true },
|
|
117
|
+
{ name: "trip_headsign", type: "TEXT", required: false },
|
|
118
|
+
{ name: "trip_short_name", type: "TEXT", required: false },
|
|
119
|
+
{ name: "direction_id", type: "INTEGER", required: false },
|
|
120
|
+
{ name: "block_id", type: "TEXT", required: false },
|
|
121
|
+
{ name: "shape_id", type: "TEXT", required: false },
|
|
122
|
+
{ name: "wheelchair_accessible", type: "INTEGER", required: false },
|
|
123
|
+
{ name: "bikes_allowed", type: "INTEGER", required: false }
|
|
124
|
+
],
|
|
125
|
+
indexes: [
|
|
126
|
+
{ name: "idx_trips_route_id", columns: ["route_id"] },
|
|
127
|
+
{ name: "idx_trips_service_id", columns: ["service_id"] },
|
|
128
|
+
{ name: "idx_trips_route_service", columns: ["route_id", "service_id"] }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "stop_times",
|
|
133
|
+
columns: [
|
|
134
|
+
{ name: "trip_id", type: "TEXT", required: true },
|
|
135
|
+
{ name: "arrival_time", type: "TEXT", required: true },
|
|
136
|
+
{ name: "departure_time", type: "TEXT", required: true },
|
|
137
|
+
{ name: "stop_id", type: "TEXT", required: true },
|
|
138
|
+
{ name: "stop_sequence", type: "INTEGER", required: true },
|
|
139
|
+
{ name: "stop_headsign", type: "TEXT", required: false },
|
|
140
|
+
{ name: "pickup_type", type: "INTEGER", required: false },
|
|
141
|
+
{ name: "drop_off_type", type: "INTEGER", required: false },
|
|
142
|
+
{ name: "continuous_pickup", type: "INTEGER", required: false },
|
|
143
|
+
{ name: "continuous_drop_off", type: "INTEGER", required: false },
|
|
144
|
+
{ name: "shape_dist_traveled", type: "REAL", required: false },
|
|
145
|
+
{ name: "timepoint", type: "INTEGER", required: false }
|
|
146
|
+
],
|
|
147
|
+
indexes: [
|
|
148
|
+
{ name: "idx_stop_times_trip_id", columns: ["trip_id"] },
|
|
149
|
+
{ name: "idx_stop_times_stop_id", columns: ["stop_id"] },
|
|
150
|
+
{ name: "idx_stop_times_trip_sequence", columns: ["trip_id", "stop_sequence"] }
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "calendar",
|
|
155
|
+
columns: [
|
|
156
|
+
{ name: "service_id", type: "TEXT", required: true, primaryKey: true },
|
|
157
|
+
{ name: "monday", type: "INTEGER", required: true },
|
|
158
|
+
{ name: "tuesday", type: "INTEGER", required: true },
|
|
159
|
+
{ name: "wednesday", type: "INTEGER", required: true },
|
|
160
|
+
{ name: "thursday", type: "INTEGER", required: true },
|
|
161
|
+
{ name: "friday", type: "INTEGER", required: true },
|
|
162
|
+
{ name: "saturday", type: "INTEGER", required: true },
|
|
163
|
+
{ name: "sunday", type: "INTEGER", required: true },
|
|
164
|
+
{ name: "start_date", type: "TEXT", required: true },
|
|
165
|
+
{ name: "end_date", type: "TEXT", required: true }
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "calendar_dates",
|
|
170
|
+
columns: [
|
|
171
|
+
{ name: "service_id", type: "TEXT", required: true },
|
|
172
|
+
{ name: "date", type: "TEXT", required: true },
|
|
173
|
+
{ name: "exception_type", type: "INTEGER", required: true }
|
|
174
|
+
],
|
|
175
|
+
indexes: [
|
|
176
|
+
{ name: "idx_calendar_dates_service_id", columns: ["service_id"] },
|
|
177
|
+
{ name: "idx_calendar_dates_date", columns: ["date"] },
|
|
178
|
+
{ name: "idx_calendar_dates_service_date", columns: ["service_id", "date"] }
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "fare_attributes",
|
|
183
|
+
columns: [
|
|
184
|
+
{ name: "fare_id", type: "TEXT", required: true, primaryKey: true },
|
|
185
|
+
{ name: "price", type: "REAL", required: true },
|
|
186
|
+
{ name: "currency_type", type: "TEXT", required: true },
|
|
187
|
+
{ name: "payment_method", type: "INTEGER", required: true },
|
|
188
|
+
{ name: "transfers", type: "INTEGER", required: true },
|
|
189
|
+
{ name: "agency_id", type: "TEXT", required: false },
|
|
190
|
+
{ name: "transfer_duration", type: "INTEGER", required: false }
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "fare_rules",
|
|
195
|
+
columns: [
|
|
196
|
+
{ name: "fare_id", type: "TEXT", required: true },
|
|
197
|
+
{ name: "route_id", type: "TEXT", required: false },
|
|
198
|
+
{ name: "origin_id", type: "TEXT", required: false },
|
|
199
|
+
{ name: "destination_id", type: "TEXT", required: false },
|
|
200
|
+
{ name: "contains_id", type: "TEXT", required: false }
|
|
201
|
+
],
|
|
202
|
+
indexes: [
|
|
203
|
+
{ name: "idx_fare_rules_fare_id", columns: ["fare_id"] },
|
|
204
|
+
{ name: "idx_fare_rules_route_id", columns: ["route_id"] }
|
|
205
|
+
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "shapes",
|
|
209
|
+
columns: [
|
|
210
|
+
{ name: "shape_id", type: "TEXT", required: true },
|
|
211
|
+
{ name: "shape_pt_lat", type: "REAL", required: true },
|
|
212
|
+
{ name: "shape_pt_lon", type: "REAL", required: true },
|
|
213
|
+
{ name: "shape_pt_sequence", type: "INTEGER", required: true },
|
|
214
|
+
{ name: "shape_dist_traveled", type: "REAL", required: false }
|
|
215
|
+
],
|
|
216
|
+
indexes: [
|
|
217
|
+
{ name: "idx_shapes_shape_id", columns: ["shape_id"] },
|
|
218
|
+
{ name: "idx_shapes_shape_sequence", columns: ["shape_id", "shape_pt_sequence"] }
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "frequencies",
|
|
223
|
+
columns: [
|
|
224
|
+
{ name: "trip_id", type: "TEXT", required: true },
|
|
225
|
+
{ name: "start_time", type: "TEXT", required: true },
|
|
226
|
+
{ name: "end_time", type: "TEXT", required: true },
|
|
227
|
+
{ name: "headway_secs", type: "INTEGER", required: true },
|
|
228
|
+
{ name: "exact_times", type: "INTEGER", required: false }
|
|
229
|
+
],
|
|
230
|
+
indexes: [
|
|
231
|
+
{ name: "idx_frequencies_trip_id", columns: ["trip_id"] }
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "transfers",
|
|
236
|
+
columns: [
|
|
237
|
+
{ name: "from_stop_id", type: "TEXT", required: true },
|
|
238
|
+
{ name: "to_stop_id", type: "TEXT", required: true },
|
|
239
|
+
{ name: "transfer_type", type: "INTEGER", required: true },
|
|
240
|
+
{ name: "min_transfer_time", type: "INTEGER", required: false }
|
|
241
|
+
],
|
|
242
|
+
indexes: [
|
|
243
|
+
{ name: "idx_transfers_from_stop_id", columns: ["from_stop_id"] },
|
|
244
|
+
{ name: "idx_transfers_to_stop_id", columns: ["to_stop_id"] }
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "pathways",
|
|
249
|
+
columns: [
|
|
250
|
+
{ name: "pathway_id", type: "TEXT", required: true, primaryKey: true },
|
|
251
|
+
{ name: "from_stop_id", type: "TEXT", required: true },
|
|
252
|
+
{ name: "to_stop_id", type: "TEXT", required: true },
|
|
253
|
+
{ name: "pathway_mode", type: "INTEGER", required: true },
|
|
254
|
+
{ name: "is_bidirectional", type: "INTEGER", required: true },
|
|
255
|
+
{ name: "length", type: "REAL", required: false },
|
|
256
|
+
{ name: "traversal_time", type: "INTEGER", required: false },
|
|
257
|
+
{ name: "stair_count", type: "INTEGER", required: false },
|
|
258
|
+
{ name: "max_slope", type: "REAL", required: false },
|
|
259
|
+
{ name: "min_width", type: "REAL", required: false },
|
|
260
|
+
{ name: "signposted_as", type: "TEXT", required: false },
|
|
261
|
+
{ name: "reversed_signposted_as", type: "TEXT", required: false }
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "levels",
|
|
266
|
+
columns: [
|
|
267
|
+
{ name: "level_id", type: "TEXT", required: true, primaryKey: true },
|
|
268
|
+
{ name: "level_index", type: "REAL", required: true },
|
|
269
|
+
{ name: "level_name", type: "TEXT", required: false }
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "feed_info",
|
|
274
|
+
columns: [
|
|
275
|
+
{ name: "feed_publisher_name", type: "TEXT", required: true },
|
|
276
|
+
{ name: "feed_publisher_url", type: "TEXT", required: true },
|
|
277
|
+
{ name: "feed_lang", type: "TEXT", required: true },
|
|
278
|
+
{ name: "default_lang", type: "TEXT", required: false },
|
|
279
|
+
{ name: "feed_start_date", type: "TEXT", required: false },
|
|
280
|
+
{ name: "feed_end_date", type: "TEXT", required: false },
|
|
281
|
+
{ name: "feed_version", type: "TEXT", required: false },
|
|
282
|
+
{ name: "feed_contact_email", type: "TEXT", required: false },
|
|
283
|
+
{ name: "feed_contact_url", type: "TEXT", required: false }
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "attributions",
|
|
288
|
+
columns: [
|
|
289
|
+
{ name: "attribution_id", type: "TEXT", required: true, primaryKey: true },
|
|
290
|
+
{ name: "organization_name", type: "TEXT", required: true },
|
|
291
|
+
{ name: "agency_id", type: "TEXT", required: false },
|
|
292
|
+
{ name: "route_id", type: "TEXT", required: false },
|
|
293
|
+
{ name: "trip_id", type: "TEXT", required: false },
|
|
294
|
+
{ name: "is_producer", type: "INTEGER", required: false },
|
|
295
|
+
{ name: "is_operator", type: "INTEGER", required: false },
|
|
296
|
+
{ name: "is_authority", type: "INTEGER", required: false },
|
|
297
|
+
{ name: "attribution_url", type: "TEXT", required: false },
|
|
298
|
+
{ name: "attribution_email", type: "TEXT", required: false },
|
|
299
|
+
{ name: "attribution_phone", type: "TEXT", required: false }
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
];
|
|
303
|
+
function generateCreateTableSQL(schema) {
|
|
304
|
+
const columns = schema.columns.map((col) => {
|
|
305
|
+
const parts = [col.name, col.type];
|
|
306
|
+
if (col.primaryKey) {
|
|
307
|
+
parts.push("PRIMARY KEY");
|
|
308
|
+
}
|
|
309
|
+
if (col.required && !col.primaryKey) {
|
|
310
|
+
parts.push("NOT NULL");
|
|
311
|
+
}
|
|
312
|
+
return parts.join(" ");
|
|
313
|
+
});
|
|
314
|
+
return `CREATE TABLE IF NOT EXISTS ${schema.name} (${columns.join(", ")})`;
|
|
315
|
+
}
|
|
316
|
+
function generateCreateIndexSQL(schema) {
|
|
317
|
+
if (!schema.indexes) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
return schema.indexes.map((idx) => {
|
|
321
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
322
|
+
const columns = idx.columns.join(", ");
|
|
323
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name} (${columns})`;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function getAllCreateTableStatements() {
|
|
327
|
+
const statements = [];
|
|
328
|
+
for (const schema of GTFS_SCHEMA) {
|
|
329
|
+
statements.push(generateCreateTableSQL(schema));
|
|
330
|
+
}
|
|
331
|
+
return statements;
|
|
332
|
+
}
|
|
333
|
+
function getAllCreateIndexStatements() {
|
|
334
|
+
const statements = [];
|
|
335
|
+
for (const schema of GTFS_SCHEMA) {
|
|
336
|
+
statements.push(...generateCreateIndexSQL(schema));
|
|
337
|
+
}
|
|
338
|
+
return statements;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/loaders/zip-loader.ts
|
|
342
|
+
import JSZip from "jszip";
|
|
343
|
+
async function loadGTFSZip(source) {
|
|
344
|
+
let zipData;
|
|
345
|
+
if (typeof source === "string") {
|
|
346
|
+
zipData = await fetchZip(source);
|
|
347
|
+
} else {
|
|
348
|
+
zipData = source;
|
|
349
|
+
}
|
|
350
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
351
|
+
const files = {};
|
|
352
|
+
const filePromises = [];
|
|
353
|
+
zip.forEach((relativePath, file) => {
|
|
354
|
+
if (!file.dir && relativePath.endsWith(".txt")) {
|
|
355
|
+
const fileName = relativePath.split("/").pop() || relativePath;
|
|
356
|
+
filePromises.push(
|
|
357
|
+
file.async("string").then((content) => {
|
|
358
|
+
files[fileName] = content;
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
await Promise.all(filePromises);
|
|
364
|
+
return files;
|
|
365
|
+
}
|
|
366
|
+
async function fetchZip(source, onProgress) {
|
|
367
|
+
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
368
|
+
if (isUrl) {
|
|
369
|
+
if (typeof fetch !== "undefined") {
|
|
370
|
+
const response = await fetch(source);
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
throw new Error(`Failed to fetch GTFS ZIP: ${response.status} ${response.statusText}`);
|
|
373
|
+
}
|
|
374
|
+
const contentLength = response.headers.get("content-length");
|
|
375
|
+
const total = contentLength ? parseInt(contentLength, 10) : null;
|
|
376
|
+
if (!onProgress || !total || !response.body) {
|
|
377
|
+
return await response.arrayBuffer();
|
|
378
|
+
}
|
|
379
|
+
const reader = response.body.getReader();
|
|
380
|
+
const chunks = [];
|
|
381
|
+
let receivedLength = 0;
|
|
382
|
+
let done = false;
|
|
383
|
+
while (!done) {
|
|
384
|
+
const result = await reader.read();
|
|
385
|
+
done = result.done;
|
|
386
|
+
if (done || !result.value) break;
|
|
387
|
+
chunks.push(result.value);
|
|
388
|
+
receivedLength += result.value.length;
|
|
389
|
+
const downloadPercent = receivedLength / total * 100;
|
|
390
|
+
const percentComplete = Math.floor(1 + downloadPercent * 29 / 100);
|
|
391
|
+
onProgress({
|
|
392
|
+
phase: "downloading",
|
|
393
|
+
currentFile: null,
|
|
394
|
+
filesCompleted: 0,
|
|
395
|
+
totalFiles: 0,
|
|
396
|
+
rowsProcessed: 0,
|
|
397
|
+
totalRows: 0,
|
|
398
|
+
bytesDownloaded: receivedLength,
|
|
399
|
+
totalBytes: total,
|
|
400
|
+
percentComplete: Math.min(percentComplete, 30),
|
|
401
|
+
message: `Downloading GTFS ZIP (${(receivedLength / 1024 / 1024).toFixed(1)} MB / ${(total / 1024 / 1024).toFixed(1)} MB)`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const allChunks = new Uint8Array(receivedLength);
|
|
405
|
+
let position = 0;
|
|
406
|
+
for (const chunk of chunks) {
|
|
407
|
+
allChunks.set(chunk, position);
|
|
408
|
+
position += chunk.length;
|
|
409
|
+
}
|
|
410
|
+
return allChunks.buffer;
|
|
411
|
+
}
|
|
412
|
+
throw new Error("fetch is not available to load URL");
|
|
413
|
+
}
|
|
414
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
415
|
+
if (isNode) {
|
|
416
|
+
try {
|
|
417
|
+
const fs = await import("fs");
|
|
418
|
+
const buffer = await fs.promises.readFile(source);
|
|
419
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new Error(`Failed to read GTFS ZIP file: ${error}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (typeof fetch !== "undefined") {
|
|
425
|
+
const response = await fetch(source);
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
throw new Error(`Failed to fetch GTFS ZIP: ${response.status} ${response.statusText}`);
|
|
428
|
+
}
|
|
429
|
+
return await response.arrayBuffer();
|
|
430
|
+
}
|
|
431
|
+
throw new Error("No method available to load ZIP file");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/loaders/csv-parser.ts
|
|
435
|
+
import Papa from "papaparse";
|
|
436
|
+
function parseCSV(text) {
|
|
437
|
+
const result = Papa.parse(text, {
|
|
438
|
+
header: true,
|
|
439
|
+
skipEmptyLines: true,
|
|
440
|
+
transformHeader: (header) => header.trim(),
|
|
441
|
+
transform: (value) => value.trim()
|
|
442
|
+
});
|
|
443
|
+
if (result.errors.length > 0) {
|
|
444
|
+
console.warn("CSV parsing warnings:", result.errors);
|
|
445
|
+
}
|
|
446
|
+
const headers = result.meta.fields || [];
|
|
447
|
+
const rows = result.data;
|
|
448
|
+
return { headers, rows };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/loaders/data-loader.ts
|
|
452
|
+
async function loadGTFSData(db, files, skipFiles, onProgress) {
|
|
453
|
+
const fileToSchema = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const schema of GTFS_SCHEMA) {
|
|
455
|
+
fileToSchema.set(`${schema.name}.txt`, schema);
|
|
456
|
+
}
|
|
457
|
+
const skipSet = new Set(skipFiles?.map((f) => f.toLowerCase()) || []);
|
|
458
|
+
const filePriority = [
|
|
459
|
+
"agency.txt",
|
|
460
|
+
"feed_info.txt",
|
|
461
|
+
"attributions.txt",
|
|
462
|
+
"levels.txt",
|
|
463
|
+
"routes.txt",
|
|
464
|
+
"calendar.txt",
|
|
465
|
+
"calendar_dates.txt",
|
|
466
|
+
"fare_attributes.txt",
|
|
467
|
+
"fare_rules.txt",
|
|
468
|
+
"stops.txt",
|
|
469
|
+
"pathways.txt",
|
|
470
|
+
"transfers.txt",
|
|
471
|
+
"trips.txt",
|
|
472
|
+
"frequencies.txt",
|
|
473
|
+
"shapes.txt",
|
|
474
|
+
"stop_times.txt"
|
|
475
|
+
// Largest file - process last
|
|
476
|
+
];
|
|
477
|
+
const sortedFiles = [];
|
|
478
|
+
for (const priorityFile of filePriority) {
|
|
479
|
+
if (files[priorityFile]) {
|
|
480
|
+
sortedFiles.push([priorityFile, files[priorityFile]]);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
484
|
+
if (!filePriority.includes(fileName)) {
|
|
485
|
+
sortedFiles.push([fileName, content]);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let totalRows = 0;
|
|
489
|
+
const fileRowCounts = /* @__PURE__ */ new Map();
|
|
490
|
+
for (const [fileName, content] of sortedFiles) {
|
|
491
|
+
const schema = fileToSchema.get(fileName);
|
|
492
|
+
if (schema && !skipSet.has(fileName.toLowerCase())) {
|
|
493
|
+
const { rows } = parseCSV(content);
|
|
494
|
+
fileRowCounts.set(fileName, rows.length);
|
|
495
|
+
totalRows += rows.length;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let rowsProcessed = 0;
|
|
499
|
+
let filesCompleted = 0;
|
|
500
|
+
for (const [fileName, content] of sortedFiles) {
|
|
501
|
+
const schema = fileToSchema.get(fileName);
|
|
502
|
+
if (!schema) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (skipSet.has(fileName.toLowerCase())) {
|
|
506
|
+
console.log(`Skipping import of ${fileName} (table ${schema.name} created but empty)`);
|
|
507
|
+
filesCompleted++;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const fileRows = fileRowCounts.get(fileName) || 0;
|
|
511
|
+
onProgress?.({
|
|
512
|
+
phase: "inserting_data",
|
|
513
|
+
currentFile: fileName,
|
|
514
|
+
filesCompleted,
|
|
515
|
+
totalFiles: sortedFiles.length,
|
|
516
|
+
rowsProcessed,
|
|
517
|
+
totalRows,
|
|
518
|
+
percentComplete: 40 + Math.floor(rowsProcessed / totalRows * 35),
|
|
519
|
+
message: `Loading ${fileName} (${fileRows.toLocaleString()} rows)`
|
|
520
|
+
});
|
|
521
|
+
await loadTableData(db, schema, content, (processedInFile) => {
|
|
522
|
+
const currentProgress = rowsProcessed + processedInFile;
|
|
523
|
+
onProgress?.({
|
|
524
|
+
phase: "inserting_data",
|
|
525
|
+
currentFile: fileName,
|
|
526
|
+
filesCompleted,
|
|
527
|
+
totalFiles: sortedFiles.length,
|
|
528
|
+
rowsProcessed: currentProgress,
|
|
529
|
+
totalRows,
|
|
530
|
+
percentComplete: 40 + Math.floor(currentProgress / totalRows * 35),
|
|
531
|
+
message: `Loading ${fileName} (${processedInFile.toLocaleString()}/${fileRows.toLocaleString()} rows)`
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
rowsProcessed += fileRows;
|
|
535
|
+
filesCompleted++;
|
|
536
|
+
onProgress?.({
|
|
537
|
+
phase: "inserting_data",
|
|
538
|
+
currentFile: null,
|
|
539
|
+
filesCompleted,
|
|
540
|
+
totalFiles: sortedFiles.length,
|
|
541
|
+
rowsProcessed,
|
|
542
|
+
totalRows,
|
|
543
|
+
percentComplete: 40 + Math.floor(rowsProcessed / totalRows * 35),
|
|
544
|
+
message: `Completed ${fileName}`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function loadTableData(db, schema, csvContent, onProgress) {
|
|
549
|
+
const { headers, rows } = parseCSV(csvContent);
|
|
550
|
+
if (rows.length === 0) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const columns = headers.filter((h) => schema.columns.some((c) => c.name === h));
|
|
554
|
+
if (columns.length === 0) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const BATCH_SIZE = 1e3;
|
|
558
|
+
let rowsProcessed = 0;
|
|
559
|
+
db.run("BEGIN TRANSACTION");
|
|
560
|
+
try {
|
|
561
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
562
|
+
const batchRows = rows.slice(i, Math.min(i + BATCH_SIZE, rows.length));
|
|
563
|
+
const placeholders = batchRows.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
564
|
+
const insertSQL = `INSERT INTO ${schema.name} (${columns.join(", ")}) VALUES ${placeholders}`;
|
|
565
|
+
const allValues = [];
|
|
566
|
+
for (const row of batchRows) {
|
|
567
|
+
for (const col of columns) {
|
|
568
|
+
const value = row[col];
|
|
569
|
+
allValues.push(value === null || value === void 0 || value === "" ? null : value);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const stmt = db.prepare(insertSQL);
|
|
573
|
+
try {
|
|
574
|
+
stmt.run(allValues);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error(`Error inserting batch into ${schema.name}:`, error);
|
|
577
|
+
console.error("Batch size:", batchRows.length);
|
|
578
|
+
throw error;
|
|
579
|
+
} finally {
|
|
580
|
+
stmt.free();
|
|
581
|
+
}
|
|
582
|
+
rowsProcessed += batchRows.length;
|
|
583
|
+
onProgress?.(rowsProcessed);
|
|
584
|
+
}
|
|
585
|
+
db.run("COMMIT");
|
|
586
|
+
} catch (error) {
|
|
587
|
+
try {
|
|
588
|
+
db.run("ROLLBACK");
|
|
589
|
+
} catch (rollbackError) {
|
|
590
|
+
console.error("Error rolling back transaction:", rollbackError);
|
|
591
|
+
}
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/schema/gtfs-rt-schema.ts
|
|
597
|
+
function createRealtimeTables(db) {
|
|
598
|
+
db.run(`
|
|
599
|
+
CREATE TABLE IF NOT EXISTS rt_alerts (
|
|
600
|
+
id TEXT PRIMARY KEY,
|
|
601
|
+
active_period TEXT, -- JSON array of TimeRange objects
|
|
602
|
+
informed_entity TEXT, -- JSON array of EntitySelector objects
|
|
603
|
+
cause INTEGER,
|
|
604
|
+
effect INTEGER,
|
|
605
|
+
url TEXT, -- JSON TranslatedString
|
|
606
|
+
header_text TEXT, -- JSON TranslatedString
|
|
607
|
+
description_text TEXT, -- JSON TranslatedString
|
|
608
|
+
rt_last_updated INTEGER NOT NULL
|
|
609
|
+
)
|
|
610
|
+
`);
|
|
611
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_alerts_updated ON rt_alerts(rt_last_updated)");
|
|
612
|
+
db.run(`
|
|
613
|
+
CREATE TABLE IF NOT EXISTS rt_vehicle_positions (
|
|
614
|
+
trip_id TEXT PRIMARY KEY,
|
|
615
|
+
route_id TEXT,
|
|
616
|
+
vehicle_id TEXT,
|
|
617
|
+
vehicle_label TEXT,
|
|
618
|
+
vehicle_license_plate TEXT,
|
|
619
|
+
latitude REAL,
|
|
620
|
+
longitude REAL,
|
|
621
|
+
bearing REAL,
|
|
622
|
+
odometer REAL,
|
|
623
|
+
speed REAL,
|
|
624
|
+
current_stop_sequence INTEGER,
|
|
625
|
+
stop_id TEXT,
|
|
626
|
+
current_status INTEGER,
|
|
627
|
+
timestamp INTEGER,
|
|
628
|
+
congestion_level INTEGER,
|
|
629
|
+
occupancy_status INTEGER,
|
|
630
|
+
rt_last_updated INTEGER NOT NULL
|
|
631
|
+
)
|
|
632
|
+
`);
|
|
633
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_vehicle_positions_updated ON rt_vehicle_positions(rt_last_updated)");
|
|
634
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_vehicle_positions_route ON rt_vehicle_positions(route_id)");
|
|
635
|
+
db.run(`
|
|
636
|
+
CREATE TABLE IF NOT EXISTS rt_trip_updates (
|
|
637
|
+
trip_id TEXT PRIMARY KEY,
|
|
638
|
+
route_id TEXT,
|
|
639
|
+
vehicle_id TEXT,
|
|
640
|
+
vehicle_label TEXT,
|
|
641
|
+
vehicle_license_plate TEXT,
|
|
642
|
+
timestamp INTEGER,
|
|
643
|
+
delay INTEGER,
|
|
644
|
+
schedule_relationship INTEGER,
|
|
645
|
+
rt_last_updated INTEGER NOT NULL
|
|
646
|
+
)
|
|
647
|
+
`);
|
|
648
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_trip_updates_updated ON rt_trip_updates(rt_last_updated)");
|
|
649
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_trip_updates_route ON rt_trip_updates(route_id)");
|
|
650
|
+
db.run(`
|
|
651
|
+
CREATE TABLE IF NOT EXISTS rt_stop_time_updates (
|
|
652
|
+
trip_id TEXT NOT NULL,
|
|
653
|
+
stop_sequence INTEGER,
|
|
654
|
+
stop_id TEXT,
|
|
655
|
+
arrival_delay INTEGER,
|
|
656
|
+
arrival_time INTEGER,
|
|
657
|
+
arrival_uncertainty INTEGER,
|
|
658
|
+
departure_delay INTEGER,
|
|
659
|
+
departure_time INTEGER,
|
|
660
|
+
departure_uncertainty INTEGER,
|
|
661
|
+
schedule_relationship INTEGER,
|
|
662
|
+
rt_last_updated INTEGER NOT NULL,
|
|
663
|
+
PRIMARY KEY (trip_id, stop_sequence),
|
|
664
|
+
FOREIGN KEY (trip_id) REFERENCES rt_trip_updates(trip_id) ON DELETE CASCADE
|
|
665
|
+
)
|
|
666
|
+
`);
|
|
667
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_stop_time_updates_updated ON rt_stop_time_updates(rt_last_updated)");
|
|
668
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_rt_stop_time_updates_stop ON rt_stop_time_updates(stop_id)");
|
|
669
|
+
}
|
|
670
|
+
function clearRealtimeData(db) {
|
|
671
|
+
db.run("DELETE FROM rt_alerts");
|
|
672
|
+
db.run("DELETE FROM rt_vehicle_positions");
|
|
673
|
+
db.run("DELETE FROM rt_trip_updates");
|
|
674
|
+
db.run("DELETE FROM rt_stop_time_updates");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/loaders/gtfs-rt-loader.ts
|
|
678
|
+
import protobuf from "protobufjs";
|
|
679
|
+
var GTFS_RT_PROTO = `
|
|
680
|
+
syntax = "proto2";
|
|
681
|
+
option java_package = "com.google.transit.realtime";
|
|
682
|
+
package transit_realtime;
|
|
683
|
+
|
|
684
|
+
message FeedMessage {
|
|
685
|
+
required FeedHeader header = 1;
|
|
686
|
+
repeated FeedEntity entity = 2;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
message FeedHeader {
|
|
690
|
+
required string gtfs_realtime_version = 1;
|
|
691
|
+
enum Incrementality {
|
|
692
|
+
FULL_DATASET = 0;
|
|
693
|
+
DIFFERENTIAL = 1;
|
|
694
|
+
}
|
|
695
|
+
optional Incrementality incrementality = 2 [default = FULL_DATASET];
|
|
696
|
+
optional uint64 timestamp = 3;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
message FeedEntity {
|
|
700
|
+
required string id = 1;
|
|
701
|
+
optional bool is_deleted = 2 [default = false];
|
|
702
|
+
optional TripUpdate trip_update = 3;
|
|
703
|
+
optional VehiclePosition vehicle = 4;
|
|
704
|
+
optional Alert alert = 5;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
message TripUpdate {
|
|
708
|
+
required TripDescriptor trip = 1;
|
|
709
|
+
optional VehicleDescriptor vehicle = 3;
|
|
710
|
+
repeated StopTimeUpdate stop_time_update = 2;
|
|
711
|
+
optional uint64 timestamp = 4;
|
|
712
|
+
optional int32 delay = 5;
|
|
713
|
+
|
|
714
|
+
message StopTimeEvent {
|
|
715
|
+
optional int32 delay = 1;
|
|
716
|
+
optional int64 time = 2;
|
|
717
|
+
optional int32 uncertainty = 3;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
message StopTimeUpdate {
|
|
721
|
+
optional uint32 stop_sequence = 1;
|
|
722
|
+
optional string stop_id = 4;
|
|
723
|
+
optional StopTimeEvent arrival = 2;
|
|
724
|
+
optional StopTimeEvent departure = 3;
|
|
725
|
+
enum ScheduleRelationship {
|
|
726
|
+
SCHEDULED = 0;
|
|
727
|
+
SKIPPED = 1;
|
|
728
|
+
NO_DATA = 2;
|
|
729
|
+
}
|
|
730
|
+
optional ScheduleRelationship schedule_relationship = 5 [default = SCHEDULED];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
message VehiclePosition {
|
|
735
|
+
optional TripDescriptor trip = 1;
|
|
736
|
+
optional VehicleDescriptor vehicle = 8;
|
|
737
|
+
optional Position position = 2;
|
|
738
|
+
optional uint32 current_stop_sequence = 3;
|
|
739
|
+
optional string stop_id = 7;
|
|
740
|
+
enum VehicleStopStatus {
|
|
741
|
+
INCOMING_AT = 0;
|
|
742
|
+
STOPPED_AT = 1;
|
|
743
|
+
IN_TRANSIT_TO = 2;
|
|
744
|
+
}
|
|
745
|
+
optional VehicleStopStatus current_status = 4 [default = IN_TRANSIT_TO];
|
|
746
|
+
optional uint64 timestamp = 5;
|
|
747
|
+
enum CongestionLevel {
|
|
748
|
+
UNKNOWN_CONGESTION_LEVEL = 0;
|
|
749
|
+
RUNNING_SMOOTHLY = 1;
|
|
750
|
+
STOP_AND_GO = 2;
|
|
751
|
+
CONGESTION = 3;
|
|
752
|
+
SEVERE_CONGESTION = 4;
|
|
753
|
+
}
|
|
754
|
+
optional CongestionLevel congestion_level = 6;
|
|
755
|
+
enum OccupancyStatus {
|
|
756
|
+
EMPTY = 0;
|
|
757
|
+
MANY_SEATS_AVAILABLE = 1;
|
|
758
|
+
FEW_SEATS_AVAILABLE = 2;
|
|
759
|
+
STANDING_ROOM_ONLY = 3;
|
|
760
|
+
CRUSHED_STANDING_ROOM_ONLY = 4;
|
|
761
|
+
FULL = 5;
|
|
762
|
+
NOT_ACCEPTING_PASSENGERS = 6;
|
|
763
|
+
}
|
|
764
|
+
optional OccupancyStatus occupancy_status = 9;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
message Alert {
|
|
768
|
+
repeated TimeRange active_period = 1;
|
|
769
|
+
repeated EntitySelector informed_entity = 5;
|
|
770
|
+
|
|
771
|
+
enum Cause {
|
|
772
|
+
UNKNOWN_CAUSE = 1;
|
|
773
|
+
OTHER_CAUSE = 2;
|
|
774
|
+
TECHNICAL_PROBLEM = 3;
|
|
775
|
+
STRIKE = 4;
|
|
776
|
+
DEMONSTRATION = 5;
|
|
777
|
+
ACCIDENT = 6;
|
|
778
|
+
HOLIDAY = 7;
|
|
779
|
+
WEATHER = 8;
|
|
780
|
+
MAINTENANCE = 9;
|
|
781
|
+
CONSTRUCTION = 10;
|
|
782
|
+
POLICE_ACTIVITY = 11;
|
|
783
|
+
MEDICAL_EMERGENCY = 12;
|
|
784
|
+
}
|
|
785
|
+
optional Cause cause = 6 [default = UNKNOWN_CAUSE];
|
|
786
|
+
|
|
787
|
+
enum Effect {
|
|
788
|
+
NO_SERVICE = 1;
|
|
789
|
+
REDUCED_SERVICE = 2;
|
|
790
|
+
SIGNIFICANT_DELAYS = 3;
|
|
791
|
+
DETOUR = 4;
|
|
792
|
+
ADDITIONAL_SERVICE = 5;
|
|
793
|
+
MODIFIED_SERVICE = 6;
|
|
794
|
+
OTHER_EFFECT = 7;
|
|
795
|
+
UNKNOWN_EFFECT = 8;
|
|
796
|
+
STOP_MOVED = 9;
|
|
797
|
+
NO_EFFECT = 10;
|
|
798
|
+
ACCESSIBILITY_ISSUE = 11;
|
|
799
|
+
}
|
|
800
|
+
optional Effect effect = 7 [default = UNKNOWN_EFFECT];
|
|
801
|
+
optional TranslatedString url = 8;
|
|
802
|
+
optional TranslatedString header_text = 10;
|
|
803
|
+
optional TranslatedString description_text = 11;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
message TimeRange {
|
|
807
|
+
optional uint64 start = 1;
|
|
808
|
+
optional uint64 end = 2;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
message Position {
|
|
812
|
+
required float latitude = 1;
|
|
813
|
+
required float longitude = 2;
|
|
814
|
+
optional float bearing = 3;
|
|
815
|
+
optional double odometer = 4;
|
|
816
|
+
optional float speed = 5;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
message TripDescriptor {
|
|
820
|
+
optional string trip_id = 1;
|
|
821
|
+
optional string route_id = 5;
|
|
822
|
+
optional uint32 direction_id = 6;
|
|
823
|
+
optional string start_time = 2;
|
|
824
|
+
optional string start_date = 3;
|
|
825
|
+
enum ScheduleRelationship {
|
|
826
|
+
SCHEDULED = 0;
|
|
827
|
+
ADDED = 1;
|
|
828
|
+
UNSCHEDULED = 2;
|
|
829
|
+
CANCELED = 3;
|
|
830
|
+
}
|
|
831
|
+
optional ScheduleRelationship schedule_relationship = 4;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
message VehicleDescriptor {
|
|
835
|
+
optional string id = 1;
|
|
836
|
+
optional string label = 2;
|
|
837
|
+
optional string license_plate = 3;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
message EntitySelector {
|
|
841
|
+
optional string agency_id = 1;
|
|
842
|
+
optional string route_id = 2;
|
|
843
|
+
optional int32 route_type = 3;
|
|
844
|
+
optional TripDescriptor trip = 4;
|
|
845
|
+
optional string stop_id = 5;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
message TranslatedString {
|
|
849
|
+
message Translation {
|
|
850
|
+
required string text = 1;
|
|
851
|
+
optional string language = 2;
|
|
852
|
+
}
|
|
853
|
+
repeated Translation translation = 1;
|
|
854
|
+
}
|
|
855
|
+
`;
|
|
856
|
+
async function fetchProtobuf(source) {
|
|
857
|
+
const isUrl = source.startsWith("http://") || source.startsWith("https://");
|
|
858
|
+
if (isUrl) {
|
|
859
|
+
const response2 = await fetch(source, {
|
|
860
|
+
headers: {
|
|
861
|
+
"Accept": "application/x-protobuf, application/octet-stream"
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
if (!response2.ok) {
|
|
865
|
+
throw new Error(`Failed to fetch GTFS-RT feed from ${source}: ${response2.status} ${response2.statusText}`);
|
|
866
|
+
}
|
|
867
|
+
const arrayBuffer2 = await response2.arrayBuffer();
|
|
868
|
+
return new Uint8Array(arrayBuffer2);
|
|
869
|
+
}
|
|
870
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
871
|
+
if (isNode) {
|
|
872
|
+
try {
|
|
873
|
+
const fs = await import("fs");
|
|
874
|
+
const buffer = await fs.promises.readFile(source);
|
|
875
|
+
return new Uint8Array(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
|
876
|
+
} catch (error) {
|
|
877
|
+
throw new Error(`Failed to read GTFS-RT file from ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const response = await fetch(source, {
|
|
881
|
+
headers: {
|
|
882
|
+
"Accept": "application/x-protobuf, application/octet-stream"
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
if (!response.ok) {
|
|
886
|
+
throw new Error(`Failed to fetch GTFS-RT feed from ${source}: ${response.status} ${response.statusText}`);
|
|
887
|
+
}
|
|
888
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
889
|
+
return new Uint8Array(arrayBuffer);
|
|
890
|
+
}
|
|
891
|
+
var gtfsRtRoot = null;
|
|
892
|
+
function loadGtfsRtProto() {
|
|
893
|
+
if (!gtfsRtRoot) {
|
|
894
|
+
gtfsRtRoot = protobuf.parse(GTFS_RT_PROTO).root;
|
|
895
|
+
}
|
|
896
|
+
return gtfsRtRoot;
|
|
897
|
+
}
|
|
898
|
+
function camelToSnake(str) {
|
|
899
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
900
|
+
}
|
|
901
|
+
function convertKeysToSnakeCase(obj) {
|
|
902
|
+
if (obj === null || obj === void 0) {
|
|
903
|
+
return obj;
|
|
904
|
+
}
|
|
905
|
+
if (Array.isArray(obj)) {
|
|
906
|
+
return obj.map(convertKeysToSnakeCase);
|
|
907
|
+
}
|
|
908
|
+
if (typeof obj === "object") {
|
|
909
|
+
const result = {};
|
|
910
|
+
for (const key in obj) {
|
|
911
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
912
|
+
const snakeKey = camelToSnake(key);
|
|
913
|
+
result[snakeKey] = convertKeysToSnakeCase(obj[key]);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
return obj;
|
|
919
|
+
}
|
|
920
|
+
function parseTranslatedString(ts) {
|
|
921
|
+
if (!ts || !ts.translation || ts.translation.length === 0) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
return JSON.stringify({
|
|
925
|
+
translation: ts.translation.map((t) => ({
|
|
926
|
+
text: t.text,
|
|
927
|
+
language: t.language || void 0
|
|
928
|
+
}))
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
function insertAlerts(db, alerts, timestamp) {
|
|
932
|
+
const stmt = db.prepare(`
|
|
933
|
+
INSERT OR REPLACE INTO rt_alerts (
|
|
934
|
+
id, active_period, informed_entity, cause, effect,
|
|
935
|
+
url, header_text, description_text, rt_last_updated
|
|
936
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
937
|
+
`);
|
|
938
|
+
for (const alert of alerts) {
|
|
939
|
+
const activePeriodSnake = convertKeysToSnakeCase(alert.activePeriod || []);
|
|
940
|
+
const informedEntitySnake = convertKeysToSnakeCase(alert.informedEntity || []);
|
|
941
|
+
stmt.run([
|
|
942
|
+
alert.id,
|
|
943
|
+
JSON.stringify(activePeriodSnake),
|
|
944
|
+
JSON.stringify(informedEntitySnake),
|
|
945
|
+
alert.cause || null,
|
|
946
|
+
alert.effect || null,
|
|
947
|
+
parseTranslatedString(alert.url),
|
|
948
|
+
parseTranslatedString(alert.headerText),
|
|
949
|
+
parseTranslatedString(alert.descriptionText),
|
|
950
|
+
timestamp
|
|
951
|
+
]);
|
|
952
|
+
}
|
|
953
|
+
stmt.free();
|
|
954
|
+
}
|
|
955
|
+
function insertVehiclePositions(db, positions, timestamp) {
|
|
956
|
+
const stmt = db.prepare(`
|
|
957
|
+
INSERT OR REPLACE INTO rt_vehicle_positions (
|
|
958
|
+
trip_id, route_id, vehicle_id, vehicle_label, vehicle_license_plate,
|
|
959
|
+
latitude, longitude, bearing, odometer, speed,
|
|
960
|
+
current_stop_sequence, stop_id, current_status, timestamp,
|
|
961
|
+
congestion_level, occupancy_status, rt_last_updated
|
|
962
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
963
|
+
`);
|
|
964
|
+
for (const vp of positions) {
|
|
965
|
+
const trip = vp.trip;
|
|
966
|
+
if (!trip || !trip.tripId) continue;
|
|
967
|
+
stmt.run([
|
|
968
|
+
trip.tripId,
|
|
969
|
+
trip.routeId || null,
|
|
970
|
+
vp.vehicle?.id || null,
|
|
971
|
+
vp.vehicle?.label || null,
|
|
972
|
+
vp.vehicle?.licensePlate || null,
|
|
973
|
+
vp.position?.latitude || null,
|
|
974
|
+
vp.position?.longitude || null,
|
|
975
|
+
vp.position?.bearing || null,
|
|
976
|
+
vp.position?.odometer || null,
|
|
977
|
+
vp.position?.speed || null,
|
|
978
|
+
vp.currentStopSequence || null,
|
|
979
|
+
vp.stopId || null,
|
|
980
|
+
vp.currentStatus || null,
|
|
981
|
+
vp.timestamp || null,
|
|
982
|
+
vp.congestionLevel || null,
|
|
983
|
+
vp.occupancyStatus || null,
|
|
984
|
+
timestamp
|
|
985
|
+
]);
|
|
986
|
+
}
|
|
987
|
+
stmt.free();
|
|
988
|
+
}
|
|
989
|
+
function insertTripUpdates(db, updates, timestamp) {
|
|
990
|
+
const tripStmt = db.prepare(`
|
|
991
|
+
INSERT OR REPLACE INTO rt_trip_updates (
|
|
992
|
+
trip_id, route_id, vehicle_id, vehicle_label, vehicle_license_plate,
|
|
993
|
+
timestamp, delay, schedule_relationship, rt_last_updated
|
|
994
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
995
|
+
`);
|
|
996
|
+
const stopTimeStmt = db.prepare(`
|
|
997
|
+
INSERT OR REPLACE INTO rt_stop_time_updates (
|
|
998
|
+
trip_id, stop_sequence, stop_id,
|
|
999
|
+
arrival_delay, arrival_time, arrival_uncertainty,
|
|
1000
|
+
departure_delay, departure_time, departure_uncertainty,
|
|
1001
|
+
schedule_relationship, rt_last_updated
|
|
1002
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1003
|
+
`);
|
|
1004
|
+
for (const tu of updates) {
|
|
1005
|
+
const trip = tu.trip;
|
|
1006
|
+
if (!trip || !trip.tripId) continue;
|
|
1007
|
+
tripStmt.run([
|
|
1008
|
+
trip.tripId,
|
|
1009
|
+
trip.routeId || null,
|
|
1010
|
+
tu.vehicle?.id || null,
|
|
1011
|
+
tu.vehicle?.label || null,
|
|
1012
|
+
tu.vehicle?.licensePlate || null,
|
|
1013
|
+
tu.timestamp || null,
|
|
1014
|
+
tu.delay || null,
|
|
1015
|
+
trip.scheduleRelationship || null,
|
|
1016
|
+
timestamp
|
|
1017
|
+
]);
|
|
1018
|
+
if (tu.stopTimeUpdate) {
|
|
1019
|
+
for (const stu of tu.stopTimeUpdate) {
|
|
1020
|
+
stopTimeStmt.run([
|
|
1021
|
+
trip.tripId,
|
|
1022
|
+
stu.stopSequence || null,
|
|
1023
|
+
stu.stopId || null,
|
|
1024
|
+
stu.arrival?.delay || null,
|
|
1025
|
+
stu.arrival?.time || null,
|
|
1026
|
+
stu.arrival?.uncertainty || null,
|
|
1027
|
+
stu.departure?.delay || null,
|
|
1028
|
+
stu.departure?.time || null,
|
|
1029
|
+
stu.departure?.uncertainty || null,
|
|
1030
|
+
stu.scheduleRelationship || null,
|
|
1031
|
+
timestamp
|
|
1032
|
+
]);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
tripStmt.free();
|
|
1037
|
+
stopTimeStmt.free();
|
|
1038
|
+
}
|
|
1039
|
+
async function loadRealtimeData(db, feedUrls) {
|
|
1040
|
+
const root = loadGtfsRtProto();
|
|
1041
|
+
const FeedMessage = root.lookupType("transit_realtime.FeedMessage");
|
|
1042
|
+
const fetchPromises = feedUrls.map(async (url) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const data = await fetchProtobuf(url);
|
|
1045
|
+
const message = FeedMessage.decode(data);
|
|
1046
|
+
return FeedMessage.toObject(message, {
|
|
1047
|
+
longs: Number,
|
|
1048
|
+
enums: Number,
|
|
1049
|
+
bytes: String,
|
|
1050
|
+
defaults: false,
|
|
1051
|
+
arrays: true,
|
|
1052
|
+
objects: true,
|
|
1053
|
+
oneofs: true
|
|
1054
|
+
});
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
throw new Error(`Failed to fetch or parse GTFS-RT feed from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
const feeds = await Promise.all(fetchPromises);
|
|
1060
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1061
|
+
const allAlerts = [];
|
|
1062
|
+
const allVehiclePositions = [];
|
|
1063
|
+
const allTripUpdates = [];
|
|
1064
|
+
for (const feed of feeds) {
|
|
1065
|
+
if (!feed.entity) continue;
|
|
1066
|
+
for (const entity of feed.entity) {
|
|
1067
|
+
if (entity.alert) {
|
|
1068
|
+
allAlerts.push({ id: entity.id, ...entity.alert });
|
|
1069
|
+
}
|
|
1070
|
+
if (entity.vehicle) {
|
|
1071
|
+
allVehiclePositions.push(entity.vehicle);
|
|
1072
|
+
}
|
|
1073
|
+
if (entity.tripUpdate) {
|
|
1074
|
+
allTripUpdates.push(entity.tripUpdate);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
db.run("BEGIN TRANSACTION");
|
|
1079
|
+
try {
|
|
1080
|
+
db.run("DELETE FROM rt_stop_time_updates");
|
|
1081
|
+
db.run("DELETE FROM rt_trip_updates");
|
|
1082
|
+
db.run("DELETE FROM rt_vehicle_positions");
|
|
1083
|
+
db.run("DELETE FROM rt_alerts");
|
|
1084
|
+
if (allAlerts.length > 0) {
|
|
1085
|
+
insertAlerts(db, allAlerts, now);
|
|
1086
|
+
}
|
|
1087
|
+
if (allVehiclePositions.length > 0) {
|
|
1088
|
+
insertVehiclePositions(db, allVehiclePositions, now);
|
|
1089
|
+
}
|
|
1090
|
+
if (allTripUpdates.length > 0) {
|
|
1091
|
+
insertTripUpdates(db, allTripUpdates, now);
|
|
1092
|
+
}
|
|
1093
|
+
db.run("COMMIT");
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
db.run("ROLLBACK");
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/cache/checksum.ts
|
|
1101
|
+
async function getCrypto() {
|
|
1102
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
1103
|
+
return crypto;
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof globalThis !== "undefined") {
|
|
1106
|
+
try {
|
|
1107
|
+
const { webcrypto } = await import("crypto");
|
|
1108
|
+
return webcrypto;
|
|
1109
|
+
} catch {
|
|
1110
|
+
throw new Error("Web Crypto API not available");
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
throw new Error("Crypto not available in this environment");
|
|
1114
|
+
}
|
|
1115
|
+
async function computeChecksum(data) {
|
|
1116
|
+
const cryptoInstance = await getCrypto();
|
|
1117
|
+
const bufferSource = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1118
|
+
const hashBuffer = await cryptoInstance.subtle.digest("SHA-256", bufferSource);
|
|
1119
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1120
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1121
|
+
return hashHex;
|
|
1122
|
+
}
|
|
1123
|
+
async function computeZipChecksum(zipData) {
|
|
1124
|
+
return computeChecksum(zipData);
|
|
1125
|
+
}
|
|
1126
|
+
function generateCacheKey(checksum, libVersion, dataVersion, filesize, source, skipFiles) {
|
|
1127
|
+
let key = `v${libVersion}_d${dataVersion}_${filesize}_${checksum}`;
|
|
1128
|
+
if (source) {
|
|
1129
|
+
const filename = source.split("/").pop() || source;
|
|
1130
|
+
const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1131
|
+
key += `_${sanitized}`;
|
|
1132
|
+
}
|
|
1133
|
+
if (skipFiles && skipFiles.length > 0) {
|
|
1134
|
+
const sortedSkips = [...skipFiles].sort();
|
|
1135
|
+
const skipsSuffix = sortedSkips.join(",").replace(/\.txt/g, "");
|
|
1136
|
+
key += `_skip-${skipsSuffix}`;
|
|
1137
|
+
}
|
|
1138
|
+
return key;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/gtfs-sqljs.ts
|
|
1142
|
+
init_utils();
|
|
1143
|
+
|
|
1144
|
+
// src/queries/agencies.ts
|
|
1145
|
+
function getAgencies(db, filters = {}) {
|
|
1146
|
+
const { agencyId, limit } = filters;
|
|
1147
|
+
const conditions = [];
|
|
1148
|
+
const params = [];
|
|
1149
|
+
if (agencyId) {
|
|
1150
|
+
const agencyIds = Array.isArray(agencyId) ? agencyId : [agencyId];
|
|
1151
|
+
if (agencyIds.length > 0) {
|
|
1152
|
+
const placeholders = agencyIds.map(() => "?").join(", ");
|
|
1153
|
+
conditions.push(`agency_id IN (${placeholders})`);
|
|
1154
|
+
params.push(...agencyIds);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
let sql = "SELECT * FROM agency";
|
|
1158
|
+
if (conditions.length > 0) {
|
|
1159
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1160
|
+
}
|
|
1161
|
+
sql += " ORDER BY agency_name";
|
|
1162
|
+
if (limit) {
|
|
1163
|
+
sql += " LIMIT ?";
|
|
1164
|
+
params.push(limit);
|
|
1165
|
+
}
|
|
1166
|
+
const stmt = db.prepare(sql);
|
|
1167
|
+
if (params.length > 0) {
|
|
1168
|
+
stmt.bind(params);
|
|
1169
|
+
}
|
|
1170
|
+
const agencies = [];
|
|
1171
|
+
while (stmt.step()) {
|
|
1172
|
+
const row = stmt.getAsObject();
|
|
1173
|
+
agencies.push(rowToAgency(row));
|
|
1174
|
+
}
|
|
1175
|
+
stmt.free();
|
|
1176
|
+
return agencies;
|
|
1177
|
+
}
|
|
1178
|
+
function rowToAgency(row) {
|
|
1179
|
+
return {
|
|
1180
|
+
agency_id: String(row.agency_id),
|
|
1181
|
+
agency_name: String(row.agency_name),
|
|
1182
|
+
agency_url: String(row.agency_url),
|
|
1183
|
+
agency_timezone: String(row.agency_timezone),
|
|
1184
|
+
agency_lang: row.agency_lang ? String(row.agency_lang) : void 0,
|
|
1185
|
+
agency_phone: row.agency_phone ? String(row.agency_phone) : void 0,
|
|
1186
|
+
agency_fare_url: row.agency_fare_url ? String(row.agency_fare_url) : void 0,
|
|
1187
|
+
agency_email: row.agency_email ? String(row.agency_email) : void 0
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/queries/stops.ts
|
|
1192
|
+
function getStops(db, filters = {}) {
|
|
1193
|
+
const { stopId, stopCode, name, tripId, limit } = filters;
|
|
1194
|
+
if (tripId) {
|
|
1195
|
+
const tripIds = Array.isArray(tripId) ? tripId : [tripId];
|
|
1196
|
+
if (tripIds.length === 0) return [];
|
|
1197
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1198
|
+
const stmt2 = db.prepare(`
|
|
1199
|
+
SELECT s.* FROM stops s
|
|
1200
|
+
INNER JOIN stop_times st ON s.stop_id = st.stop_id
|
|
1201
|
+
WHERE st.trip_id IN (${placeholders})
|
|
1202
|
+
ORDER BY st.stop_sequence
|
|
1203
|
+
`);
|
|
1204
|
+
stmt2.bind(tripIds);
|
|
1205
|
+
const stops2 = [];
|
|
1206
|
+
while (stmt2.step()) {
|
|
1207
|
+
const row = stmt2.getAsObject();
|
|
1208
|
+
stops2.push(rowToStop(row));
|
|
1209
|
+
}
|
|
1210
|
+
stmt2.free();
|
|
1211
|
+
return stops2;
|
|
1212
|
+
}
|
|
1213
|
+
const conditions = [];
|
|
1214
|
+
const params = [];
|
|
1215
|
+
if (stopId) {
|
|
1216
|
+
const stopIds = Array.isArray(stopId) ? stopId : [stopId];
|
|
1217
|
+
if (stopIds.length > 0) {
|
|
1218
|
+
const placeholders = stopIds.map(() => "?").join(", ");
|
|
1219
|
+
conditions.push(`stop_id IN (${placeholders})`);
|
|
1220
|
+
params.push(...stopIds);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (stopCode) {
|
|
1224
|
+
const stopCodes = Array.isArray(stopCode) ? stopCode : [stopCode];
|
|
1225
|
+
if (stopCodes.length > 0) {
|
|
1226
|
+
const placeholders = stopCodes.map(() => "?").join(", ");
|
|
1227
|
+
conditions.push(`stop_code IN (${placeholders})`);
|
|
1228
|
+
params.push(...stopCodes);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (name) {
|
|
1232
|
+
conditions.push("stop_name LIKE ?");
|
|
1233
|
+
params.push(`%${name}%`);
|
|
1234
|
+
}
|
|
1235
|
+
let sql = "SELECT * FROM stops";
|
|
1236
|
+
if (conditions.length > 0) {
|
|
1237
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1238
|
+
}
|
|
1239
|
+
sql += " ORDER BY stop_name";
|
|
1240
|
+
if (limit) {
|
|
1241
|
+
sql += " LIMIT ?";
|
|
1242
|
+
params.push(limit);
|
|
1243
|
+
}
|
|
1244
|
+
const stmt = db.prepare(sql);
|
|
1245
|
+
if (params.length > 0) {
|
|
1246
|
+
stmt.bind(params);
|
|
1247
|
+
}
|
|
1248
|
+
const stops = [];
|
|
1249
|
+
while (stmt.step()) {
|
|
1250
|
+
const row = stmt.getAsObject();
|
|
1251
|
+
stops.push(rowToStop(row));
|
|
1252
|
+
}
|
|
1253
|
+
stmt.free();
|
|
1254
|
+
return stops;
|
|
1255
|
+
}
|
|
1256
|
+
function rowToStop(row) {
|
|
1257
|
+
return {
|
|
1258
|
+
stop_id: String(row.stop_id),
|
|
1259
|
+
stop_name: String(row.stop_name),
|
|
1260
|
+
stop_lat: Number(row.stop_lat),
|
|
1261
|
+
stop_lon: Number(row.stop_lon),
|
|
1262
|
+
stop_code: row.stop_code ? String(row.stop_code) : void 0,
|
|
1263
|
+
stop_desc: row.stop_desc ? String(row.stop_desc) : void 0,
|
|
1264
|
+
zone_id: row.zone_id ? String(row.zone_id) : void 0,
|
|
1265
|
+
stop_url: row.stop_url ? String(row.stop_url) : void 0,
|
|
1266
|
+
location_type: row.location_type !== null ? Number(row.location_type) : void 0,
|
|
1267
|
+
parent_station: row.parent_station ? String(row.parent_station) : void 0,
|
|
1268
|
+
stop_timezone: row.stop_timezone ? String(row.stop_timezone) : void 0,
|
|
1269
|
+
wheelchair_boarding: row.wheelchair_boarding !== null ? Number(row.wheelchair_boarding) : void 0,
|
|
1270
|
+
level_id: row.level_id ? String(row.level_id) : void 0,
|
|
1271
|
+
platform_code: row.platform_code ? String(row.platform_code) : void 0
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/queries/routes.ts
|
|
1276
|
+
function getRoutes(db, filters = {}) {
|
|
1277
|
+
const { routeId, agencyId, limit } = filters;
|
|
1278
|
+
const conditions = [];
|
|
1279
|
+
const params = [];
|
|
1280
|
+
if (routeId) {
|
|
1281
|
+
const routeIds = Array.isArray(routeId) ? routeId : [routeId];
|
|
1282
|
+
if (routeIds.length > 0) {
|
|
1283
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
1284
|
+
conditions.push(`route_id IN (${placeholders})`);
|
|
1285
|
+
params.push(...routeIds);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (agencyId) {
|
|
1289
|
+
const agencyIds = Array.isArray(agencyId) ? agencyId : [agencyId];
|
|
1290
|
+
if (agencyIds.length > 0) {
|
|
1291
|
+
const placeholders = agencyIds.map(() => "?").join(", ");
|
|
1292
|
+
conditions.push(`agency_id IN (${placeholders})`);
|
|
1293
|
+
params.push(...agencyIds);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
let sql = "SELECT * FROM routes";
|
|
1297
|
+
if (conditions.length > 0) {
|
|
1298
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1299
|
+
}
|
|
1300
|
+
sql += " ORDER BY route_short_name, route_long_name";
|
|
1301
|
+
if (limit) {
|
|
1302
|
+
sql += " LIMIT ?";
|
|
1303
|
+
params.push(limit);
|
|
1304
|
+
}
|
|
1305
|
+
const stmt = db.prepare(sql);
|
|
1306
|
+
if (params.length > 0) {
|
|
1307
|
+
stmt.bind(params);
|
|
1308
|
+
}
|
|
1309
|
+
const routes = [];
|
|
1310
|
+
while (stmt.step()) {
|
|
1311
|
+
const row = stmt.getAsObject();
|
|
1312
|
+
routes.push(rowToRoute(row));
|
|
1313
|
+
}
|
|
1314
|
+
stmt.free();
|
|
1315
|
+
return routes;
|
|
1316
|
+
}
|
|
1317
|
+
function rowToRoute(row) {
|
|
1318
|
+
return {
|
|
1319
|
+
route_id: String(row.route_id),
|
|
1320
|
+
route_short_name: String(row.route_short_name),
|
|
1321
|
+
route_long_name: String(row.route_long_name),
|
|
1322
|
+
route_type: Number(row.route_type),
|
|
1323
|
+
agency_id: row.agency_id ? String(row.agency_id) : void 0,
|
|
1324
|
+
route_desc: row.route_desc ? String(row.route_desc) : void 0,
|
|
1325
|
+
route_url: row.route_url ? String(row.route_url) : void 0,
|
|
1326
|
+
route_color: row.route_color ? String(row.route_color) : void 0,
|
|
1327
|
+
route_text_color: row.route_text_color ? String(row.route_text_color) : void 0,
|
|
1328
|
+
route_sort_order: row.route_sort_order !== null ? Number(row.route_sort_order) : void 0,
|
|
1329
|
+
continuous_pickup: row.continuous_pickup !== null ? Number(row.continuous_pickup) : void 0,
|
|
1330
|
+
continuous_drop_off: row.continuous_drop_off !== null ? Number(row.continuous_drop_off) : void 0
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/queries/calendar.ts
|
|
1335
|
+
function getActiveServiceIds(db, date) {
|
|
1336
|
+
const serviceIds = /* @__PURE__ */ new Set();
|
|
1337
|
+
const year = parseInt(date.substring(0, 4));
|
|
1338
|
+
const month = parseInt(date.substring(4, 6));
|
|
1339
|
+
const day = parseInt(date.substring(6, 8));
|
|
1340
|
+
const dateObj = new Date(year, month - 1, day);
|
|
1341
|
+
const dayOfWeek = dateObj.getDay();
|
|
1342
|
+
const dayFields = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
1343
|
+
const dayField = dayFields[dayOfWeek];
|
|
1344
|
+
const calendarStmt = db.prepare(
|
|
1345
|
+
`SELECT service_id FROM calendar
|
|
1346
|
+
WHERE ${dayField} = 1
|
|
1347
|
+
AND start_date <= ?
|
|
1348
|
+
AND end_date >= ?`
|
|
1349
|
+
);
|
|
1350
|
+
calendarStmt.bind([date, date]);
|
|
1351
|
+
while (calendarStmt.step()) {
|
|
1352
|
+
const row = calendarStmt.getAsObject();
|
|
1353
|
+
serviceIds.add(row.service_id);
|
|
1354
|
+
}
|
|
1355
|
+
calendarStmt.free();
|
|
1356
|
+
const exceptionsStmt = db.prepare("SELECT service_id, exception_type FROM calendar_dates WHERE date = ?");
|
|
1357
|
+
exceptionsStmt.bind([date]);
|
|
1358
|
+
while (exceptionsStmt.step()) {
|
|
1359
|
+
const row = exceptionsStmt.getAsObject();
|
|
1360
|
+
if (row.exception_type === 1) {
|
|
1361
|
+
serviceIds.add(row.service_id);
|
|
1362
|
+
} else if (row.exception_type === 2) {
|
|
1363
|
+
serviceIds.delete(row.service_id);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
exceptionsStmt.free();
|
|
1367
|
+
return Array.from(serviceIds);
|
|
1368
|
+
}
|
|
1369
|
+
function getCalendarByServiceId(db, serviceId) {
|
|
1370
|
+
const stmt = db.prepare("SELECT * FROM calendar WHERE service_id = ?");
|
|
1371
|
+
stmt.bind([serviceId]);
|
|
1372
|
+
if (stmt.step()) {
|
|
1373
|
+
const row = stmt.getAsObject();
|
|
1374
|
+
stmt.free();
|
|
1375
|
+
return rowToCalendar(row);
|
|
1376
|
+
}
|
|
1377
|
+
stmt.free();
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
function getCalendarDates(db, serviceId) {
|
|
1381
|
+
const stmt = db.prepare("SELECT * FROM calendar_dates WHERE service_id = ? ORDER BY date");
|
|
1382
|
+
stmt.bind([serviceId]);
|
|
1383
|
+
const dates = [];
|
|
1384
|
+
while (stmt.step()) {
|
|
1385
|
+
const row = stmt.getAsObject();
|
|
1386
|
+
dates.push(rowToCalendarDate(row));
|
|
1387
|
+
}
|
|
1388
|
+
stmt.free();
|
|
1389
|
+
return dates;
|
|
1390
|
+
}
|
|
1391
|
+
function getCalendarDatesForDate(db, date) {
|
|
1392
|
+
const stmt = db.prepare("SELECT * FROM calendar_dates WHERE date = ?");
|
|
1393
|
+
stmt.bind([date]);
|
|
1394
|
+
const dates = [];
|
|
1395
|
+
while (stmt.step()) {
|
|
1396
|
+
const row = stmt.getAsObject();
|
|
1397
|
+
dates.push(rowToCalendarDate(row));
|
|
1398
|
+
}
|
|
1399
|
+
stmt.free();
|
|
1400
|
+
return dates;
|
|
1401
|
+
}
|
|
1402
|
+
function rowToCalendar(row) {
|
|
1403
|
+
return {
|
|
1404
|
+
service_id: String(row.service_id),
|
|
1405
|
+
monday: Number(row.monday),
|
|
1406
|
+
tuesday: Number(row.tuesday),
|
|
1407
|
+
wednesday: Number(row.wednesday),
|
|
1408
|
+
thursday: Number(row.thursday),
|
|
1409
|
+
friday: Number(row.friday),
|
|
1410
|
+
saturday: Number(row.saturday),
|
|
1411
|
+
sunday: Number(row.sunday),
|
|
1412
|
+
start_date: String(row.start_date),
|
|
1413
|
+
end_date: String(row.end_date)
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function rowToCalendarDate(row) {
|
|
1417
|
+
return {
|
|
1418
|
+
service_id: String(row.service_id),
|
|
1419
|
+
date: String(row.date),
|
|
1420
|
+
exception_type: Number(row.exception_type)
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/queries/rt-vehicle-positions.ts
|
|
1425
|
+
function parseVehiclePosition(row) {
|
|
1426
|
+
const vp = {
|
|
1427
|
+
trip_id: String(row.trip_id),
|
|
1428
|
+
route_id: row.route_id ? String(row.route_id) : void 0,
|
|
1429
|
+
rt_last_updated: Number(row.rt_last_updated)
|
|
1430
|
+
};
|
|
1431
|
+
if (row.vehicle_id || row.vehicle_label || row.vehicle_license_plate) {
|
|
1432
|
+
vp.vehicle = {
|
|
1433
|
+
id: row.vehicle_id ? String(row.vehicle_id) : void 0,
|
|
1434
|
+
label: row.vehicle_label ? String(row.vehicle_label) : void 0,
|
|
1435
|
+
license_plate: row.vehicle_license_plate ? String(row.vehicle_license_plate) : void 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (row.latitude !== null && row.longitude !== null) {
|
|
1439
|
+
vp.position = {
|
|
1440
|
+
latitude: Number(row.latitude),
|
|
1441
|
+
longitude: Number(row.longitude),
|
|
1442
|
+
bearing: row.bearing !== null ? Number(row.bearing) : void 0,
|
|
1443
|
+
odometer: row.odometer !== null ? Number(row.odometer) : void 0,
|
|
1444
|
+
speed: row.speed !== null ? Number(row.speed) : void 0
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (row.current_stop_sequence !== null) {
|
|
1448
|
+
vp.current_stop_sequence = Number(row.current_stop_sequence);
|
|
1449
|
+
}
|
|
1450
|
+
if (row.stop_id) {
|
|
1451
|
+
vp.stop_id = String(row.stop_id);
|
|
1452
|
+
}
|
|
1453
|
+
if (row.current_status !== null) {
|
|
1454
|
+
vp.current_status = Number(row.current_status);
|
|
1455
|
+
}
|
|
1456
|
+
if (row.timestamp !== null) {
|
|
1457
|
+
vp.timestamp = Number(row.timestamp);
|
|
1458
|
+
}
|
|
1459
|
+
if (row.congestion_level !== null) {
|
|
1460
|
+
vp.congestion_level = Number(row.congestion_level);
|
|
1461
|
+
}
|
|
1462
|
+
if (row.occupancy_status !== null) {
|
|
1463
|
+
vp.occupancy_status = Number(row.occupancy_status);
|
|
1464
|
+
}
|
|
1465
|
+
return vp;
|
|
1466
|
+
}
|
|
1467
|
+
function getVehiclePositions(db, filters = {}, stalenessThreshold = 120) {
|
|
1468
|
+
const { tripId, routeId, vehicleId, limit } = filters;
|
|
1469
|
+
const conditions = [];
|
|
1470
|
+
const params = [];
|
|
1471
|
+
if (tripId) {
|
|
1472
|
+
conditions.push("trip_id = ?");
|
|
1473
|
+
params.push(tripId);
|
|
1474
|
+
}
|
|
1475
|
+
if (routeId) {
|
|
1476
|
+
conditions.push("route_id = ?");
|
|
1477
|
+
params.push(routeId);
|
|
1478
|
+
}
|
|
1479
|
+
if (vehicleId) {
|
|
1480
|
+
conditions.push("vehicle_id = ?");
|
|
1481
|
+
params.push(vehicleId);
|
|
1482
|
+
}
|
|
1483
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1484
|
+
const staleThreshold = now - stalenessThreshold;
|
|
1485
|
+
conditions.push("rt_last_updated >= ?");
|
|
1486
|
+
params.push(staleThreshold);
|
|
1487
|
+
let sql = "SELECT * FROM rt_vehicle_positions";
|
|
1488
|
+
if (conditions.length > 0) {
|
|
1489
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1490
|
+
}
|
|
1491
|
+
sql += " ORDER BY rt_last_updated DESC";
|
|
1492
|
+
if (limit) {
|
|
1493
|
+
sql += " LIMIT ?";
|
|
1494
|
+
params.push(limit);
|
|
1495
|
+
}
|
|
1496
|
+
const stmt = db.prepare(sql);
|
|
1497
|
+
if (params.length > 0) {
|
|
1498
|
+
stmt.bind(params);
|
|
1499
|
+
}
|
|
1500
|
+
const positions = [];
|
|
1501
|
+
while (stmt.step()) {
|
|
1502
|
+
const row = stmt.getAsObject();
|
|
1503
|
+
positions.push(parseVehiclePosition(row));
|
|
1504
|
+
}
|
|
1505
|
+
stmt.free();
|
|
1506
|
+
return positions;
|
|
1507
|
+
}
|
|
1508
|
+
function getAllVehiclePositions(db) {
|
|
1509
|
+
const sql = "SELECT * FROM rt_vehicle_positions ORDER BY rt_last_updated DESC";
|
|
1510
|
+
const stmt = db.prepare(sql);
|
|
1511
|
+
const positions = [];
|
|
1512
|
+
while (stmt.step()) {
|
|
1513
|
+
const row = stmt.getAsObject();
|
|
1514
|
+
positions.push(parseVehiclePosition(row));
|
|
1515
|
+
}
|
|
1516
|
+
stmt.free();
|
|
1517
|
+
return positions;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/queries/trips.ts
|
|
1521
|
+
function mergeRealtimeData(trips, db, stalenessThreshold) {
|
|
1522
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1523
|
+
const staleThreshold = now - stalenessThreshold;
|
|
1524
|
+
const tripIds = trips.map((t) => t.trip_id);
|
|
1525
|
+
if (tripIds.length === 0) return trips;
|
|
1526
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1527
|
+
const vpStmt = db.prepare(`
|
|
1528
|
+
SELECT * FROM rt_vehicle_positions
|
|
1529
|
+
WHERE trip_id IN (${placeholders})
|
|
1530
|
+
AND rt_last_updated >= ?
|
|
1531
|
+
`);
|
|
1532
|
+
vpStmt.bind([...tripIds, staleThreshold]);
|
|
1533
|
+
const vpMap = /* @__PURE__ */ new Map();
|
|
1534
|
+
while (vpStmt.step()) {
|
|
1535
|
+
const row = vpStmt.getAsObject();
|
|
1536
|
+
const vp = parseVehiclePosition(row);
|
|
1537
|
+
vpMap.set(vp.trip_id, vp);
|
|
1538
|
+
}
|
|
1539
|
+
vpStmt.free();
|
|
1540
|
+
const tuStmt = db.prepare(`
|
|
1541
|
+
SELECT * FROM rt_trip_updates
|
|
1542
|
+
WHERE trip_id IN (${placeholders})
|
|
1543
|
+
AND rt_last_updated >= ?
|
|
1544
|
+
`);
|
|
1545
|
+
tuStmt.bind([...tripIds, staleThreshold]);
|
|
1546
|
+
const tuMap = /* @__PURE__ */ new Map();
|
|
1547
|
+
while (tuStmt.step()) {
|
|
1548
|
+
const row = tuStmt.getAsObject();
|
|
1549
|
+
const tripId = String(row.trip_id);
|
|
1550
|
+
tuMap.set(tripId, {
|
|
1551
|
+
delay: row.delay !== null ? Number(row.delay) : void 0,
|
|
1552
|
+
schedule_relationship: row.schedule_relationship !== null ? Number(row.schedule_relationship) : void 0
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
tuStmt.free();
|
|
1556
|
+
return trips.map((trip) => {
|
|
1557
|
+
const vp = vpMap.get(trip.trip_id);
|
|
1558
|
+
const tu = tuMap.get(trip.trip_id);
|
|
1559
|
+
if (!vp && !tu) {
|
|
1560
|
+
return { ...trip, realtime: { vehicle_position: null, trip_update: null } };
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
...trip,
|
|
1564
|
+
realtime: {
|
|
1565
|
+
vehicle_position: vp || null,
|
|
1566
|
+
trip_update: tu || null
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
function getTrips(db, filters = {}, stalenessThreshold = 120) {
|
|
1572
|
+
const { tripId, routeId, serviceIds, directionId, agencyId, includeRealtime, limit } = filters;
|
|
1573
|
+
const needsRoutesJoin = agencyId !== void 0;
|
|
1574
|
+
const conditions = [];
|
|
1575
|
+
const params = [];
|
|
1576
|
+
if (tripId) {
|
|
1577
|
+
const tripIds = Array.isArray(tripId) ? tripId : [tripId];
|
|
1578
|
+
if (tripIds.length > 0) {
|
|
1579
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1580
|
+
conditions.push(needsRoutesJoin ? `t.trip_id IN (${placeholders})` : `trip_id IN (${placeholders})`);
|
|
1581
|
+
params.push(...tripIds);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (routeId) {
|
|
1585
|
+
const routeIds = Array.isArray(routeId) ? routeId : [routeId];
|
|
1586
|
+
if (routeIds.length > 0) {
|
|
1587
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
1588
|
+
conditions.push(needsRoutesJoin ? `t.route_id IN (${placeholders})` : `route_id IN (${placeholders})`);
|
|
1589
|
+
params.push(...routeIds);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (serviceIds) {
|
|
1593
|
+
const serviceIdArray = Array.isArray(serviceIds) ? serviceIds : [serviceIds];
|
|
1594
|
+
if (serviceIdArray.length > 0) {
|
|
1595
|
+
const placeholders = serviceIdArray.map(() => "?").join(", ");
|
|
1596
|
+
conditions.push(needsRoutesJoin ? `t.service_id IN (${placeholders})` : `service_id IN (${placeholders})`);
|
|
1597
|
+
params.push(...serviceIdArray);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (directionId !== void 0) {
|
|
1601
|
+
const directionIds = Array.isArray(directionId) ? directionId : [directionId];
|
|
1602
|
+
if (directionIds.length > 0) {
|
|
1603
|
+
const placeholders = directionIds.map(() => "?").join(", ");
|
|
1604
|
+
conditions.push(needsRoutesJoin ? `t.direction_id IN (${placeholders})` : `direction_id IN (${placeholders})`);
|
|
1605
|
+
params.push(...directionIds);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (agencyId) {
|
|
1609
|
+
const agencyIds = Array.isArray(agencyId) ? agencyId : [agencyId];
|
|
1610
|
+
if (agencyIds.length > 0) {
|
|
1611
|
+
const placeholders = agencyIds.map(() => "?").join(", ");
|
|
1612
|
+
conditions.push(`r.agency_id IN (${placeholders})`);
|
|
1613
|
+
params.push(...agencyIds);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
let sql = needsRoutesJoin ? "SELECT t.* FROM trips t INNER JOIN routes r ON t.route_id = r.route_id" : "SELECT * FROM trips";
|
|
1617
|
+
if (conditions.length > 0) {
|
|
1618
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1619
|
+
}
|
|
1620
|
+
if (limit) {
|
|
1621
|
+
sql += " LIMIT ?";
|
|
1622
|
+
params.push(limit);
|
|
1623
|
+
}
|
|
1624
|
+
const stmt = db.prepare(sql);
|
|
1625
|
+
if (params.length > 0) {
|
|
1626
|
+
stmt.bind(params);
|
|
1627
|
+
}
|
|
1628
|
+
const trips = [];
|
|
1629
|
+
while (stmt.step()) {
|
|
1630
|
+
const row = stmt.getAsObject();
|
|
1631
|
+
trips.push(rowToTrip(row));
|
|
1632
|
+
}
|
|
1633
|
+
stmt.free();
|
|
1634
|
+
if (includeRealtime) {
|
|
1635
|
+
return mergeRealtimeData(trips, db, stalenessThreshold);
|
|
1636
|
+
}
|
|
1637
|
+
return trips;
|
|
1638
|
+
}
|
|
1639
|
+
function rowToTrip(row) {
|
|
1640
|
+
return {
|
|
1641
|
+
trip_id: String(row.trip_id),
|
|
1642
|
+
route_id: String(row.route_id),
|
|
1643
|
+
service_id: String(row.service_id),
|
|
1644
|
+
trip_headsign: row.trip_headsign ? String(row.trip_headsign) : void 0,
|
|
1645
|
+
trip_short_name: row.trip_short_name ? String(row.trip_short_name) : void 0,
|
|
1646
|
+
direction_id: row.direction_id !== null ? Number(row.direction_id) : void 0,
|
|
1647
|
+
block_id: row.block_id ? String(row.block_id) : void 0,
|
|
1648
|
+
shape_id: row.shape_id ? String(row.shape_id) : void 0,
|
|
1649
|
+
wheelchair_accessible: row.wheelchair_accessible !== null ? Number(row.wheelchair_accessible) : void 0,
|
|
1650
|
+
bikes_allowed: row.bikes_allowed !== null ? Number(row.bikes_allowed) : void 0
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/queries/stop-times.ts
|
|
1655
|
+
function mergeRealtimeData2(stopTimes, db, stalenessThreshold) {
|
|
1656
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1657
|
+
const staleThreshold = now - stalenessThreshold;
|
|
1658
|
+
const tripIds = Array.from(new Set(stopTimes.map((st) => st.trip_id)));
|
|
1659
|
+
if (tripIds.length === 0) return stopTimes;
|
|
1660
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1661
|
+
const stmt = db.prepare(`
|
|
1662
|
+
SELECT trip_id, stop_sequence, stop_id,
|
|
1663
|
+
arrival_delay, arrival_time, departure_delay, departure_time, schedule_relationship
|
|
1664
|
+
FROM rt_stop_time_updates
|
|
1665
|
+
WHERE trip_id IN (${placeholders})
|
|
1666
|
+
AND rt_last_updated >= ?
|
|
1667
|
+
`);
|
|
1668
|
+
stmt.bind([...tripIds, staleThreshold]);
|
|
1669
|
+
const rtMap = /* @__PURE__ */ new Map();
|
|
1670
|
+
while (stmt.step()) {
|
|
1671
|
+
const row = stmt.getAsObject();
|
|
1672
|
+
const key = `${row.trip_id}_${row.stop_sequence}`;
|
|
1673
|
+
rtMap.set(key, {
|
|
1674
|
+
arrival_delay: row.arrival_delay !== null ? Number(row.arrival_delay) : void 0,
|
|
1675
|
+
arrival_time: row.arrival_time !== null ? Number(row.arrival_time) : void 0,
|
|
1676
|
+
departure_delay: row.departure_delay !== null ? Number(row.departure_delay) : void 0,
|
|
1677
|
+
departure_time: row.departure_time !== null ? Number(row.departure_time) : void 0,
|
|
1678
|
+
schedule_relationship: row.schedule_relationship !== null ? Number(row.schedule_relationship) : void 0
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
stmt.free();
|
|
1682
|
+
return stopTimes.map((st) => {
|
|
1683
|
+
const key = `${st.trip_id}_${st.stop_sequence}`;
|
|
1684
|
+
const rtData = rtMap.get(key);
|
|
1685
|
+
if (rtData) {
|
|
1686
|
+
return { ...st, realtime: rtData };
|
|
1687
|
+
}
|
|
1688
|
+
return st;
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function getStopTimes(db, filters = {}, stalenessThreshold = 120) {
|
|
1692
|
+
const { tripId, stopId, routeId, serviceIds, directionId, agencyId, includeRealtime, limit } = filters;
|
|
1693
|
+
const needsTripsJoin = routeId || serviceIds || directionId !== void 0 || agencyId !== void 0;
|
|
1694
|
+
const needsRoutesJoin = agencyId !== void 0;
|
|
1695
|
+
const conditions = [];
|
|
1696
|
+
const params = [];
|
|
1697
|
+
if (tripId) {
|
|
1698
|
+
const tripIds = Array.isArray(tripId) ? tripId : [tripId];
|
|
1699
|
+
if (tripIds.length > 0) {
|
|
1700
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1701
|
+
conditions.push(needsTripsJoin ? `st.trip_id IN (${placeholders})` : `trip_id IN (${placeholders})`);
|
|
1702
|
+
params.push(...tripIds);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (stopId) {
|
|
1706
|
+
const stopIds = Array.isArray(stopId) ? stopId : [stopId];
|
|
1707
|
+
if (stopIds.length > 0) {
|
|
1708
|
+
const placeholders = stopIds.map(() => "?").join(", ");
|
|
1709
|
+
conditions.push(needsTripsJoin ? `st.stop_id IN (${placeholders})` : `stop_id IN (${placeholders})`);
|
|
1710
|
+
params.push(...stopIds);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (routeId) {
|
|
1714
|
+
const routeIds = Array.isArray(routeId) ? routeId : [routeId];
|
|
1715
|
+
if (routeIds.length > 0) {
|
|
1716
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
1717
|
+
conditions.push(`t.route_id IN (${placeholders})`);
|
|
1718
|
+
params.push(...routeIds);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (serviceIds) {
|
|
1722
|
+
const serviceIdArray = Array.isArray(serviceIds) ? serviceIds : [serviceIds];
|
|
1723
|
+
if (serviceIdArray.length > 0) {
|
|
1724
|
+
const placeholders = serviceIdArray.map(() => "?").join(", ");
|
|
1725
|
+
conditions.push(`t.service_id IN (${placeholders})`);
|
|
1726
|
+
params.push(...serviceIdArray);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (directionId !== void 0) {
|
|
1730
|
+
const directionIds = Array.isArray(directionId) ? directionId : [directionId];
|
|
1731
|
+
if (directionIds.length > 0) {
|
|
1732
|
+
const placeholders = directionIds.map(() => "?").join(", ");
|
|
1733
|
+
conditions.push(`t.direction_id IN (${placeholders})`);
|
|
1734
|
+
params.push(...directionIds);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (agencyId) {
|
|
1738
|
+
const agencyIds = Array.isArray(agencyId) ? agencyId : [agencyId];
|
|
1739
|
+
if (agencyIds.length > 0) {
|
|
1740
|
+
const placeholders = agencyIds.map(() => "?").join(", ");
|
|
1741
|
+
conditions.push(`r.agency_id IN (${placeholders})`);
|
|
1742
|
+
params.push(...agencyIds);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
let sql;
|
|
1746
|
+
if (needsRoutesJoin) {
|
|
1747
|
+
sql = "SELECT st.* FROM stop_times st INNER JOIN trips t ON st.trip_id = t.trip_id INNER JOIN routes r ON t.route_id = r.route_id";
|
|
1748
|
+
} else if (needsTripsJoin) {
|
|
1749
|
+
sql = "SELECT st.* FROM stop_times st INNER JOIN trips t ON st.trip_id = t.trip_id";
|
|
1750
|
+
} else {
|
|
1751
|
+
sql = "SELECT * FROM stop_times";
|
|
1752
|
+
}
|
|
1753
|
+
if (conditions.length > 0) {
|
|
1754
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1755
|
+
}
|
|
1756
|
+
sql += tripId ? " ORDER BY stop_sequence" : " ORDER BY arrival_time";
|
|
1757
|
+
if (limit) {
|
|
1758
|
+
sql += " LIMIT ?";
|
|
1759
|
+
params.push(limit);
|
|
1760
|
+
}
|
|
1761
|
+
const stmt = db.prepare(sql);
|
|
1762
|
+
if (params.length > 0) {
|
|
1763
|
+
stmt.bind(params);
|
|
1764
|
+
}
|
|
1765
|
+
const stopTimes = [];
|
|
1766
|
+
while (stmt.step()) {
|
|
1767
|
+
const row = stmt.getAsObject();
|
|
1768
|
+
stopTimes.push(rowToStopTime(row));
|
|
1769
|
+
}
|
|
1770
|
+
stmt.free();
|
|
1771
|
+
if (includeRealtime) {
|
|
1772
|
+
return mergeRealtimeData2(stopTimes, db, stalenessThreshold);
|
|
1773
|
+
}
|
|
1774
|
+
return stopTimes;
|
|
1775
|
+
}
|
|
1776
|
+
function buildOrderedStopList(db, tripIds) {
|
|
1777
|
+
if (tripIds.length === 0) {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1781
|
+
const stmt = db.prepare(`
|
|
1782
|
+
SELECT trip_id, stop_id, stop_sequence
|
|
1783
|
+
FROM stop_times
|
|
1784
|
+
WHERE trip_id IN (${placeholders})
|
|
1785
|
+
ORDER BY trip_id, stop_sequence
|
|
1786
|
+
`);
|
|
1787
|
+
stmt.bind(tripIds);
|
|
1788
|
+
const tripStopSequences = /* @__PURE__ */ new Map();
|
|
1789
|
+
while (stmt.step()) {
|
|
1790
|
+
const row = stmt.getAsObject();
|
|
1791
|
+
const tripId = String(row.trip_id);
|
|
1792
|
+
const stopId = String(row.stop_id);
|
|
1793
|
+
const stopSequence = Number(row.stop_sequence);
|
|
1794
|
+
if (!tripStopSequences.has(tripId)) {
|
|
1795
|
+
tripStopSequences.set(tripId, []);
|
|
1796
|
+
}
|
|
1797
|
+
tripStopSequences.get(tripId).push({ stop_id: stopId, stop_sequence: stopSequence });
|
|
1798
|
+
}
|
|
1799
|
+
stmt.free();
|
|
1800
|
+
const orderedStopIds = [];
|
|
1801
|
+
const stopIdSet = /* @__PURE__ */ new Set();
|
|
1802
|
+
for (const [, stopSequence] of tripStopSequences) {
|
|
1803
|
+
for (let i = 0; i < stopSequence.length; i++) {
|
|
1804
|
+
const currentStop = stopSequence[i];
|
|
1805
|
+
if (stopIdSet.has(currentStop.stop_id)) {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const insertIndex = findInsertionPosition(
|
|
1809
|
+
orderedStopIds,
|
|
1810
|
+
currentStop.stop_id,
|
|
1811
|
+
stopSequence,
|
|
1812
|
+
i
|
|
1813
|
+
);
|
|
1814
|
+
orderedStopIds.splice(insertIndex, 0, currentStop.stop_id);
|
|
1815
|
+
stopIdSet.add(currentStop.stop_id);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (orderedStopIds.length === 0) {
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
const stops = getStops(db, { stopId: orderedStopIds });
|
|
1822
|
+
const stopMap = /* @__PURE__ */ new Map();
|
|
1823
|
+
stops.forEach((stop) => stopMap.set(stop.stop_id, stop));
|
|
1824
|
+
return orderedStopIds.map((stopId) => stopMap.get(stopId)).filter((stop) => stop !== void 0);
|
|
1825
|
+
}
|
|
1826
|
+
function findInsertionPosition(orderedStopIds, newStopId, tripStops, currentIndex) {
|
|
1827
|
+
if (orderedStopIds.length === 0) {
|
|
1828
|
+
return 0;
|
|
1829
|
+
}
|
|
1830
|
+
let beforeIndex = -1;
|
|
1831
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
1832
|
+
const idx = orderedStopIds.indexOf(tripStops[i].stop_id);
|
|
1833
|
+
if (idx !== -1) {
|
|
1834
|
+
beforeIndex = idx;
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
let afterIndex = orderedStopIds.length;
|
|
1839
|
+
for (let i = currentIndex + 1; i < tripStops.length; i++) {
|
|
1840
|
+
const idx = orderedStopIds.indexOf(tripStops[i].stop_id);
|
|
1841
|
+
if (idx !== -1) {
|
|
1842
|
+
afterIndex = idx;
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const insertPosition = beforeIndex + 1;
|
|
1847
|
+
if (insertPosition <= afterIndex) {
|
|
1848
|
+
return insertPosition;
|
|
1849
|
+
}
|
|
1850
|
+
return beforeIndex + 1;
|
|
1851
|
+
}
|
|
1852
|
+
function rowToStopTime(row) {
|
|
1853
|
+
return {
|
|
1854
|
+
trip_id: String(row.trip_id),
|
|
1855
|
+
arrival_time: String(row.arrival_time),
|
|
1856
|
+
departure_time: String(row.departure_time),
|
|
1857
|
+
stop_id: String(row.stop_id),
|
|
1858
|
+
stop_sequence: Number(row.stop_sequence),
|
|
1859
|
+
stop_headsign: row.stop_headsign ? String(row.stop_headsign) : void 0,
|
|
1860
|
+
pickup_type: row.pickup_type !== null ? Number(row.pickup_type) : void 0,
|
|
1861
|
+
drop_off_type: row.drop_off_type !== null ? Number(row.drop_off_type) : void 0,
|
|
1862
|
+
continuous_pickup: row.continuous_pickup !== null ? Number(row.continuous_pickup) : void 0,
|
|
1863
|
+
continuous_drop_off: row.continuous_drop_off !== null ? Number(row.continuous_drop_off) : void 0,
|
|
1864
|
+
shape_dist_traveled: row.shape_dist_traveled !== null ? Number(row.shape_dist_traveled) : void 0,
|
|
1865
|
+
timepoint: row.timepoint !== null ? Number(row.timepoint) : void 0
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// src/queries/shapes.ts
|
|
1870
|
+
function getShapes(db, filters = {}) {
|
|
1871
|
+
const { shapeId, routeId, tripId, limit } = filters;
|
|
1872
|
+
const needsTripsJoin = routeId !== void 0 || tripId !== void 0;
|
|
1873
|
+
const conditions = [];
|
|
1874
|
+
const params = [];
|
|
1875
|
+
if (shapeId) {
|
|
1876
|
+
const shapeIds = Array.isArray(shapeId) ? shapeId : [shapeId];
|
|
1877
|
+
if (shapeIds.length > 0) {
|
|
1878
|
+
const placeholders = shapeIds.map(() => "?").join(", ");
|
|
1879
|
+
conditions.push(needsTripsJoin ? `s.shape_id IN (${placeholders})` : `shape_id IN (${placeholders})`);
|
|
1880
|
+
params.push(...shapeIds);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (tripId) {
|
|
1884
|
+
const tripIds = Array.isArray(tripId) ? tripId : [tripId];
|
|
1885
|
+
if (tripIds.length > 0) {
|
|
1886
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
1887
|
+
conditions.push(`t.trip_id IN (${placeholders})`);
|
|
1888
|
+
params.push(...tripIds);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (routeId) {
|
|
1892
|
+
const routeIds = Array.isArray(routeId) ? routeId : [routeId];
|
|
1893
|
+
if (routeIds.length > 0) {
|
|
1894
|
+
const placeholders = routeIds.map(() => "?").join(", ");
|
|
1895
|
+
conditions.push(`t.route_id IN (${placeholders})`);
|
|
1896
|
+
params.push(...routeIds);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
let sql;
|
|
1900
|
+
if (needsTripsJoin) {
|
|
1901
|
+
sql = `
|
|
1902
|
+
SELECT s.* FROM shapes s
|
|
1903
|
+
WHERE s.shape_id IN (
|
|
1904
|
+
SELECT DISTINCT t.shape_id FROM trips t
|
|
1905
|
+
WHERE t.shape_id IS NOT NULL
|
|
1906
|
+
${conditions.length > 0 ? " AND " + conditions.join(" AND ") : ""}
|
|
1907
|
+
)
|
|
1908
|
+
ORDER BY s.shape_id, s.shape_pt_sequence
|
|
1909
|
+
`;
|
|
1910
|
+
} else {
|
|
1911
|
+
sql = "SELECT * FROM shapes";
|
|
1912
|
+
if (conditions.length > 0) {
|
|
1913
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
1914
|
+
}
|
|
1915
|
+
sql += " ORDER BY shape_id, shape_pt_sequence";
|
|
1916
|
+
}
|
|
1917
|
+
if (limit) {
|
|
1918
|
+
sql += " LIMIT ?";
|
|
1919
|
+
params.push(limit);
|
|
1920
|
+
}
|
|
1921
|
+
const stmt = db.prepare(sql);
|
|
1922
|
+
if (params.length > 0) {
|
|
1923
|
+
stmt.bind(params);
|
|
1924
|
+
}
|
|
1925
|
+
const shapes = [];
|
|
1926
|
+
while (stmt.step()) {
|
|
1927
|
+
const row = stmt.getAsObject();
|
|
1928
|
+
shapes.push(rowToShape(row));
|
|
1929
|
+
}
|
|
1930
|
+
stmt.free();
|
|
1931
|
+
return shapes;
|
|
1932
|
+
}
|
|
1933
|
+
function getShapesToGeojson(db, filters = {}, precision = 6) {
|
|
1934
|
+
const shapes = getShapes(db, filters);
|
|
1935
|
+
const shapeGroups = /* @__PURE__ */ new Map();
|
|
1936
|
+
for (const shape of shapes) {
|
|
1937
|
+
const group = shapeGroups.get(shape.shape_id);
|
|
1938
|
+
if (group) {
|
|
1939
|
+
group.push(shape);
|
|
1940
|
+
} else {
|
|
1941
|
+
shapeGroups.set(shape.shape_id, [shape]);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const shapeRouteMap = getRoutesByShapeIds(db, Array.from(shapeGroups.keys()));
|
|
1945
|
+
const features = [];
|
|
1946
|
+
const multiplier = Math.pow(10, precision);
|
|
1947
|
+
for (const [shapeId, points] of shapeGroups) {
|
|
1948
|
+
points.sort((a, b) => a.shape_pt_sequence - b.shape_pt_sequence);
|
|
1949
|
+
const coordinates = points.map((point) => [
|
|
1950
|
+
Math.round(point.shape_pt_lon * multiplier) / multiplier,
|
|
1951
|
+
Math.round(point.shape_pt_lat * multiplier) / multiplier
|
|
1952
|
+
]);
|
|
1953
|
+
const route = shapeRouteMap.get(shapeId);
|
|
1954
|
+
const properties = {
|
|
1955
|
+
shape_id: shapeId
|
|
1956
|
+
};
|
|
1957
|
+
if (route) {
|
|
1958
|
+
properties.route_id = route.route_id;
|
|
1959
|
+
properties.route_short_name = route.route_short_name;
|
|
1960
|
+
properties.route_long_name = route.route_long_name;
|
|
1961
|
+
properties.route_type = route.route_type;
|
|
1962
|
+
if (route.route_color) properties.route_color = route.route_color;
|
|
1963
|
+
if (route.route_text_color) properties.route_text_color = route.route_text_color;
|
|
1964
|
+
if (route.agency_id) properties.agency_id = route.agency_id;
|
|
1965
|
+
}
|
|
1966
|
+
features.push({
|
|
1967
|
+
type: "Feature",
|
|
1968
|
+
properties,
|
|
1969
|
+
geometry: {
|
|
1970
|
+
type: "LineString",
|
|
1971
|
+
coordinates
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
type: "FeatureCollection",
|
|
1977
|
+
features
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function getRoutesByShapeIds(db, shapeIds) {
|
|
1981
|
+
if (shapeIds.length === 0) {
|
|
1982
|
+
return /* @__PURE__ */ new Map();
|
|
1983
|
+
}
|
|
1984
|
+
const placeholders = shapeIds.map(() => "?").join(", ");
|
|
1985
|
+
const sql = `
|
|
1986
|
+
SELECT DISTINCT t.shape_id, r.*
|
|
1987
|
+
FROM trips t
|
|
1988
|
+
INNER JOIN routes r ON t.route_id = r.route_id
|
|
1989
|
+
WHERE t.shape_id IN (${placeholders})
|
|
1990
|
+
GROUP BY t.shape_id
|
|
1991
|
+
`;
|
|
1992
|
+
const stmt = db.prepare(sql);
|
|
1993
|
+
stmt.bind(shapeIds);
|
|
1994
|
+
const result = /* @__PURE__ */ new Map();
|
|
1995
|
+
while (stmt.step()) {
|
|
1996
|
+
const row = stmt.getAsObject();
|
|
1997
|
+
const shapeId = String(row.shape_id);
|
|
1998
|
+
result.set(shapeId, rowToRoute2(row));
|
|
1999
|
+
}
|
|
2000
|
+
stmt.free();
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
function rowToShape(row) {
|
|
2004
|
+
return {
|
|
2005
|
+
shape_id: String(row.shape_id),
|
|
2006
|
+
shape_pt_lat: Number(row.shape_pt_lat),
|
|
2007
|
+
shape_pt_lon: Number(row.shape_pt_lon),
|
|
2008
|
+
shape_pt_sequence: Number(row.shape_pt_sequence),
|
|
2009
|
+
shape_dist_traveled: row.shape_dist_traveled !== null ? Number(row.shape_dist_traveled) : void 0
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function rowToRoute2(row) {
|
|
2013
|
+
return {
|
|
2014
|
+
route_id: String(row.route_id),
|
|
2015
|
+
route_short_name: row.route_short_name ? String(row.route_short_name) : "",
|
|
2016
|
+
route_long_name: row.route_long_name ? String(row.route_long_name) : "",
|
|
2017
|
+
route_type: Number(row.route_type),
|
|
2018
|
+
agency_id: row.agency_id ? String(row.agency_id) : void 0,
|
|
2019
|
+
route_desc: row.route_desc ? String(row.route_desc) : void 0,
|
|
2020
|
+
route_url: row.route_url ? String(row.route_url) : void 0,
|
|
2021
|
+
route_color: row.route_color ? String(row.route_color) : void 0,
|
|
2022
|
+
route_text_color: row.route_text_color ? String(row.route_text_color) : void 0,
|
|
2023
|
+
route_sort_order: row.route_sort_order !== null ? Number(row.route_sort_order) : void 0,
|
|
2024
|
+
continuous_pickup: row.continuous_pickup !== null ? Number(row.continuous_pickup) : void 0,
|
|
2025
|
+
continuous_drop_off: row.continuous_drop_off !== null ? Number(row.continuous_drop_off) : void 0
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/queries/rt-alerts.ts
|
|
2030
|
+
function parseAlert(row) {
|
|
2031
|
+
return {
|
|
2032
|
+
id: String(row.id),
|
|
2033
|
+
active_period: row.active_period ? JSON.parse(String(row.active_period)) : [],
|
|
2034
|
+
informed_entity: row.informed_entity ? JSON.parse(String(row.informed_entity)) : [],
|
|
2035
|
+
cause: row.cause ? Number(row.cause) : void 0,
|
|
2036
|
+
effect: row.effect ? Number(row.effect) : void 0,
|
|
2037
|
+
url: row.url ? JSON.parse(String(row.url)) : void 0,
|
|
2038
|
+
header_text: row.header_text ? JSON.parse(String(row.header_text)) : void 0,
|
|
2039
|
+
description_text: row.description_text ? JSON.parse(String(row.description_text)) : void 0,
|
|
2040
|
+
rt_last_updated: Number(row.rt_last_updated)
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function isAlertActive(alert, now) {
|
|
2044
|
+
if (!alert.active_period || alert.active_period.length === 0) {
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
for (const period of alert.active_period) {
|
|
2048
|
+
const start = period.start || 0;
|
|
2049
|
+
const end = period.end || Number.MAX_SAFE_INTEGER;
|
|
2050
|
+
if (now >= start && now <= end) {
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
function alertAffectsEntity(alert, filters) {
|
|
2057
|
+
if (!alert.informed_entity || alert.informed_entity.length === 0) {
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
for (const entity of alert.informed_entity) {
|
|
2061
|
+
if (filters.routeId && entity.route_id === filters.routeId) {
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
if (filters.stopId && entity.stop_id === filters.stopId) {
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
if (filters.tripId && entity.trip?.trip_id === filters.tripId) {
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
function getAlerts(db, filters = {}, stalenessThreshold = 120) {
|
|
2074
|
+
const {
|
|
2075
|
+
alertId,
|
|
2076
|
+
activeOnly,
|
|
2077
|
+
routeId,
|
|
2078
|
+
stopId,
|
|
2079
|
+
tripId,
|
|
2080
|
+
cause,
|
|
2081
|
+
effect,
|
|
2082
|
+
limit
|
|
2083
|
+
} = filters;
|
|
2084
|
+
const conditions = [];
|
|
2085
|
+
const params = [];
|
|
2086
|
+
if (alertId) {
|
|
2087
|
+
conditions.push("id = ?");
|
|
2088
|
+
params.push(alertId);
|
|
2089
|
+
}
|
|
2090
|
+
if (cause !== void 0) {
|
|
2091
|
+
conditions.push("cause = ?");
|
|
2092
|
+
params.push(cause);
|
|
2093
|
+
}
|
|
2094
|
+
if (effect !== void 0) {
|
|
2095
|
+
conditions.push("effect = ?");
|
|
2096
|
+
params.push(effect);
|
|
2097
|
+
}
|
|
2098
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2099
|
+
const staleThreshold = now - stalenessThreshold;
|
|
2100
|
+
conditions.push("rt_last_updated >= ?");
|
|
2101
|
+
params.push(staleThreshold);
|
|
2102
|
+
let sql = "SELECT * FROM rt_alerts";
|
|
2103
|
+
if (conditions.length > 0) {
|
|
2104
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
2105
|
+
}
|
|
2106
|
+
sql += " ORDER BY rt_last_updated DESC";
|
|
2107
|
+
if (limit) {
|
|
2108
|
+
sql += " LIMIT ?";
|
|
2109
|
+
params.push(limit);
|
|
2110
|
+
}
|
|
2111
|
+
const stmt = db.prepare(sql);
|
|
2112
|
+
if (params.length > 0) {
|
|
2113
|
+
stmt.bind(params);
|
|
2114
|
+
}
|
|
2115
|
+
const alerts = [];
|
|
2116
|
+
while (stmt.step()) {
|
|
2117
|
+
const row = stmt.getAsObject();
|
|
2118
|
+
const alert = parseAlert(row);
|
|
2119
|
+
if (activeOnly && !isAlertActive(alert, now)) {
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
if (routeId || stopId || tripId) {
|
|
2123
|
+
if (!alertAffectsEntity(alert, filters)) {
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
alerts.push(alert);
|
|
2128
|
+
}
|
|
2129
|
+
stmt.free();
|
|
2130
|
+
return alerts;
|
|
2131
|
+
}
|
|
2132
|
+
function getAllAlerts(db) {
|
|
2133
|
+
const sql = "SELECT * FROM rt_alerts ORDER BY rt_last_updated DESC";
|
|
2134
|
+
const stmt = db.prepare(sql);
|
|
2135
|
+
const alerts = [];
|
|
2136
|
+
while (stmt.step()) {
|
|
2137
|
+
const row = stmt.getAsObject();
|
|
2138
|
+
alerts.push(parseAlert(row));
|
|
2139
|
+
}
|
|
2140
|
+
stmt.free();
|
|
2141
|
+
return alerts;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/queries/rt-stop-time-updates.ts
|
|
2145
|
+
function parseStopTimeUpdate(row) {
|
|
2146
|
+
const stu = {
|
|
2147
|
+
stop_sequence: row.stop_sequence !== null ? Number(row.stop_sequence) : void 0,
|
|
2148
|
+
stop_id: row.stop_id ? String(row.stop_id) : void 0,
|
|
2149
|
+
schedule_relationship: row.schedule_relationship !== null ? Number(row.schedule_relationship) : void 0,
|
|
2150
|
+
trip_id: String(row.trip_id),
|
|
2151
|
+
rt_last_updated: Number(row.rt_last_updated)
|
|
2152
|
+
};
|
|
2153
|
+
if (row.arrival_delay !== null || row.arrival_time !== null || row.arrival_uncertainty !== null) {
|
|
2154
|
+
stu.arrival = {
|
|
2155
|
+
delay: row.arrival_delay !== null ? Number(row.arrival_delay) : void 0,
|
|
2156
|
+
time: row.arrival_time !== null ? Number(row.arrival_time) : void 0,
|
|
2157
|
+
uncertainty: row.arrival_uncertainty !== null ? Number(row.arrival_uncertainty) : void 0
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
if (row.departure_delay !== null || row.departure_time !== null || row.departure_uncertainty !== null) {
|
|
2161
|
+
stu.departure = {
|
|
2162
|
+
delay: row.departure_delay !== null ? Number(row.departure_delay) : void 0,
|
|
2163
|
+
time: row.departure_time !== null ? Number(row.departure_time) : void 0,
|
|
2164
|
+
uncertainty: row.departure_uncertainty !== null ? Number(row.departure_uncertainty) : void 0
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
return stu;
|
|
2168
|
+
}
|
|
2169
|
+
function getStopTimeUpdates(db, filters = {}, stalenessThreshold = 120) {
|
|
2170
|
+
const { tripId, stopId, stopSequence, limit } = filters;
|
|
2171
|
+
const conditions = [];
|
|
2172
|
+
const params = [];
|
|
2173
|
+
if (tripId) {
|
|
2174
|
+
const tripIds = Array.isArray(tripId) ? tripId : [tripId];
|
|
2175
|
+
if (tripIds.length > 0) {
|
|
2176
|
+
const placeholders = tripIds.map(() => "?").join(", ");
|
|
2177
|
+
conditions.push(`trip_id IN (${placeholders})`);
|
|
2178
|
+
params.push(...tripIds);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (stopId) {
|
|
2182
|
+
const stopIds = Array.isArray(stopId) ? stopId : [stopId];
|
|
2183
|
+
if (stopIds.length > 0) {
|
|
2184
|
+
const placeholders = stopIds.map(() => "?").join(", ");
|
|
2185
|
+
conditions.push(`stop_id IN (${placeholders})`);
|
|
2186
|
+
params.push(...stopIds);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (stopSequence !== void 0) {
|
|
2190
|
+
const stopSequences = Array.isArray(stopSequence) ? stopSequence : [stopSequence];
|
|
2191
|
+
if (stopSequences.length > 0) {
|
|
2192
|
+
const placeholders = stopSequences.map(() => "?").join(", ");
|
|
2193
|
+
conditions.push(`stop_sequence IN (${placeholders})`);
|
|
2194
|
+
params.push(...stopSequences);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2198
|
+
const staleThreshold = now - stalenessThreshold;
|
|
2199
|
+
conditions.push("rt_last_updated >= ?");
|
|
2200
|
+
params.push(staleThreshold);
|
|
2201
|
+
let sql = "SELECT * FROM rt_stop_time_updates";
|
|
2202
|
+
if (conditions.length > 0) {
|
|
2203
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
2204
|
+
}
|
|
2205
|
+
sql += " ORDER BY trip_id, stop_sequence";
|
|
2206
|
+
if (limit) {
|
|
2207
|
+
sql += " LIMIT ?";
|
|
2208
|
+
params.push(limit);
|
|
2209
|
+
}
|
|
2210
|
+
const stmt = db.prepare(sql);
|
|
2211
|
+
if (params.length > 0) {
|
|
2212
|
+
stmt.bind(params);
|
|
2213
|
+
}
|
|
2214
|
+
const stopTimeUpdates = [];
|
|
2215
|
+
while (stmt.step()) {
|
|
2216
|
+
const row = stmt.getAsObject();
|
|
2217
|
+
stopTimeUpdates.push(parseStopTimeUpdate(row));
|
|
2218
|
+
}
|
|
2219
|
+
stmt.free();
|
|
2220
|
+
return stopTimeUpdates;
|
|
2221
|
+
}
|
|
2222
|
+
function getAllStopTimeUpdates(db) {
|
|
2223
|
+
return getStopTimeUpdates(db, {}, Number.MAX_SAFE_INTEGER);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// src/queries/rt-trip-updates.ts
|
|
2227
|
+
function parseTripUpdate(row) {
|
|
2228
|
+
const tu = {
|
|
2229
|
+
trip_id: String(row.trip_id),
|
|
2230
|
+
route_id: row.route_id ? String(row.route_id) : void 0,
|
|
2231
|
+
stop_time_update: [],
|
|
2232
|
+
// Will be populated separately
|
|
2233
|
+
timestamp: row.timestamp !== null ? Number(row.timestamp) : void 0,
|
|
2234
|
+
delay: row.delay !== null ? Number(row.delay) : void 0,
|
|
2235
|
+
schedule_relationship: row.schedule_relationship !== null ? Number(row.schedule_relationship) : void 0,
|
|
2236
|
+
rt_last_updated: Number(row.rt_last_updated)
|
|
2237
|
+
};
|
|
2238
|
+
if (row.vehicle_id || row.vehicle_label || row.vehicle_license_plate) {
|
|
2239
|
+
tu.vehicle = {
|
|
2240
|
+
id: row.vehicle_id ? String(row.vehicle_id) : void 0,
|
|
2241
|
+
label: row.vehicle_label ? String(row.vehicle_label) : void 0,
|
|
2242
|
+
license_plate: row.vehicle_license_plate ? String(row.vehicle_license_plate) : void 0
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
return tu;
|
|
2246
|
+
}
|
|
2247
|
+
function getTripUpdates(db, filters = {}, stalenessThreshold = 120) {
|
|
2248
|
+
const { tripId, routeId, vehicleId, limit } = filters;
|
|
2249
|
+
const conditions = [];
|
|
2250
|
+
const params = [];
|
|
2251
|
+
if (tripId) {
|
|
2252
|
+
conditions.push("trip_id = ?");
|
|
2253
|
+
params.push(tripId);
|
|
2254
|
+
}
|
|
2255
|
+
if (routeId) {
|
|
2256
|
+
conditions.push("route_id = ?");
|
|
2257
|
+
params.push(routeId);
|
|
2258
|
+
}
|
|
2259
|
+
if (vehicleId) {
|
|
2260
|
+
conditions.push("vehicle_id = ?");
|
|
2261
|
+
params.push(vehicleId);
|
|
2262
|
+
}
|
|
2263
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2264
|
+
const staleThreshold = now - stalenessThreshold;
|
|
2265
|
+
conditions.push("rt_last_updated >= ?");
|
|
2266
|
+
params.push(staleThreshold);
|
|
2267
|
+
let sql = "SELECT * FROM rt_trip_updates";
|
|
2268
|
+
if (conditions.length > 0) {
|
|
2269
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
2270
|
+
}
|
|
2271
|
+
sql += " ORDER BY rt_last_updated DESC";
|
|
2272
|
+
if (limit) {
|
|
2273
|
+
sql += " LIMIT ?";
|
|
2274
|
+
params.push(limit);
|
|
2275
|
+
}
|
|
2276
|
+
const stmt = db.prepare(sql);
|
|
2277
|
+
if (params.length > 0) {
|
|
2278
|
+
stmt.bind(params);
|
|
2279
|
+
}
|
|
2280
|
+
const tripUpdates = [];
|
|
2281
|
+
while (stmt.step()) {
|
|
2282
|
+
const row = stmt.getAsObject();
|
|
2283
|
+
tripUpdates.push(parseTripUpdate(row));
|
|
2284
|
+
}
|
|
2285
|
+
stmt.free();
|
|
2286
|
+
if (tripUpdates.length > 0) {
|
|
2287
|
+
const tripIds = tripUpdates.map((tu) => tu.trip_id);
|
|
2288
|
+
const stopTimeUpdates = getStopTimeUpdates(db, { tripId: tripIds }, stalenessThreshold);
|
|
2289
|
+
const stopTimesByTripId = /* @__PURE__ */ new Map();
|
|
2290
|
+
for (const stu of stopTimeUpdates) {
|
|
2291
|
+
if (!stu.trip_id) continue;
|
|
2292
|
+
if (!stopTimesByTripId.has(stu.trip_id)) {
|
|
2293
|
+
stopTimesByTripId.set(stu.trip_id, []);
|
|
2294
|
+
}
|
|
2295
|
+
stopTimesByTripId.get(stu.trip_id).push(stu);
|
|
2296
|
+
}
|
|
2297
|
+
for (const tu of tripUpdates) {
|
|
2298
|
+
tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
return tripUpdates;
|
|
2302
|
+
}
|
|
2303
|
+
function getAllTripUpdates(db) {
|
|
2304
|
+
const sql = "SELECT * FROM rt_trip_updates ORDER BY rt_last_updated DESC";
|
|
2305
|
+
const stmt = db.prepare(sql);
|
|
2306
|
+
const tripUpdates = [];
|
|
2307
|
+
while (stmt.step()) {
|
|
2308
|
+
const row = stmt.getAsObject();
|
|
2309
|
+
tripUpdates.push(parseTripUpdate(row));
|
|
2310
|
+
}
|
|
2311
|
+
stmt.free();
|
|
2312
|
+
if (tripUpdates.length > 0) {
|
|
2313
|
+
const tripIds = tripUpdates.map((tu) => tu.trip_id);
|
|
2314
|
+
const stopTimeUpdates = getStopTimeUpdates(db, { tripId: tripIds }, Number.MAX_SAFE_INTEGER);
|
|
2315
|
+
const stopTimesByTripId = /* @__PURE__ */ new Map();
|
|
2316
|
+
for (const stu of stopTimeUpdates) {
|
|
2317
|
+
if (!stu.trip_id) continue;
|
|
2318
|
+
if (!stopTimesByTripId.has(stu.trip_id)) {
|
|
2319
|
+
stopTimesByTripId.set(stu.trip_id, []);
|
|
2320
|
+
}
|
|
2321
|
+
stopTimesByTripId.get(stu.trip_id).push(stu);
|
|
2322
|
+
}
|
|
2323
|
+
for (const tu of tripUpdates) {
|
|
2324
|
+
tu.stop_time_update = stopTimesByTripId.get(tu.trip_id) || [];
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return tripUpdates;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/gtfs-sqljs.ts
|
|
2331
|
+
var LIB_VERSION = "0.1.0";
|
|
2332
|
+
var GtfsSqlJs = class _GtfsSqlJs {
|
|
2333
|
+
/**
|
|
2334
|
+
* Private constructor - use static factory methods instead
|
|
2335
|
+
*/
|
|
2336
|
+
constructor() {
|
|
2337
|
+
this.db = null;
|
|
2338
|
+
this.SQL = null;
|
|
2339
|
+
this.realtimeFeedUrls = [];
|
|
2340
|
+
this.stalenessThreshold = 120;
|
|
2341
|
+
this.lastRealtimeFetchTimestamp = null;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Create GtfsSqlJs instance from GTFS ZIP file
|
|
2345
|
+
*/
|
|
2346
|
+
static async fromZip(zipPath, options = {}) {
|
|
2347
|
+
const instance = new _GtfsSqlJs();
|
|
2348
|
+
await instance.initFromZip(zipPath, options);
|
|
2349
|
+
return instance;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Create GtfsSqlJs instance from existing SQLite database
|
|
2353
|
+
*/
|
|
2354
|
+
static async fromDatabase(database, options = {}) {
|
|
2355
|
+
const instance = new _GtfsSqlJs();
|
|
2356
|
+
await instance.initFromDatabase(database, options);
|
|
2357
|
+
return instance;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Initialize from ZIP file
|
|
2361
|
+
*/
|
|
2362
|
+
async initFromZip(zipPath, options) {
|
|
2363
|
+
const onProgress = options.onProgress;
|
|
2364
|
+
const {
|
|
2365
|
+
cache: userCache,
|
|
2366
|
+
cacheVersion = "1.0",
|
|
2367
|
+
cacheExpirationMs = DEFAULT_CACHE_EXPIRATION_MS,
|
|
2368
|
+
skipFiles
|
|
2369
|
+
} = options;
|
|
2370
|
+
this.SQL = options.SQL || await initSqlJs(options.locateFile ? { locateFile: options.locateFile } : {});
|
|
2371
|
+
const cache = userCache === null ? null : userCache || null;
|
|
2372
|
+
if (cache) {
|
|
2373
|
+
onProgress?.({
|
|
2374
|
+
phase: "checking_cache",
|
|
2375
|
+
currentFile: null,
|
|
2376
|
+
filesCompleted: 0,
|
|
2377
|
+
totalFiles: 0,
|
|
2378
|
+
rowsProcessed: 0,
|
|
2379
|
+
totalRows: 0,
|
|
2380
|
+
percentComplete: 0,
|
|
2381
|
+
message: "Checking cache..."
|
|
2382
|
+
});
|
|
2383
|
+
let zipData2;
|
|
2384
|
+
if (typeof zipPath === "string") {
|
|
2385
|
+
zipData2 = await fetchZip(zipPath, onProgress);
|
|
2386
|
+
} else {
|
|
2387
|
+
zipData2 = zipPath;
|
|
2388
|
+
}
|
|
2389
|
+
const filesize = zipData2.byteLength;
|
|
2390
|
+
const checksum = await computeZipChecksum(zipData2);
|
|
2391
|
+
const cacheKey = generateCacheKey(
|
|
2392
|
+
checksum,
|
|
2393
|
+
LIB_VERSION,
|
|
2394
|
+
cacheVersion,
|
|
2395
|
+
filesize,
|
|
2396
|
+
typeof zipPath === "string" ? zipPath : void 0,
|
|
2397
|
+
skipFiles
|
|
2398
|
+
);
|
|
2399
|
+
const cacheEntry = await cache.get(cacheKey);
|
|
2400
|
+
if (cacheEntry) {
|
|
2401
|
+
const expired = isCacheExpired(cacheEntry.metadata, cacheExpirationMs);
|
|
2402
|
+
if (expired) {
|
|
2403
|
+
onProgress?.({
|
|
2404
|
+
phase: "checking_cache",
|
|
2405
|
+
currentFile: null,
|
|
2406
|
+
filesCompleted: 0,
|
|
2407
|
+
totalFiles: 0,
|
|
2408
|
+
rowsProcessed: 0,
|
|
2409
|
+
totalRows: 0,
|
|
2410
|
+
percentComplete: 2,
|
|
2411
|
+
message: "Cache expired, reprocessing..."
|
|
2412
|
+
});
|
|
2413
|
+
await cache.delete(cacheKey);
|
|
2414
|
+
} else {
|
|
2415
|
+
onProgress?.({
|
|
2416
|
+
phase: "loading_from_cache",
|
|
2417
|
+
currentFile: null,
|
|
2418
|
+
filesCompleted: 0,
|
|
2419
|
+
totalFiles: 0,
|
|
2420
|
+
rowsProcessed: 0,
|
|
2421
|
+
totalRows: 0,
|
|
2422
|
+
percentComplete: 50,
|
|
2423
|
+
message: "Loading from cache..."
|
|
2424
|
+
});
|
|
2425
|
+
this.db = new this.SQL.Database(new Uint8Array(cacheEntry.data));
|
|
2426
|
+
if (options.realtimeFeedUrls) {
|
|
2427
|
+
this.realtimeFeedUrls = options.realtimeFeedUrls;
|
|
2428
|
+
}
|
|
2429
|
+
if (options.stalenessThreshold !== void 0) {
|
|
2430
|
+
this.stalenessThreshold = options.stalenessThreshold;
|
|
2431
|
+
}
|
|
2432
|
+
onProgress?.({
|
|
2433
|
+
phase: "complete",
|
|
2434
|
+
currentFile: null,
|
|
2435
|
+
filesCompleted: 0,
|
|
2436
|
+
totalFiles: 0,
|
|
2437
|
+
rowsProcessed: 0,
|
|
2438
|
+
totalRows: 0,
|
|
2439
|
+
percentComplete: 100,
|
|
2440
|
+
message: "GTFS data loaded from cache"
|
|
2441
|
+
});
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
await this.loadFromZipData(zipData2, options, onProgress);
|
|
2446
|
+
onProgress?.({
|
|
2447
|
+
phase: "saving_cache",
|
|
2448
|
+
currentFile: null,
|
|
2449
|
+
filesCompleted: 0,
|
|
2450
|
+
totalFiles: 0,
|
|
2451
|
+
rowsProcessed: 0,
|
|
2452
|
+
totalRows: 0,
|
|
2453
|
+
percentComplete: 98,
|
|
2454
|
+
message: "Saving to cache..."
|
|
2455
|
+
});
|
|
2456
|
+
const dbBuffer = this.export();
|
|
2457
|
+
await cache.set(cacheKey, dbBuffer, {
|
|
2458
|
+
checksum,
|
|
2459
|
+
version: cacheVersion,
|
|
2460
|
+
timestamp: Date.now(),
|
|
2461
|
+
source: typeof zipPath === "string" ? zipPath : void 0,
|
|
2462
|
+
size: dbBuffer.byteLength,
|
|
2463
|
+
skipFiles
|
|
2464
|
+
});
|
|
2465
|
+
onProgress?.({
|
|
2466
|
+
phase: "complete",
|
|
2467
|
+
currentFile: null,
|
|
2468
|
+
filesCompleted: 0,
|
|
2469
|
+
totalFiles: 0,
|
|
2470
|
+
rowsProcessed: 0,
|
|
2471
|
+
totalRows: 0,
|
|
2472
|
+
percentComplete: 100,
|
|
2473
|
+
message: "GTFS data loaded successfully"
|
|
2474
|
+
});
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
let zipData;
|
|
2478
|
+
if (typeof zipPath === "string") {
|
|
2479
|
+
zipData = await fetchZip(zipPath, onProgress);
|
|
2480
|
+
} else {
|
|
2481
|
+
zipData = zipPath;
|
|
2482
|
+
}
|
|
2483
|
+
await this.loadFromZipData(zipData, options, onProgress);
|
|
2484
|
+
onProgress?.({
|
|
2485
|
+
phase: "complete",
|
|
2486
|
+
currentFile: null,
|
|
2487
|
+
filesCompleted: 0,
|
|
2488
|
+
totalFiles: 0,
|
|
2489
|
+
rowsProcessed: 0,
|
|
2490
|
+
totalRows: 0,
|
|
2491
|
+
percentComplete: 100,
|
|
2492
|
+
message: "GTFS data loaded successfully"
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Helper method to load GTFS data from zip data (ArrayBuffer)
|
|
2497
|
+
* Used by both cache-enabled and cache-disabled paths
|
|
2498
|
+
*/
|
|
2499
|
+
async loadFromZipData(zipData, options, onProgress) {
|
|
2500
|
+
this.db = new this.SQL.Database();
|
|
2501
|
+
this.db.run("PRAGMA synchronous = OFF");
|
|
2502
|
+
this.db.run("PRAGMA journal_mode = MEMORY");
|
|
2503
|
+
this.db.run("PRAGMA temp_store = MEMORY");
|
|
2504
|
+
this.db.run("PRAGMA cache_size = -64000");
|
|
2505
|
+
this.db.run("PRAGMA locking_mode = EXCLUSIVE");
|
|
2506
|
+
onProgress?.({
|
|
2507
|
+
phase: "creating_schema",
|
|
2508
|
+
currentFile: null,
|
|
2509
|
+
filesCompleted: 0,
|
|
2510
|
+
totalFiles: 0,
|
|
2511
|
+
rowsProcessed: 0,
|
|
2512
|
+
totalRows: 0,
|
|
2513
|
+
percentComplete: 40,
|
|
2514
|
+
message: "Creating database tables"
|
|
2515
|
+
});
|
|
2516
|
+
const createTableStatements = getAllCreateTableStatements();
|
|
2517
|
+
for (const statement of createTableStatements) {
|
|
2518
|
+
this.db.run(statement);
|
|
2519
|
+
}
|
|
2520
|
+
createRealtimeTables(this.db);
|
|
2521
|
+
onProgress?.({
|
|
2522
|
+
phase: "extracting",
|
|
2523
|
+
currentFile: null,
|
|
2524
|
+
filesCompleted: 0,
|
|
2525
|
+
totalFiles: 0,
|
|
2526
|
+
rowsProcessed: 0,
|
|
2527
|
+
totalRows: 0,
|
|
2528
|
+
percentComplete: 35,
|
|
2529
|
+
message: "Extracting GTFS ZIP file"
|
|
2530
|
+
});
|
|
2531
|
+
const files = await loadGTFSZip(zipData);
|
|
2532
|
+
onProgress?.({
|
|
2533
|
+
phase: "inserting_data",
|
|
2534
|
+
currentFile: null,
|
|
2535
|
+
filesCompleted: 0,
|
|
2536
|
+
totalFiles: Object.keys(files).length,
|
|
2537
|
+
rowsProcessed: 0,
|
|
2538
|
+
totalRows: 0,
|
|
2539
|
+
percentComplete: 40,
|
|
2540
|
+
message: "Starting data import"
|
|
2541
|
+
});
|
|
2542
|
+
await loadGTFSData(this.db, files, options.skipFiles, onProgress);
|
|
2543
|
+
onProgress?.({
|
|
2544
|
+
phase: "creating_indexes",
|
|
2545
|
+
currentFile: null,
|
|
2546
|
+
filesCompleted: Object.keys(files).length,
|
|
2547
|
+
totalFiles: Object.keys(files).length,
|
|
2548
|
+
rowsProcessed: 0,
|
|
2549
|
+
totalRows: 0,
|
|
2550
|
+
percentComplete: 75,
|
|
2551
|
+
message: "Creating database indexes"
|
|
2552
|
+
});
|
|
2553
|
+
const createIndexStatements = getAllCreateIndexStatements();
|
|
2554
|
+
let indexCount = 0;
|
|
2555
|
+
for (const statement of createIndexStatements) {
|
|
2556
|
+
this.db.run(statement);
|
|
2557
|
+
indexCount++;
|
|
2558
|
+
const indexProgress = 75 + Math.floor(indexCount / createIndexStatements.length * 10);
|
|
2559
|
+
onProgress?.({
|
|
2560
|
+
phase: "creating_indexes",
|
|
2561
|
+
currentFile: null,
|
|
2562
|
+
filesCompleted: Object.keys(files).length,
|
|
2563
|
+
totalFiles: Object.keys(files).length,
|
|
2564
|
+
rowsProcessed: 0,
|
|
2565
|
+
totalRows: 0,
|
|
2566
|
+
percentComplete: indexProgress,
|
|
2567
|
+
message: `Creating indexes (${indexCount}/${createIndexStatements.length})`
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
onProgress?.({
|
|
2571
|
+
phase: "analyzing",
|
|
2572
|
+
currentFile: null,
|
|
2573
|
+
filesCompleted: Object.keys(files).length,
|
|
2574
|
+
totalFiles: Object.keys(files).length,
|
|
2575
|
+
rowsProcessed: 0,
|
|
2576
|
+
totalRows: 0,
|
|
2577
|
+
percentComplete: 85,
|
|
2578
|
+
message: "Optimizing query performance"
|
|
2579
|
+
});
|
|
2580
|
+
this.db.run("ANALYZE");
|
|
2581
|
+
this.db.run("PRAGMA synchronous = FULL");
|
|
2582
|
+
this.db.run("PRAGMA locking_mode = NORMAL");
|
|
2583
|
+
if (options.realtimeFeedUrls) {
|
|
2584
|
+
this.realtimeFeedUrls = options.realtimeFeedUrls;
|
|
2585
|
+
}
|
|
2586
|
+
if (options.stalenessThreshold !== void 0) {
|
|
2587
|
+
this.stalenessThreshold = options.stalenessThreshold;
|
|
2588
|
+
}
|
|
2589
|
+
if (this.realtimeFeedUrls.length > 0) {
|
|
2590
|
+
onProgress?.({
|
|
2591
|
+
phase: "loading_realtime",
|
|
2592
|
+
currentFile: null,
|
|
2593
|
+
filesCompleted: 0,
|
|
2594
|
+
totalFiles: this.realtimeFeedUrls.length,
|
|
2595
|
+
rowsProcessed: 0,
|
|
2596
|
+
totalRows: 0,
|
|
2597
|
+
percentComplete: 90,
|
|
2598
|
+
message: `Loading realtime data from ${this.realtimeFeedUrls.length} feed${this.realtimeFeedUrls.length > 1 ? "s" : ""}`
|
|
2599
|
+
});
|
|
2600
|
+
try {
|
|
2601
|
+
await loadRealtimeData(this.db, this.realtimeFeedUrls);
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
console.warn("Failed to fetch initial realtime data:", error);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Initialize from existing database
|
|
2609
|
+
*/
|
|
2610
|
+
async initFromDatabase(database, options) {
|
|
2611
|
+
this.SQL = options.SQL || await initSqlJs(options.locateFile ? { locateFile: options.locateFile } : {});
|
|
2612
|
+
this.db = new this.SQL.Database(new Uint8Array(database));
|
|
2613
|
+
createRealtimeTables(this.db);
|
|
2614
|
+
if (options.realtimeFeedUrls) {
|
|
2615
|
+
this.realtimeFeedUrls = options.realtimeFeedUrls;
|
|
2616
|
+
}
|
|
2617
|
+
if (options.stalenessThreshold !== void 0) {
|
|
2618
|
+
this.stalenessThreshold = options.stalenessThreshold;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Export database to ArrayBuffer
|
|
2623
|
+
*/
|
|
2624
|
+
export() {
|
|
2625
|
+
if (!this.db) {
|
|
2626
|
+
throw new Error("Database not initialized");
|
|
2627
|
+
}
|
|
2628
|
+
const data = this.db.export();
|
|
2629
|
+
const buffer = new ArrayBuffer(data.length);
|
|
2630
|
+
new Uint8Array(buffer).set(data);
|
|
2631
|
+
return buffer;
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Close the database connection
|
|
2635
|
+
*/
|
|
2636
|
+
close() {
|
|
2637
|
+
if (this.db) {
|
|
2638
|
+
this.db.close();
|
|
2639
|
+
this.db = null;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Get direct access to the database (for advanced queries)
|
|
2644
|
+
*/
|
|
2645
|
+
getDatabase() {
|
|
2646
|
+
if (!this.db) {
|
|
2647
|
+
throw new Error("Database not initialized");
|
|
2648
|
+
}
|
|
2649
|
+
return this.db;
|
|
2650
|
+
}
|
|
2651
|
+
// ==================== Agency Methods ====================
|
|
2652
|
+
/**
|
|
2653
|
+
* Get agencies with optional filters
|
|
2654
|
+
* Pass agencyId filter to get a specific agency
|
|
2655
|
+
*/
|
|
2656
|
+
getAgencies(filters) {
|
|
2657
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2658
|
+
return getAgencies(this.db, filters);
|
|
2659
|
+
}
|
|
2660
|
+
// ==================== Stop Methods ====================
|
|
2661
|
+
/**
|
|
2662
|
+
* Get stops with optional filters
|
|
2663
|
+
* Pass stopId filter to get a specific stop
|
|
2664
|
+
*/
|
|
2665
|
+
getStops(filters) {
|
|
2666
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2667
|
+
return getStops(this.db, filters);
|
|
2668
|
+
}
|
|
2669
|
+
// ==================== Route Methods ====================
|
|
2670
|
+
/**
|
|
2671
|
+
* Get routes with optional filters
|
|
2672
|
+
* Pass routeId filter to get a specific route
|
|
2673
|
+
*/
|
|
2674
|
+
getRoutes(filters) {
|
|
2675
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2676
|
+
return getRoutes(this.db, filters);
|
|
2677
|
+
}
|
|
2678
|
+
// ==================== Calendar Methods ====================
|
|
2679
|
+
/**
|
|
2680
|
+
* Get active service IDs for a given date (YYYYMMDD format)
|
|
2681
|
+
*/
|
|
2682
|
+
getActiveServiceIds(date) {
|
|
2683
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2684
|
+
return getActiveServiceIds(this.db, date);
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Get calendar entry by service_id
|
|
2688
|
+
*/
|
|
2689
|
+
getCalendarByServiceId(serviceId) {
|
|
2690
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2691
|
+
return getCalendarByServiceId(this.db, serviceId);
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Get calendar date exceptions for a service
|
|
2695
|
+
*/
|
|
2696
|
+
getCalendarDates(serviceId) {
|
|
2697
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2698
|
+
return getCalendarDates(this.db, serviceId);
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Get calendar date exceptions for a specific date
|
|
2702
|
+
*/
|
|
2703
|
+
getCalendarDatesForDate(date) {
|
|
2704
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2705
|
+
return getCalendarDatesForDate(this.db, date);
|
|
2706
|
+
}
|
|
2707
|
+
// ==================== Trip Methods ====================
|
|
2708
|
+
/**
|
|
2709
|
+
* Get trips with optional filters
|
|
2710
|
+
* Pass tripId filter to get a specific trip
|
|
2711
|
+
*
|
|
2712
|
+
* @param filters - Optional filters
|
|
2713
|
+
* @param filters.tripId - Filter by trip ID (single value or array)
|
|
2714
|
+
* @param filters.routeId - Filter by route ID (single value or array)
|
|
2715
|
+
* @param filters.date - Filter by date (YYYYMMDD format) - will get active services for that date
|
|
2716
|
+
* @param filters.directionId - Filter by direction ID (single value or array)
|
|
2717
|
+
* @param filters.agencyId - Filter by agency ID (single value or array)
|
|
2718
|
+
* @param filters.limit - Limit number of results
|
|
2719
|
+
*
|
|
2720
|
+
* @example
|
|
2721
|
+
* // Get all trips for a route on a specific date
|
|
2722
|
+
* const trips = gtfs.getTrips({ routeId: 'ROUTE_1', date: '20240115' });
|
|
2723
|
+
*
|
|
2724
|
+
* @example
|
|
2725
|
+
* // Get all trips for a route going in one direction
|
|
2726
|
+
* const trips = gtfs.getTrips({ routeId: 'ROUTE_1', directionId: 0 });
|
|
2727
|
+
*
|
|
2728
|
+
* @example
|
|
2729
|
+
* // Get a specific trip
|
|
2730
|
+
* const trips = gtfs.getTrips({ tripId: 'TRIP_123' });
|
|
2731
|
+
*/
|
|
2732
|
+
getTrips(filters) {
|
|
2733
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2734
|
+
const { date, ...restFilters } = filters || {};
|
|
2735
|
+
const finalFilters = { ...restFilters };
|
|
2736
|
+
if (date) {
|
|
2737
|
+
const serviceIds = getActiveServiceIds(this.db, date);
|
|
2738
|
+
finalFilters.serviceIds = serviceIds;
|
|
2739
|
+
}
|
|
2740
|
+
return getTrips(this.db, finalFilters, this.stalenessThreshold);
|
|
2741
|
+
}
|
|
2742
|
+
// ==================== Shape Methods ====================
|
|
2743
|
+
/**
|
|
2744
|
+
* Get shapes with optional filters
|
|
2745
|
+
*
|
|
2746
|
+
* @param filters - Optional filters
|
|
2747
|
+
* @param filters.shapeId - Filter by shape ID (single value or array)
|
|
2748
|
+
* @param filters.routeId - Filter by route ID (single value or array) - joins with trips table
|
|
2749
|
+
* @param filters.tripId - Filter by trip ID (single value or array) - joins with trips table
|
|
2750
|
+
* @param filters.limit - Limit number of results
|
|
2751
|
+
*
|
|
2752
|
+
* @example
|
|
2753
|
+
* // Get all points for a specific shape
|
|
2754
|
+
* const shapes = gtfs.getShapes({ shapeId: 'SHAPE_1' });
|
|
2755
|
+
*
|
|
2756
|
+
* @example
|
|
2757
|
+
* // Get shapes for a specific route
|
|
2758
|
+
* const shapes = gtfs.getShapes({ routeId: 'ROUTE_1' });
|
|
2759
|
+
*
|
|
2760
|
+
* @example
|
|
2761
|
+
* // Get shapes for multiple trips
|
|
2762
|
+
* const shapes = gtfs.getShapes({ tripId: ['TRIP_1', 'TRIP_2'] });
|
|
2763
|
+
*/
|
|
2764
|
+
getShapes(filters) {
|
|
2765
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2766
|
+
return getShapes(this.db, filters);
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Get shapes as GeoJSON FeatureCollection
|
|
2770
|
+
*
|
|
2771
|
+
* Each shape is converted to a LineString Feature with route properties.
|
|
2772
|
+
* Coordinates are in [longitude, latitude] format per GeoJSON spec.
|
|
2773
|
+
*
|
|
2774
|
+
* @param filters - Optional filters (same as getShapes)
|
|
2775
|
+
* @param filters.shapeId - Filter by shape ID (single value or array)
|
|
2776
|
+
* @param filters.routeId - Filter by route ID (single value or array)
|
|
2777
|
+
* @param filters.tripId - Filter by trip ID (single value or array)
|
|
2778
|
+
* @param filters.limit - Limit number of results
|
|
2779
|
+
* @param precision - Number of decimal places for coordinates (default: 6, ~10cm precision)
|
|
2780
|
+
*
|
|
2781
|
+
* @returns GeoJSON FeatureCollection with LineString features
|
|
2782
|
+
*
|
|
2783
|
+
* @example
|
|
2784
|
+
* // Get all shapes as GeoJSON
|
|
2785
|
+
* const geojson = gtfs.getShapesToGeojson();
|
|
2786
|
+
*
|
|
2787
|
+
* @example
|
|
2788
|
+
* // Get shapes for a route with lower precision
|
|
2789
|
+
* const geojson = gtfs.getShapesToGeojson({ routeId: 'ROUTE_1' }, 5);
|
|
2790
|
+
*
|
|
2791
|
+
* @example
|
|
2792
|
+
* // Result structure:
|
|
2793
|
+
* // {
|
|
2794
|
+
* // type: 'FeatureCollection',
|
|
2795
|
+
* // features: [{
|
|
2796
|
+
* // type: 'Feature',
|
|
2797
|
+
* // properties: {
|
|
2798
|
+
* // shape_id: 'SHAPE_1',
|
|
2799
|
+
* // route_id: 'ROUTE_1',
|
|
2800
|
+
* // route_short_name: '1',
|
|
2801
|
+
* // route_long_name: 'Main Street',
|
|
2802
|
+
* // route_type: 3,
|
|
2803
|
+
* // route_color: 'FF0000'
|
|
2804
|
+
* // },
|
|
2805
|
+
* // geometry: {
|
|
2806
|
+
* // type: 'LineString',
|
|
2807
|
+
* // coordinates: [[-122.123456, 37.123456], ...]
|
|
2808
|
+
* // }
|
|
2809
|
+
* // }]
|
|
2810
|
+
* // }
|
|
2811
|
+
*/
|
|
2812
|
+
getShapesToGeojson(filters, precision = 6) {
|
|
2813
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2814
|
+
return getShapesToGeojson(this.db, filters, precision);
|
|
2815
|
+
}
|
|
2816
|
+
// ==================== Stop Time Methods ====================
|
|
2817
|
+
/**
|
|
2818
|
+
* Get stop times with optional filters
|
|
2819
|
+
*
|
|
2820
|
+
* @param filters - Optional filters
|
|
2821
|
+
* @param filters.tripId - Filter by trip ID (single value or array)
|
|
2822
|
+
* @param filters.stopId - Filter by stop ID (single value or array)
|
|
2823
|
+
* @param filters.routeId - Filter by route ID (single value or array)
|
|
2824
|
+
* @param filters.date - Filter by date (YYYYMMDD format) - will get active services for that date
|
|
2825
|
+
* @param filters.directionId - Filter by direction ID (single value or array)
|
|
2826
|
+
* @param filters.agencyId - Filter by agency ID (single value or array)
|
|
2827
|
+
* @param filters.includeRealtime - Include realtime data (delay and time fields)
|
|
2828
|
+
* @param filters.limit - Limit number of results
|
|
2829
|
+
*
|
|
2830
|
+
* @example
|
|
2831
|
+
* // Get stop times for a specific trip
|
|
2832
|
+
* const stopTimes = gtfs.getStopTimes({ tripId: 'TRIP_123' });
|
|
2833
|
+
*
|
|
2834
|
+
* @example
|
|
2835
|
+
* // Get stop times at a stop for a specific route on a date
|
|
2836
|
+
* const stopTimes = gtfs.getStopTimes({
|
|
2837
|
+
* stopId: 'STOP_123',
|
|
2838
|
+
* routeId: 'ROUTE_1',
|
|
2839
|
+
* date: '20240115'
|
|
2840
|
+
* });
|
|
2841
|
+
*
|
|
2842
|
+
* @example
|
|
2843
|
+
* // Get stop times with realtime data
|
|
2844
|
+
* const stopTimes = gtfs.getStopTimes({
|
|
2845
|
+
* tripId: 'TRIP_123',
|
|
2846
|
+
* includeRealtime: true
|
|
2847
|
+
* });
|
|
2848
|
+
*/
|
|
2849
|
+
getStopTimes(filters) {
|
|
2850
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2851
|
+
const { date, ...restFilters } = filters || {};
|
|
2852
|
+
const finalFilters = { ...restFilters };
|
|
2853
|
+
if (date) {
|
|
2854
|
+
const serviceIds = getActiveServiceIds(this.db, date);
|
|
2855
|
+
finalFilters.serviceIds = serviceIds;
|
|
2856
|
+
}
|
|
2857
|
+
return getStopTimes(this.db, finalFilters, this.stalenessThreshold);
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Build an ordered list of stops from multiple trips
|
|
2861
|
+
*
|
|
2862
|
+
* This is useful when you need to display a timetable for a route where different trips
|
|
2863
|
+
* may stop at different sets of stops (e.g., express vs local service, or trips with
|
|
2864
|
+
* different start/end points).
|
|
2865
|
+
*
|
|
2866
|
+
* The method intelligently merges stop sequences from all provided trips to create
|
|
2867
|
+
* a comprehensive ordered list of all unique stops.
|
|
2868
|
+
*
|
|
2869
|
+
* @param tripIds - Array of trip IDs to analyze
|
|
2870
|
+
* @returns Ordered array of Stop objects representing all unique stops
|
|
2871
|
+
*
|
|
2872
|
+
* @example
|
|
2873
|
+
* // Get all trips for a route going in one direction
|
|
2874
|
+
* const trips = gtfs.getTrips({ routeId: 'ROUTE_1', directionId: 0 });
|
|
2875
|
+
* const tripIds = trips.map(t => t.trip_id);
|
|
2876
|
+
*
|
|
2877
|
+
* // Build ordered stop list for all these trips
|
|
2878
|
+
* const stops = gtfs.buildOrderedStopList(tripIds);
|
|
2879
|
+
*
|
|
2880
|
+
* // Now you can display a timetable with all possible stops
|
|
2881
|
+
* stops.forEach(stop => {
|
|
2882
|
+
* console.log(stop.stop_name);
|
|
2883
|
+
* });
|
|
2884
|
+
*/
|
|
2885
|
+
buildOrderedStopList(tripIds) {
|
|
2886
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2887
|
+
return buildOrderedStopList(this.db, tripIds);
|
|
2888
|
+
}
|
|
2889
|
+
// ==================== Realtime Methods ====================
|
|
2890
|
+
/**
|
|
2891
|
+
* Set GTFS-RT feed URLs
|
|
2892
|
+
*/
|
|
2893
|
+
setRealtimeFeedUrls(urls) {
|
|
2894
|
+
this.realtimeFeedUrls = urls;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Get currently configured GTFS-RT feed URLs
|
|
2898
|
+
*/
|
|
2899
|
+
getRealtimeFeedUrls() {
|
|
2900
|
+
return [...this.realtimeFeedUrls];
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Set staleness threshold in seconds
|
|
2904
|
+
*/
|
|
2905
|
+
setStalenessThreshold(seconds) {
|
|
2906
|
+
this.stalenessThreshold = seconds;
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Get current staleness threshold
|
|
2910
|
+
*/
|
|
2911
|
+
getStalenessThreshold() {
|
|
2912
|
+
return this.stalenessThreshold;
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Get timestamp of the last successful realtime data fetch and insertion
|
|
2916
|
+
* @returns Unix timestamp in seconds, or null if no realtime data has been fetched
|
|
2917
|
+
*/
|
|
2918
|
+
getLastRealtimeFetchTimestamp() {
|
|
2919
|
+
return this.lastRealtimeFetchTimestamp;
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Fetch and load GTFS Realtime data from configured feed URLs or provided URLs
|
|
2923
|
+
* @param urls - Optional array of feed URLs. If not provided, uses configured feed URLs
|
|
2924
|
+
*/
|
|
2925
|
+
async fetchRealtimeData(urls) {
|
|
2926
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2927
|
+
const feedUrls = urls || this.realtimeFeedUrls;
|
|
2928
|
+
if (feedUrls.length === 0) {
|
|
2929
|
+
throw new Error("No realtime feed URLs configured. Use setRealtimeFeedUrls() or pass urls parameter.");
|
|
2930
|
+
}
|
|
2931
|
+
await loadRealtimeData(this.db, feedUrls);
|
|
2932
|
+
this.lastRealtimeFetchTimestamp = Math.floor(Date.now() / 1e3);
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Clear all realtime data from the database
|
|
2936
|
+
*/
|
|
2937
|
+
clearRealtimeData() {
|
|
2938
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2939
|
+
clearRealtimeData(this.db);
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Get alerts with optional filters
|
|
2943
|
+
* Pass alertId filter to get a specific alert
|
|
2944
|
+
*/
|
|
2945
|
+
getAlerts(filters) {
|
|
2946
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2947
|
+
return getAlerts(this.db, filters, this.stalenessThreshold);
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Get vehicle positions with optional filters
|
|
2951
|
+
* Pass tripId filter to get vehicle position for a specific trip
|
|
2952
|
+
*/
|
|
2953
|
+
getVehiclePositions(filters) {
|
|
2954
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2955
|
+
return getVehiclePositions(this.db, filters, this.stalenessThreshold);
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Get trip updates with optional filters
|
|
2959
|
+
* Pass tripId filter to get trip update for a specific trip
|
|
2960
|
+
*/
|
|
2961
|
+
getTripUpdates(filters) {
|
|
2962
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2963
|
+
return getTripUpdates(this.db, filters, this.stalenessThreshold);
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Get stop time updates with optional filters
|
|
2967
|
+
* Pass tripId filter to get stop time updates for a specific trip
|
|
2968
|
+
*/
|
|
2969
|
+
getStopTimeUpdates(filters) {
|
|
2970
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2971
|
+
return getStopTimeUpdates(this.db, filters, this.stalenessThreshold);
|
|
2972
|
+
}
|
|
2973
|
+
// ==================== Debug Export Methods ====================
|
|
2974
|
+
// These methods export all realtime data without staleness filtering
|
|
2975
|
+
// for debugging purposes
|
|
2976
|
+
/**
|
|
2977
|
+
* Export all alerts without staleness filtering (for debugging)
|
|
2978
|
+
*/
|
|
2979
|
+
debugExportAllAlerts() {
|
|
2980
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2981
|
+
return getAllAlerts(this.db);
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Export all vehicle positions without staleness filtering (for debugging)
|
|
2985
|
+
*/
|
|
2986
|
+
debugExportAllVehiclePositions() {
|
|
2987
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2988
|
+
return getAllVehiclePositions(this.db);
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Export all trip updates without staleness filtering (for debugging)
|
|
2992
|
+
*/
|
|
2993
|
+
debugExportAllTripUpdates() {
|
|
2994
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
2995
|
+
return getAllTripUpdates(this.db);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Export all stop time updates without staleness filtering (for debugging)
|
|
2999
|
+
* Returns stop time updates with trip_id and rt_last_updated populated
|
|
3000
|
+
*/
|
|
3001
|
+
debugExportAllStopTimeUpdates() {
|
|
3002
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
3003
|
+
return getAllStopTimeUpdates(this.db);
|
|
3004
|
+
}
|
|
3005
|
+
// ==================== Cache Management Methods ====================
|
|
3006
|
+
/**
|
|
3007
|
+
* Get cache statistics
|
|
3008
|
+
* @param cacheStore - Cache store to query (required)
|
|
3009
|
+
* @returns Cache statistics including size, entry count, and age information
|
|
3010
|
+
*/
|
|
3011
|
+
static async getCacheStats(cacheStore) {
|
|
3012
|
+
const { getCacheStats: getCacheStats2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3013
|
+
if (!cacheStore) {
|
|
3014
|
+
throw new Error("Cache store is required");
|
|
3015
|
+
}
|
|
3016
|
+
const entries = await cacheStore.list?.() || [];
|
|
3017
|
+
return getCacheStats2(entries);
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Clean expired cache entries
|
|
3021
|
+
* @param cacheStore - Cache store to clean (required)
|
|
3022
|
+
* @param expirationMs - Expiration time in milliseconds (default: 7 days)
|
|
3023
|
+
* @returns Number of entries deleted
|
|
3024
|
+
*/
|
|
3025
|
+
static async cleanExpiredCache(cacheStore, expirationMs = DEFAULT_CACHE_EXPIRATION_MS) {
|
|
3026
|
+
const { filterExpiredEntries: filterExpiredEntries2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3027
|
+
if (!cacheStore || !cacheStore.list) {
|
|
3028
|
+
throw new Error("Cache store is required and must support listing");
|
|
3029
|
+
}
|
|
3030
|
+
const allEntries = await cacheStore.list();
|
|
3031
|
+
const expiredEntries = allEntries.filter(
|
|
3032
|
+
(entry) => !filterExpiredEntries2([entry], expirationMs).length
|
|
3033
|
+
);
|
|
3034
|
+
await Promise.all(expiredEntries.map((entry) => cacheStore.delete(entry.key)));
|
|
3035
|
+
return expiredEntries.length;
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* Clear all cache entries
|
|
3039
|
+
* @param cacheStore - Cache store to clear (required)
|
|
3040
|
+
*/
|
|
3041
|
+
static async clearCache(cacheStore) {
|
|
3042
|
+
if (!cacheStore) {
|
|
3043
|
+
throw new Error("Cache store is required");
|
|
3044
|
+
}
|
|
3045
|
+
await cacheStore.clear();
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* List all cache entries
|
|
3049
|
+
* @param cacheStore - Cache store to query (required)
|
|
3050
|
+
* @param includeExpired - Include expired entries (default: false)
|
|
3051
|
+
* @returns Array of cache entries with metadata
|
|
3052
|
+
*/
|
|
3053
|
+
static async listCache(cacheStore, includeExpired = false) {
|
|
3054
|
+
const { filterExpiredEntries: filterExpiredEntries2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3055
|
+
if (!cacheStore || !cacheStore.list) {
|
|
3056
|
+
throw new Error("Cache store is required and must support listing");
|
|
3057
|
+
}
|
|
3058
|
+
const entries = await cacheStore.list();
|
|
3059
|
+
if (includeExpired) {
|
|
3060
|
+
return entries;
|
|
3061
|
+
}
|
|
3062
|
+
return filterExpiredEntries2(entries);
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
// src/types/gtfs-rt.ts
|
|
3067
|
+
var ScheduleRelationship = /* @__PURE__ */ ((ScheduleRelationship2) => {
|
|
3068
|
+
ScheduleRelationship2[ScheduleRelationship2["SCHEDULED"] = 0] = "SCHEDULED";
|
|
3069
|
+
ScheduleRelationship2[ScheduleRelationship2["ADDED"] = 1] = "ADDED";
|
|
3070
|
+
ScheduleRelationship2[ScheduleRelationship2["UNSCHEDULED"] = 2] = "UNSCHEDULED";
|
|
3071
|
+
ScheduleRelationship2[ScheduleRelationship2["CANCELED"] = 3] = "CANCELED";
|
|
3072
|
+
ScheduleRelationship2[ScheduleRelationship2["SKIPPED"] = 4] = "SKIPPED";
|
|
3073
|
+
ScheduleRelationship2[ScheduleRelationship2["NO_DATA"] = 5] = "NO_DATA";
|
|
3074
|
+
return ScheduleRelationship2;
|
|
3075
|
+
})(ScheduleRelationship || {});
|
|
3076
|
+
var VehicleStopStatus = /* @__PURE__ */ ((VehicleStopStatus2) => {
|
|
3077
|
+
VehicleStopStatus2[VehicleStopStatus2["INCOMING_AT"] = 0] = "INCOMING_AT";
|
|
3078
|
+
VehicleStopStatus2[VehicleStopStatus2["STOPPED_AT"] = 1] = "STOPPED_AT";
|
|
3079
|
+
VehicleStopStatus2[VehicleStopStatus2["IN_TRANSIT_TO"] = 2] = "IN_TRANSIT_TO";
|
|
3080
|
+
return VehicleStopStatus2;
|
|
3081
|
+
})(VehicleStopStatus || {});
|
|
3082
|
+
var CongestionLevel = /* @__PURE__ */ ((CongestionLevel2) => {
|
|
3083
|
+
CongestionLevel2[CongestionLevel2["UNKNOWN_CONGESTION_LEVEL"] = 0] = "UNKNOWN_CONGESTION_LEVEL";
|
|
3084
|
+
CongestionLevel2[CongestionLevel2["RUNNING_SMOOTHLY"] = 1] = "RUNNING_SMOOTHLY";
|
|
3085
|
+
CongestionLevel2[CongestionLevel2["STOP_AND_GO"] = 2] = "STOP_AND_GO";
|
|
3086
|
+
CongestionLevel2[CongestionLevel2["CONGESTION"] = 3] = "CONGESTION";
|
|
3087
|
+
CongestionLevel2[CongestionLevel2["SEVERE_CONGESTION"] = 4] = "SEVERE_CONGESTION";
|
|
3088
|
+
return CongestionLevel2;
|
|
3089
|
+
})(CongestionLevel || {});
|
|
3090
|
+
var OccupancyStatus = /* @__PURE__ */ ((OccupancyStatus2) => {
|
|
3091
|
+
OccupancyStatus2[OccupancyStatus2["EMPTY"] = 0] = "EMPTY";
|
|
3092
|
+
OccupancyStatus2[OccupancyStatus2["MANY_SEATS_AVAILABLE"] = 1] = "MANY_SEATS_AVAILABLE";
|
|
3093
|
+
OccupancyStatus2[OccupancyStatus2["FEW_SEATS_AVAILABLE"] = 2] = "FEW_SEATS_AVAILABLE";
|
|
3094
|
+
OccupancyStatus2[OccupancyStatus2["STANDING_ROOM_ONLY"] = 3] = "STANDING_ROOM_ONLY";
|
|
3095
|
+
OccupancyStatus2[OccupancyStatus2["CRUSHED_STANDING_ROOM_ONLY"] = 4] = "CRUSHED_STANDING_ROOM_ONLY";
|
|
3096
|
+
OccupancyStatus2[OccupancyStatus2["FULL"] = 5] = "FULL";
|
|
3097
|
+
OccupancyStatus2[OccupancyStatus2["NOT_ACCEPTING_PASSENGERS"] = 6] = "NOT_ACCEPTING_PASSENGERS";
|
|
3098
|
+
return OccupancyStatus2;
|
|
3099
|
+
})(OccupancyStatus || {});
|
|
3100
|
+
var AlertCause = /* @__PURE__ */ ((AlertCause2) => {
|
|
3101
|
+
AlertCause2[AlertCause2["UNKNOWN_CAUSE"] = 1] = "UNKNOWN_CAUSE";
|
|
3102
|
+
AlertCause2[AlertCause2["OTHER_CAUSE"] = 2] = "OTHER_CAUSE";
|
|
3103
|
+
AlertCause2[AlertCause2["TECHNICAL_PROBLEM"] = 3] = "TECHNICAL_PROBLEM";
|
|
3104
|
+
AlertCause2[AlertCause2["STRIKE"] = 4] = "STRIKE";
|
|
3105
|
+
AlertCause2[AlertCause2["DEMONSTRATION"] = 5] = "DEMONSTRATION";
|
|
3106
|
+
AlertCause2[AlertCause2["ACCIDENT"] = 6] = "ACCIDENT";
|
|
3107
|
+
AlertCause2[AlertCause2["HOLIDAY"] = 7] = "HOLIDAY";
|
|
3108
|
+
AlertCause2[AlertCause2["WEATHER"] = 8] = "WEATHER";
|
|
3109
|
+
AlertCause2[AlertCause2["MAINTENANCE"] = 9] = "MAINTENANCE";
|
|
3110
|
+
AlertCause2[AlertCause2["CONSTRUCTION"] = 10] = "CONSTRUCTION";
|
|
3111
|
+
AlertCause2[AlertCause2["POLICE_ACTIVITY"] = 11] = "POLICE_ACTIVITY";
|
|
3112
|
+
AlertCause2[AlertCause2["MEDICAL_EMERGENCY"] = 12] = "MEDICAL_EMERGENCY";
|
|
3113
|
+
return AlertCause2;
|
|
3114
|
+
})(AlertCause || {});
|
|
3115
|
+
var AlertEffect = /* @__PURE__ */ ((AlertEffect2) => {
|
|
3116
|
+
AlertEffect2[AlertEffect2["NO_SERVICE"] = 1] = "NO_SERVICE";
|
|
3117
|
+
AlertEffect2[AlertEffect2["REDUCED_SERVICE"] = 2] = "REDUCED_SERVICE";
|
|
3118
|
+
AlertEffect2[AlertEffect2["SIGNIFICANT_DELAYS"] = 3] = "SIGNIFICANT_DELAYS";
|
|
3119
|
+
AlertEffect2[AlertEffect2["DETOUR"] = 4] = "DETOUR";
|
|
3120
|
+
AlertEffect2[AlertEffect2["ADDITIONAL_SERVICE"] = 5] = "ADDITIONAL_SERVICE";
|
|
3121
|
+
AlertEffect2[AlertEffect2["MODIFIED_SERVICE"] = 6] = "MODIFIED_SERVICE";
|
|
3122
|
+
AlertEffect2[AlertEffect2["OTHER_EFFECT"] = 7] = "OTHER_EFFECT";
|
|
3123
|
+
AlertEffect2[AlertEffect2["UNKNOWN_EFFECT"] = 8] = "UNKNOWN_EFFECT";
|
|
3124
|
+
AlertEffect2[AlertEffect2["STOP_MOVED"] = 9] = "STOP_MOVED";
|
|
3125
|
+
AlertEffect2[AlertEffect2["NO_EFFECT"] = 10] = "NO_EFFECT";
|
|
3126
|
+
AlertEffect2[AlertEffect2["ACCESSIBILITY_ISSUE"] = 11] = "ACCESSIBILITY_ISSUE";
|
|
3127
|
+
return AlertEffect2;
|
|
3128
|
+
})(AlertEffect || {});
|
|
3129
|
+
|
|
3130
|
+
// src/index.ts
|
|
3131
|
+
init_utils();
|
|
3132
|
+
export {
|
|
3133
|
+
AlertCause,
|
|
3134
|
+
AlertEffect,
|
|
3135
|
+
CongestionLevel,
|
|
3136
|
+
DEFAULT_CACHE_EXPIRATION_MS,
|
|
3137
|
+
GTFS_SCHEMA,
|
|
3138
|
+
GtfsSqlJs,
|
|
3139
|
+
OccupancyStatus,
|
|
3140
|
+
ScheduleRelationship,
|
|
3141
|
+
VehicleStopStatus,
|
|
3142
|
+
computeChecksum,
|
|
3143
|
+
computeZipChecksum,
|
|
3144
|
+
filterExpiredEntries,
|
|
3145
|
+
generateCacheKey,
|
|
3146
|
+
getCacheStats,
|
|
3147
|
+
isCacheExpired
|
|
3148
|
+
};
|
|
3149
|
+
/**
|
|
3150
|
+
* gtfs-sqljs - Load GTFS data into sql.js SQLite database
|
|
3151
|
+
* @author Théophile Helleboid/SysDevRun
|
|
3152
|
+
* @license MIT
|
|
3153
|
+
*/
|
|
3154
|
+
//# sourceMappingURL=index.js.map
|