minotor 1.0.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.
Files changed (131) hide show
  1. package/.cspell.json +43 -0
  2. package/.czrc +3 -0
  3. package/.editorconfig +10 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +4 -0
  8. package/.github/workflows/minotor.yml +85 -0
  9. package/.prettierrc +7 -0
  10. package/.releaserc.json +27 -0
  11. package/CHANGELOG.md +6 -0
  12. package/LICENSE +21 -0
  13. package/README.md +166 -0
  14. package/dist/bundle.cjs.js +16507 -0
  15. package/dist/bundle.cjs.js.map +1 -0
  16. package/dist/bundle.esm.js +16496 -0
  17. package/dist/bundle.esm.js.map +1 -0
  18. package/dist/bundle.umd.js +2 -0
  19. package/dist/bundle.umd.js.map +1 -0
  20. package/dist/cli/__tests__/minotor.test.d.ts +1 -0
  21. package/dist/cli/minotor.d.ts +5 -0
  22. package/dist/cli/repl.d.ts +1 -0
  23. package/dist/cli/utils.d.ts +3 -0
  24. package/dist/cli.mjs +20504 -0
  25. package/dist/cli.mjs.map +1 -0
  26. package/dist/gtfs/__tests__/parser.test.d.ts +1 -0
  27. package/dist/gtfs/__tests__/routes.test.d.ts +1 -0
  28. package/dist/gtfs/__tests__/services.test.d.ts +1 -0
  29. package/dist/gtfs/__tests__/stops.test.d.ts +1 -0
  30. package/dist/gtfs/__tests__/time.test.d.ts +1 -0
  31. package/dist/gtfs/__tests__/transfers.test.d.ts +1 -0
  32. package/dist/gtfs/__tests__/trips.test.d.ts +1 -0
  33. package/dist/gtfs/__tests__/utils.test.d.ts +1 -0
  34. package/dist/gtfs/parser.d.ts +34 -0
  35. package/dist/gtfs/profiles/__tests__/ch.test.d.ts +1 -0
  36. package/dist/gtfs/profiles/ch.d.ts +2 -0
  37. package/dist/gtfs/profiles/standard.d.ts +2 -0
  38. package/dist/gtfs/routes.d.ts +11 -0
  39. package/dist/gtfs/services.d.ts +19 -0
  40. package/dist/gtfs/stops.d.ts +20 -0
  41. package/dist/gtfs/time.d.ts +17 -0
  42. package/dist/gtfs/transfers.d.ts +22 -0
  43. package/dist/gtfs/trips.d.ts +26 -0
  44. package/dist/gtfs/utils.d.ts +21 -0
  45. package/dist/index.d.ts +11 -0
  46. package/dist/routing/__tests__/router.test.d.ts +1 -0
  47. package/dist/routing/plotter.d.ts +11 -0
  48. package/dist/routing/query.d.ts +35 -0
  49. package/dist/routing/result.d.ts +28 -0
  50. package/dist/routing/route.d.ts +25 -0
  51. package/dist/routing/router.d.ts +33 -0
  52. package/dist/stops/__tests__/io.test.d.ts +1 -0
  53. package/dist/stops/__tests__/stopFinder.test.d.ts +1 -0
  54. package/dist/stops/i18n.d.ts +10 -0
  55. package/dist/stops/io.d.ts +4 -0
  56. package/dist/stops/proto/stops.d.ts +53 -0
  57. package/dist/stops/stops.d.ts +16 -0
  58. package/dist/stops/stopsIndex.d.ts +52 -0
  59. package/dist/timetable/__tests__/io.test.d.ts +1 -0
  60. package/dist/timetable/__tests__/timetable.test.d.ts +1 -0
  61. package/dist/timetable/duration.d.ts +51 -0
  62. package/dist/timetable/io.d.ts +8 -0
  63. package/dist/timetable/proto/timetable.d.ts +122 -0
  64. package/dist/timetable/time.d.ts +98 -0
  65. package/dist/timetable/timetable.d.ts +82 -0
  66. package/dist/umdIndex.d.ts +9 -0
  67. package/eslint.config.mjs +52 -0
  68. package/package.json +109 -0
  69. package/rollup.config.js +44 -0
  70. package/src/cli/__tests__/minotor.test.ts +23 -0
  71. package/src/cli/minotor.ts +112 -0
  72. package/src/cli/repl.ts +200 -0
  73. package/src/cli/utils.ts +36 -0
  74. package/src/gtfs/__tests__/parser.test.ts +591 -0
  75. package/src/gtfs/__tests__/resources/sample-feed/agency.txt +2 -0
  76. package/src/gtfs/__tests__/resources/sample-feed/calendar.txt +3 -0
  77. package/src/gtfs/__tests__/resources/sample-feed/calendar_dates.txt +2 -0
  78. package/src/gtfs/__tests__/resources/sample-feed/fare_attributes.txt +3 -0
  79. package/src/gtfs/__tests__/resources/sample-feed/fare_rules.txt +5 -0
  80. package/src/gtfs/__tests__/resources/sample-feed/frequencies.txt +12 -0
  81. package/src/gtfs/__tests__/resources/sample-feed/routes.txt +6 -0
  82. package/src/gtfs/__tests__/resources/sample-feed/sample-feed.zip +0 -0
  83. package/src/gtfs/__tests__/resources/sample-feed/shapes.txt +1 -0
  84. package/src/gtfs/__tests__/resources/sample-feed/stop_times.txt +34 -0
  85. package/src/gtfs/__tests__/resources/sample-feed/stops.txt +10 -0
  86. package/src/gtfs/__tests__/resources/sample-feed/trips.txt +13 -0
  87. package/src/gtfs/__tests__/resources/sample-feed.zip +0 -0
  88. package/src/gtfs/__tests__/routes.test.ts +63 -0
  89. package/src/gtfs/__tests__/services.test.ts +209 -0
  90. package/src/gtfs/__tests__/stops.test.ts +177 -0
  91. package/src/gtfs/__tests__/time.test.ts +27 -0
  92. package/src/gtfs/__tests__/transfers.test.ts +117 -0
  93. package/src/gtfs/__tests__/trips.test.ts +463 -0
  94. package/src/gtfs/__tests__/utils.test.ts +13 -0
  95. package/src/gtfs/parser.ts +154 -0
  96. package/src/gtfs/profiles/__tests__/ch.test.ts +43 -0
  97. package/src/gtfs/profiles/ch.ts +70 -0
  98. package/src/gtfs/profiles/standard.ts +39 -0
  99. package/src/gtfs/routes.ts +48 -0
  100. package/src/gtfs/services.ts +98 -0
  101. package/src/gtfs/stops.ts +112 -0
  102. package/src/gtfs/time.ts +33 -0
  103. package/src/gtfs/transfers.ts +102 -0
  104. package/src/gtfs/trips.ts +228 -0
  105. package/src/gtfs/utils.ts +42 -0
  106. package/src/index.ts +28 -0
  107. package/src/routing/__tests__/router.test.ts +760 -0
  108. package/src/routing/plotter.ts +70 -0
  109. package/src/routing/query.ts +74 -0
  110. package/src/routing/result.ts +108 -0
  111. package/src/routing/route.ts +94 -0
  112. package/src/routing/router.ts +262 -0
  113. package/src/stops/__tests__/io.test.ts +43 -0
  114. package/src/stops/__tests__/stopFinder.test.ts +185 -0
  115. package/src/stops/i18n.ts +40 -0
  116. package/src/stops/io.ts +94 -0
  117. package/src/stops/proto/stops.proto +26 -0
  118. package/src/stops/proto/stops.ts +445 -0
  119. package/src/stops/stops.ts +24 -0
  120. package/src/stops/stopsIndex.ts +151 -0
  121. package/src/timetable/__tests__/io.test.ts +175 -0
  122. package/src/timetable/__tests__/timetable.test.ts +180 -0
  123. package/src/timetable/duration.ts +85 -0
  124. package/src/timetable/io.ts +265 -0
  125. package/src/timetable/proto/timetable.proto +76 -0
  126. package/src/timetable/proto/timetable.ts +1304 -0
  127. package/src/timetable/time.ts +192 -0
  128. package/src/timetable/timetable.ts +286 -0
  129. package/src/umdIndex.ts +14 -0
  130. package/tsconfig.build.json +4 -0
  131. package/tsconfig.json +21 -0
@@ -0,0 +1,70 @@
1
+ import { Platform } from '../../stops/stops.js';
2
+ import { RouteType } from '../../timetable/timetable.js';
3
+ import { GtfsProfile } from '../parser.js';
4
+ import { StopEntry } from '../stops.js';
5
+ import { Maybe } from '../utils.js';
6
+
7
+ /**
8
+ * Parses the platform number from a stop entry.
9
+ * @param stopEntry The stop entry.
10
+ * @returns The platform corresponding to this stop.
11
+ */
12
+ const platformParser = (stopEntry: StopEntry): Maybe<Platform> => {
13
+ const stopId = String(stopEntry.stop_id);
14
+ const stopParts = stopId.split(':');
15
+ if (stopParts.length > 2) {
16
+ return stopParts[2];
17
+ }
18
+ return undefined;
19
+ };
20
+
21
+ /**
22
+ * Parses the SBB extended route type and returns the corresponding basic GTFS route type.
23
+ * @param routeType The SBB route type to parse.
24
+ * @returns The corresponding GTFS route type, or undefined if the route type is not recognized.
25
+ */
26
+ const routeTypeParser = (routeType: number): Maybe<RouteType> => {
27
+ switch (routeType) {
28
+ case 1700: // Lift
29
+ case 1400: // Cogwheel train, funicular
30
+ return 'FUNICULAR'; // Funicular
31
+ case 700: // Bus
32
+ case 705: // Night bus
33
+ case 710: // Panorama bus
34
+ case 202: // National long-distance bus
35
+ case 201: // International long-distance bus
36
+ case 702: // Express bus
37
+ case 715: // On-demand bus
38
+ return 'BUS'; // Bus
39
+ case 1300: // Chairlift, Gondola
40
+ return 'AERIAL_LIFT'; // Aerial lift
41
+ case 401: // Metro
42
+ return 'SUBWAY'; // Subway
43
+ case 1000: // Boat / Ship
44
+ return 'FERRY'; // Boat
45
+ case 900: // Tram
46
+ return 'TRAM'; // Tram
47
+ case 117: // Special train
48
+ case 102: // International train
49
+ case 104: // Car train
50
+ case 101: // International train
51
+ case 111: // Airport train
52
+ case 105: // Night train
53
+ case 103: // Fast train
54
+ case 107: // Mountain train
55
+ case 100: // No guaranteed train
56
+ case 106: // Regional train
57
+ case 109: // Urban train
58
+ case 116: // ??? train TODO figure out what this means
59
+ return 'RAIL'; // Train
60
+ case 1100: // Aircraft
61
+ case 1500: // Taxi
62
+ default:
63
+ return undefined;
64
+ }
65
+ };
66
+
67
+ export const chGtfsProfile: GtfsProfile = {
68
+ routeTypeParser,
69
+ platformParser,
70
+ };
@@ -0,0 +1,39 @@
1
+ import { Platform } from '../../stops/stops.js';
2
+ import { GtfsProfile } from '../parser.js';
3
+ import { StopEntry } from '../stops.js';
4
+ import { Maybe } from '../utils.js';
5
+
6
+ export const standardProfile: GtfsProfile = {
7
+ routeTypeParser: (routeType: number) => {
8
+ switch (routeType) {
9
+ case 0:
10
+ return 'TRAM';
11
+ case 1:
12
+ return 'SUBWAY';
13
+ case 2:
14
+ return 'RAIL';
15
+ case 3:
16
+ return 'BUS';
17
+ case 4:
18
+ return 'FERRY';
19
+ case 5:
20
+ return 'CABLE_TRAM';
21
+ case 6:
22
+ return 'AERIAL_LIFT';
23
+ case 7:
24
+ return 'FUNICULAR';
25
+ case 11:
26
+ return 'TROLLEYBUS';
27
+ case 12:
28
+ return 'MONORAIL';
29
+ default:
30
+ return undefined;
31
+ }
32
+ },
33
+ platformParser: (stopEntry: StopEntry): Maybe<Platform> => {
34
+ if (stopEntry.platform_code) {
35
+ return stopEntry.platform_code;
36
+ }
37
+ return undefined;
38
+ },
39
+ };
@@ -0,0 +1,48 @@
1
+ import log from 'loglevel';
2
+
3
+ import { RouteId, ServiceRoutesMap } from '../timetable/timetable.js';
4
+ import { GtfsProfile } from './parser.js';
5
+ import { standardProfile } from './profiles/standard.js';
6
+ import { parseCsv } from './utils.js';
7
+
8
+ // Can be a standard gtfs route type or an extended route type
9
+ // the profile converter handles the conversion to a route type.
10
+ export type GtfsRouteType = number;
11
+
12
+ type RouteEntry = {
13
+ route_id: RouteId;
14
+ agency_id: string;
15
+ route_short_name: string;
16
+ route_long_name: string;
17
+ route_desc: string;
18
+ route_type: number;
19
+ };
20
+
21
+ /**
22
+ * Parses a GTFS routes.txt file and returns a map of all the valid routes.
23
+ *
24
+ * @param routesStream A readable stream for the GTFS routes.txt file.
25
+ * @param profile A configuration object defining the specificities of the GTFS feed.
26
+ * @returns A map of all the valid routes.
27
+ */
28
+ export const parseRoutes = async (
29
+ routesStream: NodeJS.ReadableStream,
30
+ profile: GtfsProfile = standardProfile,
31
+ ): Promise<ServiceRoutesMap> => {
32
+ const routes: ServiceRoutesMap = new Map();
33
+ for await (const rawLine of parseCsv(routesStream)) {
34
+ const line = rawLine as RouteEntry;
35
+ const routeType = profile.routeTypeParser(line.route_type);
36
+ if (routeType === undefined) {
37
+ log.info(
38
+ `Unsupported route type ${line.route_type} for route ${line.route_id}.`,
39
+ );
40
+ continue;
41
+ }
42
+ routes.set(line.route_id, {
43
+ name: line.route_short_name + '',
44
+ type: routeType,
45
+ });
46
+ }
47
+ return routes;
48
+ };
@@ -0,0 +1,98 @@
1
+ import { DateTime } from 'luxon';
2
+
3
+ import { toGtfsDate } from './time.js';
4
+ import { parseCsv } from './utils.js';
5
+
6
+ export type ServiceId = string;
7
+ export type ServiceIds = Set<ServiceId>;
8
+
9
+ type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
10
+ type ActiveFlag = 0 | 1;
11
+ type RawGtfsDate = number;
12
+ type ExceptionType = 1 | 2;
13
+
14
+ type CalendarEntry = {
15
+ service_id: string;
16
+ monday: ActiveFlag;
17
+ tuesday: ActiveFlag;
18
+ wednesday: ActiveFlag;
19
+ thursday: ActiveFlag;
20
+ friday: ActiveFlag;
21
+ saturday: ActiveFlag;
22
+ sunday: ActiveFlag;
23
+ start_date: RawGtfsDate;
24
+ end_date: RawGtfsDate;
25
+ };
26
+
27
+ type CalendarDatesEntry = {
28
+ service_id: ServiceId;
29
+ date: RawGtfsDate;
30
+ exception_type: ExceptionType;
31
+ };
32
+
33
+ const weekdays = {
34
+ 1: 'monday',
35
+ 2: 'tuesday',
36
+ 3: 'wednesday',
37
+ 4: 'thursday',
38
+ 5: 'friday',
39
+ 6: 'saturday',
40
+ 7: 'sunday',
41
+ };
42
+
43
+ /**
44
+ * Parses a GTFS calendar.txt file and finds the service_ids of a given date.
45
+ *
46
+ * @param serviceIds A map of the active service ids (will be populated with active service_ids).
47
+ * @param date The active date.
48
+ * @param calendarStream A readable stream for the GTFS calendar.txt file.
49
+ */
50
+ export const parseCalendar = async (
51
+ calendarStream: NodeJS.ReadableStream,
52
+ serviceIds: ServiceIds,
53
+ date: DateTime,
54
+ ) => {
55
+ const activeDate: number = toGtfsDate(date);
56
+ const weekday = date.weekday as Weekday;
57
+ const weekdayIndex = weekdays[weekday] as keyof CalendarEntry;
58
+ for await (const rawLine of parseCsv(calendarStream)) {
59
+ const line = rawLine as CalendarEntry;
60
+ if (activeDate < line.start_date || activeDate > line.end_date) {
61
+ // If the service is not valid on this date
62
+ continue;
63
+ }
64
+ if (line[weekdayIndex] !== 1) {
65
+ // If the service is not valid on this week day
66
+ continue;
67
+ }
68
+ serviceIds.add(line['service_id']);
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Parses a gtfs calendar_dates.txt file and finds the service ids valid at a given date.
74
+ *
75
+ * @param serviceIds A map of the active service ids (will be populated and filtered).
76
+ * @param date The active date, in the format "YYYYMMDD".
77
+ * @param calendarDatesStream A readable stream for the GTFS calendar_dates.txt file.
78
+ */
79
+ export const parseCalendarDates = async (
80
+ calendarDatesStream: NodeJS.ReadableStream,
81
+ serviceIds: ServiceIds,
82
+ date: DateTime,
83
+ ) => {
84
+ const activeDate: number = toGtfsDate(date);
85
+ for await (const rawLine of parseCsv(calendarDatesStream)) {
86
+ const line = rawLine as CalendarDatesEntry;
87
+ if (line.date !== activeDate) {
88
+ // No rule on the active date
89
+ } else if (line.exception_type === 2 && serviceIds.has(line.service_id)) {
90
+ // Service has been removed for the specified date.
91
+
92
+ serviceIds.delete(line.service_id);
93
+ } else if (line.exception_type === 1) {
94
+ // Service is present on the active date
95
+ serviceIds.add(line.service_id);
96
+ }
97
+ }
98
+ };
@@ -0,0 +1,112 @@
1
+ import {
2
+ Latitude,
3
+ LocationType,
4
+ Longitude,
5
+ Platform,
6
+ Stop,
7
+ StopId,
8
+ StopsMap,
9
+ } from '../stops/stops.js';
10
+ import { Maybe, parseCsv } from './utils.js';
11
+
12
+ export type StopIds = Set<StopId>;
13
+
14
+ export type GtfsLocationType =
15
+ | 0 // simple stop or platform (can also be empty)
16
+ | 1 // station
17
+ | 2 // entrance / exit
18
+ | 3 // generic node
19
+ | 4; // boarding area
20
+
21
+ export type StopEntry = {
22
+ stop_id: StopId;
23
+ stop_name: string;
24
+ stop_lat?: Latitude;
25
+ stop_lon?: Longitude;
26
+ location_type?: GtfsLocationType;
27
+ parent_station?: StopId;
28
+ platform_code?: Platform;
29
+ };
30
+
31
+ /**
32
+ * Parses the stops.txt file from a GTFS feed.
33
+ *
34
+ * @param stopsStream The readable stream containing the stops data.
35
+ * @return A mapping of stop IDs to corresponding stop details.
36
+ */
37
+ export const parseStops = async (
38
+ stopsStream: NodeJS.ReadableStream,
39
+ platformParser?: (stopEntry: StopEntry) => Maybe<Platform>,
40
+ validStops?: StopIds,
41
+ ): Promise<StopsMap> => {
42
+ const stops: StopsMap = new Map();
43
+
44
+ for await (const rawLine of parseCsv(stopsStream)) {
45
+ const line = rawLine as StopEntry;
46
+ const stop: Stop = {
47
+ id: line.stop_id,
48
+ name: line.stop_name,
49
+ lat: line.stop_lat,
50
+ lon: line.stop_lon,
51
+ locationType: line.location_type
52
+ ? parseGtfsLocationType(line.location_type)
53
+ : 'SIMPLE_STOP_OR_PLATFORM',
54
+ children: [],
55
+ ...(line.parent_station && { parent: line.parent_station }),
56
+ };
57
+ if (platformParser) {
58
+ try {
59
+ const platform = platformParser(line);
60
+ if (platform) {
61
+ stop.platform = platform;
62
+ }
63
+ } catch {
64
+ console.info(`Could not parse platform for stop ${line.stop_id}.`);
65
+ }
66
+ }
67
+ stops.set(line.stop_id, stop);
68
+ }
69
+
70
+ for (const [stopId, stop] of stops) {
71
+ if (stop.parent) {
72
+ const parentStop = stops.get(stop.parent);
73
+ if (!parentStop) {
74
+ console.warn(`Cannot find parent stop ${stop.parent} of ${stopId}`);
75
+ continue;
76
+ }
77
+ parentStop.children.push(stopId);
78
+ }
79
+ }
80
+ if (validStops) {
81
+ // Remove all stops which don't have at least one valid stopId as a child,
82
+ // a parent or as its own.
83
+ for (const [stopId, stop] of stops) {
84
+ if (
85
+ !validStops.has(stopId) &&
86
+ (!stop.parent || !validStops.has(stop.parent)) &&
87
+ !stop.children.some((childId) => validStops.has(childId))
88
+ ) {
89
+ stops.delete(stopId);
90
+ }
91
+ }
92
+ }
93
+ return stops;
94
+ };
95
+
96
+ const parseGtfsLocationType = (
97
+ gtfsLocationType: GtfsLocationType,
98
+ ): LocationType => {
99
+ switch (gtfsLocationType) {
100
+ case 0:
101
+ default:
102
+ return 'SIMPLE_STOP_OR_PLATFORM';
103
+ case 1:
104
+ return 'STATION';
105
+ case 2:
106
+ return 'ENTRANCE_EXIT';
107
+ case 3:
108
+ return 'GENERIC_NODE';
109
+ case 4:
110
+ return 'BOARDING_AREA';
111
+ }
112
+ };
@@ -0,0 +1,33 @@
1
+ import { DateTime } from 'luxon';
2
+
3
+ import { Time } from '../timetable/time.js';
4
+
5
+ export type GtfsDate = number;
6
+ export type GtfsTime = string;
7
+
8
+ export const toGtfsDate = (date: DateTime): GtfsDate => {
9
+ return parseInt(date.toFormat('yyyyLLdd'));
10
+ };
11
+
12
+ /**
13
+ * Converts a time string in the format 'HH:mm:ss' to a Time object
14
+ * (number of seconds since midnight).
15
+ *
16
+ * This method splits the input time string into hours, minutes, and seconds,
17
+ * and then calculates the total number of seconds.
18
+ *
19
+ * @param time - A string representing the time in 'HH:mm:ss' format.
20
+ * @returns The GTFS time as the number of seconds since midnight.
21
+ * @throws An error if the input time string is not in the correct format.
22
+ */
23
+ export const toTime = (time: GtfsTime): Time => {
24
+ const splits = time.split(':');
25
+ if (!splits[0] || !splits[1] || !splits[2]) {
26
+ throw new Error(`Invalid time ${time}.`);
27
+ }
28
+ return Time.fromHMS(
29
+ parseInt(splits[0]),
30
+ parseInt(splits[1]),
31
+ parseInt(splits[2]),
32
+ );
33
+ };
@@ -0,0 +1,102 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import { Duration } from '../timetable/duration.js';
3
+ import {
4
+ ServiceRouteId,
5
+ Transfer,
6
+ TransferType,
7
+ } from '../timetable/timetable.js';
8
+ import { TripId } from './trips.js';
9
+ import { parseCsv } from './utils.js';
10
+
11
+ export type GtfsTransferType =
12
+ | 0 // recommended transfer point
13
+ | 1 // timed transfer (guaranteed)
14
+ | 2 // requires a minimal amount of time
15
+ | 3 // transfer not possible
16
+ | 4 // in-seat transfer
17
+ | 5; // in-seat transfer not allowed (must alight)
18
+
19
+ export type TransfersMap = Map<StopId, Transfer[]>;
20
+
21
+ export type TransferEntry = {
22
+ from_stop_id?: StopId;
23
+ to_stop_id?: StopId;
24
+ from_trip_id?: TripId;
25
+ to_trip_id?: TripId;
26
+ from_route_id?: ServiceRouteId;
27
+ to_route_id?: ServiceRouteId;
28
+ transfer_type: GtfsTransferType;
29
+ min_transfer_time?: number;
30
+ };
31
+
32
+ /**
33
+ * Parses the transfers.txt file from a GTFS feed.
34
+ *
35
+ * @param stopsStream The readable stream containing the stops data.
36
+ * @return A mapping of stop IDs to corresponding stop details.
37
+ */
38
+ export const parseTransfers = async (
39
+ transfersStream: NodeJS.ReadableStream,
40
+ ): Promise<TransfersMap> => {
41
+ const transfers: TransfersMap = new Map();
42
+
43
+ for await (const rawLine of parseCsv(transfersStream)) {
44
+ const transferEntry = rawLine as TransferEntry;
45
+ if (
46
+ transferEntry.transfer_type === 3 ||
47
+ transferEntry.transfer_type === 5
48
+ ) {
49
+ continue;
50
+ }
51
+ if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
52
+ console.warn(
53
+ `Unsupported transfer between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`,
54
+ );
55
+ continue;
56
+ }
57
+ if (transferEntry.from_route_id && transferEntry.to_route_id) {
58
+ console.warn(
59
+ `Unsupported transfer between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`,
60
+ );
61
+ continue;
62
+ }
63
+ if (!transferEntry.from_stop_id || !transferEntry.to_stop_id) {
64
+ console.warn(`Missing transfer origin or destination stop.`);
65
+ continue;
66
+ }
67
+ if (transferEntry.transfer_type === 2 && !transferEntry.min_transfer_time) {
68
+ console.info(
69
+ `Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`,
70
+ );
71
+ }
72
+
73
+ const transfer: Transfer = {
74
+ destination: transferEntry.to_stop_id,
75
+ type: parseGtfsTransferType(transferEntry.transfer_type),
76
+ ...(transferEntry.min_transfer_time && {
77
+ minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
78
+ }),
79
+ };
80
+
81
+ const fromStopTransfers = transfers.get(transferEntry.from_stop_id) || [];
82
+ fromStopTransfers.push(transfer);
83
+ transfers.set(transferEntry.from_stop_id, fromStopTransfers);
84
+ }
85
+ return transfers;
86
+ };
87
+
88
+ const parseGtfsTransferType = (
89
+ gtfsTransferType: GtfsTransferType,
90
+ ): TransferType => {
91
+ switch (gtfsTransferType) {
92
+ case 0:
93
+ default:
94
+ return 'RECOMMENDED';
95
+ case 1:
96
+ return 'GUARANTEED';
97
+ case 2:
98
+ return 'REQUIRES_MINIMAL_TIME';
99
+ case 4:
100
+ return 'IN_SEAT';
101
+ }
102
+ };