tracktracker/src/tam/network.js

255 lines
8.7 KiB
JavaScript
Raw Normal View History

2020-07-16 23:41:05 +00:00
/**
* @fileoverview
*
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.
*
* 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.
2020-07-16 23:41:05 +00:00
*/
const turfHelpers = require("@turf/helpers");
const turfLength = require("@turf/length").default;
const util = require("../util");
const osm = require("./sources/osm");
/**
2020-07-16 23:41:05 +00:00
* 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
2020-07-16 22:16:54 +00:00
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)
2020-07-16 22:16:54 +00:00
.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;
2020-07-24 23:16:36 +00:00
const beginIdx = path.indexOf(lineStops[stopIdx]);
const end = elements[lineStops[stopIdx + 1]].tags.ref;
2020-07-24 23:16:36 +00:00
const endIdx = path.indexOf(
lineStops[stopIdx + 1],
beginIdx
) + 1;
const id = `${begin}-${end}`;
2020-07-24 23:16:36 +00:00
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;