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/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