Improve network extraction and graph simplification + add docs
This commit is contained in:
parent
ab5e892bdd
commit
f569d1302c
|
@ -1915,6 +1915,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||||
},
|
},
|
||||||
|
"dijkstrajs": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs="
|
||||||
|
},
|
||||||
"doctrine": {
|
"doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"color": "^3.1.3",
|
"color": "^3.1.3",
|
||||||
"csv-parse": "^4.15.4",
|
"csv-parse": "^4.15.4",
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ol": "^6.5.0",
|
"ol": "^6.5.0",
|
||||||
"unzip-stream": "^0.3.1",
|
"unzip-stream": "^0.3.1",
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/env -S node --experimental-json-modules
|
||||||
|
|
||||||
|
import network from "../src/tam/network.json";
|
||||||
|
|
||||||
|
console.log("digraph {");
|
||||||
|
console.log("graph[layout=fdp, outputorder=nodesfirst]");
|
||||||
|
|
||||||
|
for (const [stopId, stop] of Object.entries(network.stops)) {
|
||||||
|
console.log(`${stopId}[label="${stop.properties.name}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const junctions = new Set();
|
||||||
|
|
||||||
|
for (const [beginId, begin] of Object.entries(network.navigation)) {
|
||||||
|
if (!(beginId in network.stops)) {
|
||||||
|
junctions.add(beginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const endId in begin) {
|
||||||
|
if (!(endId in network.stops)) {
|
||||||
|
junctions.add(endId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const junction of junctions) {
|
||||||
|
console.log(`${junction}[label="", shape=point]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [beginId, begin] of Object.entries(network.navigation)) {
|
||||||
|
for (const [endId, end] of Object.entries(begin)) {
|
||||||
|
const len = end.properties.length;
|
||||||
|
console.log(`${beginId} -> ${endId}[len=${len}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("}");
|
|
@ -45,7 +45,7 @@ const isPenultimateStop = (stop, finalStop) => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = routing.findRoute(stop, finalStop);
|
const route = routing.findPath(stop, finalStop);
|
||||||
|
|
||||||
// If there is no way to link both stops, it can’t be
|
// If there is no way to link both stops, it can’t be
|
||||||
if (route === null) {
|
if (route === null) {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
* Functions in this file also report inconsistencies in OSM data.
|
* Functions in this file also report inconsistencies in OSM data.
|
||||||
*
|
*
|
||||||
* Because of the static nature of this data, it is cached in a
|
* Because of the static nature of this data, it is cached in a
|
||||||
* version-controlled file `network.json` next to this file. To update it, use
|
* version-controlled file `network.json` next to this file. To update it,
|
||||||
* the `script/update-network` script.
|
* run the `script/update-network.js` script.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as turfHelpers from "@turf/helpers";
|
import * as turfHelpers from "@turf/helpers";
|
||||||
|
@ -17,14 +17,91 @@ import * as util from "../util.js";
|
||||||
import * as osm from "./sources/osm.js";
|
import * as osm from "./sources/osm.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch stops and lines of the network.
|
* Route of a line of a transport network.
|
||||||
* @param {string[]} lineRefs List of lines to fetch.
|
* @typedef {Object} Route
|
||||||
* @returns {{stops: Object, lines: Object, segments: Object}} Set of stops,
|
* @property {string} from Name of the starting point of the route.
|
||||||
* segments and lines.
|
* @property {string} to Name of the ending point of the route.
|
||||||
|
* @property {string?} via Optional name of a major intermediate
|
||||||
|
* stop of the route.
|
||||||
|
* @property {string} name Human-readable name of the line.
|
||||||
*/
|
*/
|
||||||
export const fetch = async lineRefs => {
|
|
||||||
// Retrieve routes, ways and stops from OpenStreetMap
|
/**
|
||||||
const rawData = await osm.runQuery(`[out:json];
|
* Line of a transport network.
|
||||||
|
* @typedef {Object} Line
|
||||||
|
* @property {string} color Hexadecimal color code of this line.
|
||||||
|
* @property {Array.<Route>} routes Routes of this line.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop in a transport network (as a GeoJSON feature point).
|
||||||
|
* @typedef {Object} Stop
|
||||||
|
* @property {string} type Always equal to "Feature".
|
||||||
|
* @property {string} id Stop identifier (unique in each network.).
|
||||||
|
* @property {Object} properties
|
||||||
|
* @property {string} properties.name Human-readable name of the stop.
|
||||||
|
* @property {string} properties.node Associated node ID in OpenStreetMap.
|
||||||
|
* @property {Array.<Array.[string,number]>} properties.routes
|
||||||
|
* List of transport lines using this stop (as pairs of
|
||||||
|
* line and route identifiers).
|
||||||
|
* @property {Object} geometry
|
||||||
|
* @property {string} geometry.type Always equal to "Point"
|
||||||
|
* @property {Array.<number>} geometry.coordinates
|
||||||
|
* Longitude and latitude of the stop point.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planned segment routing between two stops of a transport network
|
||||||
|
* (as a GeoJSON feature line string).
|
||||||
|
* @typedef {Object} Segment
|
||||||
|
* @property {string} type Always equal to "Feature".
|
||||||
|
* @property {string} id Segment identifier (format: `{begin}-{end}`).
|
||||||
|
* @property {Object} properties
|
||||||
|
* @property {string} properties.begin ID of the stop at the beginning.
|
||||||
|
* @property {string} properties.end ID of the stop at the end.
|
||||||
|
* @property {number} properties.length Length of this segment (meters).
|
||||||
|
* @property {Array.<Array.[string,number]>} properties.routes
|
||||||
|
* List of transport lines using this segment (as pairs of
|
||||||
|
* line identifiers and line direction numbers).
|
||||||
|
* @property {Object} geometry
|
||||||
|
* @property {string} geometry.type Always equal to "LineString".
|
||||||
|
* @property {Array.<Array.<number>>} geometry.coordinates
|
||||||
|
* Sequence of points forming this segment (as longitude/latitude pairs).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge of the graph for out-of-route navigation between stops.
|
||||||
|
* @typedef {Object} NavigationEdge
|
||||||
|
* @property {string} type Always equal to "Feature".
|
||||||
|
* @property {Object} properties
|
||||||
|
* @property {string} properties.begin ID of the stop or node at the beginning.
|
||||||
|
* @property {string} properties.end ID of the stop or node at the end.
|
||||||
|
* @property {number} properties.length Length of this edge (meters).
|
||||||
|
* @property {Object} geometry
|
||||||
|
* @property {string} geometry.type Always equal to "LineString".
|
||||||
|
* @property {Array.<Array.<number>>} geometry.coordinates
|
||||||
|
* Sequence of points forming this edge (as longitude/latitude pairs).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a public transport network.
|
||||||
|
* @typedef {Object} Network
|
||||||
|
* @property {Object.<string,Stop>} stops List of stops.
|
||||||
|
* @property {Object.<string,Line>} lines List of lines.
|
||||||
|
* @property {Object.<string,Segment>} segments List of segments.
|
||||||
|
* @property {Object.<string,Object.<string,NavigationEdge>>} navigation
|
||||||
|
* Graph for out-of-route navigation between stops.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve raw routes, ways and stops from OpenStreetMap for the given
|
||||||
|
* transport lines.
|
||||||
|
* @param {Array.<string>} lineRefs List of lines to fetch.
|
||||||
|
* @return {Array.<Object>} List of objects returned by OSM.
|
||||||
|
*/
|
||||||
|
// Retrieve routes, ways and stops from OpenStreetMap
|
||||||
|
const queryLines = async lineRefs => {
|
||||||
|
return (await osm.runQuery(`[out:json];
|
||||||
|
|
||||||
// Find the public transport lines bearing the requested references
|
// 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("|")})$"];
|
||||||
|
@ -43,35 +120,23 @@ relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
|
||||||
);
|
);
|
||||||
|
|
||||||
out body qt;
|
out body qt;
|
||||||
`);
|
`)).elements;
|
||||||
|
};
|
||||||
|
|
||||||
// List of retrieved objects
|
/**
|
||||||
const elementsList = rawData.elements;
|
* Assemble information about lines, stops and segments from the raw
|
||||||
|
* OpenStreetMap data.
|
||||||
// List of retrieved lines
|
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
|
||||||
const routeMasters = elementsList.filter(osm.isTransportLine);
|
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
|
||||||
|
* @return {Object} Assembled information about lines, stops and segments.
|
||||||
// Retrieved objects indexed by ID
|
*/
|
||||||
const elements = elementsList.reduce((prev, elt) => {
|
const processRoutes = (elementsList, elementsById) => {
|
||||||
prev[elt.id] = elt;
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Graph for out-of-route navigation
|
|
||||||
const navigation = {};
|
|
||||||
|
|
||||||
// Stops in the network indexed by reference
|
|
||||||
const stops = {};
|
|
||||||
|
|
||||||
// Stops indexed by OSM identifier
|
|
||||||
const stopsReverse = {};
|
|
||||||
|
|
||||||
// Transport lines of the network indexed by name
|
|
||||||
const lines = {};
|
const lines = {};
|
||||||
|
const stops = {};
|
||||||
// Segments leading from stop to stop in the planned route for each line
|
|
||||||
const segments = {};
|
const segments = {};
|
||||||
|
|
||||||
|
const routeMasters = elementsList.filter(osm.isTransportLine);
|
||||||
|
|
||||||
// Extract lines, associated stops and planned routes
|
// 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;
|
||||||
|
@ -80,14 +145,14 @@ out body qt;
|
||||||
|
|
||||||
for (const [routeRef, data] of routeMaster.members.entries()) {
|
for (const [routeRef, data] of routeMaster.members.entries()) {
|
||||||
const routeId = data.ref;
|
const routeId = data.ref;
|
||||||
const route = elements[routeId];
|
const route = elementsById[routeId];
|
||||||
const { from, via, to, name } = route.tags;
|
const { from, via, to, name } = route.tags;
|
||||||
const state = route.tags.state || "normal";
|
const state = route.tags.state || "normal";
|
||||||
|
|
||||||
// Add missing stops to the global stops object
|
// Add missing stops to the global stops object
|
||||||
for (const { ref, role } of route.members) {
|
for (const { ref, role } of route.members) {
|
||||||
if (role === "stop") {
|
if (role === "stop") {
|
||||||
const stop = elements[ref];
|
const stop = elementsById[ref];
|
||||||
|
|
||||||
if (!("ref" in stop.tags)) {
|
if (!("ref" in stop.tags)) {
|
||||||
throw new Error(`Stop ${stop.id}
|
throw new Error(`Stop ${stop.id}
|
||||||
|
@ -101,10 +166,11 @@ a “ref” tag`);
|
||||||
stop.lat
|
stop.lat
|
||||||
], {
|
], {
|
||||||
name: stop.tags.name,
|
name: stop.tags.name,
|
||||||
|
node: ref.toString(),
|
||||||
routes: [[lineRef, routeRef]],
|
routes: [[lineRef, routeRef]],
|
||||||
|
}, {
|
||||||
|
id: stop.tags.ref,
|
||||||
});
|
});
|
||||||
|
|
||||||
stopsReverse[ref] = stop.tags.ref;
|
|
||||||
} else {
|
} else {
|
||||||
stops[stop.tags.ref].properties.routes.push([
|
stops[stop.tags.ref].properties.routes.push([
|
||||||
lineRef,
|
lineRef,
|
||||||
|
@ -151,7 +217,7 @@ of ${name}`);
|
||||||
let currentNode = lineStops[0];
|
let currentNode = lineStops[0];
|
||||||
|
|
||||||
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) {
|
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) {
|
||||||
const way = elements[ways[wayIndex]];
|
const way = elementsById[ways[wayIndex]];
|
||||||
const { nodes: wayNodes } = way;
|
const { nodes: wayNodes } = way;
|
||||||
const wayNodesSet = new Set(wayNodes);
|
const wayNodesSet = new Set(wayNodes);
|
||||||
|
|
||||||
|
@ -163,7 +229,7 @@ of ${name}`);
|
||||||
let nextNodeIndex = null;
|
let nextNodeIndex = null;
|
||||||
|
|
||||||
if (wayIndex + 1 < ways.length) {
|
if (wayIndex + 1 < ways.length) {
|
||||||
const nextNodeCandidates = elements[ways[wayIndex + 1]]
|
const nextNodeCandidates = elementsById[ways[wayIndex + 1]]
|
||||||
.nodes.filter(node => wayNodesSet.has(node));
|
.nodes.filter(node => wayNodesSet.has(node));
|
||||||
|
|
||||||
if (nextNodeCandidates.length !== 1) {
|
if (nextNodeCandidates.length !== 1) {
|
||||||
|
@ -201,9 +267,9 @@ ${name} is one-way and cannot be used in reverse.`);
|
||||||
|
|
||||||
// Split the path into segments between stops
|
// Split the path into segments between stops
|
||||||
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) {
|
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) {
|
||||||
const begin = elements[lineStops[stopIdx]].tags.ref;
|
const begin = elementsById[lineStops[stopIdx]].tags.ref;
|
||||||
const beginIdx = path.indexOf(lineStops[stopIdx]);
|
const beginIdx = path.indexOf(lineStops[stopIdx]);
|
||||||
const end = elements[lineStops[stopIdx + 1]].tags.ref;
|
const end = elementsById[lineStops[stopIdx + 1]].tags.ref;
|
||||||
const endIdx = path.indexOf(
|
const endIdx = path.indexOf(
|
||||||
lineStops[stopIdx + 1],
|
lineStops[stopIdx + 1],
|
||||||
beginIdx
|
beginIdx
|
||||||
|
@ -225,15 +291,17 @@ different sequence of nodes in two or more lines.`);
|
||||||
} else {
|
} else {
|
||||||
segments[id] = turfHelpers.lineString(nodesIds.map(
|
segments[id] = turfHelpers.lineString(nodesIds.map(
|
||||||
nodeId => [
|
nodeId => [
|
||||||
elements[nodeId].lon,
|
elementsById[nodeId].lon,
|
||||||
elements[nodeId].lat
|
elementsById[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]],
|
||||||
});
|
begin: begin,
|
||||||
|
end: end,
|
||||||
|
}, { id });
|
||||||
|
|
||||||
segments[id].properties.length = (
|
segments[id].properties.length = (
|
||||||
1000 * turfLength(segments[id]));
|
1000 * turfLength(segments[id]));
|
||||||
|
@ -260,47 +328,240 @@ different sequence of nodes in two or more lines.`);
|
||||||
delete segment.properties.nodesIds;
|
delete segment.properties.nodesIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create out-of-route navigation graph
|
return { lines, stops, segments };
|
||||||
const navigationId = objId => (
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a graph for navigating between stops outside of
|
||||||
|
* regular planned routes.
|
||||||
|
* @property {Object.<string,Stop>} stops List of stops.
|
||||||
|
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
|
||||||
|
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
|
||||||
|
* @return {Object.<string,Object.<string,NavigationEdge>} Resulting graph.
|
||||||
|
*/
|
||||||
|
const createNavigationGraph = (stops, elementsList, elementsById) => {
|
||||||
|
// Graph of network stops and junctions
|
||||||
|
const navigation = {};
|
||||||
|
|
||||||
|
// Predecessors of each graph node
|
||||||
|
const navigationReverse = {};
|
||||||
|
|
||||||
|
// Stops indexed by their OSM ID instead of their network ID
|
||||||
|
const stopsReverse = Object.fromEntries(
|
||||||
|
Object.entries(stops).map(([id, stop]) => [stop.properties.node, id])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the ID of a node in the navigation graph
|
||||||
|
// (its network ID if it is a network object, otherwise its OSM id)
|
||||||
|
const getNavigationId = objId => (
|
||||||
objId in stopsReverse
|
objId in stopsReverse
|
||||||
? stopsReverse[objId]
|
? stopsReverse[objId]
|
||||||
: objId.toString()
|
: objId.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get the OSM ID of a navigation object
|
||||||
|
const getOSMId = navId => (
|
||||||
|
navId in stops
|
||||||
|
? stops[navId].properties.node
|
||||||
|
: navId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create graph nodes from OSM nodes
|
||||||
for (const obj of elementsList) {
|
for (const obj of elementsList) {
|
||||||
if (obj.type === "node") {
|
if (obj.type === "node") {
|
||||||
const id = navigationId(obj.id);
|
navigation[getNavigationId(obj.id)] = {};
|
||||||
|
navigationReverse[getNavigationId(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: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Link up graph edges with OSM ways
|
||||||
for (const obj of elementsList) {
|
for (const obj of elementsList) {
|
||||||
if (obj.type === "way") {
|
if (obj.type === "way") {
|
||||||
const oneWay = osm.isOneWay(obj);
|
const oneWay = osm.isOneWay(obj);
|
||||||
const pairs = obj.nodes.slice(0, -1).map(
|
const pairs = obj.nodes.slice(0, -1).map(
|
||||||
(node, i) => [
|
(node, i) => [
|
||||||
navigationId(node),
|
getNavigationId(node),
|
||||||
navigationId(obj.nodes[i + 1]),
|
getNavigationId(obj.nodes[i + 1]),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [from, to] of pairs) {
|
for (const [from, to] of pairs) {
|
||||||
navigation[from].successors.push(to);
|
navigation[from][to] = [from, to];
|
||||||
|
navigationReverse[to][from] = true;
|
||||||
|
|
||||||
if (!oneWay) {
|
if (!oneWay) {
|
||||||
navigation[to].successors.push(from);
|
navigation[to][from] = [to, from];
|
||||||
|
navigationReverse[from][to] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { navigation, stops, lines, segments };
|
// Mark nodes of the graph to be kept
|
||||||
|
const nodesToKeep = {};
|
||||||
|
|
||||||
|
for (const nodeId in navigation) {
|
||||||
|
if (nodeId in stops) {
|
||||||
|
// Keep stop nodes
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = new Set(Object.keys(navigationReverse[nodeId]));
|
||||||
|
const exits = new Set(Object.keys(navigation[nodeId]));
|
||||||
|
|
||||||
|
// Keep split nodes, i.e. nodes with at least two exit nodes
|
||||||
|
// and one entry node that are all distinct from each other
|
||||||
|
if (entries.size >= 1) {
|
||||||
|
if (exits.size >= 3) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exits.size === 2) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!exits.has(entry)) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep junction nodes, i.e. nodes with at least two entry nodes
|
||||||
|
// and one exit node that are all distinct from each other
|
||||||
|
if (exits.size >= 1) {
|
||||||
|
if (entries.size >= 3) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.size === 2) {
|
||||||
|
for (const exit of exits) {
|
||||||
|
if (!entries.has(exit)) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress edges between nodes of interest
|
||||||
|
for (const beginId in nodesToKeep) {
|
||||||
|
const begin = navigation[beginId];
|
||||||
|
const stack = [];
|
||||||
|
const parent = {[beginId]: beginId};
|
||||||
|
|
||||||
|
for (const succId in begin) {
|
||||||
|
stack.push(succId);
|
||||||
|
parent[succId] = beginId;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const endId = stack.pop();
|
||||||
|
const end = navigation[endId];
|
||||||
|
|
||||||
|
if (endId in nodesToKeep) {
|
||||||
|
if (endId in begin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversePath = [endId];
|
||||||
|
let trackback = parent[endId];
|
||||||
|
let oneWay = !(trackback in end);
|
||||||
|
|
||||||
|
while (trackback !== beginId) {
|
||||||
|
reversePath.push(trackback);
|
||||||
|
oneWay = oneWay || !(parent[trackback] in navigation[trackback]);
|
||||||
|
|
||||||
|
delete navigation[trackback];
|
||||||
|
trackback = parent[trackback];
|
||||||
|
}
|
||||||
|
|
||||||
|
reversePath.push(beginId);
|
||||||
|
const forwardPath = [...reversePath];
|
||||||
|
forwardPath.reverse();
|
||||||
|
|
||||||
|
delete begin[forwardPath[1]];
|
||||||
|
|
||||||
|
if (!(endId in begin)) {
|
||||||
|
begin[endId] = forwardPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oneWay) {
|
||||||
|
delete end[reversePath[1]];
|
||||||
|
|
||||||
|
if (!(beginId in end)) {
|
||||||
|
end[beginId] = reversePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let isFirst = true;
|
||||||
|
|
||||||
|
for (const succId in end) {
|
||||||
|
if (succId !== parent[endId]) {
|
||||||
|
if (isFirst) {
|
||||||
|
parent[succId] = endId;
|
||||||
|
stack.push(succId);
|
||||||
|
isFirst = false;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Multiple successors in \
|
||||||
|
non-junction node ${endId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirst) {
|
||||||
|
// Reached a dead-end: remove the path
|
||||||
|
let trackback = endId;
|
||||||
|
|
||||||
|
while (parent[trackback] !== beginId) {
|
||||||
|
delete navigation[trackback];
|
||||||
|
trackback = parent[trackback];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete navigation[trackback];
|
||||||
|
delete begin[trackback];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert graph edges to GeoJSON line strings
|
||||||
|
for (const [beginId, begin] of Object.entries(navigation)) {
|
||||||
|
for (const endId in begin) {
|
||||||
|
begin[endId] = turfHelpers.lineString(begin[endId].map(
|
||||||
|
nodeId => [
|
||||||
|
elementsById[getOSMId(nodeId)].lon,
|
||||||
|
elementsById[getOSMId(nodeId)].lat
|
||||||
|
]
|
||||||
|
), {
|
||||||
|
begin: beginId,
|
||||||
|
end: endId,
|
||||||
|
});
|
||||||
|
|
||||||
|
begin[endId].properties.length = 1000 * turfLength(begin[endId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch information about the network.
|
||||||
|
* @param {Array.<string>} lineRefs List of lines to fetch.
|
||||||
|
* @returns {Network} Network metadata extracted from OSM.
|
||||||
|
*/
|
||||||
|
export const fetch = async lineRefs => {
|
||||||
|
const elementsList = await queryLines(lineRefs);
|
||||||
|
const elementsById = elementsList.reduce((prev, elt) => {
|
||||||
|
prev[elt.id] = elt;
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const { lines, stops, segments } = processRoutes(elementsList, elementsById);
|
||||||
|
const navigation = createNavigationGraph(stops, elementsList, elementsById);
|
||||||
|
|
||||||
|
return { navigation, lines, stops, segments };
|
||||||
};
|
};
|
||||||
|
|
91252
src/tam/network.json
91252
src/tam/network.json
File diff suppressed because it is too large
Load Diff
|
@ -1,40 +1,58 @@
|
||||||
|
import * as turfHelpers from "@turf/helpers";
|
||||||
|
import dijkstra from "dijkstrajs";
|
||||||
import network from "./network.json";
|
import network from "./network.json";
|
||||||
|
|
||||||
|
// Transform the navigation graph to be in the format expected by dijkstrajs
|
||||||
|
const graph = {};
|
||||||
|
|
||||||
|
for (const [beginId, begin] of Object.entries(network.navigation)) {
|
||||||
|
const neighbors = {};
|
||||||
|
|
||||||
|
for (const [endId, end] of Object.entries(begin)) {
|
||||||
|
neighbors[endId] = end.properties.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph[beginId] = neighbors;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a route linking two nodes or stops.
|
* Find the shortest path of nodes linking two nodes or stops.
|
||||||
* @param {string} from ID of the starting stop or node.
|
* @param {string} from ID of the starting stop or node.
|
||||||
* @param {string} to ID of the ending 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
|
* @return {Array.<string>} If possible, list of nodes joining `from` to `to`.
|
||||||
* the least number of nodes possible.
|
|
||||||
*/
|
*/
|
||||||
export const findRoute = (from, to) => {
|
export const findPath = (from, to) => {
|
||||||
const queue = [from];
|
try {
|
||||||
const parent = {from: from};
|
return dijkstra.find_path(graph, from, to);
|
||||||
|
} catch (err) {
|
||||||
while (queue.length > 0 && !(to in parent)) {
|
return null;
|
||||||
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)) {
|
/**
|
||||||
|
* Find the shortest segment 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.
|
||||||
|
*/
|
||||||
|
export const findSegment = (from, to) => {
|
||||||
|
const path = findPath(from, to);
|
||||||
|
|
||||||
|
if (path === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = [];
|
const initial = network.navigation[path[0]][path[1]];
|
||||||
let current = to;
|
let points = [...initial.geometry.coordinates];
|
||||||
|
let length = initial.properties.length;
|
||||||
|
|
||||||
while (current !== from) {
|
for (let i = 1; i + 1 < path.length; ++i) {
|
||||||
path.push(current);
|
const current = network.navigation[path[i]][path[i + 1]];
|
||||||
current = parent[current];
|
points = points.concat(current.geometry.coordinates.slice(1));
|
||||||
|
length += current.properties.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
path.push(from);
|
const route = turfHelpers.lineString(points);
|
||||||
path.reverse();
|
route.properties.length = length;
|
||||||
return path;
|
return route;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import turfAlong from "@turf/along";
|
import turfAlong from "@turf/along";
|
||||||
import turfLength from "@turf/length";
|
|
||||||
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 * as routing from "./routing.js";
|
||||||
import network from "./network.json";
|
import network from "./network.json";
|
||||||
|
@ -100,20 +98,12 @@ class Course {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute a custom route between two stops
|
// Compute a custom route between two stops
|
||||||
const route = routing.findRoute(this.departureStop, this.arrivalStop);
|
this.segment = routing.findSegment(this.departureStop, this.arrivalStop);
|
||||||
|
|
||||||
if (route === null) {
|
if (this.segment === null) {
|
||||||
console.warn(`No route from ${this.departureStop} \
|
console.warn(`No route from ${this.departureStop} \
|
||||||
to ${this.arrivalStop}`);
|
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 passings data received from the server. */
|
/** Merge passings data received from the server. */
|
||||||
|
|
Loading…
Reference in New Issue