minotor 3.0.2 → 5.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/CHANGELOG.md +9 -3
- package/README.md +1 -0
- package/dist/cli.mjs +294 -307
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/trips.d.ts +12 -6
- package/dist/parser.cjs.js +290 -302
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +290 -302
- 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/route.d.ts +3 -3
- package/dist/timetable/io.d.ts +5 -4
- package/dist/timetable/proto/timetable.d.ts +7 -16
- package/dist/timetable/route.d.ts +7 -5
- package/dist/timetable/timetable.d.ts +7 -5
- package/package.json +1 -1
- package/src/__e2e__/timetable/timetable.bin +2 -2
- package/src/cli/repl.ts +0 -1
- package/src/gtfs/__tests__/parser.test.ts +2 -2
- package/src/gtfs/__tests__/routes.test.ts +3 -0
- package/src/gtfs/__tests__/trips.test.ts +123 -166
- package/src/gtfs/parser.ts +50 -9
- package/src/gtfs/routes.ts +1 -0
- package/src/gtfs/trips.ts +195 -112
- package/src/router.ts +2 -2
- package/src/routing/__tests__/route.test.ts +3 -3
- package/src/routing/__tests__/router.test.ts +186 -203
- package/src/routing/route.ts +3 -3
- package/src/routing/router.ts +1 -1
- package/src/timetable/__tests__/io.test.ts +52 -64
- package/src/timetable/__tests__/route.test.ts +25 -17
- package/src/timetable/__tests__/timetable.test.ts +16 -24
- package/src/timetable/io.ts +20 -19
- package/src/timetable/proto/timetable.proto +7 -9
- package/src/timetable/proto/timetable.ts +80 -202
- package/src/timetable/route.ts +27 -13
- package/src/timetable/timetable.ts +20 -16
package/src/gtfs/parser.ts
CHANGED
|
@@ -51,6 +51,7 @@ export class GtfsParser {
|
|
|
51
51
|
async parse(
|
|
52
52
|
date: Date,
|
|
53
53
|
): Promise<{ timetable: Timetable; stopsIndex: StopsIndex }> {
|
|
54
|
+
log.setLevel('INFO');
|
|
54
55
|
const zip = new StreamZip.async({ file: this.path });
|
|
55
56
|
const entries = await zip.entries();
|
|
56
57
|
const datetime = DateTime.fromJSDate(date);
|
|
@@ -59,50 +60,75 @@ export class GtfsParser {
|
|
|
59
60
|
const validStopIds = new Set<StopId>();
|
|
60
61
|
|
|
61
62
|
log.info(`Parsing ${STOPS_FILE}`);
|
|
63
|
+
const stopsStart = performance.now();
|
|
62
64
|
const stopsStream = await zip.stream(STOPS_FILE);
|
|
63
65
|
const parsedStops = await parseStops(
|
|
64
66
|
stopsStream,
|
|
65
67
|
this.profile.platformParser,
|
|
66
68
|
);
|
|
67
|
-
|
|
69
|
+
const stopsEnd = performance.now();
|
|
70
|
+
log.info(
|
|
71
|
+
`${parsedStops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`,
|
|
72
|
+
);
|
|
68
73
|
|
|
69
74
|
if (entries[CALENDAR_FILE]) {
|
|
70
75
|
log.info(`Parsing ${CALENDAR_FILE}`);
|
|
76
|
+
const calendarStart = performance.now();
|
|
71
77
|
const calendarStream = await zip.stream(CALENDAR_FILE);
|
|
72
78
|
await parseCalendar(calendarStream, validServiceIds, datetime);
|
|
73
|
-
|
|
79
|
+
const calendarEnd = performance.now();
|
|
80
|
+
log.info(
|
|
81
|
+
`${validServiceIds.size} valid services. (${(calendarEnd - calendarStart).toFixed(2)}ms)`,
|
|
82
|
+
);
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
if (entries[CALENDAR_DATES_FILE]) {
|
|
77
86
|
log.info(`Parsing ${CALENDAR_DATES_FILE}`);
|
|
87
|
+
const calendarDatesStart = performance.now();
|
|
78
88
|
const calendarDatesStream = await zip.stream(CALENDAR_DATES_FILE);
|
|
79
89
|
await parseCalendarDates(calendarDatesStream, validServiceIds, datetime);
|
|
80
|
-
|
|
90
|
+
const calendarDatesEnd = performance.now();
|
|
91
|
+
log.info(
|
|
92
|
+
`${validServiceIds.size} valid services. (${(calendarDatesEnd - calendarDatesStart).toFixed(2)}ms)`,
|
|
93
|
+
);
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
log.info(`Parsing ${ROUTES_FILE}`);
|
|
97
|
+
const routesStart = performance.now();
|
|
84
98
|
const routesStream = await zip.stream(ROUTES_FILE);
|
|
85
99
|
const validGtfsRoutes = await parseRoutes(routesStream, this.profile);
|
|
86
|
-
|
|
100
|
+
const routesEnd = performance.now();
|
|
101
|
+
log.info(
|
|
102
|
+
`${validGtfsRoutes.size} valid GTFS routes. (${(routesEnd - routesStart).toFixed(2)}ms)`,
|
|
103
|
+
);
|
|
87
104
|
|
|
88
105
|
log.info(`Parsing ${TRIPS_FILE}`);
|
|
106
|
+
const tripsStart = performance.now();
|
|
89
107
|
const tripsStream = await zip.stream(TRIPS_FILE);
|
|
90
108
|
const trips = await parseTrips(
|
|
91
109
|
tripsStream,
|
|
92
110
|
validServiceIds,
|
|
93
111
|
validGtfsRoutes,
|
|
94
112
|
);
|
|
95
|
-
|
|
113
|
+
const tripsEnd = performance.now();
|
|
114
|
+
log.info(
|
|
115
|
+
`${trips.size} valid trips. (${(tripsEnd - tripsStart).toFixed(2)}ms)`,
|
|
116
|
+
);
|
|
96
117
|
|
|
97
118
|
let transfers = new Map() as TransfersMap;
|
|
98
119
|
if (entries[TRANSFERS_FILE]) {
|
|
99
120
|
log.info(`Parsing ${TRANSFERS_FILE}`);
|
|
121
|
+
const transfersStart = performance.now();
|
|
100
122
|
const transfersStream = await zip.stream(TRANSFERS_FILE);
|
|
101
123
|
transfers = await parseTransfers(transfersStream, parsedStops);
|
|
102
|
-
|
|
124
|
+
const transfersEnd = performance.now();
|
|
125
|
+
log.info(
|
|
126
|
+
`${transfers.size} valid transfers. (${(transfersEnd - transfersStart).toFixed(2)}ms)`,
|
|
127
|
+
);
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
log.info(`Parsing ${STOP_TIMES_FILE}`);
|
|
131
|
+
const stopTimesStart = performance.now();
|
|
106
132
|
const stopTimesStream = await zip.stream(STOP_TIMES_FILE);
|
|
107
133
|
const routesAdjacency = await parseStopTimes(
|
|
108
134
|
stopTimesStream,
|
|
@@ -112,15 +138,21 @@ export class GtfsParser {
|
|
|
112
138
|
);
|
|
113
139
|
const stopsAdjacency = buildStopsAdjacencyStructure(
|
|
114
140
|
validStopIds,
|
|
141
|
+
validGtfsRoutes,
|
|
115
142
|
routesAdjacency,
|
|
116
143
|
transfers,
|
|
117
144
|
);
|
|
118
|
-
|
|
145
|
+
const stopTimesEnd = performance.now();
|
|
146
|
+
log.info(
|
|
147
|
+
`${routesAdjacency.length} valid unique routes. (${(stopTimesEnd - stopTimesStart).toFixed(2)}ms)`,
|
|
148
|
+
);
|
|
119
149
|
|
|
120
150
|
log.info(`Removing unused stops.`);
|
|
151
|
+
const indexStopsStart = performance.now();
|
|
121
152
|
const stops = indexStops(parsedStops, validStopIds);
|
|
153
|
+
const indexStopsEnd = performance.now();
|
|
122
154
|
log.info(
|
|
123
|
-
`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused
|
|
155
|
+
`${stops.size} used stop stops, ${parsedStops.size - stops.size} unused. (${(indexStopsEnd - indexStopsStart).toFixed(2)}ms)`,
|
|
124
156
|
);
|
|
125
157
|
|
|
126
158
|
await zip.close();
|
|
@@ -132,7 +164,12 @@ export class GtfsParser {
|
|
|
132
164
|
);
|
|
133
165
|
|
|
134
166
|
log.info(`Building stops index.`);
|
|
167
|
+
const stopsIndexStart = performance.now();
|
|
135
168
|
const stopsIndex = new StopsIndex(stops);
|
|
169
|
+
const stopsIndexEnd = performance.now();
|
|
170
|
+
log.info(
|
|
171
|
+
`Stops index built. (${(stopsIndexEnd - stopsIndexStart).toFixed(2)}ms)`,
|
|
172
|
+
);
|
|
136
173
|
|
|
137
174
|
log.info('Parsing complete.');
|
|
138
175
|
return { timetable, stopsIndex };
|
|
@@ -149,12 +186,16 @@ export class GtfsParser {
|
|
|
149
186
|
const zip = new StreamZip.async({ file: this.path });
|
|
150
187
|
|
|
151
188
|
log.info(`Parsing ${STOPS_FILE}`);
|
|
189
|
+
const stopsStart = performance.now();
|
|
152
190
|
const stopsStream = await zip.stream(STOPS_FILE);
|
|
153
191
|
const stops = indexStops(
|
|
154
192
|
await parseStops(stopsStream, this.profile.platformParser),
|
|
155
193
|
);
|
|
194
|
+
const stopsEnd = performance.now();
|
|
156
195
|
|
|
157
|
-
log.info(
|
|
196
|
+
log.info(
|
|
197
|
+
`${stops.size} parsed stops. (${(stopsEnd - stopsStart).toFixed(2)}ms)`,
|
|
198
|
+
);
|
|
158
199
|
|
|
159
200
|
await zip.close();
|
|
160
201
|
|
package/src/gtfs/routes.ts
CHANGED
package/src/gtfs/trips.ts
CHANGED
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
NOT_AVAILABLE,
|
|
7
7
|
REGULAR,
|
|
8
8
|
Route,
|
|
9
|
-
RouteId,
|
|
10
9
|
} from '../timetable/route.js';
|
|
11
10
|
import {
|
|
12
|
-
RoutesAdjacency,
|
|
13
11
|
ServiceRouteId,
|
|
14
12
|
ServiceRoutesMap,
|
|
15
13
|
StopsAdjacency,
|
|
@@ -32,10 +30,10 @@ type TripEntry = {
|
|
|
32
30
|
|
|
33
31
|
export type GtfsPickupDropOffType =
|
|
34
32
|
| '' // Not specified
|
|
35
|
-
| 0 // Regularly scheduled
|
|
36
|
-
| 1 // Not available
|
|
37
|
-
| 2 // Must phone agency
|
|
38
|
-
| 3; // Must coordinate with driver
|
|
33
|
+
| '0' // Regularly scheduled
|
|
34
|
+
| '1' // Not available
|
|
35
|
+
| '2' // Must phone agency
|
|
36
|
+
| '3'; // Must coordinate with driver
|
|
39
37
|
|
|
40
38
|
type StopTimeEntry = {
|
|
41
39
|
trip_id: TripId;
|
|
@@ -49,18 +47,133 @@ type StopTimeEntry = {
|
|
|
49
47
|
|
|
50
48
|
export type SerializedPickUpDropOffType = 0 | 1 | 2 | 3;
|
|
51
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Intermediate data structure for building routes during parsing
|
|
52
|
+
*/
|
|
53
|
+
type RouteBuilder = {
|
|
54
|
+
serviceRouteId: ServiceRouteId;
|
|
55
|
+
stops: StopId[];
|
|
56
|
+
trips: Array<{
|
|
57
|
+
firstDeparture: number;
|
|
58
|
+
arrivalTimes: number[];
|
|
59
|
+
departureTimes: number[];
|
|
60
|
+
pickUpTypes: SerializedPickUpDropOffType[];
|
|
61
|
+
dropOffTypes: SerializedPickUpDropOffType[];
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Encodes pickup/drop-off types into a Uint8Array using 2 bits per value.
|
|
67
|
+
* Layout per byte: [drop_off_1][pickup_1][drop_off_0][pickup_0] for stops 0 and 1
|
|
68
|
+
*/
|
|
69
|
+
export const encodePickUpDropOffTypes = (
|
|
70
|
+
pickUpTypes: SerializedPickUpDropOffType[],
|
|
71
|
+
dropOffTypes: SerializedPickUpDropOffType[],
|
|
72
|
+
): Uint8Array => {
|
|
73
|
+
const stopsCount = pickUpTypes.length;
|
|
74
|
+
// Each byte stores 2 pickup/drop-off pairs (4 bits each)
|
|
75
|
+
const arraySize = Math.ceil(stopsCount / 2);
|
|
76
|
+
const encoded = new Uint8Array(arraySize);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < stopsCount; i++) {
|
|
79
|
+
const byteIndex = Math.floor(i / 2);
|
|
80
|
+
const isSecondPair = i % 2 === 1;
|
|
81
|
+
const dropOffType = dropOffTypes[i];
|
|
82
|
+
const pickUpType = pickUpTypes[i];
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
dropOffType !== undefined &&
|
|
86
|
+
pickUpType !== undefined &&
|
|
87
|
+
byteIndex < encoded.length
|
|
88
|
+
) {
|
|
89
|
+
if (isSecondPair) {
|
|
90
|
+
// Second pair: upper 4 bits
|
|
91
|
+
const currentByte = encoded[byteIndex];
|
|
92
|
+
if (currentByte !== undefined) {
|
|
93
|
+
encoded[byteIndex] =
|
|
94
|
+
currentByte | (dropOffType << 4) | (pickUpType << 6);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// First pair: lower 4 bits
|
|
98
|
+
const currentByte = encoded[byteIndex];
|
|
99
|
+
if (currentByte !== undefined) {
|
|
100
|
+
encoded[byteIndex] = currentByte | dropOffType | (pickUpType << 2);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return encoded;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sorts trips by departure time and creates optimized typed arrays
|
|
110
|
+
*/
|
|
111
|
+
const finalizeRouteFromBuilder = (builder: RouteBuilder): SerializedRoute => {
|
|
112
|
+
builder.trips.sort((a, b) => a.firstDeparture - b.firstDeparture);
|
|
113
|
+
|
|
114
|
+
const stopsCount = builder.stops.length;
|
|
115
|
+
const tripsCount = builder.trips.length;
|
|
116
|
+
const stopsArray = new Uint32Array(builder.stops);
|
|
117
|
+
const stopTimesArray = new Uint16Array(stopsCount * tripsCount * 2);
|
|
118
|
+
const allPickUpTypes: SerializedPickUpDropOffType[] = [];
|
|
119
|
+
const allDropOffTypes: SerializedPickUpDropOffType[] = [];
|
|
120
|
+
|
|
121
|
+
for (let tripIndex = 0; tripIndex < tripsCount; tripIndex++) {
|
|
122
|
+
const trip = builder.trips[tripIndex];
|
|
123
|
+
if (!trip) {
|
|
124
|
+
throw new Error(`Missing trip data at index ${tripIndex}`);
|
|
125
|
+
}
|
|
126
|
+
const baseIndex = tripIndex * stopsCount * 2;
|
|
127
|
+
|
|
128
|
+
for (let stopIndex = 0; stopIndex < stopsCount; stopIndex++) {
|
|
129
|
+
const timeIndex = baseIndex + stopIndex * 2;
|
|
130
|
+
const arrivalTime = trip.arrivalTimes[stopIndex];
|
|
131
|
+
const departureTime = trip.departureTimes[stopIndex];
|
|
132
|
+
const pickUpType = trip.pickUpTypes[stopIndex];
|
|
133
|
+
const dropOffType = trip.dropOffTypes[stopIndex];
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
arrivalTime === undefined ||
|
|
137
|
+
departureTime === undefined ||
|
|
138
|
+
pickUpType === undefined ||
|
|
139
|
+
dropOffType === undefined
|
|
140
|
+
) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Missing trip data for trip ${tripIndex} at stop ${stopIndex}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stopTimesArray[timeIndex] = arrivalTime;
|
|
147
|
+
stopTimesArray[timeIndex + 1] = departureTime;
|
|
148
|
+
allDropOffTypes.push(dropOffType);
|
|
149
|
+
allPickUpTypes.push(pickUpType);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Use 2-bit encoding for pickup/drop-off types
|
|
153
|
+
const pickUpDropOffTypesArray = encodePickUpDropOffTypes(
|
|
154
|
+
allPickUpTypes,
|
|
155
|
+
allDropOffTypes,
|
|
156
|
+
);
|
|
157
|
+
return {
|
|
158
|
+
serviceRouteId: builder.serviceRouteId,
|
|
159
|
+
stops: stopsArray,
|
|
160
|
+
stopTimes: stopTimesArray,
|
|
161
|
+
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
52
165
|
/**
|
|
53
166
|
* Parses the trips.txt file from a GTFS feed
|
|
54
167
|
*
|
|
55
168
|
* @param tripsStream The readable stream containing the trips data.
|
|
56
169
|
* @param serviceIds A mapping of service IDs to corresponding route IDs.
|
|
57
|
-
* @param
|
|
170
|
+
* @param serviceRoutes A mapping of route IDs to route details.
|
|
58
171
|
* @returns A mapping of trip IDs to corresponding route IDs.
|
|
59
172
|
*/
|
|
60
173
|
export const parseTrips = async (
|
|
61
174
|
tripsStream: NodeJS.ReadableStream,
|
|
62
175
|
serviceIds: ServiceIds,
|
|
63
|
-
|
|
176
|
+
serviceRoutes: ServiceRoutesMap,
|
|
64
177
|
): Promise<TripIdsMap> => {
|
|
65
178
|
const trips: TripIdsMap = new Map();
|
|
66
179
|
for await (const rawLine of parseCsv(tripsStream, ['stop_sequence'])) {
|
|
@@ -69,7 +182,7 @@ export const parseTrips = async (
|
|
|
69
182
|
// The trip doesn't correspond to an active service
|
|
70
183
|
continue;
|
|
71
184
|
}
|
|
72
|
-
if (!
|
|
185
|
+
if (!serviceRoutes.get(line.route_id)) {
|
|
73
186
|
// The trip doesn't correspond to a supported route
|
|
74
187
|
continue;
|
|
75
188
|
}
|
|
@@ -80,23 +193,27 @@ export const parseTrips = async (
|
|
|
80
193
|
|
|
81
194
|
export const buildStopsAdjacencyStructure = (
|
|
82
195
|
validStops: Set<StopId>,
|
|
83
|
-
|
|
196
|
+
serviceRoutes: ServiceRoutesMap,
|
|
197
|
+
routes: Route[],
|
|
84
198
|
transfersMap: TransfersMap,
|
|
85
199
|
): StopsAdjacency => {
|
|
86
200
|
const stopsAdjacency: StopsAdjacency = new Map();
|
|
87
|
-
|
|
88
|
-
const route = routes.get(routeId);
|
|
89
|
-
if (!route) {
|
|
90
|
-
throw new Error(`Route ${routeId} not found`);
|
|
91
|
-
}
|
|
201
|
+
routes.forEach((route, index) => {
|
|
92
202
|
for (const stop of route.stopsIterator()) {
|
|
93
203
|
if (!stopsAdjacency.get(stop) && validStops.has(stop)) {
|
|
94
204
|
stopsAdjacency.set(stop, { routes: [], transfers: [] });
|
|
95
205
|
}
|
|
96
206
|
|
|
97
|
-
stopsAdjacency.get(stop)?.routes.push(
|
|
207
|
+
stopsAdjacency.get(stop)?.routes.push(index);
|
|
98
208
|
}
|
|
99
|
-
|
|
209
|
+
const serviceRoute = serviceRoutes.get(route.serviceRoute());
|
|
210
|
+
if (!serviceRoute) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Service route ${route.serviceRoute()} not found for route ${index}.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
serviceRoute.routes.push(index);
|
|
216
|
+
});
|
|
100
217
|
for (const [stop, transfers] of transfersMap) {
|
|
101
218
|
const s = stopsAdjacency.get(stop);
|
|
102
219
|
if (s) {
|
|
@@ -124,13 +241,14 @@ export const parseStopTimes = async (
|
|
|
124
241
|
stopsMap: ParsedStopsMap,
|
|
125
242
|
validTripIds: TripIdsMap,
|
|
126
243
|
validStopIds: Set<StopId>,
|
|
127
|
-
): Promise<
|
|
244
|
+
): Promise<Route[]> => {
|
|
128
245
|
/**
|
|
129
|
-
*
|
|
246
|
+
* Adds a trip to the appropriate route builder
|
|
130
247
|
*/
|
|
131
248
|
const addTrip = (currentTripId: TripId) => {
|
|
132
249
|
const gtfsRouteId = validTripIds.get(currentTripId);
|
|
133
|
-
|
|
250
|
+
|
|
251
|
+
if (!gtfsRouteId || stops.length === 0) {
|
|
134
252
|
stops = [];
|
|
135
253
|
arrivalTimes = [];
|
|
136
254
|
departureTimes = [];
|
|
@@ -138,90 +256,41 @@ export const parseStopTimes = async (
|
|
|
138
256
|
dropOffTypes = [];
|
|
139
257
|
return;
|
|
140
258
|
}
|
|
259
|
+
|
|
141
260
|
const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
|
|
142
261
|
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
pickUpDropOffTypesArray[i * 2] = pickUpTypes[i]!;
|
|
158
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
159
|
-
pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i]!;
|
|
160
|
-
}
|
|
161
|
-
route = {
|
|
262
|
+
const firstDeparture = departureTimes[0];
|
|
263
|
+
if (firstDeparture === undefined) {
|
|
264
|
+
console.warn(`Empty trip ${currentTripId}`);
|
|
265
|
+
stops = [];
|
|
266
|
+
arrivalTimes = [];
|
|
267
|
+
departureTimes = [];
|
|
268
|
+
pickUpTypes = [];
|
|
269
|
+
dropOffTypes = [];
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let routeBuilder = routeBuilders.get(routeId);
|
|
274
|
+
if (!routeBuilder) {
|
|
275
|
+
routeBuilder = {
|
|
162
276
|
serviceRouteId: gtfsRouteId,
|
|
163
|
-
stops:
|
|
164
|
-
|
|
165
|
-
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
277
|
+
stops: [...stops],
|
|
278
|
+
trips: [],
|
|
166
279
|
};
|
|
167
|
-
|
|
280
|
+
routeBuilders.set(routeId, routeBuilder);
|
|
168
281
|
for (const stop of stops) {
|
|
169
282
|
validStopIds.add(stop);
|
|
170
283
|
}
|
|
171
|
-
}
|
|
172
|
-
const tripFirstStopDeparture = departureTimes[0];
|
|
173
|
-
if (tripFirstStopDeparture === undefined) {
|
|
174
|
-
throw new Error(`Empty trip ${currentTripId}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Find the correct position to insert the new trip
|
|
178
|
-
const stopsCount = stops.length;
|
|
179
|
-
let insertPosition = 0;
|
|
180
|
-
const existingTripsCount = route.stopTimes.length / (stopsCount * 2);
|
|
181
|
-
|
|
182
|
-
for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) {
|
|
183
|
-
const currentDeparture =
|
|
184
|
-
route.stopTimes[tripIndex * stopsCount * 2 + 1];
|
|
185
|
-
if (currentDeparture && tripFirstStopDeparture > currentDeparture) {
|
|
186
|
-
insertPosition = (tripIndex + 1) * stopsCount;
|
|
187
|
-
} else {
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
284
|
+
}
|
|
191
285
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
286
|
+
routeBuilder.trips.push({
|
|
287
|
+
firstDeparture,
|
|
288
|
+
arrivalTimes: [...arrivalTimes],
|
|
289
|
+
departureTimes: [...departureTimes],
|
|
290
|
+
pickUpTypes: [...pickUpTypes],
|
|
291
|
+
dropOffTypes: [...dropOffTypes],
|
|
292
|
+
});
|
|
196
293
|
|
|
197
|
-
newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
|
|
198
|
-
newPickUpDropOffTypes.set(
|
|
199
|
-
route.pickUpDropOffTypes.slice(0, insertPosition * 2),
|
|
200
|
-
0,
|
|
201
|
-
);
|
|
202
|
-
for (let i = 0; i < stopsCount; i++) {
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
204
|
-
newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i]!;
|
|
205
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
206
|
-
newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i]!;
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
208
|
-
newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i]!;
|
|
209
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
210
|
-
newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i]!;
|
|
211
|
-
}
|
|
212
|
-
const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2);
|
|
213
|
-
newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2);
|
|
214
|
-
const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice(
|
|
215
|
-
insertPosition * 2,
|
|
216
|
-
);
|
|
217
|
-
newPickUpDropOffTypes.set(
|
|
218
|
-
afterInsertionTypesSlice,
|
|
219
|
-
(insertPosition + stopsCount) * 2,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
route.stopTimes = newStopTimes;
|
|
223
|
-
route.pickUpDropOffTypes = newPickUpDropOffTypes;
|
|
224
|
-
}
|
|
225
294
|
stops = [];
|
|
226
295
|
arrivalTimes = [];
|
|
227
296
|
departureTimes = [];
|
|
@@ -229,7 +298,8 @@ export const parseStopTimes = async (
|
|
|
229
298
|
dropOffTypes = [];
|
|
230
299
|
};
|
|
231
300
|
|
|
232
|
-
|
|
301
|
+
type BuilderRouteId = string;
|
|
302
|
+
const routeBuilders: Map<BuilderRouteId, RouteBuilder> = new Map();
|
|
233
303
|
|
|
234
304
|
let previousSeq = 0;
|
|
235
305
|
let stops: StopId[] = [];
|
|
@@ -242,7 +312,9 @@ export const parseStopTimes = async (
|
|
|
242
312
|
for await (const rawLine of parseCsv(stopTimesStream, ['stop_sequence'])) {
|
|
243
313
|
const line = rawLine as StopTimeEntry;
|
|
244
314
|
if (line.trip_id === currentTripId && line.stop_sequence <= previousSeq) {
|
|
245
|
-
console.warn(
|
|
315
|
+
console.warn(
|
|
316
|
+
`Stop sequences not increasing for trip ${line.trip_id}: ${line.stop_sequence} > ${previousSeq}.`,
|
|
317
|
+
);
|
|
246
318
|
continue;
|
|
247
319
|
}
|
|
248
320
|
if (!line.arrival_time && !line.departure_time) {
|
|
@@ -251,21 +323,32 @@ export const parseStopTimes = async (
|
|
|
251
323
|
);
|
|
252
324
|
continue;
|
|
253
325
|
}
|
|
254
|
-
if (line.pickup_type === 1 && line.drop_off_type === 1) {
|
|
326
|
+
if (line.pickup_type === '1' && line.drop_off_type === '1') {
|
|
255
327
|
continue;
|
|
256
328
|
}
|
|
257
329
|
if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
|
|
258
330
|
addTrip(currentTripId);
|
|
259
331
|
}
|
|
260
332
|
currentTripId = line.trip_id;
|
|
261
|
-
|
|
262
|
-
|
|
333
|
+
|
|
334
|
+
const stopData = stopsMap.get(line.stop_id);
|
|
335
|
+
if (!stopData) {
|
|
336
|
+
console.warn(`Unknown stop ID: ${line.stop_id}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
stops.push(stopData.id);
|
|
340
|
+
|
|
263
341
|
const departure = line.departure_time ?? line.arrival_time;
|
|
264
342
|
const arrival = line.arrival_time ?? line.departure_time;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
343
|
+
|
|
344
|
+
if (!arrival || !departure) {
|
|
345
|
+
console.warn(
|
|
346
|
+
`Missing time data for ${line.trip_id} at stop ${line.stop_id}`,
|
|
347
|
+
);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
arrivalTimes.push(toTime(arrival).toMinutes());
|
|
351
|
+
departureTimes.push(toTime(departure).toMinutes());
|
|
269
352
|
pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
|
|
270
353
|
dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
|
|
271
354
|
|
|
@@ -275,10 +358,10 @@ export const parseStopTimes = async (
|
|
|
275
358
|
addTrip(currentTripId);
|
|
276
359
|
}
|
|
277
360
|
|
|
278
|
-
const routesAdjacency:
|
|
279
|
-
for (const [
|
|
280
|
-
|
|
281
|
-
|
|
361
|
+
const routesAdjacency: Route[] = [];
|
|
362
|
+
for (const [, routeBuilder] of routeBuilders) {
|
|
363
|
+
const routeData = finalizeRouteFromBuilder(routeBuilder);
|
|
364
|
+
routesAdjacency.push(
|
|
282
365
|
new Route(
|
|
283
366
|
routeData.stopTimes,
|
|
284
367
|
routeData.pickUpDropOffTypes,
|
|
@@ -296,13 +379,13 @@ const parsePickupDropOffType = (
|
|
|
296
379
|
switch (gtfsType) {
|
|
297
380
|
default:
|
|
298
381
|
return REGULAR;
|
|
299
|
-
case 0:
|
|
382
|
+
case '0':
|
|
300
383
|
return REGULAR;
|
|
301
|
-
case 1:
|
|
384
|
+
case '1':
|
|
302
385
|
return NOT_AVAILABLE;
|
|
303
|
-
case 2:
|
|
386
|
+
case '2':
|
|
304
387
|
return MUST_PHONE_AGENCY;
|
|
305
|
-
case 3:
|
|
388
|
+
case '3':
|
|
306
389
|
return MUST_COORDINATE_WITH_DRIVER;
|
|
307
390
|
}
|
|
308
391
|
};
|
package/src/router.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Duration } from './timetable/duration.js';
|
|
|
12
12
|
import { Time } from './timetable/time.js';
|
|
13
13
|
import type {
|
|
14
14
|
RouteType,
|
|
15
|
-
|
|
15
|
+
ServiceRouteInfo,
|
|
16
16
|
TransferType,
|
|
17
17
|
} from './timetable/timetable.js';
|
|
18
18
|
import { Timetable } from './timetable/timetable.js';
|
|
@@ -34,7 +34,7 @@ export type {
|
|
|
34
34
|
LocationType,
|
|
35
35
|
ReachingTime,
|
|
36
36
|
RouteType,
|
|
37
|
-
|
|
37
|
+
ServiceRouteInfo,
|
|
38
38
|
SourceStopId,
|
|
39
39
|
Stop,
|
|
40
40
|
StopId,
|
|
@@ -4,7 +4,7 @@ import { describe, it } from 'node:test';
|
|
|
4
4
|
import { Stop } from '../../stops/stops.js';
|
|
5
5
|
import { Duration } from '../../timetable/duration.js';
|
|
6
6
|
import { Time } from '../../timetable/time.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ServiceRouteInfo, TransferType } from '../../timetable/timetable.js';
|
|
8
8
|
import { Route } from '../route.js';
|
|
9
9
|
|
|
10
10
|
describe('Route', () => {
|
|
@@ -40,12 +40,12 @@ describe('Route', () => {
|
|
|
40
40
|
children: [],
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
const serviceRoute:
|
|
43
|
+
const serviceRoute: ServiceRouteInfo = {
|
|
44
44
|
type: 'BUS',
|
|
45
45
|
name: 'Route 1',
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
const serviceRoute2:
|
|
48
|
+
const serviceRoute2: ServiceRouteInfo = {
|
|
49
49
|
type: 'RAIL',
|
|
50
50
|
name: 'Route 2',
|
|
51
51
|
};
|