Compare commits

..

2 Commits

4 changed files with 17422 additions and 27229 deletions

View File

@ -1,23 +1,43 @@
#!/usr/bin/env -S node --experimental-json-modules #!/usr/bin/env -S node --experimental-json-modules
import network from "../src/tam/network.json"; import network from "../src/tam/network.json";
import color from "color";
console.log("digraph {"); console.log("digraph {");
console.log("graph[layout=neato, overlap=scalexy, splines=true, outputorder=nodesfirst]"); console.log(`graph[\
layout=neato, \
mode=ipsep, \
overlap=ipsep, \
outputorder=nodesfirst, \
]`);
for (const [stopId, stop] of Object.entries(network.stops)) { const stops = new Set();
console.log(`${stopId}[label="${stop.properties.name}", shape=box]`);
for (const stop of Object.values(network.stops)) {
stops.add(stop.properties.node);
const {node, name, routes} = stop.properties;
const backgrounds = routes.map(([line]) => network.lines[line].color);
const font = color(backgrounds[0]).isLight() ? "black" : "white";
console.log(`${node}[\
label="${name}", \
style=striped, \
fillcolor="${[...new Set(backgrounds)].join(":")}", \
fontcolor="${font}", \
shape=box, \
]`);
} }
const junctions = new Set(); const junctions = new Set();
for (const [beginId, begin] of Object.entries(network.navigation)) { for (const [beginId, begin] of Object.entries(network.navigation)) {
if (!(beginId in network.stops)) { if (!stops.has(beginId)) {
junctions.add(beginId); junctions.add(beginId);
} }
for (const endId in begin) { for (const endId in begin) {
if (!(endId in network.stops)) { if (!stops.has(endId)) {
junctions.add(endId); junctions.add(endId);
} }
} }
@ -29,7 +49,7 @@ for (const junction of junctions) {
for (const [beginId, begin] of Object.entries(network.navigation)) { for (const [beginId, begin] of Object.entries(network.navigation)) {
for (const endId in begin) { for (const endId in begin) {
console.log(`${beginId} -> ${endId}`); console.log(`${beginId} -> ${endId}[len=2]`);
} }
} }

View File

@ -70,7 +70,7 @@ import * as osm from "./sources/osm.js";
*/ */
/** /**
* Edge of the graph for out-of-route navigation between stops. * Edge of the graph for navigating between stops.
* @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
@ -83,13 +83,18 @@ import * as osm from "./sources/osm.js";
* Sequence of points forming this edge (as longitude/latitude pairs). * Sequence of points forming this edge (as longitude/latitude pairs).
*/ */
/**
* Graph for navigating between stops.
* @typedef {Object.<string,Object.<string,NavigationEdge>>} Navigation
*/
/** /**
* Information about a public transport network. * Information about a public transport network.
* @typedef {Object} Network * @typedef {Object} Network
* @property {Object.<string,Stop>} stops List of stops. * @property {Object.<string,Stop>} stops List of stops.
* @property {Object.<string,Line>} lines List of lines. * @property {Object.<string,Line>} lines List of lines.
* @property {Object.<string,Segment>} segments List of segments. * @property {Object.<string,Segment>} segments List of segments.
* @property {Object.<string,Object.<string,NavigationEdge>>} navigation * @property {Navigation} navigation
* Graph for out-of-route navigation between stops. * Graph for out-of-route navigation between stops.
*/ */
@ -332,45 +337,20 @@ different sequence of nodes in two or more lines.`);
}; };
/** /**
* Create a graph for navigating between stops outside of * Create a graph for navigating between stops.
* regular planned routes.
* @property {Object.<string,Stop>} stops List of stops.
* @param {Array.<Object>} elementsList List of nodes retrieved from OSM. * @param {Array.<Object>} elementsList List of nodes retrieved from OSM.
* @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID. * @param {Object.<string,Object>} elementsById OSM nodes indexed by their ID.
* @return {Object.<string,Object.<string,NavigationEdge>} Resulting graph. * @return {Object.<string,Object.<string,NavigationEdge>} Resulting graph.
*/ */
const createNavigationGraph = (stops, elementsList, elementsById) => { const createNavigationGraph = (elementsList, elementsById) => {
// Graph of network stops and junctions
const navigation = {}; const navigation = {};
// Predecessors of each graph node
const navigationReverse = {}; 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
? stopsReverse[objId]
: 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 // Create graph nodes from OSM nodes
for (const obj of elementsList) { for (const obj of elementsList) {
if (obj.type === "node") { if (obj.type === "node") {
navigation[getNavigationId(obj.id)] = {}; navigation[obj.id] = {};
navigationReverse[getNavigationId(obj.id)] = {}; navigationReverse[obj.id] = {};
} }
} }
@ -378,95 +358,202 @@ const createNavigationGraph = (stops, elementsList, elementsById) => {
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(
(node, i) => [
getNavigationId(node),
getNavigationId(obj.nodes[i + 1]),
]
);
for (const [from, to] of pairs) { for (let i = 0; i + 1 < obj.nodes.length; ++i) {
navigation[from][to] = [from, to]; const from = obj.nodes[i];
let to = obj.nodes[i + 1];
let path = [from.toString(), to.toString()];
// Make sure we cant 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;
navigationReverse[to][from] = true; navigationReverse[to][from] = true;
if (!oneWay) { if (!oneWay) {
navigation[to][from] = [to, from]; const reversePath = [...path];
reversePath.reverse();
navigation[to][from] = reversePath;
navigationReverse[from][to] = true; navigationReverse[from][to] = true;
} }
} }
} }
} }
// Mark nodes of the graph to be kept return { navigation, navigationReverse };
const nodesToKeep = {}; };
for (const nodeId in navigation) { /**
if (nodeId in stops) { * Remove and relink nodes that connect only two nodes or less.
// Keep stop nodes * @param {Object.<string,Stop>} stops List of stops.
nodesToKeep[nodeId] = true; * @param {Navigation} navigation Input navigation graph.
continue; * @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])
);
const entries = new Set(Object.keys(navigationReverse[nodeId])); let removedDeadEnds = true;
const exits = new Set(Object.keys(navigation[nodeId])); const nodesToCompress = {};
// Keep split nodes, i.e. nodes with at least two exit nodes while (removedDeadEnds) {
// and one entry node that are all distinct from each other // Identify nodes to be compressed
if (entries.size >= 1) { for (const nodeId in navigation) {
if (exits.size >= 3) { if (nodeId in stopsReverse) {
nodesToKeep[nodeId] = true; // Keep stop nodes
continue; continue;
} }
if (exits.size === 2) { const entries = new Set(Object.keys(navigationReverse[nodeId]));
for (const entry of entries) { const exits = new Set(Object.keys(navigation[nodeId]));
if (!exits.has(entry)) {
nodesToKeep[nodeId] = true; // Keep split nodes, i.e. nodes with at least two exit nodes
continue; // 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;
}
}
}
if (isSplit) {
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) {
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;
}
// Find nodes that cannot be used to directly link up two kept nodes
const usedNodes = {};
for (const beginId in navigation) {
if (beginId in nodesToCompress) {
continue;
}
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);
}
} }
} }
} }
} }
// Keep junction nodes, i.e. nodes with at least two entry nodes // Remove dead-end nodes
// and one exit node that are all distinct from each other removedDeadEnds = false;
if (exits.size >= 1) {
if (entries.size >= 3) {
nodesToKeep[nodeId] = true;
continue;
}
if (entries.size === 2) { for (const beginId in navigation) {
for (const exit of exits) { if (!(beginId in usedNodes)) {
if (!entries.has(exit)) { for (const neighborId in navigation[beginId]) {
nodesToKeep[nodeId] = true; delete navigationReverse[neighborId][beginId];
continue;
}
} }
for (const neighborId in navigationReverse[beginId]) {
delete navigation[neighborId][beginId];
}
delete navigation[beginId];
delete navigationReverse[beginId];
removedDeadEnds = true;
} }
} }
} }
// Compress edges between nodes of interest // Perform node compression
for (const beginId in nodesToKeep) { for (const beginId in navigation) {
if (beginId in nodesToCompress) {
continue;
}
// Start a DFS from each node to be kept
const begin = navigation[beginId]; const begin = navigation[beginId];
const stack = []; const stack = [];
const parent = {[beginId]: beginId}; const parent = {[beginId]: beginId};
for (const succId in begin) { for (const succId in begin) {
stack.push(succId); if (succId in nodesToCompress) {
parent[succId] = beginId; stack.push(succId);
parent[succId] = beginId;
}
} }
while (stack.length > 0) { while (stack.length > 0) {
const endId = stack.pop(); const endId = stack.pop();
const end = navigation[endId]; const end = navigation[endId];
if (endId in nodesToKeep) { if (!(endId in nodesToCompress)) {
if (endId in begin) { // Found another kept node
continue; // Collect and remove intermediate nodes
}
const reversePath = [endId]; const reversePath = [endId];
let trackback = parent[endId]; let trackback = parent[endId];
let oneWay = !(trackback in end); let oneWay = !(trackback in end);
@ -476,6 +563,7 @@ const createNavigationGraph = (stops, elementsList, elementsById) => {
oneWay = oneWay || !(parent[trackback] in navigation[trackback]); oneWay = oneWay || !(parent[trackback] in navigation[trackback]);
delete navigation[trackback]; delete navigation[trackback];
delete navigationReverse[trackback];
trackback = parent[trackback]; trackback = parent[trackback];
} }
@ -483,20 +571,24 @@ const createNavigationGraph = (stops, elementsList, elementsById) => {
const forwardPath = [...reversePath]; const forwardPath = [...reversePath];
forwardPath.reverse(); forwardPath.reverse();
// Create edges to link both nodes directly
delete begin[forwardPath[1]]; delete begin[forwardPath[1]];
delete navigationReverse[endId][reversePath[1]];
delete end[reversePath[1]];
delete navigationReverse[beginId][forwardPath[1]];
if (!(endId in begin)) { if (!(endId in begin)) {
begin[endId] = forwardPath; begin[endId] = forwardPath;
navigationReverse[endId][beginId] = true;
} }
if (!oneWay) { if (!oneWay && !(beginId in end)) {
delete end[reversePath[1]]; end[beginId] = reversePath;
navigationReverse[beginId][endId] = true;
if (!(beginId in end)) {
end[beginId] = reversePath;
}
} }
} else { } else {
// Continue the traversal down unused nodes
let isFirst = true; let isFirst = true;
for (const succId in end) { for (const succId in end) {
@ -511,30 +603,23 @@ 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 /**
* 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) => {
for (const [beginId, begin] of Object.entries(navigation)) { for (const [beginId, begin] of Object.entries(navigation)) {
for (const endId in begin) { for (const endId in begin) {
begin[endId] = turfHelpers.lineString(begin[endId].map( begin[endId] = turfHelpers.lineString(begin[endId].map(
nodeId => [ nodeId => [
elementsById[getOSMId(nodeId)].lon, elementsById[nodeId].lon,
elementsById[getOSMId(nodeId)].lat elementsById[nodeId].lat
] ]
), { ), {
begin: beginId, begin: beginId,
@ -544,8 +629,6 @@ non-junction node ${endId}`);
begin[endId].properties.length = 1000 * turfLength(begin[endId]); begin[endId].properties.length = 1000 * turfLength(begin[endId]);
} }
} }
return navigation;
}; };
/** /**
@ -561,7 +644,12 @@ export const fetch = async lineRefs => {
}, {}); }, {});
const { lines, stops, segments } = processRoutes(elementsList, elementsById); const { lines, stops, segments } = processRoutes(elementsList, elementsById);
const navigation = createNavigationGraph(stops, elementsList, elementsById); const { navigation, navigationReverse } = createNavigationGraph(
elementsList, elementsById
);
compressNavigationGraph(stops, navigation, navigationReverse);
makeNavigationSegments(navigation, elementsById);
return { navigation, lines, stops, segments }; return { navigation, lines, stops, segments };
}; };

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,19 @@ export const isOneWay = obj => (
obj.tags.highway === "motorway") obj.tags.highway === "motorway")
); );
/**
* Determine if a node is a railway crossing or not.
*
* See <https://wiki.osm.org/Tag:railway=railway_crossing> for details.
* @param {Object} obj OSM node object.
* @return {boolean} Whether the node is a railway crossing.
*/
export const isRailwayCrossing = obj => (
obj.type === "node" &&
isObject(obj.tags) &&
(obj.tags.railway === "railway_crossing")
);
/** /**
* Determine if an OSM object is a public transport line (route master). * Determine if an OSM object is a public transport line (route master).
* *