refactor: move road compiler core into package

This commit is contained in:
2026-08-25 17:12:58 +08:00
commit eb859c40d5
13 changed files with 14817 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
"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,
};