"use strict"; const fs = require("fs"); function buildVehicleRoute(osmPath) { const xml = fs.readFileSync(osmPath, "utf8"); const bounds = osmBounds(xml); const nodes = new Map(); for (const match of xml.matchAll(/]*)>/g)) { const attrs = xmlAttrs(match[1]); if (!attrs.id || attrs.lon === undefined || attrs.lat === undefined) continue; nodes.set(attrs.id, [Number(attrs.lon), Number(attrs.lat)]); } const segments = []; for (const match of xml.matchAll(/]*)>([\s\S]*?)<\/way>/g)) { const attrs = xmlAttrs(match[1]); const body = match[2]; const tags = {}; for (const tagMatch of body.matchAll(/]*)\/?>/g)) { const tag = xmlAttrs(tagMatch[1]); if (tag.k) tags[tag.k] = tag.v || ""; } if (!isCruiseHighway(tags)) continue; const coords = []; for (const ndMatch of body.matchAll(/]*)\/?>/g)) { const nd = xmlAttrs(ndMatch[1]); const coord = nodes.get(nd.ref); if (coord) coords.push(coord); } const runs = splitInBounds(compactCoords(coords), bounds); let runIndex = 0; for (const run of runs) { const lengthMeters = routeLength(run); if (lengthMeters < 20) continue; runIndex += 1; const laneOffsetMeters = 1.3; segments.push({ id: runIndex === 1 ? (attrs.id || `way-${segments.length + 1}`) : `${attrs.id || "way"}-${runIndex}`, name: tags.name || tags.highway || "road", highway: tags.highway || "", oneWay: tags.oneway || "", lengthMeters, laneOffsetMeters, coordinates: offsetPolylineRight(run, laneOffsetMeters), centerlineCoordinates: run, }); } } segments.sort((a, b) => b.lengthMeters - a.lengthMeters); return { source: osmPath, bounds, generatedAt: new Date().toISOString(), speedMetersPerSecond: 8.0, loop: true, segments }; } function osmBounds(xml) { const match = xml.match(/]*)\/?>/); if (!match) return null; const attrs = xmlAttrs(match[1]); const bounds = { minLon: Number(attrs.minlon), minLat: Number(attrs.minlat), maxLon: Number(attrs.maxlon), maxLat: Number(attrs.maxlat) }; return Object.values(bounds).every(Number.isFinite) ? bounds : null; } function xmlAttrs(text) { const attrs = {}; for (const match of text.matchAll(/([:\w-]+)\s*=\s*("([^"]*)"|'([^']*)')/g)) { attrs[match[1]] = match[3] !== undefined ? match[3] : match[4]; } return attrs; } function isCruiseHighway(tags) { const highway = tags.highway || ""; if (!highway || tags.area === "yes") return false; return !new Set(["footway", "path", "pedestrian", "steps", "cycleway", "service", "track", "bridleway", "corridor", "elevator", "platform", "construction"]).has(highway); } function compactCoords(coords) { const out = []; for (const coord of coords) { const last = out[out.length - 1]; if (!last || last[0] !== coord[0] || last[1] !== coord[1]) out.push(coord); } return out; } function offsetPolylineRight(coords, offsetMeters) { if (coords.length < 2 || offsetMeters === 0) return coords; const refLat = coords.reduce((sum, coord) => sum + coord[1], 0) / coords.length; const metersPerLat = 111320.0; const metersPerLon = 111320.0 * Math.cos(degreesToRadians(refLat)); const points = coords.map((coord) => ({ x: coord[0] * metersPerLon, y: coord[1] * metersPerLat, lon: coord[0], lat: coord[1] })); return points.map((point, index) => { const prev = points[Math.max(0, index - 1)]; const next = points[Math.min(points.length - 1, index + 1)]; let dx = next.x - prev.x; let dy = next.y - prev.y; const length = Math.hypot(dx, dy); if (length < 0.001) return [point.lon, point.lat]; dx /= length; dy /= length; return [(point.x + dy * offsetMeters) / metersPerLon, (point.y - dx * offsetMeters) / metersPerLat]; }); } function splitInBounds(coords, bounds) { if (!bounds) return [coords]; const runs = []; let current = []; for (const coord of coords) { if (insideBounds(coord, bounds)) current.push(coord); else if (current.length) { if (current.length >= 2) runs.push(current); current = []; } } if (current.length >= 2) runs.push(current); return runs; } function insideBounds(coord, bounds) { const pad = 0.00002; return coord[0] >= bounds.minLon - pad && coord[0] <= bounds.maxLon + pad && coord[1] >= bounds.minLat - pad && coord[1] <= bounds.maxLat + pad; } function routeLength(coords) { let total = 0; for (let i = 1; i < coords.length; i += 1) total += haversineMeters(coords[i - 1], coords[i]); return total; } function haversineMeters(a, b) { const radius = 6371008.8; const lat1 = degreesToRadians(a[1]); const lat2 = degreesToRadians(b[1]); const dLat = degreesToRadians(b[1] - a[1]); const dLon = degreesToRadians(b[0] - a[0]); const sinLat = Math.sin(dLat / 2); const sinLon = Math.sin(dLon / 2); const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLon * sinLon; return 2 * radius * Math.asin(Math.min(1, Math.sqrt(h))); } function degreesToRadians(value) { return value * Math.PI / 180; } module.exports = { buildVehicleRoute };