minotor 1.0.7 → 2.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 +3 -2
- package/dist/cli.mjs +604 -531
- package/dist/cli.mjs.map +1 -1
- package/dist/gtfs/stops.d.ts +19 -5
- package/dist/gtfs/transfers.d.ts +5 -4
- package/dist/gtfs/trips.d.ts +7 -5
- package/dist/gtfs/utils.d.ts +7 -8
- package/dist/parser.cjs.js +569 -501
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +569 -501
- 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 +3 -3
- 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__/route.test.d.ts +1 -0
- package/dist/routing/query.d.ts +7 -7
- package/dist/routing/result.d.ts +3 -3
- package/dist/routing/route.d.ts +1 -0
- package/dist/stops/proto/stops.d.ts +5 -4
- package/dist/stops/stops.d.ts +10 -1
- package/dist/stops/stopsIndex.d.ts +21 -4
- package/dist/timetable/proto/timetable.d.ts +21 -18
- package/dist/timetable/timetable.d.ts +38 -14
- package/package.json +4 -3
- package/src/cli/repl.ts +13 -10
- package/src/gtfs/__tests__/parser.test.ts +50 -579
- package/src/gtfs/__tests__/stops.test.ts +181 -112
- package/src/gtfs/__tests__/transfers.test.ts +170 -12
- package/src/gtfs/__tests__/trips.test.ts +212 -141
- package/src/gtfs/__tests__/utils.test.ts +4 -4
- package/src/gtfs/parser.ts +22 -13
- package/src/gtfs/stops.ts +63 -28
- package/src/gtfs/transfers.ts +14 -6
- package/src/gtfs/trips.ts +110 -47
- package/src/gtfs/utils.ts +11 -11
- package/src/router.ts +2 -4
- package/src/routing/__tests__/route.test.ts +112 -0
- package/src/routing/__tests__/router.test.ts +234 -244
- package/src/routing/query.ts +7 -7
- package/src/routing/result.ts +9 -6
- package/src/routing/route.ts +11 -0
- package/src/routing/router.ts +26 -24
- package/src/stops/__tests__/io.test.ts +9 -8
- package/src/stops/__tests__/stopFinder.test.ts +45 -36
- package/src/stops/io.ts +8 -5
- package/src/stops/proto/stops.proto +8 -7
- package/src/stops/proto/stops.ts +68 -38
- package/src/stops/stops.ts +13 -1
- package/src/stops/stopsIndex.ts +50 -7
- package/src/timetable/__tests__/io.test.ts +40 -49
- package/src/timetable/__tests__/timetable.test.ts +50 -58
- package/src/timetable/io.ts +69 -56
- package/src/timetable/proto/timetable.proto +22 -17
- package/src/timetable/proto/timetable.ts +94 -184
- package/src/timetable/timetable.ts +62 -29
package/src/gtfs/stops.ts
CHANGED
|
@@ -3,14 +3,13 @@ import {
|
|
|
3
3
|
LocationType,
|
|
4
4
|
Longitude,
|
|
5
5
|
Platform,
|
|
6
|
+
SourceStopId,
|
|
6
7
|
Stop,
|
|
7
8
|
StopId,
|
|
8
9
|
StopsMap,
|
|
9
10
|
} from '../stops/stops.js';
|
|
10
11
|
import { Maybe, parseCsv } from './utils.js';
|
|
11
12
|
|
|
12
|
-
export type StopIds = Set<StopId>;
|
|
13
|
-
|
|
14
13
|
export type GtfsLocationType =
|
|
15
14
|
| 0 // simple stop or platform (can also be empty)
|
|
16
15
|
| 1 // station
|
|
@@ -19,15 +18,20 @@ export type GtfsLocationType =
|
|
|
19
18
|
| 4; // boarding area
|
|
20
19
|
|
|
21
20
|
export type StopEntry = {
|
|
22
|
-
stop_id:
|
|
21
|
+
stop_id: SourceStopId;
|
|
23
22
|
stop_name: string;
|
|
24
23
|
stop_lat?: Latitude;
|
|
25
24
|
stop_lon?: Longitude;
|
|
26
25
|
location_type?: GtfsLocationType;
|
|
27
|
-
parent_station?:
|
|
26
|
+
parent_station?: SourceStopId;
|
|
28
27
|
platform_code?: Platform;
|
|
29
28
|
};
|
|
30
29
|
|
|
30
|
+
type ParsedStop = Stop & {
|
|
31
|
+
parentSourceId?: SourceStopId;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ParsedStopsMap = Map<SourceStopId, ParsedStop>;
|
|
31
35
|
/**
|
|
32
36
|
* Parses the stops.txt file from a GTFS feed.
|
|
33
37
|
*
|
|
@@ -37,14 +41,14 @@ export type StopEntry = {
|
|
|
37
41
|
export const parseStops = async (
|
|
38
42
|
stopsStream: NodeJS.ReadableStream,
|
|
39
43
|
platformParser?: (stopEntry: StopEntry) => Maybe<Platform>,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
): Promise<ParsedStopsMap> => {
|
|
45
|
+
const parsedStops = new Map<SourceStopId, ParsedStop>();
|
|
46
|
+
let i = 0;
|
|
44
47
|
for await (const rawLine of parseCsv(stopsStream)) {
|
|
45
48
|
const line = rawLine as StopEntry;
|
|
46
|
-
const stop:
|
|
47
|
-
id:
|
|
49
|
+
const stop: ParsedStop = {
|
|
50
|
+
id: i,
|
|
51
|
+
sourceStopId: line.stop_id + '',
|
|
48
52
|
name: line.stop_name,
|
|
49
53
|
lat: line.stop_lat,
|
|
50
54
|
lon: line.stop_lon,
|
|
@@ -52,7 +56,7 @@ export const parseStops = async (
|
|
|
52
56
|
? parseGtfsLocationType(line.location_type)
|
|
53
57
|
: 'SIMPLE_STOP_OR_PLATFORM',
|
|
54
58
|
children: [],
|
|
55
|
-
...(line.parent_station && {
|
|
59
|
+
...(line.parent_station && { parentSourceId: line.parent_station }),
|
|
56
60
|
};
|
|
57
61
|
if (platformParser) {
|
|
58
62
|
try {
|
|
@@ -64,30 +68,61 @@ export const parseStops = async (
|
|
|
64
68
|
console.info(`Could not parse platform for stop ${line.stop_id}.`);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
parsedStops.set(line.stop_id + '', stop);
|
|
72
|
+
i = i + 1;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
for (const [
|
|
71
|
-
if (stop.
|
|
72
|
-
const parentStop =
|
|
75
|
+
for (const [sourceStopId, stop] of parsedStops) {
|
|
76
|
+
if (stop.parentSourceId) {
|
|
77
|
+
const parentStop = parsedStops.get(stop.parentSourceId);
|
|
73
78
|
if (!parentStop) {
|
|
74
|
-
console.warn(
|
|
79
|
+
console.warn(
|
|
80
|
+
`Cannot find parent stop ${stop.parentSourceId} of ${sourceStopId}`,
|
|
81
|
+
);
|
|
75
82
|
continue;
|
|
76
83
|
}
|
|
77
|
-
parentStop.
|
|
84
|
+
stop.parent = parentStop.id;
|
|
85
|
+
parentStop.children.push(stop.id);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
return parsedStops;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds the final stop map indexed by internal IDs.
|
|
93
|
+
* Excludes all stops that do not have at least one valid stopId
|
|
94
|
+
* as a child, a parent, or being valid itself.
|
|
95
|
+
*
|
|
96
|
+
* @param parsedStops - The map of parsed stops.
|
|
97
|
+
* @param validStops - A set of valid stop IDs.
|
|
98
|
+
* @returns A map of stops indexed by internal IDs.
|
|
99
|
+
*/
|
|
100
|
+
export const indexStops = (
|
|
101
|
+
parsedStops: ParsedStopsMap,
|
|
102
|
+
validStops?: Set<StopId>,
|
|
103
|
+
): StopsMap => {
|
|
104
|
+
const stops = new Map<StopId, Stop>();
|
|
105
|
+
|
|
106
|
+
for (const [, stop] of parsedStops) {
|
|
107
|
+
if (
|
|
108
|
+
!validStops ||
|
|
109
|
+
validStops.has(stop.id) ||
|
|
110
|
+
(stop.parent && validStops.has(stop.parent)) ||
|
|
111
|
+
stop.children.some((childId) => validStops.has(childId))
|
|
112
|
+
) {
|
|
113
|
+
stops.set(stop.id, {
|
|
114
|
+
id: stop.id,
|
|
115
|
+
sourceStopId: stop.sourceStopId,
|
|
116
|
+
name: stop.name,
|
|
117
|
+
lat: stop.lat,
|
|
118
|
+
lon: stop.lon,
|
|
119
|
+
locationType: stop.locationType,
|
|
120
|
+
platform: stop.platform,
|
|
121
|
+
children: stop.children.filter(
|
|
122
|
+
(childId) => !validStops || validStops.has(childId),
|
|
123
|
+
),
|
|
124
|
+
parent: stop.parent,
|
|
125
|
+
});
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
return stops;
|
package/src/gtfs/transfers.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { StopId } from '../stops/stops.js';
|
|
1
|
+
import { SourceStopId, StopId } from '../stops/stops.js';
|
|
2
2
|
import { Duration } from '../timetable/duration.js';
|
|
3
3
|
import {
|
|
4
4
|
ServiceRouteId,
|
|
5
5
|
Transfer,
|
|
6
6
|
TransferType,
|
|
7
7
|
} from '../timetable/timetable.js';
|
|
8
|
+
import { ParsedStopsMap } from './stops.js';
|
|
8
9
|
import { TripId } from './trips.js';
|
|
9
10
|
import { parseCsv } from './utils.js';
|
|
10
11
|
|
|
@@ -19,8 +20,8 @@ export type GtfsTransferType =
|
|
|
19
20
|
export type TransfersMap = Map<StopId, Transfer[]>;
|
|
20
21
|
|
|
21
22
|
export type TransferEntry = {
|
|
22
|
-
from_stop_id?:
|
|
23
|
-
to_stop_id?:
|
|
23
|
+
from_stop_id?: SourceStopId;
|
|
24
|
+
to_stop_id?: SourceStopId;
|
|
24
25
|
from_trip_id?: TripId;
|
|
25
26
|
to_trip_id?: TripId;
|
|
26
27
|
from_route_id?: ServiceRouteId;
|
|
@@ -37,11 +38,13 @@ export type TransferEntry = {
|
|
|
37
38
|
*/
|
|
38
39
|
export const parseTransfers = async (
|
|
39
40
|
transfersStream: NodeJS.ReadableStream,
|
|
41
|
+
stopsMap: ParsedStopsMap,
|
|
40
42
|
): Promise<TransfersMap> => {
|
|
41
43
|
const transfers: TransfersMap = new Map();
|
|
42
44
|
|
|
43
45
|
for await (const rawLine of parseCsv(transfersStream)) {
|
|
44
46
|
const transferEntry = rawLine as TransferEntry;
|
|
47
|
+
|
|
45
48
|
if (
|
|
46
49
|
transferEntry.transfer_type === 3 ||
|
|
47
50
|
transferEntry.transfer_type === 5
|
|
@@ -70,17 +73,22 @@ export const parseTransfers = async (
|
|
|
70
73
|
);
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
77
|
+
const fromStop = stopsMap.get(transferEntry.from_stop_id + '')!;
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
79
|
+
const toStop = stopsMap.get(transferEntry.to_stop_id + '')!;
|
|
80
|
+
|
|
73
81
|
const transfer: Transfer = {
|
|
74
|
-
destination:
|
|
82
|
+
destination: toStop.id,
|
|
75
83
|
type: parseGtfsTransferType(transferEntry.transfer_type),
|
|
76
84
|
...(transferEntry.min_transfer_time && {
|
|
77
85
|
minTransferTime: Duration.fromSeconds(transferEntry.min_transfer_time),
|
|
78
86
|
}),
|
|
79
87
|
};
|
|
80
88
|
|
|
81
|
-
const fromStopTransfers = transfers.get(
|
|
89
|
+
const fromStopTransfers = transfers.get(fromStop.id) || [];
|
|
82
90
|
fromStopTransfers.push(transfer);
|
|
83
|
-
transfers.set(
|
|
91
|
+
transfers.set(fromStop.id, fromStopTransfers);
|
|
84
92
|
}
|
|
85
93
|
return transfers;
|
|
86
94
|
};
|
package/src/gtfs/trips.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { StopId } from '../stops/stops.js';
|
|
1
|
+
import { SourceStopId, StopId } from '../stops/stops.js';
|
|
2
2
|
import {
|
|
3
|
+
MUST_COORDINATE_WITH_DRIVER,
|
|
4
|
+
MUST_PHONE_AGENCY,
|
|
5
|
+
NOT_AVAILABLE,
|
|
3
6
|
PickUpDropOffType,
|
|
7
|
+
REGULAR,
|
|
4
8
|
Route,
|
|
5
9
|
RouteId,
|
|
6
10
|
RoutesAdjacency,
|
|
7
11
|
ServiceRouteId,
|
|
8
12
|
ServiceRoutesMap,
|
|
9
13
|
StopsAdjacency,
|
|
10
|
-
StopTimes,
|
|
11
14
|
} from '../timetable/timetable.js';
|
|
12
15
|
import { ServiceIds } from './services.js';
|
|
13
|
-
import {
|
|
16
|
+
import { ParsedStopsMap } from './stops.js';
|
|
14
17
|
import { GtfsTime, toTime } from './time.js';
|
|
15
18
|
import { TransfersMap } from './transfers.js';
|
|
16
|
-
import {
|
|
19
|
+
import { hashIds, parseCsv } from './utils.js';
|
|
17
20
|
|
|
18
21
|
export type TripId = string;
|
|
19
22
|
|
|
@@ -36,7 +39,7 @@ type StopTimeEntry = {
|
|
|
36
39
|
trip_id: TripId;
|
|
37
40
|
arrival_time?: GtfsTime;
|
|
38
41
|
departure_time?: GtfsTime;
|
|
39
|
-
stop_id:
|
|
42
|
+
stop_id: SourceStopId;
|
|
40
43
|
stop_sequence: number;
|
|
41
44
|
pickup_type?: GtfsPickupDropOffType;
|
|
42
45
|
drop_off_type?: GtfsPickupDropOffType;
|
|
@@ -72,7 +75,7 @@ export const parseTrips = async (
|
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
export const buildStopsAdjacencyStructure = (
|
|
75
|
-
validStops:
|
|
78
|
+
validStops: Set<StopId>,
|
|
76
79
|
routes: RoutesAdjacency,
|
|
77
80
|
transfersMap: TransfersMap,
|
|
78
81
|
): StopsAdjacency => {
|
|
@@ -104,65 +107,130 @@ export const buildStopsAdjacencyStructure = (
|
|
|
104
107
|
* Parses the stop_times.txt data from a GTFS feed.
|
|
105
108
|
*
|
|
106
109
|
* @param stopTimesStream The readable stream containing the stop times data.
|
|
110
|
+
* @param stopsMap A map of parsed stops from the GTFS feed.
|
|
107
111
|
* @param validTripIds A map of valid trip IDs to corresponding route IDs.
|
|
108
|
-
* @param validStopIds A
|
|
109
|
-
* @returns A mapping of route IDs to route details. The routes
|
|
112
|
+
* @param validStopIds A set of valid stop IDs.
|
|
113
|
+
* @returns A mapping of route IDs to route details. The routes returned correspond to the set of trips from GTFS that share the same stop list.
|
|
110
114
|
*/
|
|
111
115
|
export const parseStopTimes = async (
|
|
112
116
|
stopTimesStream: NodeJS.ReadableStream,
|
|
117
|
+
stopsMap: ParsedStopsMap,
|
|
113
118
|
validTripIds: TripIdsMap,
|
|
114
|
-
validStopIds:
|
|
119
|
+
validStopIds: Set<StopId>,
|
|
115
120
|
): Promise<RoutesAdjacency> => {
|
|
121
|
+
/**
|
|
122
|
+
* Inserts a trip at the right place in the routes adjacency structure.
|
|
123
|
+
*/
|
|
116
124
|
const addTrip = (currentTripId: TripId) => {
|
|
117
125
|
const gtfsRouteId = validTripIds.get(currentTripId);
|
|
118
126
|
if (!gtfsRouteId) {
|
|
119
127
|
stops = [];
|
|
120
|
-
|
|
128
|
+
arrivalTimes = [];
|
|
129
|
+
departureTimes = [];
|
|
130
|
+
pickUpTypes = [];
|
|
131
|
+
dropOffTypes = [];
|
|
121
132
|
return;
|
|
122
133
|
}
|
|
123
|
-
const routeId = `${gtfsRouteId}_${
|
|
134
|
+
const routeId = `${gtfsRouteId}_${hashIds(stops)}`;
|
|
124
135
|
|
|
125
136
|
let route = routes.get(routeId);
|
|
126
137
|
if (!route) {
|
|
138
|
+
const stopsCount = stops.length;
|
|
139
|
+
const stopsArray = new Uint32Array(stops);
|
|
140
|
+
const stopTimesArray = new Uint32Array(stopsCount * 2);
|
|
141
|
+
for (let i = 0; i < stopsCount; i++) {
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
143
|
+
stopTimesArray[i * 2] = arrivalTimes[i]!;
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
145
|
+
stopTimesArray[i * 2 + 1] = departureTimes[i]!;
|
|
146
|
+
}
|
|
147
|
+
const pickUpDropOffTypesArray = new Uint8Array(stopsCount * 2);
|
|
148
|
+
for (let i = 0; i < stopsCount; i++) {
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
150
|
+
pickUpDropOffTypesArray[i * 2] = pickUpTypes[i]!;
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
152
|
+
pickUpDropOffTypesArray[i * 2 + 1] = dropOffTypes[i]!;
|
|
153
|
+
}
|
|
127
154
|
route = {
|
|
128
155
|
serviceRouteId: gtfsRouteId,
|
|
129
|
-
stops:
|
|
156
|
+
stops: stopsArray,
|
|
130
157
|
stopIndices: new Map(stops.map((stop, i) => [stop, i])),
|
|
131
|
-
stopTimes:
|
|
158
|
+
stopTimes: stopTimesArray,
|
|
159
|
+
pickUpDropOffTypes: pickUpDropOffTypesArray,
|
|
132
160
|
};
|
|
133
161
|
routes.set(routeId, route);
|
|
134
162
|
for (const stop of stops) {
|
|
135
163
|
validStopIds.add(stop);
|
|
136
164
|
}
|
|
137
165
|
} else {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
166
|
+
const tripFirstStopDeparture = departureTimes[0];
|
|
167
|
+
if (tripFirstStopDeparture === undefined) {
|
|
140
168
|
throw new Error(`Empty trip ${currentTripId}`);
|
|
141
169
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
|
|
171
|
+
// Find the correct position to insert the new trip
|
|
172
|
+
const stopsCount = stops.length;
|
|
173
|
+
let insertPosition = 0;
|
|
174
|
+
const existingTripsCount = route.stopTimes.length / (stopsCount * 2);
|
|
175
|
+
|
|
176
|
+
for (let tripIndex = 0; tripIndex < existingTripsCount; tripIndex++) {
|
|
177
|
+
const currentDeparture =
|
|
178
|
+
route.stopTimes[tripIndex * stopsCount * 2 + 1];
|
|
179
|
+
if (currentDeparture && tripFirstStopDeparture > currentDeparture) {
|
|
180
|
+
insertPosition = (tripIndex + 1) * stopsCount;
|
|
151
181
|
} else {
|
|
152
182
|
break;
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
|
-
|
|
185
|
+
|
|
186
|
+
// insert data for the new trip at the right place
|
|
187
|
+
const newStopTimesLength = route.stopTimes.length + stopsCount * 2;
|
|
188
|
+
const newStopTimes = new Uint32Array(newStopTimesLength);
|
|
189
|
+
const newPickUpDropOffTypes = new Uint8Array(newStopTimesLength);
|
|
190
|
+
|
|
191
|
+
newStopTimes.set(route.stopTimes.slice(0, insertPosition * 2), 0);
|
|
192
|
+
newPickUpDropOffTypes.set(
|
|
193
|
+
route.pickUpDropOffTypes.slice(0, insertPosition * 2),
|
|
194
|
+
0,
|
|
195
|
+
);
|
|
196
|
+
for (let i = 0; i < stopsCount; i++) {
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
198
|
+
newStopTimes[(insertPosition + i) * 2] = arrivalTimes[i]!;
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
200
|
+
newStopTimes[(insertPosition + i) * 2 + 1] = departureTimes[i]!;
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
202
|
+
newPickUpDropOffTypes[(insertPosition + i) * 2] = pickUpTypes[i]!;
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
204
|
+
newPickUpDropOffTypes[(insertPosition + i) * 2 + 1] = dropOffTypes[i]!;
|
|
205
|
+
}
|
|
206
|
+
const afterInsertionSlice = route.stopTimes.slice(insertPosition * 2);
|
|
207
|
+
newStopTimes.set(afterInsertionSlice, (insertPosition + stopsCount) * 2);
|
|
208
|
+
const afterInsertionTypesSlice = route.pickUpDropOffTypes.slice(
|
|
209
|
+
insertPosition * 2,
|
|
210
|
+
);
|
|
211
|
+
newPickUpDropOffTypes.set(
|
|
212
|
+
afterInsertionTypesSlice,
|
|
213
|
+
(insertPosition + stopsCount) * 2,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
route.stopTimes = newStopTimes;
|
|
217
|
+
route.pickUpDropOffTypes = newPickUpDropOffTypes;
|
|
156
218
|
}
|
|
157
219
|
stops = [];
|
|
158
|
-
|
|
220
|
+
arrivalTimes = [];
|
|
221
|
+
departureTimes = [];
|
|
222
|
+
pickUpTypes = [];
|
|
223
|
+
dropOffTypes = [];
|
|
159
224
|
};
|
|
160
225
|
|
|
161
226
|
const routes: RoutesAdjacency = new Map();
|
|
162
227
|
|
|
163
228
|
let previousSeq = 0;
|
|
164
229
|
let stops: StopId[] = [];
|
|
165
|
-
let
|
|
230
|
+
let arrivalTimes: number[] = [];
|
|
231
|
+
let departureTimes: number[] = [];
|
|
232
|
+
let pickUpTypes: PickUpDropOffType[] = [];
|
|
233
|
+
let dropOffTypes: PickUpDropOffType[] = [];
|
|
166
234
|
let currentTripId: TripId | undefined = undefined;
|
|
167
235
|
|
|
168
236
|
for await (const rawLine of parseCsv(stopTimesStream)) {
|
|
@@ -180,26 +248,21 @@ export const parseStopTimes = async (
|
|
|
180
248
|
if (line.pickup_type === 1 && line.drop_off_type === 1) {
|
|
181
249
|
continue;
|
|
182
250
|
}
|
|
183
|
-
if (
|
|
184
|
-
currentTripId &&
|
|
185
|
-
line.trip_id !== currentTripId &&
|
|
186
|
-
stops.length > 0 &&
|
|
187
|
-
stopTimes.length > 0
|
|
188
|
-
) {
|
|
251
|
+
if (currentTripId && line.trip_id !== currentTripId && stops.length > 0) {
|
|
189
252
|
addTrip(currentTripId);
|
|
190
253
|
}
|
|
191
254
|
currentTripId = line.trip_id;
|
|
192
|
-
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
256
|
+
stops.push(stopsMap.get(line.stop_id + '')!.id);
|
|
193
257
|
const departure = line.departure_time ?? line.arrival_time;
|
|
194
258
|
const arrival = line.arrival_time ?? line.departure_time;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
});
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
260
|
+
arrivalTimes.push(toTime(arrival!).toSeconds());
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
262
|
+
departureTimes.push(toTime(departure!).toSeconds());
|
|
263
|
+
pickUpTypes.push(parsePickupDropOffType(line.pickup_type));
|
|
264
|
+
dropOffTypes.push(parsePickupDropOffType(line.drop_off_type));
|
|
265
|
+
|
|
203
266
|
previousSeq = line.stop_sequence;
|
|
204
267
|
}
|
|
205
268
|
if (currentTripId) {
|
|
@@ -215,14 +278,14 @@ const parsePickupDropOffType = (
|
|
|
215
278
|
switch (gtfsType) {
|
|
216
279
|
default:
|
|
217
280
|
console.warn(`Unknown pickup/drop-off type ${gtfsType}`);
|
|
218
|
-
return
|
|
281
|
+
return REGULAR;
|
|
219
282
|
case 0:
|
|
220
|
-
return
|
|
283
|
+
return REGULAR;
|
|
221
284
|
case 1:
|
|
222
|
-
return
|
|
285
|
+
return NOT_AVAILABLE;
|
|
223
286
|
case 2:
|
|
224
|
-
return
|
|
287
|
+
return MUST_PHONE_AGENCY;
|
|
225
288
|
case 3:
|
|
226
|
-
return
|
|
289
|
+
return MUST_COORDINATE_WITH_DRIVER;
|
|
227
290
|
}
|
|
228
291
|
};
|
package/src/gtfs/utils.ts
CHANGED
|
@@ -3,23 +3,23 @@ import { parse, Parser } from 'csv-parse';
|
|
|
3
3
|
export type Maybe<T> = T | undefined;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Generates a simple hash from
|
|
6
|
+
* Generates a simple hash from an array of numeric IDs.
|
|
7
7
|
*
|
|
8
|
-
* This function computes a hash for a given
|
|
9
|
-
*
|
|
10
|
-
* The final hash is then converted to a base-36 string
|
|
11
|
-
* ensure a minimum length of 6 characters.
|
|
8
|
+
* This function computes a hash for a given array of numbers by iterating over each
|
|
9
|
+
* ID and applying bitwise operations to accumulate a hash value.
|
|
10
|
+
* The final hash is then converted to a base-36 string.
|
|
12
11
|
*
|
|
13
|
-
* @param
|
|
14
|
-
* @returns A hashed string representation of the input.
|
|
12
|
+
* @param ids - The array of numeric IDs to hash.
|
|
13
|
+
* @returns A hashed string representation of the input array.
|
|
15
14
|
*/
|
|
16
|
-
export const
|
|
15
|
+
export const hashIds = (ids: number[]): string => {
|
|
17
16
|
let hash = 0;
|
|
18
|
-
for (let i = 0; i <
|
|
19
|
-
|
|
17
|
+
for (let i = 0; i < ids.length; i++) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
19
|
+
hash = (hash << 5) - hash + ids[i]!;
|
|
20
20
|
hash &= hash;
|
|
21
21
|
}
|
|
22
|
-
return (hash >>> 0).toString(36)
|
|
22
|
+
return (hash >>> 0).toString(36);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
/**
|
package/src/router.ts
CHANGED
|
@@ -3,12 +3,11 @@ import { Query } from './routing/query.js';
|
|
|
3
3
|
import { Result } from './routing/result.js';
|
|
4
4
|
import { Leg, Route, Transfer, VehicleLeg } from './routing/route.js';
|
|
5
5
|
import { ReachingTime, Router } from './routing/router.js';
|
|
6
|
-
import { LocationType,
|
|
6
|
+
import { LocationType, SourceStopId, Stop } from './stops/stops.js';
|
|
7
7
|
import { StopsIndex } from './stops/stopsIndex.js';
|
|
8
8
|
import { Duration } from './timetable/duration.js';
|
|
9
9
|
import { Time } from './timetable/time.js';
|
|
10
10
|
import {
|
|
11
|
-
PickUpDropOffType,
|
|
12
11
|
RouteType,
|
|
13
12
|
ServiceRoute,
|
|
14
13
|
Timetable,
|
|
@@ -19,7 +18,6 @@ export {
|
|
|
19
18
|
Duration,
|
|
20
19
|
Leg,
|
|
21
20
|
LocationType,
|
|
22
|
-
PickUpDropOffType,
|
|
23
21
|
Plotter,
|
|
24
22
|
Query,
|
|
25
23
|
ReachingTime,
|
|
@@ -29,7 +27,7 @@ export {
|
|
|
29
27
|
RouteType,
|
|
30
28
|
ServiceRoute,
|
|
31
29
|
Stop,
|
|
32
|
-
StopId,
|
|
30
|
+
SourceStopId as StopId,
|
|
33
31
|
StopsIndex,
|
|
34
32
|
Time,
|
|
35
33
|
Timetable,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { Stop } from '../../stops/stops.js';
|
|
5
|
+
import { Duration } from '../../timetable/duration.js';
|
|
6
|
+
import { Time } from '../../timetable/time.js';
|
|
7
|
+
import { ServiceRoute, TransferType } from '../../timetable/timetable.js';
|
|
8
|
+
import { Route } from '../route.js';
|
|
9
|
+
|
|
10
|
+
describe('Route', () => {
|
|
11
|
+
const stopA: Stop = {
|
|
12
|
+
id: 1,
|
|
13
|
+
sourceStopId: 'A',
|
|
14
|
+
name: 'Stop A',
|
|
15
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
16
|
+
children: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const stopB: Stop = {
|
|
20
|
+
id: 2,
|
|
21
|
+
sourceStopId: 'B',
|
|
22
|
+
name: 'Stop B',
|
|
23
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
24
|
+
children: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const stopC: Stop = {
|
|
28
|
+
id: 3,
|
|
29
|
+
sourceStopId: 'C',
|
|
30
|
+
name: 'Stop C',
|
|
31
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
32
|
+
children: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const stopD: Stop = {
|
|
36
|
+
id: 4,
|
|
37
|
+
sourceStopId: 'D',
|
|
38
|
+
name: 'Stop D',
|
|
39
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
40
|
+
children: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const serviceRoute: ServiceRoute = {
|
|
44
|
+
type: 'BUS',
|
|
45
|
+
name: 'Route 1',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const serviceRoute2: ServiceRoute = {
|
|
49
|
+
type: 'RAIL',
|
|
50
|
+
name: 'Route 2',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const vehicleLeg = {
|
|
54
|
+
from: stopA,
|
|
55
|
+
to: stopB,
|
|
56
|
+
route: serviceRoute,
|
|
57
|
+
departureTime: Time.fromHMS(8, 0, 0),
|
|
58
|
+
arrivalTime: Time.fromHMS(8, 30, 0),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const transferLeg = {
|
|
62
|
+
from: stopB,
|
|
63
|
+
to: stopC,
|
|
64
|
+
type: 'RECOMMENDED' as TransferType,
|
|
65
|
+
minTransferTime: Duration.fromMinutes(5),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const secondVehicleLeg = {
|
|
69
|
+
from: stopC,
|
|
70
|
+
to: stopD,
|
|
71
|
+
route: serviceRoute2,
|
|
72
|
+
departureTime: Time.fromHMS(8, 40, 0),
|
|
73
|
+
arrivalTime: Time.fromHMS(9, 0, 0),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
it('should calculate the correct departure time', () => {
|
|
77
|
+
const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
|
|
78
|
+
const departureTime = route.departureTime();
|
|
79
|
+
assert.strictEqual(
|
|
80
|
+
departureTime.toSeconds(),
|
|
81
|
+
Time.fromHMS(8, 0, 0).toSeconds(),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should calculate the correct arrival time', () => {
|
|
86
|
+
const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
|
|
87
|
+
const arrivalTime = route.arrivalTime();
|
|
88
|
+
assert.strictEqual(
|
|
89
|
+
arrivalTime.toSeconds(),
|
|
90
|
+
Time.fromHMS(9, 0, 0).toSeconds(),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should calculate the total duration of the route', () => {
|
|
95
|
+
const route = new Route([vehicleLeg, transferLeg, secondVehicleLeg]);
|
|
96
|
+
const totalDuration = route.totalDuration();
|
|
97
|
+
assert.strictEqual(
|
|
98
|
+
totalDuration.toSeconds(),
|
|
99
|
+
Duration.fromMinutes(60).toSeconds(),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw an error if no vehicle leg is found for departure time', () => {
|
|
104
|
+
const route = new Route([transferLeg]);
|
|
105
|
+
assert.throws(() => route.departureTime(), /No vehicle leg found in route/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw an error if no vehicle leg is found for arrival time', () => {
|
|
109
|
+
const route = new Route([transferLeg]);
|
|
110
|
+
assert.throws(() => route.arrivalTime(), /No vehicle leg found in route/);
|
|
111
|
+
});
|
|
112
|
+
});
|