fix: correct native road edge and junction boundary geometry
修复四处几何缺陷,并新增复杂路口候选识别与工作台调试视图。 几何修复: - junctionBoundary 的进口边改用 cutback 处朝向 (headingAtCutback)。原先取节点端 首段朝向, 与 roadRing 的端边不平行, 15 个进口有偏差 (最大 152°), 路口面与道路面 之间张开楔形。凸包兜底路口 11 -> 8。 - compileEdgeLines 偏移量改用 totalWidth/2。原用单方向 road.widthMeters/2, 双向段的路缘线画在车行道正中间 (实测中位偏差 1.63m = w/2)。同时以实际发出的 路面为准过滤无路面的路, 并统一复杂路口的裁剪口径。 顶点偏离 >0.5m: 64% -> 0%, 最大偏离 4.53m -> 0.06m (= 半个带宽, 理论最优)。 - roadRing 新增 removeOffsetFolds。定宽法线偏移无斜接限制, 转弯处相邻段较短时 偏移点前后颠倒, 多边形折回自身, 按 even-odd 渲染成叶片状空洞。 道路面自交 1 -> 0, 最尖角 0.1° -> 18.8°。 - junctionBoundary 新增 mergeParallelApproachPoints。双幅路两幅同向汇入时, 四个侧点按角度排序后交错, 在交错点之间插入的圆角曲线从外圈深挖回节点, 渲染成拱形凹口。合并近平行 (<25°) 进口为单一面。 路口面最尖角 6.9° -> 全部 >=82.6°, 自交 0。 新增: - detectComplexJunctionCandidates: 按"短链接连接的路口节点"聚类并做紧凑度过滤 (直径 <=45m), 输出 complex-junction-candidate 诊断与建议参数。纯诊断, 不启用模板、不改几何; test:native-road 断言了这条契约。 - road:workbench --debug: 地图标注候选编号与包络, 点选给出可粘贴配置片段, 并提供 POST /api/junction-clusters 一键写入区域配置。写入前用 readAreaConfig 校验、检测节点冲突、要求候选存在于最新编译结果, 原子落盘。 Regression: test:native-road / test:road-workbench / test:gaode-junction-reference / test:preflight / test:native-preview-traffic / test:package-contract / test:traffic-signals 全绿; road:check ok=true, errors=[]。 源 OSM 与参考 GeoJSON 校验和未变。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,24 @@ assert.ok(geometry.intersectionSurface.features.every((feature) => feature.prope
|
||||
assert.ok(geometry.intersectionSurface.features.every((feature) => ["approach-envelope", "rounded-approach-envelope", "connector-convex-fallback"].includes(feature.properties.boundary_mode)));
|
||||
assert.ok(geometry.intersectionSurface.features.every((feature) => feature.properties.approach_area_m2 > 0 && feature.properties.surface_area_m2 > 0 && feature.properties.expansion_ratio >= 1));
|
||||
for (const feature of geometry.intersectionSurface.features.filter((item) => item.properties.boundary_mode === "connector-convex-fallback")) assert.ok(geometry.diagnostics.some((item) => item.subjectId === feature.properties.native_id && item.rule === "junction-connector-envelope-fallback"));
|
||||
// Complex-junction candidate detection: two junction nodes joined by a short
|
||||
// link are one physical intersection; the same pair pushed far apart is a
|
||||
// corridor and must not be reported. Detection is advisory only, so the
|
||||
// geometry must be identical either way.
|
||||
const candidateOsm = (lonB) => `<osm><node id="1" lon="113.999" lat="30"/><node id="2" lon="114" lat="30"/><node id="3" lon="${lonB}" lat="30"/><node id="4" lon="114.01" lat="30"/><node id="5" lon="114" lat="30.001"/><node id="6" lon="${lonB}" lat="29.999"/><way id="70"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/></way><way id="71"><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/></way><way id="72"><nd ref="3"/><nd ref="4"/><tag k="highway" v="residential"/></way><way id="73"><nd ref="2"/><nd ref="5"/><tag k="highway" v="residential"/></way><way id="74"><nd ref="3"/><nd ref="6"/><tag k="highway" v="residential"/></way></osm>`;
|
||||
const nearGeometry = compileGeometry(compileRoadModel(candidateOsm("114.0002"), empty));
|
||||
const nearCandidates = nearGeometry.diagnostics.filter((item) => item.rule === "complex-junction-candidate");
|
||||
assert.equal(nearCandidates.length, 1);
|
||||
assert.deepEqual(nearCandidates[0].suggestedCluster.nodeIds, ["2", "3"]);
|
||||
assert.equal(nearCandidates[0].suggestedCluster.template, "complex-junction-v1");
|
||||
assert.equal(nearCandidates[0].severity, "info");
|
||||
assert.ok(nearCandidates[0].suggestedCluster.diameterMeters > 0 && nearCandidates[0].suggestedCluster.coreRadiusMeters >= 12);
|
||||
// Same topology, nodes 2 and 3 now ~965 m apart: no cluster, and no template.
|
||||
const farGeometry = compileGeometry(compileRoadModel(candidateOsm("114.01"), empty));
|
||||
assert.equal(farGeometry.diagnostics.filter((item) => item.rule === "complex-junction-candidate").length, 0);
|
||||
assert.ok(nearGeometry.intersectionSurface.features.every((feature) => feature.properties.template === null));
|
||||
assert.ok(nearGeometry.roadSurface.features.every((feature) => !feature.properties.cluster_id));
|
||||
|
||||
const controlOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.00080" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="zebra"/></node><node id="3" lon="114.001" lat="30"/><node id="4" lon="114.002" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="unmarked"/></node><node id="5" lon="114.0035" lat="30"><tag k="highway" v="crossing"/></node><node id="6" lon="114.004" lat="30"/><node id="7" lon="114.001" lat="30.001"/><way id="60"><nd ref="1"/><nd ref="2"/><nd ref="3"/><nd ref="4"/><tag k="highway" v="residential"/></way><way id="61"><nd ref="5"/><nd ref="6"/><tag k="highway" v="footway"/></way><way id="62"><nd ref="3"/><nd ref="7"/><tag k="highway" v="residential"/></way></osm>`;
|
||||
const controlGeometry = compileGeometry(compileRoadModel(controlOsm, empty));
|
||||
assert.equal(controlGeometry.crosswalks.features.length, 6);
|
||||
|
||||
Reference in New Issue
Block a user