119 lines
4.0 KiB
JavaScript
119 lines
4.0 KiB
JavaScript
'use strict';
|
|
|
|
function parseOsm(xml) {
|
|
const boundsMatch = xml.match(/<bounds\b([^>]*)\/?\s*>/);
|
|
const boundsAttrs = boundsMatch ? xmlAttrs(boundsMatch[1]) : {};
|
|
const candidateBounds = {
|
|
minLon: Number(boundsAttrs.minlon),
|
|
minLat: Number(boundsAttrs.minlat),
|
|
maxLon: Number(boundsAttrs.maxlon),
|
|
maxLat: Number(boundsAttrs.maxlat),
|
|
};
|
|
const bounds = Object.values(candidateBounds).every(Number.isFinite) ? candidateBounds : null;
|
|
const nodes = new Map();
|
|
const trafficSignalControls = [];
|
|
const nodePattern = /<node\b([^>]*?)(?:\/>|>([\s\S]*?)<\/node>)/g;
|
|
for (const match of xml.matchAll(nodePattern)) {
|
|
const attrs = xmlAttrs(match[1]);
|
|
if (attrs.action === 'delete' || !attrs.id || attrs.lon === undefined || attrs.lat === undefined) continue;
|
|
const coordinate = [Number(attrs.lon), Number(attrs.lat)];
|
|
if (!coordinate.every(Number.isFinite)) continue;
|
|
nodes.set(attrs.id, coordinate);
|
|
const tags = parseTags(match[2] || '');
|
|
if (tags.highway === 'traffic_signals') {
|
|
trafficSignalControls.push({ id: attrs.id, longitude: coordinate[0], latitude: coordinate[1], tags });
|
|
}
|
|
}
|
|
const ways = [];
|
|
for (const match of xml.matchAll(/<way\b([^>]*)>([\s\S]*?)<\/way>/g)) {
|
|
const attrs = xmlAttrs(match[1]);
|
|
if (attrs.action === 'delete') continue;
|
|
const body = match[2];
|
|
const refs = [];
|
|
for (const ndMatch of body.matchAll(/<nd\b([^>]*)\/?\s*>/g)) {
|
|
const ref = xmlAttrs(ndMatch[1]).ref;
|
|
if (ref && nodes.has(ref)) refs.push(ref);
|
|
}
|
|
if (refs.length >= 2) ways.push({ id: attrs.id || `way-${ways.length + 1}`, refs, tags: parseTags(body) });
|
|
}
|
|
for (const control of trafficSignalControls) {
|
|
const arms = [];
|
|
for (const way of ways) {
|
|
if (!isMotorRoad(way.tags)) continue;
|
|
for (let index = 0; index < way.refs.length; index += 1) {
|
|
if (way.refs[index] !== control.id) continue;
|
|
for (const neighborIndex of [index - 1, index + 1]) {
|
|
const neighbor = way.refs[neighborIndex];
|
|
if (!neighbor || !nodes.has(neighbor)) continue;
|
|
const neighborPoint = nodes.get(neighbor);
|
|
arms.push({
|
|
headingDegrees: headingBetween(control, neighborPoint),
|
|
wayId: String(way.id),
|
|
neighborNodeId: String(neighbor),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
control.arms = dedupeHeadings(arms);
|
|
control.junctionType = control.arms.length === 3 ? 'T' : control.arms.length === 4 ? 'cross' : 'other';
|
|
}
|
|
return { bounds, nodes, ways, trafficSignalControls };
|
|
}
|
|
|
|
function isMotorRoad(tags) {
|
|
const highway = tags.highway || '';
|
|
return (
|
|
highway &&
|
|
tags.area !== 'yes' &&
|
|
!new Set([
|
|
'footway',
|
|
'path',
|
|
'pedestrian',
|
|
'steps',
|
|
'cycleway',
|
|
'service',
|
|
'track',
|
|
'bridleway',
|
|
'corridor',
|
|
'elevator',
|
|
'platform',
|
|
'construction',
|
|
]).has(highway)
|
|
);
|
|
}
|
|
|
|
function headingBetween(from, to) {
|
|
const latitude = (((from.latitude + to[1]) / 2) * Math.PI) / 180;
|
|
return (Math.atan2((to[0] - from.longitude) * Math.cos(latitude), to[1] - from.latitude) * 180) / Math.PI;
|
|
}
|
|
|
|
function dedupeHeadings(arms) {
|
|
const normalized = (value) => ((value % 360) + 360) % 360;
|
|
const distance = (a, b) => Math.abs(((a - b + 540) % 360) - 180);
|
|
const result = [];
|
|
for (const arm of arms) {
|
|
arm.headingDegrees = normalized(arm.headingDegrees);
|
|
if (!result.some((other) => distance(other.headingDegrees, arm.headingDegrees) <= 25)) result.push(arm);
|
|
}
|
|
return result.sort((a, b) => a.headingDegrees - b.headingDegrees);
|
|
}
|
|
|
|
function xmlAttrs(text) {
|
|
const attrs = {};
|
|
for (const match of text.matchAll(/([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
attrs[match[1]] = match[2] !== undefined ? match[2] : match[3];
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
function parseTags(body) {
|
|
const tags = {};
|
|
for (const match of body.matchAll(/<tag\b([^>]*)\/?\s*>/g)) {
|
|
const tag = xmlAttrs(match[1]);
|
|
if (tag.k) tags[tag.k] = tag.v || '';
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
module.exports = { parseOsm };
|