feat: add native preview traffic simulation

This commit is contained in:
2026-08-18 16:57:27 +08:00
parent 0bc949bc24
commit 5403936ae4
21 changed files with 777 additions and 45 deletions

View File

@@ -267,7 +267,7 @@ function compileGeometry(model, overrides = { overrides: [] }, options = {}) {
const centerLines = compileCenterLines(model, overrides, junctionPlans, controls, diagnostics);
const markings = compileLaneMarkings(model, overrides, lanes, diagnostics, junctionPlans, controls);
const sidewalks = compileSidewalkSurfaces(model, diagnostics, junctionPlans);
const connectorResult = compileConnectors(model, lanes, diagnostics, overrides);
const connectorResult = compileConnectors(model, lanes, diagnostics, overrides, junctionPlans);
const junctionFeatures = compileJunctionSurfaces(model, junctionPlans, connectorResult.features, connectorResult.movements, diagnostics);
validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics);
return { roadSurface: { type: "FeatureCollection", features }, edgeLines: { type: "FeatureCollection", features: edgeLines }, sidewalkSurface: { type: "FeatureCollection", features: sidewalks }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, laneSeparators: { type: "FeatureCollection", features: markings.separators }, centerLines: { type: "FeatureCollection", features: centerLines }, directionArrows: { type: "FeatureCollection", features: markings.directionArrows }, turnArrows: { type: "FeatureCollection", features: markings.turnArrows }, crosswalks: { type: "FeatureCollection", features: controls.crosswalks }, vehicleStopLines: { type: "FeatureCollection", features: controls.stopLines }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics };
@@ -653,7 +653,7 @@ function compileLaneCenterlines(model, diagnostics, junctionPlans) {
return { features, byRoadId };
}
function compileConnectors(model, lanes, diagnostics, overrides) {
function compileConnectors(model, lanes, diagnostics, overrides, junctionPlans) {
const features = [];
const movements = [];
for (const connection of model.connections.filter((item) => item.enabled)) {
@@ -669,8 +669,12 @@ function compileConnectors(model, lanes, diagnostics, overrides) {
const override = laneOverride(overrides, defaultFromLane.id, defaultToLane.id);
if ((!laneAllowsTurn(fromRoad, index, turn) && override?.enabled !== true) || override?.enabled === false) continue;
const from = defaultFromLane.coordinates.at(-1); const to = defaultToLane.coordinates[0];
const control = connectorControlPoint(model, connection, from, to);
const coordinates = quadraticCurve(from, control, to, 12);
const plan = junctionPlans.get(connection.nodeId);
// Cross intersections retain the earlier center-node curve while T junctions
// use lane tangents so their through movement does not bow toward the stem.
const coordinates = plan?.segmentIds.size === 4
? quadraticCurve(from, endpointCoordinate(model, connection.fromEndpointId), to, 12)
: connectorCurve(defaultFromLane.coordinates, defaultToLane.coordinates, turn);
const length = lineLengthMeters(coordinates);
const id = `movement:${connection.id}:${defaultFromLane.id}->${defaultToLane.id}`;
const provenance = override ? `override:${override.id}` : connection.provenance;
@@ -712,12 +716,47 @@ function targetLaneIndex(turn, sourceIndex, sourceCount, targetCount) {
if (turn === "uturn") return 0;
return Math.min(targetCount - 1, Math.round(sourceIndex / Math.max(1, sourceCount - 1) * Math.max(0, targetCount - 1)));
}
function connectorControlPoint(model, connection, from, to) {
const node = endpointCoordinate(model, connection.fromEndpointId);
if (!node) return [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2];
// Nearby manual joins may not share exactly the same point. The midpoint
// keeps their curve smooth without rewriting the authoritative OSM geometry.
return node;
function connectorCurve(incoming, outgoing, turn) {
const start = incoming.at(-1);
const end = outgoing[0];
if (turn === "through") return lineCurve(start, end, 12);
const incomingHeading = headingDegrees(incoming.at(-2), start);
const outgoingHeading = headingDegrees(end, outgoing[1]);
const chord = distanceMeters(start, end);
const incomingSpan = distanceMeters(incoming.at(-2), start);
const outgoingSpan = distanceMeters(end, outgoing[1]);
const tangentIntersection = intersectTangentRays(start, end, incomingHeading, outgoingHeading);
const fallbackDistance = Math.min(8, Math.max(.75, Math.min(chord * .42, incomingSpan * .8, outgoingSpan * .8)));
const firstDistance = tangentIntersection && tangentIntersection.incoming >= 0 ? Math.min(tangentIntersection.incoming, Math.min(8, Math.max(.75, incomingSpan * 2.4))) / 3 : fallbackDistance;
const secondDistance = tangentIntersection && tangentIntersection.outgoing >= 0 ? Math.min(tangentIntersection.outgoing, Math.min(8, Math.max(.75, outgoingSpan * 2.4))) / 3 : fallbackDistance;
const firstControl = offsetCoordinate(start, incomingHeading, firstDistance);
const secondControl = offsetCoordinate(end, outgoingHeading + 180, secondDistance);
return cubicBezier(start, firstControl, secondControl, end, 12);
}
function intersectTangentRays(start, end, incomingHeading, outgoingHeading) {
const incoming = headingVector(incomingHeading);
const outgoing = headingVector(outgoingHeading);
const delta = project(end, start);
const cross = incoming[0] * outgoing[1] - incoming[1] * outgoing[0];
if (Math.abs(cross) < 1e-6) return null;
return {
incoming: (delta[0] * outgoing[1] - delta[1] * outgoing[0]) / cross,
outgoing: (delta[0] * incoming[1] - delta[1] * incoming[0]) / cross,
};
}
function lineCurve(start, end, segments) {
return Array.from({ length: segments + 1 }, (_, index) => interpolate(start, end, index / segments));
}
function cubicBezier(a, firstControl, secondControl, b, segments) {
const result = [];
for (let index = 0; index <= segments; index += 1) {
const t = index / segments; const u = 1 - t;
result.push([u ** 3 * a[0] + 3 * u * u * t * firstControl[0] + 3 * u * t * t * secondControl[0] + t ** 3 * b[0], u ** 3 * a[1] + 3 * u * u * t * firstControl[1] + 3 * u * t * t * secondControl[1] + t ** 3 * b[1]]);
}
return result;
}
function quadraticCurve(a, control, b, segments) {