Implement out-of-route navigation
This commit is contained in:
parent
ff84c7c93b
commit
5f38c6c64e
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"back": "node src/back",
|
"back": "node --experimental-json-modules src/back",
|
||||||
"front:dev": "vite src/front",
|
"front:dev": "vite src/front",
|
||||||
"front:prod": "vite build src/front",
|
"front:prod": "vite build src/front",
|
||||||
"lint": "eslint ."
|
"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 * as courses from "../src/tam/courses.js";
|
||||||
import {displayTime} from '../src/util.js';
|
import network from "../src/tam/network.json";
|
||||||
import process from 'process';
|
import {displayTime} from "../src/util.js";
|
||||||
import path from 'path';
|
import process from "process";
|
||||||
import { readFile } from 'fs/promises';
|
import path from "path";
|
||||||
|
|
||||||
const network = JSON.parse(await readFile(
|
|
||||||
new URL('../src/tam/network.json', import.meta.url)
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert stop ID to human-readable stop name.
|
* Convert stop ID to human-readable stop name.
|
||||||
|
|
|
@ -6,11 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as tam from "./sources/tam.js";
|
import * as tam from "./sources/tam.js";
|
||||||
import { readFile } from 'fs/promises';
|
import * as routing from "./routing.js";
|
||||||
|
import network from "./network.json";
|
||||||
const network = JSON.parse(await readFile(
|
|
||||||
new URL('./network.json', import.meta.url)
|
|
||||||
));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about the course of a vehicle.
|
* 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. */
|
/** 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 [hours, minutes, seconds] = time.split(':').map(x => parseInt(x, 10));
|
||||||
const result = new Date(reference);
|
const result = new Date(reference);
|
||||||
|
|
||||||
|
@ -41,6 +37,32 @@ const parseTime = (time, reference) =>
|
||||||
return result;
|
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.
|
* Fetch information about courses in the TaM network.
|
||||||
*
|
*
|
||||||
|
@ -127,7 +149,9 @@ export const fetch = async (kind = 'realtime') => {
|
||||||
if (course.finalStopId === undefined) {
|
if (course.finalStopId === undefined) {
|
||||||
course.finalStopId = lastPassing[0];
|
course.finalStopId = lastPassing[0];
|
||||||
} else if (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
|
// Retrieve routes, ways and stops from OpenStreetMap
|
||||||
const rawData = await osm.runQuery(`[out:json];
|
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("|")})$"];
|
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;
|
out body qt;
|
||||||
`);
|
`);
|
||||||
|
@ -47,20 +57,25 @@ out body qt;
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// All stops in the network
|
// Graph for out-of-route navigation
|
||||||
|
const navigation = {};
|
||||||
|
|
||||||
|
// Stops in the network indexed by reference
|
||||||
const stops = {};
|
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 = {};
|
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 = {};
|
const segments = {};
|
||||||
|
|
||||||
|
// Extract lines, associated stops and planned routes
|
||||||
for (const routeMaster of routeMasters) {
|
for (const routeMaster of routeMasters) {
|
||||||
const lineRef = routeMaster.tags.ref;
|
const lineRef = routeMaster.tags.ref;
|
||||||
const color = routeMaster.tags.colour || "#000000";
|
const color = routeMaster.tags.colour || "#000000";
|
||||||
|
|
||||||
// Extract all routes for the given line
|
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
|
||||||
for (const [routeRef, data] of routeMaster.members.entries()) {
|
for (const [routeRef, data] of routeMaster.members.entries()) {
|
||||||
|
@ -87,8 +102,9 @@ a “ref” tag`);
|
||||||
], {
|
], {
|
||||||
name: stop.tags.name,
|
name: stop.tags.name,
|
||||||
routes: [[lineRef, routeRef]],
|
routes: [[lineRef, routeRef]],
|
||||||
successors: []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stopsReverse[ref] = stop.tags.ref;
|
||||||
} else {
|
} else {
|
||||||
stops[stop.tags.ref].properties.routes.push([
|
stops[stop.tags.ref].properties.routes.push([
|
||||||
lineRef,
|
lineRef,
|
||||||
|
@ -163,13 +179,11 @@ but there are ${nextNodeCandidates.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (curNodeIndex < nextNodeIndex) {
|
if (curNodeIndex < nextNodeIndex) {
|
||||||
|
|
||||||
// Use the way in its normal direction
|
// Use the way in its normal direction
|
||||||
path = path.concat(
|
path = path.concat(
|
||||||
wayNodes.slice(curNodeIndex, nextNodeIndex)
|
wayNodes.slice(curNodeIndex, nextNodeIndex)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Use the way in the reverse direction
|
// Use the way in the reverse direction
|
||||||
if (osm.isOneWay(way)) {
|
if (osm.isOneWay(way)) {
|
||||||
throw new Error(`Way n°${wayIndex} in
|
throw new Error(`Way n°${wayIndex} in
|
||||||
|
@ -215,14 +229,12 @@ different sequence of nodes in two or more lines.`);
|
||||||
elements[nodeId].lat
|
elements[nodeId].lat
|
||||||
]
|
]
|
||||||
), {
|
), {
|
||||||
|
|
||||||
// Keep track of the original sequence of nodes to
|
// Keep track of the original sequence of nodes to
|
||||||
// compare with duplicates
|
// compare with duplicates
|
||||||
nodesIds,
|
nodesIds,
|
||||||
routes: [[lineRef, routeRef]]
|
routes: [[lineRef, routeRef]]
|
||||||
});
|
});
|
||||||
|
|
||||||
stops[begin].properties.successors.push(end);
|
|
||||||
segments[id].properties.length = (
|
segments[id].properties.length = (
|
||||||
1000 * turfLength(segments[id]));
|
1000 * turfLength(segments[id]));
|
||||||
}
|
}
|
||||||
|
@ -248,5 +260,47 @@ different sequence of nodes in two or more lines.`);
|
||||||
delete segment.properties.nodesIds;
|
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 turfLength from "@turf/length";
|
||||||
import * as turfHelpers from "@turf/helpers";
|
import * as turfHelpers from "@turf/helpers";
|
||||||
import * as turfProjection from "@turf/projection";
|
import * as turfProjection from "@turf/projection";
|
||||||
|
import * as routing from "./routing.js";
|
||||||
import network from "./network.json";
|
import network from "./network.json";
|
||||||
|
|
||||||
const server = "http://localhost:4321";
|
const server = "http://localhost:4321";
|
||||||
|
@ -81,6 +82,7 @@ class Course {
|
||||||
|
|
||||||
const name = `${this.departureStop}-${this.arrivalStop}`;
|
const name = `${this.departureStop}-${this.arrivalStop}`;
|
||||||
|
|
||||||
|
// Use predefined segment if it exists
|
||||||
if (name in network.segments) {
|
if (name in network.segments) {
|
||||||
this.segment = network.segments[name];
|
this.segment = network.segments[name];
|
||||||
return;
|
return;
|
||||||
|
@ -98,11 +100,21 @@ class Course {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.segment = turfHelpers.lineString([
|
// Compute a custom route between two stops
|
||||||
network.stops[this.departureStop].geometry.coordinates,
|
const route = routing.findRoute(this.departureStop, this.arrivalStop);
|
||||||
network.stops[this.arrivalStop].geometry.coordinates,
|
|
||||||
]);
|
if (route === null) {
|
||||||
this.segment.properties.length = turfLength(this.segment);
|
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. */
|
/** Merge data received from the server. */
|
||||||
|
|
Loading…
Reference in New Issue