feat: complete native traffic signal workflow
This commit is contained in:
@@ -59,7 +59,11 @@ function buildTrafficSignalFeatures(stopLines, intersections, controls = []) {
|
||||
properties: {
|
||||
signal_uid: signalUid, display_id: signalUid, control_id: String(control.id),
|
||||
approach_id: approachId, source_way_id: sourceWayId,
|
||||
heading_deg: candidate.headingDegrees, phase_group: groups[index],
|
||||
// These are independent assembly controls. heading_deg remains a
|
||||
// migration hint for older native documents only.
|
||||
mast_heading_deg: normalizeDegrees(candidate.headingDegrees - 90),
|
||||
face_heading_deg: normalizeDegrees(candidate.headingDegrees + 180),
|
||||
phase_group: groups[index],
|
||||
mast_reach_m: MAST_REACH_METERS,
|
||||
stop_lon: candidate.center[0], stop_lat: candidate.center[1],
|
||||
enabled: true, z_offset_m: 0,
|
||||
@@ -114,13 +118,30 @@ function validateTrafficSignalFeatures(collection) {
|
||||
if (!approachId.startsWith(`${sourceWayId}:`)) throw new Error(`${label}: approach_id does not match source_way_id`);
|
||||
const expectedUid = `osm-${controlId}-${approachId.replace(":", "-")}`;
|
||||
if (signalUid !== expectedUid) throw new Error(`${label}: signal_uid does not match source identity (expected '${expectedUid}')`);
|
||||
const legacyHeading = input.heading_deg == null || input.heading_deg === "" ? null : normalizeDegrees(number("heading_deg"));
|
||||
if (legacyHeading == null && (input.mast_heading_deg == null || input.mast_heading_deg === "")) {
|
||||
throw new Error(`${label}: missing mast_heading_deg`);
|
||||
}
|
||||
if (legacyHeading == null && (input.face_heading_deg == null || input.face_heading_deg === "")) {
|
||||
throw new Error(`${label}: missing face_heading_deg`);
|
||||
}
|
||||
const mastHeading = input.mast_heading_deg == null || input.mast_heading_deg === ""
|
||||
? normalizeDegrees((legacyHeading == null ? 0 : legacyHeading) - 90)
|
||||
: normalizeDegrees(number("mast_heading_deg"));
|
||||
const faceHeading = input.face_heading_deg == null || input.face_heading_deg === ""
|
||||
? normalizeDegrees((legacyHeading == null ? 0 : legacyHeading) + 180)
|
||||
: normalizeDegrees(number("face_heading_deg"));
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: { type: "Point", coordinates: feature.geometry.coordinates.slice(0, 2).map(Number) },
|
||||
properties: {
|
||||
...input, signal_uid: signalUid, display_id: displayId,
|
||||
control_id: controlId, approach_id: approachId,
|
||||
source_way_id: sourceWayId, heading_deg: normalizeDegrees(number("heading_deg")),
|
||||
source_way_id: sourceWayId,
|
||||
// Retain the legacy value only for migration compatibility. Runtime
|
||||
// geometry is entirely defined by mast_heading_deg and face_heading_deg.
|
||||
heading_deg: legacyHeading,
|
||||
mast_heading_deg: mastHeading, face_heading_deg: faceHeading,
|
||||
phase_group: phaseGroup, mast_reach_m: number("mast_reach_m", { min: 0.1, max: 30 }),
|
||||
stop_lon: number("stop_lon", { min: -180, max: 180 }),
|
||||
stop_lat: number("stop_lat", { min: -90, max: 90 }),
|
||||
@@ -136,16 +157,20 @@ function buildTrafficSignalsFromFeatures(collection) {
|
||||
const signals = normalized.features.filter((feature) => feature.properties.enabled).map((feature) => {
|
||||
const p = feature.properties;
|
||||
const point = feature.geometry.coordinates;
|
||||
const axis = headingVector(p.heading_deg);
|
||||
const mastAxis = headingVector(p.mast_heading_deg);
|
||||
return {
|
||||
id: p.signal_uid, signalUid: p.signal_uid, displayId: p.display_id,
|
||||
nodeKey: signalNodeKey(p.signal_uid),
|
||||
controlId: p.control_id, approachId: p.approach_id, sourceWayId: p.source_way_id,
|
||||
phaseGroup: p.phase_group, longitude: point[0], latitude: point[1],
|
||||
stopLongitude: p.stop_lon, stopLatitude: p.stop_lat,
|
||||
headingDegrees: p.heading_deg, mastReachMeters: p.mast_reach_m,
|
||||
// Existing Blender readers require headingDegrees. It is a compatibility
|
||||
// alias only; the independent mast/face fields below define all geometry.
|
||||
headingDegrees: p.heading_deg == null ? p.mast_heading_deg : p.heading_deg,
|
||||
mastHeadingDegrees: p.mast_heading_deg,
|
||||
faceHeadingDegrees: p.face_heading_deg, mastReachMeters: p.mast_reach_m,
|
||||
zOffsetMeters: p.z_offset_m,
|
||||
pose: buildSignalPose(point, axis, p.mast_reach_m, p.z_offset_m),
|
||||
pose: buildSignalPose(point, mastAxis, p.face_heading_deg, p.mast_reach_m, p.z_offset_m),
|
||||
};
|
||||
});
|
||||
return { version: 3, layout: SIGNAL_LAYOUT, signals };
|
||||
@@ -235,12 +260,13 @@ function phaseGroups(arms) {
|
||||
groups[main[0]] = 0; groups[main[1]] = 0; return groups;
|
||||
}
|
||||
|
||||
function buildSignalPose(pole, axis, mastReach, zOffset = 0) {
|
||||
const lateral = [axis[1], -axis[0]]; const face = [-axis[0], -axis[1]];
|
||||
const head = moveMeters(pole, lateral, -mastReach); const faceHeadingDegrees = Math.atan2(face[0], face[1]) * 180 / Math.PI;
|
||||
function buildSignalPose(pole, mastAxis, faceHeadingDegrees, mastReach, zOffset = 0) {
|
||||
const face = headingVector(faceHeadingDegrees);
|
||||
const head = moveMeters(pole, mastAxis, mastReach);
|
||||
const position = (point, height) => ({ longitude: point[0], latitude: point[1], height: height + zOffset });
|
||||
const lensPoint = moveMeters(head, face, SIGNAL_LAYOUT.lensFaceOffsetMeters);
|
||||
const board = moveMeters(moveMeters(head, lateral, SIGNAL_LAYOUT.countdownLateralMeters), face, SIGNAL_LAYOUT.countdownFaceOffsetMeters);
|
||||
const faceRight = [-face[1], face[0]];
|
||||
const board = moveMeters(moveMeters(head, faceRight, SIGNAL_LAYOUT.countdownLateralMeters), face, SIGNAL_LAYOUT.countdownFaceOffsetMeters);
|
||||
return { pole: position(pole, 0), arm: { from: position(pole, SIGNAL_LAYOUT.mastHeightMeters), to: position(head, SIGNAL_LAYOUT.mastHeightMeters) }, head: { ...position(head, SIGNAL_LAYOUT.headCenterHeightMeters), faceHeadingDegrees }, lenses: ["red", "yellow", "green"].map((state, index) => ({ state, ...position(lensPoint, SIGNAL_LAYOUT.headCenterHeightMeters + SIGNAL_LAYOUT.lensVerticalOffsetsMeters[index]) })), countdown: { ...position(board, SIGNAL_LAYOUT.mastHeightMeters), faceHeadingDegrees } };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user