feat: add native road center lines
This commit is contained in:
@@ -414,6 +414,83 @@ GeoJSON,native Blender 构建通过 `catalog.NATIVE_ROAD_LAYERS` 消费它们
|
|||||||
正确:道路方向箭头进入 `direction_arrows.geojson`;只有 OSM 明确标注的动作进入
|
正确:道路方向箭头进入 `direction_arrows.geojson`;只有 OSM 明确标注的动作进入
|
||||||
`turn_arrows.geojson`。工作台用两个开关呈现,Blender 复用同一现有箭头材质。
|
`turn_arrows.geojson`。工作台用两个开关呈现,Blender 复用同一现有箭头材质。
|
||||||
|
|
||||||
|
## Native 道路中心虚线
|
||||||
|
|
||||||
|
### 1. 范围与触发条件
|
||||||
|
|
||||||
|
`node scripts/compile-native-roads.js --config <area>` 为可确认的双向非 service
|
||||||
|
道路段写入 `native-road/layers/center_lines.geojson`。这是原生几何:只能依据
|
||||||
|
canonical OSM 中心线、native 双方向道路模型和 native 路口 cutback 生成,osm2streets
|
||||||
|
的 `center_lines.geojson` 只可作为视觉基准,绝不能作为输入。
|
||||||
|
|
||||||
|
### 2. 调用形式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native
|
||||||
|
```
|
||||||
|
|
||||||
|
工作台 `GET /api/state` 通过 `layers.centerLines` 返回该 FeatureCollection;native
|
||||||
|
Blender adapter 必须将 source `center_lines` 映射到既有 `center_lines` material layer。
|
||||||
|
|
||||||
|
### 3. 契约
|
||||||
|
|
||||||
|
- 每一 dash 均为 Polygon,长 `2m`、宽 `0.25m`、确定性间隔 `2m`,使用
|
||||||
|
`native-road-center-line/v1` provenance。
|
||||||
|
- 要素必须含有 `native_id`、`segment_id`、`road_id`、`directional_road_ids`、
|
||||||
|
`osm_way_ids`、`dash_index`、`dash_length_m`、`dash_gap_m` 与 `placement_rule`,以便
|
||||||
|
Workbench 用中文显示“道路中心虚线”并可回溯来源。
|
||||||
|
- 只有同一 `segment_id` 恰有一条 forward 和一条 backward native road 时才生成;单向、
|
||||||
|
`highway=service` 或退化中心线都不能伪造中心线。退化中心线须写
|
||||||
|
`invalid-center-line` diagnostic。
|
||||||
|
- 线段必须先经过 native junction cutback;再与 crosswalk、vehicle stop line 的控制面
|
||||||
|
求冲突,冲突 dash 直接略去。控制标线优先于中心虚线。
|
||||||
|
- `compiled.json.layers.centerLines`、`comparison.json.nativeCenterLineFeatures`、
|
||||||
|
`build-area.js` 的 native input records 与 required-layer 验证必须全部使用
|
||||||
|
`layers/center_lines.geojson`;缺失时在 Blender 启动前失败,不得静默漏画。
|
||||||
|
|
||||||
|
### 4. 校验与错误矩阵
|
||||||
|
|
||||||
|
| 条件 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 可确认的双向普通道路段 | 生成 2m / 0.25m 黄虚线,固定 2m gap |
|
||||||
|
| 单向或 `highway=service` 道路 | 不生成中心虚线 |
|
||||||
|
| 中心线不足两点、长度不可用 | `invalid-center-line` diagnostic,不写畸形 Polygon |
|
||||||
|
| dash 进入 junction cutback | trim 后不生成该范围 dash |
|
||||||
|
| dash 与斑马线或停止线相交 | 不生成冲突 dash |
|
||||||
|
| native Blender 输入缺 `center_lines.geojson` | `ensureNativeRoadLayers()` 抛错 |
|
||||||
|
|
||||||
|
### 5. 正常、基础与错误示例
|
||||||
|
|
||||||
|
- 正常:一条有 forward/backward carriageway 的 residential 段在两个方向道路之间生成黄虚线。
|
||||||
|
- 基础:没有双向证据的道路仍可有车道分隔线,但不产生道路中心虚线。
|
||||||
|
- 错误:从 osm2streets 图层复制或裁剪中心线;这会将渲染器缺陷重新变成 native 数据依赖。
|
||||||
|
|
||||||
|
### 6. 必需测试
|
||||||
|
|
||||||
|
- `npm run test:native-road`:断言双向生成、provenance、2m 尺寸/间隔、单向和 service 跳过。
|
||||||
|
- `npm run test:road-workbench`:断言 `centerLines` API、中文开关、选择溯源和概览计数。
|
||||||
|
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`:输出合法图层,检查中心线与控制标线不重叠。
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:日志列出 `center_lines`,且不运行 `package`。
|
||||||
|
|
||||||
|
### 7. 错误与正确写法
|
||||||
|
|
||||||
|
错误:只按一条 directional road 生成中心线,或忽略控制标线。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const line = road.centerline;
|
||||||
|
features.push(makeDash(line));
|
||||||
|
```
|
||||||
|
|
||||||
|
正确:先确认成对的双方向道路、做路口裁剪,再排除控制标线冲突。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (roads.length !== 2 || !forward || !backward || forward.highway === "service") continue;
|
||||||
|
const line = trimLineAtJunctions(forward.centerline, forward.sourceNodeIds, junctionPlans);
|
||||||
|
if (!ringsOverlapControl([ring], [...controls.crosswalks, ...controls.stopLines])) features.push(dash);
|
||||||
|
```
|
||||||
|
|
||||||
## Native 控制标线
|
## Native 控制标线
|
||||||
|
|
||||||
### 1. 范围与触发条件
|
### 1. 范围与触发条件
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"subtasks": [],
|
"subtasks": [],
|
||||||
"children": [
|
"children": [
|
||||||
"08-14-native-road-lane-markings",
|
"08-14-native-road-lane-markings",
|
||||||
"08-17-native-road-control-markings"
|
"08-17-native-road-control-markings",
|
||||||
|
"08-17-native-road-center-lines"
|
||||||
],
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"relatedFiles": [],
|
"relatedFiles": [],
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
50
.trellis/tasks/08-17-native-road-center-lines/design.md
Normal file
50
.trellis/tasks/08-17-native-road-center-lines/design.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Native Road Centre Lines Design
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The native compiler adds one independent polygon layer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
canonical bidirectional segment + trimmed OSM centreline
|
||||||
|
-> repeated 2m x 0.25m dash polygons
|
||||||
|
-> native-road/layers/center_lines.geojson
|
||||||
|
-> Workbench / native Blender adapter / Cesium
|
||||||
|
```
|
||||||
|
|
||||||
|
It does not read osm2streets `center_lines.geojson`. The legacy layer is a
|
||||||
|
visual baseline only.
|
||||||
|
|
||||||
|
## Placement
|
||||||
|
|
||||||
|
- A candidate must have exactly forward and backward native roads for the same
|
||||||
|
`segmentId`, neither be `highway=service`, and have a valid trimmed OSM
|
||||||
|
centreline.
|
||||||
|
- Dashes use the legacy observed dimensions: 2m length, 0.25m width, with a
|
||||||
|
deterministic 2m gap. The first dash starts at a fixed segment-local offset
|
||||||
|
so rebuilds do not drift.
|
||||||
|
- The line is trimmed with the same `junctionPlans` cutback used by native lane
|
||||||
|
centreline generation. Any dash intersecting a crosswalk or stop line is
|
||||||
|
omitted, preserving the control-marking priority already established.
|
||||||
|
- One-way and service segments have no generated feature. A degenerate source
|
||||||
|
line produces a diagnostic rather than malformed geometry.
|
||||||
|
|
||||||
|
## Contracts
|
||||||
|
|
||||||
|
`layers/center_lines.geojson` is a Polygon FeatureCollection. Every feature
|
||||||
|
has `native_id`, `segment_id`, `directional_road_ids`, `osm_way_ids`,
|
||||||
|
`dash_index`, `dash_length_m`, `dash_gap_m`, `placement_rule`, and
|
||||||
|
`provenance="native-road-center-line/v1"`.
|
||||||
|
|
||||||
|
`compiled.json.layers.centerLines`, comparison count
|
||||||
|
`nativeCenterLineFeatures`, and `build-area.js` required native records use the
|
||||||
|
same filename. `catalog.NATIVE_ROAD_LAYERS` maps source `center_lines` to the
|
||||||
|
existing `center_lines` material layer.
|
||||||
|
|
||||||
|
The Workbench loads this source into the marking overlay, exposes a Chinese
|
||||||
|
toggle, and identifies it as `道路中心虚线` on selection.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The change is additive within `native-road/`; `--road-provider osm2streets`
|
||||||
|
remains unaffected and is rollback. A native Blender build treats a missing
|
||||||
|
layer as an input error rather than silently omitting it.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
19
.trellis/tasks/08-17-native-road-center-lines/implement.md
Normal file
19
.trellis/tasks/08-17-native-road-center-lines/implement.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
1. Add centre-dash constants and a pure native compiler routine based on the
|
||||||
|
shared bidirectional segment model and junction-trimmed centreline.
|
||||||
|
2. Filter invalid, one-way, service, and control-marking-conflicting dashes;
|
||||||
|
record actionable diagnostics for invalid geometry only.
|
||||||
|
3. Write `center_lines.geojson`, compilation/comparison counts, native build
|
||||||
|
records, required-layer checks, and Blender material adapter mapping.
|
||||||
|
4. Add Workbench API state, Chinese toggle, summary count, rendering, and
|
||||||
|
source evidence for a selected centre dash.
|
||||||
|
5. Add focused native and Workbench tests for generation, skips, control
|
||||||
|
avoidance, provenance, and file/API contracts.
|
||||||
|
6. Run native/unit/workbench/build-stage tests, compile/check Nantaizi, then
|
||||||
|
validate `blender,cesium,preview --road-provider native` without `package`.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Choose `--road-provider osm2streets`; no legacy layer or published package is
|
||||||
|
modified.
|
||||||
58
.trellis/tasks/08-17-native-road-center-lines/prd.md
Normal file
58
.trellis/tasks/08-17-native-road-center-lines/prd.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Native road center lines
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Restore the existing osm2streets-style road-centre dashed markings in the
|
||||||
|
native-road provider before treating native output as ready for broader quality
|
||||||
|
gates or new-area validation.
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- Nantaizi's existing `osm2streets_web_out/center_lines.geojson` has 885
|
||||||
|
`type="center line"` polygons. Its sampled dash geometry is approximately
|
||||||
|
2.00m long by 0.25m wide.
|
||||||
|
- The legacy extraction excludes centre lines that overlap crosswalk zones and
|
||||||
|
that fall on service driving polygons (`build-osm2streets-qgis.js:474-480`).
|
||||||
|
- Native output currently has only same-direction `lane_separators`; it has no
|
||||||
|
`center_lines.geojson`, so a bidirectional road's directional carriageways
|
||||||
|
lack their visual and semantic divider.
|
||||||
|
- Existing Blender already owns a `center_lines` / `Center Line` material
|
||||||
|
layer. Native must adapt to it rather than creating a second scene registry.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- R1: For supported non-service, bidirectional native road segments, emit a
|
||||||
|
`center_lines.geojson` dashed centre divider derived from the canonical OSM
|
||||||
|
centreline and the segment's two directional carriageways.
|
||||||
|
- R2: Match the established visual baseline: yellow 0.25m-wide, 2m-long
|
||||||
|
dashes, clipped away from ordinary junction cutbacks and control markings.
|
||||||
|
- R3: Preserve source traceability: each dash records its native segment,
|
||||||
|
source OSM way, involved directional roads, placement interval/rule, and a
|
||||||
|
dedicated provenance value.
|
||||||
|
- R4: Add the layer to compilation records, Road Workbench display/selection
|
||||||
|
in Chinese, and the existing Blender/Cesium `center_lines` material path.
|
||||||
|
- R5: Do not render a centre divider on one-way or `highway=service` segments;
|
||||||
|
report invalid geometry instead of fabricating a divider.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Native Nantaizi output contains a valid `layers/center_lines.geojson`
|
||||||
|
with source-traceable 2m x 0.25m dashed polygons.
|
||||||
|
- [ ] No native centre dash intersects a generated crosswalk or vehicle stop
|
||||||
|
line, and no dash reaches into the supported junction surface cutback.
|
||||||
|
- [ ] The Workbench can toggle and select centre lines, presenting their
|
||||||
|
source and rule in Chinese rather than calling them lane separators.
|
||||||
|
- [ ] Native `blender,cesium,preview` consumes the layer using the existing
|
||||||
|
`center_lines` material and does not run `package`.
|
||||||
|
- [ ] Tests cover two-way generation, one-way/service skips, control-marking
|
||||||
|
avoidance, provenance, output contract, and Workbench wiring.
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- Centre-line editing overrides, solid/double-centre-line semantics, arbitrary
|
||||||
|
OSM `overtaking` interpretation, lane colouring changes, and other areas.
|
||||||
|
|
||||||
|
## Key Decision
|
||||||
|
|
||||||
|
This task deliberately matches the existing stable visual layer first. It does
|
||||||
|
not claim to infer a country-wide legal marking taxonomy from sparse OSM tags.
|
||||||
26
.trellis/tasks/08-17-native-road-center-lines/task.json
Normal file
26
.trellis/tasks/08-17-native-road-center-lines/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "native-road-center-lines",
|
||||||
|
"name": "native-road-center-lines",
|
||||||
|
"title": "Native road center lines",
|
||||||
|
"description": "Align native-road output with the existing center_lines layer before broader quality work.",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-17",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "feature/native-road-compiler",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": "08-13-native-road-compiler",
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -799,11 +799,11 @@ def build(args):
|
|||||||
target = source["material_layer"]
|
target = source["material_layer"]
|
||||||
layer = material_layers[target]
|
layer = material_layers[target]
|
||||||
source_path = os.path.join(native_road_dir, "layers", source["source"] + ".geojson")
|
source_path = os.path.join(native_road_dir, "layers", source["source"] + ".geojson")
|
||||||
|
if not os.path.isfile(source_path):
|
||||||
|
raise RuntimeError("Native road layer is missing: " + source_path)
|
||||||
count = _roads.assemble_geojson_layer(
|
count = _roads.assemble_geojson_layer(
|
||||||
source_path, source["source"], projector, roads_c,
|
source_path, source["source"], projector, roads_c,
|
||||||
road_mats[target], layer["z"])
|
road_mats[target], layer["z"])
|
||||||
if count == 0:
|
|
||||||
raise RuntimeError("Native road layer has no usable geometry: " + source_path)
|
|
||||||
road_counts[source["source"]] = count
|
road_counts[source["source"]] = count
|
||||||
elif geojson_dir and os.path.isdir(geojson_dir):
|
elif geojson_dir and os.path.isdir(geojson_dir):
|
||||||
for problem in catalog.check_layers(geojson_dir):
|
for problem in catalog.check_layers(geojson_dir):
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ NATIVE_ROAD_LAYERS = (
|
|||||||
{"source": "intersection_surface", "material_layer": "intersection_surface"},
|
{"source": "intersection_surface", "material_layer": "intersection_surface"},
|
||||||
{"source": "sidewalk_surface", "material_layer": "sidewalks"},
|
{"source": "sidewalk_surface", "material_layer": "sidewalks"},
|
||||||
{"source": "lane_separators", "material_layer": "lane_separators"},
|
{"source": "lane_separators", "material_layer": "lane_separators"},
|
||||||
|
{"source": "center_lines", "material_layer": "center_lines"},
|
||||||
{"source": "direction_arrows", "material_layer": "lane_arrows_webscale"},
|
{"source": "direction_arrows", "material_layer": "lane_arrows_webscale"},
|
||||||
{"source": "turn_arrows", "material_layer": "lane_arrows_webscale"},
|
{"source": "turn_arrows", "material_layer": "lane_arrows_webscale"},
|
||||||
{"source": "crosswalks", "material_layer": "crosswalks"},
|
{"source": "crosswalks", "material_layer": "crosswalks"},
|
||||||
|
|||||||
@@ -53,7 +53,13 @@ class RoadLayerCatalogTest(unittest.TestCase):
|
|||||||
for entry in NATIVE_ROAD_LAYERS],
|
for entry in NATIVE_ROAD_LAYERS],
|
||||||
[("road_surface", "road_surface"),
|
[("road_surface", "road_surface"),
|
||||||
("intersection_surface", "intersection_surface"),
|
("intersection_surface", "intersection_surface"),
|
||||||
("sidewalk_surface", "sidewalks")])
|
("sidewalk_surface", "sidewalks"),
|
||||||
|
("lane_separators", "lane_separators"),
|
||||||
|
("center_lines", "center_lines"),
|
||||||
|
("direction_arrows", "lane_arrows_webscale"),
|
||||||
|
("turn_arrows", "lane_arrows_webscale"),
|
||||||
|
("crosswalks", "crosswalks"),
|
||||||
|
("vehicle_stop_lines", "vehicle_stop_lines")])
|
||||||
self.assertTrue(all(entry["material_layer"] in layers
|
self.assertTrue(all(entry["material_layer"] in layers
|
||||||
for entry in NATIVE_ROAD_LAYERS))
|
for entry in NATIVE_ROAD_LAYERS))
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ function buildBlenderScene(area, roadProvider) {
|
|||||||
|
|
||||||
function ensureNativeRoadLayers(area) {
|
function ensureNativeRoadLayers(area) {
|
||||||
ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation");
|
ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation");
|
||||||
for (const file of ["road_surface.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) {
|
for (const file of ["road_surface.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "center_lines.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) {
|
||||||
ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`);
|
ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,6 +305,7 @@ function nativeRoadRecords(area) {
|
|||||||
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
|
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
|
||||||
nativeSidewalkSurface: fileRecord(path.join(root, "sidewalk_surface.geojson")),
|
nativeSidewalkSurface: fileRecord(path.join(root, "sidewalk_surface.geojson")),
|
||||||
nativeLaneSeparators: fileRecord(path.join(root, "lane_separators.geojson")),
|
nativeLaneSeparators: fileRecord(path.join(root, "lane_separators.geojson")),
|
||||||
|
nativeCenterLines: fileRecord(path.join(root, "center_lines.geojson")),
|
||||||
nativeDirectionArrows: fileRecord(path.join(root, "direction_arrows.geojson")),
|
nativeDirectionArrows: fileRecord(path.join(root, "direction_arrows.geojson")),
|
||||||
nativeTurnArrows: fileRecord(path.join(root, "turn_arrows.geojson")),
|
nativeTurnArrows: fileRecord(path.join(root, "turn_arrows.geojson")),
|
||||||
nativeCrosswalks: fileRecord(path.join(root, "crosswalks.geojson")),
|
nativeCrosswalks: fileRecord(path.join(root, "crosswalks.geojson")),
|
||||||
@@ -319,6 +320,7 @@ function nativeRoadFeatureCounts(area) {
|
|||||||
intersectionSurface: featureCount(path.join(root, "intersection_surface.geojson")),
|
intersectionSurface: featureCount(path.join(root, "intersection_surface.geojson")),
|
||||||
sidewalkSurface: featureCount(path.join(root, "sidewalk_surface.geojson")),
|
sidewalkSurface: featureCount(path.join(root, "sidewalk_surface.geojson")),
|
||||||
laneSeparators: featureCount(path.join(root, "lane_separators.geojson")),
|
laneSeparators: featureCount(path.join(root, "lane_separators.geojson")),
|
||||||
|
centerLines: featureCount(path.join(root, "center_lines.geojson")),
|
||||||
directionArrows: featureCount(path.join(root, "direction_arrows.geojson")),
|
directionArrows: featureCount(path.join(root, "direction_arrows.geojson")),
|
||||||
turnArrows: featureCount(path.join(root, "turn_arrows.geojson")),
|
turnArrows: featureCount(path.join(root, "turn_arrows.geojson")),
|
||||||
crosswalks: featureCount(path.join(root, "crosswalks.geojson")),
|
crosswalks: featureCount(path.join(root, "crosswalks.geojson")),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function compileArea(configPath) {
|
|||||||
model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections },
|
model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections },
|
||||||
movements: compiled.movements,
|
movements: compiled.movements,
|
||||||
diagnostics: compiled.diagnostics,
|
diagnostics: compiled.diagnostics,
|
||||||
layers: { roadSurface: "layers/road_surface.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", laneSeparators: "layers/lane_separators.geojson", directionArrows: "layers/direction_arrows.geojson", turnArrows: "layers/turn_arrows.geojson", crosswalks: "layers/crosswalks.geojson", vehicleStopLines: "layers/vehicle_stop_lines.geojson", connectors: "layers/connectors.geojson" },
|
layers: { roadSurface: "layers/road_surface.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", laneSeparators: "layers/lane_separators.geojson", centerLines: "layers/center_lines.geojson", directionArrows: "layers/direction_arrows.geojson", turnArrows: "layers/turn_arrows.geojson", crosswalks: "layers/crosswalks.geojson", vehicleStopLines: "layers/vehicle_stop_lines.geojson", connectors: "layers/connectors.geojson" },
|
||||||
};
|
};
|
||||||
const comparison = compareOsm2Streets(area, result.model, compiled);
|
const comparison = compareOsm2Streets(area, result.model, compiled);
|
||||||
writeJsonAtomic(path.join(staging, "compiled.json"), result);
|
writeJsonAtomic(path.join(staging, "compiled.json"), result);
|
||||||
@@ -45,6 +45,7 @@ function compileArea(configPath) {
|
|||||||
writeJsonAtomic(path.join(staging, "layers", "intersection_surface.geojson"), compiled.intersectionSurface);
|
writeJsonAtomic(path.join(staging, "layers", "intersection_surface.geojson"), compiled.intersectionSurface);
|
||||||
writeJsonAtomic(path.join(staging, "layers", "lane_centerlines.geojson"), compiled.laneCenterlines);
|
writeJsonAtomic(path.join(staging, "layers", "lane_centerlines.geojson"), compiled.laneCenterlines);
|
||||||
writeJsonAtomic(path.join(staging, "layers", "lane_separators.geojson"), compiled.laneSeparators);
|
writeJsonAtomic(path.join(staging, "layers", "lane_separators.geojson"), compiled.laneSeparators);
|
||||||
|
writeJsonAtomic(path.join(staging, "layers", "center_lines.geojson"), compiled.centerLines);
|
||||||
writeJsonAtomic(path.join(staging, "layers", "direction_arrows.geojson"), compiled.directionArrows);
|
writeJsonAtomic(path.join(staging, "layers", "direction_arrows.geojson"), compiled.directionArrows);
|
||||||
writeJsonAtomic(path.join(staging, "layers", "turn_arrows.geojson"), compiled.turnArrows);
|
writeJsonAtomic(path.join(staging, "layers", "turn_arrows.geojson"), compiled.turnArrows);
|
||||||
writeJsonAtomic(path.join(staging, "layers", "crosswalks.geojson"), compiled.crosswalks);
|
writeJsonAtomic(path.join(staging, "layers", "crosswalks.geojson"), compiled.crosswalks);
|
||||||
@@ -86,6 +87,7 @@ function compareOsm2Streets(area, model, compiled) {
|
|||||||
nativeMaxJunctionExpansionRatio: junctions.reduce((maximum, feature) => Math.max(maximum, Number(feature.properties.expansion_ratio) || 0), 0),
|
nativeMaxJunctionExpansionRatio: junctions.reduce((maximum, feature) => Math.max(maximum, Number(feature.properties.expansion_ratio) || 0), 0),
|
||||||
nativeLaneCenterlineFeatures: compiled.laneCenterlines.features.length,
|
nativeLaneCenterlineFeatures: compiled.laneCenterlines.features.length,
|
||||||
nativeLaneSeparatorFeatures: compiled.laneSeparators.features.length,
|
nativeLaneSeparatorFeatures: compiled.laneSeparators.features.length,
|
||||||
|
nativeCenterLineFeatures: compiled.centerLines.features.length,
|
||||||
nativeDirectionArrowFeatures: compiled.directionArrows.features.length,
|
nativeDirectionArrowFeatures: compiled.directionArrows.features.length,
|
||||||
nativeTurnArrowFeatures: compiled.turnArrows.features.length,
|
nativeTurnArrowFeatures: compiled.turnArrows.features.length,
|
||||||
nativeCrosswalkFeatures: compiled.crosswalks.features.length,
|
nativeCrosswalkFeatures: compiled.crosswalks.features.length,
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const DIRECTION_ARROW_INTERVAL_METERS = 32;
|
|||||||
const DIRECTION_ARROW_ENDPOINT_BUFFER_METERS = 14;
|
const DIRECTION_ARROW_ENDPOINT_BUFFER_METERS = 14;
|
||||||
const STOP_LINE_OFFSET_METERS = 2.7;
|
const STOP_LINE_OFFSET_METERS = 2.7;
|
||||||
const STOP_LINE_MAX_APPROACH_DISTANCE_METERS = 25;
|
const STOP_LINE_MAX_APPROACH_DISTANCE_METERS = 25;
|
||||||
|
const CENTER_LINE_DASH_LENGTH_METERS = 2;
|
||||||
|
const CENTER_LINE_DASH_GAP_METERS = 2;
|
||||||
|
const CENTER_LINE_WIDTH_METERS = .25;
|
||||||
|
|
||||||
function parseOsmRoads(xml) {
|
function parseOsmRoads(xml) {
|
||||||
const nodes = new Map();
|
const nodes = new Map();
|
||||||
@@ -244,12 +247,39 @@ function compileGeometry(model, overrides = { overrides: [] }) {
|
|||||||
}
|
}
|
||||||
const lanes = compileLaneCenterlines(model, diagnostics, junctionPlans);
|
const lanes = compileLaneCenterlines(model, diagnostics, junctionPlans);
|
||||||
const controls = compileControlMarkings(model, lanes, diagnostics);
|
const controls = compileControlMarkings(model, lanes, diagnostics);
|
||||||
|
const centerLines = compileCenterLines(model, junctionPlans, controls, diagnostics);
|
||||||
const markings = compileLaneMarkings(model, lanes, diagnostics, junctionPlans, controls);
|
const markings = compileLaneMarkings(model, lanes, diagnostics, junctionPlans, controls);
|
||||||
const sidewalks = compileSidewalkSurfaces(model, diagnostics, junctionPlans);
|
const sidewalks = compileSidewalkSurfaces(model, diagnostics, junctionPlans);
|
||||||
const connectorResult = compileConnectors(model, lanes, diagnostics, overrides);
|
const connectorResult = compileConnectors(model, lanes, diagnostics, overrides);
|
||||||
const junctionFeatures = compileJunctionSurfaces(model, junctionPlans, connectorResult.features, connectorResult.movements, diagnostics);
|
const junctionFeatures = compileJunctionSurfaces(model, junctionPlans, connectorResult.features, connectorResult.movements, diagnostics);
|
||||||
validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics);
|
validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics);
|
||||||
return { roadSurface: { type: "FeatureCollection", features }, sidewalkSurface: { type: "FeatureCollection", features: sidewalks }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, laneSeparators: { type: "FeatureCollection", features: markings.separators }, 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 };
|
return { roadSurface: { type: "FeatureCollection", features }, 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCenterLines(model, junctionPlans, controls, diagnostics) {
|
||||||
|
const features = [];
|
||||||
|
const controlFeatures = [...controls.crosswalks, ...controls.stopLines];
|
||||||
|
const segments = new Map();
|
||||||
|
for (const road of model.roads) {
|
||||||
|
if (!segments.has(road.segmentId)) segments.set(road.segmentId, []);
|
||||||
|
segments.get(road.segmentId).push(road);
|
||||||
|
}
|
||||||
|
for (const [segmentId, roads] of segments) {
|
||||||
|
const forward = roads.find((road) => road.direction === "forward");
|
||||||
|
const backward = roads.find((road) => road.direction === "backward");
|
||||||
|
if (roads.length !== 2 || !forward || !backward || forward.highway === "service" || backward.highway === "service") continue;
|
||||||
|
const line = trimLineAtJunctions(forward.centerline, forward.sourceNodeIds, junctionPlans);
|
||||||
|
const length = lineLengthMeters(line);
|
||||||
|
if (line.length < 2 || !Number.isFinite(length)) { diagnostics.push(diagnostic("warning", segmentId, forward.osmWayIds, "invalid-center-line", "双向道路无法生成有效道路中心虚线。", forward.centerline[0])); continue; }
|
||||||
|
for (let start = 0, dashIndex = 1; start + CENTER_LINE_DASH_LENGTH_METERS <= length; start += CENTER_LINE_DASH_LENGTH_METERS + CENTER_LINE_DASH_GAP_METERS, dashIndex += 1) {
|
||||||
|
const placement = pointAndAxisAlongLine(line, start + CENTER_LINE_DASH_LENGTH_METERS / 2);
|
||||||
|
if (!placement) continue;
|
||||||
|
const ring = rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], CENTER_LINE_DASH_LENGTH_METERS, CENTER_LINE_WIDTH_METERS, 0);
|
||||||
|
if (ringsOverlapControl([ring], controlFeatures)) continue;
|
||||||
|
features.push({ type: "Feature", properties: { native_id: `center-line:${segmentId}:${dashIndex}`, segment_id: segmentId, road_id: forward.id, directional_road_ids: roads.map((road) => road.id).join(","), osm_way_ids: forward.osmWayIds.join(","), dash_index: dashIndex, dash_length_m: CENTER_LINE_DASH_LENGTH_METERS, dash_gap_m: CENTER_LINE_DASH_GAP_METERS, placement_rule: "native-bidirectional-centerline/v1", provenance: "native-road-center-line/v1" }, geometry: { type: "Polygon", coordinates: [ring] } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileControlMarkings(model, lanes, diagnostics) {
|
function compileControlMarkings(model, lanes, diagnostics) {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function handle(request, response, area, configPath) {
|
|||||||
function state(area) {
|
function state(area) {
|
||||||
const nativeDir = area.outputs.nativeRoadDir;
|
const nativeDir = area.outputs.nativeRoadDir;
|
||||||
const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson");
|
const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson");
|
||||||
return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), comparison: readJson(path.join(nativeDir, "comparison.json")), layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), nativeSidewalkSurface: readLayer(path.join(nativeDir, "layers", "sidewalk_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), laneSeparators: readLayer(path.join(nativeDir, "layers", "lane_separators.geojson")), directionArrows: readLayer(path.join(nativeDir, "layers", "direction_arrows.geojson")), turnArrows: readLayer(path.join(nativeDir, "layers", "turn_arrows.geojson")), crosswalks: readLayer(path.join(nativeDir, "layers", "crosswalks.geojson")), vehicleStopLines: readLayer(path.join(nativeDir, "layers", "vehicle_stop_lines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } };
|
return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), comparison: readJson(path.join(nativeDir, "comparison.json")), layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), nativeSidewalkSurface: readLayer(path.join(nativeDir, "layers", "sidewalk_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), laneSeparators: readLayer(path.join(nativeDir, "layers", "lane_separators.geojson")), centerLines: readLayer(path.join(nativeDir, "layers", "center_lines.geojson")), directionArrows: readLayer(path.join(nativeDir, "layers", "direction_arrows.geojson")), turnArrows: readLayer(path.join(nativeDir, "layers", "turn_arrows.geojson")), crosswalks: readLayer(path.join(nativeDir, "layers", "crosswalks.geojson")), vehicleStopLines: readLayer(path.join(nativeDir, "layers", "vehicle_stop_lines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } };
|
||||||
}
|
}
|
||||||
function readCompiled(area) { return readJson(path.join(area.outputs.nativeRoadDir, "compiled.json")); }
|
function readCompiled(area) { return readJson(path.join(area.outputs.nativeRoadDir, "compiled.json")); }
|
||||||
function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ assert.equal(geometry.sidewalkSurface.features.length, 2);
|
|||||||
assert.ok(geometry.sidewalkSurface.features.every((feature) => feature.geometry.coordinates[0].length >= 5));
|
assert.ok(geometry.sidewalkSurface.features.every((feature) => feature.geometry.coordinates[0].length >= 5));
|
||||||
assert.equal(geometry.laneCenterlines.features.length, model.roads.reduce((sum, road) => sum + road.laneCount, 0));
|
assert.equal(geometry.laneCenterlines.features.length, model.roads.reduce((sum, road) => sum + road.laneCount, 0));
|
||||||
assert.ok(geometry.laneSeparators.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-lane-separator/v1"));
|
assert.ok(geometry.laneSeparators.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-lane-separator/v1"));
|
||||||
|
assert.ok(geometry.centerLines.features.length > 0);
|
||||||
|
assert.ok(geometry.centerLines.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-center-line/v1" && feature.properties.dash_length_m === 2 && feature.properties.dash_gap_m === 2));
|
||||||
|
for (const feature of geometry.centerLines.features) {
|
||||||
|
const ring = feature.geometry.coordinates[0];
|
||||||
|
const lengths = [distance(ring[0], ring[1]), distance(ring[1], ring[2])].sort((a, b) => a - b);
|
||||||
|
assert.ok(Math.abs(lengths[0] - .25) < .01 && Math.abs(lengths[1] - 2) < .01);
|
||||||
|
assert.ok(feature.properties.segment_id && feature.properties.directional_road_ids && feature.properties.osm_way_ids && feature.properties.placement_rule);
|
||||||
|
}
|
||||||
assert.ok(geometry.directionArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-direction-arrow/v1" && feature.properties.placement_interval_meters === 32));
|
assert.ok(geometry.directionArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-direction-arrow/v1" && feature.properties.placement_interval_meters === 32));
|
||||||
assert.ok(geometry.turnArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-turn-arrow/v1"));
|
assert.ok(geometry.turnArrows.features.every((feature) => feature.geometry.type === "Polygon" && feature.properties.provenance === "native-road-turn-arrow/v1"));
|
||||||
assert.ok(geometry.connectors.features.length > 0);
|
assert.ok(geometry.connectors.features.length > 0);
|
||||||
@@ -46,10 +54,15 @@ assert.equal(controlGeometry.vehicleStopLines.features.length, 1);
|
|||||||
assert.ok(controlGeometry.crosswalks.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-crosswalk/v1"));
|
assert.ok(controlGeometry.crosswalks.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-crosswalk/v1"));
|
||||||
assert.ok(controlGeometry.vehicleStopLines.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-stop-line/v1"));
|
assert.ok(controlGeometry.vehicleStopLines.features.every((feature) => feature.properties.crossing_node_id === "2" && feature.properties.provenance === "native-road-stop-line/v1"));
|
||||||
assert.ok(controlGeometry.diagnostics.some((item) => item.rule === "crossing-no-native-lane" && item.sourceIds.includes("5")));
|
assert.ok(controlGeometry.diagnostics.some((item) => item.rule === "crossing-no-native-lane" && item.sourceIds.includes("5")));
|
||||||
|
assert.ok(controlGeometry.centerLines.features.length > 0);
|
||||||
|
assert.ok(controlGeometry.centerLines.features.every((dash) => ![...controlGeometry.crosswalks.features, ...controlGeometry.vehicleStopLines.features].some((control) => ringsOverlap(dash.geometry.coordinates[0], control.geometry.coordinates[0]))));
|
||||||
const arrowControlOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.00095" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="zebra"/></node><node id="3" lon="114.001" lat="30"/><way id="63"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/><tag k="lanes" v="1"/><tag k="turn:lanes" v="through"/></way></osm>`;
|
const arrowControlOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.00095" lat="30"><tag k="highway" v="crossing"/><tag k="crossing:markings" v="zebra"/></node><node id="3" lon="114.001" lat="30"/><way id="63"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="oneway" v="yes"/><tag k="lanes" v="1"/><tag k="turn:lanes" v="through"/></way></osm>`;
|
||||||
const arrowControlGeometry = compileGeometry(compileRoadModel(arrowControlOsm, empty));
|
const arrowControlGeometry = compileGeometry(compileRoadModel(arrowControlOsm, empty));
|
||||||
assert.ok(arrowControlGeometry.turnArrows.features.length > 0);
|
assert.ok(arrowControlGeometry.turnArrows.features.length > 0);
|
||||||
assert.ok(arrowControlGeometry.turnArrows.features.every((feature) => feature.properties.placement_distance_meters > 6));
|
assert.ok(arrowControlGeometry.turnArrows.features.every((feature) => feature.properties.placement_distance_meters > 6));
|
||||||
|
assert.equal(arrowControlGeometry.centerLines.features.length, 0);
|
||||||
|
const serviceCenterLineOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><way id="64"><nd ref="1"/><nd ref="2"/><tag k="highway" v="service"/></way></osm>`;
|
||||||
|
assert.equal(compileGeometry(compileRoadModel(serviceCenterLineOsm, empty)).centerLines.features.length, 0);
|
||||||
const crossOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.002" lat="30"/><node id="4" lon="114.001" lat="30.001"/><node id="5" lon="114.001" lat="29.999"/><way id="40"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="41"><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="42"><nd ref="5"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="43"><nd ref="2"/><nd ref="4"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way></osm>`;
|
const crossOsm = `<osm><node id="1" lon="114" lat="30"/><node id="2" lon="114.001" lat="30"/><node id="3" lon="114.002" lat="30"/><node id="4" lon="114.001" lat="30.001"/><node id="5" lon="114.001" lat="29.999"/><way id="40"><nd ref="1"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="41"><nd ref="2"/><nd ref="3"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="42"><nd ref="5"/><nd ref="2"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way><way id="43"><nd ref="2"/><nd ref="4"/><tag k="highway" v="residential"/><tag k="sidewalk" v="both"/></way></osm>`;
|
||||||
const crossCenter = [114.001, 30];
|
const crossCenter = [114.001, 30];
|
||||||
const crossGeometry = compileGeometry(compileRoadModel(crossOsm, empty));
|
const crossGeometry = compileGeometry(compileRoadModel(crossOsm, empty));
|
||||||
@@ -112,6 +125,9 @@ try {
|
|||||||
const compiledArea = compileArea(config);
|
const compiledArea = compileArea(config);
|
||||||
assert.equal(compiledArea.result.areaId, "fresh");
|
assert.equal(compiledArea.result.areaId, "fresh");
|
||||||
assert.ok(fs.existsSync(path.join(outputRoot, "fresh", "native-road", "compiled.json")));
|
assert.ok(fs.existsSync(path.join(outputRoot, "fresh", "native-road", "compiled.json")));
|
||||||
|
const centerLineLayer = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "layers", "center_lines.geojson"), "utf8"));
|
||||||
|
assert.equal(centerLineLayer.type, "FeatureCollection");
|
||||||
|
assert.equal(centerLineLayer.features.length, compiledArea.comparison.nativeCenterLineFeatures);
|
||||||
assert.equal(compiledArea.comparison.schema, "native-road-comparison/v2");
|
assert.equal(compiledArea.comparison.schema, "native-road-comparison/v2");
|
||||||
assert.equal(compiledArea.comparison.nativeRoadCount, compiledArea.result.model.roads.length);
|
assert.equal(compiledArea.comparison.nativeRoadCount, compiledArea.result.model.roads.length);
|
||||||
assert.equal(compiledArea.comparison.nativePublishedMovementCount, compiledArea.result.movements.filter((movement) => movement.geometryPublished).length);
|
assert.equal(compiledArea.comparison.nativePublishedMovementCount, compiledArea.result.movements.filter((movement) => movement.geometryPublished).length);
|
||||||
@@ -123,4 +139,13 @@ try {
|
|||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(freshArea, { recursive: true, force: true });
|
fs.rmSync(freshArea, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function distance(a, b) {
|
||||||
|
return Math.hypot((b[0] - a[0]) * 111320 * Math.cos(a[1] * Math.PI / 180), (b[1] - a[1]) * 111320);
|
||||||
|
}
|
||||||
|
function ringsOverlap(first, second) {
|
||||||
|
const bounds = (ring) => [Math.min(...ring.map((point) => point[0])), Math.min(...ring.map((point) => point[1])), Math.max(...ring.map((point) => point[0])), Math.max(...ring.map((point) => point[1]))];
|
||||||
|
const a = bounds(first); const b = bounds(second);
|
||||||
|
return !(a[0] > b[2] || a[2] < b[0] || a[1] > b[3] || a[3] < b[1]);
|
||||||
|
}
|
||||||
console.log("native road tests passed");
|
console.log("native road tests passed");
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ assert.match(app, /外缘扩张倍率/);
|
|||||||
assert.match(app, /最大外缘扩张/);
|
assert.match(app, /最大外缘扩张/);
|
||||||
assert.match(app, /道路方向箭头/);
|
assert.match(app, /道路方向箭头/);
|
||||||
assert.match(app, /native-road-direction-arrow\/v1/);
|
assert.match(app, /native-road-direction-arrow\/v1/);
|
||||||
|
assert.match(app, /data-layer="centerLines" type="checkbox" checked> 道路中心虚线/);
|
||||||
|
assert.match(app, /centerLines: new VectorLayer/);
|
||||||
|
assert.match(app, /state\.layers\.centerLines/);
|
||||||
|
assert.match(app, /native-road-center-line\/v1/);
|
||||||
assert.match(app, /data-layer="controls" type="checkbox" checked> 斑马线与停止线/);
|
assert.match(app, /data-layer="controls" type="checkbox" checked> 斑马线与停止线/);
|
||||||
assert.match(app, /controls: new VectorLayer/);
|
assert.match(app, /controls: new VectorLayer/);
|
||||||
assert.match(app, /state\.layers\.crosswalks/);
|
assert.match(app, /state\.layers\.crosswalks/);
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ const directionArrowsToggle = document.createElement("label");
|
|||||||
directionArrowsToggle.innerHTML = '<input data-layer="directionArrows" type="checkbox" checked> 道路方向箭头';
|
directionArrowsToggle.innerHTML = '<input data-layer="directionArrows" type="checkbox" checked> 道路方向箭头';
|
||||||
const markingsToggle = document.createElement("label");
|
const markingsToggle = document.createElement("label");
|
||||||
markingsToggle.innerHTML = '<input data-layer="markings" type="checkbox" checked> 车道分隔线与路口转向箭头';
|
markingsToggle.innerHTML = '<input data-layer="markings" type="checkbox" checked> 车道分隔线与路口转向箭头';
|
||||||
|
const centerLinesToggle = document.createElement("label");
|
||||||
|
centerLinesToggle.innerHTML = '<input data-layer="centerLines" type="checkbox" checked> 道路中心虚线';
|
||||||
const controlsToggle = document.createElement("label");
|
const controlsToggle = document.createElement("label");
|
||||||
controlsToggle.innerHTML = '<input data-layer="controls" type="checkbox" checked> 斑马线与停止线';
|
controlsToggle.innerHTML = '<input data-layer="controls" type="checkbox" checked> 斑马线与停止线';
|
||||||
document.querySelector('[data-layer="lanes"]').closest("label").after(directionArrowsToggle, markingsToggle, controlsToggle);
|
document.querySelector('[data-layer="lanes"]').closest("label").after(directionArrowsToggle, markingsToggle, centerLinesToggle, controlsToggle);
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
let selectedRoad = null;
|
let selectedRoad = null;
|
||||||
@@ -66,6 +68,7 @@ const layers = {
|
|||||||
lanes: new VectorLayer({ source: source(), style: laneStyle }),
|
lanes: new VectorLayer({ source: source(), style: laneStyle }),
|
||||||
directionArrows: new VectorLayer({ source: source(), style: markingStyle }),
|
directionArrows: new VectorLayer({ source: source(), style: markingStyle }),
|
||||||
markings: new VectorLayer({ source: source(), style: markingStyle }),
|
markings: new VectorLayer({ source: source(), style: markingStyle }),
|
||||||
|
centerLines: new VectorLayer({ source: source(), style: centerLineStyle }),
|
||||||
controls: new VectorLayer({ source: source(), style: markingStyle }),
|
controls: new VectorLayer({ source: source(), style: markingStyle }),
|
||||||
osmDirection: new VectorLayer({ source: source(), style: (feature) => new Style({ image: new RegularShape({ points: 3, radius: 9, rotation: feature.get("rotation"), fill: new Fill({ color: "#006e91" }), stroke: new Stroke({ color: "#fff", width: 1.5 }) }) }), zIndex: 11 }),
|
osmDirection: new VectorLayer({ source: source(), style: (feature) => new Style({ image: new RegularShape({ points: 3, radius: 9, rotation: feature.get("rotation"), fill: new Fill({ color: "#006e91" }), stroke: new Stroke({ color: "#fff", width: 1.5 }) }) }), zIndex: 11 }),
|
||||||
connectors: new VectorLayer({ source: source(), style: (feature) => effectiveConnectorEnabled(feature.getProperties()) ? new Style({ stroke: new Stroke({ color: roadIdFromLane(feature.get("from_lane_id")) === selectedRoad?.id ? "#d1226f" : "#ad3a76", width: roadIdFromLane(feature.get("from_lane_id")) === selectedRoad?.id ? 4 : 2, lineDash: [7, 5] }) }) : null }),
|
connectors: new VectorLayer({ source: source(), style: (feature) => effectiveConnectorEnabled(feature.getProperties()) ? new Style({ stroke: new Stroke({ color: roadIdFromLane(feature.get("from_lane_id")) === selectedRoad?.id ? "#d1226f" : "#ad3a76", width: roadIdFromLane(feature.get("from_lane_id")) === selectedRoad?.id ? 4 : 2, lineDash: [7, 5] }) }) : null }),
|
||||||
@@ -73,8 +76,8 @@ const layers = {
|
|||||||
selectedRoad: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#00a5cf", width: 8 }) }), zIndex: 10 }),
|
selectedRoad: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#00a5cf", width: 8 }) }), zIndex: 10 }),
|
||||||
selectedMovement: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#f0b323", width: 6 }) }), zIndex: 12 }),
|
selectedMovement: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#f0b323", width: 6 }) }), zIndex: 12 }),
|
||||||
};
|
};
|
||||||
const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.sidewalks, layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.controls, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) });
|
const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.sidewalks, layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) });
|
||||||
const select = new Select({ condition: click, layers: (layer) => manualFromEndpoint ? layer === layers.osm : [layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.controls, layers.connectors, layers.native, layers.diagnostics].includes(layer), hitTolerance: 8, style: new Style({ stroke: new Stroke({ color: "#005e89", width: 5 }), fill: new Fill({ color: "rgba(0, 94, 137, .18)" }) }) });
|
const select = new Select({ condition: click, layers: (layer) => manualFromEndpoint ? layer === layers.osm : [layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.connectors, layers.native, layers.diagnostics].includes(layer), hitTolerance: 8, style: new Style({ stroke: new Stroke({ color: "#005e89", width: 5 }), fill: new Fill({ color: "rgba(0, 94, 137, .18)" }) }) });
|
||||||
map.addInteraction(select);
|
map.addInteraction(select);
|
||||||
select.on("select", ({ selected }) => {
|
select.on("select", ({ selected }) => {
|
||||||
const feature = selected[0];
|
const feature = selected[0];
|
||||||
@@ -87,15 +90,17 @@ select.on("select", ({ selected }) => {
|
|||||||
const road = roadForFeature(feature);
|
const road = roadForFeature(feature);
|
||||||
const directionArrow = provenance === "native-road-direction-arrow/v1";
|
const directionArrow = provenance === "native-road-direction-arrow/v1";
|
||||||
const turnArrow = provenance === "native-road-turn-arrow/v1";
|
const turnArrow = provenance === "native-road-turn-arrow/v1";
|
||||||
|
const centerLine = provenance === "native-road-center-line/v1";
|
||||||
const crosswalk = provenance === "native-road-crosswalk/v1";
|
const crosswalk = provenance === "native-road-crosswalk/v1";
|
||||||
const stopLine = provenance === "native-road-stop-line/v1";
|
const stopLine = provenance === "native-road-stop-line/v1";
|
||||||
const markingType = crosswalk ? "斑马线" : stopLine ? "停止线" : directionArrow ? "道路方向箭头" : turnArrow ? "路口转向箭头" : "车道分隔线";
|
const markingType = centerLine ? "道路中心虚线" : crosswalk ? "斑马线" : stopLine ? "停止线" : directionArrow ? "道路方向箭头" : turnArrow ? "路口转向箭头" : "车道分隔线";
|
||||||
selectRoad(road);
|
selectRoad(road);
|
||||||
evidence.textContent = JSON.stringify({
|
evidence.textContent = JSON.stringify({
|
||||||
标线类型: markingType,
|
标线类型: markingType,
|
||||||
人行横道节点: crosswalk || stopLine ? feature.get("crossing_node_id") : null,
|
人行横道节点: crosswalk || stopLine ? feature.get("crossing_node_id") : null,
|
||||||
OSM道路: feature.get("osm_way_ids"),
|
OSM道路: feature.get("osm_way_ids"),
|
||||||
原生道路: feature.get("road_id"),
|
原生道路: feature.get("road_id"),
|
||||||
|
道路段: centerLine ? feature.get("segment_id") : null,
|
||||||
方向: feature.get("direction"),
|
方向: feature.get("direction"),
|
||||||
车道: feature.get("lane_id") || feature.get("lane_index") || `${feature.get("left_lane_index")} 与 ${feature.get("right_lane_index")} 之间`,
|
车道: feature.get("lane_id") || feature.get("lane_index") || `${feature.get("left_lane_index")} 与 ${feature.get("right_lane_index")} 之间`,
|
||||||
转向: turnArrow ? feature.get("maneuver") : null,
|
转向: turnArrow ? feature.get("maneuver") : null,
|
||||||
@@ -125,6 +130,7 @@ function laneIndex(laneId) { return Number(String(laneId).split(":").at(-1)); }
|
|||||||
function lanePositionLabel(road, index) { return road?.laneCount === 1 ? "唯一车道" : `左起第 ${index} 车道`; }
|
function lanePositionLabel(road, index) { return road?.laneCount === 1 ? "唯一车道" : `左起第 ${index} 车道`; }
|
||||||
function laneStyle(feature) { const selected = feature.get("road_id") === selectedRoad?.id; return new Style({ stroke: new Stroke({ color: selected ? "#006e91" : "#f5f6ee", width: selected ? 3 : 1.3, lineDash: [5, 4] }) }); }
|
function laneStyle(feature) { const selected = feature.get("road_id") === selectedRoad?.id; return new Style({ stroke: new Stroke({ color: selected ? "#006e91" : "#f5f6ee", width: selected ? 3 : 1.3, lineDash: [5, 4] }) }); }
|
||||||
function markingStyle() { return new Style({ fill: new Fill({ color: "#f5f6ee" }), stroke: new Stroke({ color: "#d9dacf", width: 1 }) }); }
|
function markingStyle() { return new Style({ fill: new Fill({ color: "#f5f6ee" }), stroke: new Stroke({ color: "#d9dacf", width: 1 }) }); }
|
||||||
|
function centerLineStyle() { return new Style({ fill: new Fill({ color: "#f5be2a" }), stroke: new Stroke({ color: "#d29d16", width: .8 }) }); }
|
||||||
function nativeSurfaceStyle(feature) {
|
function nativeSurfaceStyle(feature) {
|
||||||
// Split road features meet at OSM junction nodes. Their per-feature outlines
|
// Split road features meet at OSM junction nodes. Their per-feature outlines
|
||||||
// are editing aids, not physical seams, so scene mode must render fills only.
|
// are editing aids, not physical seams, so scene mode must render fills only.
|
||||||
@@ -159,6 +165,7 @@ function updateSources() {
|
|||||||
layers.lanes.getSource().clear(); layers.lanes.getSource().addFeatures(readFeatures(state.layers.laneCenterlines));
|
layers.lanes.getSource().clear(); layers.lanes.getSource().addFeatures(readFeatures(state.layers.laneCenterlines));
|
||||||
layers.directionArrows.getSource().clear(); layers.directionArrows.getSource().addFeatures(readFeatures(state.layers.directionArrows));
|
layers.directionArrows.getSource().clear(); layers.directionArrows.getSource().addFeatures(readFeatures(state.layers.directionArrows));
|
||||||
layers.markings.getSource().clear(); layers.markings.getSource().addFeatures([...readFeatures(state.layers.laneSeparators), ...readFeatures(state.layers.turnArrows)]);
|
layers.markings.getSource().clear(); layers.markings.getSource().addFeatures([...readFeatures(state.layers.laneSeparators), ...readFeatures(state.layers.turnArrows)]);
|
||||||
|
layers.centerLines.getSource().clear(); layers.centerLines.getSource().addFeatures(readFeatures(state.layers.centerLines));
|
||||||
layers.controls.getSource().clear(); layers.controls.getSource().addFeatures([...readFeatures(state.layers.crosswalks), ...readFeatures(state.layers.vehicleStopLines)]);
|
layers.controls.getSource().clear(); layers.controls.getSource().addFeatures([...readFeatures(state.layers.crosswalks), ...readFeatures(state.layers.vehicleStopLines)]);
|
||||||
layers.connectors.getSource().clear(); layers.connectors.getSource().addFeatures(readFeatures(state.layers.connectors));
|
layers.connectors.getSource().clear(); layers.connectors.getSource().addFeatures(readFeatures(state.layers.connectors));
|
||||||
layers.diagnostics.getSource().clear(); layers.diagnostics.getSource().addFeatures(readFeatures({ type: "FeatureCollection", features: state.compiled.diagnostics.filter((item) => item.geometry).map(({ geometry, ...properties }) => ({ type: "Feature", properties, geometry })) }));
|
layers.diagnostics.getSource().clear(); layers.diagnostics.getSource().addFeatures(readFeatures({ type: "FeatureCollection", features: state.compiled.diagnostics.filter((item) => item.geometry).map(({ geometry, ...properties }) => ({ type: "Feature", properties, geometry })) }));
|
||||||
@@ -219,6 +226,7 @@ function renderSummary() {
|
|||||||
["普通构面路口", comparison.nativeApproachEnvelopeJunctions],
|
["普通构面路口", comparison.nativeApproachEnvelopeJunctions],
|
||||||
["兜底构面路口", comparison.nativeFallbackJunctions],
|
["兜底构面路口", comparison.nativeFallbackJunctions],
|
||||||
["最大外缘扩张", comparison.nativeMaxJunctionExpansionRatio],
|
["最大外缘扩张", comparison.nativeMaxJunctionExpansionRatio],
|
||||||
|
["道路中心虚线", comparison.nativeCenterLineFeatures],
|
||||||
["道路方向箭头", comparison.nativeDirectionArrowFeatures],
|
["道路方向箭头", comparison.nativeDirectionArrowFeatures],
|
||||||
["路口转向箭头", comparison.nativeTurnArrowFeatures],
|
["路口转向箭头", comparison.nativeTurnArrowFeatures],
|
||||||
["斑马线条带", comparison.nativeCrosswalkFeatures],
|
["斑马线条带", comparison.nativeCrosswalkFeatures],
|
||||||
@@ -250,6 +258,7 @@ scenePreviewToggle.onchange = () => {
|
|||||||
layers.osmDirection.setVisible(!scenePreview && document.querySelector('[data-layer="osm"]').checked);
|
layers.osmDirection.setVisible(!scenePreview && document.querySelector('[data-layer="osm"]').checked);
|
||||||
layers.connectors.setVisible(!scenePreview && document.querySelector('[data-layer="lanes"]').checked);
|
layers.connectors.setVisible(!scenePreview && document.querySelector('[data-layer="lanes"]').checked);
|
||||||
layers.sidewalks.setVisible(document.querySelector('[data-layer="sidewalks"]').checked);
|
layers.sidewalks.setVisible(document.querySelector('[data-layer="sidewalks"]').checked);
|
||||||
|
layers.centerLines.setVisible(document.querySelector('[data-layer="centerLines"]').checked);
|
||||||
layers.controls.setVisible(document.querySelector('[data-layer="controls"]').checked);
|
layers.controls.setVisible(document.querySelector('[data-layer="controls"]').checked);
|
||||||
layers.diagnostics.setVisible(!scenePreview);
|
layers.diagnostics.setVisible(!scenePreview);
|
||||||
layers.native.changed(); layers.sidewalks.changed();
|
layers.native.changed(); layers.sidewalks.changed();
|
||||||
|
|||||||
Reference in New Issue
Block a user