minotor 7.0.2 → 9.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.
- package/.cspell.json +11 -1
- package/CHANGELOG.md +8 -3
- package/README.md +26 -24
- package/dist/cli.mjs +1786 -791
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/transfers.d.ts +29 -5
- package/dist/gtfs/trips.d.ts +10 -5
- package/dist/parser.cjs.js +972 -525
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +972 -525
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/plotter.test.d.ts +1 -0
- package/dist/routing/plotter.d.ts +42 -3
- package/dist/routing/result.d.ts +23 -7
- package/dist/routing/route.d.ts +2 -0
- package/dist/routing/router.d.ts +78 -19
- package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +4 -2
- package/dist/timetable/proto/timetable.d.ts +15 -1
- package/dist/timetable/route.d.ts +48 -23
- package/dist/timetable/timetable.d.ts +24 -7
- package/dist/timetable/tripBoardingId.d.ts +34 -0
- package/package.json +1 -1
- package/src/__e2e__/router.test.ts +114 -105
- package/src/__e2e__/timetable/stops.bin +2 -2
- package/src/__e2e__/timetable/timetable.bin +2 -2
- package/src/cli/repl.ts +245 -1
- package/src/gtfs/__tests__/parser.test.ts +19 -4
- package/src/gtfs/__tests__/transfers.test.ts +773 -37
- package/src/gtfs/__tests__/trips.test.ts +308 -27
- package/src/gtfs/parser.ts +36 -6
- package/src/gtfs/transfers.ts +193 -19
- package/src/gtfs/trips.ts +58 -21
- package/src/router.ts +2 -2
- package/src/routing/__tests__/plotter.test.ts +230 -0
- package/src/routing/__tests__/result.test.ts +486 -125
- package/src/routing/__tests__/route.test.ts +7 -3
- package/src/routing/__tests__/router.test.ts +380 -172
- package/src/routing/plotter.ts +279 -48
- package/src/routing/result.ts +114 -34
- package/src/routing/route.ts +0 -3
- package/src/routing/router.ts +344 -211
- package/src/timetable/__tests__/io.test.ts +34 -1
- package/src/timetable/__tests__/route.test.ts +74 -81
- package/src/timetable/__tests__/timetable.test.ts +232 -61
- package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
- package/src/timetable/io.ts +72 -10
- package/src/timetable/proto/timetable.proto +16 -2
- package/src/timetable/proto/timetable.ts +256 -22
- package/src/timetable/route.ts +174 -58
- package/src/timetable/timetable.ts +66 -16
- package/src/timetable/tripBoardingId.ts +94 -0
- package/tsconfig.json +2 -2
package/src/timetable/route.ts
CHANGED
|
@@ -26,11 +26,14 @@ export const MUST_PHONE_AGENCY = 2;
|
|
|
26
26
|
export const MUST_COORDINATE_WITH_DRIVER = 3;
|
|
27
27
|
|
|
28
28
|
/*
|
|
29
|
-
* A trip index corresponds to the index of
|
|
30
|
-
* first stop time in the trip divided by the number of stops
|
|
31
|
-
* in the given route
|
|
29
|
+
* A trip route index corresponds to the index of a given trip in a route.
|
|
32
30
|
*/
|
|
33
|
-
export type
|
|
31
|
+
export type TripRouteIndex = number;
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* A stop route index corresponds to the index of a given stop in a route.
|
|
35
|
+
*/
|
|
36
|
+
export type StopRouteIndex = number;
|
|
34
37
|
|
|
35
38
|
const pickUpDropOffTypeMap: PickUpDropOffType[] = [
|
|
36
39
|
'REGULAR',
|
|
@@ -59,6 +62,7 @@ const toPickupDropOffType = (numericalType: number): PickUpDropOffType => {
|
|
|
59
62
|
* A route identifies all trips of a given service route sharing the same list of stops.
|
|
60
63
|
*/
|
|
61
64
|
export class Route {
|
|
65
|
+
public readonly id: RouteId;
|
|
62
66
|
/**
|
|
63
67
|
* Arrivals and departures encoded as minutes from midnight.
|
|
64
68
|
* Format: [arrival1, departure1, arrival2, departure2, etc.]
|
|
@@ -90,7 +94,7 @@ export class Route {
|
|
|
90
94
|
* ...
|
|
91
95
|
* }
|
|
92
96
|
*/
|
|
93
|
-
private readonly stopIndices: Map<StopId,
|
|
97
|
+
private readonly stopIndices: Map<StopId, StopRouteIndex[]>;
|
|
94
98
|
/**
|
|
95
99
|
* The identifier of the route as a service shown to users.
|
|
96
100
|
*/
|
|
@@ -107,22 +111,131 @@ export class Route {
|
|
|
107
111
|
private readonly nbTrips: number;
|
|
108
112
|
|
|
109
113
|
constructor(
|
|
114
|
+
id: RouteId,
|
|
110
115
|
stopTimes: Uint16Array,
|
|
111
116
|
pickUpDropOffTypes: Uint8Array,
|
|
112
117
|
stops: Uint32Array,
|
|
113
118
|
serviceRouteId: ServiceRouteId,
|
|
114
119
|
) {
|
|
120
|
+
this.id = id;
|
|
115
121
|
this.stopTimes = stopTimes;
|
|
116
122
|
this.pickUpDropOffTypes = pickUpDropOffTypes;
|
|
117
123
|
this.stops = stops;
|
|
118
124
|
this.serviceRouteId = serviceRouteId;
|
|
119
125
|
this.nbStops = stops.length;
|
|
120
126
|
this.nbTrips = this.stopTimes.length / (this.stops.length * 2);
|
|
121
|
-
this.stopIndices = new Map<
|
|
127
|
+
this.stopIndices = new Map<StopId, StopRouteIndex[]>();
|
|
122
128
|
for (let i = 0; i < stops.length; i++) {
|
|
123
129
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
124
|
-
|
|
130
|
+
const stopId = stops[i]!;
|
|
131
|
+
const existingIndices = this.stopIndices.get(stopId);
|
|
132
|
+
if (existingIndices) {
|
|
133
|
+
existingIndices.push(i);
|
|
134
|
+
} else {
|
|
135
|
+
this.stopIndices.set(stopId, [i]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates a new route from multiple trips with their stops.
|
|
142
|
+
*
|
|
143
|
+
* @param params The route parameters including ID, service route ID, and trips.
|
|
144
|
+
* @returns The new route.
|
|
145
|
+
*/
|
|
146
|
+
static of(params: {
|
|
147
|
+
id: RouteId;
|
|
148
|
+
serviceRouteId: ServiceRouteId;
|
|
149
|
+
trips: Array<{
|
|
150
|
+
stops: Array<{
|
|
151
|
+
id: StopId;
|
|
152
|
+
arrivalTime: Time;
|
|
153
|
+
departureTime: Time;
|
|
154
|
+
dropOffType?: number;
|
|
155
|
+
pickUpType?: number;
|
|
156
|
+
}>;
|
|
157
|
+
}>;
|
|
158
|
+
}): Route {
|
|
159
|
+
const { id, serviceRouteId, trips } = params;
|
|
160
|
+
|
|
161
|
+
if (trips.length === 0) {
|
|
162
|
+
throw new Error('At least one trip must be provided');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// All trips must have the same stops in the same order
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
167
|
+
const firstTrip = trips[0]!;
|
|
168
|
+
const stopIds = new Uint32Array(firstTrip.stops.map((stop) => stop.id));
|
|
169
|
+
const numStops = stopIds.length;
|
|
170
|
+
|
|
171
|
+
// Validate all trips have the same stops
|
|
172
|
+
for (let tripIndex = 1; tripIndex < trips.length; tripIndex++) {
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
174
|
+
const trip = trips[tripIndex]!;
|
|
175
|
+
if (trip.stops.length !== numStops) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Trip ${tripIndex} has ${trip.stops.length} stops, expected ${numStops}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
182
|
+
if (trip.stops[stopIndex]!.id !== stopIds[stopIndex]) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Trip ${tripIndex} has different stop at index ${stopIndex}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create stopTimes array with arrivals and departures for all trips
|
|
191
|
+
const stopTimes = new Uint16Array(trips.length * numStops * 2);
|
|
192
|
+
for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
194
|
+
const trip = trips[tripIndex]!;
|
|
195
|
+
for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
197
|
+
const stop = trip.stops[stopIndex]!;
|
|
198
|
+
const baseIndex = (tripIndex * numStops + stopIndex) * 2;
|
|
199
|
+
stopTimes[baseIndex] = stop.arrivalTime.toMinutes();
|
|
200
|
+
stopTimes[baseIndex + 1] = stop.departureTime.toMinutes();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create pickUpDropOffTypes array (2-bit encoded) for all trips
|
|
205
|
+
const totalStopEntries = trips.length * numStops;
|
|
206
|
+
const pickUpDropOffTypes = new Uint8Array(Math.ceil(totalStopEntries / 2));
|
|
207
|
+
|
|
208
|
+
for (let tripIndex = 0; tripIndex < trips.length; tripIndex++) {
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
210
|
+
const trip = trips[tripIndex]!;
|
|
211
|
+
for (let stopIndex = 0; stopIndex < numStops; stopIndex++) {
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
213
|
+
const stop = trip.stops[stopIndex]!;
|
|
214
|
+
const globalIndex = tripIndex * numStops + stopIndex;
|
|
215
|
+
const pickUp = stop.pickUpType ?? REGULAR;
|
|
216
|
+
const dropOff = stop.dropOffType ?? REGULAR;
|
|
217
|
+
const byteIndex = Math.floor(globalIndex / 2);
|
|
218
|
+
const isSecondPair = globalIndex % 2 === 1;
|
|
219
|
+
|
|
220
|
+
if (isSecondPair) {
|
|
221
|
+
// Second pair: pickup in upper 2 bits, dropOff in bits 4-5
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
223
|
+
pickUpDropOffTypes[byteIndex]! |= (pickUp << 6) | (dropOff << 4);
|
|
224
|
+
} else {
|
|
225
|
+
// First pair: pickup in bits 2-3, dropOff in lower 2 bits
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
227
|
+
pickUpDropOffTypes[byteIndex]! |= (pickUp << 2) | dropOff;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
125
230
|
}
|
|
231
|
+
|
|
232
|
+
return new Route(
|
|
233
|
+
id,
|
|
234
|
+
stopTimes,
|
|
235
|
+
pickUpDropOffTypes,
|
|
236
|
+
stopIds,
|
|
237
|
+
serviceRouteId,
|
|
238
|
+
);
|
|
126
239
|
}
|
|
127
240
|
|
|
128
241
|
/**
|
|
@@ -140,35 +253,21 @@ export class Route {
|
|
|
140
253
|
}
|
|
141
254
|
|
|
142
255
|
/**
|
|
143
|
-
*
|
|
256
|
+
* Retrieves the number of stops in the route.
|
|
144
257
|
*
|
|
145
|
-
* @
|
|
146
|
-
* @param stopB - The StopId of the second stop.
|
|
147
|
-
* @returns True if stop A is before stop B, false otherwise.
|
|
258
|
+
* @returns The total number of stops in the route.
|
|
148
259
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (stopAIndex === undefined) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Stop index ${stopAIndex} not found in route ${this.serviceRouteId}`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
const stopBIndex = this.stopIndices.get(stopB);
|
|
157
|
-
if (stopBIndex === undefined) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
`Stop index ${stopBIndex} not found in route ${this.serviceRouteId}`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
return stopAIndex < stopBIndex;
|
|
260
|
+
getNbStops(): number {
|
|
261
|
+
return this.nbStops;
|
|
163
262
|
}
|
|
164
263
|
|
|
165
264
|
/**
|
|
166
|
-
* Retrieves the number of
|
|
265
|
+
* Retrieves the number of trips in the route.
|
|
167
266
|
*
|
|
168
|
-
* @returns The total number of
|
|
267
|
+
* @returns The total number of trips in the route.
|
|
169
268
|
*/
|
|
170
|
-
|
|
171
|
-
return this.
|
|
269
|
+
getNbTrips(): number {
|
|
270
|
+
return this.nbTrips;
|
|
172
271
|
}
|
|
173
272
|
|
|
174
273
|
/**
|
|
@@ -184,17 +283,16 @@ export class Route {
|
|
|
184
283
|
/**
|
|
185
284
|
* Retrieves the arrival time at a specific stop for a given trip.
|
|
186
285
|
*
|
|
187
|
-
* @param
|
|
286
|
+
* @param stopIndex - The index of the stop in the route.
|
|
188
287
|
* @param tripIndex - The index of the trip.
|
|
189
288
|
* @returns The arrival time at the specified stop and trip as a Time object.
|
|
190
289
|
*/
|
|
191
|
-
arrivalAt(
|
|
192
|
-
const arrivalIndex =
|
|
193
|
-
(tripIndex * this.stops.length + this.stopIndex(stopId)) * 2;
|
|
290
|
+
arrivalAt(stopIndex: StopRouteIndex, tripIndex: TripRouteIndex): Time {
|
|
291
|
+
const arrivalIndex = (tripIndex * this.stops.length + stopIndex) * 2;
|
|
194
292
|
const arrival = this.stopTimes[arrivalIndex];
|
|
195
293
|
if (arrival === undefined) {
|
|
196
294
|
throw new Error(
|
|
197
|
-
`Arrival time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
295
|
+
`Arrival time not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
198
296
|
);
|
|
199
297
|
}
|
|
200
298
|
return Time.fromMinutes(arrival);
|
|
@@ -203,17 +301,16 @@ export class Route {
|
|
|
203
301
|
/**
|
|
204
302
|
* Retrieves the departure time at a specific stop for a given trip.
|
|
205
303
|
*
|
|
206
|
-
* @param
|
|
304
|
+
* @param stopIndex - The index of the stop in the route.
|
|
207
305
|
* @param tripIndex - The index of the trip.
|
|
208
306
|
* @returns The departure time at the specified stop and trip as a Time object.
|
|
209
307
|
*/
|
|
210
|
-
departureFrom(
|
|
211
|
-
const departureIndex =
|
|
212
|
-
(tripIndex * this.stops.length + this.stopIndex(stopId)) * 2 + 1;
|
|
308
|
+
departureFrom(stopIndex: StopRouteIndex, tripIndex: TripRouteIndex): Time {
|
|
309
|
+
const departureIndex = (tripIndex * this.stops.length + stopIndex) * 2 + 1;
|
|
213
310
|
const departure = this.stopTimes[departureIndex];
|
|
214
311
|
if (departure === undefined) {
|
|
215
312
|
throw new Error(
|
|
216
|
-
`Departure time not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
313
|
+
`Departure time not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
217
314
|
);
|
|
218
315
|
}
|
|
219
316
|
return Time.fromMinutes(departure);
|
|
@@ -222,19 +319,22 @@ export class Route {
|
|
|
222
319
|
/**
|
|
223
320
|
* Retrieves the pick-up type for a specific stop and trip.
|
|
224
321
|
*
|
|
225
|
-
* @param
|
|
322
|
+
* @param stopIndex - The index of the stop in the route.
|
|
226
323
|
* @param tripIndex - The index of the trip.
|
|
227
324
|
* @returns The pick-up type at the specified stop and trip.
|
|
228
325
|
*/
|
|
229
|
-
pickUpTypeFrom(
|
|
230
|
-
|
|
326
|
+
pickUpTypeFrom(
|
|
327
|
+
stopIndex: StopRouteIndex,
|
|
328
|
+
tripIndex: TripRouteIndex,
|
|
329
|
+
): PickUpDropOffType {
|
|
330
|
+
const globalIndex = tripIndex * this.stops.length + stopIndex;
|
|
231
331
|
const byteIndex = Math.floor(globalIndex / 2);
|
|
232
332
|
const isSecondPair = globalIndex % 2 === 1;
|
|
233
333
|
|
|
234
334
|
const byte = this.pickUpDropOffTypes[byteIndex];
|
|
235
335
|
if (byte === undefined) {
|
|
236
336
|
throw new Error(
|
|
237
|
-
`Pick up type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
337
|
+
`Pick up type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
238
338
|
);
|
|
239
339
|
}
|
|
240
340
|
|
|
@@ -247,19 +347,22 @@ export class Route {
|
|
|
247
347
|
/**
|
|
248
348
|
* Retrieves the drop-off type for a specific stop and trip.
|
|
249
349
|
*
|
|
250
|
-
* @param
|
|
350
|
+
* @param stopIndex - The index of the stop in the route.
|
|
251
351
|
* @param tripIndex - The index of the trip.
|
|
252
352
|
* @returns The drop-off type at the specified stop and trip.
|
|
253
353
|
*/
|
|
254
|
-
dropOffTypeAt(
|
|
255
|
-
|
|
354
|
+
dropOffTypeAt(
|
|
355
|
+
stopIndex: StopRouteIndex,
|
|
356
|
+
tripIndex: TripRouteIndex,
|
|
357
|
+
): PickUpDropOffType {
|
|
358
|
+
const globalIndex = tripIndex * this.stops.length + stopIndex;
|
|
256
359
|
const byteIndex = Math.floor(globalIndex / 2);
|
|
257
360
|
const isSecondPair = globalIndex % 2 === 1;
|
|
258
361
|
|
|
259
362
|
const byte = this.pickUpDropOffTypes[byteIndex];
|
|
260
363
|
if (byte === undefined) {
|
|
261
364
|
throw new Error(
|
|
262
|
-
`Drop off type not found for stop ${stopId} at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
365
|
+
`Drop off type not found for stop ${this.stopId(stopIndex)} (${stopIndex}) at trip index ${tripIndex} in route ${this.serviceRouteId}`,
|
|
263
366
|
);
|
|
264
367
|
}
|
|
265
368
|
|
|
@@ -274,7 +377,7 @@ export class Route {
|
|
|
274
377
|
* optionally constrained by a latest trip index and a time before which the trip
|
|
275
378
|
* should not depart.
|
|
276
379
|
* *
|
|
277
|
-
* @param
|
|
380
|
+
* @param stopIndex - The route index of the stop where the trip should be found.
|
|
278
381
|
* @param [after=Time.origin()] - The earliest time after which the trip should depart.
|
|
279
382
|
* If not provided, searches all available trips.
|
|
280
383
|
* @param [beforeTrip] - (Optional) The index of the trip before which the search should be constrained.
|
|
@@ -282,10 +385,10 @@ export class Route {
|
|
|
282
385
|
* @returns The index of the earliest trip meeting the criteria, or undefined if no such trip is found.
|
|
283
386
|
*/
|
|
284
387
|
findEarliestTrip(
|
|
285
|
-
|
|
388
|
+
stopIndex: StopRouteIndex,
|
|
286
389
|
after: Time = Time.origin(),
|
|
287
|
-
beforeTrip?:
|
|
288
|
-
):
|
|
390
|
+
beforeTrip?: TripRouteIndex,
|
|
391
|
+
): TripRouteIndex | undefined {
|
|
289
392
|
if (this.nbTrips <= 0) return undefined;
|
|
290
393
|
|
|
291
394
|
let hi = this.nbTrips - 1;
|
|
@@ -296,7 +399,7 @@ export class Route {
|
|
|
296
399
|
let lb = -1;
|
|
297
400
|
while (lo <= hi) {
|
|
298
401
|
const mid = (lo + hi) >>> 1;
|
|
299
|
-
const depMid = this.departureFrom(
|
|
402
|
+
const depMid = this.departureFrom(stopIndex, mid);
|
|
300
403
|
if (depMid.isBefore(after)) {
|
|
301
404
|
lo = mid + 1;
|
|
302
405
|
} else {
|
|
@@ -307,7 +410,7 @@ export class Route {
|
|
|
307
410
|
if (lb === -1) return undefined;
|
|
308
411
|
|
|
309
412
|
for (let t = lb; t < (beforeTrip ?? this.nbTrips); t++) {
|
|
310
|
-
const pickup = this.pickUpTypeFrom(
|
|
413
|
+
const pickup = this.pickUpTypeFrom(stopIndex, t);
|
|
311
414
|
if (pickup !== 'NOT_AVAILABLE') {
|
|
312
415
|
return t;
|
|
313
416
|
}
|
|
@@ -316,17 +419,30 @@ export class Route {
|
|
|
316
419
|
}
|
|
317
420
|
|
|
318
421
|
/**
|
|
319
|
-
* Retrieves the
|
|
422
|
+
* Retrieves the indices of a stop within the route.
|
|
320
423
|
* @param stopId The StopId of the stop to locate in the route.
|
|
321
|
-
* @returns
|
|
424
|
+
* @returns An array of indices where the stop appears in the route, or an empty array if the stop is not found.
|
|
322
425
|
*/
|
|
323
|
-
public
|
|
426
|
+
public stopRouteIndices(stopId: StopId): StopRouteIndex[] {
|
|
324
427
|
const stopIndex = this.stopIndices.get(stopId);
|
|
325
428
|
if (stopIndex === undefined) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
return stopIndex;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Retrieves the id of a stop at a given index in a route.
|
|
436
|
+
* @param stopRouteIndex The route index of the stop.
|
|
437
|
+
* @returns The id of the stop at the given index in the route.
|
|
438
|
+
*/
|
|
439
|
+
public stopId(stopRouteIndex: StopRouteIndex): StopId {
|
|
440
|
+
const stopId = this.stops[stopRouteIndex];
|
|
441
|
+
if (stopId === undefined) {
|
|
326
442
|
throw new Error(
|
|
327
|
-
`
|
|
443
|
+
`StopId for stop at index ${stopRouteIndex} not found in route ${this.serviceRouteId}`,
|
|
328
444
|
);
|
|
329
445
|
}
|
|
330
|
-
return
|
|
446
|
+
return stopId;
|
|
331
447
|
}
|
|
332
448
|
}
|
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
deserializeRoutesAdjacency,
|
|
8
8
|
deserializeServiceRoutesMap,
|
|
9
9
|
deserializeStopsAdjacency,
|
|
10
|
+
deserializeTripContinuations,
|
|
10
11
|
serializeRoutesAdjacency,
|
|
11
12
|
serializeServiceRoutesMap,
|
|
12
13
|
serializeStopsAdjacency,
|
|
14
|
+
serializeTripContinuations,
|
|
13
15
|
} from './io.js';
|
|
14
16
|
import { Timetable as ProtoTimetable } from './proto/timetable.js';
|
|
15
|
-
import { Route, RouteId } from './route.js';
|
|
17
|
+
import { Route, RouteId, StopRouteIndex, TripRouteIndex } from './route.js';
|
|
18
|
+
import { encode, TripBoardingId } from './tripBoardingId.js';
|
|
16
19
|
|
|
17
20
|
export type TransferType =
|
|
18
21
|
| 'RECOMMENDED'
|
|
@@ -26,11 +29,19 @@ export type Transfer = {
|
|
|
26
29
|
minTransferTime?: Duration;
|
|
27
30
|
};
|
|
28
31
|
|
|
32
|
+
export type TripBoarding = {
|
|
33
|
+
hopOnStopIndex: StopRouteIndex;
|
|
34
|
+
routeId: RouteId;
|
|
35
|
+
tripIndex: TripRouteIndex;
|
|
36
|
+
};
|
|
37
|
+
|
|
29
38
|
export type StopAdjacency = {
|
|
30
|
-
transfers
|
|
39
|
+
transfers?: Transfer[];
|
|
31
40
|
routes: RouteId[];
|
|
32
41
|
};
|
|
33
42
|
|
|
43
|
+
export type TripContinuations = Map<TripBoardingId, TripBoarding[]>;
|
|
44
|
+
|
|
34
45
|
export type ServiceRouteId = number;
|
|
35
46
|
|
|
36
47
|
export type RouteType =
|
|
@@ -68,7 +79,9 @@ export const ALL_TRANSPORT_MODES: Set<RouteType> = new Set([
|
|
|
68
79
|
'MONORAIL',
|
|
69
80
|
]);
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
const EMPTY_TRIP_CONTINUATIONS: TripBoarding[] = [];
|
|
83
|
+
|
|
84
|
+
export const CURRENT_VERSION = '0.0.9';
|
|
72
85
|
|
|
73
86
|
/**
|
|
74
87
|
* The internal transit timetable format.
|
|
@@ -77,21 +90,26 @@ export class Timetable {
|
|
|
77
90
|
private readonly stopsAdjacency: StopAdjacency[];
|
|
78
91
|
private readonly routesAdjacency: Route[];
|
|
79
92
|
private readonly serviceRoutes: ServiceRoute[];
|
|
93
|
+
private readonly tripContinuations?: TripContinuations;
|
|
80
94
|
private readonly activeStops: Set<StopId>;
|
|
81
95
|
|
|
82
96
|
constructor(
|
|
83
97
|
stopsAdjacency: StopAdjacency[],
|
|
84
98
|
routesAdjacency: Route[],
|
|
85
99
|
routes: ServiceRoute[],
|
|
100
|
+
tripContinuations?: TripContinuations,
|
|
86
101
|
) {
|
|
87
102
|
this.stopsAdjacency = stopsAdjacency;
|
|
88
103
|
this.routesAdjacency = routesAdjacency;
|
|
89
104
|
this.serviceRoutes = routes;
|
|
105
|
+
this.tripContinuations = tripContinuations;
|
|
90
106
|
this.activeStops = new Set<StopId>();
|
|
91
107
|
for (let i = 0; i < stopsAdjacency.length; i++) {
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
93
108
|
const stop = stopsAdjacency[i]!;
|
|
94
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
stop.routes.length > 0 ||
|
|
111
|
+
(stop.transfers && stop.transfers.length > 0)
|
|
112
|
+
) {
|
|
95
113
|
this.activeStops.add(i);
|
|
96
114
|
}
|
|
97
115
|
}
|
|
@@ -108,6 +126,9 @@ export class Timetable {
|
|
|
108
126
|
stopsAdjacency: serializeStopsAdjacency(this.stopsAdjacency),
|
|
109
127
|
routesAdjacency: serializeRoutesAdjacency(this.routesAdjacency),
|
|
110
128
|
serviceRoutes: serializeServiceRoutesMap(this.serviceRoutes),
|
|
129
|
+
tripContinuations: serializeTripContinuations(
|
|
130
|
+
this.tripContinuations || new Map<TripBoardingId, TripBoarding[]>(),
|
|
131
|
+
),
|
|
111
132
|
};
|
|
112
133
|
const writer = new BinaryWriter();
|
|
113
134
|
ProtoTimetable.encode(protoTimetable, writer);
|
|
@@ -131,8 +152,8 @@ export class Timetable {
|
|
|
131
152
|
return new Timetable(
|
|
132
153
|
deserializeStopsAdjacency(protoTimetable.stopsAdjacency),
|
|
133
154
|
deserializeRoutesAdjacency(protoTimetable.routesAdjacency),
|
|
134
|
-
|
|
135
155
|
deserializeServiceRoutesMap(protoTimetable.serviceRoutes),
|
|
156
|
+
deserializeTripContinuations(protoTimetable.tripContinuations),
|
|
136
157
|
);
|
|
137
158
|
}
|
|
138
159
|
|
|
@@ -166,7 +187,33 @@ export class Timetable {
|
|
|
166
187
|
* @returns An array of transfer options available at the stop.
|
|
167
188
|
*/
|
|
168
189
|
getTransfers(stopId: StopId): Transfer[] {
|
|
169
|
-
|
|
190
|
+
const stopAdjacency = this.stopsAdjacency[stopId];
|
|
191
|
+
if (!stopAdjacency) {
|
|
192
|
+
throw new Error(`Stop ID ${stopId} not found`);
|
|
193
|
+
}
|
|
194
|
+
return stopAdjacency.transfers || [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Retrieves all trip continuation options available at the specified stop for a given trip.
|
|
199
|
+
*
|
|
200
|
+
* @param stopIndex - The index in the route of the stop to get trip continuations for.
|
|
201
|
+
* @param routeId - The ID of the route to get continuations for.
|
|
202
|
+
* @param tripIndex - The index of the trip to get continuations for.
|
|
203
|
+
* @returns An array of trip continuation options available at the stop for the specified trip.
|
|
204
|
+
*/
|
|
205
|
+
getContinuousTrips(
|
|
206
|
+
stopIndex: StopRouteIndex,
|
|
207
|
+
routeId: RouteId,
|
|
208
|
+
tripIndex: TripRouteIndex,
|
|
209
|
+
): TripBoarding[] {
|
|
210
|
+
const tripContinuations = this.tripContinuations?.get(
|
|
211
|
+
encode(stopIndex, routeId, tripIndex),
|
|
212
|
+
);
|
|
213
|
+
if (!tripContinuations) {
|
|
214
|
+
return EMPTY_TRIP_CONTINUATIONS;
|
|
215
|
+
}
|
|
216
|
+
return tripContinuations;
|
|
170
217
|
}
|
|
171
218
|
|
|
172
219
|
/**
|
|
@@ -211,18 +258,18 @@ export class Timetable {
|
|
|
211
258
|
|
|
212
259
|
/**
|
|
213
260
|
* Finds routes that are reachable from a set of stop IDs.
|
|
214
|
-
* Also identifies the first stop available to hop on each route among
|
|
261
|
+
* Also identifies the first stop index available to hop on each route among
|
|
215
262
|
* the input stops.
|
|
216
263
|
*
|
|
217
264
|
* @param fromStops - The set of stop IDs to find reachable routes from.
|
|
218
265
|
* @param transportModes - The set of transport modes to consider for reachable routes.
|
|
219
|
-
* @returns A map of reachable routes to the first stop available to hop on each route.
|
|
266
|
+
* @returns A map of reachable routes to the first stop index available to hop on each route.
|
|
220
267
|
*/
|
|
221
268
|
findReachableRoutes(
|
|
222
269
|
fromStops: Set<StopId>,
|
|
223
270
|
transportModes: Set<RouteType> = ALL_TRANSPORT_MODES,
|
|
224
|
-
): Map<Route,
|
|
225
|
-
const reachableRoutes = new Map<Route,
|
|
271
|
+
): Map<Route, StopRouteIndex> {
|
|
272
|
+
const reachableRoutes = new Map<Route, StopRouteIndex>();
|
|
226
273
|
const fromStopsArray = Array.from(fromStops);
|
|
227
274
|
for (let i = 0; i < fromStopsArray.length; i++) {
|
|
228
275
|
const originStop = fromStopsArray[i]!;
|
|
@@ -234,14 +281,17 @@ export class Timetable {
|
|
|
234
281
|
);
|
|
235
282
|
for (let j = 0; j < validRoutes.length; j++) {
|
|
236
283
|
const route = validRoutes[j]!;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
const originStopIndices = route.stopRouteIndices(originStop);
|
|
285
|
+
const originStopIndex = originStopIndices[0]!;
|
|
286
|
+
|
|
287
|
+
const existingHopOnStopIndex = reachableRoutes.get(route);
|
|
288
|
+
if (existingHopOnStopIndex !== undefined) {
|
|
289
|
+
if (originStopIndex < existingHopOnStopIndex) {
|
|
240
290
|
// if the current stop is before the existing hop on stop, replace it
|
|
241
|
-
reachableRoutes.set(route,
|
|
291
|
+
reachableRoutes.set(route, originStopIndex);
|
|
242
292
|
}
|
|
243
293
|
} else {
|
|
244
|
-
reachableRoutes.set(route,
|
|
294
|
+
reachableRoutes.set(route, originStopIndex);
|
|
245
295
|
}
|
|
246
296
|
}
|
|
247
297
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { RouteId, StopRouteIndex, TripRouteIndex } from './route.js';
|
|
2
|
+
|
|
3
|
+
// Each value uses 20 bits, allowing values from 0 to 1,048,575 (2^20 - 1)
|
|
4
|
+
const VALUE_MASK = (1n << 20n) - 1n; // 0xFFFFF
|
|
5
|
+
const MAX_VALUE = 1_048_575; // 2^20 - 1
|
|
6
|
+
|
|
7
|
+
// Bit positions for each value in the 60-bit bigint
|
|
8
|
+
const TRIP_INDEX_SHIFT = 0n;
|
|
9
|
+
const ROUTE_ID_SHIFT = 20n;
|
|
10
|
+
const STOP_INDEX_SHIFT = 40n;
|
|
11
|
+
|
|
12
|
+
// A TripId encodes a stop index, route ID, and trip index into a single bigint value
|
|
13
|
+
export type TripBoardingId = bigint;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a value fits within 20 bits (0 to 1,048,575)
|
|
17
|
+
* @param value - The value to validate
|
|
18
|
+
* @param name - The name of the value for error reporting
|
|
19
|
+
* @throws Error if the value is out of range
|
|
20
|
+
*/
|
|
21
|
+
const validateValue = (value: number, name: string): void => {
|
|
22
|
+
if (value < 0 || value > MAX_VALUE) {
|
|
23
|
+
throw new Error(`${name} must be between 0 and ${MAX_VALUE}, got ${value}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Encodes a stop index, route ID, and trip index into a single trip boarding ID.
|
|
29
|
+
* @param stopIndex - The index of the stop within the route (0 to 1,048,575)
|
|
30
|
+
* @param routeId - The route identifier (0 to 1,048,575)
|
|
31
|
+
* @param tripIndex - The index of the trip within the route (0 to 1,048,575)
|
|
32
|
+
* @returns The encoded trip ID as a bigint
|
|
33
|
+
*/
|
|
34
|
+
export const encode = (
|
|
35
|
+
stopIndex: StopRouteIndex,
|
|
36
|
+
routeId: RouteId,
|
|
37
|
+
tripIndex: TripRouteIndex,
|
|
38
|
+
): TripBoardingId => {
|
|
39
|
+
validateValue(stopIndex, 'stopIndex');
|
|
40
|
+
validateValue(routeId, 'routeId');
|
|
41
|
+
validateValue(tripIndex, 'tripIndex');
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
(BigInt(stopIndex) << STOP_INDEX_SHIFT) |
|
|
45
|
+
(BigInt(routeId) << ROUTE_ID_SHIFT) |
|
|
46
|
+
(BigInt(tripIndex) << TRIP_INDEX_SHIFT)
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decodes a trip boarding ID back into its constituent stop index, route ID, and trip index.
|
|
52
|
+
* @param tripBoardingId - The encoded trip ID
|
|
53
|
+
* @returns A tuple containing [stopIndex, routeId, tripIndex]
|
|
54
|
+
*/
|
|
55
|
+
export const decode = (
|
|
56
|
+
tripBoardingId: TripBoardingId,
|
|
57
|
+
): [StopRouteIndex, RouteId, TripRouteIndex] => {
|
|
58
|
+
const stopIndex = Number((tripBoardingId >> STOP_INDEX_SHIFT) & VALUE_MASK);
|
|
59
|
+
const routeId = Number((tripBoardingId >> ROUTE_ID_SHIFT) & VALUE_MASK);
|
|
60
|
+
const tripIndex = Number((tripBoardingId >> TRIP_INDEX_SHIFT) & VALUE_MASK);
|
|
61
|
+
|
|
62
|
+
return [stopIndex, routeId, tripIndex];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extracts just the stop index from a trip ID without full decoding.
|
|
67
|
+
* @param tripBoardingId - The encoded trip boarding ID
|
|
68
|
+
* @returns The stop index
|
|
69
|
+
*/
|
|
70
|
+
export const getStopIndex = (
|
|
71
|
+
tripBoardingId: TripBoardingId,
|
|
72
|
+
): StopRouteIndex => {
|
|
73
|
+
return Number((tripBoardingId >> STOP_INDEX_SHIFT) & VALUE_MASK);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extracts just the route ID from a trip ID without full decoding.
|
|
78
|
+
* @param tripBoardingId - The encoded trip boarding ID
|
|
79
|
+
* @returns The route ID
|
|
80
|
+
*/
|
|
81
|
+
export const getRouteId = (tripBoardingId: TripBoardingId): RouteId => {
|
|
82
|
+
return Number((tripBoardingId >> ROUTE_ID_SHIFT) & VALUE_MASK);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extracts just the trip index from a trip ID without full decoding.
|
|
87
|
+
* @param tripBoardingId - The encoded trip boarding ID
|
|
88
|
+
* @returns The trip index
|
|
89
|
+
*/
|
|
90
|
+
export const getTripIndex = (
|
|
91
|
+
tripBoardingId: TripBoardingId,
|
|
92
|
+
): TripRouteIndex => {
|
|
93
|
+
return Number((tripBoardingId >> TRIP_INDEX_SHIFT) & VALUE_MASK);
|
|
94
|
+
};
|