minotor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell.json +43 -0
- package/.czrc +3 -0
- package/.editorconfig +10 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +4 -0
- package/.github/workflows/minotor.yml +85 -0
- package/.prettierrc +7 -0
- package/.releaserc.json +27 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/bundle.cjs.js +16507 -0
- package/dist/bundle.cjs.js.map +1 -0
- package/dist/bundle.esm.js +16496 -0
- package/dist/bundle.esm.js.map +1 -0
- package/dist/bundle.umd.js +2 -0
- package/dist/bundle.umd.js.map +1 -0
- package/dist/cli/__tests__/minotor.test.d.ts +1 -0
- package/dist/cli/minotor.d.ts +5 -0
- package/dist/cli/repl.d.ts +1 -0
- package/dist/cli/utils.d.ts +3 -0
- package/dist/cli.mjs +20504 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/gtfs/__tests__/parser.test.d.ts +1 -0
- package/dist/gtfs/__tests__/routes.test.d.ts +1 -0
- package/dist/gtfs/__tests__/services.test.d.ts +1 -0
- package/dist/gtfs/__tests__/stops.test.d.ts +1 -0
- package/dist/gtfs/__tests__/time.test.d.ts +1 -0
- package/dist/gtfs/__tests__/transfers.test.d.ts +1 -0
- package/dist/gtfs/__tests__/trips.test.d.ts +1 -0
- package/dist/gtfs/__tests__/utils.test.d.ts +1 -0
- package/dist/gtfs/parser.d.ts +34 -0
- package/dist/gtfs/profiles/__tests__/ch.test.d.ts +1 -0
- package/dist/gtfs/profiles/ch.d.ts +2 -0
- package/dist/gtfs/profiles/standard.d.ts +2 -0
- package/dist/gtfs/routes.d.ts +11 -0
- package/dist/gtfs/services.d.ts +19 -0
- package/dist/gtfs/stops.d.ts +20 -0
- package/dist/gtfs/time.d.ts +17 -0
- package/dist/gtfs/transfers.d.ts +22 -0
- package/dist/gtfs/trips.d.ts +26 -0
- package/dist/gtfs/utils.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/routing/__tests__/router.test.d.ts +1 -0
- package/dist/routing/plotter.d.ts +11 -0
- package/dist/routing/query.d.ts +35 -0
- package/dist/routing/result.d.ts +28 -0
- package/dist/routing/route.d.ts +25 -0
- package/dist/routing/router.d.ts +33 -0
- package/dist/stops/__tests__/io.test.d.ts +1 -0
- package/dist/stops/__tests__/stopFinder.test.d.ts +1 -0
- package/dist/stops/i18n.d.ts +10 -0
- package/dist/stops/io.d.ts +4 -0
- package/dist/stops/proto/stops.d.ts +53 -0
- package/dist/stops/stops.d.ts +16 -0
- package/dist/stops/stopsIndex.d.ts +52 -0
- package/dist/timetable/__tests__/io.test.d.ts +1 -0
- package/dist/timetable/__tests__/timetable.test.d.ts +1 -0
- package/dist/timetable/duration.d.ts +51 -0
- package/dist/timetable/io.d.ts +8 -0
- package/dist/timetable/proto/timetable.d.ts +122 -0
- package/dist/timetable/time.d.ts +98 -0
- package/dist/timetable/timetable.d.ts +82 -0
- package/dist/umdIndex.d.ts +9 -0
- package/eslint.config.mjs +52 -0
- package/package.json +109 -0
- package/rollup.config.js +44 -0
- package/src/cli/__tests__/minotor.test.ts +23 -0
- package/src/cli/minotor.ts +112 -0
- package/src/cli/repl.ts +200 -0
- package/src/cli/utils.ts +36 -0
- package/src/gtfs/__tests__/parser.test.ts +591 -0
- package/src/gtfs/__tests__/resources/sample-feed/agency.txt +2 -0
- package/src/gtfs/__tests__/resources/sample-feed/calendar.txt +3 -0
- package/src/gtfs/__tests__/resources/sample-feed/calendar_dates.txt +2 -0
- package/src/gtfs/__tests__/resources/sample-feed/fare_attributes.txt +3 -0
- package/src/gtfs/__tests__/resources/sample-feed/fare_rules.txt +5 -0
- package/src/gtfs/__tests__/resources/sample-feed/frequencies.txt +12 -0
- package/src/gtfs/__tests__/resources/sample-feed/routes.txt +6 -0
- package/src/gtfs/__tests__/resources/sample-feed/sample-feed.zip +0 -0
- package/src/gtfs/__tests__/resources/sample-feed/shapes.txt +1 -0
- package/src/gtfs/__tests__/resources/sample-feed/stop_times.txt +34 -0
- package/src/gtfs/__tests__/resources/sample-feed/stops.txt +10 -0
- package/src/gtfs/__tests__/resources/sample-feed/trips.txt +13 -0
- package/src/gtfs/__tests__/resources/sample-feed.zip +0 -0
- package/src/gtfs/__tests__/routes.test.ts +63 -0
- package/src/gtfs/__tests__/services.test.ts +209 -0
- package/src/gtfs/__tests__/stops.test.ts +177 -0
- package/src/gtfs/__tests__/time.test.ts +27 -0
- package/src/gtfs/__tests__/transfers.test.ts +117 -0
- package/src/gtfs/__tests__/trips.test.ts +463 -0
- package/src/gtfs/__tests__/utils.test.ts +13 -0
- package/src/gtfs/parser.ts +154 -0
- package/src/gtfs/profiles/__tests__/ch.test.ts +43 -0
- package/src/gtfs/profiles/ch.ts +70 -0
- package/src/gtfs/profiles/standard.ts +39 -0
- package/src/gtfs/routes.ts +48 -0
- package/src/gtfs/services.ts +98 -0
- package/src/gtfs/stops.ts +112 -0
- package/src/gtfs/time.ts +33 -0
- package/src/gtfs/transfers.ts +102 -0
- package/src/gtfs/trips.ts +228 -0
- package/src/gtfs/utils.ts +42 -0
- package/src/index.ts +28 -0
- package/src/routing/__tests__/router.test.ts +760 -0
- package/src/routing/plotter.ts +70 -0
- package/src/routing/query.ts +74 -0
- package/src/routing/result.ts +108 -0
- package/src/routing/route.ts +94 -0
- package/src/routing/router.ts +262 -0
- package/src/stops/__tests__/io.test.ts +43 -0
- package/src/stops/__tests__/stopFinder.test.ts +185 -0
- package/src/stops/i18n.ts +40 -0
- package/src/stops/io.ts +94 -0
- package/src/stops/proto/stops.proto +26 -0
- package/src/stops/proto/stops.ts +445 -0
- package/src/stops/stops.ts +24 -0
- package/src/stops/stopsIndex.ts +151 -0
- package/src/timetable/__tests__/io.test.ts +175 -0
- package/src/timetable/__tests__/timetable.test.ts +180 -0
- package/src/timetable/duration.ts +85 -0
- package/src/timetable/io.ts +265 -0
- package/src/timetable/proto/timetable.proto +76 -0
- package/src/timetable/proto/timetable.ts +1304 -0
- package/src/timetable/time.ts +192 -0
- package/src/timetable/timetable.ts +286 -0
- package/src/umdIndex.ts +14 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { StopId } from '../stops/stops.js';
|
|
2
|
+
import { Result } from './result.js';
|
|
3
|
+
import { TripLeg } from './router.js';
|
|
4
|
+
|
|
5
|
+
export class Plotter {
|
|
6
|
+
private result: Result;
|
|
7
|
+
|
|
8
|
+
constructor(result: Result) {
|
|
9
|
+
this.result = result;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Plots the path three as a DOT for debugging purposes.
|
|
14
|
+
*
|
|
15
|
+
* @returns A string representing the DOT graph of the path tree.
|
|
16
|
+
*/
|
|
17
|
+
plotDotGraph(): string {
|
|
18
|
+
const earliestArrivalsPerRound: Map<StopId, TripLeg>[] =
|
|
19
|
+
this.result.earliestArrivalsPerRound;
|
|
20
|
+
|
|
21
|
+
const dotParts: string[] = [
|
|
22
|
+
'digraph PathTree {',
|
|
23
|
+
' graph [overlap=false];',
|
|
24
|
+
' node [shape=ellipse style=filled fillcolor=lightgrey];',
|
|
25
|
+
];
|
|
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
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
dotParts.push('}');
|
|
68
|
+
return dotParts.join('\n');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { StopId } from '../stops/stops.js';
|
|
2
|
+
import { Duration } from '../timetable/duration.js';
|
|
3
|
+
import { Time } from '../timetable/time.js';
|
|
4
|
+
import { ALL_TRANSPORT_MODES, RouteType } from '../timetable/timetable.js';
|
|
5
|
+
|
|
6
|
+
export class Query {
|
|
7
|
+
from: StopId;
|
|
8
|
+
to: StopId[];
|
|
9
|
+
departureTime: Time;
|
|
10
|
+
lastDepartureTime?: Time;
|
|
11
|
+
options: {
|
|
12
|
+
maxTransfers: number;
|
|
13
|
+
minTransferTime: Duration;
|
|
14
|
+
transportModes: RouteType[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
constructor(builder: typeof Query.Builder.prototype) {
|
|
18
|
+
this.from = builder.fromValue;
|
|
19
|
+
this.to = builder.toValue;
|
|
20
|
+
this.departureTime = builder.departureTimeValue;
|
|
21
|
+
this.options = builder.optionsValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static Builder = class {
|
|
25
|
+
fromValue!: StopId;
|
|
26
|
+
toValue!: StopId[];
|
|
27
|
+
departureTimeValue!: Time;
|
|
28
|
+
// lastDepartureTimeValue?: Date;
|
|
29
|
+
// via: StopId[] = [];
|
|
30
|
+
optionsValue: {
|
|
31
|
+
maxTransfers: number;
|
|
32
|
+
minTransferTime: Duration;
|
|
33
|
+
transportModes: RouteType[];
|
|
34
|
+
} = {
|
|
35
|
+
maxTransfers: 5,
|
|
36
|
+
minTransferTime: Duration.fromSeconds(120),
|
|
37
|
+
transportModes: ALL_TRANSPORT_MODES,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
from(from: StopId): this {
|
|
41
|
+
this.fromValue = from;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
to(to: StopId | StopId[]): this {
|
|
46
|
+
this.toValue = Array.isArray(to) ? to : [to];
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
departureTime(departureTime: Time): this {
|
|
51
|
+
this.departureTimeValue = departureTime;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
maxTransfers(maxTransfers: number): this {
|
|
56
|
+
this.optionsValue.maxTransfers = maxTransfers;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
minTransferTime(minTransferTime: Duration): this {
|
|
61
|
+
this.optionsValue.minTransferTime = minTransferTime;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
transportModes(transportModes: RouteType[]): this {
|
|
66
|
+
this.optionsValue.transportModes = transportModes;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
build(): Query {
|
|
71
|
+
return new Query(this);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { StopId } from '../stops/stops.js';
|
|
2
|
+
import { StopsIndex } from '../umdIndex.js';
|
|
3
|
+
import { Query } from './query.js';
|
|
4
|
+
import { Leg, Route } from './route.js';
|
|
5
|
+
import { ReachingTime, TripLeg } from './router.js';
|
|
6
|
+
|
|
7
|
+
export class Result {
|
|
8
|
+
private readonly query: Query;
|
|
9
|
+
private readonly earliestArrivals: Map<StopId, ReachingTime>;
|
|
10
|
+
public readonly earliestArrivalsPerRound: Map<StopId, TripLeg>[];
|
|
11
|
+
private readonly stopsIndex: StopsIndex;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
query: Query,
|
|
15
|
+
earliestArrivals: Map<StopId, ReachingTime>,
|
|
16
|
+
earliestArrivalsPerRound: Map<StopId, TripLeg>[],
|
|
17
|
+
stopsIndex: StopsIndex,
|
|
18
|
+
) {
|
|
19
|
+
this.query = query;
|
|
20
|
+
this.earliestArrivals = earliestArrivals;
|
|
21
|
+
this.earliestArrivalsPerRound = earliestArrivalsPerRound;
|
|
22
|
+
this.stopsIndex = stopsIndex;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reconstructs the best route to a stop.
|
|
27
|
+
* (to any stop reachable in less time / transfers than the destination(s) of the query)
|
|
28
|
+
*
|
|
29
|
+
* @param to The destination stop. Defaults to the destination of the original query.
|
|
30
|
+
* @returns a route to the destination stop if it exists.
|
|
31
|
+
*/
|
|
32
|
+
bestRoute(to?: StopId | StopId[]): Route | undefined {
|
|
33
|
+
const destinationList = Array.isArray(to) ? to : to ? [to] : this.query.to;
|
|
34
|
+
const destinations = destinationList.flatMap((destination) =>
|
|
35
|
+
this.stopsIndex.equivalentStops(destination),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
let fastestDestination: StopId | undefined = undefined;
|
|
39
|
+
let fastestTime: ReachingTime | undefined = undefined;
|
|
40
|
+
for (const destination of destinations) {
|
|
41
|
+
const arrivalTime = this.earliestArrivals.get(destination);
|
|
42
|
+
if (arrivalTime !== undefined) {
|
|
43
|
+
if (
|
|
44
|
+
fastestTime === undefined ||
|
|
45
|
+
arrivalTime.time.toSeconds() < fastestTime.time.toSeconds()
|
|
46
|
+
) {
|
|
47
|
+
fastestDestination = destination;
|
|
48
|
+
fastestTime = arrivalTime;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!fastestDestination || !fastestTime) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const route: Leg[] = [];
|
|
57
|
+
let currentStop = fastestDestination;
|
|
58
|
+
let round = fastestTime.legNumber;
|
|
59
|
+
while (fastestTime.origin !== currentStop) {
|
|
60
|
+
const tripLeg = this.earliestArrivalsPerRound[round]?.get(currentStop);
|
|
61
|
+
if (!tripLeg?.leg) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`No leg found for a trip leg: start stop=${
|
|
64
|
+
tripLeg?.leg?.from.id ?? 'unknown'
|
|
65
|
+
}, end stop=${currentStop}, round=${round}, origin=${fastestTime.origin}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
route.unshift(tripLeg.leg);
|
|
69
|
+
currentStop = tripLeg.leg.from.id;
|
|
70
|
+
if ('route' in tripLeg.leg) {
|
|
71
|
+
round -= 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return new Route(route);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the arrival time at any stop reachable in less time / transfers than the destination(s) of the query)
|
|
79
|
+
*
|
|
80
|
+
* @param stop The target stop for which to return the arrival time.
|
|
81
|
+
* @param maxTransfers The optional maximum number of transfers allowed.
|
|
82
|
+
* @returns The arrival time if the target stop is reachable, otherwise undefined.
|
|
83
|
+
*/
|
|
84
|
+
arrivalAt(stop: StopId, maxTransfers?: number): ReachingTime | undefined {
|
|
85
|
+
const equivalentStops = this.stopsIndex.equivalentStops(stop);
|
|
86
|
+
let earliestArrival: ReachingTime | undefined = undefined;
|
|
87
|
+
|
|
88
|
+
const relevantArrivals =
|
|
89
|
+
maxTransfers !== undefined
|
|
90
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
91
|
+
this.earliestArrivalsPerRound[maxTransfers - 1]!
|
|
92
|
+
: this.earliestArrivals;
|
|
93
|
+
|
|
94
|
+
for (const equivalentStop of equivalentStops) {
|
|
95
|
+
const arrivalTime = relevantArrivals.get(equivalentStop);
|
|
96
|
+
if (arrivalTime !== undefined) {
|
|
97
|
+
if (
|
|
98
|
+
earliestArrival === undefined ||
|
|
99
|
+
arrivalTime.time.toSeconds() < earliestArrival.time.toSeconds()
|
|
100
|
+
) {
|
|
101
|
+
earliestArrival = arrivalTime;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return earliestArrival;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Stop } from '../stops/stops.js';
|
|
2
|
+
import { Duration } from '../timetable/duration.js';
|
|
3
|
+
import { Time } from '../timetable/time.js';
|
|
4
|
+
import { ServiceRoute } from '../timetable/timetable.js';
|
|
5
|
+
|
|
6
|
+
export type BaseLeg = {
|
|
7
|
+
from: Stop;
|
|
8
|
+
to: Stop;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Transfer = BaseLeg & {
|
|
12
|
+
minTransferTime?: Duration;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type VehicleLeg = BaseLeg & {
|
|
16
|
+
route: ServiceRoute;
|
|
17
|
+
departureTime: Time;
|
|
18
|
+
arrivalTime: Time;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Leg = Transfer | VehicleLeg;
|
|
22
|
+
|
|
23
|
+
export class Route {
|
|
24
|
+
legs: Leg[];
|
|
25
|
+
|
|
26
|
+
constructor(legs: Leg[]) {
|
|
27
|
+
if (legs.length === 0) {
|
|
28
|
+
throw new Error('There must be at least one leg in a route');
|
|
29
|
+
}
|
|
30
|
+
this.legs = legs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
departureTime(): Time {
|
|
34
|
+
const cumulativeTransferTime: Duration = Duration.zero();
|
|
35
|
+
for (let i = 0; i < this.legs.length; i++) {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
37
|
+
const leg = this.legs[i]!;
|
|
38
|
+
if ('departureTime' in leg) {
|
|
39
|
+
return leg.departureTime.minus(cumulativeTransferTime);
|
|
40
|
+
}
|
|
41
|
+
if ('minTransferTime' in leg && leg.minTransferTime) {
|
|
42
|
+
cumulativeTransferTime.add(leg.minTransferTime);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error('No vehicle leg found in route');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
arrivalTime(): Time {
|
|
49
|
+
let lastVehicleArrivalTime: Time = Time.origin();
|
|
50
|
+
const totalTransferTime: Duration = Duration.zero();
|
|
51
|
+
let vehicleLegFound = false;
|
|
52
|
+
|
|
53
|
+
for (let i = this.legs.length - 1; i >= 0; i--) {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
55
|
+
const leg = this.legs[i]!;
|
|
56
|
+
|
|
57
|
+
if ('arrivalTime' in leg && !vehicleLegFound) {
|
|
58
|
+
lastVehicleArrivalTime = leg.arrivalTime;
|
|
59
|
+
vehicleLegFound = true;
|
|
60
|
+
} else if (
|
|
61
|
+
'minTransferTime' in leg &&
|
|
62
|
+
leg.minTransferTime &&
|
|
63
|
+
vehicleLegFound
|
|
64
|
+
) {
|
|
65
|
+
totalTransferTime.add(leg.minTransferTime);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!vehicleLegFound) {
|
|
70
|
+
throw new Error('No vehicle leg found in route');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lastVehicleArrivalTime.plus(totalTransferTime);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
totalDuration(): Duration {
|
|
77
|
+
if (this.legs.length === 0) return Duration.zero();
|
|
78
|
+
return this.arrivalTime().diff(this.departureTime());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
print(): string {
|
|
82
|
+
return this.legs
|
|
83
|
+
.map((leg, index) => {
|
|
84
|
+
if ('route' in leg) {
|
|
85
|
+
return `Leg ${index + 1}: ${leg.from.name} to ${leg.to.name}
|
|
86
|
+
via route ${leg.route.type} ${leg.route.name},
|
|
87
|
+
departs at ${leg.departureTime.toString()}, arrives at ${leg.arrivalTime.toString()}`;
|
|
88
|
+
}
|
|
89
|
+
return `Leg ${index + 1}: Transfer from ${leg.from.name} to ${leg.to.name},
|
|
90
|
+
minimum transfer time: ${leg.minTransferTime?.toString() ?? 'not specified'}`;
|
|
91
|
+
})
|
|
92
|
+
.join('\n');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { StopId } from '../stops/stops.js';
|
|
3
|
+
import { Duration } from '../timetable/duration.js';
|
|
4
|
+
import { Time } from '../timetable/time.js';
|
|
5
|
+
import { Timetable } from '../timetable/timetable.js';
|
|
6
|
+
import { StopsIndex } from '../umdIndex.js';
|
|
7
|
+
import { Query } from './query.js';
|
|
8
|
+
import { Result } from './result.js';
|
|
9
|
+
import { Leg } from './route.js';
|
|
10
|
+
|
|
11
|
+
const UNREACHED = Time.infinity();
|
|
12
|
+
|
|
13
|
+
export type TripLeg = ReachingTime & {
|
|
14
|
+
leg?: Leg; // leg is not set for the very first segment
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ReachingTime = {
|
|
18
|
+
time: Time;
|
|
19
|
+
legNumber: number;
|
|
20
|
+
origin: StopId;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CurrentTrip = {
|
|
24
|
+
trip: number;
|
|
25
|
+
origin: StopId;
|
|
26
|
+
bestHopOnStop: StopId;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Router {
|
|
30
|
+
private readonly timetable: Timetable;
|
|
31
|
+
private readonly stopsIndex: StopsIndex;
|
|
32
|
+
|
|
33
|
+
constructor(timetable: Timetable, stopsIndex: StopsIndex) {
|
|
34
|
+
this.timetable = timetable;
|
|
35
|
+
this.stopsIndex = stopsIndex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Evaluates possible transfers for a given query on a transport
|
|
40
|
+
* network, updating the earliest arrivals at various stops and marking new
|
|
41
|
+
* stops that can be reached through these transfers.
|
|
42
|
+
*/
|
|
43
|
+
private considerTransfers(
|
|
44
|
+
query: Query,
|
|
45
|
+
markedStops: Set<StopId>,
|
|
46
|
+
arrivalsAtCurrentRound: Map<StopId, TripLeg>,
|
|
47
|
+
earliestArrivals: Map<StopId, ReachingTime>,
|
|
48
|
+
round: number,
|
|
49
|
+
): void {
|
|
50
|
+
const { options } = query;
|
|
51
|
+
const newlyMarkedStops: Set<StopId> = new Set();
|
|
52
|
+
for (const stop of markedStops) {
|
|
53
|
+
for (const transfer of this.timetable.getTransfers(stop)) {
|
|
54
|
+
let transferTime: Duration;
|
|
55
|
+
if (transfer.minTransferTime) {
|
|
56
|
+
transferTime = transfer.minTransferTime;
|
|
57
|
+
} else if (transfer.type === 'IN_SEAT') {
|
|
58
|
+
transferTime = Duration.zero();
|
|
59
|
+
} else {
|
|
60
|
+
transferTime = options.minTransferTime;
|
|
61
|
+
}
|
|
62
|
+
const arrivalAfterTransfer = arrivalsAtCurrentRound
|
|
63
|
+
.get(stop)!
|
|
64
|
+
.time.plus(transferTime);
|
|
65
|
+
const originalArrival =
|
|
66
|
+
arrivalsAtCurrentRound.get(transfer.destination)?.time ?? UNREACHED;
|
|
67
|
+
if (arrivalAfterTransfer.toSeconds() < originalArrival.toSeconds()) {
|
|
68
|
+
const origin = arrivalsAtCurrentRound.get(stop)?.origin ?? stop;
|
|
69
|
+
arrivalsAtCurrentRound.set(transfer.destination, {
|
|
70
|
+
time: arrivalAfterTransfer,
|
|
71
|
+
legNumber: round,
|
|
72
|
+
origin: origin,
|
|
73
|
+
leg: {
|
|
74
|
+
from: this.stopsIndex.findStopById(stop)!,
|
|
75
|
+
to: this.stopsIndex.findStopById(transfer.destination)!,
|
|
76
|
+
minTransferTime: transfer.minTransferTime,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
earliestArrivals.set(transfer.destination, {
|
|
80
|
+
time: arrivalAfterTransfer,
|
|
81
|
+
legNumber: round,
|
|
82
|
+
origin: origin,
|
|
83
|
+
});
|
|
84
|
+
newlyMarkedStops.add(transfer.destination);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const newStop of newlyMarkedStops) {
|
|
89
|
+
markedStops.add(newStop);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The main Raptor algorithm implementation.
|
|
95
|
+
*
|
|
96
|
+
* @param query The query containing the main parameters for the routing.
|
|
97
|
+
* @returns A result object containing data structures allowing to reconstruct routes and .
|
|
98
|
+
*/
|
|
99
|
+
route(query: Query): Result {
|
|
100
|
+
const { from, to, departureTime, options } = query;
|
|
101
|
+
// Consider children or siblings of the "from" stop as potential origins
|
|
102
|
+
const origins = this.stopsIndex.equivalentStops(from);
|
|
103
|
+
// Consider children or siblings of the "to" stop(s) as potential destinations
|
|
104
|
+
const destinations = to.flatMap((destination) =>
|
|
105
|
+
this.stopsIndex.equivalentStops(destination),
|
|
106
|
+
);
|
|
107
|
+
const earliestArrivals = new Map<StopId, ReachingTime>();
|
|
108
|
+
|
|
109
|
+
const earliestArrivalsWithoutAnyLeg = new Map<StopId, TripLeg>();
|
|
110
|
+
const earliestArrivalsPerRound = [earliestArrivalsWithoutAnyLeg];
|
|
111
|
+
// Stops that have been improved at round k-1
|
|
112
|
+
const markedStops = new Set<StopId>();
|
|
113
|
+
|
|
114
|
+
for (const originStop of origins) {
|
|
115
|
+
markedStops.add(originStop);
|
|
116
|
+
earliestArrivals.set(originStop, {
|
|
117
|
+
time: departureTime,
|
|
118
|
+
legNumber: 0,
|
|
119
|
+
origin: originStop,
|
|
120
|
+
});
|
|
121
|
+
earliestArrivalsWithoutAnyLeg.set(originStop, {
|
|
122
|
+
time: departureTime,
|
|
123
|
+
legNumber: 0,
|
|
124
|
+
origin: originStop,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// on the first round we need to first consider transfers to discover all possible route origins
|
|
128
|
+
this.considerTransfers(
|
|
129
|
+
query,
|
|
130
|
+
markedStops,
|
|
131
|
+
earliestArrivalsWithoutAnyLeg,
|
|
132
|
+
earliestArrivals,
|
|
133
|
+
0,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (let round = 1; round <= options.maxTransfers + 1; round++) {
|
|
137
|
+
const arrivalsAtCurrentRound = new Map<StopId, TripLeg>();
|
|
138
|
+
earliestArrivalsPerRound.push(arrivalsAtCurrentRound);
|
|
139
|
+
const arrivalsAtPreviousRound = earliestArrivalsPerRound[round - 1]!;
|
|
140
|
+
// Routes that contain at least one stop reached with at least round - 1 legs
|
|
141
|
+
// together with corresponding hop on stop index (earliest marked stop)
|
|
142
|
+
const reachableRoutes = this.timetable.findReachableRoutes(
|
|
143
|
+
markedStops,
|
|
144
|
+
options.transportModes,
|
|
145
|
+
);
|
|
146
|
+
markedStops.clear();
|
|
147
|
+
// for each route that can be reached with at least round - 1 trips
|
|
148
|
+
for (const [routeId, hopOnStop] of reachableRoutes.entries()) {
|
|
149
|
+
const route = this.timetable.getRoute(routeId)!;
|
|
150
|
+
let currentTrip: CurrentTrip | undefined = undefined;
|
|
151
|
+
const hopOnIndex = route.stopIndices.get(hopOnStop)!;
|
|
152
|
+
// for each stops in the route starting with the hop-on one
|
|
153
|
+
for (let i = hopOnIndex; i < route.stops.length; i++) {
|
|
154
|
+
const currentStop = route.stops[i]!;
|
|
155
|
+
const stopNumbers = route.stops.length;
|
|
156
|
+
if (currentTrip !== undefined) {
|
|
157
|
+
const currentStopTimes =
|
|
158
|
+
route.stopTimes[currentTrip.trip * stopNumbers + i]!;
|
|
159
|
+
const earliestArrivalAtCurrentStop =
|
|
160
|
+
earliestArrivals.get(currentStop)?.time ?? UNREACHED;
|
|
161
|
+
let arrivalToImprove = earliestArrivalAtCurrentStop;
|
|
162
|
+
if (destinations.length > 0) {
|
|
163
|
+
const earliestArrivalsAtDestinations: Time[] = [];
|
|
164
|
+
// if multiple destinations are specified, the target pruning
|
|
165
|
+
// should compare to the earliest arrival at any of them
|
|
166
|
+
for (const destinationStop of destinations) {
|
|
167
|
+
const earliestArrivalAtDestination =
|
|
168
|
+
earliestArrivals.get(destinationStop)?.time ?? UNREACHED;
|
|
169
|
+
earliestArrivalsAtDestinations.push(
|
|
170
|
+
earliestArrivalAtDestination,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const earliestArrivalAtDestination = Time.min(
|
|
174
|
+
...earliestArrivalsAtDestinations,
|
|
175
|
+
);
|
|
176
|
+
arrivalToImprove = Time.min(
|
|
177
|
+
earliestArrivalAtCurrentStop,
|
|
178
|
+
earliestArrivalAtDestination,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
currentStopTimes.dropOffType !== 'NOT_AVAILABLE' &&
|
|
183
|
+
currentStopTimes.arrival.toSeconds() <
|
|
184
|
+
arrivalToImprove.toSeconds()
|
|
185
|
+
) {
|
|
186
|
+
const bestHopOnStopIndex = route.stopIndices.get(
|
|
187
|
+
currentTrip.bestHopOnStop,
|
|
188
|
+
)!;
|
|
189
|
+
const bestHopOnStopTimes =
|
|
190
|
+
route.stopTimes[
|
|
191
|
+
currentTrip.trip * stopNumbers + bestHopOnStopIndex
|
|
192
|
+
]!;
|
|
193
|
+
arrivalsAtCurrentRound.set(currentStop, {
|
|
194
|
+
time: currentStopTimes.arrival,
|
|
195
|
+
legNumber: round,
|
|
196
|
+
origin: currentTrip.origin,
|
|
197
|
+
leg: {
|
|
198
|
+
from: this.stopsIndex.findStopById(
|
|
199
|
+
currentTrip.bestHopOnStop,
|
|
200
|
+
)!,
|
|
201
|
+
to: this.stopsIndex.findStopById(currentStop)!,
|
|
202
|
+
departureTime: bestHopOnStopTimes.departure,
|
|
203
|
+
arrivalTime: currentStopTimes.arrival,
|
|
204
|
+
route: this.timetable.getServiceRoute(route.serviceRouteId),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
earliestArrivals.set(currentStop, {
|
|
208
|
+
time: currentStopTimes.arrival,
|
|
209
|
+
legNumber: round,
|
|
210
|
+
origin: currentTrip.origin,
|
|
211
|
+
});
|
|
212
|
+
markedStops.add(currentStop);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// check if we can catch a previous trip at the current stop
|
|
216
|
+
// if there was no current trip, find the first one reachable
|
|
217
|
+
const earliestArrivalOnPreviousRound =
|
|
218
|
+
arrivalsAtPreviousRound.get(currentStop)?.time;
|
|
219
|
+
if (
|
|
220
|
+
earliestArrivalOnPreviousRound !== undefined &&
|
|
221
|
+
(currentTrip === undefined ||
|
|
222
|
+
earliestArrivalOnPreviousRound.toSeconds() <=
|
|
223
|
+
route.stopTimes[
|
|
224
|
+
currentTrip.trip * stopNumbers + i
|
|
225
|
+
]!.departure.toSeconds())
|
|
226
|
+
) {
|
|
227
|
+
const earliestTrip = this.timetable.findEarliestTrip(
|
|
228
|
+
route,
|
|
229
|
+
currentStop,
|
|
230
|
+
currentTrip?.trip,
|
|
231
|
+
earliestArrivalOnPreviousRound,
|
|
232
|
+
);
|
|
233
|
+
if (earliestTrip !== undefined) {
|
|
234
|
+
currentTrip = {
|
|
235
|
+
trip: earliestTrip,
|
|
236
|
+
// we need to keep track of the best hop-on stop to reconstruct the route at the end
|
|
237
|
+
bestHopOnStop: currentStop,
|
|
238
|
+
origin:
|
|
239
|
+
arrivalsAtPreviousRound.get(currentStop)?.origin ??
|
|
240
|
+
currentStop,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.considerTransfers(
|
|
247
|
+
query,
|
|
248
|
+
markedStops,
|
|
249
|
+
arrivalsAtCurrentRound,
|
|
250
|
+
earliestArrivals,
|
|
251
|
+
round,
|
|
252
|
+
);
|
|
253
|
+
if (markedStops.size === 0) break;
|
|
254
|
+
}
|
|
255
|
+
return new Result(
|
|
256
|
+
query,
|
|
257
|
+
earliestArrivals,
|
|
258
|
+
earliestArrivalsPerRound,
|
|
259
|
+
this.stopsIndex,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { deserializeStopsMap, serializeStopsMap } from '../io.js';
|
|
5
|
+
import { StopsMap } from '../stops.js';
|
|
6
|
+
|
|
7
|
+
describe('stops io', () => {
|
|
8
|
+
const stopsMap: StopsMap = new Map([
|
|
9
|
+
[
|
|
10
|
+
'stop1',
|
|
11
|
+
{
|
|
12
|
+
id: 'stop1',
|
|
13
|
+
name: 'Stop 1',
|
|
14
|
+
lat: 40.712776,
|
|
15
|
+
lon: -74.005974,
|
|
16
|
+
children: ['stop2'],
|
|
17
|
+
parent: 'parentStop',
|
|
18
|
+
locationType: 'SIMPLE_STOP_OR_PLATFORM',
|
|
19
|
+
platform: 'Platform 1',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
'stop2',
|
|
24
|
+
{
|
|
25
|
+
id: 'stop2',
|
|
26
|
+
name: 'Stop 2',
|
|
27
|
+
lat: 34.052235,
|
|
28
|
+
lon: -118.243683,
|
|
29
|
+
children: [],
|
|
30
|
+
parent: 'stop1',
|
|
31
|
+
locationType: 'STATION',
|
|
32
|
+
platform: 'Platform 2',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
it('should serialize and deserialize stops correctly', () => {
|
|
38
|
+
const serializedData = serializeStopsMap(stopsMap);
|
|
39
|
+
const deserializedStopsMap = deserializeStopsMap(serializedData);
|
|
40
|
+
|
|
41
|
+
assert.deepStrictEqual(deserializedStopsMap, stopsMap);
|
|
42
|
+
});
|
|
43
|
+
});
|