Update network fetching to be more generic
This commit is contained in:
parent
aeef4b5ae9
commit
17d36a032c
|
@ -3,8 +3,13 @@
|
||||||
import * as network from '../src/tam/network.js';
|
import * as network from '../src/tam/network.js';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
const lines = ['1', '2', '3', '4'];
|
/** Geographical bounds of the Montpellier metropolitan area. */
|
||||||
const data = await network.fetch(lines);
|
const bounds = [
|
||||||
|
"43.49552248630757", "3.660507202148437",
|
||||||
|
"43.73736766145917", "4.092750549316406",
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = await network.fetch("TaM", "tram", bounds);
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
new URL("../src/tam/network.json", import.meta.url),
|
new URL("../src/tam/network.json", import.meta.url),
|
||||||
|
|
|
@ -17,33 +17,24 @@ import * as util from "../util.js";
|
||||||
import * as osm from "./sources/osm.js";
|
import * as osm from "./sources/osm.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route of a line of a transport network.
|
* Rectangle area of geographical points.
|
||||||
* @typedef {Object} Route
|
*
|
||||||
* @property {string} from Name of the starting point of the route.
|
* Should contain four values, [lat1, lon1, lat2, lon2], corresponding to two
|
||||||
* @property {string} to Name of the ending point of the route.
|
* opposed corners of the rectangle.
|
||||||
* @property {string?} via Optional name of a major intermediate
|
* @typedef {Array.<string>} Bounds
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop in a transport network (as a GeoJSON feature point).
|
* Stop in a transport network (as a GeoJSON feature point).
|
||||||
* @typedef {Object} Stop
|
* @typedef {Object} Stop
|
||||||
* @property {string} type Always equal to "Feature".
|
* @property {string} type Always equal to "Feature".
|
||||||
* @property {string} id Stop identifier (unique in each network.).
|
* @property {string} id Stop identifier (unique in each network).
|
||||||
* @property {Object} properties
|
* @property {Object} properties
|
||||||
* @property {string} properties.name Human-readable name of the stop.
|
* @property {string} properties.name Human-readable name of the stop.
|
||||||
* @property {string} properties.node Associated node ID in OpenStreetMap.
|
* @property {string} properties.node Associated node ID in OpenStreetMap.
|
||||||
* @property {Array.<Array.[string,number]>} properties.routes
|
* @property {Array.<Array.[string,number]>} properties.routes
|
||||||
* List of transport lines using this stop (as pairs of
|
* List of transport lines using this stop (as pairs containing the
|
||||||
* line and route identifiers).
|
* line number and line route identifier).
|
||||||
* @property {Object} geometry
|
* @property {Object} geometry
|
||||||
* @property {string} geometry.type Always equal to "Point"
|
* @property {string} geometry.type Always equal to "Point"
|
||||||
* @property {Array.<number>} geometry.coordinates
|
* @property {Array.<number>} geometry.coordinates
|
||||||
|
@ -51,295 +42,66 @@ import * as osm from "./sources/osm.js";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Planned segment routing between two stops of a transport network
|
* Find all public transport stops matching a set of criteria.
|
||||||
* (as a GeoJSON feature line string).
|
* @param {string} network Name of the public transport network
|
||||||
* @typedef {Object} Segment
|
* to which the stops must belong.
|
||||||
* @property {string} type Always equal to "Feature".
|
* @param {string} type Type of public transport vehicle.
|
||||||
* @property {string} id Segment identifier (format: `{begin}-{end}`).
|
* @param {Bounds} bounds Area in which stops are searched.
|
||||||
* @property {Object} properties
|
* @return {Object.<string,Stop>} List of stops indexed by their ID.
|
||||||
* @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).
|
|
||||||
*/
|
*/
|
||||||
|
const fetchStops = async (network, type, bounds) => {
|
||||||
|
const filter = osm.buildFilter({
|
||||||
|
public_transport: "stop_position",
|
||||||
|
ref: true,
|
||||||
|
network,
|
||||||
|
[type]: "yes",
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementsList = (await osm.runQuery(`[out:json];
|
||||||
|
node${filter}(${bounds});
|
||||||
|
out body qt;
|
||||||
|
`)).elements;
|
||||||
|
const stops = {};
|
||||||
|
|
||||||
|
for (const stop of elementsList) {
|
||||||
|
stops[stop.tags.ref] = turfHelpers.point([
|
||||||
|
stop.lon,
|
||||||
|
stop.lat,
|
||||||
|
], {
|
||||||
|
name: stop.tags.name,
|
||||||
|
node: stop.id.toString(),
|
||||||
|
routes: [],
|
||||||
|
}, {
|
||||||
|
id: stop.tags.ref,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return stops;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edge of the graph for navigating between stops.
|
* Route between two OpenStreetMap nodes.
|
||||||
* @typedef {Object} NavigationEdge
|
* @typedef {Object} NavigationEdge
|
||||||
* @property {string} type Always equal to "Feature".
|
* @property {string} type Always equal to "Feature".
|
||||||
* @property {Object} properties
|
* @property {Object} properties
|
||||||
* @property {string} properties.begin ID of the stop or node at the beginning.
|
* @property {string} properties.begin ID of the node at the beginning.
|
||||||
* @property {string} properties.end ID of the stop or node at the end.
|
* @property {string} properties.end ID of the node at the end.
|
||||||
* @property {number} properties.length Length of this edge (meters).
|
* @property {number} properties.length Length of this path (meters).
|
||||||
* @property {Object} geometry
|
* @property {Object} geometry
|
||||||
* @property {string} geometry.type Always equal to "LineString".
|
* @property {string} geometry.type Always equal to "LineString".
|
||||||
* @property {Array.<Array.<number>>} geometry.coordinates
|
* @property {Array.<Array.<number>>} geometry.coordinates
|
||||||
* Sequence of points forming this edge (as longitude/latitude pairs).
|
* Sequence of points along this route (as longitude/latitude pairs).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Graph for navigating between stops.
|
* Navigation graph between OpenStreetMap nodes.
|
||||||
* @typedef {Object.<string,Object.<string,NavigationEdge>>} Navigation
|
* @typedef {Object.<string,Object.<string,NavigationEdge>>} Navigation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about a public transport network.
|
* Assemble a raw navigation graph from OSM data.
|
||||||
* @typedef {Object} Network
|
* @param {Array.<Object>} elementsList List of elements retrieved from OSM.
|
||||||
* @property {Object.<string,Stop>} stops List of stops.
|
* @param {Object.<string,Object>} elementsById OSM elements indexed by ID.
|
||||||
* @property {Object.<string,Line>} lines List of lines.
|
|
||||||
* @property {Object.<string,Segment>} segments List of segments.
|
|
||||||
* @property {Navigation} 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
|
|
||||||
relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join("|")})$"];
|
|
||||||
|
|
||||||
(
|
|
||||||
._;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
`)).elements;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) => {
|
|
||||||
const lines = {};
|
|
||||||
const stops = {};
|
|
||||||
const segments = {};
|
|
||||||
|
|
||||||
const routeMasters = elementsList.filter(osm.isTransportLine);
|
|
||||||
|
|
||||||
// Extract lines, associated stops and planned routes
|
|
||||||
for (const routeMaster of routeMasters) {
|
|
||||||
const lineRef = routeMaster.tags.ref;
|
|
||||||
const color = routeMaster.tags.colour || "#000000";
|
|
||||||
const routes = [];
|
|
||||||
|
|
||||||
for (const [routeRef, data] of routeMaster.members.entries()) {
|
|
||||||
const routeId = data.ref;
|
|
||||||
const route = elementsById[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 = elementsById[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,
|
|
||||||
node: ref.toString(),
|
|
||||||
routes: [[lineRef, routeRef]],
|
|
||||||
}, {
|
|
||||||
id: stop.tags.ref,
|
|
||||||
});
|
|
||||||
} 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 route’s 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 = elementsById[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 = elementsById[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 = elementsById[lineStops[stopIdx]].tags.ref;
|
|
||||||
const beginIdx = path.indexOf(lineStops[stopIdx]);
|
|
||||||
const end = elementsById[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 => [
|
|
||||||
elementsById[nodeId].lon,
|
|
||||||
elementsById[nodeId].lat
|
|
||||||
]
|
|
||||||
), {
|
|
||||||
// Keep track of the original sequence of nodes to
|
|
||||||
// compare with duplicates
|
|
||||||
nodesIds,
|
|
||||||
routes: [[lineRef, routeRef]],
|
|
||||||
begin: begin,
|
|
||||||
end: end,
|
|
||||||
}, { id });
|
|
||||||
|
|
||||||
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 { lines, stops, segments };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a graph for navigating between stops.
|
|
||||||
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
|
|
||||||
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
|
|
||||||
* @return {Object} Resulting graph and reverse arcs.
|
* @return {Object} Resulting graph and reverse arcs.
|
||||||
*/
|
*/
|
||||||
const createNavigationGraph = (elementsList, elementsById) => {
|
const createNavigationGraph = (elementsList, elementsById) => {
|
||||||
|
@ -364,7 +126,7 @@ const createNavigationGraph = (elementsList, elementsById) => {
|
||||||
let to = obj.nodes[i + 1].toString();
|
let to = obj.nodes[i + 1].toString();
|
||||||
let path = [from, to];
|
let path = [from, to];
|
||||||
|
|
||||||
// Make sure we can’t jump between rails at railway crossings
|
// Make sure we don’t switch between rails at railway crossings
|
||||||
if (i + 2 < obj.nodes.length
|
if (i + 2 < obj.nodes.length
|
||||||
&& osm.isRailwayCrossing(elementsById[to])) {
|
&& osm.isRailwayCrossing(elementsById[to])) {
|
||||||
const next = obj.nodes[i + 2].toString();
|
const next = obj.nodes[i + 2].toString();
|
||||||
|
@ -390,18 +152,17 @@ const createNavigationGraph = (elementsList, elementsById) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identify intermediate nodes of the navigation graph that can be simplified.
|
* Identify intermediate nodes that can be simplified in a navigation graph.
|
||||||
* @param {Set.<string>} stopsSet OSM IDs of stop nodes.
|
* @param {Set.<string>} keep ID of nodes that must be kept.
|
||||||
* @param {Navigation} navigation Input navigation graph.
|
* @param {Navigation} navigation Input navigation graph.
|
||||||
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
|
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
|
||||||
* @return {Set.<string>} Set of compressible nodes.
|
* @return {Set.<string>} Set of compressible nodes.
|
||||||
*/
|
*/
|
||||||
const findCompressibleNodes = (stopsSet, navigation, navigationReverse) => {
|
const findCompressibleNodes = (keep, navigation, navigationReverse) => {
|
||||||
const compressible = new Set();
|
const compressible = new Set();
|
||||||
|
|
||||||
for (const nodeId in navigation) {
|
for (const nodeId in navigation) {
|
||||||
if (stopsSet.has(nodeId)) {
|
if (keep.has(nodeId)) {
|
||||||
// Keep stop nodes
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,14 +428,11 @@ const cleanUpIsolatedNodes = (navigation, navigationReverse) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove and relink nodes that connect only two nodes or less.
|
* Remove and relink nodes that connect only two nodes or less.
|
||||||
* @param {Object.<string,Stop>} stops List of stops.
|
* @param {Set.<string>} keep ID of nodes that must be kept.
|
||||||
* @param {Navigation} navigation Input navigation graph.
|
* @param {Navigation} navigation Input navigation graph.
|
||||||
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
|
* @param {Object.<string,Set.<string>>} navigationReverse Reverse arcs.
|
||||||
*/
|
*/
|
||||||
const compressNavigationGraph = (stops, navigation, navigationReverse) => {
|
const compressNavigationGraph = (keep, navigation, navigationReverse) => {
|
||||||
const stopsSet = new Set(
|
|
||||||
Object.values(stops).map(stop => stop.properties.node)
|
|
||||||
);
|
|
||||||
let compressible = null;
|
let compressible = null;
|
||||||
let didCompress = true;
|
let didCompress = true;
|
||||||
|
|
||||||
|
@ -683,7 +441,7 @@ const compressNavigationGraph = (stops, navigation, navigationReverse) => {
|
||||||
|
|
||||||
while (didRemove) {
|
while (didRemove) {
|
||||||
compressible = findCompressibleNodes(
|
compressible = findCompressibleNodes(
|
||||||
stopsSet, navigation, navigationReverse
|
keep, navigation, navigationReverse
|
||||||
);
|
);
|
||||||
didRemove = removeDeadEnds(
|
didRemove = removeDeadEnds(
|
||||||
navigation, navigationReverse, compressible
|
navigation, navigationReverse, compressible
|
||||||
|
@ -721,24 +479,186 @@ const makeNavigationSegments = (navigation, elementsById) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch information about the network.
|
* Fetch the network of routes that connect the given nodes.
|
||||||
* @param {Array.<string>} lineRefs List of lines to fetch.
|
* @param {Set.<string>} nodes ID of nodes to connect.
|
||||||
* @returns {Network} Network metadata extracted from OSM.
|
* @param {string} type Type of public transport vehicle.
|
||||||
|
* @param {Bounds} bounds Rectangle bounding the network.
|
||||||
|
* @return {Navigation} Resulting graph.
|
||||||
*/
|
*/
|
||||||
export const fetch = async lineRefs => {
|
const fetchNavigationGraph = async (nodes, type, bounds) => {
|
||||||
const elementsList = await queryLines(lineRefs);
|
const filter = osm.buildFilter(osm.vehicleWayFilter(type));
|
||||||
|
const elementsList = (await osm.runQuery(`[out:json];
|
||||||
|
way${filter}(${bounds});
|
||||||
|
(._; >;);
|
||||||
|
out body qt;
|
||||||
|
`)).elements;
|
||||||
|
|
||||||
const elementsById = elementsList.reduce((prev, elt) => {
|
const elementsById = elementsList.reduce((prev, elt) => {
|
||||||
prev[elt.id] = elt;
|
prev[elt.id] = elt;
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const { lines, stops, segments } = processRoutes(elementsList, elementsById);
|
|
||||||
const { navigation, navigationReverse } = createNavigationGraph(
|
const { navigation, navigationReverse } = createNavigationGraph(
|
||||||
elementsList, elementsById
|
elementsList, elementsById
|
||||||
);
|
);
|
||||||
|
|
||||||
compressNavigationGraph(stops, navigation, navigationReverse);
|
compressNavigationGraph(nodes, navigation, navigationReverse);
|
||||||
makeNavigationSegments(navigation, elementsById);
|
makeNavigationSegments(navigation, elementsById);
|
||||||
|
|
||||||
return { navigation, lines, stops, segments };
|
return navigation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 route.
|
||||||
|
* @property {Array.<string>} stops Sequence of stop IDs along the route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Line of a transport network.
|
||||||
|
* @typedef {Object} Line
|
||||||
|
* @property {string} color Hexadecimal color code of the line.
|
||||||
|
* @property {Array.<Route>} routes Routes of the line.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all public transport lines and routes matching a set of criteria.
|
||||||
|
* @param {string} network Name of the public transport network
|
||||||
|
* to which the lines must belong.
|
||||||
|
* @param {string} type Type of public transport vehicle.
|
||||||
|
* @param {Bounds} bounds Area bounding the public transport network.
|
||||||
|
* @return {Object.<string,Line>} Assembled information about lines and routes.
|
||||||
|
*/
|
||||||
|
const queryLines = async (network, type, bounds) => {
|
||||||
|
const routeFilter = osm.buildFilter({
|
||||||
|
type: "route",
|
||||||
|
route: type,
|
||||||
|
network,
|
||||||
|
});
|
||||||
|
const masterFilter = osm.buildFilter({
|
||||||
|
type: "route_master",
|
||||||
|
route_master: type,
|
||||||
|
network,
|
||||||
|
});
|
||||||
|
const elementsList = (await osm.runQuery(`[out:json];
|
||||||
|
relation${routeFilter}(${bounds});
|
||||||
|
relation${masterFilter}(br);
|
||||||
|
(._; >>;);
|
||||||
|
out body;
|
||||||
|
`)).elements;
|
||||||
|
|
||||||
|
const elementsById = elementsList.reduce((prev, elt) => {
|
||||||
|
prev[elt.id] = elt;
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const lines = {};
|
||||||
|
const routeMasters = elementsList.filter(osm.isTransportLine);
|
||||||
|
|
||||||
|
// Extract lines, associated stops and planned routes
|
||||||
|
for (const routeMaster of routeMasters) {
|
||||||
|
const lineRef = routeMaster.tags.ref;
|
||||||
|
const color = routeMaster.tags.colour.toUpperCase() || "#000000";
|
||||||
|
const routes = [];
|
||||||
|
|
||||||
|
for (const [routeRef, data] of routeMaster.members.entries()) {
|
||||||
|
const routeId = data.ref;
|
||||||
|
const route = elementsById[routeId];
|
||||||
|
const { from, via, to, name } = route.tags;
|
||||||
|
|
||||||
|
// 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 }) => osm.isInitialRouteRole(role)
|
||||||
|
)) {
|
||||||
|
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
|
||||||
|
const stops = route.members.slice(0, relationPivot)
|
||||||
|
.filter(({ role }) => role === "stop")
|
||||||
|
.map(({ ref }) => elementsById[ref].tags.ref);
|
||||||
|
|
||||||
|
routes.push({
|
||||||
|
from,
|
||||||
|
...(via && { via }),
|
||||||
|
to,
|
||||||
|
name,
|
||||||
|
stops,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lines[lineRef] = {
|
||||||
|
color,
|
||||||
|
routes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record which lines use which stops.
|
||||||
|
* @param {Object.<string,Stop>} stops List of stops.
|
||||||
|
* @param {Object.<string,Line>} lines List of lines.
|
||||||
|
*/
|
||||||
|
const recordStops = (stops, lines) => {
|
||||||
|
for (const [lineRef, line] of Object.entries(lines)) {
|
||||||
|
for (const [routeRef, route] of line.routes.entries()) {
|
||||||
|
for (const stop of route.stops) {
|
||||||
|
const routes = stops[stop].properties.routes;
|
||||||
|
|
||||||
|
if (routes.findIndex(([testLineRef, testRouteRef]) =>
|
||||||
|
lineRef === testLineRef && routeRef === testRouteRef
|
||||||
|
) === -1) {
|
||||||
|
routes.push([lineRef, routeRef]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Navigation} navigation Graph for navigating between stops.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch information about a public transport network.
|
||||||
|
* @param {string} network Name of the public transport network.
|
||||||
|
* @param {string} type Type of public transport vehicle.
|
||||||
|
* @param {Bounds} bounds Area bounding the public transport network.
|
||||||
|
* @returns {Network} Network metadata extracted from OSM.
|
||||||
|
*/
|
||||||
|
export const fetch = async (network, type, bounds) => {
|
||||||
|
const stops = await fetchStops(network, type, bounds);
|
||||||
|
const stopIds = new Set(
|
||||||
|
Object.values(stops).map(stop => stop.properties.node)
|
||||||
|
);
|
||||||
|
const navigation = await fetchNavigationGraph(stopIds, type, bounds);
|
||||||
|
const lines = await queryLines(network, type, bounds);
|
||||||
|
recordStops(stops, lines);
|
||||||
|
return { navigation, stops, lines };
|
||||||
};
|
};
|
||||||
|
|
27651
src/tam/network.json
27651
src/tam/network.json
File diff suppressed because it is too large
Load Diff
|
@ -7,10 +7,65 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { isObject } from "../../util.js";
|
import { isObject } from "../../util.js";
|
||||||
|
|
||||||
|
/** Replace a value for use in an Overpass query. */
|
||||||
|
export const escape = value =>
|
||||||
|
value.replace("'", "\\'").replace('"', '\\"').replace("\\", "\\\\");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a set of tag queries into an Overpass set of filters.
|
||||||
|
*
|
||||||
|
* @param {Object} tags A set of tag queries.
|
||||||
|
* @return {string} Corresponding Overpass filter.
|
||||||
|
*/
|
||||||
|
export const buildFilter = tags => Object.entries(tags).filter(
|
||||||
|
([key, value]) => {
|
||||||
|
return value !== undefined && value !== null;
|
||||||
|
}
|
||||||
|
).map(
|
||||||
|
([key, value]) => {
|
||||||
|
if (value === true) {
|
||||||
|
return `[${key}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === false) {
|
||||||
|
return `[!${key}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
// See <https://github.com/drolbr/Overpass-API/issues/92>
|
||||||
|
return `[${key}~"^$"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${key}="${escape(value)}"]`
|
||||||
|
}
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate tag query for matching ways that can be used by a given
|
||||||
|
* public transport vehicle.
|
||||||
|
* @param {string} type Type of public transport vehicle.
|
||||||
|
* @return {Object} Set of tag queries.
|
||||||
|
*/
|
||||||
|
export const vehicleWayFilter = type => {
|
||||||
|
switch (type) {
|
||||||
|
case "bus":
|
||||||
|
return {highway: true};
|
||||||
|
|
||||||
|
case "train":
|
||||||
|
case "subway":
|
||||||
|
case "light_rail":
|
||||||
|
case "tram":
|
||||||
|
return {railway: type};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown vehicle type: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit a query to an Overpass endpoint.
|
* Submit a query to an Overpass endpoint.
|
||||||
*
|
*
|
||||||
* See <https://wiki.osm.org/Overpass_API/Overpass_QL> for more
|
* @see <https://wiki.osm.org/Overpass_API/Overpass_QL> for more
|
||||||
* information on the Overpass Query Language (Overpass QL).
|
* information on the Overpass Query Language (Overpass QL).
|
||||||
* @async
|
* @async
|
||||||
* @param {string} query Query to send.
|
* @param {string} query Query to send.
|
||||||
|
@ -22,10 +77,11 @@ import { isObject } from "../../util.js";
|
||||||
export const runQuery = (
|
export const runQuery = (
|
||||||
query,
|
query,
|
||||||
endpoint = "https://lz4.overpass-api.de/api/interpreter"
|
endpoint = "https://lz4.overpass-api.de/api/interpreter"
|
||||||
) => (
|
) => {
|
||||||
axios.post(endpoint, `data=${query}`)
|
console.log("Running query:", query);
|
||||||
.then(res => res.data)
|
return axios.post(endpoint, `data=${query}`)
|
||||||
);
|
.then(res => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a link to view a node.
|
* Create a link to view a node.
|
||||||
|
@ -37,7 +93,7 @@ export const viewNode = id => `https://www.osm.org/node/${id}`;
|
||||||
/**
|
/**
|
||||||
* Determine if an OSM way is one-way or not.
|
* Determine if an OSM way is one-way or not.
|
||||||
*
|
*
|
||||||
* See <https://wiki.osm.org/Key:oneway> for details.
|
* @see <https://wiki.osm.org/Key:oneway>
|
||||||
* @param {Object} obj OSM way object.
|
* @param {Object} obj OSM way object.
|
||||||
* @returns {boolean} Whether the way is one-way.
|
* @returns {boolean} Whether the way is one-way.
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +107,7 @@ export const isOneWay = obj => (
|
||||||
/**
|
/**
|
||||||
* Determine if a node is a railway crossing or not.
|
* Determine if a node is a railway crossing or not.
|
||||||
*
|
*
|
||||||
* See <https://wiki.osm.org/Tag:railway=railway_crossing> for details.
|
* @see <https://wiki.osm.org/Tag:railway=railway_crossing>
|
||||||
* @param {Object} obj OSM node object.
|
* @param {Object} obj OSM node object.
|
||||||
* @return {boolean} Whether the node is a railway crossing.
|
* @return {boolean} Whether the node is a railway crossing.
|
||||||
*/
|
*/
|
||||||
|
@ -64,8 +120,8 @@ export const isRailwayCrossing = obj => (
|
||||||
/**
|
/**
|
||||||
* Determine if an OSM object is a public transport line (route master).
|
* Determine if an OSM object is a public transport line (route master).
|
||||||
*
|
*
|
||||||
* See <https://wiki.osm.org/Relation:route_master>
|
* @see <https://wiki.osm.org/Relation:route_master>
|
||||||
* and <https://wiki.osm.org/Public_transport#Route_Master_relations>.
|
* @see <https://wiki.osm.org/Public_transport#Route_Master_relations>
|
||||||
* @param {Object} obj OSM relation object.
|
* @param {Object} obj OSM relation object.
|
||||||
* @returns {boolean} Whether the relation is a public transport line.
|
* @returns {boolean} Whether the relation is a public transport line.
|
||||||
*/
|
*/
|
||||||
|
@ -74,3 +130,17 @@ export const isTransportLine = obj => (
|
||||||
isObject(obj.tags) &&
|
isObject(obj.tags) &&
|
||||||
obj.tags.type === "route_master"
|
obj.tags.type === "route_master"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a public transport route member has a valid role for
|
||||||
|
* the initial segment of stops and platforms according to PTv2.
|
||||||
|
*
|
||||||
|
* @see <https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes>
|
||||||
|
* @param {string} role Role to check.
|
||||||
|
* @returns {boolean} Whether the role is valid.
|
||||||
|
*/
|
||||||
|
export const isInitialRouteRole = role => (
|
||||||
|
role === "stop" || role === "stop_entry_only" || role === "stop_exit_only"
|
||||||
|
|| role === "platform" || role === "platform_entry_only"
|
||||||
|
|| role === "platform_exit_only"
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in New Issue