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.
Files changed (60) hide show
  1. package/.cspell.json +11 -1
  2. package/CHANGELOG.md +8 -3
  3. package/README.md +26 -24
  4. package/dist/cli.mjs +1786 -791
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/gtfs/transfers.d.ts +29 -5
  7. package/dist/gtfs/trips.d.ts +10 -5
  8. package/dist/parser.cjs.js +972 -525
  9. package/dist/parser.cjs.js.map +1 -1
  10. package/dist/parser.esm.js +972 -525
  11. package/dist/parser.esm.js.map +1 -1
  12. package/dist/router.cjs.js +1 -1
  13. package/dist/router.cjs.js.map +1 -1
  14. package/dist/router.d.ts +2 -2
  15. package/dist/router.esm.js +1 -1
  16. package/dist/router.esm.js.map +1 -1
  17. package/dist/router.umd.js +1 -1
  18. package/dist/router.umd.js.map +1 -1
  19. package/dist/routing/__tests__/plotter.test.d.ts +1 -0
  20. package/dist/routing/plotter.d.ts +42 -3
  21. package/dist/routing/result.d.ts +23 -7
  22. package/dist/routing/route.d.ts +2 -0
  23. package/dist/routing/router.d.ts +78 -19
  24. package/dist/timetable/__tests__/tripBoardingId.test.d.ts +1 -0
  25. package/dist/timetable/io.d.ts +4 -2
  26. package/dist/timetable/proto/timetable.d.ts +15 -1
  27. package/dist/timetable/route.d.ts +48 -23
  28. package/dist/timetable/timetable.d.ts +24 -7
  29. package/dist/timetable/tripBoardingId.d.ts +34 -0
  30. package/package.json +1 -1
  31. package/src/__e2e__/router.test.ts +114 -105
  32. package/src/__e2e__/timetable/stops.bin +2 -2
  33. package/src/__e2e__/timetable/timetable.bin +2 -2
  34. package/src/cli/repl.ts +245 -1
  35. package/src/gtfs/__tests__/parser.test.ts +19 -4
  36. package/src/gtfs/__tests__/transfers.test.ts +773 -37
  37. package/src/gtfs/__tests__/trips.test.ts +308 -27
  38. package/src/gtfs/parser.ts +36 -6
  39. package/src/gtfs/transfers.ts +193 -19
  40. package/src/gtfs/trips.ts +58 -21
  41. package/src/router.ts +2 -2
  42. package/src/routing/__tests__/plotter.test.ts +230 -0
  43. package/src/routing/__tests__/result.test.ts +486 -125
  44. package/src/routing/__tests__/route.test.ts +7 -3
  45. package/src/routing/__tests__/router.test.ts +380 -172
  46. package/src/routing/plotter.ts +279 -48
  47. package/src/routing/result.ts +114 -34
  48. package/src/routing/route.ts +0 -3
  49. package/src/routing/router.ts +344 -211
  50. package/src/timetable/__tests__/io.test.ts +34 -1
  51. package/src/timetable/__tests__/route.test.ts +74 -81
  52. package/src/timetable/__tests__/timetable.test.ts +232 -61
  53. package/src/timetable/__tests__/tripBoardingId.test.ts +57 -0
  54. package/src/timetable/io.ts +72 -10
  55. package/src/timetable/proto/timetable.proto +16 -2
  56. package/src/timetable/proto/timetable.ts +256 -22
  57. package/src/timetable/route.ts +174 -58
  58. package/src/timetable/timetable.ts +66 -16
  59. package/src/timetable/tripBoardingId.ts +94 -0
  60. package/tsconfig.json +2 -2
@@ -1,68 +1,299 @@
1
1
  import { StopId } from '../stops/stops.js';
2
2
  import { Result } from './result.js';
3
- import { TripLeg } from './router.js';
3
+ import { RoutingEdge, TransferEdge, VehicleEdge } from './router.js';
4
4
 
5
5
  export class Plotter {
6
6
  private result: Result;
7
+ private readonly ROUND_COLORS = [
8
+ '#60a5fa', // Round 1
9
+ '#ff9800', // Round 2
10
+ '#14b8a6', // Round 3
11
+ '#fb7185', // Round 4
12
+ '#ffdf00', // Round 5
13
+ '#b600ff', // Round 6
14
+ '#ee82ee', // Round 7+
15
+ ];
7
16
 
8
17
  constructor(result: Result) {
9
18
  this.result = result;
10
19
  }
11
20
 
12
21
  /**
13
- * Plots the path three as a DOT for debugging purposes.
14
- *
15
- * @returns A string representing the DOT graph of the path tree.
22
+ * Gets the color for a round based on the specified palette.
16
23
  */
17
- plotDotGraph(): string {
18
- const earliestArrivalsPerRound: Map<StopId, TripLeg>[] =
19
- this.result.earliestArrivalsPerRound;
24
+ private getRoundColor(round: number): string {
25
+ if (round === 0) return '#888888';
26
+
27
+ const colorIndex = Math.min(round - 1, this.ROUND_COLORS.length - 1);
28
+ return this.ROUND_COLORS[colorIndex] ?? '#ee82ee';
29
+ }
30
+
31
+ /**
32
+ * Escapes special characters in DOT strings to prevent syntax errors.
33
+ */
34
+ private escapeDotString(str: string): string {
35
+ return str
36
+ .replace(/\\/g, '\\\\')
37
+ .replace(/"/g, '\\"')
38
+ .replace(/\n/g, '\\n')
39
+ .replace(/\r/g, '\\r')
40
+ .replace(/\t/g, '\\t');
41
+ }
42
+
43
+ /**
44
+ * Determines station type (origin/destination) information.
45
+ */
46
+ private getStationInfo(stopId: StopId): {
47
+ isOrigin: boolean;
48
+ isDestination: boolean;
49
+ } {
50
+ const isOrigin = this.result.routingState.graph[0]?.has(stopId) ?? false;
51
+ const isDestination =
52
+ this.result.routingState.destinations.includes(stopId);
53
+ return { isOrigin, isDestination };
54
+ }
55
+
56
+ /**
57
+ * Formats a stop name for display, including platform information.
58
+ */
59
+ private formatStopName(stopId: StopId): string {
60
+ const stop = this.result.stopsIndex.findStopById(stopId);
61
+ if (!stop) return `Unknown Stop (${stopId})`;
62
+
63
+ const escapedName = this.escapeDotString(stop.name);
64
+ const escapedPlatform = stop.platform
65
+ ? this.escapeDotString(stop.platform)
66
+ : '';
67
+ return escapedPlatform
68
+ ? `${escapedName}\\nPl. ${escapedPlatform}`
69
+ : escapedName;
70
+ }
71
+
72
+ /**
73
+ * Gets the appropriate fill color for a station based on its type.
74
+ */
75
+ private getStationFillColor(
76
+ isOrigin: boolean,
77
+ isDestination: boolean,
78
+ ): string {
79
+ if (isOrigin) return '#60a5fa';
80
+ if (isDestination) return '#ee82ee';
81
+ return 'white';
82
+ }
20
83
 
21
- const dotParts: string[] = [
22
- 'digraph PathTree {',
23
- ' graph [overlap=false];',
24
- ' node [shape=ellipse style=filled fillcolor=lightgrey];',
84
+ /**
85
+ * Creates a DOT node for a station.
86
+ */
87
+ private createStationNode(stopId: StopId): string {
88
+ const stop = this.result.stopsIndex.findStopById(stopId);
89
+ if (!stop) return '';
90
+
91
+ const displayName = this.formatStopName(stopId);
92
+ const stopIdStr = this.escapeDotString(String(stopId));
93
+ const nodeId = `s_${stopId}`;
94
+ const stationInfo = this.getStationInfo(stopId);
95
+ const fillColor = this.getStationFillColor(
96
+ stationInfo.isOrigin,
97
+ stationInfo.isDestination,
98
+ );
99
+
100
+ return ` "${nodeId}" [label="${displayName}\\n${stopIdStr}" shape=box style=filled fillcolor="${fillColor}"];`;
101
+ }
102
+
103
+ /**
104
+ * Creates a vehicle edge with route information oval in the middle.
105
+ */
106
+ private createVehicleEdge(edge: VehicleEdge, round: number): string[] {
107
+ const fromNodeId = `s_${edge.from}`;
108
+ const toNodeId = `s_${edge.to}`;
109
+ const roundColor = this.getRoundColor(round);
110
+ const routeOvalId = `e_${edge.from}_${edge.to}_${edge.routeId}_${round}`;
111
+
112
+ const route = this.result.timetable.getRoute(edge.routeId);
113
+ const serviceRouteInfo = route
114
+ ? this.result.timetable.getServiceRouteInfo(route)
115
+ : null;
116
+
117
+ const routeName = serviceRouteInfo?.name ?? `Route ${String(edge.routeId)}`;
118
+ const routeType = serviceRouteInfo?.type || 'UNKNOWN';
119
+
120
+ const departureTime = route
121
+ ? route.departureFrom(edge.from, edge.tripIndex).toString()
122
+ : 'N/A';
123
+ const arrivalTime = edge.arrival.toString();
124
+
125
+ const escapedRouteName = this.escapeDotString(routeName);
126
+ const escapedRouteType = this.escapeDotString(routeType);
127
+ const routeInfo = `${edge.routeId}:${edge.tripIndex}`;
128
+ const ovalLabel = `${escapedRouteType} ${escapedRouteName}\\n${routeInfo}\\n${departureTime} → ${arrivalTime}`;
129
+
130
+ return [
131
+ ` "${routeOvalId}" [label="${ovalLabel}" shape=oval style=filled fillcolor="white" color="${roundColor}"];`,
132
+ ` "${fromNodeId}" -> "${routeOvalId}" [color="${roundColor}"];`,
133
+ ` "${routeOvalId}" -> "${toNodeId}" [color="${roundColor}"];`,
25
134
  ];
26
- earliestArrivalsPerRound.forEach((arrivalsInRound, round) => {
27
- arrivalsInRound.forEach((tripLeg: TripLeg) => {
28
- const { origin, leg } = tripLeg;
29
- if (!leg) return; // Skip if leg is undefined
30
- const fromStop = this.result['stopsIndex'].findStopById(leg.from.id);
31
- const toStop = this.result['stopsIndex'].findStopById(leg.to.id);
32
- const originStop = this.result['stopsIndex'].findStopById(origin);
33
-
34
- if (fromStop && toStop && originStop) {
35
- const fromName = fromStop.platform
36
- ? `${fromStop.name} (Pl. ${fromStop.platform})`
37
- : fromStop.name;
38
- const toName = toStop.platform
39
- ? `${toStop.name} (Pl. ${toStop.platform})`
40
- : toStop.name;
41
- const originName = originStop.platform
42
- ? `${originStop.name} (Pl. ${originStop.platform})`
43
- : originStop.name;
44
- const isVehicle = 'route' in leg;
45
- const routeLabelContent = isVehicle
46
- ? `${leg.route.name}\n${leg.departureTime.toString()} - ${leg.arrivalTime.toString()}`
47
- : leg.minTransferTime
48
- ? leg.minTransferTime.toString()
49
- : '';
50
- const intermediateNode = `IntermediateNode${fromStop.id}_${toStop.id}`;
51
- const lineColor = isVehicle ? '' : ', color="red", fontcolor="red"';
52
- const labelColor = isVehicle ? '' : ' fontcolor="red"';
53
-
54
- dotParts.push(
55
- ` "${fromName} (Origin: ${originName}) [R${round}]\n(${fromStop.id})" -> "${intermediateNode}" [shape=point${lineColor}];`,
56
- );
57
- dotParts.push(
58
- ` "${intermediateNode}" [label="${routeLabelContent}" shape=rect style=filled fillcolor=white${labelColor} border=0];`,
59
- );
60
- dotParts.push(
61
- ` "${intermediateNode}" -> "${toName} (Origin: ${originName}) [R${round}]\n(${toStop.id})" [${lineColor.replace(', ', '')}];`,
62
- );
135
+ }
136
+
137
+ /**
138
+ * Creates a transfer edge with transfer information oval in the middle.
139
+ */
140
+ private createTransferEdge(edge: TransferEdge, round: number): string[] {
141
+ const fromNodeId = `s_${edge.from}`;
142
+ const toNodeId = `s_${edge.to}`;
143
+ const roundColor = this.getRoundColor(round);
144
+ const transferOvalId = `e_${edge.from}_${edge.to}_${round}`;
145
+
146
+ const transferTime = edge.minTransferTime?.toString() || 'N/A';
147
+ const escapedTransferTime = this.escapeDotString(transferTime);
148
+ const ovalLabel = `Transfer\\n${escapedTransferTime}`;
149
+
150
+ return [
151
+ ` "${transferOvalId}" [label="${ovalLabel}" shape=oval style="dashed,filled" fillcolor="white" color="${roundColor}"];`,
152
+ ` "${fromNodeId}" -> "${transferOvalId}" [color="${roundColor}" style="dashed"];`,
153
+ ` "${transferOvalId}" -> "${toNodeId}" [color="${roundColor}" style="dashed"];`,
154
+ ];
155
+ }
156
+
157
+ /**
158
+ * Creates a continuation edge to visually link trip continuations.
159
+ */
160
+ private createContinuationEdge(
161
+ fromEdge: VehicleEdge,
162
+ toEdge: VehicleEdge,
163
+ round: number,
164
+ ): string[] {
165
+ const fromStationId = `s_${fromEdge.to}`;
166
+ const toStationId = `s_${toEdge.from}`;
167
+ const roundColor = this.getRoundColor(round);
168
+ const continuationOvalId = `continuation_${fromEdge.to}_${toEdge.from}_${round}`;
169
+
170
+ const fromRoute = this.result.timetable.getRoute(fromEdge.routeId);
171
+ const toRoute = this.result.timetable.getRoute(toEdge.routeId);
172
+
173
+ const fromServiceRouteInfo = fromRoute
174
+ ? this.result.timetable.getServiceRouteInfo(fromRoute)
175
+ : null;
176
+ const toServiceRouteInfo = toRoute
177
+ ? this.result.timetable.getServiceRouteInfo(toRoute)
178
+ : null;
179
+
180
+ const fromRouteName =
181
+ fromServiceRouteInfo?.name ?? `Route ${String(fromEdge.routeId)}`;
182
+ const toRouteName =
183
+ toServiceRouteInfo?.name ?? `Route ${String(toEdge.routeId)}`;
184
+
185
+ const fromRouteType = fromServiceRouteInfo?.type || 'UNKNOWN';
186
+ const toRouteType = toServiceRouteInfo?.type || 'UNKNOWN';
187
+
188
+ const fromArrivalTime = fromEdge.arrival.toString();
189
+ const toDepartureTime = toRoute
190
+ ? toRoute.departureFrom(toEdge.from, toEdge.tripIndex).toString()
191
+ : 'N/A';
192
+
193
+ const escapedFromRouteName = this.escapeDotString(fromRouteName);
194
+ const escapedToRouteName = this.escapeDotString(toRouteName);
195
+ const escapedFromRouteType = this.escapeDotString(fromRouteType);
196
+ const escapedToRouteType = this.escapeDotString(toRouteType);
197
+
198
+ const fromRouteInfo = `${fromEdge.routeId}:${fromEdge.tripIndex}`;
199
+ const toRouteInfo = `${toEdge.routeId}:${toEdge.tripIndex}`;
200
+
201
+ const ovalLabel = `${escapedFromRouteType} ${escapedFromRouteName} (${fromRouteInfo}) ${fromArrivalTime}\\n↓\\n${escapedToRouteType} ${escapedToRouteName} (${toRouteInfo}) ${toDepartureTime}`;
202
+
203
+ return [
204
+ ` "${continuationOvalId}" [label="${ovalLabel}" shape=oval style="filled,bold" fillcolor="#ffffcc" color="${roundColor}" penwidth="2"];`,
205
+ ` "${fromStationId}" -> "${continuationOvalId}" [color="${roundColor}" style="bold" penwidth="3"];`,
206
+ ` "${continuationOvalId}" -> "${toStationId}" [color="${roundColor}" style="bold" penwidth="3"];`,
207
+ ];
208
+ }
209
+
210
+ /**
211
+ * Collects all stations and edges for the graph.
212
+ */
213
+ private collectGraphData(): {
214
+ stations: Set<StopId>;
215
+ edges: string[];
216
+ } {
217
+ const stations = new Set<StopId>();
218
+ const edges: string[] = [];
219
+ const continuationEdges: string[] = [];
220
+ const graph: Map<StopId, RoutingEdge>[] = this.result.routingState.graph;
221
+
222
+ // Collect all stops that appear in the graph
223
+ graph.forEach((roundMap) => {
224
+ roundMap.forEach((edge, stopId) => {
225
+ stations.add(stopId);
226
+ if ('from' in edge && 'to' in edge) {
227
+ stations.add(edge.from);
228
+ stations.add(edge.to);
229
+ }
230
+ });
231
+ });
232
+
233
+ // Create edges for each round
234
+ graph.forEach((roundMap, round) => {
235
+ if (round === 0) {
236
+ // Skip round 0 as it contains only origin nodes
237
+ return;
238
+ }
239
+
240
+ roundMap.forEach((edge) => {
241
+ if ('from' in edge && 'to' in edge) {
242
+ if ('routeId' in edge) {
243
+ const vehicleEdgeParts = this.createVehicleEdge(edge, round);
244
+ edges.push(...vehicleEdgeParts);
245
+ if (edge.continuationOf) {
246
+ let currentEdge = edge;
247
+ let previousEdge: VehicleEdge | undefined = edge.continuationOf;
248
+
249
+ while (previousEdge) {
250
+ const continuationEdgeParts = this.createContinuationEdge(
251
+ previousEdge,
252
+ currentEdge,
253
+ round,
254
+ );
255
+ continuationEdges.push(...continuationEdgeParts);
256
+
257
+ currentEdge = previousEdge;
258
+ previousEdge = previousEdge.continuationOf;
259
+ }
260
+ }
261
+ } else {
262
+ const transferEdgeParts = this.createTransferEdge(edge, round);
263
+ edges.push(...transferEdgeParts);
264
+ }
63
265
  }
64
266
  });
65
267
  });
268
+ edges.push(...continuationEdges);
269
+
270
+ return { stations, edges };
271
+ }
272
+
273
+ /**
274
+ * Plots the routing graph as a DOT graph for visualization.
275
+ */
276
+ plotDotGraph(): string {
277
+ const { stations, edges } = this.collectGraphData();
278
+
279
+ const dotParts = [
280
+ 'digraph RoutingGraph {',
281
+ ' graph [overlap=false, splines=true, rankdir=TB, bgcolor=white, nodesep=0.8, ranksep=1.2, concentrate=true];',
282
+ ' node [fontname="Arial" margin=0.1];',
283
+ ' edge [fontname="Arial" fontsize=10];',
284
+ '',
285
+ ' // Stations',
286
+ ];
287
+
288
+ stations.forEach((stopId) => {
289
+ const stationNode = this.createStationNode(stopId);
290
+ if (stationNode) {
291
+ dotParts.push(stationNode);
292
+ }
293
+ });
294
+
295
+ dotParts.push('', ' // Edges');
296
+ dotParts.push(...edges);
66
297
 
67
298
  dotParts.push('}');
68
299
  return dotParts.join('\n');
@@ -1,25 +1,26 @@
1
+ import { Timetable } from '../router.js';
1
2
  import { SourceStopId, StopId } from '../stops/stops.js';
2
3
  import { StopsIndex } from '../stops/stopsIndex.js';
3
4
  import { Query } from './query.js';
4
- import { Leg, Route } from './route.js';
5
- import { ReachingTime, TripLeg } from './router.js';
5
+ import { Leg, Route, Transfer, VehicleLeg } from './route.js';
6
+ import { Arrival, RoutingState, TransferEdge, VehicleEdge } from './router.js';
6
7
 
7
8
  export class Result {
8
9
  private readonly query: Query;
9
- public readonly earliestArrivals: Map<StopId, ReachingTime>;
10
- public readonly earliestArrivalsPerRound: Map<StopId, TripLeg>[];
11
- private readonly stopsIndex: StopsIndex;
10
+ public readonly routingState: RoutingState;
11
+ public readonly stopsIndex: StopsIndex;
12
+ public readonly timetable: Timetable;
12
13
 
13
14
  constructor(
14
15
  query: Query,
15
- earliestArrivals: Map<StopId, ReachingTime>,
16
- earliestArrivalsPerRound: Map<StopId, TripLeg>[],
16
+ routingState: RoutingState,
17
17
  stopsIndex: StopsIndex,
18
+ timetable: Timetable,
18
19
  ) {
19
20
  this.query = query;
20
- this.earliestArrivals = earliestArrivals;
21
- this.earliestArrivalsPerRound = earliestArrivalsPerRound;
21
+ this.routingState = routingState;
22
22
  this.stopsIndex = stopsIndex;
23
+ this.timetable = timetable;
23
24
  }
24
25
 
25
26
  /**
@@ -39,11 +40,13 @@ export class Result {
39
40
  const destinations = destinationList.flatMap((destination) =>
40
41
  this.stopsIndex.equivalentStops(destination),
41
42
  );
42
-
43
+ // find the first reached destination
43
44
  let fastestDestination: StopId | undefined = undefined;
44
- let fastestTime: ReachingTime | undefined = undefined;
45
+ let fastestTime: Arrival | undefined = undefined;
45
46
  for (const destination of destinations) {
46
- const arrivalTime = this.earliestArrivals.get(destination.id);
47
+ const arrivalTime = this.routingState.earliestArrivals.get(
48
+ destination.id,
49
+ );
47
50
  if (arrivalTime !== undefined) {
48
51
  if (
49
52
  fastestTime === undefined ||
@@ -57,28 +60,96 @@ export class Result {
57
60
  if (!fastestDestination || !fastestTime) {
58
61
  return undefined;
59
62
  }
60
-
61
63
  const route: Leg[] = [];
62
64
  let currentStop = fastestDestination;
63
65
  let round = fastestTime.legNumber;
64
- while (fastestTime.origin !== currentStop) {
65
- const tripLeg = this.earliestArrivalsPerRound[round]?.get(currentStop);
66
- if (!tripLeg?.leg) {
66
+ while (round > 0) {
67
+ const edge = this.routingState.graph[round]?.get(currentStop);
68
+ if (!edge) {
67
69
  throw new Error(
68
- `No leg found for a trip leg: start stop=${
69
- tripLeg?.leg?.from.id ?? 'unknown'
70
- }, end stop=${currentStop}, round=${round}, origin=${fastestTime.origin}`,
70
+ `No edge arriving at stop ${currentStop} at round ${round}`,
71
71
  );
72
72
  }
73
- route.unshift(tripLeg.leg);
74
- currentStop = tripLeg.leg.from.id;
75
- if ('route' in tripLeg.leg) {
73
+ let leg: Leg;
74
+ if ('routeId' in edge) {
75
+ let vehicleEdge = edge;
76
+ // Handle leg reconstruction for in-seat trip continuations
77
+ const chainedEdges = [vehicleEdge];
78
+ while ('routeId' in vehicleEdge && vehicleEdge.continuationOf) {
79
+ chainedEdges.push(vehicleEdge.continuationOf);
80
+ vehicleEdge = vehicleEdge.continuationOf;
81
+ }
82
+ leg = this.buildVehicleLeg(chainedEdges);
83
+ } else if ('type' in edge) {
84
+ leg = this.buildTransferLeg(edge);
85
+ } else {
86
+ break;
87
+ }
88
+ route.unshift(leg);
89
+ currentStop = leg.from.id;
90
+ if ('routeId' in edge) {
76
91
  round -= 1;
77
92
  }
78
93
  }
79
94
  return new Route(route);
80
95
  }
81
96
 
97
+ /**
98
+ * Builds a vehicle leg from a chain of vehicle edges.
99
+ *
100
+ * @param edges Array of vehicle edges representing continuous trips on transit vehicles
101
+ * @returns A vehicle leg with departure/arrival information and route details
102
+ * @throws Error if the edges array is empty
103
+ */
104
+ private buildVehicleLeg(edges: VehicleEdge[]): VehicleLeg {
105
+ if (edges.length === 0) {
106
+ throw new Error('Cannot build vehicle leg from empty edges');
107
+ }
108
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
109
+ const firstEdge = edges[edges.length - 1]!;
110
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
111
+ const lastEdge = edges[0]!;
112
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
113
+ const firstRoute = this.timetable.getRoute(firstEdge.routeId)!;
114
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
115
+ const lastRoute = this.timetable.getRoute(lastEdge.routeId)!;
116
+ return {
117
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
118
+ from: this.stopsIndex.findStopById(firstRoute.stopId(firstEdge.from))!,
119
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120
+ to: this.stopsIndex.findStopById(lastRoute.stopId(lastEdge.to))!,
121
+ // The route info comes from the first boarded route in case on continuous trips
122
+ route: this.timetable.getServiceRouteInfo(firstRoute),
123
+ departureTime: firstRoute.departureFrom(
124
+ firstEdge.from,
125
+ firstEdge.tripIndex,
126
+ ),
127
+ arrivalTime: lastEdge.arrival,
128
+ pickUpType: firstRoute.pickUpTypeFrom(
129
+ firstEdge.from,
130
+ firstEdge.tripIndex,
131
+ ),
132
+ dropOffType: lastRoute.dropOffTypeAt(lastEdge.to, lastEdge.tripIndex),
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Builds a transfer leg from a transfer edge.
138
+ *
139
+ * @param edge Transfer edge representing a walking connection between stops
140
+ * @returns A transfer leg with from/to stops and transfer details
141
+ */
142
+ private buildTransferLeg(edge: TransferEdge): Transfer {
143
+ return {
144
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
145
+ from: this.stopsIndex.findStopById(edge.from)!,
146
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147
+ to: this.stopsIndex.findStopById(edge.to)!,
148
+ minTransferTime: edge.minTransferTime,
149
+ type: edge.type,
150
+ };
151
+ }
152
+
82
153
  /**
83
154
  * Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
84
155
  *
@@ -86,21 +157,30 @@ export class Result {
86
157
  * @param maxTransfers The optional maximum number of transfers allowed.
87
158
  * @returns The arrival time if the target stop is reachable, otherwise undefined.
88
159
  */
89
- arrivalAt(
90
- stop: SourceStopId,
91
- maxTransfers?: number,
92
- ): ReachingTime | undefined {
160
+ arrivalAt(stop: SourceStopId, maxTransfers?: number): Arrival | undefined {
93
161
  const equivalentStops = this.stopsIndex.equivalentStops(stop);
94
- let earliestArrival: ReachingTime | undefined = undefined;
95
-
96
- const relevantArrivals =
97
- maxTransfers !== undefined
98
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99
- this.earliestArrivalsPerRound[maxTransfers + 1]!
100
- : this.earliestArrivals;
162
+ let earliestArrival: Arrival | undefined = undefined;
101
163
 
102
164
  for (const equivalentStop of equivalentStops) {
103
- const arrivalTime = relevantArrivals.get(equivalentStop.id);
165
+ let arrivalTime;
166
+ if (maxTransfers === undefined) {
167
+ arrivalTime = this.routingState.earliestArrivals.get(equivalentStop.id);
168
+ } else {
169
+ // We have no guarantee that the stop was visited in the last round,
170
+ // so we need to check all rounds if it's not found in the last one.
171
+ for (let i = maxTransfers + 1; i >= 0; i--) {
172
+ const arrivalEdge = this.routingState.graph[i]?.get(
173
+ equivalentStop.id,
174
+ );
175
+ if (arrivalEdge !== undefined) {
176
+ arrivalTime = {
177
+ arrival: arrivalEdge.arrival,
178
+ legNumber: i,
179
+ };
180
+ break;
181
+ }
182
+ }
183
+ }
104
184
  if (arrivalTime !== undefined) {
105
185
  if (
106
186
  earliestArrival === undefined ||
@@ -38,11 +38,8 @@ export type VehicleLeg = BaseLeg & {
38
38
  route: ServiceRouteInfo;
39
39
  departureTime: Time;
40
40
  arrivalTime: Time;
41
- // TODO support pick up and drop off types
42
- /*
43
41
  pickUpType: PickUpDropOffType;
44
42
  dropOffType: PickUpDropOffType;
45
- */
46
43
  };
47
44
 
48
45
  export type Leg = Transfer | VehicleLeg;