2020-07-16 23:41:05 +00:00
|
|
|
|
/**
|
2020-07-25 16:05:43 +00:00
|
|
|
|
* @fileoverview
|
2020-07-17 10:13:25 +00:00
|
|
|
|
*
|
2020-07-16 23:41:05 +00:00
|
|
|
|
* Extract static information about the TaM network from OpenStreetMap (OSM):
|
|
|
|
|
* tram and bus lines, stops and routes.
|
|
|
|
|
*
|
2021-05-11 13:35:03 +00:00
|
|
|
|
* Functions in this file also report inconsistencies in OSM data.
|
2020-07-17 10:13:25 +00:00
|
|
|
|
*
|
|
|
|
|
* Because of the static nature of this data, it is cached in a
|
2021-05-21 21:59:51 +00:00
|
|
|
|
* version-controlled file `network.json` next to this file. To update it,
|
|
|
|
|
* run the `script/update-network.js` script.
|
2020-07-16 23:41:05 +00:00
|
|
|
|
*/
|
|
|
|
|
|
2021-05-11 19:39:24 +00:00
|
|
|
|
import * as turfHelpers from "@turf/helpers";
|
|
|
|
|
import turfLength from "@turf/length";
|
|
|
|
|
import * as util from "../util.js";
|
|
|
|
|
import * as osm from "./sources/osm.js";
|
2020-01-14 23:19:26 +00:00
|
|
|
|
|
|
|
|
|
/**
|
2021-05-21 21:59:51 +00:00
|
|
|
|
* Route of a line of a transport network.
|
|
|
|
|
* @typedef {Object} Route
|
|
|
|
|
* @property {string} from Name of the starting point of the route.
|
|
|
|
|
* @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.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
2020-01-14 23:19:26 +00:00
|
|
|
|
*/
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
2021-05-22 22:45:09 +00:00
|
|
|
|
* Edge of the graph for navigating between stops.
|
2021-05-21 21:59:51 +00:00
|
|
|
|
* @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).
|
|
|
|
|
*/
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
/**
|
|
|
|
|
* Graph for navigating between stops.
|
|
|
|
|
* @typedef {Object.<string,Object.<string,NavigationEdge>>} Navigation
|
|
|
|
|
*/
|
|
|
|
|
|
2021-05-21 21:59:51 +00:00
|
|
|
|
/**
|
|
|
|
|
* 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.
|
2021-05-22 22:45:09 +00:00
|
|
|
|
* @property {Navigation} navigation
|
2021-05-21 21:59:51 +00:00
|
|
|
|
* 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];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2021-05-16 10:01:51 +00:00
|
|
|
|
// Find the public transport lines bearing the requested references
|
2020-07-25 16:05:43 +00:00
|
|
|
|
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2021-05-16 10:01:51 +00:00
|
|
|
|
(
|
|
|
|
|
._;
|
|
|
|
|
|
|
|
|
|
// Recursively fetch routes, ways and stops inside the lines
|
|
|
|
|
>>;
|
|
|
|
|
|
|
|
|
|
// Find adjacent tracks (used for out-of-route navigation)
|
|
|
|
|
complete {
|
|
|
|
|
way(around:0)[railway="tram"];
|
|
|
|
|
};
|
|
|
|
|
>;
|
|
|
|
|
);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
out body qt;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
`)).elements;
|
|
|
|
|
};
|
2021-05-16 10:01:51 +00:00
|
|
|
|
|
2021-05-21 21:59:51 +00:00
|
|
|
|
/**
|
|
|
|
|
* Assemble information about lines, stops and segments from the raw
|
|
|
|
|
* OpenStreetMap data.
|
|
|
|
|
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
|
|
|
|
|
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
|
|
|
|
|
* @return {Object} Assembled information about lines, stops and segments.
|
|
|
|
|
*/
|
|
|
|
|
const processRoutes = (elementsList, elementsById) => {
|
2020-01-14 13:08:08 +00:00
|
|
|
|
const lines = {};
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const stops = {};
|
2020-07-17 21:48:32 +00:00
|
|
|
|
const segments = {};
|
|
|
|
|
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const routeMasters = elementsList.filter(osm.isTransportLine);
|
|
|
|
|
|
2021-05-16 10:01:51 +00:00
|
|
|
|
// Extract lines, associated stops and planned routes
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (const routeMaster of routeMasters) {
|
2020-01-14 13:08:08 +00:00
|
|
|
|
const lineRef = routeMaster.tags.ref;
|
2020-07-25 16:05:43 +00:00
|
|
|
|
const color = routeMaster.tags.colour || "#000000";
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const routes = [];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (const [routeRef, data] of routeMaster.members.entries()) {
|
|
|
|
|
const routeId = data.ref;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const route = elementsById[routeId];
|
2020-07-25 16:05:43 +00:00
|
|
|
|
const { from, via, to, name } = route.tags;
|
|
|
|
|
const state = route.tags.state || "normal";
|
2020-07-22 21:10:43 +00:00
|
|
|
|
|
|
|
|
|
// Add missing stops to the global stops object
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (const { ref, role } of route.members) {
|
|
|
|
|
if (role === "stop") {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const stop = elementsById[ref];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (!("ref" in stop.tags)) {
|
2020-07-22 21:10:43 +00:00
|
|
|
|
throw new Error(`Stop ${stop.id}
|
|
|
|
|
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
|
|
|
|
|
a “ref” tag`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (!(stop.tags.ref in stops)) {
|
2020-07-25 14:28:51 +00:00
|
|
|
|
stops[stop.tags.ref] = turfHelpers.point([
|
|
|
|
|
stop.lon,
|
|
|
|
|
stop.lat
|
|
|
|
|
], {
|
2020-01-14 13:08:08 +00:00
|
|
|
|
name: stop.tags.name,
|
2021-05-21 21:59:51 +00:00
|
|
|
|
node: ref.toString(),
|
2020-07-27 19:01:21 +00:00
|
|
|
|
routes: [[lineRef, routeRef]],
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}, {
|
|
|
|
|
id: stop.tags.ref,
|
2020-07-25 14:28:51 +00:00
|
|
|
|
});
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} else {
|
2020-07-25 14:28:51 +00:00
|
|
|
|
stops[stop.tags.ref].properties.routes.push([
|
|
|
|
|
lineRef,
|
|
|
|
|
routeRef
|
|
|
|
|
]);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Check that the route consists of a block of stops and platforms
|
|
|
|
|
// followed by a block of routes as dictated by PTv2
|
|
|
|
|
const relationPivot = route.members.findIndex(
|
2020-07-25 16:05:43 +00:00
|
|
|
|
({ role }) => role === ""
|
2020-07-16 20:56:39 +00:00
|
|
|
|
);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (!route.members.slice(0, relationPivot).every(
|
2020-07-25 16:05:43 +00:00
|
|
|
|
({ role }) => role === "stop" || role === "platform"
|
|
|
|
|
)) {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`Members with invalid roles in between stops
|
2020-07-17 10:13:25 +00:00
|
|
|
|
of ${name}`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
if (!route.members.slice(relationPivot).every(
|
2020-07-25 16:05:43 +00:00
|
|
|
|
({ role }) => role === ""
|
|
|
|
|
)) {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`Members with invalid roles inside the path
|
2020-07-17 10:13:25 +00:00
|
|
|
|
of ${name}`);
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// List of stops in the route, expected to be in the timetable
|
|
|
|
|
// order as per PTv2 and to be traversed in order by the sequence
|
|
|
|
|
// of ways extracted below
|
2020-07-22 21:10:43 +00:00
|
|
|
|
const lineStops = route.members.slice(0, relationPivot)
|
2020-07-25 16:05:43 +00:00
|
|
|
|
.filter(({ role }) => role === "stop")
|
|
|
|
|
.map(({ ref }) => ref);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// List of ways making up the route’s path through its stops
|
|
|
|
|
// with each way connected to the next through a single point
|
|
|
|
|
const ways = route.members.slice(relationPivot)
|
2020-07-25 16:05:43 +00:00
|
|
|
|
.map(({ ref }) => ref);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Merge all used ways in a single path
|
|
|
|
|
let path = [];
|
2020-07-22 21:10:43 +00:00
|
|
|
|
let currentNode = lineStops[0];
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const way = elementsById[ways[wayIndex]];
|
2020-07-25 16:05:43 +00:00
|
|
|
|
const { nodes: wayNodes } = way;
|
2020-07-16 20:56:39 +00:00
|
|
|
|
const wayNodesSet = new Set(wayNodes);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
const curNodeIndex = wayNodes.indexOf(currentNode);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// If not the last way, find a connection point to the next way
|
|
|
|
|
// (there should be exactly one)
|
|
|
|
|
let nextNode = null;
|
|
|
|
|
let nextNodeIndex = null;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (wayIndex + 1 < ways.length) {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const nextNodeCandidates = elementsById[ways[wayIndex + 1]]
|
2020-07-16 20:56:39 +00:00
|
|
|
|
.nodes.filter(node => wayNodesSet.has(node));
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (nextNodeCandidates.length !== 1) {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`There should be exactly one point
|
2020-07-17 10:13:25 +00:00
|
|
|
|
connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name},
|
2020-07-16 20:56:39 +00:00
|
|
|
|
but there are ${nextNodeCandidates.length}`);
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
2020-07-16 20:56:39 +00:00
|
|
|
|
|
|
|
|
|
nextNode = nextNodeCandidates[0];
|
|
|
|
|
nextNodeIndex = wayNodes.indexOf(nextNode);
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} else {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
nextNodeIndex = wayNodes.length;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (curNodeIndex < nextNodeIndex) {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Use the way in its normal direction
|
|
|
|
|
path = path.concat(
|
|
|
|
|
wayNodes.slice(curNodeIndex, nextNodeIndex)
|
|
|
|
|
);
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} else {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Use the way in the reverse direction
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (osm.isOneWay(way)) {
|
2020-07-16 20:56:39 +00:00
|
|
|
|
throw new Error(`Way n°${wayIndex} in
|
2020-07-17 10:13:25 +00:00
|
|
|
|
${name} is one-way and cannot be used in reverse.`);
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path = path.concat(
|
|
|
|
|
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
|
2020-07-16 22:16:54 +00:00
|
|
|
|
.reverse()
|
2020-07-16 20:56:39 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNode = nextNode;
|
2020-01-14 13:08:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-16 20:56:39 +00:00
|
|
|
|
// Split the path into segments between stops
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const begin = elementsById[lineStops[stopIdx]].tags.ref;
|
2020-07-24 23:16:36 +00:00
|
|
|
|
const beginIdx = path.indexOf(lineStops[stopIdx]);
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const end = elementsById[lineStops[stopIdx + 1]].tags.ref;
|
2020-07-24 23:16:36 +00:00
|
|
|
|
const endIdx = path.indexOf(
|
|
|
|
|
lineStops[stopIdx + 1],
|
|
|
|
|
beginIdx
|
|
|
|
|
) + 1;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
|
|
|
|
|
const id = `${begin}-${end}`;
|
2020-07-24 23:16:36 +00:00
|
|
|
|
const nodesIds = path.slice(beginIdx, endIdx);
|
2020-07-17 21:48:32 +00:00
|
|
|
|
|
2020-07-25 16:05:43 +00:00
|
|
|
|
if (id in segments) {
|
2020-07-25 14:28:51 +00:00
|
|
|
|
if (!util.arraysEqual(
|
|
|
|
|
nodesIds,
|
|
|
|
|
segments[id].properties.nodesIds
|
2020-07-25 16:05:43 +00:00
|
|
|
|
)) {
|
2020-07-17 21:48:32 +00:00
|
|
|
|
throw new Error(`Segment ${id} is defined as a
|
|
|
|
|
different sequence of nodes in two or more lines.`);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-25 14:28:51 +00:00
|
|
|
|
segments[id].properties.routes.push([lineRef, routeRef]);
|
2020-07-25 16:05:43 +00:00
|
|
|
|
} else {
|
|
|
|
|
segments[id] = turfHelpers.lineString(nodesIds.map(
|
|
|
|
|
nodeId => [
|
2021-05-21 21:59:51 +00:00
|
|
|
|
elementsById[nodeId].lon,
|
|
|
|
|
elementsById[nodeId].lat
|
2020-07-25 16:05:43 +00:00
|
|
|
|
]
|
|
|
|
|
), {
|
2020-07-17 21:48:32 +00:00
|
|
|
|
// Keep track of the original sequence of nodes to
|
|
|
|
|
// compare with duplicates
|
2020-07-24 23:02:07 +00:00
|
|
|
|
nodesIds,
|
2021-05-21 21:59:51 +00:00
|
|
|
|
routes: [[lineRef, routeRef]],
|
|
|
|
|
begin: begin,
|
|
|
|
|
end: end,
|
|
|
|
|
}, { id });
|
2020-07-25 14:28:51 +00:00
|
|
|
|
|
|
|
|
|
segments[id].properties.length = (
|
|
|
|
|
1000 * turfLength(segments[id]));
|
2020-07-17 21:48:32 +00:00
|
|
|
|
}
|
2020-07-16 20:56:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
routes.push({
|
2020-07-25 16:05:43 +00:00
|
|
|
|
from,
|
|
|
|
|
via,
|
|
|
|
|
to,
|
|
|
|
|
name,
|
|
|
|
|
state
|
2020-07-16 20:56:39 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-14 13:08:08 +00:00
|
|
|
|
|
|
|
|
|
lines[lineRef] = {
|
|
|
|
|
color,
|
2020-07-25 16:05:43 +00:00
|
|
|
|
routes
|
2020-01-14 13:08:08 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-17 21:48:32 +00:00
|
|
|
|
// Remove OSM nodes from segments that were only used for checking validity
|
2020-07-25 16:05:43 +00:00
|
|
|
|
for (const segment of Object.values(segments)) {
|
2020-07-25 14:28:51 +00:00
|
|
|
|
delete segment.properties.nodesIds;
|
2020-07-17 21:48:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-21 21:59:51 +00:00
|
|
|
|
return { lines, stops, segments };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
2021-05-22 22:45:09 +00:00
|
|
|
|
* Create a graph for navigating between stops.
|
2021-05-21 21:59:51 +00:00
|
|
|
|
* @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.
|
|
|
|
|
*/
|
2021-05-22 22:45:09 +00:00
|
|
|
|
const createNavigationGraph = (elementsList, elementsById) => {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const navigation = {};
|
|
|
|
|
const navigationReverse = {};
|
|
|
|
|
|
|
|
|
|
// Create graph nodes from OSM nodes
|
2021-05-16 10:01:51 +00:00
|
|
|
|
for (const obj of elementsList) {
|
|
|
|
|
if (obj.type === "node") {
|
2021-05-22 22:45:09 +00:00
|
|
|
|
navigation[obj.id] = {};
|
|
|
|
|
navigationReverse[obj.id] = {};
|
2021-05-16 10:01:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-21 21:59:51 +00:00
|
|
|
|
// Link up graph edges with OSM ways
|
2021-05-16 10:01:51 +00:00
|
|
|
|
for (const obj of elementsList) {
|
|
|
|
|
if (obj.type === "way") {
|
|
|
|
|
const oneWay = osm.isOneWay(obj);
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
for (let i = 0; i + 1 < obj.nodes.length; ++i) {
|
|
|
|
|
const from = obj.nodes[i];
|
|
|
|
|
let to = obj.nodes[i + 1];
|
|
|
|
|
let path = [from.toString(), to.toString()];
|
|
|
|
|
|
|
|
|
|
// Make sure we can’t jump between rails at railway crossings
|
|
|
|
|
if (i + 2 < obj.nodes.length
|
|
|
|
|
&& osm.isRailwayCrossing(elementsById[to])) {
|
|
|
|
|
const next = obj.nodes[i + 2];
|
|
|
|
|
path = [from.toString(), to.toString(), next.toString()];
|
|
|
|
|
to = next;
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
navigation[from][to] = path;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
navigationReverse[to][from] = true;
|
|
|
|
|
|
|
|
|
|
if (!oneWay) {
|
2021-05-22 22:45:09 +00:00
|
|
|
|
const reversePath = [...path];
|
|
|
|
|
reversePath.reverse();
|
|
|
|
|
navigation[to][from] = reversePath;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
navigationReverse[from][to] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
return { navigation, navigationReverse };
|
|
|
|
|
};
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
/**
|
|
|
|
|
* Remove and relink nodes that connect only two nodes or less.
|
|
|
|
|
* @param {Object.<string,Stop>} stops List of stops.
|
|
|
|
|
* @param {Navigation} navigation Input navigation graph.
|
|
|
|
|
* @param {Object.<string,Object.<string,boolean>>} navigationReverse
|
|
|
|
|
* Backward edges of the navigation graph.
|
|
|
|
|
*/
|
|
|
|
|
const compressNavigationGraph = (stops, navigation, navigationReverse) => {
|
|
|
|
|
const stopsReverse = Object.fromEntries(
|
|
|
|
|
Object.entries(stops).map(([id, stop]) => [stop.properties.node, id])
|
|
|
|
|
);
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
let removedDeadEnds = true;
|
|
|
|
|
const nodesToCompress = {};
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
while (removedDeadEnds) {
|
|
|
|
|
// Identify nodes to be compressed
|
|
|
|
|
for (const nodeId in navigation) {
|
|
|
|
|
if (nodeId in stopsReverse) {
|
|
|
|
|
// Keep stop nodes
|
2021-05-21 21:59:51 +00:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
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) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isSplit = false;
|
|
|
|
|
|
|
|
|
|
if (exits.size === 2) {
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
if (!exits.has(entry)) {
|
|
|
|
|
isSplit = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-22 22:45:09 +00:00
|
|
|
|
|
|
|
|
|
if (isSplit) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
2021-05-22 22:45:09 +00:00
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isJunction = false;
|
|
|
|
|
|
|
|
|
|
if (entries.size === 2) {
|
|
|
|
|
for (const exit of exits) {
|
|
|
|
|
if (!entries.has(exit)) {
|
|
|
|
|
isJunction = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isJunction) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compress all other nodes
|
|
|
|
|
nodesToCompress[nodeId] = true;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
// Find nodes that cannot be used to directly link up two kept nodes
|
|
|
|
|
const usedNodes = {};
|
|
|
|
|
|
|
|
|
|
for (const beginId in navigation) {
|
|
|
|
|
if (beginId in nodesToCompress) {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
usedNodes[beginId] = true;
|
|
|
|
|
|
|
|
|
|
// Start a DFS from each node to be kept
|
|
|
|
|
const begin = navigation[beginId];
|
|
|
|
|
const stack = [];
|
|
|
|
|
const parent = {[beginId]: beginId};
|
|
|
|
|
|
|
|
|
|
for (const succId in begin) {
|
|
|
|
|
if (succId in nodesToCompress) {
|
|
|
|
|
stack.push(succId);
|
|
|
|
|
parent[succId] = beginId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
const endId = stack.pop();
|
|
|
|
|
const end = navigation[endId];
|
|
|
|
|
|
|
|
|
|
if (!(endId in nodesToCompress)) {
|
|
|
|
|
let trackback = parent[endId];
|
|
|
|
|
|
|
|
|
|
while (trackback !== beginId) {
|
|
|
|
|
usedNodes[trackback] = true;
|
|
|
|
|
trackback = parent[trackback];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (const succId in end) {
|
|
|
|
|
if (succId !== parent[endId]) {
|
|
|
|
|
parent[succId] = endId;
|
|
|
|
|
stack.push(succId);
|
|
|
|
|
}
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-22 22:45:09 +00:00
|
|
|
|
|
|
|
|
|
// Remove dead-end nodes
|
|
|
|
|
removedDeadEnds = false;
|
|
|
|
|
|
|
|
|
|
for (const beginId in navigation) {
|
|
|
|
|
if (!(beginId in usedNodes)) {
|
|
|
|
|
for (const neighborId in navigation[beginId]) {
|
|
|
|
|
delete navigationReverse[neighborId][beginId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const neighborId in navigationReverse[beginId]) {
|
|
|
|
|
delete navigation[neighborId][beginId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete navigation[beginId];
|
|
|
|
|
delete navigationReverse[beginId];
|
|
|
|
|
removedDeadEnds = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
// Perform node compression
|
|
|
|
|
for (const beginId in navigation) {
|
|
|
|
|
if (beginId in nodesToCompress) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start a DFS from each node to be kept
|
2021-05-21 21:59:51 +00:00
|
|
|
|
const begin = navigation[beginId];
|
|
|
|
|
const stack = [];
|
|
|
|
|
const parent = {[beginId]: beginId};
|
|
|
|
|
|
|
|
|
|
for (const succId in begin) {
|
2021-05-22 22:45:09 +00:00
|
|
|
|
if (succId in nodesToCompress) {
|
|
|
|
|
stack.push(succId);
|
|
|
|
|
parent[succId] = beginId;
|
|
|
|
|
}
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
const endId = stack.pop();
|
|
|
|
|
const end = navigation[endId];
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
if (!(endId in nodesToCompress)) {
|
|
|
|
|
// Found another kept node
|
|
|
|
|
// Collect and remove intermediate nodes
|
2021-05-21 21:59:51 +00:00
|
|
|
|
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];
|
2021-05-22 22:45:09 +00:00
|
|
|
|
delete navigationReverse[trackback];
|
2021-05-21 21:59:51 +00:00
|
|
|
|
trackback = parent[trackback];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reversePath.push(beginId);
|
|
|
|
|
const forwardPath = [...reversePath];
|
|
|
|
|
forwardPath.reverse();
|
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
// Create edges to link both nodes directly
|
2021-05-21 21:59:51 +00:00
|
|
|
|
delete begin[forwardPath[1]];
|
2021-05-22 22:45:09 +00:00
|
|
|
|
delete navigationReverse[endId][reversePath[1]];
|
|
|
|
|
|
|
|
|
|
delete end[reversePath[1]];
|
|
|
|
|
delete navigationReverse[beginId][forwardPath[1]];
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
|
|
|
|
if (!(endId in begin)) {
|
|
|
|
|
begin[endId] = forwardPath;
|
2021-05-22 22:45:09 +00:00
|
|
|
|
navigationReverse[endId][beginId] = true;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
2021-05-16 10:01:51 +00:00
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
if (!oneWay && !(beginId in end)) {
|
|
|
|
|
end[beginId] = reversePath;
|
|
|
|
|
navigationReverse[beginId][endId] = true;
|
2021-05-21 21:59:51 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2021-05-22 22:45:09 +00:00
|
|
|
|
// Continue the traversal down unused nodes
|
2021-05-21 21:59:51 +00:00
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-16 10:01:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-22 22:45:09 +00:00
|
|
|
|
};
|
2021-05-16 10:01:51 +00:00
|
|
|
|
|
2021-05-22 22:45:09 +00:00
|
|
|
|
/**
|
|
|
|
|
* Transform navigation graph edges into GeoJSON segments.
|
|
|
|
|
* @param {Navigation} navigation Input navigation graph.
|
|
|
|
|
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
|
|
|
|
|
*/
|
|
|
|
|
const makeNavigationSegments = (navigation, elementsById) => {
|
2021-05-21 21:59:51 +00:00
|
|
|
|
for (const [beginId, begin] of Object.entries(navigation)) {
|
|
|
|
|
for (const endId in begin) {
|
|
|
|
|
begin[endId] = turfHelpers.lineString(begin[endId].map(
|
|
|
|
|
nodeId => [
|
2021-05-22 22:45:09 +00:00
|
|
|
|
elementsById[nodeId].lon,
|
|
|
|
|
elementsById[nodeId].lat
|
2021-05-21 21:59:51 +00:00
|
|
|
|
]
|
|
|
|
|
), {
|
|
|
|
|
begin: beginId,
|
|
|
|
|
end: endId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
begin[endId].properties.length = 1000 * turfLength(begin[endId]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2021-05-22 22:45:09 +00:00
|
|
|
|
const { navigation, navigationReverse } = createNavigationGraph(
|
|
|
|
|
elementsList, elementsById
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
compressNavigationGraph(stops, navigation, navigationReverse);
|
|
|
|
|
makeNavigationSegments(navigation, elementsById);
|
2021-05-21 21:59:51 +00:00
|
|
|
|
|
|
|
|
|
return { navigation, lines, stops, segments };
|
2020-01-14 23:19:26 +00:00
|
|
|
|
};
|