"use strict"; const EARTH_RADIUS_METERS = 6371008.8; function laneCenterline(lane) { const ring = lane?.geometry?.type === "Polygon" ? lane.geometry.coordinates?.[0] : null; if (!Array.isArray(ring) || ring.length < 5 || !sameCoordinate(ring[0], ring.at(-1))) return null; const vertices = ring.slice(0, -1); if (!vertices.every(validCoordinate)) return null; const half = vertices.length / 2; if (!Number.isInteger(half) || half < 2) return null; const centerline = vertices.slice(0, half).map((point, index) => [ (point[0] + vertices[vertices.length - 1 - index][0]) / 2, (point[1] + vertices[vertices.length - 1 - index][1]) / 2, ]); return polylineLength(centerline) > 0.01 ? centerline : null; } function orientPolyline(polyline, reference) { if (!polyline?.length || !reference?.length) return null; const forward = projectedDistanceAlong(reference, polyline.at(-1)) - projectedDistanceAlong(reference, polyline[0]); if (Math.abs(forward) < 0.01) return null; return forward > 0 ? polyline.map(copyCoordinate) : [...polyline].reverse().map(copyCoordinate); } function stitchPolylines(polylines, maxGapMeters) { if (!polylines.length) return null; const result = []; for (const polyline of polylines) { if (!polyline?.length) return null; if (result.length && haversineMeters(result.at(-1), polyline[0]) > maxGapMeters) return null; appendCoordinates(result, polyline); } return result; } function projectedDistanceAlong(polyline, point) { let traversed = 0; let best = { distance: Infinity, along: 0, lateral: 0 }; for (let index = 1; index < polyline.length; index += 1) { const start = polyline[index - 1]; const end = polyline[index]; const meters = metersAt((start[1] + end[1]) / 2); const dx = (end[0] - start[0]) * meters.lon; const dy = (end[1] - start[1]) * meters.lat; const px = (point[0] - start[0]) * meters.lon; const py = (point[1] - start[1]) * meters.lat; const length = Math.hypot(dx, dy); if (length < 0.001) continue; const ratio = Math.max(0, Math.min(1, (px * dx + py * dy) / (length * length))); const offsetX = px - dx * ratio; const offsetY = py - dy * ratio; const distance = Math.hypot(offsetX, offsetY); if (distance < best.distance) { const rightX = dy / length; const rightY = -dx / length; best = { distance, along: traversed + length * ratio, lateral: offsetX * rightX + offsetY * rightY, }; } traversed += length; } return best.along; } function lateralOffsetFrom(polyline, point) { let best = null; for (let index = 1; index < polyline.length; index += 1) { const start = polyline[index - 1]; const end = polyline[index]; const meters = metersAt((start[1] + end[1]) / 2); const dx = (end[0] - start[0]) * meters.lon; const dy = (end[1] - start[1]) * meters.lat; const px = (point[0] - start[0]) * meters.lon; const py = (point[1] - start[1]) * meters.lat; const length = Math.hypot(dx, dy); if (length < 0.001) continue; const ratio = Math.max(0, Math.min(1, (px * dx + py * dy) / (length * length))); const offsetX = px - dx * ratio; const offsetY = py - dy * ratio; const distance = Math.hypot(offsetX, offsetY); if (!best || distance < best.distance) { best = { distance, lateral: offsetX * dy / length - offsetY * dx / length }; } } return best; } function polylineMidpoint(polyline) { const target = polylineLength(polyline) / 2; let traversed = 0; for (let index = 1; index < polyline.length; index += 1) { const length = haversineMeters(polyline[index - 1], polyline[index]); if (traversed + length >= target) { const ratio = length ? (target - traversed) / length : 0; return [ polyline[index - 1][0] + (polyline[index][0] - polyline[index - 1][0]) * ratio, polyline[index - 1][1] + (polyline[index][1] - polyline[index - 1][1]) * ratio, ]; } traversed += length; } return polyline.length ? copyCoordinate(polyline.at(-1)) : null; } function polylineLength(polyline) { let total = 0; for (let index = 1; index < (polyline?.length || 0); index += 1) { total += haversineMeters(polyline[index - 1], polyline[index]); } return total; } function haversineMeters(a, b) { 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 h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h))); } function appendCoordinates(target, coordinates) { for (const coordinate of coordinates) { if (!sameCoordinate(target.at(-1), coordinate)) target.push(copyCoordinate(coordinate)); } } function validCoordinate(value) { return Array.isArray(value) && value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1]); } function sameCoordinate(a, b) { return Boolean(a && b && a[0] === b[0] && a[1] === b[1]); } function copyCoordinate(coordinate) { return [coordinate[0], coordinate[1]]; } function metersAt(latitude) { return { lon: 111320 * Math.cos(degreesToRadians(latitude)), lat: 111320 }; } function degreesToRadians(value) { return value * Math.PI / 180; } module.exports = { appendCoordinates, haversineMeters, laneCenterline, lateralOffsetFrom, orientPolyline, polylineLength, polylineMidpoint, projectedDistanceAlong, stitchPolylines, };