Compare commits
No commits in common. "a51a80a4b3019ab278096c25a7b72b842a49e460" and "452c7b9586eaa2dd048d29519ff325c6988512b7" have entirely different histories.
a51a80a4b3
...
452c7b9586
|
@ -1,43 +1,23 @@
|
||||||
#!/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[\
|
console.log("graph[layout=neato, overlap=scalexy, splines=true, outputorder=nodesfirst]");
|
||||||
layout=neato, \
|
|
||||||
mode=ipsep, \
|
|
||||||
overlap=ipsep, \
|
|
||||||
outputorder=nodesfirst, \
|
|
||||||
]`);
|
|
||||||
|
|
||||||
const stops = new Set();
|
for (const [stopId, stop] of Object.entries(network.stops)) {
|
||||||
|
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 (!stops.has(beginId)) {
|
if (!(beginId in network.stops)) {
|
||||||
junctions.add(beginId);
|
junctions.add(beginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const endId in begin) {
|
for (const endId in begin) {
|
||||||
if (!stops.has(endId)) {
|
if (!(endId in network.stops)) {
|
||||||
junctions.add(endId);
|
junctions.add(endId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +29,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}[len=2]`);
|
console.log(`${beginId} -> ${endId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ import * as osm from "./sources/osm.js";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edge of the graph for navigating between stops.
|
* Edge of the graph for out-of-route navigation 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,18 +83,13 @@ 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 {Navigation} navigation
|
* @property {Object.<string,Object.<string,NavigationEdge>>} navigation
|
||||||
* Graph for out-of-route navigation between stops.
|
* Graph for out-of-route navigation between stops.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -337,20 +332,45 @@ different sequence of nodes in two or more lines.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a graph for navigating between stops.
|
* Create a graph for navigating between stops outside of
|
||||||
|
* 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 = (elementsList, elementsById) => {
|
const createNavigationGraph = (stops, 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[obj.id] = {};
|
navigation[getNavigationId(obj.id)] = {};
|
||||||
navigationReverse[obj.id] = {};
|
navigationReverse[getNavigationId(obj.id)] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,202 +378,95 @@ const createNavigationGraph = (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 (let i = 0; i + 1 < obj.nodes.length; ++i) {
|
for (const [from, to] of pairs) {
|
||||||
const from = obj.nodes[i];
|
navigation[from][to] = [from, to];
|
||||||
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;
|
|
||||||
navigationReverse[to][from] = true;
|
navigationReverse[to][from] = true;
|
||||||
|
|
||||||
if (!oneWay) {
|
if (!oneWay) {
|
||||||
const reversePath = [...path];
|
navigation[to][from] = [to, from];
|
||||||
reversePath.reverse();
|
|
||||||
navigation[to][from] = reversePath;
|
|
||||||
navigationReverse[from][to] = true;
|
navigationReverse[from][to] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { navigation, navigationReverse };
|
// Mark nodes of the graph to be kept
|
||||||
};
|
const nodesToKeep = {};
|
||||||
|
|
||||||
/**
|
for (const nodeId in navigation) {
|
||||||
* Remove and relink nodes that connect only two nodes or less.
|
if (nodeId in stops) {
|
||||||
* @param {Object.<string,Stop>} stops List of stops.
|
// Keep stop nodes
|
||||||
* @param {Navigation} navigation Input navigation graph.
|
nodesToKeep[nodeId] = true;
|
||||||
* @param {Object.<string,Object.<string,boolean>>} navigationReverse
|
continue;
|
||||||
* 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])
|
|
||||||
);
|
|
||||||
|
|
||||||
let removedDeadEnds = true;
|
|
||||||
const nodesToCompress = {};
|
|
||||||
|
|
||||||
while (removedDeadEnds) {
|
|
||||||
// Identify nodes to be compressed
|
|
||||||
for (const nodeId in navigation) {
|
|
||||||
if (nodeId in stopsReverse) {
|
|
||||||
// Keep stop nodes
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 entries = new Set(Object.keys(navigationReverse[nodeId]));
|
||||||
const usedNodes = {};
|
const exits = new Set(Object.keys(navigation[nodeId]));
|
||||||
|
|
||||||
for (const beginId in navigation) {
|
// Keep split nodes, i.e. nodes with at least two exit nodes
|
||||||
if (beginId in nodesToCompress) {
|
// and one entry node that are all distinct from each other
|
||||||
|
if (entries.size >= 1) {
|
||||||
|
if (exits.size >= 3) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
usedNodes[beginId] = true;
|
if (exits.size === 2) {
|
||||||
|
for (const entry of entries) {
|
||||||
// Start a DFS from each node to be kept
|
if (!exits.has(entry)) {
|
||||||
const begin = navigation[beginId];
|
nodesToKeep[nodeId] = true;
|
||||||
const stack = [];
|
continue;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove dead-end nodes
|
// Keep junction nodes, i.e. nodes with at least two entry nodes
|
||||||
removedDeadEnds = false;
|
// and one exit node that are all distinct from each other
|
||||||
|
if (exits.size >= 1) {
|
||||||
|
if (entries.size >= 3) {
|
||||||
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const beginId in navigation) {
|
if (entries.size === 2) {
|
||||||
if (!(beginId in usedNodes)) {
|
for (const exit of exits) {
|
||||||
for (const neighborId in navigation[beginId]) {
|
if (!entries.has(exit)) {
|
||||||
delete navigationReverse[neighborId][beginId];
|
nodesToKeep[nodeId] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const neighborId in navigationReverse[beginId]) {
|
|
||||||
delete navigation[neighborId][beginId];
|
|
||||||
}
|
|
||||||
|
|
||||||
delete navigation[beginId];
|
|
||||||
delete navigationReverse[beginId];
|
|
||||||
removedDeadEnds = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform node compression
|
// Compress edges between nodes of interest
|
||||||
for (const beginId in navigation) {
|
for (const beginId in nodesToKeep) {
|
||||||
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) {
|
||||||
if (succId in nodesToCompress) {
|
stack.push(succId);
|
||||||
stack.push(succId);
|
parent[succId] = beginId;
|
||||||
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 nodesToCompress)) {
|
if (endId in nodesToKeep) {
|
||||||
// Found another kept node
|
if (endId in begin) {
|
||||||
// Collect and remove intermediate nodes
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const reversePath = [endId];
|
const reversePath = [endId];
|
||||||
let trackback = parent[endId];
|
let trackback = parent[endId];
|
||||||
let oneWay = !(trackback in end);
|
let oneWay = !(trackback in end);
|
||||||
|
@ -563,7 +476,6 @@ const compressNavigationGraph = (stops, navigation, navigationReverse) => {
|
||||||
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,24 +483,20 @@ const compressNavigationGraph = (stops, navigation, navigationReverse) => {
|
||||||
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 && !(beginId in end)) {
|
if (!oneWay) {
|
||||||
end[beginId] = reversePath;
|
delete end[reversePath[1]];
|
||||||
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) {
|
||||||
|
@ -603,23 +511,30 @@ 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[nodeId].lon,
|
elementsById[getOSMId(nodeId)].lon,
|
||||||
elementsById[nodeId].lat
|
elementsById[getOSMId(nodeId)].lat
|
||||||
]
|
]
|
||||||
), {
|
), {
|
||||||
begin: beginId,
|
begin: beginId,
|
||||||
|
@ -629,6 +544,8 @@ const makeNavigationSegments = (navigation, elementsById) => {
|
||||||
begin[endId].properties.length = 1000 * turfLength(begin[endId]);
|
begin[endId].properties.length = 1000 * turfLength(begin[endId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return navigation;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -644,12 +561,7 @@ export const fetch = async lineRefs => {
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const { lines, stops, segments } = processRoutes(elementsList, elementsById);
|
const { lines, stops, segments } = processRoutes(elementsList, elementsById);
|
||||||
const { navigation, navigationReverse } = createNavigationGraph(
|
const navigation = createNavigationGraph(stops, elementsList, elementsById);
|
||||||
elementsList, elementsById
|
|
||||||
);
|
|
||||||
|
|
||||||
compressNavigationGraph(stops, navigation, navigationReverse);
|
|
||||||
makeNavigationSegments(navigation, elementsById);
|
|
||||||
|
|
||||||
return { navigation, lines, stops, segments };
|
return { navigation, lines, stops, segments };
|
||||||
};
|
};
|
||||||
|
|
44490
src/tam/network.json
44490
src/tam/network.json
File diff suppressed because it is too large
Load Diff
|
@ -48,19 +48,6 @@ 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).
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue