tracktracker/src/tam/network.js

255 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @fileoverview
*
* Extract static information about the TaM network from OpenStreetMap (OSM):
* tram and bus lines, stops and routes.
*
* Functions in this file also report and offer to correct errors that may
* occur in OSM data.
*
* 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
* the `script/update-network` script.
*/
const turfHelpers = require("@turf/helpers");
const turfLength = require("@turf/length").default;
const util = require("../util");
const osm = require("./sources/osm");
/**
* Fetch stops and lines of the network.
* @param {string[]} lineRefs List of lines to fetch.
* @returns {{stops: Object, lines: Object, segments: Object}} Set of stops,
* segments and lines.
*/
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
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
// Recursively fetch routes, ways and stops inside the line
(._; >>;);
out body qt;
`);
// List of retrieved objects
const elementsList = rawData.elements;
// List of retrieved lines
const routeMasters = elementsList.filter(osm.isTransportLine);
// Retrieved objects indexed by ID
const elements = elementsList.reduce((prev, elt) => {
prev[elt.id] = elt;
return prev;
}, {});
// All stops in the network
const stops = {};
// All transport lines of the network
const lines = {};
// All segments leading from one stop to another
const segments = {};
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()) {
const routeId = data.ref;
const route = elements[routeId];
const { from, via, to, name } = route.tags;
const state = route.tags.state || "normal";
// Add missing stops to the global stops object
for (const { ref, role } of route.members) {
if (role === "stop") {
const stop = elements[ref];
if (!("ref" in stop.tags)) {
throw new Error(`Stop ${stop.id}
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
a “ref” tag`);
}
if (!(stop.tags.ref in stops)) {
stops[stop.tags.ref] = turfHelpers.point([
stop.lon,
stop.lat
], {
name: stop.tags.name,
routes: [[lineRef, routeRef]]
});
} else {
stops[stop.tags.ref].properties.routes.push([
lineRef,
routeRef
]);
}
}
}
// 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(
({ role }) => role === ""
);
if (!route.members.slice(0, relationPivot).every(
({ role }) => role === "stop" || role === "platform"
)) {
throw new Error(`Members with invalid roles in between stops
of ${name}`);
}
if (!route.members.slice(relationPivot).every(
({ role }) => role === ""
)) {
throw new Error(`Members with invalid roles inside the path
of ${name}`);
}
// 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
const lineStops = route.members.slice(0, relationPivot)
.filter(({ role }) => role === "stop")
.map(({ ref }) => ref);
// List of ways making up the routes path through its stops
// with each way connected to the next through a single point
const ways = route.members.slice(relationPivot)
.map(({ ref }) => ref);
// Merge all used ways in a single path
let path = [];
let currentNode = lineStops[0];
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1) {
const way = elements[ways[wayIndex]];
const { nodes: wayNodes } = way;
const wayNodesSet = new Set(wayNodes);
const curNodeIndex = wayNodes.indexOf(currentNode);
// If not the last way, find a connection point to the next way
// (there should be exactly one)
let nextNode = null;
let nextNodeIndex = null;
if (wayIndex + 1 < ways.length) {
const nextNodeCandidates = elements[ways[wayIndex + 1]]
.nodes.filter(node => wayNodesSet.has(node));
if (nextNodeCandidates.length !== 1) {
throw new Error(`There should be exactly one point
connecting way n°${wayIndex} and way n°${wayIndex + 1} in ${name},
but there are ${nextNodeCandidates.length}`);
}
nextNode = nextNodeCandidates[0];
nextNodeIndex = wayNodes.indexOf(nextNode);
} else {
nextNodeIndex = wayNodes.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
${name} is one-way and cannot be used in reverse.`);
}
path = path.concat(
wayNodes.slice(nextNodeIndex + 1, curNodeIndex + 1)
.reverse()
);
}
currentNode = nextNode;
}
// Split the path into segments between stops
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx) {
const begin = elements[lineStops[stopIdx]].tags.ref;
const beginIdx = path.indexOf(lineStops[stopIdx]);
const end = elements[lineStops[stopIdx + 1]].tags.ref;
const endIdx = path.indexOf(
lineStops[stopIdx + 1],
beginIdx
) + 1;
const id = `${begin}-${end}`;
const nodesIds = path.slice(beginIdx, endIdx);
if (id in segments) {
if (!util.arraysEqual(
nodesIds,
segments[id].properties.nodesIds
)) {
throw new Error(`Segment ${id} is defined as a
different sequence of nodes in two or more lines.`);
}
segments[id].properties.routes.push([lineRef, routeRef]);
} else {
segments[id] = turfHelpers.lineString(nodesIds.map(
nodeId => [
elements[nodeId].lon,
elements[nodeId].lat
]
), {
// Keep track of the original sequence of nodes to
// compare with duplicates
nodesIds,
routes: [[lineRef, routeRef]]
});
segments[id].properties.length = (
1000 * turfLength(segments[id]));
}
}
routes.push({
from,
via,
to,
name,
state
});
}
lines[lineRef] = {
color,
routes
};
}
// Remove OSM nodes from segments that were only used for checking validity
for (const segment of Object.values(segments)) {
delete segment.properties.nodesIds;
}
return { stops, lines, segments };
};
exports.fetch = fetch;