feat: add OSM turn lane arrows

This commit is contained in:
2026-08-05 10:17:34 +08:00
parent 9970e3eeef
commit 9a23f74f0f
34 changed files with 1540 additions and 33 deletions

View File

@@ -51,6 +51,7 @@ cp config/examples/template.json config/areas/my-area.json
| `blenderApp` | | `/Applications/Blender.app` | |
| `stages` | | 见下 | 各阶段默认开关 |
| `qgis` | | 见下 | QGIS/osm2streets 旋钮 |
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 |
| `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 |
@@ -97,6 +98,15 @@ cp config/examples/template.json config/areas/my-area.json
> 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。
> 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。
### `turnLaneArrows`
| 字段 | 默认 | 说明 |
|---|---|---|
| `enabled` | `false` | 仅在样张经用户确认后启用。启用时从 `turn:lanes:forward` / `turn:lanes:backward` 追加经过测试的自定义箭头;未测试素材永不参与映射。 |
该开关经 `normalizeAreaConfig()``writeDerivedConfig()` 传入 intermediates 阶段。必须用
`??` 保留 `false`;不要将它改为按隐式标签或环境变量自动启用。
### `osm2streets`
原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认:

View File

@@ -83,6 +83,18 @@ cesium-preview.js 浏览器
## 本项目真实踩过的坑
### 坑 0用原始 OSM 节点度数代替归一化路网拓扑
OSM way 的端点不一定在原始 XML 中有三个以上相连 wayosm2streets 可能把相邻 way
合并、切分或通过 `network.intersections[*].osm_ids` 表达路口。任何需要判断道路是否
进入路口的中间层逻辑,都必须优先使用已经生成的 normalized `network.json` 事实源,
原始节点度数只能作为没有 normalized network 的纯单元测试回退。
### 坑 0.1:普通 JSON 误走 FeatureCollection 写入器
`writeJson()` 的隐式契约是传入带 `features` 数组的图层集合;诊断 manifest、计数摘要等
普通对象必须用显式 `JSON.stringify` 写入,不能为了复用日志代码把它们塞进图层写入器。
### 坑 1同一份事实存了四份
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。

View File

@@ -189,6 +189,158 @@ const osm = parseOsm(fs.readFileSync(area.input, "utf8"));
const preflight = analyzeOsmPreflight(osm);
```
## OSM 转向车道箭头
### 1. 范围与触发条件
区域配置设为 `turnLaneArrows.enabled: true` 时,`intermediates` 阶段会将已支持的
OSM `turn:lanes:forward` / `turn:lanes:backward` 标线追加到既有的
`lane_arrows_webscale` 图层。这只是既有图层的新增数据来源,不能新增 QGIS、Blender
或 Cesium 图层。
### 2. 调用形式
```json
{
"turnLaneArrows": { "enabled": true }
}
```
该字段由 `normalizeAreaConfig()` 归一化、`writeDerivedConfig()` 写入派生配置,
并由 `build-osm2streets-qgis.js` 消费。
### 3. 契约
- 已支持且已测试的转向为:`through``left``right``through;left`
`through;right``through;left;right`
- `assets/lane-icons/manifest.json` 是上游 CC0 来源、原始 SVG、镜像规则、箭杆轴线、
支持状态和测试状态的唯一事实源。未同时标记为 `supported``tested` 的素材不得写入
生产 GeoJSON。
- 自定义要素必须带有 `source="osm_turn_lanes"``osm_way_id``direction`
`lane_index``maneuver``source_asset``arrow_part` 与确定性的
`custom_arrow_id`
- `turn_lane_arrow_diagnostics.json` 记录 `generated` 和结构化跳过原因;它是普通 JSON
不是 FeatureCollection。
- 归一化后的 `network.intersections[*].osm_ids` 用来判断有效路口端点。只有单元测试中
没有 network 时,才可退回原始 OSM 节点度数。
- 可直接关联时,`Driving` 面通过 `osm_way_ids` 匹配,中心线取其相对两边的中点;闭合
四边形车道面也必须有效。JOSM 拆分产生的临时 ID 未被渲染面保留时,只能匹配距离近且
行驶方向一致的车道中心线,并按实际横向位置排序,不得单独依赖渲染器的 `index`
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 功能关闭 | 不生成自定义要素;诊断原因为 `disabled` |
| 缺少车道数或无法判定路口端点 | 带原因跳过,不能猜测位置 |
| 转向未支持或未测试 | 以 `unsupported_or_untested_maneuver` 跳过 |
| SVG 缺少受支持路径命令或 manifest 锚点 | 写入 GeoJSON 前抛错 |
| 合法且已支持的 OSM 车道 | 每个 SVG 填充或扩展描边部件追加一个 Polygon |
### 5. 正常、基础与错误示例
- 正常:`through;right` 生成多个合法 Polygon 部件,但共享同一 OSM 溯源元组,直行杆
轴线与车道中心对齐。
- 基础:`enabled: false` 保持 osm2streets 原有箭头输出不变。
- 错误:将组合箭头写为一个 `MultiPolygon`。既有 QGIS 归一化器只接受逐个 Polygon。
### 6. 必需测试
- `npm run test:turn-lane-arrows`:覆盖转向归一化、正反向放置、确定性 ID、箭杆锚点、
关闭行为及未测试素材排除。
- 目标区域 `intermediates` 构建:确认 `lane_arrows_webscale.geojson` 含自定义 `source`
要素,且诊断可读。
- 用户确认样张后,运行完整目标区域构建和
`npm run check:area -- --config <area>`
### 7. 错误与正确写法
错误:
```js
if ((roadCounts.get(endpoint.id) || 0) < 3) return null;
writeJson(diagnosticsPath, diagnostics);
```
正确:
```js
const networkSaysIntersection = networkIntersectionNodes.has(endpoint.id);
if (!networkSaysIntersection && (roadCounts.get(endpoint.id) || 0) < 3) return null;
fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`);
```
## 斑马线与停止线来源
### 1. 范围与触发条件
`intermediates` 阶段从带标记的 OSM `highway=crossing` 节点生成路口标线,并独占
`crosswalks``vehicle_stop_lines` 两个场景图层的数据来源。
### 2. 调用形式
```js
const crosswalkData = buildCrosswalks(osm, lanePolygons.features);
// split.vehicleStopLines === crosswalkData.stopLines
// split.crosswalks === crosswalkData.stripes
```
### 3. 契约
- `buildCrosswalks()` 是输出 `vehicle_stop_lines` 的唯一生产者。不得将 osm2streets
`lane_markings` 中类型为 `vehicle stop line` 的要素追加回来,否则会产生位置不同的
重复标线。
- 每个输出的过街进口固定生成 6 条 `crosswalk stripe`,以及恰好 1 条带有
`source="crosswalk"``crossing_node_id``vehicle stop line`
- 斑马线参考点先沿进口方向放在过街节点簇中心外 7 米处;再以相邻归一化 `Driving`
修正横向中心和方向:取局部车道中心锚点的平均值,并使用方向一致的车道切线。只有没有
匹配的渲染车道时,才回退到原始 OSM way 几何。
- 停止线从同一校正后的斑马线坐标系,以外侧固定 1.2 米偏移推导。osm2streets 的道路
横断面与原始 OSM 几何不同,斑马线与停止线也必须同步移动。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 有匹配渲染车道的标记过街 | 中心与方向跟随实际渲染道路横断面 |
| 没有匹配渲染车道的标记过街 | 安全回退到原始 OSM 方向向量 |
| 多个源节点投影到同一进口中心 | 写入斑马线和停止线前去重 |
| 存在 osm2streets 原生停止线 | 忽略,不写入输出图层 |
### 5. 正常、基础与错误示例
- 正常:两个四向路口产生 48 条斑马线条带和 8 条停止线,全部为
`source="crosswalk"`
- 基础:归一化 `Driving` 面之外的过街仍可按原始 way 方向渲染。
- 错误:因为原生 `vehicle stop line` 与人工线相距数米就保留它;这会重新引入第二个
生产者和视觉重复。
### 6. 必需测试
- 运行目标区域 `intermediates` 构建。
- 断言 `vehicle_stop_lines.geojson` 每个要素的 `crossing_node_id` 均不重复,且所有要素
都有 `source="crosswalk"`
- 检查四向路口的 QGIS 预览:条带必须横向居中于渲染道路宽度,每条停止线必须位于其
对应斑马线之后。
### 7. 错误与正确写法
错误:
```js
if (feature.properties.type === "vehicle stop line") {
out.vehicleStopLines.features.push(feature);
}
```
正确:
```js
const crosswalkData = buildCrosswalks(osm, lanePolygons.features);
out.vehicleStopLines = crosswalkData.stopLines;
// 原生 lane_markings 停止线不得复制到输出。
```
## 区域诊断命令
### 1. Scope / Trigger