minotor 9.0.2 → 9.2.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.
@@ -1,5 +1,5 @@
1
1
  import { Timetable } from '../router.js';
2
- import { SourceStopId } from '../stops/stops.js';
2
+ import { SourceStopId, StopId } from '../stops/stops.js';
3
3
  import { StopsIndex } from '../stops/stopsIndex.js';
4
4
  import { Query } from './query.js';
5
5
  import { Route } from './route.js';
@@ -10,6 +10,14 @@ export declare class Result {
10
10
  readonly stopsIndex: StopsIndex;
11
11
  readonly timetable: Timetable;
12
12
  constructor(query: Query, routingState: RoutingState, stopsIndex: StopsIndex, timetable: Timetable);
13
+ /**
14
+ * Reconstructs the best route to a stop by StopId.
15
+ * (to any stop reachable in less time / transfers than the destination(s) of the query)
16
+ *
17
+ * @param to The destination stop by StopId.
18
+ * @returns a route to the destination stop if it exists.
19
+ */
20
+ bestRouteToStopId(to: StopId | Set<StopId>): Route | undefined;
13
21
  /**
14
22
  * Reconstructs the best route to a stop.
15
23
  * (to any stop reachable in less time / transfers than the destination(s) of the query)
@@ -1,6 +1,6 @@
1
1
  import { SourceStopId, Stop, StopId } from './stops.js';
2
2
  /**
3
- * The StopMap class provides functionality to search for public transport stops
3
+ * The StopsIndex class provides functionality to search for public transport stops
4
4
  * by name or geographic location. It leverages text search and geospatial indexing
5
5
  * to efficiently find stops based on user queries.
6
6
  */
@@ -66,4 +66,10 @@ export declare class StopsIndex {
66
66
  * Find ids of all sibling stops.
67
67
  */
68
68
  equivalentStops(sourceId: SourceStopId): Stop[];
69
+ /**
70
+ * Makes the StopsIndex iterable, allowing iteration over all stops.
71
+ *
72
+ * @returns An iterator for the stops.
73
+ */
74
+ [Symbol.iterator](): Iterator<Stop>;
69
75
  }
package/eslint.config.mjs CHANGED
@@ -18,7 +18,6 @@ export default tseslint.config(
18
18
  sourceType: 'module',
19
19
  parser: tseslint.parser,
20
20
  parserOptions: {
21
- project: './tsconfig.json',
22
21
  projectService: true,
23
22
  tsconfigRootDir: import.meta.dirname,
24
23
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minotor",
3
- "version": "9.0.2",
3
+ "version": "9.2.0",
4
4
  "description": "A lightweight client-side transit routing library.",
5
5
  "keywords": [
6
6
  "minotor",
@@ -42,11 +42,11 @@
42
42
  }
43
43
  },
44
44
  "engines": {
45
- "node": ">=22.18.0",
45
+ "node": ">=24.11.0",
46
46
  "npm": ">=11.5.2"
47
47
  },
48
48
  "volta": {
49
- "node": "22.18.0",
49
+ "node": "24.12.0",
50
50
  "npm": "11.5.2"
51
51
  },
52
52
  "publishConfig": {
@@ -74,44 +74,44 @@
74
74
  "semantic-release": "semantic-release"
75
75
  },
76
76
  "devDependencies": {
77
- "@eslint/js": "^9.34.0",
78
- "@rollup/plugin-commonjs": "^28.0.6",
79
- "@rollup/plugin-node-resolve": "^16.0.1",
77
+ "@eslint/js": "^9.39.2",
78
+ "@rollup/plugin-commonjs": "^29.0.0",
79
+ "@rollup/plugin-node-resolve": "^16.0.3",
80
80
  "@rollup/plugin-terser": "^0.4.4",
81
- "@rollup/plugin-typescript": "^12.1.4",
81
+ "@rollup/plugin-typescript": "^12.3.0",
82
82
  "@ryansonshine/commitizen": "^4.2.8",
83
83
  "@ryansonshine/cz-conventional-changelog": "^3.3.4",
84
84
  "@semantic-release/changelog": "^6.0.3",
85
85
  "@semantic-release/commit-analyzer": "^13.0.1",
86
- "@semantic-release/github": "^11.0.4",
87
- "@semantic-release/npm": "^12.0.2",
88
- "@semantic-release/release-notes-generator": "^14.0.3",
86
+ "@semantic-release/github": "^12.0.2",
87
+ "@semantic-release/npm": "^13.1.3",
88
+ "@semantic-release/release-notes-generator": "^14.1.0",
89
89
  "@types/geokdbush": "^1.1.5",
90
90
  "@types/luxon": "^3.7.1",
91
- "@types/node": "^24.3.0",
91
+ "@types/node": "^25.0.3",
92
92
  "c8": "^10.1.3",
93
- "cspell": "^9.2.0",
94
- "eslint": "^9.34.0",
93
+ "cspell": "^9.4.0",
94
+ "eslint": "^9.39.2",
95
95
  "eslint-config-prettier": "^10.1.8",
96
96
  "eslint-plugin-simple-import-sort": "^12.1.1",
97
- "prettier": "^3.6.2",
98
- "rimraf": "^6.0.1",
99
- "rollup": "^4.49.0",
100
- "semantic-release": "^24.2.7",
101
- "ts-proto": "^2.7.7",
102
- "tsx": "^4.20.5",
103
- "typescript": "^5.9.2",
104
- "typescript-eslint": "^8.41.0"
97
+ "prettier": "^3.7.4",
98
+ "rimraf": "^6.1.2",
99
+ "rollup": "^4.55.1",
100
+ "semantic-release": "^25.0.2",
101
+ "ts-proto": "^2.10.1",
102
+ "tsx": "^4.21.0",
103
+ "typescript": "^5.9.3",
104
+ "typescript-eslint": "^8.52.0"
105
105
  },
106
106
  "dependencies": {
107
- "@bufbuild/protobuf": "^2.7.0",
108
- "commander": "^14.0.0",
107
+ "@bufbuild/protobuf": "^2.10.2",
108
+ "commander": "^14.0.2",
109
109
  "csv-parse": "^6.1.0",
110
110
  "geokdbush": "^2.0.1",
111
111
  "kdbush": "^4.0.2",
112
112
  "loglevel": "^1.9.2",
113
- "luxon": "^3.7.1",
113
+ "luxon": "^3.7.2",
114
114
  "node-stream-zip": "^1.15.0",
115
- "slimsearch": "^2.2.2"
115
+ "slimsearch": "^2.3.0"
116
116
  }
117
117
  }
@@ -15,7 +15,7 @@ const routes = [
15
15
  from: '8504100:0:2',
16
16
  to: '8504086:0:2',
17
17
  departure: '08:34',
18
- arrival: '09:11',
18
+ arrival: '09:10',
19
19
  route: { type: 'RAIL', name: 'RE2' },
20
20
  },
21
21
  {
@@ -33,13 +33,13 @@ const routes = [
33
33
  },
34
34
  {
35
35
  from: '8504077:0:1',
36
- to: '8577737',
36
+ to: '8577737:0:B',
37
37
  type: 'REQUIRES_MINIMAL_TIME',
38
38
  minTransferTime: '02:00',
39
39
  },
40
40
  {
41
- from: '8577737',
42
- to: '8504880',
41
+ from: '8577737:0:B',
42
+ to: '8504880:0:10000',
43
43
  departure: '09:33',
44
44
  arrival: '09:44',
45
45
  route: { type: 'BUS', name: '263' },
@@ -60,54 +60,28 @@ const routes = [
60
60
  },
61
61
  {
62
62
  from: '8503000:0:33',
63
- to: '8503000:0:9',
63
+ to: '8503000:0:6',
64
64
  type: 'REQUIRES_MINIMAL_TIME',
65
65
  minTransferTime: '07:00',
66
66
  },
67
67
  {
68
- from: '8503000:0:9',
69
- to: '8509002:0:2',
68
+ from: '8503000:0:6',
69
+ to: '8509000:0:9',
70
70
  departure: '13:38',
71
- arrival: '14:41',
71
+ arrival: '14:52',
72
72
  route: { type: 'RAIL', name: 'IC3' },
73
73
  },
74
74
  {
75
- from: '8509002:0:2',
76
- to: '8509002:0:6',
75
+ from: '8509000:0:9',
76
+ to: '8509000:0:10',
77
77
  type: 'REQUIRES_MINIMAL_TIME',
78
- minTransferTime: '04:00',
79
- },
80
- {
81
- from: '8509002:0:6',
82
- to: '8509269:0:3',
83
- departure: '14:49',
84
- arrival: '15:52',
85
- route: { type: 'RAIL', name: 'RE24' },
86
- },
87
- {
88
- from: '8509269:0:3',
89
- to: '8509269:0:4',
90
- type: 'REQUIRES_MINIMAL_TIME',
91
- minTransferTime: '01:00',
92
- },
93
- {
94
- from: '8509269:0:4',
95
- to: '8509251:0:3',
96
- departure: '15:54',
97
- arrival: '16:43',
98
- route: { type: 'RAIL', name: 'R15' },
99
- },
100
- {
101
- from: '8509251:0:3',
102
- to: '8509251:0:2',
103
- type: 'REQUIRES_MINIMAL_TIME',
104
- minTransferTime: '01:00',
78
+ minTransferTime: '03:00',
105
79
  },
106
80
  {
107
- from: '8509251:0:2',
81
+ from: '8509000:0:10',
108
82
  to: '8509253:0:1',
109
- departure: '16:48',
110
- arrival: '17:00',
83
+ departure: '14:58',
84
+ arrival: '16:55',
111
85
  route: { type: 'RAIL', name: 'IR38' },
112
86
  },
113
87
  ],
@@ -121,7 +95,7 @@ const routes = [
121
95
  from: '8500010:0:33',
122
96
  to: '8718213',
123
97
  departure: '17:08',
124
- arrival: '17:15',
98
+ arrival: '17:16',
125
99
  route: { type: 'RAIL', name: 'TER' },
126
100
  },
127
101
  {
@@ -140,13 +114,26 @@ const routes = [
140
114
  route: [
141
115
  {
142
116
  from: '8504100:0:3',
143
- to: '8503000:0:33',
144
- departure: '09:03',
145
- arrival: '10:28',
146
- route: { type: 'RAIL', name: 'IC1' },
117
+ to: '8507000:0:10',
118
+ departure: '08:33',
119
+ arrival: '08:56',
120
+ route: { type: 'RAIL', name: 'IR15' },
147
121
  },
148
122
  {
149
- from: '8503000:0:33',
123
+ from: '8507000:0:10',
124
+ to: '8507000:0:2',
125
+ type: 'REQUIRES_MINIMAL_TIME',
126
+ minTransferTime: '06:00',
127
+ },
128
+ {
129
+ from: '8507000:0:2',
130
+ to: '8503000:0:34',
131
+ departure: '09:02',
132
+ arrival: '09:58',
133
+ route: { type: 'RAIL', name: 'IC81' },
134
+ },
135
+ {
136
+ from: '8503000:0:34',
150
137
  to: '8503000:0:10',
151
138
  type: 'REQUIRES_MINIMAL_TIME',
152
139
  minTransferTime: '07:00',
@@ -154,8 +141,8 @@ const routes = [
154
141
  {
155
142
  from: '8503000:0:10',
156
143
  to: '8509002:0:2',
157
- departure: '10:38',
158
- arrival: '11:41',
144
+ departure: '10:07',
145
+ arrival: '11:11',
159
146
  route: { type: 'RAIL', name: 'IC3' },
160
147
  },
161
148
  {
@@ -166,10 +153,10 @@ const routes = [
166
153
  },
167
154
  {
168
155
  from: '8509002:0:6',
169
- to: '8509073:0:1',
170
- departure: '11:49',
171
- arrival: '13:03',
172
- route: { type: 'RAIL', name: 'RE24' },
156
+ to: '8509073:0:2',
157
+ departure: '11:20',
158
+ arrival: '12:27',
159
+ route: { type: 'RAIL', name: 'RE13' },
173
160
  },
174
161
  ],
175
162
  },
@@ -204,7 +191,7 @@ describe('E2E Tests for Transit Router', () => {
204
191
 
205
192
  const result = router.route(queryObject);
206
193
  const bestRoute = result.bestRoute(toStop.sourceStopId);
207
-
194
+ console.log();
208
195
  assert.ok(bestRoute, 'No route found');
209
196
  const actualRoute = bestRoute.asJson();
210
197
 
@@ -1,3 +1,3 @@
1
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:18eaba61a6014cc31041103c3bcc74a4bebd37c234781c57238113a1de61ffd1
3
- size 5147203
2
+ oid sha256:8147a3beb12e5dd65fe2bb5ed4013adaa0897ac84bd9722f53a7f38ebfaa9100
3
+ size 4542160
@@ -1,3 +1,3 @@
1
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b8c1539545653fd5a1e8d83f52f6b8383c8f1ed6087947105e3093334b046c0b
3
- size 17521380
2
+ oid sha256:f18f8795ee3dcb04accc3f372cde7041ec4c6d2812fee03e2a0c9de39d4149a6
3
+ size 18441262
@@ -409,6 +409,87 @@ describe('Result', () => {
409
409
  });
410
410
  });
411
411
 
412
+ describe('bestRouteToStopId', () => {
413
+ it('should return route when given a single StopId', () => {
414
+ const earliestArrivals = new Map([
415
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
416
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // destination
417
+ ]);
418
+
419
+ const vehicleEdge: VehicleEdge = {
420
+ arrival: Time.fromHMS(9, 0, 0),
421
+ from: 0,
422
+ to: 2,
423
+ routeId: 0,
424
+ tripIndex: 0,
425
+ };
426
+
427
+ const graph: Map<StopId, RoutingEdge>[] = [
428
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
429
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge]]), // Round 1
430
+ ];
431
+
432
+ const result = new Result(
433
+ mockQuery,
434
+ {
435
+ earliestArrivals,
436
+ graph,
437
+ destinations: [2],
438
+ },
439
+ mockStopsIndex,
440
+ mockTimetable,
441
+ );
442
+
443
+ const route = result.bestRouteToStopId(2); // Using StopId instead of SourceStopId
444
+ assert(route);
445
+ assert.strictEqual(route.legs.length, 1);
446
+ const firstLeg = route.legs[0];
447
+ assert(firstLeg);
448
+ assert.strictEqual(firstLeg.from.id, 0);
449
+ assert.strictEqual(firstLeg.to.id, 2);
450
+ });
451
+
452
+ it('should return route to closest destination when given a Set of StopIds', () => {
453
+ const earliestArrivals = new Map([
454
+ [0, { arrival: Time.fromHMS(8, 0, 0), legNumber: 0 }], // origin
455
+ [2, { arrival: Time.fromHMS(9, 0, 0), legNumber: 1 }], // destination (faster)
456
+ [3, { arrival: Time.fromHMS(9, 45, 0), legNumber: 1 }], // destination (slower)
457
+ ]);
458
+
459
+ const vehicleEdge: VehicleEdge = {
460
+ arrival: Time.fromHMS(9, 0, 0),
461
+ from: 0,
462
+ to: 2,
463
+ routeId: 0,
464
+ tripIndex: 0,
465
+ };
466
+
467
+ const graph: Map<StopId, RoutingEdge>[] = [
468
+ new Map<StopId, RoutingEdge>([[0, { arrival: Time.fromHMS(8, 0, 0) }]]), // Round 0 - origins
469
+ new Map<StopId, RoutingEdge>([[2, vehicleEdge]]), // Round 1
470
+ ];
471
+
472
+ const result = new Result(
473
+ mockQuery,
474
+ {
475
+ earliestArrivals,
476
+ graph,
477
+ destinations: [2, 3],
478
+ },
479
+ mockStopsIndex,
480
+ mockTimetable,
481
+ );
482
+
483
+ const route = result.bestRouteToStopId(new Set([2, 3])); // Using Set of StopIds
484
+ assert(route);
485
+ assert.strictEqual(route.legs.length, 1);
486
+ const firstLeg = route.legs[0];
487
+ assert(firstLeg);
488
+ assert.strictEqual(firstLeg.from.id, 0);
489
+ assert.strictEqual(firstLeg.to.id, 2); // Should route to stop 2 (faster arrival)
490
+ });
491
+ });
492
+
412
493
  describe('continuous trips', () => {
413
494
  it('should handle single continuous trip correctly', () => {
414
495
  const earliestArrivals = new Map([
@@ -23,6 +23,37 @@ export class Result {
23
23
  this.timetable = timetable;
24
24
  }
25
25
 
26
+ /**
27
+ * Reconstructs the best route to a stop by StopId.
28
+ * (to any stop reachable in less time / transfers than the destination(s) of the query)
29
+ *
30
+ * @param to The destination stop by StopId.
31
+ * @returns a route to the destination stop if it exists.
32
+ */
33
+ bestRouteToStopId(to: StopId | Set<StopId>): Route | undefined {
34
+ const sourceStopIds =
35
+ to instanceof Set
36
+ ? new Set(
37
+ Array.from(to)
38
+ .map(
39
+ (stopId) => this.stopsIndex.findStopById(stopId)?.sourceStopId,
40
+ )
41
+ .filter(
42
+ (sourceId): sourceId is SourceStopId => sourceId !== undefined,
43
+ ),
44
+ )
45
+ : this.stopsIndex.findStopById(to)?.sourceStopId;
46
+
47
+ if (
48
+ sourceStopIds === undefined ||
49
+ (sourceStopIds instanceof Set && sourceStopIds.size === 0)
50
+ ) {
51
+ return undefined;
52
+ }
53
+
54
+ return this.bestRoute(sourceStopIds);
55
+ }
56
+
26
57
  /**
27
58
  * Reconstructs the best route to a stop.
28
59
  * (to any stop reachable in less time / transfers than the destination(s) of the query)
@@ -37,23 +68,24 @@ export class Result {
37
68
  : to
38
69
  ? [to]
39
70
  : Array.from(this.query.to);
40
- const destinations = destinationList.flatMap((destination) =>
41
- this.stopsIndex.equivalentStops(destination),
42
- );
43
71
  // find the first reached destination
44
72
  let fastestDestination: StopId | undefined = undefined;
45
73
  let fastestTime: Arrival | undefined = undefined;
46
- for (const destination of destinations) {
47
- const arrivalTime = this.routingState.earliestArrivals.get(
48
- destination.id,
49
- );
50
- if (arrivalTime !== undefined) {
51
- if (
52
- fastestTime === undefined ||
53
- arrivalTime.arrival.isBefore(fastestTime.arrival)
54
- ) {
55
- fastestDestination = destination.id;
56
- fastestTime = arrivalTime;
74
+ for (const sourceDestination of destinationList) {
75
+ const equivalentStops =
76
+ this.stopsIndex.equivalentStops(sourceDestination);
77
+ for (const destination of equivalentStops) {
78
+ const arrivalTime = this.routingState.earliestArrivals.get(
79
+ destination.id,
80
+ );
81
+ if (arrivalTime !== undefined) {
82
+ if (
83
+ fastestTime === undefined ||
84
+ arrivalTime.arrival.isBefore(fastestTime.arrival)
85
+ ) {
86
+ fastestDestination = destination.id;
87
+ fastestTime = arrivalTime;
88
+ }
57
89
  }
58
90
  }
59
91
  }
@@ -170,4 +170,13 @@ describe('Stop Finder', () => {
170
170
  assert.deepStrictEqual(equivalentStops, []);
171
171
  });
172
172
  });
173
+
174
+ describe('iterator', () => {
175
+ it('should iterate over all stops', () => {
176
+ const stops = [...stopFinder];
177
+ assert.strictEqual(stops.length, 7);
178
+ assert.strictEqual(stops[0]?.id, 0);
179
+ assert.strictEqual(stops[6]?.id, 6);
180
+ });
181
+ });
173
182
  });
@@ -1,6 +1,6 @@
1
1
  // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
2
  // versions:
3
- // protoc-gen-ts_proto v2.7.7
3
+ // protoc-gen-ts_proto v2.10.1
4
4
  // protoc v4.23.4
5
5
  // source: src/stops/proto/stops.proto
6
6
 
@@ -11,7 +11,7 @@ import { SourceStopId, SourceStopsMap, Stop, StopId } from './stops.js';
11
11
  type StopPoint = { id: StopId; lat: number; lon: number };
12
12
 
13
13
  /**
14
- * The StopMap class provides functionality to search for public transport stops
14
+ * The StopsIndex class provides functionality to search for public transport stops
15
15
  * by name or geographic location. It leverages text search and geospatial indexing
16
16
  * to efficiently find stops based on user queries.
17
17
  */
@@ -187,4 +187,15 @@ export class StopsIndex {
187
187
  (stopId) => this.stops[stopId] as Stop,
188
188
  );
189
189
  }
190
+
191
+ /**
192
+ * Makes the StopsIndex iterable, allowing iteration over all stops.
193
+ *
194
+ * @returns An iterator for the stops.
195
+ */
196
+ *[Symbol.iterator](): Iterator<Stop> {
197
+ for (const stop of this.stops) {
198
+ yield stop;
199
+ }
200
+ }
190
201
  }
@@ -1,6 +1,6 @@
1
1
  // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
2
  // versions:
3
- // protoc-gen-ts_proto v2.7.7
3
+ // protoc-gen-ts_proto v2.10.1
4
4
  // protoc v4.23.4
5
5
  // source: src/timetable/proto/timetable.proto
6
6