"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 };