Implement out-of-route navigation

This commit is contained in:
Mattéo Delabre 2021-05-16 12:01:51 +02:00
parent ff84c7c93b
commit 5f38c6c64e
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
7 changed files with 40883 additions and 594 deletions

View File

@ -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 ."

View File

@ -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.

View File

@ -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, its 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 cant be
if (route === null) {
return false;
}
// If there is another stop in the way, it cant be either
for (const node of route.slice(1, -1)) {
if (node in network.stops) {
return false;
}
}
// Otherwise, assume its 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]);
}
} }
} }

View File

@ -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 };
}; };

File diff suppressed because it is too large Load Diff

40
src/tam/routing.js Normal file
View File

@ -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;
};

View File

@ -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. */