gtfs-to-html 2.12.5 → 2.12.7

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/app/index.js CHANGED
@@ -306,6 +306,30 @@ function formatTripNameForCSV(trip, timetable) {
306
306
  return tripName;
307
307
  }
308
308
 
309
+ // src/lib/errors.ts
310
+ import { GtfsErrorCategory, isGtfsError } from "gtfs";
311
+ var GtfsToHtmlError = class extends Error {
312
+ code;
313
+ category;
314
+ isOperational;
315
+ details;
316
+ constructor(message, options) {
317
+ super(message, { cause: options.cause });
318
+ this.name = "GtfsToHtmlError";
319
+ this.code = options.code;
320
+ this.category = options.category;
321
+ this.isOperational = options.isOperational ?? true;
322
+ this.details = options.details;
323
+ }
324
+ };
325
+ function isGtfsToHtmlError(error) {
326
+ if (!error || typeof error !== "object") {
327
+ return false;
328
+ }
329
+ const candidate = error;
330
+ return candidate.name === "GtfsToHtmlError" && typeof candidate.message === "string" && typeof candidate.code === "string" && typeof candidate.category === "string" && typeof candidate.isOperational === "boolean";
331
+ }
332
+
309
333
  // src/lib/file-utils.ts
310
334
  var homeDirectory = homedir();
311
335
  function getPathToThisModuleFolder() {
@@ -385,7 +409,7 @@ import { featureCollection, round } from "@turf/helpers";
385
409
  import { clearLine, cursorTo } from "readline";
386
410
  import { noop } from "lodash-es";
387
411
  import * as colors from "yoctocolors";
388
- import { getAgencies, getFeedInfo } from "gtfs";
412
+ import { getAgencies, getFeedInfo, isGtfsError as isGtfsError2, formatGtfsError } from "gtfs";
389
413
  import Table from "cli-table";
390
414
  function logWarning(config2) {
391
415
  if (config2.logFunction) {
@@ -402,6 +426,10 @@ function formatWarning(text) {
402
426
  return colors.yellow(warningMessage);
403
427
  }
404
428
 
429
+ // src/lib/trip-id-utils.ts
430
+ var getBaseTripId = (tripId) => tripId.replace(/_freq_\d+$/, "");
431
+ var getBaseTripIds = (trips) => Array.from(new Set(trips.map((trip) => getBaseTripId(trip.trip_id))));
432
+
405
433
  // src/lib/geojson-utils.ts
406
434
  var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
407
435
  var truncateGeoJSONDecimals = (geojson, config2) => {
@@ -431,18 +459,19 @@ var truncateGeoJSONDecimals = (geojson, config2) => {
431
459
  return geojson;
432
460
  };
433
461
  function getTimetableGeoJSON(timetable, config2) {
462
+ const tripIds = getBaseTripIds(timetable.orderedTrips);
434
463
  const shapesGeojsons = timetable.route_ids.map(
435
464
  (routeId) => getShapesAsGeoJSON({
436
465
  route_id: routeId,
437
466
  direction_id: timetable.direction_id,
438
- trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
467
+ trip_id: tripIds
439
468
  })
440
469
  );
441
470
  const stopsGeojsons = timetable.route_ids.map(
442
471
  (routeId) => getStopsAsGeoJSON({
443
472
  route_id: routeId,
444
473
  direction_id: timetable.direction_id,
445
- trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
474
+ trip_id: tripIds
446
475
  })
447
476
  );
448
477
  const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
@@ -480,7 +509,7 @@ function getAgencyGeoJSON(config2) {
480
509
  // package.json
481
510
  var package_default = {
482
511
  name: "gtfs-to-html",
483
- version: "2.12.5",
512
+ version: "2.12.7",
484
513
  private: false,
485
514
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
486
515
  keywords: [
@@ -537,21 +566,21 @@ var package_default = {
537
566
  archiver: "^7.0.1",
538
567
  "cli-table": "^0.3.11",
539
568
  "css.escape": "^1.5.1",
540
- "csv-stringify": "^6.6.0",
569
+ "csv-stringify": "^6.7.0",
541
570
  express: "^5.2.1",
542
- gtfs: "^4.18.3",
571
+ gtfs: "^4.18.5",
543
572
  "gtfs-realtime-pbf-js-module": "^1.0.0",
544
573
  "js-beautify": "^1.15.4",
545
- "lodash-es": "^4.17.23",
546
- "maplibre-gl": "^5.20.1",
547
- marked: "^17.0.4",
574
+ "lodash-es": "^4.18.1",
575
+ "maplibre-gl": "^5.21.1",
576
+ marked: "^17.0.5",
548
577
  moment: "^2.30.1",
549
578
  pbf: "^4.0.1",
550
579
  "pretty-error": "^4.0.0",
551
580
  pug: "^3.0.4",
552
- puppeteer: "^24.39.1",
553
- "sanitize-filename": "^1.6.3",
554
- "sanitize-html": "^2.17.1",
581
+ puppeteer: "^24.40.0",
582
+ "sanitize-filename": "^1.6.4",
583
+ "sanitize-html": "^2.17.2",
555
584
  sqlstring: "^2.3.3",
556
585
  toposort: "^2.0.2",
557
586
  yargs: "^18.0.0",
@@ -561,10 +590,8 @@ var package_default = {
561
590
  "@types/archiver": "^7.0.0",
562
591
  "@types/cli-table": "^0.3.4",
563
592
  "@types/express": "^5.0.6",
564
- "@types/insane": "^1.0.0",
565
593
  "@types/js-beautify": "^1.14.3",
566
594
  "@types/lodash-es": "^4.17.12",
567
- "@types/morgan": "^1.9.10",
568
595
  "@types/node": "^25",
569
596
  "@types/pug": "^2.0.10",
570
597
  "@types/sanitize-html": "^2.16.1",
@@ -575,7 +602,7 @@ var package_default = {
575
602
  "lint-staged": "^16.4.0",
576
603
  prettier: "^3.8.1",
577
604
  tsup: "^8.5.1",
578
- typescript: "^5.9.3"
605
+ typescript: "^6.0.2"
579
606
  },
580
607
  engines: {
581
608
  node: ">= 22"
@@ -805,7 +832,7 @@ var getTimetableNotesForTimetable = (timetable, config2) => {
805
832
  }),
806
833
  // Get all notes for all trips in this timetable.
807
834
  ...getTimetableNotesReferences({
808
- trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
835
+ trip_id: getBaseTripIds(timetable.orderedTrips)
809
836
  }),
810
837
  // Get all notes for all stops in this timetable.
811
838
  ...getTimetableNotesReferences({
@@ -999,7 +1026,7 @@ var convertRoutesToTimetablePages = (config2) => {
999
1026
  if (config2.groupTimetablesIntoPages === true) {
1000
1027
  timetablePages.push(
1001
1028
  createTimetablePage({
1002
- timetablePageId: `route_${route.route_short_name ?? route.route_long_name}`,
1029
+ timetablePageId: `route_${route.route_id}`,
1003
1030
  timetables,
1004
1031
  config: config2
1005
1032
  })
@@ -1146,8 +1173,17 @@ var getStopsForTimetable = (timetable, config2) => {
1146
1173
  stop_id: stopId
1147
1174
  });
1148
1175
  if (stops.length === 0) {
1149
- throw new Error(
1150
- `No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`
1176
+ throw new GtfsToHtmlError(
1177
+ `No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`,
1178
+ {
1179
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1180
+ category: "query" /* QUERY */,
1181
+ details: {
1182
+ entity: "stop",
1183
+ stopId,
1184
+ timetableId: timetable.timetable_id
1185
+ }
1186
+ }
1151
1187
  );
1152
1188
  }
1153
1189
  const stop = {
@@ -1182,7 +1218,14 @@ var getCalendarsFromConfig = (config2) => {
1182
1218
  const whereClauses = [];
1183
1219
  if (config2.endDate) {
1184
1220
  if (!moment2(config2.endDate).isValid()) {
1185
- throw new Error(`Invalid endDate=${config2.endDate} in config.json`);
1221
+ throw new GtfsToHtmlError(
1222
+ `Invalid endDate=${config2.endDate} in config.json`,
1223
+ {
1224
+ code: "GTFS_TO_HTML_CONFIG_DATE_INVALID" /* CONFIG_DATE_INVALID */,
1225
+ category: "config" /* CONFIG */,
1226
+ details: { field: "endDate", value: config2.endDate }
1227
+ }
1228
+ );
1186
1229
  }
1187
1230
  whereClauses.push(
1188
1231
  `start_date <= ${sqlString.escape(moment2(config2.endDate).format("YYYYMMDD"))}`
@@ -1190,7 +1233,14 @@ var getCalendarsFromConfig = (config2) => {
1190
1233
  }
1191
1234
  if (config2.startDate) {
1192
1235
  if (!moment2(config2.startDate).isValid()) {
1193
- throw new Error(`Invalid startDate=${config2.startDate} in config.json`);
1236
+ throw new GtfsToHtmlError(
1237
+ `Invalid startDate=${config2.startDate} in config.json`,
1238
+ {
1239
+ code: "GTFS_TO_HTML_CONFIG_DATE_INVALID" /* CONFIG_DATE_INVALID */,
1240
+ category: "config" /* CONFIG */,
1241
+ details: { field: "startDate", value: config2.startDate }
1242
+ }
1243
+ );
1194
1244
  }
1195
1245
  whereClauses.push(
1196
1246
  `end_date >= ${sqlString.escape(moment2(config2.startDate).format("YYYYMMDD"))}`
@@ -1215,16 +1265,34 @@ var getCalendarsFromTimetable = (timetable) => {
1215
1265
  const whereClauses = [];
1216
1266
  if (timetable.end_date) {
1217
1267
  if (!moment2(timetable.end_date, "YYYYMMDD", true).isValid()) {
1218
- throw new Error(
1219
- `Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`
1268
+ throw new GtfsToHtmlError(
1269
+ `Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`,
1270
+ {
1271
+ code: "GTFS_TO_HTML_QUERY_INVALID" /* QUERY_INVALID */,
1272
+ category: "validation" /* VALIDATION */,
1273
+ details: {
1274
+ field: "end_date",
1275
+ value: timetable.end_date,
1276
+ timetableId: timetable.timetable_id
1277
+ }
1278
+ }
1220
1279
  );
1221
1280
  }
1222
1281
  whereClauses.push(`start_date <= ${sqlString.escape(timetable.end_date)}`);
1223
1282
  }
1224
1283
  if (timetable.start_date) {
1225
1284
  if (!moment2(timetable.start_date, "YYYYMMDD", true).isValid()) {
1226
- throw new Error(
1227
- `Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`
1285
+ throw new GtfsToHtmlError(
1286
+ `Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`,
1287
+ {
1288
+ code: "GTFS_TO_HTML_QUERY_INVALID" /* QUERY_INVALID */,
1289
+ category: "validation" /* VALIDATION */,
1290
+ details: {
1291
+ field: "start_date",
1292
+ value: timetable.start_date,
1293
+ timetableId: timetable.timetable_id
1294
+ }
1295
+ }
1228
1296
  );
1229
1297
  }
1230
1298
  whereClauses.push(`end_date >= ${sqlString.escape(timetable.start_date)}`);
@@ -1269,7 +1337,11 @@ var getAllStationStopIds = (stopId) => {
1269
1337
  stop_id: stopId
1270
1338
  });
1271
1339
  if (stops.length === 0) {
1272
- throw new Error(`No stop found for stop_id=${stopId}`);
1340
+ throw new GtfsToHtmlError(`No stop found for stop_id=${stopId}`, {
1341
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1342
+ category: "query" /* QUERY */,
1343
+ details: { entity: "stop", stopId }
1344
+ });
1273
1345
  }
1274
1346
  const stop = stops[0];
1275
1347
  if (isNullOrEmpty(stop.parent_station)) {
@@ -1303,8 +1375,13 @@ var getTripsWithSameBlock = (trip, timetable) => {
1303
1375
  [["stop_sequence", "ASC"]]
1304
1376
  );
1305
1377
  if (stopTimes.length === 0) {
1306
- throw new Error(
1307
- `No stoptimes found found for trip_id=${blockTrip.trip_id}`
1378
+ throw new GtfsToHtmlError(
1379
+ `No stoptimes found found for trip_id=${blockTrip.trip_id}`,
1380
+ {
1381
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1382
+ category: "query" /* QUERY */,
1383
+ details: { entity: "stoptime", tripId: blockTrip.trip_id }
1384
+ }
1308
1385
  );
1309
1386
  }
1310
1387
  blockTrip.firstStoptime = first(stopTimes);
@@ -1564,9 +1641,7 @@ var formatTimetables = (timetables, config2) => {
1564
1641
  if (config2.showMap) {
1565
1642
  timetable.geojson = getTimetableGeoJSON(timetable, config2);
1566
1643
  }
1567
- timetable.trip_ids = uniq(
1568
- timetable.orderedTrips.map((trip) => trip.trip_id)
1569
- );
1644
+ timetable.trip_ids = uniq(getBaseTripIds(timetable.orderedTrips));
1570
1645
  timetable.orderedTrips = filterTrips(timetable, calendars, config2);
1571
1646
  timetable.stops = formatStops(timetable, config2);
1572
1647
  return timetable;
@@ -1645,8 +1720,18 @@ var getDataForTimetablePageById = (timetablePageId) => {
1645
1720
  );
1646
1721
  const uniqueTripDirections = uniqBy2(trips, (trip) => trip.direction_id);
1647
1722
  if (uniqueTripDirections.length === 0) {
1648
- throw new Error(
1649
- `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1723
+ throw new GtfsToHtmlError(
1724
+ `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`,
1725
+ {
1726
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1727
+ category: "query" /* QUERY */,
1728
+ details: {
1729
+ entity: "trip",
1730
+ timetablePageId,
1731
+ routeId,
1732
+ directionId
1733
+ }
1734
+ }
1650
1735
  );
1651
1736
  }
1652
1737
  if (/^[01]*$/.test(calendarCode ?? "")) {
@@ -1676,8 +1761,13 @@ var getTimetablePageById = (timetablePageId, config2) => {
1676
1761
  getTimetables()
1677
1762
  );
1678
1763
  if (timetablePages.length > 1) {
1679
- throw new Error(
1680
- `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1764
+ throw new GtfsToHtmlError(
1765
+ `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`,
1766
+ {
1767
+ code: "GTFS_TO_HTML_QUERY_RESULT_AMBIGUOUS" /* QUERY_RESULT_AMBIGUOUS */,
1768
+ category: "query" /* QUERY */,
1769
+ details: { entity: "timetable_page", timetablePageId }
1770
+ }
1681
1771
  );
1682
1772
  }
1683
1773
  if (timetablePages.length === 1) {
@@ -1700,8 +1790,13 @@ var getTimetablePageById = (timetablePageId, config2) => {
1700
1790
  (timetable2) => timetable2.timetable_id === timetablePageId
1701
1791
  );
1702
1792
  if (timetablePageTimetables.length === 0) {
1703
- throw new Error(
1704
- `No timetable found for timetable_page_id=${timetablePageId}`
1793
+ throw new GtfsToHtmlError(
1794
+ `No timetable found for timetable_page_id=${timetablePageId}`,
1795
+ {
1796
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1797
+ category: "query" /* QUERY */,
1798
+ details: { entity: "timetable", timetablePageId }
1799
+ }
1705
1800
  );
1706
1801
  }
1707
1802
  return createTimetablePage({
@@ -1712,11 +1807,16 @@ var getTimetablePageById = (timetablePageId, config2) => {
1712
1807
  }
1713
1808
  if (timetablePageId.startsWith("route_")) {
1714
1809
  const routes = getRoutes({
1715
- route_short_name: timetablePageId.split("_")[1]
1810
+ route_id: timetablePageId.split("_")[1]
1716
1811
  });
1717
1812
  if (routes.length === 0) {
1718
- throw new Error(
1719
- `No route found for timetable_page_id=${timetablePageId}`
1813
+ throw new GtfsToHtmlError(
1814
+ `No route found for timetable_page_id=${timetablePageId}`,
1815
+ {
1816
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
1817
+ category: "query" /* QUERY */,
1818
+ details: { entity: "route", timetablePageId }
1819
+ }
1720
1820
  );
1721
1821
  }
1722
1822
  const { calendars: calendars2, calendarDates: calendarDates2 } = getCalendarsFromConfig(config2);
@@ -1907,7 +2007,11 @@ function generateTimetableHTML(timetablePage, config2) {
1907
2007
  function generateOverviewHTML(timetablePages, config2) {
1908
2008
  const agencies = getAgencies2();
1909
2009
  if (agencies.length === 0) {
1910
- throw new Error("No agencies found");
2010
+ throw new GtfsToHtmlError("No agencies found", {
2011
+ code: "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */,
2012
+ category: "query" /* QUERY */,
2013
+ details: { entity: "agency" }
2014
+ });
1911
2015
  }
1912
2016
  const geojson = config2.showMap ? getAgencyGeoJSON(config2) : void 0;
1913
2017
  const templateVars = {
@@ -2287,7 +2391,15 @@ try {
2287
2391
  console.error(
2288
2392
  `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists and run gtfs-to-html to import GTFS before running this app.`
2289
2393
  );
2290
- throw error;
2394
+ throw new GtfsToHtmlError(
2395
+ `Unable to open sqlite database "${config.sqlitePath}"`,
2396
+ {
2397
+ code: "GTFS_TO_HTML_DATABASE_OPEN_FAILED" /* DATABASE_OPEN_FAILED */,
2398
+ category: "database" /* DATABASE */,
2399
+ details: { sqlitePath: config.sqlitePath, dbCode: error?.code },
2400
+ cause: error
2401
+ }
2402
+ );
2291
2403
  }
2292
2404
  app.set("views", getPathToViewsFolder(config));
2293
2405
  app.set("view engine", "pug");
@@ -2371,7 +2483,7 @@ app.get("/timetables/:timetablePageId", async (req, res, next) => {
2371
2483
  const html = await generateTimetableHTML(timetablePage, config);
2372
2484
  res.send(html);
2373
2485
  } catch (error) {
2374
- if (error?.message.startsWith("No timetable found")) {
2486
+ if (isGtfsToHtmlError(error) && error.code === "GTFS_TO_HTML_QUERY_RESULT_NOT_FOUND" /* QUERY_RESULT_NOT_FOUND */) {
2375
2487
  res.status(404).send("Timetable page not found");
2376
2488
  return;
2377
2489
  }