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,228 @@
1
+ import { StopId } from '../stops/stops.js';
2
+ import {
3
+ PickUpDropOffType,
4
+ Route,
5
+ RouteId,
6
+ RoutesAdjacency,
7
+ ServiceRouteId,
8
+ ServiceRoutesMap,
9
+ StopsAdjacency,
10
+ StopTimes,
11
+ } from '../timetable/timetable.js';
12
+ import { ServiceIds } from './services.js';
13
+ import { StopIds } from './stops.js';
14
+ import { GtfsTime, toTime } from './time.js';
15
+ import { TransfersMap } from './transfers.js';
16
+ import { hash, parseCsv } from './utils.js';
17
+
18
+ export type TripId = string;
19
+
20
+ export type TripIdsMap = Map<TripId, ServiceRouteId>;
21
+
22
+ type TripEntry = {
23
+ route_id: RouteId;
24
+ service_id: ServiceRouteId;
25
+ trip_id: TripId;
26
+ };
27
+
28
+ export type GtfsPickupDropOffType =
29
+ | '' // Not specified
30
+ | 0 // Regularly scheduled
31
+ | 1 // Not available
32
+ | 2 // Must phone agency
33
+ | 3; // Must coordinate with driver
34
+
35
+ type StopTimeEntry = {
36
+ trip_id: TripId;
37
+ arrival_time?: GtfsTime;
38
+ departure_time?: GtfsTime;
39
+ stop_id: StopId;
40
+ stop_sequence: number;
41
+ pickup_type?: GtfsPickupDropOffType;
42
+ drop_off_type?: GtfsPickupDropOffType;
43
+ };
44
+
45
+ /**
46
+ * Parses the trips.txt file from a GTFS feed
47
+ *
48
+ * @param tripsStream The readable stream containing the trips data.
49
+ * @param serviceIds A mapping of service IDs to corresponding route IDs.
50
+ * @param routeIds A mapping of route IDs to route details.
51
+ * @returns A mapping of trip IDs to corresponding route IDs.
52
+ */
53
+ export const parseTrips = async (
54
+ tripsStream: NodeJS.ReadableStream,
55
+ serviceIds: ServiceIds,
56
+ routeIds: ServiceRoutesMap,
57
+ ): Promise<TripIdsMap> => {
58
+ const trips: TripIdsMap = new Map();
59
+ for await (const rawLine of parseCsv(tripsStream)) {
60
+ const line = rawLine as TripEntry;
61
+ if (!serviceIds.has(line.service_id)) {
62
+ // The trip doesn't correspond to an active service
63
+ continue;
64
+ }
65
+ if (!routeIds.get(line.route_id)) {
66
+ // The trip doesn't correspond to a supported route
67
+ continue;
68
+ }
69
+ trips.set(line.trip_id, line.route_id);
70
+ }
71
+ return trips;
72
+ };
73
+
74
+ export const buildStopsAdjacencyStructure = (
75
+ validStops: StopIds,
76
+ routes: RoutesAdjacency,
77
+ transfersMap: TransfersMap,
78
+ ): StopsAdjacency => {
79
+ const stopsAdjacency: StopsAdjacency = new Map();
80
+ for (const routeId of routes.keys()) {
81
+ const route = routes.get(routeId) as Route;
82
+ for (const stop of route.stops) {
83
+ if (!stopsAdjacency.get(stop) && validStops.has(stop)) {
84
+ stopsAdjacency.set(stop, { routes: [], transfers: [] });
85
+ }
86
+
87
+ stopsAdjacency.get(stop)?.routes.push(routeId);
88
+ }
89
+ }
90
+ for (const [stop, transfers] of transfersMap) {
91
+ const s = stopsAdjacency.get(stop);
92
+ if (s) {
93
+ for (const transfer of transfers) {
94
+ if (validStops.has(transfer.destination)) {
95
+ s.transfers.push(transfer);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return stopsAdjacency;
101
+ };
102
+
103
+ /**
104
+ * Parses the stop_times.txt data from a GTFS feed.
105
+ *
106
+ * @param stopTimesStream The readable stream containing the stop times data.
107
+ * @param validTripIds A map of valid trip IDs to corresponding route IDs.
108
+ * @param validStopIds A map of valid stop IDs.
109
+ * @returns A mapping of route IDs to route details. The routes return corresponds to the set of trips from GTFS that share the same stop list.
110
+ */
111
+ export const parseStopTimes = async (
112
+ stopTimesStream: NodeJS.ReadableStream,
113
+ validTripIds: TripIdsMap,
114
+ validStopIds: StopIds,
115
+ ): Promise<RoutesAdjacency> => {
116
+ const addTrip = (currentTripId: TripId) => {
117
+ const gtfsRouteId = validTripIds.get(currentTripId);
118
+ if (!gtfsRouteId) {
119
+ stops = [];
120
+ stopTimes = [];
121
+ return;
122
+ }
123
+ const routeId = `${gtfsRouteId}_${hash(stops.join('$'))}`;
124
+
125
+ let route = routes.get(routeId);
126
+ if (!route) {
127
+ route = {
128
+ serviceRouteId: gtfsRouteId,
129
+ stops: [...stops],
130
+ stopIndices: new Map(stops.map((stop, i) => [stop, i])),
131
+ stopTimes: [...stopTimes],
132
+ };
133
+ routes.set(routeId, route);
134
+ for (const stop of stops) {
135
+ validStopIds.add(stop);
136
+ }
137
+ } else {
138
+ const tripFirstStop = stopTimes[0];
139
+ if (!tripFirstStop) {
140
+ throw new Error(`Empty trip ${currentTripId}`);
141
+ }
142
+ // insert the stopTimes at the right position
143
+ let stopTimesIndex = 0;
144
+ for (let i = 0; i < route.stopTimes.length; i += stops.length) {
145
+ const currentDeparture = route.stopTimes[i];
146
+ if (
147
+ currentDeparture &&
148
+ tripFirstStop.departure > currentDeparture.departure
149
+ ) {
150
+ stopTimesIndex = i + stops.length;
151
+ } else {
152
+ break;
153
+ }
154
+ }
155
+ route.stopTimes.splice(stopTimesIndex, 0, ...stopTimes);
156
+ }
157
+ stops = [];
158
+ stopTimes = [];
159
+ };
160
+
161
+ const routes: RoutesAdjacency = new Map();
162
+
163
+ let previousSeq = 0;
164
+ let stops: StopId[] = [];
165
+ let stopTimes: StopTimes[] = [];
166
+ let currentTripId: TripId | undefined = undefined;
167
+
168
+ for await (const rawLine of parseCsv(stopTimesStream)) {
169
+ const line = rawLine as StopTimeEntry;
170
+ if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) {
171
+ console.warn(`Stop sequences not increasing for trip ${line.trip_id}.`);
172
+ continue;
173
+ }
174
+ if (!line.arrival_time && !line.departure_time) {
175
+ console.warn(
176
+ `Missing arrival or departure time for ${line.trip_id} at stop ${line.stop_id}.`,
177
+ );
178
+ continue;
179
+ }
180
+ if (line.pickup_type === 1 && line.drop_off_type === 1) {
181
+ continue;
182
+ }
183
+ if (
184
+ currentTripId &&
185
+ line.trip_id !== currentTripId &&
186
+ stops.length > 0 &&
187
+ stopTimes.length > 0
188
+ ) {
189
+ addTrip(currentTripId);
190
+ }
191
+ currentTripId = line.trip_id;
192
+ stops.push(line.stop_id);
193
+ const departure = line.departure_time ?? line.arrival_time;
194
+ const arrival = line.arrival_time ?? line.departure_time;
195
+ stopTimes.push({
196
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
197
+ departure: toTime(departure!),
198
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
199
+ arrival: toTime(arrival!),
200
+ pickUpType: parsePickupDropOffType(line.pickup_type),
201
+ dropOffType: parsePickupDropOffType(line.drop_off_type),
202
+ });
203
+ previousSeq = line.stop_sequence;
204
+ }
205
+ if (currentTripId) {
206
+ addTrip(currentTripId);
207
+ }
208
+
209
+ return routes;
210
+ };
211
+
212
+ const parsePickupDropOffType = (
213
+ gtfsType?: GtfsPickupDropOffType,
214
+ ): PickUpDropOffType => {
215
+ switch (gtfsType) {
216
+ default:
217
+ console.warn(`Unknown pickup/drop-off type ${gtfsType}`);
218
+ return 'REGULAR';
219
+ case 0:
220
+ return 'REGULAR';
221
+ case 1:
222
+ return 'NOT_AVAILABLE';
223
+ case 2:
224
+ return 'MUST_PHONE_AGENCY';
225
+ case 3:
226
+ return 'MUST_COORDINATE_WITH_DRIVER';
227
+ }
228
+ };
@@ -0,0 +1,42 @@
1
+ import { parse, Parser } from 'csv-parse';
2
+
3
+ export type Maybe<T> = T | undefined;
4
+
5
+ /**
6
+ * Generates a simple hash from a string.
7
+ *
8
+ * This function computes a hash for a given string by iterating over each
9
+ * character and applying bitwise operations to accumulate a hash value.
10
+ * The final hash is then converted to a base-36 string and padded to
11
+ * ensure a minimum length of 6 characters.
12
+ *
13
+ * @param str - The input string to hash.
14
+ * @returns A hashed string representation of the input.
15
+ */
16
+ export const hash = (str: string): string => {
17
+ let hash = 0;
18
+ for (let i = 0; i < str.length; i++) {
19
+ hash = (hash << 5) - hash + str.charCodeAt(i);
20
+ hash &= hash;
21
+ }
22
+ return (hash >>> 0).toString(36).padStart(6, '0');
23
+ };
24
+
25
+ /**
26
+ * Parses a CSV stream with a sensible configuration for GTFS feeds.
27
+ *
28
+ * @param stream The CSV stream.
29
+ * @returns A parser from the csv-parse library.
30
+ */
31
+ export const parseCsv = (stream: NodeJS.ReadableStream): Parser => {
32
+ return stream.pipe(
33
+ parse({
34
+ delimiter: ',',
35
+ columns: true,
36
+ cast: true,
37
+ bom: true,
38
+ ignore_last_delimiters: true,
39
+ relax_column_count: true,
40
+ }),
41
+ );
42
+ };
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /*
2
+ * An index containing the full library, including node-dependent modules.
3
+ */
4
+
5
+ import { GtfsParser, GtfsProfile } from './gtfs/parser.js';
6
+ import { chGtfsProfile } from './gtfs/profiles/ch.js';
7
+ import { Plotter } from './routing/plotter.js';
8
+ import { Query } from './routing/query.js';
9
+ import { Result } from './routing/result.js';
10
+ import { Route } from './routing/route.js';
11
+ import { Router } from './routing/router.js';
12
+ import { StopsIndex } from './stops/stopsIndex.js';
13
+ import { Time } from './timetable/time.js';
14
+ import { Timetable } from './timetable/timetable.js';
15
+
16
+ export {
17
+ chGtfsProfile,
18
+ GtfsParser,
19
+ GtfsProfile,
20
+ Plotter,
21
+ Query,
22
+ Result,
23
+ Route,
24
+ Router,
25
+ StopsIndex,
26
+ Time,
27
+ Timetable,
28
+ };