'use strict'; function parseOsm(xml) { const boundsMatch = xml.match(/]*)\/?\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 = /]*?)(?:\/>|>([\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(/]*)>([\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(/]*)\/?\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(/]*)\/?\s*>/g)) { const tag = xmlAttrs(match[1]); if (tag.k) tags[tag.k] = tag.v || ''; } return tags; } module.exports = { parseOsm };