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

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the expected intermediates artifacts and dependency boundaries."}
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"Requires preserving existing lane-arrow layer collection and order across QGIS and Blender."}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines quality-gate and target-area validation expectations."}
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Defines the source provenance and assertion that untested assets cannot be mapped."}

View File

@@ -0,0 +1,74 @@
# Design: OSM turn-lane arrows
## Boundaries
- `scripts/build-osm2streets-qgis.js` remains the intermediates owner. It will
append generated custom features to the existing in-memory `laneArrows`
collection before it is written as `lane_arrows_webscale.geojson`.
- No render layer, QGIS symbol, Blender material, or Cesium export contract
changes. Existing `lane_arrows_webscale` styling and catalog ownership apply
to generated features automatically.
- Vendored upstream artwork lives under a dedicated repository-local asset
directory with its CC0 license and a machine-readable manifest. The manifest
is the source of truth for upstream path, source revision, local path,
support status, and test status.
## Asset Contract
- Import every arrow SVG from `openstreetmap/lane-icons/docs/` that represents
an arrow. Preserve the source bytes and retain `LICENSE.md` with the assets.
- Mark only `through`, `left`, `right`, `through;left`, and `through;right` as
`supported` and `tested` in the manifest. All remaining imported arrows are
`unsupported` and `untested`; no production parser may select them.
- The upstream repository has left-side examples only. Derive `right` and
`through;right` through a deterministic horizontal mirror of the matching
left-side geometry, recording `derived_from` in the manifest.
## Data Flow
1. Parse OSM ways once using the existing OSM parser.
2. Read `turn:lanes:forward` / `turn:lanes:backward`, split lane declarations
by `|`, and map only the five supported normalized maneuvers to manifest
entries.
3. For each valid lane near a determinable intersection endpoint, calculate a
stable lane center and road-local axis; transform the selected local arrow
polygon into WGS84 coordinates.
4. Add a GeoJSON feature with existing render-layer behavior and explicit
provenance: custom source, OSM way id, direction, lane index, maneuver,
source asset, and deterministic feature id/order.
5. Record a structured skipped item when tags, lane geometry, or endpoint
classification are insufficient. Do not invent a fallback location.
6. Existing normalization, scene merge, GeoPackage import, Blender assembly,
and Cesium export consume the expanded collection unchanged.
## Geometry and Compatibility
- Parse only the vendored SVG subset used by supported assets at build time:
`line` elements and `path` commands are converted into local meter-scale
polygons, with cubic curves sampled and SVG strokes expanded into outlines.
The source SVG is the sole geometry authority; no hand-drawn replacement
template is permitted. This stays dependency-free because the supported
upstream syntax is intentionally small and covered by Node tests.
- Use the upstream Standard Arrow shape as the geometry basis, but let the
existing QGIS fill and outline preserve the project's visual language.
- Preserve the existing osm2streets-generated arrow features. Custom features
are additive and have provenance that distinguishes them from upstream
osm2streets output.
- Deterministic ordering is by way id, travel direction, and lane index.
- Each asset manifest entry defines the source SVG's shaft-axis `anchor_x`.
Placement aligns that axis, rather than the SVG viewBox center, with the
lane centerline. Mirrored right-side variants mirror around the same axis.
- Upstream SVG units are converted at `0.10 m` per unit. This calibrates their
local extent against the existing approximately 1.4 m osm2streets arrows;
do not use SVG display-pixel scale as road-marking scale.
## Risks and Rollback
- OSM lane counts, one-way semantics, or endpoint topology can be incomplete.
These cases remain skipped with diagnostics rather than creating misleading
markings.
- Asset import is self-contained and CC0. Rollback removes the custom feature
generation and local asset directory; the existing arrow layer remains
unchanged.
- User approval of the two initial visual samples is required before the
feature is enabled in production output.

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the intermediates data flow, CommonJS conventions, and existing layer ownership."}
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"The feature adds a source to an existing layer and must preserve the cross-language layer contract."}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines stage ownership and target-area validation commands."}
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Records the selected upstream SVG source, CC0 provenance, and supported-versus-untested asset policy."}

View File

@@ -0,0 +1,39 @@
# Implementation Plan: OSM turn-lane arrows
## Steps
1. Add the complete lane-icons arrow asset set, upstream revision metadata,
CC0 license text, and a manifest with the five supported/tested entries and
explicit untested status for all other entries.
2. Add a small CommonJS helper for asset-manifest loading, source-SVG path and
stroke conversion, maneuver parsing, road-local placement, and diagnostic
skip records. Keep it dependency-free and separately testable.
3. Wire the helper into the intermediates build so custom features are merged
into `laneArrows` before the existing GeoJSON write and normalization path.
4. Add Node tests for the five supported maneuvers, forward/backward endpoint
choice, lane index placement, deterministic order/id, and every required
skip reason. Assert that untested manifest entries cannot be selected.
5. Generate `right` and `through;right` inspection samples from the exact
local templates and request user confirmation. Do not enable custom output
until this gate is met.
6. Run the focused tests, syntax checks, the Nantaizi intermediates build, the
full Nantaizi chain, and `check:area`. Compare against a turn-tag-removed
control to prove an observable GeoJSON difference.
## Validation
- `node --check` for changed CommonJS scripts.
- New pure Node test script(s), invoked directly or through a package script.
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
- Full target-area pipeline after visual approval.
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
## Review Gates
- No generated custom feature may use an asset whose manifest status is not
both supported and tested.
- Samples must show the same fill/outline treatment as the existing lane-arrow
layer.
- Keep the existing layer registry unchanged; this is a new source for an
existing layer, not a new layer.
- Do not commit without the user's explicit confirmation.

View File

@@ -0,0 +1,63 @@
# Add OSM turn lane arrows
## Goal
让 OSM `turn:lanes:forward` / `turn:lanes:backward` 在现有 QGIS -> Blender -> Cesium
道路链中生成可辨识的 lane arrow同时保持与 osm2streets 既有道路标线相同的视觉语言。
## Confirmed Facts
- 当前 `osm2streets-js-node@0.1.4` 生成 lane arrow但纯内存移除 way `98607123`
`98607128``98607147``turn:lanes:*`593 个原始箭头的 JSON 内容逐字一致;
因此该依赖不消费这些 OSM 标签。
- 当前南台子完整链已经通过:中间层落盘 580 个归一化箭头Blender 装配 397 个,
Cesium / preview / compress manifest 均 fresh`check:area` PASS。
- 现有 `lane_arrows_webscale` 已是 QGIS、Blender、Cesium 共用图层,样式为浅色填充
`#fffff6`、深色描边 `#2b2b28`,正常外轮廓为七顶点箭头。
- `turn:lanes:forward=right``turn:lanes:backward=through;right` 等标准标签已存在于
南台子 OSM。
- `openstreetmap/lane-icons` 是 OSM 官方组织发布的 CC0 SVG 图标集;其 `DESIGN.md`
以 MUTCD Standard Arrow 为基准,并规定组合转向共享箭杆、箭头数量增加时缩小箭头。
仓库只提供左侧转向示例,右转及 `through;right` 需在几何层对其镜像,不能直接作为
当前 GeoJSON polygon 图层输入。
- 用户已决定将该库中首版需要的转向图标作为本仓库维护的源素材,而不是只作为视觉参考。
## Requirements
1. 在写入 `lane_arrows_webscale.geojson` 前,从 OSM 标准 `turn:lanes:*` 生成自定义箭头
polygon并沿用既有图层、材质、QGIS/Blender/Cesium 路径。
2. 支持首版实际需要的 `through``left``right``through;left``through;right`
同一 OSM 输入必须产生确定性结果。
3. 自定义箭头必须使用现有道路标线的视觉语言,不新建一套颜色、描边或图标风格。
4. 在 OSM way 进入路口的一端、按 forward/backward 和 lane index 放置;车道配置不完整或
端点不是可判定路口时给出可诊断的跳过原因。
5. 首先完成并获得用户对 `right``through;right` 轮廓样张的确认,之后才能将其接入
GeoJSON / Blender 导出。
6. 未经用户确认,不提交 Git。
7.`openstreetmap/lane-icons` 的全部箭头 SVG 和 CC0 许可证说明纳入本仓库维护;
右转变体由左转源图镜像生成。生成的 polygon 仍由本仓库维护,并继续使用
`lane_arrows_webscale` 的既有填充与描边样式。
8. 素材清单必须逐项标记验证状态:仅 `through``left``right``through;left`
`through;right` 在首版具备 OSM 映射与自动化测试;其他已收录箭头明确标为未测试,
且不得自动参与生产 GeoJSON 输出。
## Acceptance Criteria
- [ ] 用户确认箭头样张的轮廓与视觉语言。
- [ ] 目标 OSM 标签导致 lane-arrow GeoJSON 与移除这些标签的对照产生可观察差异。
- [ ] 自定义 arrow properties 标识来源和 OSM way / direction / lane便于诊断。
- [ ] 纯 Node 测试覆盖标签解析、forward/backward、lane placement、组合转向和幂等排序。
- [ ] 本地素材清单包含所有上游箭头,逐项标记 CC0 来源、支持状态和测试状态;未测试条目
不会参与生产映射。
- [ ] 仅南台子完整链和质量门验证通过。
- [ ] 无 Git 提交,直到用户明确确认。
## Out Of Scope
- 修改 osm2streets 依赖、道路改为 Cesium 直渲染、增加新的道路渲染图层。
- 支持首版范围之外的 OSM maneuver 词汇、匝道标线、文字或自行车专用箭头。
## Delivery Gate
在自定义箭头接入生产 GeoJSON / Blender 导出前,用户必须确认 `right`
`through;right` 样张的轮廓和视觉语言。

View File

@@ -0,0 +1,40 @@
# lane-icons research
Source: <https://github.com/openstreetmap/lane-icons>, inspected 2026-08-04.
## Provenance
- Owner: `openstreetmap` GitHub organization.
- Repository tree revision: `db3cab321da93a757c2cf3cdb4f1f1db6b1a091c` (`master`).
- License: `LICENSE.md` is CC0 1.0 Universal. Keep that text adjacent to any
vendored source assets.
## Relevant Contents
- `docs/through.svg`
- `docs/left.svg`
- `docs/left-through.svg`
- `docs/slight_left.svg`
- `docs/slight_left-through.svg`
- `docs/sharp_left.svg`
- `docs/sharp_left-through.svg`
- `docs/reverse_left.svg`
- `docs/reverse_left-through.svg`
- `docs/reverse_left-left-slight_left-through.svg`
- `docs/left-slight_left-through.svg`
## Design Rules
`DESIGN.md` bases arrows on the MUTCD Standard Arrow. It specifies a shared
shaft for compound arrows, smooth curves, aligned arrow bottoms, and reduced
arrowhead scale as the number of directions grows. The upstream SVGs are
single-color and stroke-free; our existing render layer supplies the project
fill and outline treatment.
## Adoption Decision
Vendor all listed arrow SVGs with provenance and CC0 license text. The
production mapping is initially restricted to `through`, `left`, `right`,
`through;left`, and `through;right`. Right-side variants are deterministic
mirrors of upstream left-side assets. Every other vendored arrow remains
explicitly untested and unavailable to the production mapper.

View File

@@ -0,0 +1,26 @@
{
"id": "add-osm-turn-lane-arrows",
"name": "add-osm-turn-lane-arrows",
"title": "Add OSM turn lane arrows",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}