Implement out-of-route navigation
This commit is contained in:
parent
ff84c7c93b
commit
5f38c6c64e
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"back": "node src/back",
|
||||
"back": "node --experimental-json-modules src/back",
|
||||
"front:dev": "vite src/front",
|
||||
"front:prod": "vite build src/front",
|
||||
"lint": "eslint ."
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S node --experimental-json-modules
|
||||
|
||||
import * as courses from '../src/tam/courses.js';
|
||||
import {displayTime} from '../src/util.js';
|
||||
import process from 'process';
|
||||
import path from 'path';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
const network = JSON.parse(await readFile(
|
||||
new URL('../src/tam/network.json', import.meta.url)
|
||||
));
|
||||
import * as courses from "../src/tam/courses.js";
|
||||
import network from "../src/tam/network.json";
|
||||
import {displayTime} from "../src/util.js";
|
||||
import process from "process";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Convert stop ID to human-readable stop name.
|
||||
|
|
|
@ -6,11 +6,8 @@
|
|||
*/
|
||||
|
||||
import * as tam from "./sources/tam.js";
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
const network = JSON.parse(await readFile(
|
||||
new URL('./network.json', import.meta.url)
|
||||
));
|
||||
import * as routing from "./routing.js";
|
||||
import network from "./network.json";
|
||||
|
||||
/**
|
||||
* Information about the course of a vehicle.
|
||||
|
@ -24,8 +21,7 @@ const network = JSON.parse(await readFile(
|
|||
*/
|
||||
|
||||
/** Parse time information relative to the current date. */
|
||||
const parseTime = (time, reference) =>
|
||||
{
|
||||
const parseTime = (time, reference) => {
|
||||
const [hours, minutes, seconds] = time.split(':').map(x => parseInt(x, 10));
|
||||
const result = new Date(reference);
|
||||
|
||||
|
@ -41,6 +37,32 @@ const parseTime = (time, reference) =>
|
|||
return result;
|
||||
};
|
||||
|
||||
/** Guess whether a stop comes directly before another one. */
|
||||
const isPenultimateStop = (stop, finalStop) => {
|
||||
// If there is a standard segment linking both stops, it’s certainly
|
||||
// the penultimate stop
|
||||
if ((stop + "-" + finalStop) in network.segments) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const route = routing.findRoute(stop, finalStop);
|
||||
|
||||
// If there is no way to link both stops, it can’t be
|
||||
if (route === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is another stop in the way, it can’t be either
|
||||
for (const node of route.slice(1, -1)) {
|
||||
if (node in network.stops) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, assume it’s the penultimate stop
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch information about courses in the TaM network.
|
||||
*
|
||||
|
@ -127,7 +149,9 @@ export const fetch = async (kind = 'realtime') => {
|
|||
if (course.finalStopId === undefined) {
|
||||
course.finalStopId = lastPassing[0];
|
||||
} else if (course.finalStopId !== lastPassing[0]) {
|
||||
course.passings.push([course.finalStopId, lastPassing[1] + 60000]);
|
||||
if (isPenultimateStop(lastPassing[0], course.finalStopId)) {
|
||||
course.passings.push([course.finalStopId, lastPassing[1] + 60000]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,11 +26,21 @@ export const fetch = async lineRefs => {
|
|||
// Retrieve routes, ways and stops from OpenStreetMap
|
||||
const rawData = await osm.runQuery(`[out:json];
|
||||
|
||||
// Find the public transport line bearing the requested reference
|
||||
// Find the public transport lines bearing the requested references
|
||||
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
|
||||
|
||||
// Recursively fetch routes, ways and stops inside the line
|
||||
(._; >>;);
|
||||
(
|
||||
._;
|
||||
|
||||
// Recursively fetch routes, ways and stops inside the lines
|
||||
>>;
|
||||
|
||||
// Find adjacent tracks (used for out-of-route navigation)
|
||||
complete {
|
||||
way(around:0)[railway="tram"];
|
||||
};
|
||||
>;
|
||||
);
|
||||
|
||||
out body qt;
|
||||
`);
|
||||
|
@ -47,20 +57,25 @@ out body qt;
|
|||
return prev;
|
||||
}, {});
|
||||
|
||||
// All stops in the network
|
||||
// Graph for out-of-route navigation
|
||||
const navigation = {};
|
||||
|
||||
// Stops in the network indexed by reference
|
||||
const stops = {};
|
||||
|
||||
// All transport lines of the network
|
||||
// Stops indexed by OSM identifier
|
||||
const stopsReverse = {};
|
||||
|
||||
// Transport lines of the network indexed by name
|
||||
const lines = {};
|
||||
|
||||
// All segments leading from one stop to another
|
||||
// Segments leading from stop to stop in the planned route for each line
|
||||
const segments = {};
|
||||
|
||||
// Extract lines, associated stops and planned routes
|
||||
for (const routeMaster of routeMasters) {
|
||||
const lineRef = routeMaster.tags.ref;
|
||||
const color = routeMaster.tags.colour || "#000000";
|
||||
|
||||
// Extract all routes for the given line
|
||||
const routes = [];
|
||||
|
||||
for (const [routeRef, data] of routeMaster.members.entries()) {
|
||||
|
@ -87,8 +102,9 @@ a “ref” tag`);
|
|||
], {
|
||||
name: stop.tags.name,
|
||||
routes: [[lineRef, routeRef]],
|
||||
successors: []
|
||||
});
|
||||
|
||||
stopsReverse[ref] = stop.tags.ref;
|
||||
} else {
|
||||
stops[stop.tags.ref].properties.routes.push([
|
||||
lineRef,
|
||||
|
@ -163,13 +179,11 @@ but there are ${nextNodeCandidates.length}`);
|
|||
}
|
||||
|
||||
if (curNodeIndex < nextNodeIndex) {
|
||||
|
||||
// Use the way in its normal direction
|
||||
path = path.concat(
|
||||
wayNodes.slice(curNodeIndex, nextNodeIndex)
|
||||
);
|
||||
} else {
|
||||
|
||||
// Use the way in the reverse direction
|
||||
if (osm.isOneWay(way)) {
|
||||
throw new Error(`Way n°${wayIndex} in
|
||||
|
@ -215,14 +229,12 @@ different sequence of nodes in two or more lines.`);
|
|||
elements[nodeId].lat
|
||||
]
|
||||
), {
|
||||
|
||||
// Keep track of the original sequence of nodes to
|
||||
// compare with duplicates
|
||||
nodesIds,
|
||||
routes: [[lineRef, routeRef]]
|
||||
});
|
||||
|
||||
stops[begin].properties.successors.push(end);
|
||||
segments[id].properties.length = (
|
||||
1000 * turfLength(segments[id]));
|
||||
}
|
||||
|
@ -248,5 +260,47 @@ different sequence of nodes in two or more lines.`);
|
|||
delete segment.properties.nodesIds;
|
||||
}
|
||||
|
||||
return { stops, lines, segments };
|
||||
// Create out-of-route navigation graph
|
||||
const navigationId = objId => (
|
||||
objId in stopsReverse
|
||||
? stopsReverse[objId]
|
||||
: objId.toString()
|
||||
);
|
||||
|
||||
for (const obj of elementsList) {
|
||||
if (obj.type === "node") {
|
||||
const id = navigationId(obj.id);
|
||||
|
||||
navigation[id] = {
|
||||
// Position of this node
|
||||
lon: obj.lon,
|
||||
lat: obj.lat,
|
||||
|
||||
// List of other nodes that can be accessed from this node
|
||||
successors: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const obj of elementsList) {
|
||||
if (obj.type === "way") {
|
||||
const oneWay = osm.isOneWay(obj);
|
||||
const pairs = obj.nodes.slice(0, -1).map(
|
||||
(node, i) => [
|
||||
navigationId(node),
|
||||
navigationId(obj.nodes[i + 1]),
|
||||
]
|
||||
);
|
||||
|
||||
for (const [from, to] of pairs) {
|
||||
navigation[from].successors.push(to);
|
||||
|
||||
if (!oneWay) {
|
||||
navigation[to].successors.push(from);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { navigation, stops, lines, segments };
|
||||
};
|
||||
|
|
41275
src/tam/network.json
41275
src/tam/network.json
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
import network from "./network.json";
|
||||
|
||||
/**
|
||||
* Find a route linking two nodes or stops.
|
||||
* @param {string} from ID of the starting stop or node.
|
||||
* @param {string} to ID of the ending stop or node.
|
||||
* @return {lineString?} If it exists, a segment linking the two nodes that has
|
||||
* the least number of nodes possible.
|
||||
*/
|
||||
export const findRoute = (from, to) => {
|
||||
const queue = [from];
|
||||
const parent = {from: from};
|
||||
|
||||
while (queue.length > 0 && !(to in parent)) {
|
||||
const current = queue.shift();
|
||||
|
||||
for (const successor of network.navigation[current].successors) {
|
||||
if (!(successor in parent)) {
|
||||
queue.push(successor);
|
||||
parent[successor] = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(to in parent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = [];
|
||||
let current = to;
|
||||
|
||||
while (current !== from) {
|
||||
path.push(current);
|
||||
current = parent[current];
|
||||
}
|
||||
|
||||
path.push(from);
|
||||
path.reverse();
|
||||
return path;
|
||||
};
|
|
@ -3,6 +3,7 @@ import turfAlong from "@turf/along";
|
|||
import turfLength from "@turf/length";
|
||||
import * as turfHelpers from "@turf/helpers";
|
||||
import * as turfProjection from "@turf/projection";
|
||||
import * as routing from "./routing.js";
|
||||
import network from "./network.json";
|
||||
|
||||
const server = "http://localhost:4321";
|
||||
|
@ -81,6 +82,7 @@ class Course {
|
|||
|
||||
const name = `${this.departureStop}-${this.arrivalStop}`;
|
||||
|
||||
// Use predefined segment if it exists
|
||||
if (name in network.segments) {
|
||||
this.segment = network.segments[name];
|
||||
return;
|
||||
|
@ -98,11 +100,21 @@ class Course {
|
|||
return;
|
||||
}
|
||||
|
||||
this.segment = turfHelpers.lineString([
|
||||
network.stops[this.departureStop].geometry.coordinates,
|
||||
network.stops[this.arrivalStop].geometry.coordinates,
|
||||
]);
|
||||
this.segment.properties.length = turfLength(this.segment);
|
||||
// Compute a custom route between two stops
|
||||
const route = routing.findRoute(this.departureStop, this.arrivalStop);
|
||||
|
||||
if (route === null) {
|
||||
console.warn(`No route from ${this.departureStop} \
|
||||
to ${this.arrivalStop}`);
|
||||
}
|
||||
|
||||
this.segment = turfHelpers.lineString(
|
||||
route.map(node => [
|
||||
network.navigation[node].lon,
|
||||
network.navigation[node].lat,
|
||||
])
|
||||
);
|
||||
this.segment.properties.length = 1000 * turfLength(this.segment);
|
||||
}
|
||||
|
||||
/** Merge data received from the server. */
|
||||
|
|
Loading…
Reference in New Issue