Skip to content

Commit db5039d

Browse files
committed
Added RAPTOR algorithm planner
1 parent d8eab87 commit db5039d

12 files changed

Lines changed: 445 additions & 63 deletions

File tree

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
},
1616
"dependencies": {
1717
"cors": "~2.8.5",
18+
"dayjs": "~1.11.19",
1819
"express": "~5.2.1",
1920
"http-errors": "~2.0.1",
2021
"js-toml": "~1.0.2",

src/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createRoutesRouter } from "./controllers/routes.js";
1010
import { createTransfersRouter } from "./controllers/transfers.js";
1111
import { createServicesRouter } from "./controllers/services.js";
1212
import { createNotificationsRouter } from "./controllers/notifications.js";
13+
import { createPlannerRouter } from './controllers/planner.js';
1314

1415

1516
// Create the Winston logger
@@ -49,6 +50,7 @@ export function createApp(logger, feed) {
4950
app.use('/timetable/transfers', createTransfersRouter(app));
5051
app.use('/timetable/services', createServicesRouter(app));
5152
app.use('/timetable/notifications', createNotificationsRouter(app));
53+
app.use('/planner', createPlannerRouter(app));
5254

5355
// Add the error middleware to the app
5456
app.use(respondWithError(logger));

src/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dayjs from 'dayjs';
2+
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
13
import http from 'http';
24

35
import { createApp, createLogger } from './app.js'
@@ -7,6 +9,9 @@ import { FeedError } from './model/exception.js';
79

810
// Main function
911
async function main() {
12+
// Initialize Day.js
13+
dayjs.extend(customParseFormat);
14+
1015
// Create the logger
1116
const logger = createLogger();
1217

src/model/journey.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { RouteLeg } from './routeLeg.js';
2+
import { TransferLeg } from './transferLeg.js';
3+
4+
5+
// Class that defines a journey
6+
export class Journey
7+
{
8+
// Constructor
9+
constructor(legs, departureTime) {
10+
this.legs = legs;
11+
12+
this.departureTime = departureTime;
13+
this.arrivalTime = departureTime.add(legs.at(-1).cumulativeTime, 'seconds');
14+
this.duration = this.arrivalTime.diff(this.departureTime, 'seconds');
15+
16+
// Iterate over the legs
17+
for (let leg of this.legs) {
18+
// Check the type of the leg
19+
if (leg instanceof RouteLeg) {
20+
// Copy the route
21+
leg.route = leg.route._copy();
22+
23+
// Iterate over the stops of the route and set the journey time
24+
for (let stop of leg.route.stops)
25+
stop._journeyTime = this.departureTime.add(stop._cumulativeTime, 'seconds');
26+
} else if (leg instanceof TransferLeg) {
27+
// Copy the transfer
28+
leg.transfer = leg.transfer._copy();
29+
30+
// Set the journey time
31+
leg.transfer._journeyTime = this.departureTime.add(leg.transfer.duration, 'seconds');
32+
}
33+
}
34+
}
35+
36+
// Return the JSON representation of the route leg
37+
toJSON(options) {
38+
return {
39+
from: this.from.toJSON(options),
40+
to: this.to.toJSON(options),
41+
departureTime: this.departureTime.format('YYYY-MM-DD[T]HH:mm:ss'),
42+
formattedDepartureTime: this.departureTime.format('H:mm'),
43+
arrivalTime: this.arrivalTime.format('YYYY-MM-DD[T]HH:mm:ss'),
44+
formattedArrivalTime: this.arrivalTime.format('H:mm'),
45+
duration: this.duration,
46+
formattedDuration: `${Math.floor(this.duration / 3600)}:${Math.floor(this.duration % 3600 / 60).toString().padStart(2, '0')}`,
47+
transfers: this.transfers,
48+
legs: this.legs.map(leg => leg.toJSON(options)),
49+
};
50+
}
51+
52+
53+
// Return the departure node of the journey
54+
get from() {
55+
return this.legs.at(0).from;
56+
}
57+
58+
// Return the arrival node of the journey
59+
get to() {
60+
return this.legs.at(-1).to;
61+
}
62+
63+
// Return the amount of transfers of the journey
64+
get transfers() {
65+
return this.legs.filter(l => l instanceof RouteLeg).length - 1;
66+
}
67+
}

src/model/node.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ export class Node extends Record
7272

7373
// Add the stops to a route that stops at a node
7474
_addStopsToRoute(route) {
75-
return route.getStopsAtNode(this).map(stop => {
76-
const stopRoute = route._sliceBeginningAtSequence(stop.sequence);
77-
stopRoute.stopAtNode = stop;
78-
return stopRoute;
79-
});
75+
return route.getStopsAtNode(this)
76+
.map(stop => route._sliceBeginningAtSequence(stop.sequence, {_stopAtNode: stop}));
8077
}
8178
}

src/model/route.js

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ export class Route extends Record
1111

1212
if (!('name' in data))
1313
throw new FeedError(`Missing required field "name" in route with id "${this.id}"`);
14+
if (!('headsign' in data))
15+
throw new FeedError(`Missing required field "headsign" in route with id "${this.id}"`);
1416
if (!('agency' in data))
1517
throw new FeedError(`Missing required field "agency" in route with id "${this.id}"`);
1618
if (!('modality' in data))
1719
throw new FeedError(`Missing required field "modality" in route with id "${this.id}"`);
18-
if (!('headsign' in data))
19-
throw new FeedError(`Missing required field "modality" in route with id "${this.id}"`);
2020
if (!('stops' in data))
2121
throw new FeedError(`Missing required field "stops" in route with id "${this.id}"`);
2222

@@ -32,8 +32,8 @@ export class Route extends Record
3232
this.stops = data.stops;
3333
this.visible = data.visible ?? true;
3434

35-
this.stopAtNode = undefined;
36-
this.initialTime = data.initialTime ?? 0;
35+
this._stopAtNode = data._stopAtNode ?? undefined;
36+
this._initialTime = data._initialTime ?? 0;
3737
}
3838

3939
// Finalize loading the route
@@ -42,22 +42,16 @@ export class Route extends Record
4242
this.stops = this.stops.map(s => s._copy());
4343

4444
// Calculate cumulative time for the stops of the route
45-
let lastHeadsignSequence = null;
45+
let lastHeadsignStopSequence = null;
4646
for (let [index, stop] of this.stops.entries()) {
47-
// Set the flags of the first and last stops
48-
stop.first = index === 0;
49-
stop.last = index === this.stops.length - 1;
50-
51-
// Set the time of the first stop
52-
stop.time = index > 0 ? stop.time : 0;
47+
// Set the duration and cumulative time of the stop
48+
stop.duration = index > 0 ? stop.duration : 0;
49+
stop._cumulativeTime = (index > 0 ? this.stops[index - 1]._cumulativeTime : this._initialTime) + stop.duration;
5350

54-
// Calculate cumulative time for the stop
55-
stop.cumulativeTime = (index > 0 ? this.stops[index - 1].cumulativeTime : this.initialTime) + stop.time;
56-
57-
// Set the actual headsign sequence of the stop
51+
// Set the headsign stop sequence of the stop
5852
if (stop.headsign !== null)
59-
lastHeadsignSequence = stop.sequence;
60-
stop._actualHeadsignSequence = lastHeadsignSequence;
53+
lastHeadsignStopSequence = stop.sequence;
54+
stop._headsignStopSequence = lastHeadsignStopSequence;
6155
}
6256
}
6357

@@ -66,7 +60,7 @@ export class Route extends Record
6660
return {
6761
id: this.id,
6862
name: this._feed.applyTranslation(this, 'name', options?.language),
69-
headsign: this._feed.applyTranslation(this, ['stops', this.stopAtNode?._actualHeadsignSequence, 'headsign'], options?.language)
63+
headsign: this._feed.applyTranslation(this, ['stops', this._stopAtNode?._actualHeadsignSequence, 'headsign'], options?.language)
7064
?? this._feed.applyTranslation(this, 'headsign', options?.language),
7165
abbr: this._feed.applyTranslation(this, 'abbr', options?.language),
7266
description: this._feed.applyTranslation(this, 'description', options?.language),
@@ -76,8 +70,9 @@ export class Route extends Record
7670
modality: this.modality.toJSON(options),
7771
icon: this.icon,
7872
stops: this.stops.map(stop => stop.toJSON(options)),
79-
stopAtNode: this.stopAtNode?.toJSON(options),
8073
visible: this.visible,
74+
75+
stopAtNode: this._stopAtNode?.toJSON(options),
8176
};
8277
}
8378

@@ -122,31 +117,26 @@ export class Route extends Record
122117
}
123118

124119
// Slice the route to begin at the specified sequence
125-
_sliceBeginningAtSequence(seqence) {
120+
_sliceBeginningAtSequence(seqence, modifiedProps = {}) {
126121
let index = this.getStopIndexWithSequence(seqence);
127-
return index > -1 ? this._copy({stops: this.stops.slice(index)}) : this._copy();
122+
return index > -1 ? this._copy({...modifiedProps, stops: this.stops.slice(index)}) : this._copy(modifiedProps);
128123
}
129124

130125
// Slice the route to end at the specified sequence
131-
_sliceEndingAtSequence(seqence) {
126+
_sliceEndingAtSequence(seqence, modifiedProps = {}) {
132127
let index = this.getStopIndexWithSequence(seqence);
133-
return index > -1 ? this._copy({stops: this.stops.slice(0, index + 1)}) : this._copy();
128+
return index > -1 ? this._copy({...modifiedProps, stops: this.stops.slice(0, index + 1)}) : this._copy(modifiedProps);
134129
}
135130

136131
// Slice the route to begin at the specified node
137-
_sliceBeginningAtNode(node) {
132+
_sliceBeginningAtNode(node, modifiedProps = {}) {
138133
let index = this.getStopIndexAtNode(node);
139-
return index > -1 ? this._copy({stops: this.stops.slice(index)}) : this._copy();
134+
return index > -1 ? this._copy({...modifiedProps, stops: this.stops.slice(index)}) : this._copy(modifiedProps);
140135
}
141136

142137
// Slice the route to end at the specified node
143-
_sliceEndingAtNode(node) {
138+
_sliceEndingAtNode(node, modifiedProps = {}) {
144139
let index = this.getStopIndexAtNode(node);
145-
return index > -1 ? this._copy({stops: this.stops.slice(0, index + 1)}) : this._copy();
146-
}
147-
148-
// Apply an initial time to the route
149-
_withInitialTime(initialTime) {
150-
return this._copy({initialTime});
140+
return index > -1 ? this._copy({...modifiedProps, stops: this.stops.slice(0, index + 1)}) : this._copy(modifiedProps);
151141
}
152142
}

src/model/routeLeg.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Class that defines a route leg of a journey
2+
export class RouteLeg
3+
{
4+
// Constructor
5+
constructor(route) {
6+
this.route = route;
7+
}
8+
9+
// Return the JSON representation of the route leg
10+
toJSON(options) {
11+
return {
12+
type: 'route',
13+
from: this.from,
14+
to: this.to,
15+
route: this.route.toJSON(options),
16+
};
17+
}
18+
19+
20+
// Return the departure node of the route
21+
get from() {
22+
return this.route.stops.at(0).node;
23+
}
24+
25+
// Return the arrival node of the route
26+
get to() {
27+
return this.route.stops.at(-1).node;
28+
}
29+
30+
// Return the cumulative time of the route
31+
get cumulativeTime() {
32+
return this.route.stops.at(-1)._cumulativeTime;
33+
}
34+
}

src/model/routeStop.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,63 @@ export class RouteStop
88
this.sequence = parseInt(sequence);
99

1010
if (!('node' in data))
11-
throw new FeedError(`Missing required field "node" in route stop with sequence ${this.sequence}`);
11+
throw new FeedError(`Missing required field "node" in route stop with sequence ${this.sequence} in route with id "${this._route.id}"`);
12+
if (!('duration' in data))
13+
throw new FeedError(`Missing required field "duration" in route stop with sequence ${this.sequence} in route with id "${this._route.id}"`);
1214

1315
this.node = data.node;
14-
this.time = data.time ?? 0;
16+
this.duration = data.duration;
1517
this.halts = data.halts ?? true;
1618
this.platform = data.platform ?? null;
1719
this.headsign = data.headsign ?? null;
1820
this.alightDirection = data.alightDirection ?? null;
1921
this.status = data.status ?? null;
2022

21-
this.first = false;
22-
this.last = false;
23-
this.cumulativeTime = 0;
24-
this.actualHeadsignSequence = undefined;
23+
this._headsignStopSequence = undefined;
24+
this._cumulativeTime = 0;
25+
this._journeyTime = undefined;
2526
}
2627

2728
// Return the JSON representation of the route stop
2829
toJSON(options) {
2930
return {
3031
sequence: this.sequence,
3132
node: this.node.toJSON(options),
32-
time: this.time,
33+
duration: this.duration,
34+
formattedDuration: `${Math.floor(this.duration / 3600)}:${Math.floor(this.duration % 3600 / 60).toString().padStart(2, '0')}`,
3335
halts: this.halts,
3436
platform: this.platform,
35-
headsign: this.actualHeadsignSequence !== undefined
36-
? this._feed.applyTranslation(this._route.getStopWithSequence(this.actualHeadsignSequence), 'headsign', options?.language)
37-
: this._feed.applyTranslation(this._route, 'headsign', options?.language),
37+
headsign: this._feed.applyTranslation(this._headsignStopSequence !== undefined ? this.headsignStop : this._route, 'headsign', options?.language),
3838
alightDirection: this.alightDirection,
3939
status: this.status ?? null,
40+
41+
journeyTime: this._journeyTime?.format('YYYY-MM-DD[T]HH:mm:ss'),
42+
formattedJourneyTime: this._journeyTime?.format('H:mm'),
4043
};
4144
}
4245

4346

47+
// Return if this stop is the first stop of the route
48+
get isFirstStop() {
49+
return this === this._route.stops.at(0);
50+
}
51+
52+
// Return if this stop is the first stop of the route
53+
get isLastStop() {
54+
return this === this._route.stops.at(-1);
55+
}
56+
57+
// Return the stop that contains the headsign for this stop
58+
get headsignStop() {
59+
if (this._headsignStopSequence === undefined)
60+
return undefined;
61+
else if (this._headsignStopSequence === this.sequence)
62+
return this;
63+
else
64+
return this._route.getStopWithSequence(this._headsignStopSequence);
65+
}
66+
67+
4468
// Copy the stop
4569
_copy(modifiedProps = {}) {
4670
return new RouteStop(this._feed, this._route, this.sequence, {...this, modifiedProps});

0 commit comments

Comments
 (0)