minotor 7.0.1 → 8.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 +1268 -290
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/transfers.d.ts +13 -4
- package/dist/gtfs/trips.d.ts +12 -7
- package/dist/parser.cjs.js +519 -95
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +519 -95
- 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__/tripId.test.d.ts +1 -0
- package/dist/timetable/io.d.ts +4 -2
- package/dist/timetable/proto/timetable.d.ts +13 -1
- package/dist/timetable/route.d.ts +41 -8
- package/dist/timetable/timetable.d.ts +18 -3
- package/dist/timetable/tripId.d.ts +15 -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 +259 -1
- package/src/gtfs/__tests__/transfers.test.ts +468 -12
- package/src/gtfs/__tests__/trips.test.ts +350 -28
- package/src/gtfs/parser.ts +16 -4
- package/src/gtfs/transfers.ts +61 -18
- package/src/gtfs/trips.ts +97 -22
- 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 +378 -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 +332 -209
- package/src/timetable/__tests__/io.test.ts +33 -1
- package/src/timetable/__tests__/route.test.ts +10 -3
- package/src/timetable/__tests__/timetable.test.ts +225 -57
- package/src/timetable/__tests__/tripId.test.ts +27 -0
- package/src/timetable/io.ts +71 -10
- package/src/timetable/proto/timetable.proto +14 -2
- package/src/timetable/proto/timetable.ts +218 -20
- package/src/timetable/route.ts +152 -19
- package/src/timetable/time.ts +23 -9
- package/src/timetable/timetable.ts +46 -9
- package/src/timetable/tripId.ts +29 -0
package/src/gtfs/transfers.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TransferType,
|
|
7
7
|
} from '../timetable/timetable.js';
|
|
8
8
|
import { GtfsStopsMap } from './stops.js';
|
|
9
|
-
import {
|
|
9
|
+
import { GtfsTripId } from './trips.js';
|
|
10
10
|
import { parseCsv } from './utils.js';
|
|
11
11
|
|
|
12
12
|
export type GtfsTransferType =
|
|
@@ -19,11 +19,19 @@ export type GtfsTransferType =
|
|
|
19
19
|
|
|
20
20
|
export type TransfersMap = Map<StopId, Transfer[]>;
|
|
21
21
|
|
|
22
|
+
export type GtfsTripBoarding = {
|
|
23
|
+
fromTrip: GtfsTripId;
|
|
24
|
+
toTrip: GtfsTripId;
|
|
25
|
+
hopOnStop: StopId;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type TripContinuationsMap = Map<StopId, GtfsTripBoarding[]>;
|
|
29
|
+
|
|
22
30
|
export type TransferEntry = {
|
|
23
31
|
from_stop_id?: SourceStopId;
|
|
24
32
|
to_stop_id?: SourceStopId;
|
|
25
|
-
from_trip_id?:
|
|
26
|
-
to_trip_id?:
|
|
33
|
+
from_trip_id?: GtfsTripId;
|
|
34
|
+
to_trip_id?: GtfsTripId;
|
|
27
35
|
from_route_id?: ServiceRouteId;
|
|
28
36
|
to_route_id?: ServiceRouteId;
|
|
29
37
|
transfer_type: GtfsTransferType;
|
|
@@ -39,9 +47,12 @@ export type TransferEntry = {
|
|
|
39
47
|
export const parseTransfers = async (
|
|
40
48
|
transfersStream: NodeJS.ReadableStream,
|
|
41
49
|
stopsMap: GtfsStopsMap,
|
|
42
|
-
): Promise<
|
|
50
|
+
): Promise<{
|
|
51
|
+
transfers: TransfersMap;
|
|
52
|
+
tripContinuations: TripContinuationsMap;
|
|
53
|
+
}> => {
|
|
43
54
|
const transfers: TransfersMap = new Map();
|
|
44
|
-
|
|
55
|
+
const tripContinuations: TripContinuationsMap = new Map();
|
|
45
56
|
for await (const rawLine of parseCsv(transfersStream, [
|
|
46
57
|
'transfer_type',
|
|
47
58
|
'min_transfer_time',
|
|
@@ -54,33 +65,62 @@ export const parseTransfers = async (
|
|
|
54
65
|
) {
|
|
55
66
|
continue;
|
|
56
67
|
}
|
|
68
|
+
if (!transferEntry.from_stop_id || !transferEntry.to_stop_id) {
|
|
69
|
+
console.warn(`Missing transfer origin or destination stop.`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
73
|
+
const fromStop = stopsMap.get(transferEntry.from_stop_id)!;
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
75
|
+
const toStop = stopsMap.get(transferEntry.to_stop_id)!;
|
|
76
|
+
|
|
77
|
+
if (transferEntry.transfer_type === 4) {
|
|
78
|
+
if (
|
|
79
|
+
transferEntry.from_trip_id === undefined ||
|
|
80
|
+
transferEntry.from_trip_id === '' ||
|
|
81
|
+
transferEntry.to_trip_id === undefined ||
|
|
82
|
+
transferEntry.to_trip_id === ''
|
|
83
|
+
) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`Unsupported in-seat transfer, missing from_trip_id and/or to_trip_id.`,
|
|
86
|
+
);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const tripBoardingEntry: GtfsTripBoarding = {
|
|
90
|
+
fromTrip: transferEntry.from_trip_id,
|
|
91
|
+
toTrip: transferEntry.to_trip_id,
|
|
92
|
+
hopOnStop: toStop.id,
|
|
93
|
+
};
|
|
94
|
+
const existingBoardings = tripContinuations.get(fromStop.id);
|
|
95
|
+
if (existingBoardings) {
|
|
96
|
+
existingBoardings.push(tripBoardingEntry);
|
|
97
|
+
} else {
|
|
98
|
+
tripContinuations.set(fromStop.id, [tripBoardingEntry]);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
57
102
|
if (transferEntry.from_trip_id && transferEntry.to_trip_id) {
|
|
58
103
|
console.warn(
|
|
59
|
-
`Unsupported transfer between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`,
|
|
104
|
+
`Unsupported transfer of type ${transferEntry.transfer_type} between trips ${transferEntry.from_trip_id} and ${transferEntry.to_trip_id}.`,
|
|
60
105
|
);
|
|
61
106
|
continue;
|
|
62
107
|
}
|
|
63
108
|
if (transferEntry.from_route_id && transferEntry.to_route_id) {
|
|
64
109
|
console.warn(
|
|
65
|
-
`Unsupported transfer between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`,
|
|
110
|
+
`Unsupported transfer of type ${transferEntry.transfer_type} between routes ${transferEntry.from_route_id} and ${transferEntry.to_route_id}.`,
|
|
66
111
|
);
|
|
67
112
|
continue;
|
|
68
113
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
transferEntry.transfer_type === 2 &&
|
|
117
|
+
transferEntry.min_transfer_time === undefined
|
|
118
|
+
) {
|
|
74
119
|
console.info(
|
|
75
120
|
`Missing minimum transfer time between ${transferEntry.from_stop_id} and ${transferEntry.to_stop_id}.`,
|
|
76
121
|
);
|
|
77
122
|
}
|
|
78
123
|
|
|
79
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
80
|
-
const fromStop = stopsMap.get(transferEntry.from_stop_id)!;
|
|
81
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
82
|
-
const toStop = stopsMap.get(transferEntry.to_stop_id)!;
|
|
83
|
-
|
|
84
124
|
const transfer: Transfer = {
|
|
85
125
|
destination: toStop.id,
|
|
86
126
|
type: parseGtfsTransferType(transferEntry.transfer_type),
|
|
@@ -93,7 +133,10 @@ export const parseTransfers = async (
|
|
|
93
133
|
fromStopTransfers.push(transfer);
|
|
94
134
|
transfers.set(fromStop.id, fromStopTransfers);
|
|
95
135
|
}
|
|
96
|
-
return
|
|
136
|
+
return {
|
|
137
|
+
transfers,
|
|
138
|
+
tripContinuations,
|
|
139
|
+
};
|
|
97
140
|
};
|
|
98
141
|
|
|
99
142
|
const parseGtfsTransferType = (
|
package/src/gtfs/trips.ts
CHANGED
|
@@ -6,27 +6,35 @@ import {
|
|
|
6
6
|
NOT_AVAILABLE,
|
|
7
7
|
REGULAR,
|
|
8
8
|
Route,
|
|
9
|
+
RouteId,
|
|
10
|
+
TripRouteIndex,
|
|
9
11
|
} from '../timetable/route.js';
|
|
10
12
|
import {
|
|
11
13
|
ServiceRoute,
|
|
12
14
|
ServiceRouteId,
|
|
13
15
|
StopAdjacency,
|
|
14
16
|
} from '../timetable/timetable.js';
|
|
17
|
+
import { encode } from '../timetable/tripId.js';
|
|
15
18
|
import { GtfsRouteId, GtfsRoutesMap } from './routes.js';
|
|
16
19
|
import { ServiceId, ServiceIds } from './services.js';
|
|
17
20
|
import { GtfsStopsMap } from './stops.js';
|
|
18
21
|
import { GtfsTime, toTime } from './time.js';
|
|
19
|
-
import { TransfersMap } from './transfers.js';
|
|
22
|
+
import { TransfersMap, TripContinuationsMap } from './transfers.js';
|
|
20
23
|
import { hashIds, parseCsv } from './utils.js';
|
|
21
24
|
|
|
22
|
-
export type
|
|
25
|
+
export type GtfsTripId = string;
|
|
23
26
|
|
|
24
|
-
export type
|
|
27
|
+
export type GtfsTripIdsMap = Map<GtfsTripId, GtfsRouteId>;
|
|
28
|
+
|
|
29
|
+
export type TripsMapping = Map<
|
|
30
|
+
GtfsTripId,
|
|
31
|
+
{ routeId: RouteId; tripRouteIndex: TripRouteIndex }
|
|
32
|
+
>;
|
|
25
33
|
|
|
26
34
|
type TripEntry = {
|
|
27
35
|
route_id: GtfsRouteId;
|
|
28
36
|
service_id: ServiceId;
|
|
29
|
-
trip_id:
|
|
37
|
+
trip_id: GtfsTripId;
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
export type GtfsPickupDropOffType =
|
|
@@ -37,7 +45,7 @@ export type GtfsPickupDropOffType =
|
|
|
37
45
|
| '3'; // Must coordinate with driver
|
|
38
46
|
|
|
39
47
|
type StopTimeEntry = {
|
|
40
|
-
trip_id:
|
|
48
|
+
trip_id: GtfsTripId;
|
|
41
49
|
arrival_time?: GtfsTime;
|
|
42
50
|
departure_time?: GtfsTime;
|
|
43
51
|
stop_id: SourceStopId;
|
|
@@ -55,6 +63,7 @@ type RouteBuilder = {
|
|
|
55
63
|
serviceRouteId: ServiceRouteId;
|
|
56
64
|
stops: StopId[];
|
|
57
65
|
trips: Array<{
|
|
66
|
+
gtfsTripId: GtfsTripId;
|
|
58
67
|
firstDeparture: number;
|
|
59
68
|
arrivalTimes: number[];
|
|
60
69
|
departureTimes: number[];
|
|
@@ -109,7 +118,9 @@ export const encodePickUpDropOffTypes = (
|
|
|
109
118
|
/**
|
|
110
119
|
* Sorts trips by departure time and creates optimized typed arrays
|
|
111
120
|
*/
|
|
112
|
-
const finalizeRouteFromBuilder = (
|
|
121
|
+
const finalizeRouteFromBuilder = (
|
|
122
|
+
builder: RouteBuilder,
|
|
123
|
+
): [SerializedRoute, GtfsTripId[]] => {
|
|
113
124
|
builder.trips.sort((a, b) => a.firstDeparture - b.firstDeparture);
|
|
114
125
|
|
|
115
126
|
const stopsCount = builder.stops.length;
|
|
@@ -119,11 +130,13 @@ const finalizeRouteFromBuilder = (builder: RouteBuilder): SerializedRoute => {
|
|
|
119
130
|
const allPickUpTypes: SerializedPickUpDropOffType[] = [];
|
|
120
131
|
const allDropOffTypes: SerializedPickUpDropOffType[] = [];
|
|
121
132
|
|
|
133
|
+
const gtfsTripIds = [];
|
|
122
134
|
for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
|
|
123
135
|
const trip = builder.trips[tripIndex];
|
|
124
136
|
if (!trip) {
|
|
125
137
|
throw new Error(`Missing trip data at index ${tripIndex}`);
|
|
126
138
|
}
|
|
139
|
+
gtfsTripIds.push(trip.gtfsTripId);
|
|
127
140
|
const baseIndex = tripIndex * stopsCount * 2;
|
|
128
141
|
|
|
129
142
|
for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
|
|
@@ -155,12 +168,15 @@ const finalizeRouteFromBuilder = (builder: RouteBuilder): SerializedRoute => {
|
|
|
155
168
|
allPickUpTypes,
|
|
156
169
|
allDropOffTypes,
|
|
157
170
|
);
|
|
158
|
-
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
serviceRouteId: builder.serviceRouteId,
|
|
174
|
+
stops: stopsArray,
|
|
175
|
+
stopTimes: stopTimesArray,
|
|
176
|
+
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
177
|
+
},
|
|
178
|
+
gtfsTripIds,
|
|
179
|
+
];
|
|
164
180
|
};
|
|
165
181
|
|
|
166
182
|
/**
|
|
@@ -175,8 +191,8 @@ export const parseTrips = async (
|
|
|
175
191
|
tripsStream: NodeJS.ReadableStream,
|
|
176
192
|
serviceIds: ServiceIds,
|
|
177
193
|
validGtfsRoutes: GtfsRoutesMap,
|
|
178
|
-
): Promise<
|
|
179
|
-
const trips:
|
|
194
|
+
): Promise<GtfsTripIdsMap> => {
|
|
195
|
+
const trips: GtfsTripIdsMap = new Map();
|
|
180
196
|
for await (const rawLine of parseCsv(tripsStream, ['stop_sequence'])) {
|
|
181
197
|
const line = rawLine as TripEntry;
|
|
182
198
|
if (!serviceIds.has(line.service_id)) {
|
|
@@ -193,16 +209,19 @@ export const parseTrips = async (
|
|
|
193
209
|
};
|
|
194
210
|
|
|
195
211
|
export const buildStopsAdjacencyStructure = (
|
|
212
|
+
tripsMapping: TripsMapping,
|
|
196
213
|
serviceRoutes: ServiceRoute[],
|
|
197
214
|
routes: Route[],
|
|
198
215
|
transfersMap: TransfersMap,
|
|
216
|
+
tripContinuationsMap: TripContinuationsMap,
|
|
199
217
|
nbStops: number,
|
|
200
218
|
activeStops: Set<StopId>,
|
|
201
219
|
): StopAdjacency[] => {
|
|
202
|
-
// TODO somehow works when it's a map
|
|
203
220
|
const stopsAdjacency = new Array<StopAdjacency>(nbStops);
|
|
204
221
|
for (let i = 0; i < nbStops; i++) {
|
|
205
|
-
stopsAdjacency[i] = {
|
|
222
|
+
stopsAdjacency[i] = {
|
|
223
|
+
routes: [],
|
|
224
|
+
};
|
|
206
225
|
}
|
|
207
226
|
for (let index = 0; index < routes.length; index++) {
|
|
208
227
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -229,12 +248,51 @@ export const buildStopsAdjacencyStructure = (
|
|
|
229
248
|
const transfer = transfers[i]!;
|
|
230
249
|
if (activeStops.has(stop) || activeStops.has(transfer.destination)) {
|
|
231
250
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
232
|
-
stopsAdjacency[stop]
|
|
251
|
+
const stopAdj = stopsAdjacency[stop]!;
|
|
252
|
+
if (!stopAdj.transfers) {
|
|
253
|
+
stopAdj.transfers = [];
|
|
254
|
+
}
|
|
255
|
+
stopAdj.transfers.push(transfer);
|
|
233
256
|
activeStops.add(transfer.destination);
|
|
234
257
|
activeStops.add(stop);
|
|
235
258
|
}
|
|
236
259
|
}
|
|
237
260
|
}
|
|
261
|
+
for (const [stop, tripContinuations] of tripContinuationsMap) {
|
|
262
|
+
for (let i = 0; i < tripContinuations.length; i++) {
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
264
|
+
const tripContinuation = tripContinuations[i]!;
|
|
265
|
+
if (
|
|
266
|
+
activeStops.has(stop) ||
|
|
267
|
+
activeStops.has(tripContinuation.hopOnStop)
|
|
268
|
+
) {
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
270
|
+
const stopAdj = stopsAdjacency[stop]!;
|
|
271
|
+
if (!stopAdj.tripContinuations) {
|
|
272
|
+
stopAdj.tripContinuations = new Map();
|
|
273
|
+
}
|
|
274
|
+
const originTrip = tripsMapping.get(tripContinuation.fromTrip);
|
|
275
|
+
const destinationTrip = tripsMapping.get(tripContinuation.toTrip);
|
|
276
|
+
if (destinationTrip === undefined || originTrip === undefined) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const tripBoarding = {
|
|
280
|
+
hopOnStop: tripContinuation.hopOnStop,
|
|
281
|
+
routeId: destinationTrip.routeId,
|
|
282
|
+
tripIndex: destinationTrip.tripRouteIndex,
|
|
283
|
+
};
|
|
284
|
+
const tripId = encode(originTrip.routeId, originTrip.tripRouteIndex);
|
|
285
|
+
const existingContinuations = stopAdj.tripContinuations.get(tripId);
|
|
286
|
+
if (existingContinuations) {
|
|
287
|
+
existingContinuations.push(tripBoarding);
|
|
288
|
+
} else {
|
|
289
|
+
stopAdj.tripContinuations.set(tripId, [tripBoarding]);
|
|
290
|
+
}
|
|
291
|
+
activeStops.add(tripContinuation.hopOnStop);
|
|
292
|
+
activeStops.add(stop);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
238
296
|
return stopsAdjacency;
|
|
239
297
|
};
|
|
240
298
|
|
|
@@ -250,16 +308,17 @@ export const buildStopsAdjacencyStructure = (
|
|
|
250
308
|
export const parseStopTimes = async (
|
|
251
309
|
stopTimesStream: NodeJS.ReadableStream,
|
|
252
310
|
stopsMap: GtfsStopsMap,
|
|
253
|
-
activeTripIds:
|
|
311
|
+
activeTripIds: GtfsTripIdsMap,
|
|
254
312
|
activeStopIds: Set<StopId>,
|
|
255
313
|
): Promise<{
|
|
256
314
|
routes: Route[];
|
|
257
315
|
serviceRoutesMap: Map<GtfsRouteId, ServiceRouteId>;
|
|
316
|
+
tripsMapping: TripsMapping;
|
|
258
317
|
}> => {
|
|
259
318
|
/**
|
|
260
319
|
* Adds a trip to the appropriate route builder
|
|
261
320
|
*/
|
|
262
|
-
const addTrip = (currentTripId:
|
|
321
|
+
const addTrip = (currentTripId: GtfsTripId) => {
|
|
263
322
|
const gtfsRouteId = activeTripIds.get(currentTripId);
|
|
264
323
|
|
|
265
324
|
if (!gtfsRouteId || stops.length === 0) {
|
|
@@ -304,6 +363,7 @@ export const parseStopTimes = async (
|
|
|
304
363
|
|
|
305
364
|
routeBuilder.trips.push({
|
|
306
365
|
firstDeparture,
|
|
366
|
+
gtfsTripId: currentTripId,
|
|
307
367
|
arrivalTimes: arrivalTimes,
|
|
308
368
|
departureTimes: departureTimes,
|
|
309
369
|
pickUpTypes: pickUpTypes,
|
|
@@ -330,7 +390,7 @@ export const parseStopTimes = async (
|
|
|
330
390
|
let departureTimes: number[] = [];
|
|
331
391
|
let pickUpTypes: SerializedPickUpDropOffType[] = [];
|
|
332
392
|
let dropOffTypes: SerializedPickUpDropOffType[] = [];
|
|
333
|
-
let currentTripId:
|
|
393
|
+
let currentTripId: GtfsTripId | undefined = undefined;
|
|
334
394
|
|
|
335
395
|
for await (const rawLine of parseCsv(stopTimesStream, ['stop_sequence'])) {
|
|
336
396
|
const line = rawLine as StopTimeEntry;
|
|
@@ -347,6 +407,9 @@ export const parseStopTimes = async (
|
|
|
347
407
|
continue;
|
|
348
408
|
}
|
|
349
409
|
if (line.pickup_type === '1' && line.drop_off_type === '1') {
|
|
410
|
+
// Warning: could potentially lead to issues if there is an in-seat transfer
|
|
411
|
+
// at this stop - it can be not boardable nor alightable but still useful for an in-seat transfer.
|
|
412
|
+
// This doesn't seem to happen in practice for now so keeping this condition to save memory.
|
|
350
413
|
continue;
|
|
351
414
|
}
|
|
352
415
|
if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
|
|
@@ -382,18 +445,30 @@ export const parseStopTimes = async (
|
|
|
382
445
|
}
|
|
383
446
|
|
|
384
447
|
const routesAdjacency: Route[] = [];
|
|
448
|
+
const tripsMapping = new Map<
|
|
449
|
+
GtfsTripId,
|
|
450
|
+
{ routeId: RouteId; tripRouteIndex: TripRouteIndex }
|
|
451
|
+
>();
|
|
385
452
|
for (const [, routeBuilder] of routeBuilders) {
|
|
386
|
-
const routeData = finalizeRouteFromBuilder(routeBuilder);
|
|
453
|
+
const [routeData, gtfsTripIds] = finalizeRouteFromBuilder(routeBuilder);
|
|
454
|
+
const routeId = routesAdjacency.length;
|
|
387
455
|
routesAdjacency.push(
|
|
388
456
|
new Route(
|
|
457
|
+
routeId,
|
|
389
458
|
routeData.stopTimes,
|
|
390
459
|
routeData.pickUpDropOffTypes,
|
|
391
460
|
routeData.stops,
|
|
392
461
|
routeData.serviceRouteId,
|
|
393
462
|
),
|
|
394
463
|
);
|
|
464
|
+
gtfsTripIds.forEach((tripId, index) => {
|
|
465
|
+
tripsMapping.set(tripId, {
|
|
466
|
+
routeId,
|
|
467
|
+
tripRouteIndex: index,
|
|
468
|
+
});
|
|
469
|
+
});
|
|
395
470
|
}
|
|
396
|
-
return { routes: routesAdjacency, serviceRoutesMap };
|
|
471
|
+
return { routes: routesAdjacency, serviceRoutesMap, tripsMapping };
|
|
397
472
|
};
|
|
398
473
|
|
|
399
474
|
const parsePickupDropOffType = (
|
package/src/router.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Query } from './routing/query.js';
|
|
|
3
3
|
import { Result } from './routing/result.js';
|
|
4
4
|
import type { Leg, Transfer, VehicleLeg } from './routing/route.js';
|
|
5
5
|
import { Route } from './routing/route.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { Arrival } from './routing/router.js';
|
|
7
7
|
import { Router } from './routing/router.js';
|
|
8
8
|
import type { LocationType, SourceStopId, StopId } from './stops/stops.js';
|
|
9
9
|
import type { Stop } from './stops/stops.js';
|
|
@@ -30,9 +30,9 @@ export {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export type {
|
|
33
|
+
Arrival,
|
|
33
34
|
Leg,
|
|
34
35
|
LocationType,
|
|
35
|
-
ReachingTime,
|
|
36
36
|
RouteType,
|
|
37
37
|
ServiceRouteInfo,
|
|
38
38
|
SourceStopId,
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { Timetable } from '../../router.js';
|
|
5
|
+
import { Stop, StopId } from '../../stops/stops.js';
|
|
6
|
+
import { StopsIndex } from '../../stops/stopsIndex.js';
|
|
7
|
+
import { Route } from '../../timetable/route.js';
|
|
8
|
+
import { Time } from '../../timetable/time.js';
|
|
9
|
+
import { ServiceRoute, StopAdjacency } from '../../timetable/timetable.js';
|
|
10
|
+
import { Plotter } from '../plotter.js';
|
|
11
|
+
import { Query } from '../query.js';
|
|
12
|
+
import { Result } from '../result.js';
|
|
13
|
+
import { RoutingEdge } from '../router.js';
|
|
14
|
+
|
|
15
|
+
describe('Plotter', () => {
|
|
16
|
+
const stop1: Stop = {
|
|
17
|
+
id: 0,
|
|
18
|
+
sourceStopId: 'stop1',
|
|
19
|
+
name: 'Lausanne',
|
|
20
|
+
children: [],
|
|
21
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const stop2: Stop = {
|
|
25
|
+
id: 1,
|
|
26
|
+
sourceStopId: 'stop2',
|
|
27
|
+
name: 'Fribourg',
|
|
28
|
+
children: [],
|
|
29
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const stopsAdjacency: StopAdjacency[] = [{ routes: [0] }, { routes: [0] }];
|
|
33
|
+
|
|
34
|
+
const routesAdjacency = [
|
|
35
|
+
Route.of({
|
|
36
|
+
id: 0,
|
|
37
|
+
serviceRouteId: 0,
|
|
38
|
+
trips: [
|
|
39
|
+
{
|
|
40
|
+
stops: [
|
|
41
|
+
{
|
|
42
|
+
id: 0,
|
|
43
|
+
arrivalTime: Time.fromString('08:00:00'),
|
|
44
|
+
departureTime: Time.fromString('08:05:00'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 1,
|
|
48
|
+
arrivalTime: Time.fromString('08:30:00'),
|
|
49
|
+
departureTime: Time.fromString('08:35:00'),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const routes: ServiceRoute[] = [
|
|
58
|
+
{
|
|
59
|
+
type: 'RAIL',
|
|
60
|
+
name: 'IC 1',
|
|
61
|
+
routes: [0],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const mockStopsIndex = new StopsIndex([stop1, stop2]);
|
|
66
|
+
const mockTimetable = new Timetable(stopsAdjacency, routesAdjacency, routes);
|
|
67
|
+
const mockQuery = new Query.Builder()
|
|
68
|
+
.from('stop1')
|
|
69
|
+
.to(new Set(['stop2']))
|
|
70
|
+
.departureTime(Time.fromHMS(8, 0, 0))
|
|
71
|
+
.build();
|
|
72
|
+
|
|
73
|
+
describe('plotDotGraph', () => {
|
|
74
|
+
it('should generate valid DOT graph structure', () => {
|
|
75
|
+
const result = new Result(
|
|
76
|
+
mockQuery,
|
|
77
|
+
{
|
|
78
|
+
earliestArrivals: new Map(),
|
|
79
|
+
graph: [],
|
|
80
|
+
destinations: [],
|
|
81
|
+
},
|
|
82
|
+
mockStopsIndex,
|
|
83
|
+
mockTimetable,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const plotter = new Plotter(result);
|
|
87
|
+
const dotGraph = plotter.plotDotGraph();
|
|
88
|
+
|
|
89
|
+
assert(dotGraph.includes('digraph RoutingGraph {'));
|
|
90
|
+
assert(dotGraph.includes('// Stations'));
|
|
91
|
+
assert(dotGraph.includes('// Edges'));
|
|
92
|
+
assert(dotGraph.endsWith('}'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should include station nodes', () => {
|
|
96
|
+
const graph: Map<StopId, RoutingEdge>[] = [
|
|
97
|
+
new Map([[0, { arrival: Time.fromHMS(8, 0, 0) }]]),
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const result = new Result(
|
|
101
|
+
mockQuery,
|
|
102
|
+
{
|
|
103
|
+
earliestArrivals: new Map(),
|
|
104
|
+
graph,
|
|
105
|
+
destinations: [0],
|
|
106
|
+
},
|
|
107
|
+
mockStopsIndex,
|
|
108
|
+
mockTimetable,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const plotter = new Plotter(result);
|
|
112
|
+
const dotGraph = plotter.plotDotGraph();
|
|
113
|
+
|
|
114
|
+
assert(dotGraph.includes('"s_0"'));
|
|
115
|
+
assert(dotGraph.includes('Lausanne'));
|
|
116
|
+
assert(dotGraph.includes('shape=box'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle empty graph gracefully', () => {
|
|
120
|
+
const result = new Result(
|
|
121
|
+
mockQuery,
|
|
122
|
+
{
|
|
123
|
+
earliestArrivals: new Map(),
|
|
124
|
+
graph: [],
|
|
125
|
+
destinations: [],
|
|
126
|
+
},
|
|
127
|
+
mockStopsIndex,
|
|
128
|
+
mockTimetable,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const plotter = new Plotter(result);
|
|
132
|
+
const dotGraph = plotter.plotDotGraph();
|
|
133
|
+
|
|
134
|
+
assert(dotGraph.includes('digraph RoutingGraph {'));
|
|
135
|
+
assert(dotGraph.endsWith('}'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should escape special characters', () => {
|
|
139
|
+
const specialStop: Stop = {
|
|
140
|
+
id: 2,
|
|
141
|
+
sourceStopId: 'test"stop\\with\nlines\rand\ttabs',
|
|
142
|
+
name: 'Station "Test"\nWith\\Special\rChars\tAndTabs',
|
|
143
|
+
lat: 0,
|
|
144
|
+
lon: 0,
|
|
145
|
+
children: [],
|
|
146
|
+
parent: undefined,
|
|
147
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const specialStopsIndex = new StopsIndex([stop1, stop2, specialStop]);
|
|
151
|
+
const graph: Map<StopId, RoutingEdge>[] = [
|
|
152
|
+
new Map([[2, { arrival: Time.fromHMS(8, 0, 0) }]]),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const result = new Result(
|
|
156
|
+
mockQuery,
|
|
157
|
+
{
|
|
158
|
+
earliestArrivals: new Map(),
|
|
159
|
+
graph,
|
|
160
|
+
destinations: [2],
|
|
161
|
+
},
|
|
162
|
+
specialStopsIndex,
|
|
163
|
+
mockTimetable,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const plotter = new Plotter(result);
|
|
167
|
+
const dotGraph = plotter.plotDotGraph();
|
|
168
|
+
|
|
169
|
+
// Check that special characters are properly escaped in the station label
|
|
170
|
+
assert(
|
|
171
|
+
dotGraph.includes(
|
|
172
|
+
'Station \\"Test\\"\\nWith\\\\Special\\rChars\\tAndTabs\\n2',
|
|
173
|
+
),
|
|
174
|
+
'Station name should have properly escaped special characters',
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should use correct colors', () => {
|
|
179
|
+
const graph: Map<StopId, RoutingEdge>[] = [
|
|
180
|
+
new Map([[0, { arrival: Time.fromHMS(8, 0, 0) }]]),
|
|
181
|
+
new Map([
|
|
182
|
+
[
|
|
183
|
+
1,
|
|
184
|
+
{
|
|
185
|
+
from: 0,
|
|
186
|
+
to: 1,
|
|
187
|
+
arrival: Time.fromHMS(8, 30, 0),
|
|
188
|
+
routeId: 0,
|
|
189
|
+
tripIndex: 0,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
]),
|
|
193
|
+
new Map([
|
|
194
|
+
[
|
|
195
|
+
1,
|
|
196
|
+
{
|
|
197
|
+
from: 0,
|
|
198
|
+
to: 1,
|
|
199
|
+
arrival: Time.fromHMS(8, 45, 0),
|
|
200
|
+
type: 'WALKING',
|
|
201
|
+
minTransferTime: Time.fromHMS(0, 5, 0),
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
]),
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const result = new Result(
|
|
208
|
+
mockQuery,
|
|
209
|
+
{
|
|
210
|
+
earliestArrivals: new Map(),
|
|
211
|
+
graph,
|
|
212
|
+
destinations: [1],
|
|
213
|
+
},
|
|
214
|
+
mockStopsIndex,
|
|
215
|
+
mockTimetable,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const plotter = new Plotter(result);
|
|
219
|
+
const dotGraph = plotter.plotDotGraph();
|
|
220
|
+
assert(
|
|
221
|
+
dotGraph.includes('color="#60a5fa"'),
|
|
222
|
+
'Round 1 should use blue color (#60a5fa)',
|
|
223
|
+
);
|
|
224
|
+
assert(
|
|
225
|
+
dotGraph.includes('color="#ff9800"'),
|
|
226
|
+
'Round 2 should use orange color (#ff9800)',
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|