fix: stabilize fengshu native road compilation
This commit is contained in:
@@ -39,7 +39,12 @@
|
|||||||
"coreRadiusMeters": 28,
|
"coreRadiusMeters": 28,
|
||||||
"cornerRadiusMeters": 25,
|
"cornerRadiusMeters": 25,
|
||||||
"outerRadiusExtraMeters": 24,
|
"outerRadiusExtraMeters": 24,
|
||||||
"nodeIds": ["8005332807", "8024512135", "8024512145", "8024512147"]
|
"nodeIds": [
|
||||||
|
"8005332807",
|
||||||
|
"8024512135",
|
||||||
|
"8024512145",
|
||||||
|
"8024512147"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
13362
inputs/osm/枫树二路.osm
13362
inputs/osm/枫树二路.osm
File diff suppressed because it is too large
Load Diff
@@ -23,9 +23,23 @@ function compileArea(configPath) {
|
|||||||
const area = readAreaConfig(configPath, { repoRoot });
|
const area = readAreaConfig(configPath, { repoRoot });
|
||||||
const overrides = loadOverrides(area.outputs.nativeRoadOverrides);
|
const overrides = loadOverrides(area.outputs.nativeRoadOverrides);
|
||||||
const model = compileRoadModel(fs.readFileSync(area.input, "utf8"), overrides);
|
const model = compileRoadModel(fs.readFileSync(area.input, "utf8"), overrides);
|
||||||
validateOverrides(overrides, model);
|
// Editing the source OSM retires the ids some overrides point at. Those
|
||||||
|
// entries can no longer match anything, so drop them with a diagnostic rather
|
||||||
|
// than aborting the whole compile — otherwise every OSM edit blocks the
|
||||||
|
// pipeline until the file is hand-pruned, one error message at a time.
|
||||||
|
const validated = validateOverrides(overrides, model, { skipStaleTargets: true });
|
||||||
|
for (const item of validated.stale) console.warn(`[warning] 忽略失效的 override(目标已不存在):${item.id} -> ${item.target}`);
|
||||||
fs.mkdirSync(area.outputs.pipelineDir, { recursive: true });
|
fs.mkdirSync(area.outputs.pipelineDir, { recursive: true });
|
||||||
const compiled = compileGeometry(model, overrides, { edgeLines: area.nativeRoad.edgeLines, junctionTemplates: area.nativeRoad.junctionTemplates });
|
const compiled = compileGeometry(model, overrides, { edgeLines: area.nativeRoad.edgeLines, junctionTemplates: area.nativeRoad.junctionTemplates });
|
||||||
|
compiled.diagnostics.push(...validated.stale.map((item) => ({
|
||||||
|
id: `diagnostic:stale-override:${item.id}`,
|
||||||
|
severity: "warning",
|
||||||
|
subjectId: item.id,
|
||||||
|
sourceIds: [],
|
||||||
|
rule: "stale-override-target",
|
||||||
|
message: `该设置指向的 ${item.kind} 目标 ${item.target} 已不存在(OSM 改动后 id 失效),本次编译已忽略。可在工作台重新设置,或从 native-road-overrides.json 中删除。`,
|
||||||
|
geometry: null,
|
||||||
|
})));
|
||||||
const signalDocument = loadOrGenerate(area.outputs.nativeTrafficSignals, fs.readFileSync(area.input, "utf8"), compiled.vehicleStopLines, compiled.intersectionSurface);
|
const signalDocument = loadOrGenerate(area.outputs.nativeTrafficSignals, fs.readFileSync(area.input, "utf8"), compiled.vehicleStopLines, compiled.intersectionSurface);
|
||||||
const signalRuntime = runtime(signalDocument);
|
const signalRuntime = runtime(signalDocument);
|
||||||
// Persist validation normalization, including one-time legacy heading migration.
|
// Persist validation normalization, including one-time legacy heading migration.
|
||||||
|
|||||||
@@ -261,9 +261,15 @@ function readReferenceCalibration(cluster) {
|
|||||||
function complexJunctionMetrics(cluster) {
|
function complexJunctionMetrics(cluster) {
|
||||||
if (metricsCache.has(cluster)) return metricsCache.get(cluster);
|
if (metricsCache.has(cluster)) return metricsCache.get(cluster);
|
||||||
const calibration = readReferenceCalibration(cluster);
|
const calibration = readReferenceCalibration(cluster);
|
||||||
|
// Without a reference geometry `coreRadiusMeters` is the only size input the
|
||||||
|
// template has, so honour it literally within the schema's own 12..80 range.
|
||||||
|
// It used to be scaled by .52 and clamped to 17, which silently capped every
|
||||||
|
// unreferenced junction at a core far smaller than its own approach envelope
|
||||||
|
// — a 24 m dual-carriageway arm was being fitted into a 16.6 m core no matter
|
||||||
|
// what the config asked for. The calibrated branch is unchanged.
|
||||||
const coreRadius = calibration
|
const coreRadius = calibration
|
||||||
? Math.max(12, Math.min(24, calibration.shortSpanMeters * .14))
|
? Math.max(12, Math.min(24, calibration.shortSpanMeters * .14))
|
||||||
: Math.max(11, Math.min(17, cluster.coreRadiusMeters * .52));
|
: Math.max(12, Math.min(80, Number(cluster.coreRadiusMeters) || 28));
|
||||||
const metrics = { calibration, coreRadius, approachOuterRadius: coreRadius + (Number(cluster.outerRadiusExtraMeters) || 18) };
|
const metrics = { calibration, coreRadius, approachOuterRadius: coreRadius + (Number(cluster.outerRadiusExtraMeters) || 18) };
|
||||||
metricsCache.set(cluster, metrics);
|
metricsCache.set(cluster, metrics);
|
||||||
return metrics;
|
return metrics;
|
||||||
|
|||||||
@@ -154,7 +154,26 @@ function loadOverrides(file) {
|
|||||||
return validateOverrides(JSON.parse(fs.readFileSync(file, "utf8")));
|
return validateOverrides(JSON.parse(fs.readFileSync(file, "utf8")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOverrides(value, model) {
|
// An override points at an id derived from OSM. Editing the source can retire
|
||||||
|
// that id — a way deleted, or split differently so `segment/6` no longer
|
||||||
|
// exists — which leaves the entry pointing at nothing. That is stale data, not
|
||||||
|
// a malformed override, so callers that merely consume overrides can ask to
|
||||||
|
// skip them and keep going. Callers that *save* overrides still use the default
|
||||||
|
// strict mode: writing a reference that cannot resolve is a real error.
|
||||||
|
function staleOverrideTarget(item, sets) {
|
||||||
|
if (!sets.roadIds) return null;
|
||||||
|
if (item.kind === "road" && typeof item.roadId === "string" && !sets.roadIds.has(item.roadId)) return item.roadId;
|
||||||
|
if (item.kind === "lane-separator-style" && typeof item.roadId === "string" && !sets.roadIds.has(item.roadId)) return item.roadId;
|
||||||
|
if (item.kind === "edge-line-style" && typeof item.roadId === "string" && !sets.directionalRoadIds.has(item.roadId)) return item.roadId;
|
||||||
|
if (item.kind === "center-line-style" && typeof item.segmentId === "string" && !sets.segmentIds.has(item.segmentId)) return item.segmentId;
|
||||||
|
if (item.kind === "junction-connection" && typeof item.fromEndpointId === "string" && typeof item.toEndpointId === "string"
|
||||||
|
&& (!sets.endpointIds.has(item.fromEndpointId) || !sets.endpointIds.has(item.toEndpointId))) return `${item.fromEndpointId} → ${item.toEndpointId}`;
|
||||||
|
if (item.kind === "lane-connection" && typeof item.fromLaneId === "string" && typeof item.toLaneId === "string"
|
||||||
|
&& (!sets.laneIds.has(item.fromLaneId) || !sets.laneIds.has(item.toLaneId))) return `${item.fromLaneId} → ${item.toLaneId}`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOverrides(value, model, options = {}) {
|
||||||
if (!value || value.schema !== OVERRIDE_SCHEMA || !Array.isArray(value.overrides)) throw new Error(`Overrides must use ${OVERRIDE_SCHEMA}.`);
|
if (!value || value.schema !== OVERRIDE_SCHEMA || !Array.isArray(value.overrides)) throw new Error(`Overrides must use ${OVERRIDE_SCHEMA}.`);
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
const roadIds = model ? new Set(model.roads.flatMap((road) => [road.id, road.sourceRoadId])) : null;
|
const roadIds = model ? new Set(model.roads.flatMap((road) => [road.id, road.sourceRoadId])) : null;
|
||||||
@@ -162,9 +181,16 @@ function validateOverrides(value, model) {
|
|||||||
const endpointIds = model ? new Set(model.endpoints.map((endpoint) => endpoint.id)) : null;
|
const endpointIds = model ? new Set(model.endpoints.map((endpoint) => endpoint.id)) : null;
|
||||||
const laneIds = model ? new Set(model.roads.flatMap((road) => Array.from({ length: road.laneCount }, (_, index) => `lane:${road.id}:${index + 1}`))) : null;
|
const laneIds = model ? new Set(model.roads.flatMap((road) => Array.from({ length: road.laneCount }, (_, index) => `lane:${road.id}:${index + 1}`))) : null;
|
||||||
const segmentIds = model ? new Set(model.roads.map((road) => road.segmentId)) : null;
|
const segmentIds = model ? new Set(model.roads.map((road) => road.segmentId)) : null;
|
||||||
|
const sets = { roadIds, directionalRoadIds, endpointIds, laneIds, segmentIds };
|
||||||
|
const kept = [];
|
||||||
|
const stale = [];
|
||||||
for (const item of value.overrides) {
|
for (const item of value.overrides) {
|
||||||
if (!item || typeof item.id !== "string" || !item.id || ids.has(item.id)) throw new Error("Each override needs a unique id.");
|
if (!item || typeof item.id !== "string" || !item.id || ids.has(item.id)) throw new Error("Each override needs a unique id.");
|
||||||
ids.add(item.id);
|
ids.add(item.id);
|
||||||
|
if (options.skipStaleTargets) {
|
||||||
|
const target = staleOverrideTarget(item, sets);
|
||||||
|
if (target) { stale.push({ id: item.id, kind: item.kind, target }); continue; }
|
||||||
|
}
|
||||||
if (item.kind === "road") {
|
if (item.kind === "road") {
|
||||||
if (typeof item.roadId !== "string" || roadIds && !roadIds.has(item.roadId)) throw new Error(`Unknown road override target: ${item.roadId}`);
|
if (typeof item.roadId !== "string" || roadIds && !roadIds.has(item.roadId)) throw new Error(`Unknown road override target: ${item.roadId}`);
|
||||||
for (const key of ["widthMeters", "laneCount"]) if (item[key] !== undefined && (!Number.isFinite(item[key]) || item[key] <= 0 || (key === "laneCount" && !Number.isInteger(item[key])))) throw new Error(`Invalid road override ${key}.`);
|
for (const key of ["widthMeters", "laneCount"]) if (item[key] !== undefined && (!Number.isFinite(item[key]) || item[key] <= 0 || (key === "laneCount" && !Number.isInteger(item[key])))) throw new Error(`Invalid road override ${key}.`);
|
||||||
@@ -181,8 +207,9 @@ function validateOverrides(value, model) {
|
|||||||
} else if (item.kind === "edge-line-style") {
|
} else if (item.kind === "edge-line-style") {
|
||||||
if (typeof item.roadId !== "string" || (directionalRoadIds && !directionalRoadIds.has(item.roadId)) || !["left", "right"].includes(item.side) || !CENTER_LINE_COLORS.has(item.color) || !CENTER_LINE_PATTERNS.has(item.pattern)) throw new Error("Invalid edge line style override.");
|
if (typeof item.roadId !== "string" || (directionalRoadIds && !directionalRoadIds.has(item.roadId)) || !["left", "right"].includes(item.side) || !CENTER_LINE_COLORS.has(item.color) || !CENTER_LINE_PATTERNS.has(item.pattern)) throw new Error("Invalid edge line style override.");
|
||||||
} else throw new Error(`Unsupported override kind: ${item.kind}`);
|
} else throw new Error(`Unsupported override kind: ${item.kind}`);
|
||||||
|
kept.push(item);
|
||||||
}
|
}
|
||||||
return { schema: OVERRIDE_SCHEMA, overrides: value.overrides };
|
return { schema: OVERRIDE_SCHEMA, overrides: kept, stale };
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyRoadOverrides(road, overrides, diagnostics) {
|
function applyRoadOverrides(road, overrides, diagnostics) {
|
||||||
|
|||||||
@@ -379,6 +379,20 @@ const laneOverrides = validateOverrides({ schema: "native-road-overrides/v1", ov
|
|||||||
assert.equal(compileGeometry(turnModel, laneOverrides).connectors.features.length, 0);
|
assert.equal(compileGeometry(turnModel, laneOverrides).connectors.features.length, 0);
|
||||||
assert.equal(compileGeometry(turnModel, laneOverrides).movements.length, 0);
|
assert.equal(compileGeometry(turnModel, laneOverrides).movements.length, 0);
|
||||||
assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad", kind: "road", roadId: "missing", widthMeters: 4 }] }, initial), /Unknown road/);
|
assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad", kind: "road", roadId: "missing", widthMeters: 4 }] }, initial), /Unknown road/);
|
||||||
|
// Editing OSM retires ids. Saving such a reference is still an error, but a
|
||||||
|
// consumer may ask to skip it so one stale entry cannot block the whole compile.
|
||||||
|
const staleDoc = { schema: "native-road-overrides/v1", overrides: [
|
||||||
|
{ id: "stale-road", kind: "road", roadId: "road:way/does-not-exist:forward", widthMeters: 4 },
|
||||||
|
{ id: "live-road", kind: "road", roadId: target.id, widthMeters: 9 },
|
||||||
|
] };
|
||||||
|
assert.throws(() => validateOverrides(staleDoc, initial), /Unknown road/);
|
||||||
|
const skipped = validateOverrides(staleDoc, initial, { skipStaleTargets: true });
|
||||||
|
assert.equal(skipped.overrides.length, 1);
|
||||||
|
assert.equal(skipped.overrides[0].id, "live-road");
|
||||||
|
assert.deepEqual(skipped.stale.map((item) => item.id), ["stale-road"]);
|
||||||
|
assert.equal(skipped.stale[0].target, "road:way/does-not-exist:forward");
|
||||||
|
// Skipping only forgives missing targets; a malformed entry still throws.
|
||||||
|
assert.throws(() => validateOverrides({ schema: "native-road-overrides/v1", overrides: [{ id: "bad-width", kind: "road", roadId: target.id, widthMeters: -1 }] }, initial, { skipStaleTargets: true }), /Invalid road override/);
|
||||||
const freshArea = fs.mkdtempSync(path.join(os.tmpdir(), "native-road-fresh-area-"));
|
const freshArea = fs.mkdtempSync(path.join(os.tmpdir(), "native-road-fresh-area-"));
|
||||||
try {
|
try {
|
||||||
const input = path.join(freshArea, "input.osm");
|
const input = path.join(freshArea, "input.osm");
|
||||||
|
|||||||
Reference in New Issue
Block a user