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

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

View File

@ -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, 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.
*
@ -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]);
}
}
}

View File

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

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