From 5cde08090e91d915e3057d92f22568d3fb9fe516 Mon Sep 17 00:00:00 2001 From: que01 Date: Wed, 26 Aug 2026 09:23:46 +0800 Subject: [PATCH] refactor: consume external road compiler --- .trellis/spec/pipeline/index.md | 1 + .trellis/spec/pipeline/native-road-package.md | 9 + .../tasks/08-25-rc-p2-repo-split/check.jsonl | 5 +- .../tasks/08-25-rc-p2-repo-split/design.md | 44 + .../08-25-rc-p2-repo-split/implement.jsonl | 5 +- .../tasks/08-25-rc-p2-repo-split/implement.md | 99 +- .trellis/tasks/08-25-rc-p2-repo-split/prd.md | 98 +- .../tasks/08-25-rc-p2-repo-split/task.json | 2 +- package-lock.json | 11 + package.json | 5 +- packages/road-compiler/package.json | 10 - packages/road-compiler/src/check.js | 27 - .../road-compiler/src/compile/compiler.js | 141 - .../src/compile/complex-junction.js | 474 - .../road-compiler/src/compile/native-road.js | 1695 --- .../src/compile/turn-lane-arrows.js | 502 - .../src/geometry/lane-geometry.js | 161 - packages/road-compiler/src/index.js | 14 - .../src/native-traffic-signals.js | 49 - packages/road-compiler/src/osm.js | 102 - packages/road-compiler/src/reference/gaode.js | 231 - packages/road-compiler/src/traffic-signals.js | 330 - .../test/fixtures/fengshu-er-road.osm | 11091 ---------------- .../fengshu-junction-reference.geojson | 1 - packages/road-compiler/test/index.js | 19 - .../road-compiler/workbench/client/app.css | 1 - .../road-compiler/workbench/client/app.js | 459 - .../road-compiler/workbench/client/index.html | 4 - packages/road-compiler/workbench/server.js | 152 - scripts/build-area.js | 4 +- scripts/build-osm2streets-qgis.js | 2 +- scripts/check-native-roads.js | 4 +- scripts/compile-native-roads.js | 10 +- scripts/inspect-junction-reference.js | 2 +- scripts/lib/road-compiler-cli.js | 47 + scripts/lib/traffic-signals.js | 3 +- scripts/lib/vehicle-route.js | 5 +- scripts/reimport-gpkg.js | 2 +- scripts/render-turn-lane-arrow-samples.js | 2 +- scripts/road-workbench.js | 2 +- scripts/test-gaode-junction-reference.js | 2 +- scripts/test-native-road.js | 24 +- scripts/test-preview-assets.js | 3 +- scripts/test-road-compiler-cli.js | 16 + scripts/test-road-workbench.js | 5 +- scripts/test-traffic-signals.js | 4 +- scripts/test-turn-lane-arrows.js | 2 +- scripts/test-v2x-cesium-overlay.js | 2 +- 48 files changed, 257 insertions(+), 15626 deletions(-) create mode 100644 .trellis/spec/pipeline/native-road-package.md create mode 100644 .trellis/tasks/08-25-rc-p2-repo-split/design.md delete mode 100644 packages/road-compiler/package.json delete mode 100644 packages/road-compiler/src/check.js delete mode 100644 packages/road-compiler/src/compile/compiler.js delete mode 100644 packages/road-compiler/src/compile/complex-junction.js delete mode 100644 packages/road-compiler/src/compile/native-road.js delete mode 100644 packages/road-compiler/src/compile/turn-lane-arrows.js delete mode 100644 packages/road-compiler/src/geometry/lane-geometry.js delete mode 100644 packages/road-compiler/src/index.js delete mode 100644 packages/road-compiler/src/native-traffic-signals.js delete mode 100644 packages/road-compiler/src/osm.js delete mode 100644 packages/road-compiler/src/reference/gaode.js delete mode 100644 packages/road-compiler/src/traffic-signals.js delete mode 100644 packages/road-compiler/test/fixtures/fengshu-er-road.osm delete mode 100644 packages/road-compiler/test/fixtures/fengshu-junction-reference.geojson delete mode 100644 packages/road-compiler/test/index.js delete mode 100644 packages/road-compiler/workbench/client/app.css delete mode 100644 packages/road-compiler/workbench/client/app.js delete mode 100644 packages/road-compiler/workbench/client/index.html delete mode 100644 packages/road-compiler/workbench/server.js create mode 100644 scripts/lib/road-compiler-cli.js create mode 100644 scripts/test-road-compiler-cli.js diff --git a/.trellis/spec/pipeline/index.md b/.trellis/spec/pipeline/index.md index c902cf3..fd56190 100644 --- a/.trellis/spec/pipeline/index.md +++ b/.trellis/spec/pipeline/index.md @@ -18,6 +18,7 @@ | 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 | | 改预览页生成 | [../preview/](../preview/index.md) | | 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) | +| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) | --- diff --git a/.trellis/spec/pipeline/native-road-package.md b/.trellis/spec/pipeline/native-road-package.md new file mode 100644 index 0000000..3d90d5a --- /dev/null +++ b/.trellis/spec/pipeline/native-road-package.md @@ -0,0 +1,9 @@ +# Native Road Compiler Contract + +The native-road package is maintained in the private `road-compiler` repository: + +`https://git.app.que01.top/que01/road-compiler` + +This host consumes the exact git tag recorded in `package.json` and lockfile. The host owns area configuration normalization and writes `RoadCompilerInput`; the compiler owns the `native-road-package/v1` file contract and CLI. Production code must invoke the installed compiler CLI through `scripts/lib/road-compiler-cli.js`, not import compiler source files or read `packages/road-compiler`. + +The completion marker is `NATIVE_ROAD_COMPILE_DONE `. `build-area.js` validates the marker's count, JSON payload, area id, output path, process status, and signal before starting Blender. diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/check.jsonl b/.trellis/tasks/08-25-rc-p2-repo-split/check.jsonl index 9dd3234..b29938c 100644 --- a/.trellis/tasks/08-25-rc-p2-repo-split/check.jsonl +++ b/.trellis/tasks/08-25-rc-p2-repo-split/check.jsonl @@ -1 +1,4 @@ -{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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."} +{"file":".trellis/spec/pipeline/external-tools.md","reason":"检查 CLI 子进程错误、signal 与日志处理是否符合宿主管线约定。"} +{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"检查编译阶段、完成标记和 stage manifest 的跨层影响。"} +{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"检查配置、Node pipeline、Blender 消费链的边界没有漂移。"} +{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"检查两个有效区域的严格基线 parity。"} diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/design.md b/.trellis/tasks/08-25-rc-p2-repo-split/design.md new file mode 100644 index 0000000..5b52d5d --- /dev/null +++ b/.trellis/tasks/08-25-rc-p2-repo-split/design.md @@ -0,0 +1,44 @@ +# Phase 2:独立仓库与 CLI 边界设计 + +## Architecture + +P1 已将实现收敛到 `packages/road-compiler/`。P2 把该目录以保留历史的方式导出为独立 package 仓库,保留两个明确方向的边界: + +``` +宿主 area config + -> scripts/lib/area-config.js + -> RoadCompilerInput JSON(宿主 staging) + -> @osm-asset/road-compiler CLI(子进程) + -> native-road/ + NATIVE_ROAD_COMPILE_DONE + -> build-area.js / Blender / preview +``` + +编译器不知道区域配置、宿主仓库、Blender 或宿主输出目录约定以外的输入路径。宿主不知道 compiler 内部源文件;它只拥有输入 JSON、CLI 的完成标记和写出的契约文件。 + +## CLI Contract + +- CLI 接收一个 JSON 文件中的 `RoadCompilerInput`,而不是宿主的 area config。 +- `comparisonDir` 继续作为可选输入,以维持现有 `comparison.json` 产物和 P0 baseline 的可比性。 +- 成功只打印一次 `NATIVE_ROAD_COMPILE_DONE `。payload 必须至少含 `areaId`、`output`、`roads`、`endpoints` 和 `diagnostics`。 +- `build-area.js` 保持同步批处理模式,子进程日志对用户可见;为了验证标记,输出同时被捕获或 tee。它对启动失败、非零状态、signal、缺失/重复/非法标记和 payload 与当前输入不一致都报错。 +- 产物先由 compiler 的现有 staging/atomic promotion 逻辑写入目标 `native-road/`;宿主不接管这一写入过程。 + +## Repository Contents + +独立仓库拥有:compiler source、public API、CLI、check、workbench、fixture、两个 parity baseline、契约文档、README、package lockfile 和 workbench 的 `ol` 依赖。测试和 workbench 均必须在没有宿主 checkout 的环境运行。 + +宿主保留:area-config 读取/归一化、`RoadCompilerInput` 映射、pipeline 阶段编排、Blender/preview、区域配置、运行产物和一份升级回归 baseline。宿主 spec 仅保留指针,契约正文随 compiler 仓库维护。 + +## History Extraction + +导出只允许 `git subtree split` 或 `git filter-repo`。必须在独立仓库验证 `git log --follow src/compile/native-road.js`;验证的是迁移前的逻辑历史,而非仅 P1 的边界移动提交。导出和验证先在临时分支/clone 完成,外部远端创建和 push 必须使用用户给出的地址和授权范围。 + +## Dependency and Rollout + +初始发布源定为私有 `https://git.app.que01.top/que01/road-compiler.git`。仓库以带注释 tag `v0.1.0` 首发,宿主以该精确 tag 作为依赖并写入 lockfile。开发临时形态可以指向独立 checkout 的本地 `file:` 依赖;它不能成为验收时的唯一消费方式。两个区域 parity 通过后删除 `packages/road-compiler/`;回滚由 revert 宿主消费改造实现,不能让 production host 继续直接 import 该目录。 + +## Compatibility and Risks + +这是位置和调用方式迁移,不改变 compiler 行为。两个区域的 strict content/order hash 是主要 oracle;信号文档的确定性生成仍应通过 parity 覆盖。P2 不处理 `check:area` 的历史 stage-manifest freshness warning,也不把 Blender/Metal 沙箱问题归因为道路产物。 + +若外部 CLI 无法及时使用,回滚宿主消费 commit 即可;外部仓库历史和 tag 不需要删除。对于完整 Blender 构建,macOS Metal 初始化必须在提权的宿主环境运行。 diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/implement.jsonl b/.trellis/tasks/08-25-rc-p2-repo-split/implement.jsonl index 9dd3234..671ad79 100644 --- a/.trellis/tasks/08-25-rc-p2-repo-split/implement.jsonl +++ b/.trellis/tasks/08-25-rc-p2-repo-split/implement.jsonl @@ -1 +1,4 @@ -{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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."} +{"file":".trellis/spec/pipeline/external-tools.md","reason":"CLI 子进程必须沿用管线的启动、日志和失败状态语义。"} +{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"编译是 Blender 前的 pipeline 输入阶段,需保持阶段与 stdout 契约。"} +{"file":".trellis/spec/config/index.md","reason":"宿主拥有区域配置归一化及 RoadCompilerInput 映射。"} +{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"本阶段声称产物不变,必须用严格 parity 验证。"} diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/implement.md b/.trellis/tasks/08-25-rc-p2-repo-split/implement.md index 32ec4a0..30dfc9f 100644 --- a/.trellis/tasks/08-25-rc-p2-repo-split/implement.md +++ b/.trellis/tasks/08-25-rc-p2-repo-split/implement.md @@ -1,74 +1,55 @@ # Phase 2 执行计划 -## Step 1 — 前置 +执行前提:用户已在最新 P2 规划摘要后明确批准。发布目标已定为私有 `https://git.app.que01.top/que01/road-compiler.git`,首发 tag 为 `v0.1.0`。 -- [ ] Phase 1 的 AC1.1–AC1.8 全绿 -- [ ] 定档 §「待决策」四项(仓库名 / 托管 / 分发 / 过渡期策略) +## 1. 导出并核验历史 -## Step 2 — 拆出仓库(保历史) +- [ ] 创建临时 export 分支或临时 clone,使用 `git subtree split --prefix=packages/road-compiler` 或 `git filter-repo --path packages/road-compiler/ --path-rename packages/road-compiler/:` 导出。 +- [ ] 在导出仓库验证 `git log --follow src/compile/native-road.js` 包含 2026-08-13 以来的 native compiler 历史。 +- [ ] 在私有 `https://git.app.que01.top/que01/road-compiler.git` 创建并推送独立仓库;创建带注释的 `v0.1.0` tag。 + +## 2. 使编译器仓库自持 + +- [ ] 将 CLI 作为 package `bin` 入口,接受由宿主写出的 `RoadCompilerInput` JSON,并继续输出唯一的 `NATIVE_ROAD_COMPILE_DONE` JSON 标记。 +- [ ] 补全 `package.json`、锁文件和 `ol` 依赖;workbench vendor 文件从自身安装目录提供。 +- [ ] 迁入 `docs/native-road-package-v1.md`、两个 baseline 和它们的测试驱动,新增独立 README。 +- [ ] 在不含宿主文件的临时 clone 内执行依赖安装、单元测试、两区域 parity 与 workbench 启动测试。 +- [ ] grep 验证没有 `area-config`、`config/areas`、宿主项目名或宿主相对路径引用。 + +## 3. 宿主改为外部 CLI 消费 + +- [ ] 将 compiler 以精确 git tag `v0.1.0` 安装到宿主并记录 lockfile;禁止范围版本、`latest`、`*` 和裸分支。 +- [ ] 将 `toRoadCompilerInput()` 序列化到每个区域的 pipeline staging 目录,传给 compiler CLI。 +- [ ] 在 `build-area.js` 的 blender 前置阶段调用 CLI。沿用管线的 `spawnSync` / `runCommand` 失败语义(启动错误、exit status、signal)和直通日志;缓冲或 tee stdout 后,解析恰好一个完成标记并验证其 `areaId`、`output` 与当前区域匹配。 +- [ ] 将 host workbench 的 fresh compile 回调切换到同一 CLI,不改变 fresh-process 行为。 +- [ ] 把所有 host 对 `packages/road-compiler/**` 的直接 import 替换成已安装包的公开 API 或 CLI;两个区域 parity 均通过后删除 in-host 副本。 +- [ ] 将宿主 pipeline spec 改为指向编译器仓库所拥有的契约文档。 + +## 4. 验证与发布门 + +新仓库(干净 clone): ```bash -# 方案 A:subtree split -git subtree split --prefix=packages/road-compiler -b road-compiler-export -# 在新目录初始化并拉入该分支 - -# 方案 B:filter-repo(更彻底,推荐) -git clone --no-local . /tmp/road-compiler-split -cd /tmp/road-compiler-split -git filter-repo --path packages/road-compiler/ --path-rename packages/road-compiler/: +npm ci +npm test +npm run test:road-parity +npm run road:workbench -- --config test/fixtures/fengshu-er-road.input.json --no-compile ``` -⚠️ 注意:Phase 1 的搬迁若用了 `git mv`,历史可 `--follow` 追溯; -若用了删除+新建,`filter-repo` 只能追到 Phase 1。 -**Phase 1 搬迁时必须用 `git mv`** —— 这条已在 Phase 1 implement Step 3 隐含, -此处显式记录依赖。 - -- [ ] 拆出后验证:`git log --follow src/compile/native-road.js | tail -20` - 能看到 08-13 native-road-compiler 的提交 - -## Step 3 — 新仓库自持化 - -- [ ] `package.json` 补全依赖(含 K4 的 OpenLayers) -- [ ] `npm install && npm test` 在新仓库独立通过 -- [ ] 基线 JSON 迁入 `test/baseline/` -- [ ] 契约文档迁入 `docs/` -- [ ] README:契约摘要 + 与宿主项目关系说明 - -## Step 4 — 宿主改造 - -- [ ] `package.json` 依赖指向新仓库(开发期 `file:`) -- [ ] `build-area.js` 改子进程调用 + 解析 `NATIVE_ROAD_COMPILE_DONE` -- [ ] `area-config.js` 的 `toRoadCompilerInput()` 输出改为 CLI 参数或 input JSON 文件 -- [ ] 删除 `packages/road-compiler/`(或按过渡期决策保留) -- [ ] `.trellis/spec/pipeline/` 加契约指针 - -## Step 5 — 验证 +宿主两个有效区域的逐字节 parity: ```bash -# 宿主端到端 -npm run build:area -- --config config/areas/fengshu-er-road.json - -# 三区域 parity -for a in fengshu-er-road hanyang-block nantaizi-lake-innovation-valley; do - node scripts/road-parity.js --config config/areas/$a.json \ - --compare .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json || echo "FAIL $a" -done - -# 反向依赖为 0(在新仓库内跑) -grep -rn "osm2streets-qgis-workflow\|area-config" . --exclude-dir=node_modules --exclude-dir=.git \ - && echo "VIOLATION" - -# 锁版本检查 -node -e 'const d=require("./package.json").dependencies;console.log(d)' | grep -i road +node scripts/road-parity.js --config config/areas/fengshu-er-road.json \ + --compare .trellis/tasks/08-25-road-compiler-extraction/baseline/fengshu-er-road.json +node scripts/road-parity.js --config config/areas/nantaizi-lake-innovation-valley.json \ + --compare .trellis/tasks/08-25-road-compiler-extraction/baseline/nantaizi-lake-innovation-valley.json ``` -- [ ] 在一台未 clone 宿主的环境(或 `/tmp` 全新 clone)验证 AC2.2 - -## Review Gate - -AC2.1–AC2.7 全绿。特别是 AC2.2(脱离宿主可自测)—— 这是"独立可维护"的实质。 +- [ ] 为 CLI 标记成功、缺失、重复、非法 JSON、areaId 不匹配和 output 不匹配补测试。 +- [ ] 跑 package 单测、宿主 native-road / workbench / traffic signals / turn-lane-arrows 测试,以及两个 parity 检查。 +- [ ] 使用 `trellis-check` 做最终跨层检查;Blender 完整构建需以提权方式运行,原因是 macOS Metal 初始化不能在沙箱内启动。 ## Rollback -- 宿主依赖改回 `file:packages/road-compiler`(若过渡期保留了该目录) -- 或 `git revert` 宿主侧改造 commit;新仓库留着不影响宿主 +- 外部仓库和已发布 tag 保留不动;在宿主 revert 消费改造即可回到 P1 边界。 +- 若按决定临时保留 in-host 副本,可将依赖回指该副本以恢复;该退路不取代 host parity 验证。 diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/prd.md b/.trellis/tasks/08-25-rc-p2-repo-split/prd.md index c625eb7..ed924ec 100644 --- a/.trellis/tasks/08-25-rc-p2-repo-split/prd.md +++ b/.trellis/tasks/08-25-rc-p2-repo-split/prd.md @@ -1,80 +1,62 @@ # Phase 2:拆仓库 父任务:`.trellis/tasks/08-25-road-compiler-extraction/` -技术设计:父任务 `design.md` §1.3(消费方式)、§2(模块清单) ## Goal -把 `packages/road-compiler/` 拆成保留 git 历史的独立仓库, -宿主改为锁版本依赖 + 子进程调用消费。 +把当前 `packages/road-compiler/` 拆为保留 git 历史、可脱离宿主独立测试和运行的仓库;宿主改为消费一个锁定版本的编译器 CLI。这样道路编译器可以独立演进,宿主依然通过版本化文件契约稳定构建场景。 + +## Confirmed Facts + +- P1 已完成:编译器实现和 workbench 的所有权在 `packages/road-compiler/`;宿主负责将区域配置映射为 `RoadCompilerInput`。 +- `git log --follow packages/road-compiler/src/compile/native-road.js` 已能追溯到 2026-08-13 的 native-road compiler 历史。 +- 契约为 `native-road-package/v1`,完成标记为 `NATIVE_ROAD_COMPILE_DONE `;`comparisonDir` 是可选输入,继续保留 `comparison.json`。 +- 当前宿主远端为内部 Git 服务 `https://git.app.que01.top/que01/osmWorkflow.git`;P2 独立编译器仓库定为私有 `https://git.app.que01.top/que01/road-compiler.git`。 +- 可维护范围只有 `fengshu-er-road` 和 `nantaizi-lake-innovation-valley`。`hanyang-block` 是废案,不参与基线、测试、验收或迁移语料。 ## Requirements -### R2.1 保留 git 历史 🔴 +### R2.1 保留 Git 历史 -- 用 `git subtree split` 或 `git filter-repo` 拆出,**不得**用 `cp` + `git init` -- 理由(父任务 R3.2):1695 行几何逻辑的 blame 是踩坑记录, - 丢了以后没人敢改 `compileGeometry` 里任何一行 -- 验证:新仓库内 `git log --follow src/compile/native-road.js` 能看到 - 08-13 native-road-compiler 以来的完整历史 +- 用 `git subtree split` 或 `git filter-repo` 导出,不得以复制目录再 `git init` 替代。 +- 新仓库中 `git log --follow src/compile/native-road.js` 必须能追溯到 P1 之前的 native-road compiler 提交。 -### R2.2 子进程为主契约 +### R2.2 新仓库可独立运行 -- 宿主 `build-area.js` 改为 `execFileSync` 调编译器 CLI, - 解析 `NATIVE_ROAD_COMPILE_DONE` stdout 标记(父任务 design §1.3) -- in-process `require` 可保留为可选优化路径,但不得是唯一路径 -- 与既有 QGIS / GDAL / Blender 调用方式一致 - (见 `.trellis/spec/pipeline/external-tools.md`) +- 新仓库拥有 compiler、CLI、check、workbench、其测试 fixture、两个 parity baseline、契约文档和 README。 +- 新仓库 own `ol` 及其 workbench 所需的运行依赖;workbench 不得从宿主 `node_modules` 提供浏览器资源。 +- 新仓库不得引用宿主的 `area-config`、`config/areas`、`scripts/` 或本仓库绝对路径。 -### R2.3 锁版本依赖 +### R2.3 宿主的消费边界 -- 开发期:`file:` 或 workspace 依赖 -- 稳定后:git tag / 私有 npm,宿主 `package.json` **锁具体版本,不用 `latest`** -- 契约版本 `native-road-package/v1` 与包版本分开演进: - 包可以发 patch,契约版本只在破坏性变更时升 +- `scripts/lib/area-config.js` 继续是唯一的区域配置归一化与 `RoadCompilerInput` 映射位置。 +- `scripts/build-area.js` 用已安装编译器的 CLI 子进程编译,继承 stdout/stderr,检查启动错误、退出状态和 signal,并解析且校验唯一的 `NATIVE_ROAD_COMPILE_DONE` 标记。 +- 宿主只依赖 CLI、写入的文件和版本化输入 JSON;不再以相对路径 import 编译器内部模块。保留的宿主 signal 文件 I/O 适配层改为只调用公开包 API。 +- `road-workbench` 保持每次编译均启动新进程的语义,但改为调用已安装 CLI。 -### R2.4 K4:workbench 依赖自持 +### R2.4 版本与迁移 -- `road-workbench` 的 OpenLayers 从新仓库自己的 `node_modules` 提供 -- import map 路径相应调整 - -### R2.5 基线迁移 - -- Phase 0 的三区域基线 JSON 搬进新仓库当测试语料(父任务 R4 第三条) -- 新仓库 CI/test 能独立跑 parity,无需宿主在场 -- **同时**宿主保留一份,用于验证升级编译器版本后产物未变 - -### R2.6 契约文档迁移 - -- `docs/native-road-package-v1.md` 搬进新仓库 -- 宿主 `.trellis/spec/pipeline/` 留指针,说明契约由编译器仓库拥有 +- 宿主依赖必须锁定一个具体编译器版本,禁止 `latest` 或 `*`;包版本与 `native-road-package/v1` 的契约版本独立演进。 +- 初始分发使用带注释的 git tag `v0.1.0`;宿主锁定该 tag。完整 parity 验证后移除 `packages/road-compiler/`,不保留 in-host 副本作为正常消费路径。 +- 宿主保留两份 baseline,用来验证今后升级编译器版本后没有产物漂移。 ## Acceptance Criteria -- [ ] AC2.1 新仓库 `git log --follow` 能追到搬迁前的历史 -- [ ] AC2.2 新仓库 `npm test` 在**未 clone 宿主**的机器上通过 -- [ ] AC2.3 宿主从锁版本依赖构建,三区域 parity 对基线全绿 -- [ ] AC2.4 `build-area.js` 走子进程路径,`NATIVE_ROAD_COMPILE_DONE` 被正确解析 -- [ ] AC2.5 宿主 `package.json` 依赖为具体版本,非 `latest` / 非 `*` -- [ ] AC2.6 编译器仓库对宿主反向依赖数为 0(父任务 AC8,grep 验证) -- [ ] AC2.7 `npm run road:workbench` 在新仓库内独立可跑 - -## 依赖与顺序 - -- **前置**:Phase 1 完成且 AC1.1–AC1.8 全绿 -- **阻塞**:Phase 3、Phase 4 - -## 待决策(进入本阶段时定) - -| 项 | 选项 | -|---|---| -| 仓库名 | `road-compiler` / `native-road-compiler` / `osm-road-compiler` | -| 托管 | GitHub 私有 / 公开 / 内部 git | -| 分发 | git tag 依赖 / 私有 npm registry | -| 宿主过渡期 | 是否保留 `packages/road-compiler/` 一段时间做双跑对照 | +- [ ] AC2.1 独立仓库的 `git log --follow src/compile/native-road.js` 可见 P1 前的 compiler 历史。 +- [ ] AC2.2 在未 clone 宿主的干净目录中,新仓库 `npm ci`(或等价锁文件安装)和 `npm test` 均通过。 +- [ ] AC2.3 新仓库的两区域 parity(`fengshu-er-road`、`nantaizi-lake-innovation-valley`)均与迁入 baseline 完全一致。 +- [ ] AC2.4 宿主用锁版本依赖运行 CLI;`build-area.js` 成功解析一个有效的 `NATIVE_ROAD_COMPILE_DONE` 标记,缺失、重复或非法标记会失败。 +- [ ] AC2.5 宿主的两个区域 parity 均对保留 baseline 全绿。 +- [ ] AC2.6 宿主 `package.json` 的 compiler 依赖是具体、可复现版本,不是 `latest`、`*` 或裸分支。 +- [ ] AC2.7 编译器仓库对宿主反向依赖为 0,且其 workbench 可独立启动。 ## Out of Scope -- layer manifest(Phase 3) -- drawtonomy 扩展(Phase 4) -- 任何编译器内部逻辑改动 —— 本阶段代码内容不变,只换位置与消费方式 +- 编译器几何、规则或输出内容改动。 +- 图层 manifest 和道路/建筑渲染分离(Phase 3)。 +- drawtonomy 扩展(Phase 4)。 +- `hanyang-block` 修复、迁移或作为验收样本。 + +## Release Decision + +独立仓库为私有 `https://git.app.que01.top/que01/road-compiler.git`。首发以带注释 tag `v0.1.0` 发布,宿主依赖精确锁定该 tag;两个区域 parity 均通过后删除 `packages/road-compiler/`。该决定授权 P2 在该内部远端创建和推送仓库,但不授权发布到 npm 或其他托管平台。 diff --git a/.trellis/tasks/08-25-rc-p2-repo-split/task.json b/.trellis/tasks/08-25-rc-p2-repo-split/task.json index 6a4c84d..2721544 100644 --- a/.trellis/tasks/08-25-rc-p2-repo-split/task.json +++ b/.trellis/tasks/08-25-rc-p2-repo-split/task.json @@ -3,7 +3,7 @@ "name": "rc-p2-repo-split", "title": "Phase 2:拆仓库", "description": "保留 git 历史拆出独立仓库,宿主以锁版本依赖消费,子进程为主契约", - "status": "planning", + "status": "in_progress", "dev_type": null, "scope": null, "package": null, diff --git a/package-lock.json b/package-lock.json index 8e1526e..99fea7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "dependencies": { "@inquirer/prompts": "^8.5.2", + "@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.1.2", "ol": "^10.10.0", "osm2streets-js-node": "0.1.4" } @@ -341,6 +342,16 @@ } } }, + "node_modules/@osm-asset/road-compiler": { + "version": "0.1.0", + "resolved": "git+https://git.app.que01.top/que01/road-compiler.git#90400d967f9be6ad6420b8b29e5d7d652eee3493", + "dependencies": { + "ol": "10.10.0" + }, + "bin": { + "road-compiler": "bin/road-compiler.js" + } + }, "node_modules/@petamoriken/float16": { "version": "3.9.3", "resolved": "http://172.16.1.86:4873/@petamoriken/float16/-/float16-3.9.3.tgz", diff --git a/package.json b/package.json index 075ec8a..07896cd 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,6 @@ "version": "0.3.0", "private": true, "type": "commonjs", - "workspaces": [ - "packages/road-compiler" - ], "scripts": { "build": "node scripts/interactive-build.js", "build:area": "node scripts/build-area.js", @@ -21,6 +18,7 @@ "serve:v2x-preview": "node scripts/v2x-preview-server.js", "test:road-workbench": "node scripts/test-road-workbench.js", "test:native-road": "node scripts/test-native-road.js", + "test:road-compiler-cli": "node scripts/test-road-compiler-cli.js", "test:road-parity": "node scripts/test-road-parity.js", "test:gaode-junction-reference": "node scripts/test-gaode-junction-reference.js", "test:preflight": "node scripts/test-area-preflight.js", @@ -39,6 +37,7 @@ }, "dependencies": { "@inquirer/prompts": "^8.5.2", + "@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.1.2", "ol": "^10.10.0", "osm2streets-js-node": "0.1.4" } diff --git a/packages/road-compiler/package.json b/packages/road-compiler/package.json deleted file mode 100644 index 336e583..0000000 --- a/packages/road-compiler/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@osm-asset/road-compiler", - "version": "0.1.0", - "private": true, - "type": "commonjs", - "main": "src/index.js", - "scripts": { - "test": "node test/index.js" - } -} diff --git a/packages/road-compiler/src/check.js b/packages/road-compiler/src/check.js deleted file mode 100644 index bab9cff..0000000 --- a/packages/road-compiler/src/check.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -function checkOutput({ areaId, outDir }) { - if (typeof areaId !== "string" || !areaId) throw new Error("RoadCompilerCheckInput.areaId must be a non-empty string"); - if (typeof outDir !== "string" || !outDir) throw new Error("RoadCompilerCheckInput.outDir must be a non-empty string"); - const compiledPath = path.join(outDir, "compiled.json"); - if (!fs.existsSync(compiledPath)) throw new Error(`Native road output is missing: ${compiledPath}`); - const compiled = readJson(compiledPath); - const connectors = readJson(path.join(outDir, "layers", "connectors.geojson")); - const published = new Set(connectors.features.map((feature) => feature.properties.movement_id)); - const failures = []; - for (const movement of compiled.movements || []) { - if (movement.geometryPublished && !published.has(movement.id)) failures.push(`Published movement has no connector: ${movement.id}`); - if (!movement.geometryPublished && published.has(movement.id)) failures.push(`Non-published movement has a connector: ${movement.id}`); - if (!movement.geometryStatus) failures.push(`Movement has no geometry status: ${movement.id}`); - } - const errors = (compiled.diagnostics || []).filter((item) => item.severity === "error"); - const warnings = (compiled.diagnostics || []).filter((item) => item.severity === "warning"); - return { schema: "native-road-check/v1", areaId, ok: failures.length === 0 && errors.length === 0, movementCount: (compiled.movements || []).length, connectorCount: connectors.features.length, errors: errors.map((item) => ({ id: item.id, rule: item.rule, message: item.message })), warningCount: warnings.length, failures }; -} - -function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } - -module.exports = { checkOutput }; diff --git a/packages/road-compiler/src/compile/compiler.js b/packages/road-compiler/src/compile/compiler.js deleted file mode 100644 index 008f728..0000000 --- a/packages/road-compiler/src/compile/compiler.js +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const { compileRoadModel, compileGeometry, loadOverrides, validateOverrides, writeJsonAtomic } = require("./native-road"); -const { loadOrGenerate, runtime } = require("../native-traffic-signals"); - -function compileInput(input) { - validateInput(input); - const area = { - id: input.areaId, - input: input.osmFile, - nativeRoad: input.options, - outputs: { - nativeRoadOverrides: input.overridesFile, - nativeTrafficSignals: input.trafficSignalsFile, - nativeRoadDir: input.outDir, - pipelineDir: input.stagingDir, - geojsonDir: input.comparisonDir || null, - }, - }; - const overrides = loadOverrides(area.outputs.nativeRoadOverrides); - const model = compileRoadModel(fs.readFileSync(area.input, "utf8"), overrides); - // 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 }); - 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 signalRuntime = runtime(signalDocument); - // Persist validation normalization, including one-time legacy heading migration. - writeJsonAtomic(area.outputs.nativeTrafficSignals, signalDocument); - const staging = fs.mkdtempSync(path.join(area.outputs.pipelineDir, "native-road-")); - try { - const result = { - schema: "native-road-compiled/v1", - areaId: area.id, - source: { osm: area.input, overrides: area.outputs.nativeRoadOverrides, trafficSignals: area.outputs.nativeTrafficSignals }, - model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections }, - movements: compiled.movements, - trafficSignals: { assemblies: "traffic-signal-assemblies.json", runtime: "traffic-signals.json", count: signalRuntime.signals.length }, - diagnostics: compiled.diagnostics, - layers: { roadSurface: "layers/road_surface.geojson", edgeLines: "layers/edge_lines.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); - writeJsonAtomic(path.join(staging, "compiled.json"), result); - writeJsonAtomic(path.join(staging, "diagnostics.json"), { schema: "native-road-diagnostics/v1", diagnostics: compiled.diagnostics }); - writeJsonAtomic(path.join(staging, "comparison.json"), comparison); - writeJsonAtomic(path.join(staging, "traffic-signal-assemblies.json"), signalDocument.assemblies); - writeJsonAtomic(path.join(staging, "traffic-signals.json"), signalRuntime); - writeJsonAtomic(path.join(staging, "layers", "road_surface.geojson"), compiled.roadSurface); - writeJsonAtomic(path.join(staging, "layers", "edge_lines.geojson"), compiled.edgeLines); - writeJsonAtomic(path.join(staging, "layers", "sidewalk_surface.geojson"), compiled.sidewalkSurface); - 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_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", "turn_arrows.geojson"), compiled.turnArrows); - writeJsonAtomic(path.join(staging, "layers", "crosswalks.geojson"), compiled.crosswalks); - writeJsonAtomic(path.join(staging, "layers", "vehicle_stop_lines.geojson"), compiled.vehicleStopLines); - writeJsonAtomic(path.join(staging, "layers", "connectors.geojson"), compiled.connectors); - fs.rmSync(area.outputs.nativeRoadDir, { recursive: true, force: true }); - fs.renameSync(staging, area.outputs.nativeRoadDir); - return { area, result, comparison }; - } catch (error) { - fs.rmSync(staging, { recursive: true, force: true }); - throw error; - } -} - -function compareOsm2Streets(area, model, compiled) { - const source = area.outputs.geojsonDir ? path.join(area.outputs.geojsonDir, "road_surface.geojson") : null; - let featureCount = null; - if (source && fs.existsSync(source)) { - const collection = JSON.parse(fs.readFileSync(source, "utf8")); - featureCount = Array.isArray(collection.features) ? collection.features.length : null; - } - const diagnosticsBySeverity = {}; - const diagnosticsByRule = {}; - for (const item of compiled.diagnostics) { - diagnosticsBySeverity[item.severity] = (diagnosticsBySeverity[item.severity] || 0) + 1; - diagnosticsByRule[item.rule] = (diagnosticsByRule[item.rule] || 0) + 1; - } - const dangling = compiled.diagnostics.filter((item) => item.rule === "unconnected-interior-road-end"); - const junctions = compiled.intersectionSurface.features; - const fallbackJunctions = junctions.filter((feature) => feature.properties.boundary_mode === "connector-convex-fallback"); - return { - schema: "native-road-comparison/v2", - nativeRoadCount: model.roads.length, - nativeRoadSurfaceFeatures: compiled.roadSurface.features.length, - nativeSidewalkSurfaceFeatures: compiled.sidewalkSurface.features.length, - nativeJunctionSurfaceFeatures: compiled.intersectionSurface.features.length, - nativeApproachEnvelopeJunctions: junctions.length - fallbackJunctions.length, - nativeFallbackJunctions: fallbackJunctions.length, - nativeMaxJunctionExpansionRatio: junctions.reduce((maximum, feature) => Math.max(maximum, Number(feature.properties.expansion_ratio) || 0), 0), - nativeLaneCenterlineFeatures: compiled.laneCenterlines.features.length, - nativeLaneSeparatorFeatures: compiled.laneSeparators.features.length, - nativeCenterLineFeatures: compiled.centerLines.features.length, - nativeDirectionArrowFeatures: compiled.directionArrows.features.length, - nativeTurnArrowFeatures: compiled.turnArrows.features.length, - nativeCrosswalkFeatures: compiled.crosswalks.features.length, - nativeVehicleStopLineFeatures: compiled.vehicleStopLines.features.length, - nativeConnectorFeatures: compiled.connectors.features.length, - nativeMovementCount: compiled.movements.length, - nativePublishedMovementCount: compiled.movements.filter((movement) => movement.geometryPublished).length, - nativeConnectionCount: model.connections.length, - unconnectedInteriorRoadEnds: dangling.length, - unconnectedEndsWithManualCandidates: dangling.filter((item) => item.manualCandidates?.length).length, - diagnosticsBySeverity, - diagnosticsByRule, - osm2streetsRoadSurfaceFeatures: featureCount, - osm2streetsAvailable: featureCount !== null, - note: "Counts are coverage evidence only; geometry quality requires diagnostic and visual review.", - }; -} - -function validateInput(input) { - if (!input || typeof input !== "object") throw new Error("RoadCompilerInput must be an object"); - for (const key of ["areaId", "osmFile", "outDir", "stagingDir", "overridesFile", "trafficSignalsFile"]) { - if (typeof input[key] !== "string" || input[key].trim() === "") throw new Error(`RoadCompilerInput.${key} must be a non-empty string`); - } - if (!input.options || typeof input.options !== "object") throw new Error("RoadCompilerInput.options must be an object"); - if (typeof input.options.edgeLines !== "boolean") throw new Error("RoadCompilerInput.options.edgeLines must be a boolean"); - if (!input.options.junctionTemplates || typeof input.options.junctionTemplates !== "object") throw new Error("RoadCompilerInput.options.junctionTemplates must be an object"); -} - -module.exports = { compileInput, validateInput }; diff --git a/packages/road-compiler/src/compile/complex-junction.js b/packages/road-compiler/src/compile/complex-junction.js deleted file mode 100644 index 5f41891..0000000 --- a/packages/road-compiler/src/compile/complex-junction.js +++ /dev/null @@ -1,474 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const { convertGeoJson, boundsOf } = require("../reference/gaode"); -const metricsCache = new WeakMap(); -const CORNER_FILLET_SEGMENTS = 12; -// Must match DEFAULT_SIDEWALK_WIDTH_METERS in native-road.js so the corner band -// lines up with the straight strips it joins. -const SIDEWALK_WIDTH_METERS = 2; -// The straight strips are trimmed against the cluster boundary using the road -// centerline, so they stop a little beyond the carriageway end. Run the corner -// past that end and let the two overlap rather than chase an exact seam. -const SIDEWALK_CORNER_OVERRUN_METERS = 6; - -function buildComplexJunctionGeometry(model, cluster, helpers) { - const nodeIds = new Set(cluster.nodeIds.map(String)); - const nodes = cluster.nodeIds.map((id) => helpers.junctionPlans.get(String(id))?.node).filter(Boolean); - if (nodes.length < 2) return { features: [], diagnostics: [helpers.diagnostic("warning", `junction-cluster:${cluster.id}`, [...nodeIds], "complex-junction-insufficient-nodes", "复合路口至少需要两个有效节点。", null)] }; - const center = nodes.reduce((sum, point) => [sum[0] + point[0] / nodes.length, sum[1] + point[1] / nodes.length], [0, 0]); - const approaches = []; - const carriageways = []; - for (const [nodeId, plan] of helpers.junctionPlans) { - if (!nodeIds.has(String(nodeId))) continue; - for (const approach of plan.approaches) { - const endpoint = approach.line.at(-1); - if (nodes.some((node) => node !== plan.node && helpers.distanceMeters(endpoint, node) < 4)) continue; - const heading = helpers.headingAtEndpoint(approach.line); - const length = helpers.lineLengthMeters(approach.line); - carriageways.push({ nodeId, approach, plan, heading, length }); - if (approaches.some((item) => Math.abs(normalizeHeading(item.heading - heading)) < 20)) continue; - approaches.push({ nodeId, approach, plan, heading, length }); - } - } - if (approaches.length < 3) return { features: [], diagnostics: [helpers.diagnostic("warning", `junction-cluster:${cluster.id}`, [...nodeIds], "complex-junction-insufficient-approaches", "复合路口无法识别足够的外部进口。", center)] }; - const { calibration, coreRadius } = complexJunctionMetrics(cluster); - const sorted = [...approaches].sort((a, b) => a.heading - b.heading); - const arms = sorted.map((representative) => ({ - representative, - heading: averageHeading(carriageways.filter((candidate) => Math.abs(normalizeHeading(candidate.heading - representative.heading)) < 20).map((candidate) => candidate.heading)), - members: carriageways.filter((candidate) => Math.abs(normalizeHeading(candidate.heading - representative.heading)) < 20), - })); - const outerRadius = complexJunctionMetrics(cluster).approachOuterRadius; - const boundaryParts = []; - const crosswalks = []; - const stopLines = []; - const islands = []; - const armCrosswalkRadius = coreRadius * .68; - for (const item of carriageways) { - const outer = pointOnCarriagewayRadius(item, center, outerRadius, helpers); - const inner = pointOnCarriagewayRadius(item, center, coreRadius * .7, helpers); - const outerHalf = item.approach.widthMeters / 2; - const innerHalf = outerHalf; - boundaryParts.push({ item, outer, inner, outerHalf, innerHalf }); - const incomingRoad = item.approach.roadIds.map((roadId) => model.roads.find((road) => road.id === roadId)).find((road) => String(road?.sourceNodeIds.at(-1)) === String(item.nodeId)); - if (incomingRoad) { - // Keep the stop bar just outside the road crosswalk. The previous fixed - // core-radius offset placed it nearly ten metres beyond the crossing. - const stopCenter = pointOnCarriagewayRadius(item, center, armCrosswalkRadius + 3, helpers); - const ring = [ - helpers.offsetCoordinate(helpers.offsetCoordinate(stopCenter, item.heading + 90, -outerHalf), item.heading, -.24), - helpers.offsetCoordinate(helpers.offsetCoordinate(stopCenter, item.heading + 90, outerHalf), item.heading, -.24), - helpers.offsetCoordinate(helpers.offsetCoordinate(stopCenter, item.heading + 90, outerHalf), item.heading, .24), - helpers.offsetCoordinate(helpers.offsetCoordinate(stopCenter, item.heading + 90, -outerHalf), item.heading, .24), - helpers.offsetCoordinate(helpers.offsetCoordinate(stopCenter, item.heading + 90, -outerHalf), item.heading, -.24), - ]; - stopLines.push({ type: "Feature", properties: { native_id: `complex-stop-line:${cluster.id}:${item.approach.segmentId}`, cluster_id: cluster.id, kind: "complex-stop-line", road_id: incomingRoad.id, node_id: item.nodeId, direction: item.heading, provenance: "native-road-complex-junction-stop-line/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - // The four support lines provide a common corner frame, but each long - // crossing remains clipped to its OSM-derived road envelope. Corner islands - // fill the remaining frame gaps; crossings must never do that job. - for (const arm of arms) { - const envelope = armEnvelopeAtRadius(arm, armCrosswalkRadius, center, helpers); - if (!envelope) continue; - arm.crosswalkFrame = { - center: envelope.center, - groupDepth: 3.4, - supportHeading: normalizeHeading(arm.heading + 90), - envelopeWidthMeters: envelope.widthMeters, - }; - } - const frameCorners = arms.map((arm, index) => { - const next = arms[(index + 1) % arms.length]; - const delta = positiveHeadingDelta(arm.heading, next.heading); - if (delta < 45 || delta > 135 || !arm.crosswalkFrame || !next.crosswalkFrame) return null; - return supportLineIntersection(arm.crosswalkFrame, next.crosswalkFrame, center); - }); - for (let index = 0; index < arms.length; index += 1) { - const arm = arms[index]; - if (!arm.crosswalkFrame) continue; - const item = arm.representative; - const roadEdgeInset = .35; - const usableSpan = Math.max(.42, arm.crosswalkFrame.envelopeWidthMeters - roadEdgeInset * 2); - const endpoints = [ - helpers.offsetCoordinate(arm.crosswalkFrame.center, arm.heading + 90, -usableSpan / 2), - helpers.offsetCoordinate(arm.crosswalkFrame.center, arm.heading + 90, usableSpan / 2), - ]; - const groupDepth = arm.crosswalkFrame.groupDepth; - const stripeWidth = .42; - const stripeCount = Math.max(6, Math.floor((usableSpan - stripeWidth) / .82) + 1); - const stripeSpacing = stripeCount > 1 ? (usableSpan - stripeWidth) / (stripeCount - 1) : 0; - arm.crosswalkFrame.center = midpoint(...endpoints); - arm.crosswalkFrame.endpoints = endpoints; - arm.crosswalkFrame.spanMeters = usableSpan; - arm.crosswalkFrame.roadEdgeInsetMeters = roadEdgeInset; - for (let stripe = 0; stripe < stripeCount; stripe += 1) { - const along = stripeWidth / 2 + stripe * stripeSpacing; - const centerPoint = helpers.offsetCoordinate(endpoints[0], bearing(...endpoints), along); - const ring = [ - helpers.offsetCoordinate(helpers.offsetCoordinate(centerPoint, arm.heading, -groupDepth / 2), arm.heading + 90, -stripeWidth / 2), - helpers.offsetCoordinate(helpers.offsetCoordinate(centerPoint, arm.heading, groupDepth / 2), arm.heading + 90, -stripeWidth / 2), - helpers.offsetCoordinate(helpers.offsetCoordinate(centerPoint, arm.heading, groupDepth / 2), arm.heading + 90, stripeWidth / 2), - helpers.offsetCoordinate(helpers.offsetCoordinate(centerPoint, arm.heading, -groupDepth / 2), arm.heading + 90, stripeWidth / 2), - helpers.offsetCoordinate(helpers.offsetCoordinate(centerPoint, arm.heading, -groupDepth / 2), arm.heading + 90, -stripeWidth / 2), - ]; - crosswalks.push({ type: "Feature", properties: { native_id: `complex-crosswalk:${cluster.id}:${item.nodeId}:${item.approach.segmentId}:${stripe + 1}`, cluster_id: cluster.id, kind: "complex-crosswalk", crossing_node_id: item.nodeId, road_id: item.approach.roadIds[0], direction: arm.heading, radial_distance_m: armCrosswalkRadius, span_m: usableSpan, road_envelope_span_m: arm.crosswalkFrame.envelopeWidthMeters, road_edge_inset_m: roadEdgeInset, group_depth_m: groupDepth, stripe_width_m: stripeWidth, stripe_spacing_m: stripeSpacing, frame_center: arm.crosswalkFrame.center, frame_support_heading: arm.crosswalkFrame.supportHeading, provenance: "native-road-complex-junction-crosswalk/v6-road-clipped" }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - // The four arm groups are the sides of one pedestrian frame. Each diagonal - // group is anchored at the intersection of its adjacent side support lines, - // so all eight groups stay one composition when the OSM arms are skewed. - for (let index = 0; index < arms.length; index += 1) { - const first = arms[index]; - const second = arms[(index + 1) % arms.length]; - const delta = positiveHeadingDelta(first.heading, second.heading); - if (delta < 45 || delta > 135) continue; - if (!first.crosswalkFrame || !second.crosswalkFrame) continue; - const bisector = normalizeHeading(first.heading + delta / 2); - const frameCorner = frameCorners[index]; - if (!frameCorner) continue; - const cornerStripeSpacing = .62; - const cornerStripeWidth = .4; - const cornerGroupHalfDepth = (5 * cornerStripeSpacing + cornerStripeWidth) / 2; - const endpointForCorner = (arm) => [...arm.crosswalkFrame.endpoints].sort((a, b) => helpers.distanceMeters(a, frameCorner) - helpers.distanceMeters(b, frameCorner))[0]; - const outerEdgeAtCorner = (arm) => { - const endpoint = endpointForCorner(arm); - return [arm.heading, arm.heading + 180] - .map((heading) => helpers.offsetCoordinate(endpoint, heading, arm.crosswalkFrame.groupDepth / 2)) - .sort((a, b) => directionalProjectionMeters(center, b, bisector) - directionalProjectionMeters(center, a, bisector))[0]; - }; - const islandBaseGap = .05; - const islandApexOffset = 1.5; - const islandBase = [outerEdgeAtCorner(first), outerEdgeAtCorner(second)].map((point) => helpers.offsetCoordinate(point, bisector, islandBaseGap)); - const islandApex = helpers.offsetCoordinate(frameCorner, bisector, islandApexOffset); - const islandCrossingClearance = .2; - const cornerCrossingOffset = islandApexOffset + islandCrossingClearance + cornerGroupHalfDepth; - const islandInnerRadius = Math.min(...islandBase.map((point) => directionalProjectionMeters(center, point, bisector))); - const islandOuterRadius = directionalProjectionMeters(center, islandApex, bisector); - const cornerCrossingCenter = helpers.offsetCoordinate(frameCorner, bisector, cornerCrossingOffset); - const islandRing = roundedPolygonRing([islandBase[0], islandApex, islandBase[1]], .24); - islands.push({ type: "Feature", properties: { native_id: `complex-corner-island:${cluster.id}:${index + 1}`, cluster_id: cluster.id, kind: "complex-corner-island", corner_index: index + 1, from_heading: first.heading, to_heading: second.heading, frame_corner: frameCorner, base_points: islandBase, apex_point: islandApex, inner_radius_m: islandInnerRadius, outer_radius_m: islandOuterRadius, base_width_m: helpers.distanceMeters(...islandBase), crossing_clearance_m: islandCrossingClearance, corner_rounding_ratio: .24, provenance: "native-road-complex-junction-corner/v7-road-gap-fill" }, geometry: { type: "Polygon", coordinates: [islandRing] } }); - - let cornerCrossingHalfSpan = .4; - for (let stripe = 0; stripe < 6; stripe += 1) { - const stripeOffset = (stripe - 2.5) * cornerStripeSpacing; - const stripeCenter = helpers.offsetCoordinate(cornerCrossingCenter, bisector, stripeOffset); - const stripeRadius = directionalProjectionMeters(center, stripeCenter, bisector); - const curbPair = limitedCornerPair(first, second, stripeRadius, bisector, center, 6.5, helpers); - if (!curbPair) continue; - const halfSpan = Math.max(.4, Math.min(6.5, helpers.distanceMeters(...curbPair)) / 2); - cornerCrossingHalfSpan = Math.max(cornerCrossingHalfSpan, halfSpan); - const stripePair = [ - helpers.offsetCoordinate(stripeCenter, bisector - 90, halfSpan), - helpers.offsetCoordinate(stripeCenter, bisector + 90, halfSpan), - ]; - const ring = [ - helpers.offsetCoordinate(stripePair[0], bisector, -cornerStripeWidth / 2), - helpers.offsetCoordinate(stripePair[1], bisector, -cornerStripeWidth / 2), - helpers.offsetCoordinate(stripePair[1], bisector, cornerStripeWidth / 2), - helpers.offsetCoordinate(stripePair[0], bisector, cornerStripeWidth / 2), - helpers.offsetCoordinate(stripePair[0], bisector, -cornerStripeWidth / 2), - ]; - crosswalks.push({ type: "Feature", properties: { native_id: `complex-corner-crosswalk:${cluster.id}:${index + 1}:${stripe + 1}`, cluster_id: cluster.id, kind: "complex-corner-crosswalk", corner_index: index + 1, direction: bisector, radial_distance_m: stripeRadius, frame_corner: frameCorner, from_heading: first.heading, to_heading: second.heading, provenance: "native-road-complex-junction-corner-crosswalk/v3" }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - // Each carriageway ends in its own rectangle, so adjacent arms meet at a - // sharp notch instead of a curb. A real corner is one tangent-continuous - // sweep between the two outermost road edges, so fit a fixed-radius fillet - // into the wedge those edges form and fill the sector behind it. - const cornerFills = []; - const sidewalkCorners = []; - const cornerDiagnostics = []; - const cornerRadius = Math.max(4, Math.min(25, Number(cluster.cornerRadiusMeters) || 12)); - for (let index = 0; index < arms.length; index += 1) { - const first = arms[index]; - const second = arms[(index + 1) % arms.length]; - const delta = positiveHeadingDelta(first.heading, second.heading); - if (delta < 45 || delta > 135) continue; - const bisector = normalizeHeading(first.heading + delta / 2); - const edges = [first, second].map((arm) => cornerEdgeAt(arm, coreRadius + 6, bisector, center, helpers)); - if (!edges.every(Boolean)) continue; - const apex = rayIntersection(edges[0], edges[1], center); - const apexReach = apex ? directionalProjectionMeters(center, apex, bisector) : null; - // The wedge apex has to sit ahead of the core and inside the arm handoff; - // outside that band the two edges are near parallel and any fillet fitted - // to them would sweep across the carriageways instead of the corner. - if (apexReach === null || apexReach < 1 || apexReach > outerRadius) { - cornerDiagnostics.push(helpers.diagnostic("warning", `junction-cluster:${cluster.id}`, [...nodeIds], "complex-junction-corner-fillet-fallback", "该夹角的道路边缘切线无法安全构造圆角,已保留直角过渡。", center)); - continue; - } - // Tangent distance for a circle of `cornerRadius` inscribed in a wedge of - // opening `delta`, clamped so the tangent points stay on the built arms. - const tangentDistance = Math.min(cornerRadius / Math.tan(delta * Math.PI / 360), Math.max(2, outerRadius - apexReach)); - const tangents = edges.map((edge) => helpers.offsetCoordinate(apex, edge.heading, tangentDistance)); - const curve = quadraticCurve(tangents[0], apex, tangents[1], CORNER_FILLET_SEGMENTS); - const ring = [...curve, center, curve[0]]; - if (!ring.every((point) => point.every(Number.isFinite))) continue; - cornerFills.push({ type: "Feature", properties: { native_id: `complex-corner-fillet:${cluster.id}:${index + 1}`, cluster_id: cluster.id, kind: "complex-corner-fillet", complex_part: "corner-fillet", corner_index: index + 1, from_heading: first.heading, to_heading: second.heading, bisector_heading: bisector, corner_radius_m: cornerRadius, tangent_distance_m: Math.round(tangentDistance * 100) / 100, apex_reach_m: Math.round(apexReach * 100) / 100, provenance: "native-road-complex-junction-corner-fillet/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }); - - // The straight pedestrian strips are trimmed at the cluster boundary, so - // two arms that both carry a footway still meet as two loose ends across - // an empty wedge. Bridge them along the curb the fillet already defines. - // The corner faces clockwise from `first` and counter-clockwise from - // `second`, so each arm must carry the footway on that facing side. - if (!armCarriesSidewalk(first, model, true) || !armCarriesSidewalk(second, model, false)) continue; - const curb = [ - ...edgeRunToRadius(apex, edges[0], tangentDistance, outerRadius + SIDEWALK_CORNER_OVERRUN_METERS, center, helpers).reverse(), - ...curve.slice(1, -1), - ...edgeRunToRadius(apex, edges[1], tangentDistance, outerRadius + SIDEWALK_CORNER_OVERRUN_METERS, center, helpers), - ]; - const outerEdge = offsetPolylineAwayFromCenter(curb, center, SIDEWALK_WIDTH_METERS, helpers); - const sidewalkRing = [...curb, ...outerEdge.slice().reverse(), curb[0]]; - if (!sidewalkRing.every((point) => point.every(Number.isFinite)) || ringSelfIntersects(sidewalkRing)) { - cornerDiagnostics.push(helpers.diagnostic("warning", `junction-cluster:${cluster.id}`, [...nodeIds], "complex-junction-sidewalk-corner-fallback", "该夹角的人行道转角几何自交或无效,已跳过,两侧步行带保持断开。", center)); - continue; - } - sidewalkCorners.push({ type: "Feature", properties: { native_id: `complex-sidewalk-corner:${cluster.id}:${index + 1}`, cluster_id: cluster.id, kind: "complex-sidewalk-corner", corner_index: index + 1, from_heading: first.heading, to_heading: second.heading, bisector_heading: bisector, width_m: SIDEWALK_WIDTH_METERS, overrun_m: SIDEWALK_CORNER_OVERRUN_METERS, provenance: "native-road-complex-junction-sidewalk-corner/v1" }, geometry: { type: "Polygon", coordinates: [sidewalkRing] } }); - } - const corePoints = boundaryParts.flatMap(({ item, inner, innerHalf }) => [helpers.offsetCoordinate(inner, item.heading + 90, innerHalf), helpers.offsetCoordinate(inner, item.heading - 90, innerHalf)]).sort((first, second) => angleAround(center, first) - angleAround(center, second)); - const coreRing = roundedPolygonRing(corePoints, .16); - const features = [{ type: "Feature", properties: { native_id: `complex-junction:${cluster.id}:core`, cluster_id: cluster.id, kind: "complex-core", complex_part: "core", center, radius_m: coreRadius, configured_radius_m: cluster.coreRadiusMeters, approach_count: approaches.length, carriageway_count: carriageways.length, approach_headings: sorted.map((item) => Math.round(item.heading * 10) / 10), corner_rounding_ratio: .16, provenance: "native-road-complex-junction/v6-rounded-core" }, geometry: { type: "Polygon", coordinates: [coreRing] } }]; - for (const { item, outer, inner, outerHalf, innerHalf } of boundaryParts) { - const ring = [helpers.offsetCoordinate(outer, item.heading + 90, outerHalf), helpers.offsetCoordinate(inner, item.heading + 90, innerHalf), helpers.offsetCoordinate(inner, item.heading - 90, innerHalf), helpers.offsetCoordinate(outer, item.heading - 90, outerHalf), helpers.offsetCoordinate(outer, item.heading + 90, outerHalf)]; - features.push({ type: "Feature", properties: { native_id: `complex-junction:${cluster.id}:carriageway:${item.approach.segmentId}`, cluster_id: cluster.id, kind: "complex-approach", complex_part: "carriageway", heading_deg: item.heading, lane_count: item.approach.roadIds.reduce((sum, roadId) => sum + (model.roads.find((road) => road.id === roadId)?.laneCount || 0), 0), width_m: item.approach.widthMeters, provenance: "native-road-complex-junction/v5" }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - // Corner fills come last so they overlay the rectangular carriageway ends - // they are smoothing; they never replace an OSM-derived road surface. - features.push(...cornerFills); - // `coreRadiusMeters` is only consulted when there is no reference geometry. - // Under calibration the radius comes from the reference span, so a configured - // value that silently does nothing has to be reported, not swallowed. - const configuredRadiusIgnored = calibration && Number.isFinite(cluster.coreRadiusMeters) && Math.abs(coreRadius - cluster.coreRadiusMeters) > .5; - const configurationDiagnostics = configuredRadiusIgnored - ? [helpers.diagnostic("info", `junction-cluster:${cluster.id}`, [...nodeIds], "complex-junction-configured-radius-ignored", `已按参考几何校准核心半径为 ${Math.round(coreRadius * 10) / 10} 米,配置的 coreRadiusMeters=${cluster.coreRadiusMeters} 在有参考文件时不生效。`, center)] - : []; - return { features, islands: [...islands, ...sidewalkCorners], crosswalks, stopLines, center, approaches, diagnostics: [...cornerDiagnostics, ...configurationDiagnostics, helpers.diagnostic("info", `junction-cluster:${cluster.id}`, [...nodeIds], calibration ? "complex-junction-reference-calibrated" : "complex-junction-generated", calibration ? `已使用参考几何校准参数后,由 OSM/native 重新生成 ${approaches.length} 个进口、道路面、中央分隔带、斑马线和停止线。` : `已独立生成 ${approaches.length} 个进口、道路面、中央分隔带、斑马线和停止线。`, center)] }; -} - -function readReferenceCalibration(cluster) { - if (!cluster.referenceFile || !fs.existsSync(cluster.referenceFile)) return null; - try { - const converted = convertGeoJson(JSON.parse(fs.readFileSync(cluster.referenceFile, "utf8"))); - const bounds = boundsOf({ features: converted.features.filter((feature) => [1, 2, 3, 4].includes(Number(feature.properties?.type))) }); - const lonScale = 111320 * Math.cos(((bounds.minLat + bounds.maxLat) / 2) * Math.PI / 180); - return { longSpanMeters: (bounds.maxLon - bounds.minLon) * lonScale, shortSpanMeters: (bounds.maxLat - bounds.minLat) * 111320 }; - } catch (_) { - return null; - } -} - -function complexJunctionMetrics(cluster) { - if (metricsCache.has(cluster)) return metricsCache.get(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 - ? Math.max(12, Math.min(24, calibration.shortSpanMeters * .14)) - : Math.max(12, Math.min(80, Number(cluster.coreRadiusMeters) || 28)); - const metrics = { calibration, coreRadius, approachOuterRadius: coreRadius + (Number(cluster.outerRadiusExtraMeters) || 18) }; - metricsCache.set(cluster, metrics); - return metrics; -} - -function normalizeHeading(value) { return ((value + 180) % 360 + 360) % 360 - 180; } - -// `arm.heading` points outward from the junction, so the corner clockwise from -// it sits at heading+90 and the one counter-clockwise at heading-90. A road's -// own sidewalk flags are relative to its digitisation direction, so flip them -// whenever the arm runs against that direction. -function armCarriesSidewalk(arm, model, cornerIsClockwise) { - return arm.members.some((member) => member.approach.roadIds - .map((roadId) => model.roads.find((road) => road.id === roadId)) - .filter(Boolean) - .some((road) => { - const outwardIsForward = String(road.sourceNodeIds[0]) === String(member.nodeId); - const onClockwiseSide = outwardIsForward ? road.sidewalkRight : road.sidewalkLeft; - const onCounterClockwiseSide = outwardIsForward ? road.sidewalkLeft : road.sidewalkRight; - return Boolean(cornerIsClockwise ? onClockwiseSide : onCounterClockwiseSide); - })); -} - -// Walk outward along a wedge edge from its tangent point until the curb reaches -// `targetRadius`, so the corner band overlaps the straight strip it joins. -function edgeRunToRadius(apex, edge, tangentDistance, targetRadius, center, helpers) { - const points = []; - for (let extra = 0; extra <= 40; extra += 2) { - const point = helpers.offsetCoordinate(apex, edge.heading, tangentDistance + extra); - points.push(point); - if (helpers.distanceMeters(point, center) >= targetRadius) break; - } - return points; -} - -// Offset each vertex along the polyline normal that increases distance from the -// junction centre. The curb is star-shaped around that centre, so "farther from -// the centre" is a reliable stand-in for "on the pedestrian side". -function offsetPolylineAwayFromCenter(points, center, meters, helpers) { - return points.map((point, index) => { - const previous = points[Math.max(0, index - 1)]; - const next = points[Math.min(points.length - 1, index + 1)]; - const tangent = previous === next ? 0 : bearing(previous, next); - return [tangent + 90, tangent - 90] - .map((heading) => helpers.offsetCoordinate(point, heading, meters)) - .sort((first, second) => helpers.distanceMeters(second, center) - helpers.distanceMeters(first, center))[0]; - }); -} - -function ringSelfIntersects(ring) { - const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); - const straddles = (p1, p2, p3, p4) => { - const d1 = cross(p3, p4, p1); const d2 = cross(p3, p4, p2); - const d3 = cross(p1, p2, p3); const d4 = cross(p1, p2, p4); - return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)); - }; - for (let first = 0; first < ring.length - 1; first += 1) { - for (let second = first + 2; second < ring.length - 1; second += 1) { - if (first === 0 && second === ring.length - 2) continue; - if (straddles(ring[first], ring[first + 1], ring[second], ring[second + 1])) return true; - } - } - return false; -} - -function angleAround(center, point) { return Math.atan2(point[1] - center[1], point[0] - center[0]); } -function signedLateralMeters(origin, point, heading) { - const east = (point[0] - origin[0]) * 111320 * Math.cos(origin[1] * Math.PI / 180); - const north = (point[1] - origin[1]) * 111320; - const radians = (heading + 90) * Math.PI / 180; - return east * Math.sin(radians) + north * Math.cos(radians); -} -function bearing(first, second) { - const east = (second[0] - first[0]) * Math.cos(first[1] * Math.PI / 180); - const north = second[1] - first[1]; - return Math.atan2(east, north) * 180 / Math.PI; -} -function midpoint(first, second) { return [(first[0] + second[0]) / 2, (first[1] + second[1]) / 2]; } -function averageHeading(headings) { - const vector = headings.reduce((sum, heading) => { - const radians = heading * Math.PI / 180; - return [sum[0] + Math.sin(radians), sum[1] + Math.cos(radians)]; - }, [0, 0]); - return Math.atan2(vector[0], vector[1]) * 180 / Math.PI; -} -function positiveHeadingDelta(first, second) { return ((second - first) % 360 + 360) % 360; } -function pointOnCarriagewayRadius(item, center, radius, helpers) { - const start = item.approach.line[0]; - const startRadius = directionalProjectionMeters(center, start, item.heading); - return helpers.pointAlongLine(item.approach.line, Math.max(0, Math.min(item.length, radius - startRadius))); -} -function armEnvelopeAtRadius(arm, radius, center, helpers) { - if (!arm.members.length) return null; - const centers = arm.members.map((member) => pointOnCarriagewayRadius(member, center, radius, helpers)); - const reference = centers[0]; - let minimum = Infinity; - let maximum = -Infinity; - centers.forEach((point, index) => { - const lateral = signedLateralMeters(reference, point, arm.heading); - const halfWidth = arm.members[index].approach.widthMeters / 2; - minimum = Math.min(minimum, lateral - halfWidth); - maximum = Math.max(maximum, lateral + halfWidth); - }); - if (!Number.isFinite(minimum) || maximum - minimum < 1) return null; - return { center: helpers.offsetCoordinate(reference, arm.heading + 90, (minimum + maximum) / 2), widthMeters: maximum - minimum }; -} -function supportLineIntersection(first, second, origin) { - const lonScale = 111320 * Math.cos(origin[1] * Math.PI / 180); - const toLocal = (point) => [(point[0] - origin[0]) * lonScale, (point[1] - origin[1]) * 111320]; - const firstPoint = toLocal(first.center); - const secondPoint = toLocal(second.center); - const direction = (heading) => { - const radians = heading * Math.PI / 180; - return [Math.sin(radians), Math.cos(radians)]; - }; - const firstDirection = direction(first.supportHeading); - const secondDirection = direction(second.supportHeading); - const denominator = firstDirection[0] * secondDirection[1] - firstDirection[1] * secondDirection[0]; - if (Math.abs(denominator) < 1e-6) return null; - const delta = [secondPoint[0] - firstPoint[0], secondPoint[1] - firstPoint[1]]; - const distanceAlongFirst = (delta[0] * secondDirection[1] - delta[1] * secondDirection[0]) / denominator; - const intersection = [firstPoint[0] + firstDirection[0] * distanceAlongFirst, firstPoint[1] + firstDirection[1] * distanceAlongFirst]; - return [origin[0] + intersection[0] / lonScale, origin[1] + intersection[1] / 111320]; -} -function limitedCornerPair(first, second, radius, bisector, center, maxWidth, helpers) { - const pair = [cornerEdgeAtRadius(first, radius, bisector, center, helpers), cornerEdgeAtRadius(second, radius, bisector, center, helpers)]; - if (!pair.every(Boolean)) return null; - const width = helpers.distanceMeters(pair[0], pair[1]); - const middle = midpoint(pair[0], pair[1]); - const halfWidth = Math.max(.4, Math.min(width, maxWidth) / 2); - const acrossHeading = width > .1 ? bearing(pair[0], pair[1]) : bisector + 90; - return [helpers.offsetCoordinate(middle, acrossHeading + 180, halfWidth), helpers.offsetCoordinate(middle, acrossHeading, halfWidth)]; -} -function cornerEdgeAtRadius(arm, radius, bisector, center, helpers) { - return cornerEdgeAt(arm, radius, bisector, center, helpers)?.point || null; -} -function cornerEdgeAt(arm, radius, bisector, center, helpers) { - const candidates = arm.members.flatMap((member) => { - const point = pointOnCarriagewayRadius(member, center, radius, helpers); - const halfWidth = member.approach.widthMeters / 2; - return [90, -90].map((side) => ({ point: helpers.offsetCoordinate(point, member.heading + side, halfWidth), heading: member.heading })); - }); - return candidates.sort((first, second) => directionalProjectionMeters(center, second.point, bisector) - directionalProjectionMeters(center, first.point, bisector))[0] || null; -} -function rayIntersection(first, second, origin) { - const lonScale = 111320 * Math.cos(origin[1] * Math.PI / 180); - const toLocal = (point) => [(point[0] - origin[0]) * lonScale, (point[1] - origin[1]) * 111320]; - const direction = (heading) => { - const radians = heading * Math.PI / 180; - return [Math.sin(radians), Math.cos(radians)]; - }; - const firstPoint = toLocal(first.point); - const secondPoint = toLocal(second.point); - const firstDirection = direction(first.heading); - const secondDirection = direction(second.heading); - const denominator = firstDirection[0] * secondDirection[1] - firstDirection[1] * secondDirection[0]; - if (Math.abs(denominator) < 1e-4) return null; - const delta = [secondPoint[0] - firstPoint[0], secondPoint[1] - firstPoint[1]]; - const distanceAlongFirst = (delta[0] * secondDirection[1] - delta[1] * secondDirection[0]) / denominator; - const local = [firstPoint[0] + firstDirection[0] * distanceAlongFirst, firstPoint[1] + firstDirection[1] * distanceAlongFirst]; - if (!local.every(Number.isFinite)) return null; - return [origin[0] + local[0] / lonScale, origin[1] + local[1] / 111320]; -} -function quadraticCurve(start, control, end, segments) { - const result = []; - for (let index = 0; index <= segments; index += 1) { - const t = index / segments; - const u = 1 - t; - result.push([u * u * start[0] + 2 * u * t * control[0] + t * t * end[0], u * u * start[1] + 2 * u * t * control[1] + t * t * end[1]]); - } - return result; -} -function directionalProjectionMeters(origin, point, heading) { - const east = (point[0] - origin[0]) * 111320 * Math.cos(origin[1] * Math.PI / 180); - const north = (point[1] - origin[1]) * 111320; - const radians = heading * Math.PI / 180; - return east * Math.sin(radians) + north * Math.cos(radians); -} -function smoothClosedRing(vertices) { - // Curb-edge candidates can arrive in opposite winding orders when an OSM - // carriageway bends slightly. Sort this local corner only around its own - // centroid before rounding, avoiding a self-crossing safety island while - // keeping the global junction boundary fully OSM-driven. - const centroid = vertices.reduce((sum, point) => [sum[0] + point[0] / vertices.length, sum[1] + point[1] / vertices.length], [0, 0]); - const ordered = [...vertices].sort((first, second) => Math.atan2(first[1] - centroid[1], first[0] - centroid[0]) - Math.atan2(second[1] - centroid[1], second[0] - centroid[0])); - const points = ordered.flatMap((point, index) => { - const next = ordered[(index + 1) % ordered.length]; - return [interpolateCoordinate(point, next, .18), interpolateCoordinate(point, next, .82)]; - }); - return [...points, points[0]]; -} -function roundedPolygonRing(vertices, ratio) { - const points = vertices.flatMap((point, index) => { - const previous = vertices[(index - 1 + vertices.length) % vertices.length]; - const next = vertices[(index + 1) % vertices.length]; - return [interpolateCoordinate(previous, point, 1 - ratio), interpolateCoordinate(point, next, ratio)]; - }); - return [...points, points[0]]; -} -function interpolateCoordinate(first, second, ratio) { return [first[0] + (second[0] - first[0]) * ratio, first[1] + (second[1] - first[1]) * ratio]; } - -module.exports = { buildComplexJunctionGeometry, complexJunctionMetrics }; diff --git a/packages/road-compiler/src/compile/native-road.js b/packages/road-compiler/src/compile/native-road.js deleted file mode 100644 index 692275a..0000000 --- a/packages/road-compiler/src/compile/native-road.js +++ /dev/null @@ -1,1695 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const { arrowRingsAt, normalizeManeuver } = require("./turn-lane-arrows"); -const { buildComplexJunctionGeometry, complexJunctionMetrics } = require("./complex-junction"); - -const OVERRIDE_SCHEMA = "native-road-overrides/v1"; -const MOTOR_HIGHWAYS = new Set(["motorway", "trunk", "primary", "secondary", "tertiary", "unclassified", "residential", "living_street", "service"]); -const DEFAULT_WIDTHS = { motorway: 12, trunk: 10, primary: 10, secondary: 8, tertiary: 7, unclassified: 6, residential: 6, living_street: 5, service: 4 }; -const DEFAULT_SIDEWALK_WIDTH_METERS = 2; -const DIRECTION_ARROW_INTERVAL_METERS = 32; -const DIRECTION_ARROW_ENDPOINT_BUFFER_METERS = 14; -const STOP_LINE_OFFSET_METERS = 2.7; -const STOP_LINE_MAX_APPROACH_DISTANCE_METERS = 25; -const CROSSWALK_JUNCTION_INSET_METERS = 1.5; -const CROSSWALK_MAX_JUNCTION_INSET_METERS = 4; -const CENTER_LINE_DASH_LENGTH_METERS = 2; -const CENTER_LINE_DASH_GAP_METERS = 2; -const CENTER_LINE_WIDTH_METERS = .25; -const CENTER_LINE_SOLID_OVERLAP_METERS = .04; -const CENTER_LINE_CONTROL_CLEARANCE_METERS = 1; -const CENTER_LINE_COLORS = new Set(["yellow", "white"]); -const CENTER_LINE_PATTERNS = new Set(["dashed", "solid"]); -const CONNECTOR_BOUNDARY_TOLERANCE_METERS = .05; -// A lane centerline is drawn as a hairline, so probe the crossing with a narrow -// band. Using the full lane width would clip the line metres early. -const LANE_CENTERLINE_PROBE_WIDTH_METERS = .12; -const JUNCTION_CURVE_SEGMENTS = 8; - -function parseOsmRoads(xml) { - const nodes = new Map(); - const crossingNodes = []; - for (const match of xml.matchAll(/]*?)(?:\/>|>([\s\S]*?)<\/node>)/g)) { - const attrs = xmlAttrs(match[1]); - if (attrs.action === "delete" || !attrs.id || attrs.lon === undefined || attrs.lat === undefined) continue; - const coordinate = [Number(attrs.lon), Number(attrs.lat)]; - if (!coordinate.every(Number.isFinite)) continue; - const id = String(attrs.id); const tags = parseTags(match[2] || ""); - nodes.set(id, coordinate); - if (tags.highway === "crossing" && !["no", "none", "unmarked"].includes(tags["crossing:markings"])) crossingNodes.push({ id, coordinate, tags }); - } - const ways = []; - for (const match of xml.matchAll(/]*)>([\s\S]*?)<\/way>/g)) { - const attrs = xmlAttrs(match[1]); - const body = match[2]; - const tags = parseTags(body); - if (attrs.action === "delete" || !MOTOR_HIGHWAYS.has(tags.highway || "")) continue; - const refs = [...body.matchAll(/]*)\/?\s*>/g)].map((item) => xmlAttrs(item[1]).ref).filter(Boolean); - const coords = refs.map((ref) => nodes.get(String(ref))).filter(Boolean); - if (coords.length < 2 || coords.length !== refs.length) continue; - ways.push({ id: String(attrs.id), refs: refs.map(String), coords, tags }); - } - return { nodes, ways, crossingNodes }; -} - -function compileRoadModel(xml, overrides) { - const parsed = parseOsmRoads(xml); - const diagnostics = []; - const roads = []; - const endpoints = []; - const byNode = new Map(); - const sharedNodeWayIds = new Map(); - for (const way of parsed.ways) for (const nodeId of new Set(way.refs)) { - if (!sharedNodeWayIds.has(nodeId)) sharedNodeWayIds.set(nodeId, new Set()); - sharedNodeWayIds.get(nodeId).add(way.id); - } - for (const sourceWay of parsed.ways) { - const segments = splitWayAtSharedNodes(sourceWay, sharedNodeWayIds); - for (const way of segments) { - const directions = way.tags.oneway === "yes" || way.tags.oneway === "1" || way.tags.junction === "roundabout" ? ["forward"] : ["forward", "backward"]; - for (const direction of directions) { - const base = roadAttributes(way.tags, direction); - const id = `road:way/${way.id}${way.segmentIndex === null ? "" : `:segment/${way.segmentIndex}`}:${direction}`; - const road = { id, osmWayIds: [way.id], segmentId: `segment:way/${way.id}/${way.segmentIndex ?? 0}`, sourceRoadId: `road:way/${way.id}:${direction}`, direction, highway: way.tags.highway, centerline: direction === "forward" ? way.coords : [...way.coords].reverse(), sourceNodeIds: direction === "forward" ? [way.refs[0], way.refs.at(-1)] : [way.refs.at(-1), way.refs[0]], tags: way.tags, ...base, appliedOverrideIds: [], diagnostics: [] }; - applyRoadOverrides(road, overrides, diagnostics); - roads.push(road); - for (const side of ["start", "end"]) { - const nodeId = side === "start" ? road.sourceNodeIds[0] : road.sourceNodeIds[1]; - const endpoint = { id: `endpoint:${road.id}:${side}`, roadId: id, side, nodeId, coordinate: side === "start" ? road.centerline[0] : road.centerline.at(-1), direction }; - endpoints.push(endpoint); - if (!byNode.has(nodeId)) byNode.set(nodeId, []); - byNode.get(nodeId).push(endpoint); - } - } - } - } - const connections = resolveConnections(endpoints, byNode, overrides, diagnostics); - const extent = roadExtent(roads); - for (const [nodeId, items] of byNode) { - if (items.length === 1 && distanceToExtentEdgeMeters(items[0].coordinate, extent) > 25) { - const endpoint = items[0]; - diagnostics.push({ ...diagnostic("warning", endpoint.roadId, [nodeId], "unconnected-interior-road-end", "道路在区域内部结束,未连接到其他机动车道路。请确认这是实际断头,还是 OSM 节点尚未连接。", endpoint.coordinate), endpointId: endpoint.id, manualCandidates: nearbyManualCandidates(endpoints, endpoint) }); - } - } - const crossings = parsed.crossingNodes.map((crossing) => ({ ...crossing, osmWayIds: parsed.ways.filter((way) => way.refs.includes(crossing.id)).map((way) => way.id) })); - return { schema: "native-road-model/v1", roads, endpoints, connections, crossings, diagnostics }; -} - -function splitWayAtSharedNodes(way, sharedNodeWayIds) { - const splitIndexes = [0]; - for (let index = 1; index < way.refs.length - 1; index += 1) if ((sharedNodeWayIds.get(way.refs[index])?.size || 0) > 1) splitIndexes.push(index); - splitIndexes.push(way.refs.length - 1); - if (splitIndexes.length === 2) return [{ ...way, segmentIndex: null }]; - return splitIndexes.slice(1).map((end, index) => { - const start = splitIndexes[index]; - return { ...way, refs: way.refs.slice(start, end + 1), coords: way.coords.slice(start, end + 1), segmentIndex: index + 1 }; - }); -} - -function roadExtent(roads) { - const points = roads.flatMap((road) => road.centerline); - return { minLon: Math.min(...points.map((point) => point[0])), maxLon: Math.max(...points.map((point) => point[0])), minLat: Math.min(...points.map((point) => point[1])), maxLat: Math.max(...points.map((point) => point[1])) }; -} - -function distanceToExtentEdgeMeters(point, extent) { - const lonScale = 111320 * Math.cos(point[1] * Math.PI / 180); - return Math.min((point[0] - extent.minLon) * lonScale, (extent.maxLon - point[0]) * lonScale, (point[1] - extent.minLat) * 111320, (extent.maxLat - point[1]) * 111320); -} - -function roadAttributes(tags, direction) { - const directional = direction === "forward" ? "forward" : "backward"; - const laneTag = tags[`lanes:${directional}`] ?? (tags.oneway === "yes" ? tags.lanes : null); - const parsedLanes = positiveInteger(laneTag); - const totalLanes = positiveInteger(tags.lanes); - const lanes = parsedLanes || (totalLanes ? Math.max(1, Math.ceil(totalLanes / (tags.oneway === "yes" ? 1 : 2))) : 1); - const parsedWidth = positiveNumber(tags.width); - const forwardLanes = positiveInteger(tags["lanes:forward"]); - const backwardLanes = positiveInteger(tags["lanes:backward"]); - const directionalLaneTotal = forwardLanes && backwardLanes ? forwardLanes + backwardLanes : totalLanes; - // `width` describes the whole OSM way. A directional road receives its lane - // share; absent width falls back to a realistic per-lane carriageway width. - const width = parsedWidth ? parsedWidth * lanes / (directionalLaneTotal || (tags.oneway === "yes" ? lanes : lanes * 2)) : lanes * 3.25; - return { - laneCount: lanes, - widthMeters: width, - sidewalkLeft: sidewalkState(tags, direction, "left"), - sidewalkRight: sidewalkState(tags, direction, "right"), - provenance: { - laneCount: parsedLanes || totalLanes ? `tag:${parsedLanes ? `lanes:${directional}` : "lanes"}` : "inferred:default-lanes", - widthMeters: parsedWidth ? "tag:width (按方向车道数分配)" : "inferred:3.25m-per-lane", - }, - }; -} - -function sidewalkState(tags, direction, side) { - const osmSide = direction === "forward" ? side : side === "left" ? "right" : "left"; - const value = tags[`sidewalk:${osmSide}`] ?? tags.sidewalk; - return value === "both" || value === "yes" || value === osmSide; -} - -function loadOverrides(file) { - if (!fs.existsSync(file)) return { schema: OVERRIDE_SCHEMA, overrides: [] }; - return validateOverrides(JSON.parse(fs.readFileSync(file, "utf8"))); -} - -// 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}.`); - const ids = new Set(); - const roadIds = model ? new Set(model.roads.flatMap((road) => [road.id, road.sourceRoadId])) : null; - const directionalRoadIds = model ? new Set(model.roads.map((road) => road.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 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) { - 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); - 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 (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 ["sidewalkLeft", "sidewalkRight"]) if (item[key] !== undefined && typeof item[key] !== "boolean") throw new Error(`Invalid road override ${key}.`); - } else if (item.kind === "junction-connection") { - if (typeof item.fromEndpointId !== "string" || typeof item.toEndpointId !== "string" || typeof item.enabled !== "boolean" || (endpointIds && (!endpointIds.has(item.fromEndpointId) || !endpointIds.has(item.toEndpointId)))) throw new Error("Invalid junction connection override."); - if (model && !connectionEndpointsCompatible(model, item.fromEndpointId, item.toEndpointId)) throw new Error("A manual junction connection must go from a road end to a nearby road start (within 35m)."); - } else if (item.kind === "lane-connection") { - if (typeof item.fromLaneId !== "string" || typeof item.toLaneId !== "string" || typeof item.enabled !== "boolean" || (laneIds && (!laneIds.has(item.fromLaneId) || !laneIds.has(item.toLaneId)))) throw new Error("Invalid lane connection override."); - } else if (item.kind === "center-line-style") { - if (typeof item.segmentId !== "string" || !CENTER_LINE_COLORS.has(item.color) || !CENTER_LINE_PATTERNS.has(item.pattern) || (item.double !== undefined && typeof item.double !== "boolean") || (item.double && (item.color !== "yellow" || item.pattern !== "solid")) || (segmentIds && !segmentIds.has(item.segmentId))) throw new Error("Invalid center line style override."); - } else if (item.kind === "lane-separator-style") { - if (typeof item.roadId !== "string" || (roadIds && !roadIds.has(item.roadId)) || !Number.isInteger(item.leftLaneIndex) || item.rightLaneIndex !== item.leftLaneIndex + 1 || !CENTER_LINE_COLORS.has(item.color) || !CENTER_LINE_PATTERNS.has(item.pattern)) throw new Error("Invalid lane separator style override."); - } 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."); - } else throw new Error(`Unsupported override kind: ${item.kind}`); - kept.push(item); - } - return { schema: OVERRIDE_SCHEMA, overrides: kept, stale }; -} - -function applyRoadOverrides(road, overrides, diagnostics) { - const matching = overrides.overrides.filter((entry) => entry.kind === "road" && (entry.roadId === road.sourceRoadId || entry.roadId === road.id)); - // A legacy whole-way edit remains the baseline; a segment-specific edit can - // deliberately refine it after the compiler has introduced split segments. - matching.sort((first, second) => Number(first.roadId === road.id) - Number(second.roadId === road.id)); - for (const item of matching) { - for (const key of ["widthMeters", "laneCount", "sidewalkLeft", "sidewalkRight"]) if (item[key] !== undefined) road[key] = item[key]; - road.appliedOverrideIds.push(item.id); - for (const key of ["widthMeters", "laneCount"]) if (item[key] !== undefined) road.provenance[key] = `override:${item.id}`; - } - if (road.widthMeters < road.laneCount * 2.4) diagnostics.push(diagnostic("warning", road.id, road.osmWayIds, "narrow-lane-width", "Configured road width is narrow for the selected lane count.", road.centerline[0])); -} - -function resolveConnections(endpoints, byNode, overrides, diagnostics) { - const result = []; - for (const [nodeId, items] of byNode) { - const arrivals = items.filter((endpoint) => endpoint.side === "end"); - const departures = items.filter((endpoint) => endpoint.side === "start"); - for (const arrival of arrivals) for (const departure of departures) { - if (arrival.roadId === departure.roadId) continue; - const arrivalRoad = endpoints.find((endpoint) => endpoint.id === arrival.id)?.roadId; - const departureRoad = endpoints.find((endpoint) => endpoint.id === departure.id)?.roadId; - if (sameOsmWay(endpoints, arrivalRoad, departureRoad)) continue; - const override = overrides.overrides.find((entry) => entry.kind === "junction-connection" && entry.fromEndpointId === arrival.id && entry.toEndpointId === departure.id); - result.push({ id: `connection:${arrival.id}:${departure.id}`, nodeId, fromEndpointId: arrival.id, toEndpointId: departure.id, enabled: override ? override.enabled : true, provenance: override ? `override:${override.id}` : "osm:shared-node" }); - } - if (items.length > 8) diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "complex-junction", "Junction has more than eight directional endpoints and is not compiled as an ordinary junction.", items[0].coordinate)); - } - // Overrides can add a deliberate movement omitted by the initial inference. - // Keep it only when both endpoints still belong to the same OSM junction. - for (const override of overrides.overrides.filter((item) => item.kind === "junction-connection")) { - const exists = result.some((connection) => connection.fromEndpointId === override.fromEndpointId && connection.toEndpointId === override.toEndpointId); - if (exists) continue; - const from = endpoints.find((endpoint) => endpoint.id === override.fromEndpointId); - const to = endpoints.find((endpoint) => endpoint.id === override.toEndpointId); - if (!from || !to || !connectionEndpointsCompatible({ endpoints }, from.id, to.id)) continue; - result.push({ id: `connection:${from.id}:${to.id}`, nodeId: from.nodeId, fromEndpointId: from.id, toEndpointId: to.id, enabled: override.enabled, provenance: `override:${override.id}` }); - } - return result; -} - -function endpointNode(model, endpointId) { return model.endpoints.find((endpoint) => endpoint.id === endpointId)?.nodeId; } -function sameOsmWay(endpoints, firstRoadId, secondRoadId) { - const roadFor = (roadId) => endpoints.find((endpoint) => endpoint.roadId === roadId)?.roadId; - const segmentId = (roadId) => roadId.replace(/:(forward|backward)$/, ""); - return segmentId(roadFor(firstRoadId) || firstRoadId) === segmentId(roadFor(secondRoadId) || secondRoadId); -} -function connectionEndpointsCompatible(model, fromId, toId) { - const from = model.endpoints.find((endpoint) => endpoint.id === fromId); - const to = model.endpoints.find((endpoint) => endpoint.id === toId); - if (!from || !to || from.roadId === to.roadId || sameOsmWay(model.endpoints, from.roadId, to.roadId) || from.side !== "end" || to.side !== "start") return false; - if (from.nodeId === to.nodeId) return true; - const dx = (from.coordinate[0] - to.coordinate[0]) * 111320 * Math.cos(from.coordinate[1] * Math.PI / 180); - const dy = (from.coordinate[1] - to.coordinate[1]) * 111320; - return Math.hypot(dx, dy) <= 35; -} - -function nearbyManualCandidates(endpoints, from) { - return endpoints.filter((to) => to.side === "start" && to.roadId !== from.roadId && !sameOsmWay(endpoints, from.roadId, to.roadId)).map((to) => ({ to, distanceMeters: distanceMeters(from.coordinate, to.coordinate) })).filter((item) => item.distanceMeters <= 35).sort((a, b) => a.distanceMeters - b.distanceMeters).slice(0, 3).map(({ to, distanceMeters: meters }) => ({ toEndpointId: to.id, roadId: to.roadId, distanceMeters: Math.round(meters * 10) / 10 })); -} - -// A single physical intersection is often mapped as several nodes joined by -// short links: a dual carriageway crossing, a slip lane, a staggered junction. -// Each node then compiles its own surface and the shared area between them is -// left as ordinary road, which is what produces the width jumps and stray -// medians around those nodes. Report the clusters so they can be configured as -// `complex-junction-v1`. Detection is advisory only — it never enables a -// template or changes geometry, because flipping a junction between the -// ordinary and complex paths silently on an OSM edit would be unpredictable. -const COMPLEX_CANDIDATE_MAX_LINK_METERS = 30; -const COMPLEX_CANDIDATE_MIN_NODES = 2; -// Short links chain transitively, so a run of closely spaced junctions along -// one street unions into a single 'cluster' that is really a corridor. A real -// intersection stays compact, so bound the cluster by its own diameter: the -// surveyed 珠山湖大道 cluster spans 24.6 m on its four mapped nodes and 40.7 m -// once the neighbouring service-road junction is folded in. -const COMPLEX_CANDIDATE_MAX_DIAMETER_METERS = 45; - -function detectComplexJunctionCandidates(model, junctionPlans, options, diagnostics) { - const configured = new Set((options.junctionTemplates?.clusters || []).flatMap((cluster) => (cluster.nodeIds || []).map(String))); - const segmentsByNode = new Map(); - for (const endpoint of model.endpoints) { - const key = String(endpoint.nodeId); - if (!segmentsByNode.has(key)) segmentsByNode.set(key, new Set()); - segmentsByNode.get(key).add(endpoint.roadId.replace(/:(forward|backward)$/, "")); - } - const junctionNodes = new Set([...segmentsByNode].filter(([, segments]) => segments.size >= 3).map(([nodeId]) => nodeId)); - const parent = new Map(); - const find = (id) => { - if (!parent.has(id)) parent.set(id, id); - while (parent.get(id) !== id) { parent.set(id, parent.get(parent.get(id))); id = parent.get(id); } - return id; - }; - const union = (first, second) => { const a = find(first); const b = find(second); if (a !== b) parent.set(a, b); }; - const links = new Map(); - const seenSegments = new Set(); - for (const road of model.roads) { - if (seenSegments.has(road.segmentId)) continue; - seenSegments.add(road.segmentId); - const start = String(road.sourceNodeIds[0]); const end = String(road.sourceNodeIds.at(-1)); - if (start === end || !junctionNodes.has(start) || !junctionNodes.has(end)) continue; - const length = lineLengthMeters(road.centerline); - if (length > COMPLEX_CANDIDATE_MAX_LINK_METERS) continue; - union(start, end); - links.set(road.segmentId, { start, end, length }); - } - const clusters = new Map(); - for (const nodeId of parent.keys()) { - const root = find(nodeId); - if (!clusters.has(root)) clusters.set(root, []); - clusters.get(root).push(nodeId); - } - for (const nodeIds of clusters.values()) { - if (nodeIds.length < COMPLEX_CANDIDATE_MIN_NODES) continue; - if (nodeIds.some((nodeId) => configured.has(nodeId))) continue; - const points = nodeIds.map((nodeId) => junctionPlans.get(nodeId)?.node).filter(Boolean); - if (points.length !== nodeIds.length) continue; - const center = points.reduce((sum, point) => [sum[0] + point[0] / points.length, sum[1] + point[1] / points.length], [0, 0]); - const spreadMeters = Math.max(...points.map((point) => distanceMeters(center, point))); - let diameterMeters = 0; - for (let first = 0; first < points.length; first += 1) { - for (let second = first + 1; second < points.length; second += 1) diameterMeters = Math.max(diameterMeters, distanceMeters(points[first], points[second])); - } - if (diameterMeters > COMPLEX_CANDIDATE_MAX_DIAMETER_METERS) continue; - const inner = [...links.values()].filter((link) => nodeIds.includes(link.start) && nodeIds.includes(link.end)); - const widths = nodeIds.flatMap((nodeId) => (junctionPlans.get(nodeId)?.approaches || []).map((approach) => approach.widthMeters)); - const widestApproach = widths.length ? Math.max(...widths) : 0; - // Enough core to cover every member node plus the widest approach's half - // width, with a little slack. A starting point for tuning, not a result. - const suggestedCoreRadius = Math.min(80, Math.max(12, Math.round(spreadMeters + widestApproach / 2 + 4))); - diagnostics.push({ - ...diagnostic("info", `junction-cluster-candidate:${nodeIds.slice().sort().join("+")}`, nodeIds, "complex-junction-candidate", - `检测到 ${nodeIds.length} 个路口节点由 ${inner.length} 条短路段(最长 ${Math.round(Math.max(...inner.map((link) => link.length)) * 10) / 10} 米)相连,可能是同一个物理路口。当前按独立路口编译;如需合并请在 nativeRoad.junctionTemplates.clusters 中配置。`, - center), - suggestedCluster: { - template: "complex-junction-v1", - nodeIds: nodeIds.slice().sort(), - nodeCount: nodeIds.length, - spreadMeters: Math.round(spreadMeters * 10) / 10, - diameterMeters: Math.round(diameterMeters * 10) / 10, - longestLinkMeters: Math.round(Math.max(...inner.map((link) => link.length)) * 10) / 10, - widestApproachMeters: Math.round(widestApproach * 100) / 100, - coreRadiusMeters: suggestedCoreRadius, - }, - }); - } -} - -function compileGeometry(model, overrides = { overrides: [] }, options = {}) { - const diagnostics = [...model.diagnostics]; - const junctionPlans = compileJunctionPlans(model, options, diagnostics); - detectComplexJunctionCandidates(model, junctionPlans, options, diagnostics); - const features = []; - const activeClusters = options.junctionTemplates?.enabled ? (options.junctionTemplates.clusters || []) : []; - const clusterByNode = new Map(activeClusters.flatMap((cluster) => cluster.nodeIds.map((nodeId) => [String(nodeId), cluster]))); - const complexClusterCenters = new Map(activeClusters.filter((cluster) => cluster.template === "complex-junction-v1").map((cluster) => { - const points = cluster.nodeIds.map((nodeId) => junctionPlans.get(String(nodeId))?.node).filter(Boolean); - const center = points.length ? points.reduce((sum, point) => [sum[0] + point[0] / points.length, sum[1] + point[1] / points.length], [0, 0]) : null; - return [cluster.id, center]; - })); - const emittedSegments = new Set(); - const generatedComplexSidewalks = []; - const generatedComplexCrosswalks = []; - const generatedComplexStopLines = []; - for (const road of model.roads) { - const segmentKey = road.segmentId; - if (emittedSegments.has(segmentKey)) continue; - emittedSegments.add(segmentKey); - const directions = model.roads.filter((item) => item.segmentId === segmentKey); - const startCluster = clusterByNode.get(String(road.sourceNodeIds[0])); - const endCluster = clusterByNode.get(String(road.sourceNodeIds.at(-1))); - if (startCluster?.template === "complex-junction-v1" && endCluster?.template === "complex-junction-v1" && startCluster.id === endCluster.id) continue; - const totalWidth = directions.reduce((sum, item) => sum + item.widthMeters, 0); - // The approach surface stops at the junction cutback. The junction layer - // owns the intervening rounded corners; leaving approaches untrimmed - // would cover that outline with rectangular road ends in Blender/Cesium. - const cluster = clusterByNode.get(String(road.sourceNodeIds[0])) || clusterByNode.get(String(road.sourceNodeIds.at(-1))); - if (cluster?.template === "complex-junction-v1") { - const center = complexClusterCenters.get(cluster.id); - const length = lineLengthMeters(road.centerline); - const farEndpoint = cluster.nodeIds.map(String).includes(String(road.sourceNodeIds[0])) ? road.centerline.at(-1) : road.centerline[0]; - const outerRadius = complexJunctionMetrics(cluster).approachOuterRadius; - if (center && length < outerRadius + 8 && distanceMeters(farEndpoint, center) < outerRadius) continue; - } - const line = cluster?.template === "complex-junction-v1" - ? trimLineAtComplexCluster(road.centerline, road.sourceNodeIds, junctionPlans, cluster, complexClusterCenters.get(cluster.id)) - : trimLineAtJunctions(road.centerline, road.sourceNodeIds, junctionPlans); - const ring = roadRing(line, totalWidth); - if (!ring) { diagnostics.push(diagnostic("error", road.id, road.osmWayIds, "unclosed-road-surface", "Could not construct a valid road polygon from this centerline.", road.centerline[0])); continue; } - const surfaceId = road.segmentId.endsWith("/0") ? `surface:way/${road.osmWayIds.join(",")}` : `surface:${segmentKey}`; - features.push({ type: "Feature", properties: { native_id: surfaceId, cluster_id: cluster?.template === "complex-junction-v1" ? cluster.id : null, directional_road_ids: directions.map((item) => item.id).join(","), osm_way_ids: road.osmWayIds.join(","), source_road_id: road.sourceRoadId, width_m: totalWidth, lane_count: directions.reduce((sum, item) => item.laneCount + sum, 0), provenance: JSON.stringify(directions.map((item) => item.provenance)), override_ids: directions.flatMap((item) => item.appliedOverrideIds).join(",") }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - for (const [nodeId, plan] of junctionPlans) { - if (plan.clusterId && activeComplexCluster(options, plan.clusterId)) continue; - if (!plan.template) continue; - for (const approach of plan.approaches) { - const transition = templateApproachRing(approach, plan); - if (!transition) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-approach-fallback", "进口道路长度不足以生成规整过渡面,已保留该进口的 native 直筒道路。", plan.node)); - continue; - } - features.push({ type: "Feature", properties: { native_id: `junction-approach:${plan.template}:node/${nodeId}:${approach.segmentId}`, osm_node_id: nodeId, segment_id: approach.segmentId, directional_road_ids: approach.roadIds.join(","), width_m: approach.widthMeters, approach_width_m: Math.round(approach.widthMeters * plan.approachWidthMultiplier * 10) / 10, approach_length_m: Math.round(transition.lengthMeters * 10) / 10, template: plan.template, provenance: "native-road-junction-approach-template/v1" }, geometry: { type: "Polygon", coordinates: [transition.ring] } }); - } - } - for (const cluster of activeClusters) { - if (cluster.template !== "complex-junction-v1") continue; - const generated = buildComplexJunctionGeometry(model, cluster, { junctionPlans, diagnostic, distanceMeters, lineLengthMeters, pointAlongLine, offsetCoordinate, headingAtEndpoint, headingVector, circleRing }); - features.push(...generated.features); - if (generated.crosswalks) generatedComplexCrosswalks.push(...generated.crosswalks); - if (generated.stopLines) generatedComplexStopLines.push(...generated.stopLines); - if (generated.islands) generatedComplexSidewalks.push(...generated.islands); - diagnostics.push(...generated.diagnostics); - } - const lanes = compileLaneCenterlines(model, diagnostics, junctionPlans, options, { crosswalks: generatedComplexCrosswalks, stopLines: generatedComplexStopLines }); - const edgeLines = options.edgeLines === false ? [] : compileEdgeLines(model, overrides, junctionPlans, options, features); - const controls = compileControlMarkings(model, lanes, diagnostics, junctionPlans); - const allControls = { crosswalks: [...controls.crosswalks, ...generatedComplexCrosswalks], stopLines: [...controls.stopLines, ...generatedComplexStopLines] }; - const centerLines = compileCenterLines(model, overrides, junctionPlans, allControls, diagnostics, options); - const markings = compileLaneMarkings(model, overrides, lanes, diagnostics, junctionPlans, allControls, options); - markings.separators.push(...compileComplexLaneSeparators(lanes.features, [...allControls.crosswalks, ...allControls.stopLines])); - markings.directionArrows.push(...compileComplexPreviewArrows(lanes.features, generatedComplexStopLines)); - const sidewalks = compileSidewalkSurfaces(model, diagnostics, junctionPlans, options); - // Complex crosswalks are generated from native approach tangents; do not - // synthesize side strips that can be mistaken for crosswalks. - sidewalks.push(...generatedComplexSidewalks); - const connectorResult = compileConnectors(model, lanes, diagnostics, overrides, junctionPlans); - const junctionFeatures = compileJunctionSurfaces(model, junctionPlans, connectorResult.features, connectorResult.movements, diagnostics, options); - validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics); - return { roadSurface: { type: "FeatureCollection", features }, edgeLines: { type: "FeatureCollection", features: edgeLines }, 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, ...generatedComplexCrosswalks] }, vehicleStopLines: { type: "FeatureCollection", features: [...controls.stopLines, ...generatedComplexStopLines] }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics }; -} - -function compileComplexPreviewArrows(features, stopLines) { - const result = []; - for (const feature of features) { - if (!feature.properties?.cluster_preview || !feature.properties.incoming || feature.properties.maneuver === "outbound") continue; - const line = feature.geometry?.coordinates || []; - if (line.length < 2) continue; - const stopLine = stopLines.find((candidate) => candidate.properties?.road_id === feature.properties.road_id); - if (!stopLine) continue; - const stopRing = stopLine.geometry?.coordinates?.[0]; - if (!stopRing || stopRing.length < 4) continue; - const stopPoints = stopRing.slice(0, -1); - const stopCenter = [stopPoints.reduce((sum, point) => sum + point[0], 0) / stopPoints.length, stopPoints.reduce((sum, point) => sum + point[1], 0) / stopPoints.length]; - const placement = distanceMeters(line.at(-1), stopCenter) + 8; - const placementInfo = pointAndAxisAlongLine(line, Math.max(0, lineLengthMeters(line) - placement)); - if (!placementInfo) continue; - const rings = arrowRingsAt(feature.properties.maneuver, placementInfo.point, placementInfo.axis); - for (let part = 0; part < rings.length; part += 1) result.push({ - type: "Feature", - properties: { native_id: `${feature.properties.native_id}:arrow:${part}`, road_id: feature.properties.road_id, lane_id: feature.properties.native_id, cluster_id: feature.properties.cluster_id, cluster_preview: true, maneuver: feature.properties.maneuver, travel_heading_deg: headingDegrees(line[0], line.at(-1)), placement_distance_from_stop_meters: 8, provenance: "native-road-complex-preview-arrow/v2-stop-anchored" }, - geometry: { type: "Polygon", coordinates: [rings[part]] }, - }); - } - return result; -} - -function compileComplexLaneSeparators(features, controls = []) { - const groups = new Map(); - for (const feature of features) { - if (!feature.properties?.cluster_preview || !feature.properties.road_id) continue; - if (!groups.has(feature.properties.road_id)) groups.set(feature.properties.road_id, []); - groups.get(feature.properties.road_id).push(feature); - } - const result = []; - for (const [roadId, lanes] of groups) { - lanes.sort((first, second) => first.properties.lane_index - second.properties.lane_index); - for (let index = 1; index < lanes.length; index += 1) { - const left = lanes[index - 1].geometry.coordinates; const right = lanes[index].geometry.coordinates; - if (left.length !== right.length) continue; - const line = left.map((point, pointIndex) => [(point[0] + right[pointIndex][0]) / 2, (point[1] + right[pointIndex][1]) / 2]); - const visibleLine = trimLineBeforeFirstControl(line, controls, .12); - const ring = visibleLine ? roadRing(visibleLine, .12) : null; - if (!ring) continue; - result.push({ type: "Feature", properties: { native_id: `complex-lane-separator:${roadId}:${index}-${index + 1}`, road_id: roadId, cluster_id: lanes[0].properties.cluster_id, left_lane_index: index, right_lane_index: index + 1, color: "white", pattern: "solid", effective_style: "white-solid", provenance: "native-road-complex-lane-separator/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - return result; -} - -function trimLineBeforeFirstControl(line, controls, width) { - const total = lineLengthMeters(line); - const step = .25; - for (let distance = step; distance <= total; distance += step) { - const placement = pointAndAxisAlongLine(line, Math.min(total, distance - step / 2)); - if (!placement) continue; - const ring = rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], step, width, 0); - if (!ringsOverlapControl([ring], controls)) continue; - let cutoff = Math.max(0, distance - step - .12); - while (cutoff > .5) { - const candidate = roadRing([line[0], pointAlongLine(line, cutoff)], width); - if (candidate && !ringsOverlapControl([candidate], controls)) return [line[0], pointAlongLine(line, cutoff)]; - cutoff -= .25; - } - return null; - } - return line; -} - -// `trimLineBeforeFirstControl` always keeps the head of the line, so the caller -// must hand it a line that already runs from the road towards the junction. -// Lane centerlines arrive in either orientation (a preview lane runs inward -// from the outer radius, an outgoing road lane runs outward from the cluster -// node), so orient by radius first and restore the original order afterwards. -function trimLaneOutsideControls(line, controls, width, center) { - if (!controls.length || !center || !Array.isArray(line) || line.length < 2) return line; - const outwardFirst = distanceMeters(line[0], center) >= distanceMeters(line.at(-1), center); - const oriented = outwardFirst ? line : [...line].reverse(); - const trimmed = trimLineBeforeFirstControl(oriented, controls, width); - if (!trimmed) return null; - return outwardFirst ? trimmed : [...trimmed].reverse(); -} - -// Reference identity is not reliable here: the reversed path rebuilds the array -// even when nothing was cut. Compare travelled length instead. -function laneWasClipped(original, visible) { - return Boolean(visible) && lineLengthMeters(visible) < lineLengthMeters(original) - .01; -} - -function compileEdgeLines(model, overrides, junctionPlans, options = {}, roadSurfaces = []) { - const features = []; - const clusters = options.junctionTemplates?.enabled ? (options.junctionTemplates.clusters || []) : []; - const clusterByNode = new Map(clusters.flatMap((cluster) => cluster.nodeIds.map((nodeId) => [String(nodeId), cluster]))); - const clusterCenters = new Map(clusters.map((cluster) => [cluster.id, clusterCenter(cluster, junctionPlans)])); - // Roads swallowed by a complex cluster never get a surface. Deriving their - // edge from the centerline anyway paints a curb across bare ground, so take - // the surfaces actually emitted as the authority on what can be outlined. - const surfaced = new Set(roadSurfaces.flatMap((feature) => String(feature.properties?.directional_road_ids || "").split(",")).filter(Boolean)); - for (const road of model.roads) { - if (surfaced.size && !surfaced.has(road.id)) continue; - // The road surface is one polygon per segment, centred on this centerline - // and spanning the sum of both directions. Deriving the edge from a single - // direction's width puts it half a carriageway inside the asphalt. - const directions = model.roads.filter((item) => item.segmentId === road.segmentId); - const totalWidth = directions.reduce((sum, item) => sum + item.widthMeters, 0); - const bidirectional = directions.length > 1; - // Match the surface's trim exactly. A cluster road is cut at the cluster - // boundary, not at the ordinary junction cutback; using the cutback here - // runs the edge line out past the asphalt it is supposed to outline. - const cluster = clusterByNode.get(String(road.sourceNodeIds[0])) || clusterByNode.get(String(road.sourceNodeIds.at(-1))); - const line = cluster?.template === "complex-junction-v1" - ? trimLineAtComplexCluster(road.centerline, road.sourceNodeIds, junctionPlans, cluster, clusterCenters.get(cluster.id)) - : trimLineAtJunctions(road.centerline, road.sourceNodeIds, junctionPlans); - // On a two-way segment, the inner edge is the road centre boundary and is - // owned by center_lines. Emit only each directional carriageway's outer - // edge; emitting both sides makes the layer look like a second centreline. - const offsets = bidirectional ? [-1] : [-1, 1]; - for (const offset of offsets) { - const side = offset < 0 ? "right" : "left"; - const style = edgeLineStyle(overrides, road.id, side); - const centerline = offsetLine(line, offset * totalWidth / 2); - if (!centerline) continue; - if (style.pattern === "solid") { - const ring = roadRing(centerline, .12); - if (ring) features.push(edgeLineFeature(road, side, style, ring)); - continue; - } - for (let distance = 1, part = 1; distance + 1 <= lineLengthMeters(centerline); distance += 4, part += 1) { - const placement = pointAndAxisAlongLine(centerline, distance); - if (!placement) continue; - features.push(edgeLineFeature(road, side, style, rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], 2, .12, 0), part)); - } - } - } - return features; -} - -function edgeLineFeature(road, side, style, ring, part = null) { - return { type: "Feature", properties: { native_id: `edge-line:${road.id}:${side}${part ? `:${part}` : ""}`, road_id: road.id, side, osm_way_ids: road.osmWayIds.join(","), color: style.color, pattern: style.pattern, effective_style: `${style.color}-${style.pattern}`, provenance: "native-road-edge-line/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }; -} - -function compileCenterLines(model, overrides, junctionPlans, controls, diagnostics, options = {}) { - 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 cluster = clusterForRoad(forward, options) || clusterForRoad(backward, options); - const internalCluster = cluster && roadInternalToCluster(forward, cluster); - const line = cluster - ? trimLineAtComplexCluster(forward.centerline, forward.sourceNodeIds, junctionPlans, cluster, clusterCenter(cluster, junctionPlans)) - : 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; } - const style = centerLineStyle(overrides, segmentId); - const visibleLine = trimLineBeforeFirstControl(line, controlFeatures, CENTER_LINE_WIDTH_METERS); - const visibleLength = visibleLine ? lineLengthMeters(visibleLine) : 0; - if (!visibleLine || visibleLength < CENTER_LINE_DASH_LENGTH_METERS) continue; - const gap = style.pattern === "solid" ? 0 : CENTER_LINE_DASH_GAP_METERS; - const markLength = CENTER_LINE_DASH_LENGTH_METERS + (style.pattern === "solid" ? CENTER_LINE_SOLID_OVERLAP_METERS : 0); - for (let start = 0, dashIndex = 1; start + markLength <= visibleLength; start += CENTER_LINE_DASH_LENGTH_METERS + gap, dashIndex += 1) { - const placement = pointAndAxisAlongLine(visibleLine, start + markLength / 2); - if (!placement) continue; - const clearanceRing = rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], markLength + CENTER_LINE_CONTROL_CLEARANCE_METERS * 2, CENTER_LINE_WIDTH_METERS + CENTER_LINE_CONTROL_CLEARANCE_METERS * 2, 0); - for (const offset of style.double ? [-.16, .16] : [0]) { const ring = rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], markLength, CENTER_LINE_WIDTH_METERS, offset); features.push({ type: "Feature", properties: { native_id: `center-line:${segmentId}:${dashIndex}:${offset}`, segment_id: segmentId, road_id: forward.id, cluster_id: cluster?.id || null, cluster_internal: Boolean(internalCluster), cluster_preview_hidden: Boolean(internalCluster), directional_road_ids: roads.map((road) => road.id).join(","), osm_way_ids: forward.osmWayIds.join(","), dash_index: dashIndex, dash_length_m: markLength, dash_gap_m: gap, color: style.color, pattern: style.pattern, double: Boolean(style.double), effective_style: `${style.double ? "double-" : ""}${style.color}-${style.pattern}`, placement_rule: "native-bidirectional-centerline/v1", provenance: "native-road-center-line/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }); } - } - } - return features; -} - -function centerLineStyle(overrides, segmentId) { - const override = overrides.overrides.find((item) => item.kind === "center-line-style" && item.segmentId === segmentId); - return override ? { color: override.color, pattern: override.pattern, double: Boolean(override.double) } : { color: "yellow", pattern: "dashed", double: false }; -} - -function edgeLineStyle(overrides, roadId, side) { - const value = overrides.overrides.find((item) => item.kind === "edge-line-style" && item.roadId === roadId && item.side === side); - return value ? { color: value.color, pattern: value.pattern } : { color: "white", pattern: "solid" }; -} - -function compileControlMarkings(model, lanes, diagnostics, junctionPlans = new Map()) { - const crosswalks = []; const stopLines = []; - const arrivalEndpointIds = new Set(model.connections.filter((connection) => connection.enabled).map((connection) => connection.fromEndpointId)); - for (const crossing of model.crossings || []) { - const candidates = model.roads.filter((road) => crossing.osmWayIds.includes(road.osmWayIds[0])).flatMap((road) => (lanes.byRoadId.get(road.id) || []).map((lane) => ({ road, lane, placement: nearestLanePlacement(lane.coordinates, crossing.coordinate), junctionDistanceMeters: distanceMeters(crossing.coordinate, road.centerline.at(-1)) })).filter((item) => item.placement)); - const candidate = candidates.sort((a, b) => a.placement.distance - b.placement.distance)[0]; - if (!candidate || candidate.placement.distance > 12) { diagnostics.push(diagnostic("warning", `crossing:node/${crossing.id}`, [crossing.id], "crossing-no-native-lane", "人行横道无法匹配到安全的原生车道,未生成标线。", crossing.coordinate)); continue; } - const approach = candidates.filter((item) => arrivalEndpointIds.has(`endpoint:${item.road.id}:end`) && item.junctionDistanceMeters > STOP_LINE_OFFSET_METERS && item.junctionDistanceMeters <= STOP_LINE_MAX_APPROACH_DISTANCE_METERS).sort((a, b) => a.junctionDistanceMeters - b.junctionDistanceMeters || a.placement.distance - b.placement.distance)[0]; - const crosswalkCandidate = approach || candidate; - const junctionInsetMeters = approach ? crossingJunctionInset(approach, junctionPlans) : 0; - const controlCenter = offsetByMeters(crossing.coordinate, crosswalkCandidate.placement.axis, junctionInsetMeters); - const { axis } = crosswalkCandidate.placement; const across = [-axis[1], axis[0]]; - for (let index = 0; index < 6; index += 1) crosswalks.push(controlFeature("crosswalk", crossing, crosswalkCandidate, index + 1, rectangleAt(controlCenter, axis, across, 3, .45, -2.25 + index * .9), { junctionInsetMeters })); - if (!approach) { diagnostics.push(diagnostic("info", `crossing:node/${crossing.id}`, [crossing.id], "crossing-no-safe-stop-line", "人行横道没有可确认的路口进口车道,保留斑马线但未生成停止线。", crossing.coordinate)); continue; } - const rawRoadPlacement = nearestLanePlacement(approach.road.centerline, controlCenter); - const laneOffset = rawRoadPlacement ? project(approach.placement.point, rawRoadPlacement.point) : [0, 0]; - const lateralOffset = laneOffset[0] * across[0] + laneOffset[1] * across[1]; - const laneCenterAtCrossing = offsetByMeters(controlCenter, across, lateralOffset); - const stopCenter = offsetByMeters(laneCenterAtCrossing, approach.placement.axis, -STOP_LINE_OFFSET_METERS); - stopLines.push(controlFeature("stop-line", crossing, approach, 1, rectangleAt(stopCenter, across, approach.placement.axis, approach.road.widthMeters, .45, 0), { junctionInsetMeters })); - } - return { crosswalks, stopLines }; -} - -function crossingJunctionInset(candidate, junctionPlans) { - const junctionNodeId = candidate.road.sourceNodeIds.at(-1); - const plan = junctionPlans.get(junctionNodeId); - if (!plan) return 0; - const targetDistance = Math.max(0, plan.cutbackMeters - CROSSWALK_JUNCTION_INSET_METERS); - return Math.min(CROSSWALK_MAX_JUNCTION_INSET_METERS, Math.max(0, candidate.junctionDistanceMeters - targetDistance)); -} - -function nearestLanePlacement(line, target) { let best = null; let traversedMeters = 0; for (let index = 1; index < line.length; index += 1) { const a = line[index - 1]; const b = line[index]; const vector = project(b, a); const length = Math.hypot(...vector); if (!length) continue; const relative = project(target, a); const ratio = Math.max(0, Math.min(1, (relative[0] * vector[0] + relative[1] * vector[1]) / (length * length))); const point = interpolate(a, b, ratio); const distance = distanceMeters(point, target); if (!best || distance < best.distance) best = { point, axis: [vector[0] / length, vector[1] / length], distance, distanceToEndMeters: lineLengthMeters(line) - traversedMeters - length * ratio }; traversedMeters += length; } return best; } -function offsetByMeters(point, axis, meters) { return unproject([axis[0] * meters, axis[1] * meters], point); } -function rectangleAt(center, axis, across, length, width, offset) { const shifted = offsetByMeters(center, across, offset); const corners = [[-length / 2, -width / 2], [length / 2, -width / 2], [length / 2, width / 2], [-length / 2, width / 2]].map(([forward, side]) => unproject([axis[0] * forward + across[0] * side, axis[1] * forward + across[1] * side], shifted)); return [...corners, corners[0]]; } -function controlFeature(kind, crossing, candidate, part, ring, placement = {}) { const stop = kind === "stop-line"; return { type: "Feature", properties: { native_id: `${kind}:node/${crossing.id}:${part}`, crossing_node_id: crossing.id, road_id: candidate.road.id, lane_id: candidate.lane.id, osm_way_ids: candidate.road.osmWayIds.join(","), direction: candidate.road.direction, placement_method: "native-lane-nearest-point/v1", junction_inset_m: Math.round((placement.junctionInsetMeters || 0) * 100) / 100, provenance: stop ? "native-road-stop-line/v1" : "native-road-crosswalk/v1" }, geometry: { type: "Polygon", coordinates: [ring] } }; } - -function compileLaneMarkings(model, overrides, lanes, diagnostics, junctionPlans, controls, options = {}) { - const separators = []; const directionArrows = []; const turnArrows = []; - const controlFeatures = [...controls.crosswalks, ...controls.stopLines]; - for (const road of model.roads) { - const cluster = clusterForRoad(road, options); - const internalCluster = cluster && roadInternalToCluster(road, cluster); - const roadLanes = (lanes.markingByRoadId || lanes.byRoadId).get(road.id) || []; - for (let index = 1; index < roadLanes.length; index += 1) { - const left = roadLanes[index - 1].coordinates; const right = roadLanes[index].coordinates; - if (left.length !== right.length) continue; - const centerline = left.map((point, pointIndex) => [(point[0] + right[pointIndex][0]) / 2, (point[1] + right[pointIndex][1]) / 2]); - const style = laneSeparatorStyle(overrides, road.id, index, index + 1); - const properties = { road_id: road.id, left_lane_index: index, right_lane_index: index + 1, cluster_id: cluster?.id || null, cluster_internal: Boolean(internalCluster), cluster_preview_hidden: Boolean(internalCluster), osm_way_ids: road.osmWayIds.join(","), color: style.color, pattern: style.pattern, effective_style: `${style.color}-${style.pattern}`, provenance: "native-road-lane-separator/v1" }; - if (style.pattern === "solid") { - const visibleLine = trimLineBeforeFirstControl(centerline, controlFeatures, .12); - const ring = visibleLine ? roadRing(visibleLine, .12) : null; - if (ring) separators.push({ type: "Feature", properties: { native_id: `lane-separator:${road.id}:${index}-${index + 1}`, ...properties }, geometry: { type: "Polygon", coordinates: [ring] } }); - } else { - const visibleLine = trimLineBeforeFirstControl(centerline, controlFeatures, .12); - const visibleLength = visibleLine ? lineLengthMeters(visibleLine) : 0; - for (let distance = 1, part = 1; visibleLine && distance + 1 <= visibleLength; distance += 4, part += 1) { - const placement = pointAndAxisAlongLine(visibleLine, distance); - if (!placement) continue; - const ring = rectangleAt(placement.point, placement.axis, [-placement.axis[1], placement.axis[0]], 2, .12, 0); - separators.push({ type: "Feature", properties: { native_id: `lane-separator:${road.id}:${index}-${index + 1}:${part}`, ...properties }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - } - for (const lane of roadLanes) directionArrows.push(...directionArrowFeatures(road, lane, controlFeatures, diagnostics).map((feature) => ({ ...feature, properties: { ...feature.properties, cluster_id: cluster?.id || null, cluster_internal: Boolean(internalCluster), cluster_preview_hidden: Boolean(internalCluster) } }))); - const turns = road.tags[`turn:lanes:${road.direction}`] ?? road.tags["turn:lanes"]; - const maneuvers = turns ? String(turns).split("|") : []; - for (let index = 0; index < roadLanes.length; index += 1) { - const lane = roadLanes[index]; const explicitManeuver = maneuvers[index]; - if (!explicitManeuver) continue; - const maneuver = normalizeManeuver(explicitManeuver); - if (!lane) { diagnostics.push(diagnostic("warning", road.id, road.osmWayIds, "turn-arrow-lane-missing", "转向标签引用了不存在的车道,未生成箭头。", road.centerline.at(-1))); continue; } - if (!arrowRingsAt(maneuver, lane.coordinates.at(-1), [0, 1]).length) { diagnostics.push(diagnostic("info", lane.id, road.osmWayIds, "turn-arrow-unsupported", "转向标签不在当前已测试的箭头集合中,未生成箭头。", lane.coordinates.at(-1))); continue; } - if (lineLengthMeters(lane.coordinates) < 8) { diagnostics.push(diagnostic("warning", lane.id, road.osmWayIds, "turn-arrow-no-safe-placement", "驶入路口前的车道过短,未生成转向箭头。", lane.coordinates.at(-1))); continue; } - const previous = lane.coordinates.at(-2); const end = lane.coordinates.at(-1); - const meters = project(end, end); const vector = project(previous, end); const length = Math.hypot(-vector[0], -vector[1]); - const axis = length ? [-vector[0] / length, -vector[1] / length] : null; - const placement = axis ? [6, 10, 14, 18, 22].find((distance) => distance < lineLengthMeters(lane.coordinates) - 2 && !ringsOverlapControl(arrowRingsAt(maneuver, pointAlongLine([...lane.coordinates].reverse(), distance), axis), controlFeatures)) : null; - if (!placement) { diagnostics.push(diagnostic("info", lane.id, road.osmWayIds, "turn-arrow-control-conflict", "转向箭头会压住斑马线或停止线,未生成该箭头。", lane.coordinates.at(-1))); continue; } - const center = pointAlongLine([...lane.coordinates].reverse(), placement); - const rings = arrowRingsAt(maneuver, center, axis); - if (!rings.length) continue; - for (let part = 0; part < rings.length; part += 1) turnArrows.push({ type: "Feature", properties: { native_id: `turn-arrow:${lane.id}:${maneuver}:${part}`, road_id: road.id, lane_id: lane.id, cluster_id: cluster?.id || null, cluster_internal: Boolean(internalCluster), cluster_preview_hidden: Boolean(internalCluster), osm_way_ids: road.osmWayIds.join(","), direction: road.direction, lane_index: lane.index, maneuver, arrow_part: part, placement_distance_meters: placement, provenance: "native-road-turn-arrow/v1" }, geometry: { type: "Polygon", coordinates: [rings[part]] } }); - } - } - return { separators, directionArrows, turnArrows }; -} - -function clusterForRoad(road, options) { - const clusters = options.junctionTemplates?.enabled ? options.junctionTemplates.clusters || [] : []; - return clusters.find((cluster) => cluster.template === "complex-junction-v1" && road.sourceNodeIds.some((nodeId) => cluster.nodeIds.map(String).includes(String(nodeId)))) || null; -} - -function roadInternalToCluster(road, cluster) { - const nodeIds = new Set(cluster.nodeIds.map(String)); - return nodeIds.has(String(road.sourceNodeIds[0])) && nodeIds.has(String(road.sourceNodeIds.at(-1))); -} - -function clusterCenter(cluster, junctionPlans) { - const points = cluster.nodeIds.map((nodeId) => junctionPlans.get(String(nodeId))?.node).filter(Boolean); - return points.length ? points.reduce((sum, point) => [sum[0] + point[0] / points.length, sum[1] + point[1] / points.length], [0, 0]) : null; -} - -function angularDistance(first, second) { - return Math.abs(((first - second + 180) % 360) - 180); -} - -function laneSeparatorStyle(overrides, roadId, leftLaneIndex, rightLaneIndex) { const value = overrides.overrides.find((item) => item.kind === "lane-separator-style" && item.roadId === roadId && item.leftLaneIndex === leftLaneIndex && item.rightLaneIndex === rightLaneIndex); return value ? { color: value.color, pattern: value.pattern } : { color: "white", pattern: "dashed" }; } - -function directionArrowFeatures(road, lane, controlFeatures, diagnostics) { - const length = lineLengthMeters(lane.coordinates); - const features = []; - for (let distance = DIRECTION_ARROW_ENDPOINT_BUFFER_METERS, sequence = 1; distance <= length - DIRECTION_ARROW_ENDPOINT_BUFFER_METERS; distance += DIRECTION_ARROW_INTERVAL_METERS, sequence += 1) { - const placement = pointAndAxisAlongLine(lane.coordinates, distance); - if (!placement) continue; - const rings = arrowRingsAt("through", placement.point, placement.axis); - if (ringsOverlapControl(rings, controlFeatures)) { diagnostics.push(diagnostic("info", lane.id, road.osmWayIds, "direction-arrow-control-conflict", "默认直行箭头会压住斑马线或停止线,已跳过该位置。", placement.point)); continue; } - for (let part = 0; part < rings.length; part += 1) features.push({ type: "Feature", properties: { native_id: `direction-arrow:${lane.id}:${sequence}:${part}`, road_id: road.id, lane_id: lane.id, osm_way_ids: road.osmWayIds.join(","), direction: road.direction, lane_index: lane.index, maneuver: "through", sequence, distance_along_lane_meters: Math.round(distance * 10) / 10, placement_interval_meters: DIRECTION_ARROW_INTERVAL_METERS, provenance: "native-road-direction-arrow/v1" }, geometry: { type: "Polygon", coordinates: [rings[part]] } }); - } - return features; -} - -function ringsOverlapControl(rings, controls) { - return rings.some((ring) => controls.some((feature) => ringsOverlap(ring, feature.geometry.coordinates[0]))); -} -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); - if (a[0] > b[2] || a[2] < b[0] || a[1] > b[3] || a[3] < b[1]) return false; - if (first.some((point) => pointInPolygon(point, second)) || second.some((point) => pointInPolygon(point, first))) return true; - return first.slice(1).some((point, index) => second.slice(1).some((other, otherIndex) => segmentsIntersect(first[index], point, second[otherIndex], other))); -} - -function compileSidewalkSurfaces(model, diagnostics, junctionPlans, options = {}) { - const features = []; - const byWay = new Map(); - for (const road of model.roads) { - const key = road.segmentId; - if (!byWay.has(key)) byWay.set(key, []); - byWay.get(key).push(road); - } - for (const [wayKey, directions] of byWay) { - const forward = directions.find((road) => road.direction === "forward") || directions[0]; - const backward = directions.find((road) => road.id !== forward.id); - const totalWidth = directions.reduce((sum, road) => sum + road.widthMeters, 0); - const sides = [ - ["left", forward.sidewalkLeft || Boolean(backward?.sidewalkRight)], - ["right", forward.sidewalkRight || Boolean(backward?.sidewalkLeft)], - ]; - const cluster = clusterForRoad(forward, options) || (backward ? clusterForRoad(backward, options) : null); - const center = cluster ? clusterCenter(cluster, junctionPlans) : null; - for (const [side, enabled] of sides) { - if (!enabled) continue; - const centerline = cluster - ? trimLineAtComplexCluster(forward.centerline, forward.sourceNodeIds, junctionPlans, cluster, center) - : trimLineAtJunctions(forward.centerline, forward.sourceNodeIds, junctionPlans); - const ring = sidewalkRing(centerline, totalWidth / 2, totalWidth / 2 + DEFAULT_SIDEWALK_WIDTH_METERS, side === "left" ? 1 : -1); - if (!ring) { diagnostics.push(diagnostic("warning", forward.id, forward.osmWayIds, "invalid-sidewalk-surface", "无法为该道路生成连续人行道面。", forward.centerline[0])); continue; } - const sidewalkId = forward.segmentId.endsWith("/0") ? `sidewalk:way/${forward.osmWayIds.join(",")}:${side}` : `sidewalk:${wayKey}:${side}`; - features.push({ type: "Feature", properties: { native_id: sidewalkId, cluster_id: cluster?.id || null, osm_way_ids: forward.osmWayIds.join(","), source_road_id: forward.sourceRoadId, side, width_m: DEFAULT_SIDEWALK_WIDTH_METERS, directional_road_ids: directions.map((road) => road.id).join(","), provenance: "native-road-sidewalk/v1", override_ids: directions.flatMap((road) => road.appliedOverrideIds).join(",") }, geometry: { type: "Polygon", coordinates: [ring] } }); - } - } - features.push(...compileSidewalkCorners(model, junctionPlans, options)); - return features; -} - -function compileSidewalkCorners(model, junctionPlans, options = {}) { - const result = []; - for (const [nodeId, plan] of junctionPlans) { - if (plan.clusterId && activeComplexCluster(options, plan.clusterId)) continue; - const candidates = []; - for (const approach of plan.approaches) { - const directions = model.roads.filter((road) => road.segmentId === approach.segmentId); - const forward = directions.find((road) => road.direction === "forward") || directions[0]; - if (!forward) continue; - const outwardIsForward = forward.sourceNodeIds[0] === nodeId; - const sideStates = outwardIsForward - ? { left: forward.sidewalkLeft, right: forward.sidewalkRight } - : { left: forward.sidewalkRight, right: forward.sidewalkLeft }; - const cutback = pointAlongLine(approach.line, plan.cutbackMeters); - if (!cutback) continue; - const heading = headingAtEndpoint(approach.line); - const halfWidth = approach.widthMeters / 2; - for (const [side, enabled] of Object.entries(sideStates)) { - if (!enabled) continue; - // offsetLine's positive normal is driver's left, which is heading -90 - // in this north-based heading convention. - const sideHeading = heading + (side === "left" ? -90 : 90); - candidates.push({ - wayKey: approach.segmentId, - sourceWayKey: forward.osmWayIds.join(","), - side, - outwardHeading: heading, - normalDegrees: sideHeading, - curb: offsetCoordinate(cutback, sideHeading, halfWidth), - outer: offsetCoordinate(cutback, sideHeading, halfWidth + DEFAULT_SIDEWALK_WIDTH_METERS), - }); - } - } - candidates.sort((a, b) => angleAround(plan.node, a.curb) - angleAround(plan.node, b.curb)); - for (let index = 0; index < candidates.length; index += 1) { - const first = candidates[index]; - const second = candidates[(index + 1) % candidates.length]; - if (first.wayKey === second.wayKey) continue; - const continuation = isStraightSidewalkContinuation(first, second); - if (first.sourceWayKey === second.sourceWayKey && !continuation) continue; - // A split-through road has two approaches at this node. Its pedestrian - // strip is a direct continuation, not a curb corner. Treating it as a - // curve creates the oversized outer lobe seen at T junctions. - const ring = continuation - ? [first.curb, first.outer, second.outer, second.curb, first.curb] - : roundedSidewalkCorner(plan.node, first, second); - if (hasSelfIntersection(ring)) continue; - if (continuation && cornerFallsIntoOtherApproach(ring, first.sourceWayKey, plan.approaches)) continue; - result.push({ - type: "Feature", - properties: { - native_id: `sidewalk-corner:node/${nodeId}:${first.wayKey}:${first.side}->${second.wayKey}:${second.side}`, - osm_node_id: nodeId, - kind: continuation ? "continuation" : "corner", - width_m: DEFAULT_SIDEWALK_WIDTH_METERS, - provenance: continuation ? "native-road-sidewalk-continuation/v1" : "native-road-sidewalk-corner/v1", - }, - geometry: { type: "Polygon", coordinates: [ring] }, - }); - } - } - return result; -} - -function roundedSidewalkCorner(node, first, second) { - // Keep the established vehicle curb geometry, then derive the outer edge - // from it. Independent Bezier curves drift apart and leave asphalt exposed - // between the junction and pedestrian layers. - const curbForward = roundedCorner(node, first.curb, second.curb, first.outwardHeading, second.outwardHeading) || [first.curb, second.curb]; - // Construct the outside edge from the same tangent-support rule. A linear - // point-by-point offset changes the curvature and makes the two boundaries - // visibly disagree at the middle of the corner. - const outerForward = roundedCorner(node, first.outer, second.outer, first.outwardHeading, second.outwardHeading) - || offsetCornerArc(curbForward, first.curb, first.outer, second.curb, second.outer); - const curbArc = [...curbForward].reverse(); - return [ - first.curb, - first.outer, - ...outerForward.slice(1, -1), - second.outer, - second.curb, - ...curbArc.slice(1, -1), - first.curb, - ]; -} - -function offsetCornerArc(curbArc, firstCurb, firstOuter, secondCurb, secondOuter) { - return curbArc.map((point, index) => { - const ratio = curbArc.length === 1 ? 0 : index / (curbArc.length - 1); - const firstOffset = [firstOuter[0] - firstCurb[0], firstOuter[1] - firstCurb[1]]; - const secondOffset = [secondOuter[0] - secondCurb[0], secondOuter[1] - secondCurb[1]]; - return [point[0] + firstOffset[0] + (secondOffset[0] - firstOffset[0]) * ratio, point[1] + firstOffset[1] + (secondOffset[1] - firstOffset[1]) * ratio]; - }); -} - -function samePhysicalSide(first, second) { - const radians = (first.normalDegrees - second.normalDegrees) * Math.PI / 180; - return Math.cos(radians) >= 0.98; -} - -function isStraightSidewalkContinuation(first, second) { - if (first.sourceWayKey !== second.sourceWayKey || !samePhysicalSide(first, second)) return false; - const radians = (first.outwardHeading - second.outwardHeading) * Math.PI / 180; - return Math.cos(radians) <= -0.98; -} - -function cornerFallsIntoOtherApproach(ring, sourceWayKey, approaches) { - const vertices = ring.slice(0, -1); - const center = vertices.reduce((sum, point) => [sum[0] + point[0] / vertices.length, sum[1] + point[1] / vertices.length], [0, 0]); - return approaches.filter((approach) => approach.sourceWayKey !== sourceWayKey).some((approach) => { - const carriageway = roadRing(approach.line, approach.widthMeters); - return carriageway && pointInPolygon(center, carriageway); - }); -} - -function validateConnectorContainment(connectors, junctionFeatures, diagnostics) { - const junctionByNode = new Map(); - for (const feature of junctionFeatures) { - if (feature.properties.osm_node_ids) for (const nodeId of String(feature.properties.osm_node_ids).split(",")) junctionByNode.set(nodeId, feature); - else if (feature.properties.osm_node_id) junctionByNode.set(feature.properties.osm_node_id, feature); - } - for (const connector of connectors) { - const junction = junctionByNode.get(connector.properties.node_id); - if (!junction) continue; - if (junction.properties.kind === "cluster") continue; - const ring = junction.geometry.coordinates[0]; - if (!connector.geometry.coordinates.every((point) => pointInOrNearPolygon(point, ring, CONNECTOR_BOUNDARY_TOLERANCE_METERS))) { - diagnostics.push(diagnostic("warning", connector.properties.connection_id, [connector.properties.node_id], "connector-outside-junction", "转向路径有部分落在路口面外,请检查道路截面或转向连接。", connector.geometry.coordinates[0])); - } - } -} - -function pointInPolygon(point, ring) { - for (let index = 1; index < ring.length; index += 1) if (pointOnSegment(point, ring[index - 1], ring[index])) return true; - let inside = false; - for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index++) { - const a = ring[index]; const b = ring[previous]; - const intersect = a[1] > point[1] !== b[1] > point[1] && point[0] < (b[0] - a[0]) * (point[1] - a[1]) / (b[1] - a[1]) + a[0]; - if (intersect) inside = !inside; - } - return inside; -} -function pointInOrNearPolygon(point, ring, toleranceMeters) { - return pointInPolygon(point, ring) || ring.slice(1).some((end, index) => distancePointToSegmentMeters(point, ring[index], end) <= toleranceMeters); -} -function distancePointToSegmentMeters(point, start, end) { - const localPoint = project(point, start); - const localEnd = project(end, start); - const lengthSquared = localEnd[0] ** 2 + localEnd[1] ** 2; - if (lengthSquared < .0001) return Math.hypot(...localPoint); - const ratio = Math.max(0, Math.min(1, (localPoint[0] * localEnd[0] + localPoint[1] * localEnd[1]) / lengthSquared)); - return Math.hypot(localPoint[0] - localEnd[0] * ratio, localPoint[1] - localEnd[1] * ratio); -} -function pointOnSegment(point, a, b) { - const cross = (point[0] - a[0]) * (b[1] - a[1]) - (point[1] - a[1]) * (b[0] - a[0]); - if (Math.abs(cross) > 1e-12) return false; - return point[0] >= Math.min(a[0], b[0]) - 1e-12 && point[0] <= Math.max(a[0], b[0]) + 1e-12 && point[1] >= Math.min(a[1], b[1]) - 1e-12 && point[1] <= Math.max(a[1], b[1]) + 1e-12; -} - -// `complexControls` carries the crosswalks and stop bars the complex-junction -// templates already emitted. Ordinary controls cannot be passed here: they are -// placed *from* these lane centerlines, so only the template-generated ones -// exist this early. -function compileLaneCenterlines(model, diagnostics, junctionPlans, options = {}, complexControls = {}) { - const features = []; - const controlFeatures = [...(complexControls.crosswalks || []), ...(complexControls.stopLines || [])]; - const byRoadId = new Map(); - const markingByRoadId = new Map(); - const clusters = options.junctionTemplates?.enabled ? (options.junctionTemplates.clusters || []) : []; - const clusterByNode = new Map(clusters.flatMap((cluster) => cluster.nodeIds.map((nodeId) => [String(nodeId), cluster]))); - const clusterCenters = new Map(clusters.map((cluster) => [cluster.id, clusterCenter(cluster, junctionPlans)])); - for (const road of model.roads) { - const lanes = []; - const markingLanes = []; - const laneWidth = road.widthMeters / road.laneCount; - const siblings = model.roads.filter((item) => item.segmentId === road.segmentId); - const opposite = siblings.find((item) => item.id !== road.id); - const boundaryCluster = clusterByNode.get(String(road.sourceNodeIds[0])) || clusterByNode.get(String(road.sourceNodeIds.at(-1))); - const internalCluster = boundaryCluster && roadInternalToCluster(road, boundaryCluster); - const clippedRoadLine = boundaryCluster?.template === "complex-junction-v1" - ? trimLineAtComplexCluster(road.centerline, road.sourceNodeIds, junctionPlans, boundaryCluster, clusterCenters.get(boundaryCluster.id)) - : trimLineAtJunctions(road.centerline, road.sourceNodeIds, junctionPlans); - // OSM centerline is the shared carriageway center. On a two-way road, - // offset each directed carriageway to its own side before placing lanes. - const carriagewayOffset = opposite ? (road.direction === "forward" ? -opposite.widthMeters / 2 : -road.widthMeters / 2) : 0; - for (let index = 0; index < road.laneCount; index += 1) { - // OSM `turn:lanes` is ordered from left to right. Keep lane 1 on the - // driver's left so tag positions and generated lane IDs have one meaning. - const offset = carriagewayOffset + (road.widthMeters / 2 - laneWidth * (index + 0.5)); - const coordinates = offsetLine(trimLineAtJunctions(road.centerline, road.sourceNodeIds, junctionPlans), offset); - const publishedCoordinates = offsetLine(clippedRoadLine, offset); - if (!coordinates || !publishedCoordinates) { diagnostics.push(diagnostic("error", road.id, road.osmWayIds, "invalid-lane-centerline", "无法为该道路生成车道中心线。", road.centerline[0])); continue; } - const lane = { id: `lane:${road.id}:${index + 1}`, roadId: road.id, index: index + 1, coordinates }; - lanes.push(lane); - // Only the published geometry stops at the crossing. `coordinates` stays - // whole because connectors are derived from it; a lane that ends at the - // stop bar would otherwise break every turn path through the junction. - const visibleCoordinates = boundaryCluster?.template === "complex-junction-v1" - ? trimLaneOutsideControls(publishedCoordinates, controlFeatures, LANE_CENTERLINE_PROBE_WIDTH_METERS, clusterCenters.get(boundaryCluster.id)) - : publishedCoordinates; - if (!visibleCoordinates) { diagnostics.push(diagnostic("warning", road.id, road.osmWayIds, "lane-centerline-fully-inside-control", "该车道中心线整体落在斑马线或停止线内,已按未裁剪几何发布。", publishedCoordinates[0])); } - // Lane markings are laid out along this line. Feeding it the clipped - // geometry is what keeps separators and arrows from being painted *past* - // a crossing: control avoidance only stops them landing *on* one. - markingLanes.push({ ...lane, coordinates: visibleCoordinates || publishedCoordinates }); - features.push({ type: "Feature", properties: { native_id: lane.id, road_id: road.id, lane_index: lane.index, cluster_id: boundaryCluster?.id || null, cluster_internal: Boolean(internalCluster), cluster_preview_hidden: Boolean(internalCluster), cluster_boundary_clipped: Boolean(boundaryCluster && !internalCluster), control_clipped: laneWasClipped(publishedCoordinates, visibleCoordinates), source: "native-road-lane-centerline/v3-control-clipped" }, geometry: { type: "LineString", coordinates: visibleCoordinates || publishedCoordinates } }); - } - byRoadId.set(road.id, lanes); - markingByRoadId.set(road.id, markingLanes); - } - for (const cluster of clusters) { - const clusterNodes = new Set(cluster.nodeIds.map(String)); - const clusterCoordinates = [...clusterNodes].map((nodeId) => junctionPlans.get(nodeId)?.node).filter(Boolean); - const compositeCenter = clusterCoordinates.length - ? clusterCoordinates.reduce((sum, point) => [sum[0] + point[0] / clusterCoordinates.length, sum[1] + point[1] / clusterCoordinates.length], [0, 0]) - : null; - const corridors = []; - for (const [nodeId, plan] of junctionPlans) { - if (!clusterNodes.has(String(nodeId))) continue; - for (const approach of plan.approaches) { - const end = approach.line.at(-1); - if ([...clusterNodes].some((candidate) => candidate !== String(nodeId) && distanceMeters(end, junctionPlans.get(candidate)?.node || [Infinity, Infinity]) < 3)) continue; - const heading = ((headingAtEndpoint(approach.line) + 180) % 360) - 180; - corridors.push({ nodeId, heading, approach, plan }); - } - } - for (const corridor of corridors) { - const approach = corridor.approach; const plan = corridor.plan; - const outerRadius = complexJunctionMetrics(cluster).approachOuterRadius; - const length = Math.min(distanceAlongLineToRadius(approach.line, compositeCenter, outerRadius), lineLengthMeters(approach.line)); - if (length < 12) continue; - const line = approach.line; const outer = pointAlongLine(line, Math.max(0, length)); const inner = pointAlongLine(line, Math.min(Math.max(3, Number(cluster.coreRadiusMeters || 28) * .14), Math.max(3, length - 8))); - const corridorRoads = approach.roadIds.map((roadId) => model.roads.find((road) => road.id === roadId)).filter(Boolean); - const incoming = corridorRoads.some((road) => String(road.sourceNodeIds.at(-1)) === String(corridor.nodeId)); - const count = Math.max(1, corridorRoads.reduce((sum, road) => sum + road.laneCount, 0)); - const laneWidth = approach.widthMeters / count; - const axis = project(inner, outer); const total = Math.hypot(...axis); if (!total) continue; - const normalized = [axis[0] / total, axis[1] / total]; const across = [-normalized[1], normalized[0]]; - for (let index = 0; index < count; index += 1) { - const offset = approach.widthMeters / 2 - laneWidth * (index + .5); - const start = unproject([across[0] * offset, across[1] * offset], outer); - const end = unproject([across[0] * offset, across[1] * offset], inner); - const maneuver = incoming ? index === 0 ? "left" : index === count - 1 ? "right" : "through" : "outbound"; - // Preview lanes are laid out radially from the outer radius inwards, so - // an untrimmed one runs straight over the arm crossing. Stop it at the - // first control: incoming lanes land on the stop bar, outgoing lanes on - // the far edge of the crossing. - const visible = trimLaneOutsideControls([start, end], controlFeatures, LANE_CENTERLINE_PROBE_WIDTH_METERS, compositeCenter); - features.push({ type: "Feature", properties: { native_id: `cluster-approach-lane:${cluster.id}:${approach.segmentId}:${index + 1}`, road_id: corridorRoads[0]?.id || null, cluster_id: cluster.id, cluster_preview: true, incoming, lane_index: index + 1, maneuver, control_clipped: laneWasClipped([start, end], visible), source: "native-road-junction-cluster-lane/v4-control-clipped" }, geometry: { type: "LineString", coordinates: visible || [start, end] } }); - } - } - } - return { features, byRoadId, markingByRoadId }; -} - -function cubicTurnCurve(start, end, startHeading, endHeading, radius, turn, center) { - const reach = turn === "right" ? Math.max(5, radius * .75) : Math.max(9, radius * 1.35); - const first = offsetCoordinate(start, startHeading, reach); - const second = offsetCoordinate(end, endHeading, reach); - const points = []; - for (let index = 0; index <= 18; index += 1) { - const t = index / 18; const inverse = 1 - t; - points.push([ - inverse ** 3 * start[0] + 3 * inverse ** 2 * t * first[0] + 3 * inverse * t ** 2 * second[0] + t ** 3 * end[0], - inverse ** 3 * start[1] + 3 * inverse ** 2 * t * first[1] + 3 * inverse * t ** 2 * second[1] + t ** 3 * end[1], - ]); - } - return points.every((point) => point.every(Number.isFinite)) ? points : [start, center, end]; -} - -function compileConnectors(model, lanes, diagnostics, overrides, junctionPlans) { - const features = []; - const movements = []; - for (const connection of model.connections.filter((item) => item.enabled)) { - const fromRoad = model.roads.find((road) => road.id === endpointRoadId(model, connection.fromEndpointId)); - const toRoad = model.roads.find((road) => road.id === endpointRoadId(model, connection.toEndpointId)); - const fromLanes = lanes.byRoadId.get(fromRoad?.id) || []; - const toLanes = lanes.byRoadId.get(endpointRoadId(model, connection.toEndpointId)) || []; - if (!fromLanes.length || !toLanes.length) { diagnostics.push(diagnostic("warning", connection.id, [connection.nodeId], "connector-missing-lane", "转向连接缺少可用车道中心线。", endpointCoordinate(model, connection.fromEndpointId))); continue; } - for (let index = 0; index < fromLanes.length; index += 1) { - const turn = connectionTurn(fromRoad, toRoad); - const defaultTargetIndex = targetLaneIndex(turn, index, fromLanes.length, toLanes.length); - const defaultFromLane = fromLanes[index]; const defaultToLane = toLanes[defaultTargetIndex]; - const override = laneOverride(overrides, defaultFromLane.id, defaultToLane.id); - if ((!laneAllowsTurn(fromRoad, index, turn) && override?.enabled !== true) || override?.enabled === false) continue; - const from = defaultFromLane.coordinates.at(-1); const to = defaultToLane.coordinates[0]; - const plan = junctionPlans.get(connection.nodeId); - // Cross intersections retain the earlier center-node curve while T junctions - // use lane tangents so their through movement does not bow toward the stem. - const coordinates = plan?.segmentIds.size === 4 - ? quadraticCurve(from, endpointCoordinate(model, connection.fromEndpointId), to, 12) - : connectorCurve(defaultFromLane.coordinates, defaultToLane.coordinates, turn); - const length = lineLengthMeters(coordinates); - const id = `movement:${connection.id}:${defaultFromLane.id}->${defaultToLane.id}`; - const provenance = override ? `override:${override.id}` : connection.provenance; - const connectorId = `connector:${id}`; - const geometryStatus = length < .4 ? "continuous" : length > 80 ? "deferred-too-long" : "connector"; - const movement = { id, connectorId, connectionId: connection.id, nodeId: connection.nodeId, fromRoadId: fromRoad.id, toRoadId: defaultToLane.roadId, fromLaneId: defaultFromLane.id, toLaneId: defaultToLane.id, turn, provenance, appliedOverrideIds: override ? [override.id] : [], geometryPublished: geometryStatus === "connector", geometryStatus }; - if (length < .4) { movements.push(movement); continue; } - if (length > 80) { diagnostics.push(diagnostic("warning", connection.id, [connection.nodeId], "connector-too-long", "转向路径超过 80 米,未发布几何;请检查路口拓扑或人工连接。", from)); movements.push(movement); continue; } - const cluster = plan?.clusterId || null; - features.push({ type: "Feature", properties: { native_id: connectorId, movement_id: id, connection_id: connection.id, node_id: connection.nodeId, cluster_id: cluster, cluster_internal: Boolean(cluster), from_lane_id: defaultFromLane.id, to_lane_id: defaultToLane.id, turn, provenance }, geometry: { type: "LineString", coordinates } }); - movements.push(movement); - } - } - return { features, movements }; -} -function laneOverride(overrides, fromLaneId, toLaneId) { return overrides.overrides.find((item) => item.kind === "lane-connection" && item.fromLaneId === fromLaneId && item.toLaneId === toLaneId); } - -function endpointRoadId(model, endpointId) { return model.endpoints.find((endpoint) => endpoint.id === endpointId)?.roadId; } -function endpointCoordinate(model, endpointId) { return model.endpoints.find((endpoint) => endpoint.id === endpointId)?.coordinate; } -function connectionTurn(fromRoad, toRoad) { - if (!fromRoad || !toRoad) return "unknown"; - const incoming = headingDegrees(fromRoad.centerline.at(-2), fromRoad.centerline.at(-1)); - const outgoing = headingDegrees(toRoad.centerline[0], toRoad.centerline[1]); - const delta = ((outgoing - incoming + 540) % 360) - 180; - if (Math.abs(delta) >= 150) return "uturn"; - if (Math.abs(delta) <= 30) return "through"; - return delta > 0 ? "right" : "left"; -} -function laneAllowsTurn(road, zeroIndex, turn) { - if (!road) return true; - const tag = road.tags[`turn:lanes:${road.direction}`] ?? road.tags["turn:lanes"]; - if (!tag) return true; - const lanes = String(tag).split("|").map((lane) => lane.split(";").map((value) => value.trim().replace("slight_", "")).filter(Boolean)); - const allowed = lanes[zeroIndex]; - return !allowed || allowed.includes(turn) || turn === "uturn" && allowed.includes("reverse"); -} -function targetLaneIndex(turn, sourceIndex, sourceCount, targetCount) { - if (turn === "left") return 0; - if (turn === "right") return targetCount - 1; - if (turn === "uturn") return 0; - return Math.min(targetCount - 1, Math.round(sourceIndex / Math.max(1, sourceCount - 1) * Math.max(0, targetCount - 1))); -} -function connectorCurve(incoming, outgoing, turn) { - const start = incoming.at(-1); - const end = outgoing[0]; - if (turn === "through") return lineCurve(start, end, 12); - const incomingHeading = headingDegrees(incoming.at(-2), start); - const outgoingHeading = headingDegrees(end, outgoing[1]); - const chord = distanceMeters(start, end); - const incomingSpan = distanceMeters(incoming.at(-2), start); - const outgoingSpan = distanceMeters(end, outgoing[1]); - const tangentIntersection = intersectTangentRays(start, end, incomingHeading, outgoingHeading); - const fallbackDistance = Math.min(8, Math.max(.75, Math.min(chord * .42, incomingSpan * .8, outgoingSpan * .8))); - const firstDistance = tangentIntersection && tangentIntersection.incoming >= 0 ? Math.min(tangentIntersection.incoming, Math.min(8, Math.max(.75, incomingSpan * 2.4))) / 3 : fallbackDistance; - const secondDistance = tangentIntersection && tangentIntersection.outgoing >= 0 ? Math.min(tangentIntersection.outgoing, Math.min(8, Math.max(.75, outgoingSpan * 2.4))) / 3 : fallbackDistance; - const firstControl = offsetCoordinate(start, incomingHeading, firstDistance); - const secondControl = offsetCoordinate(end, outgoingHeading + 180, secondDistance); - return cubicBezier(start, firstControl, secondControl, end, 12); -} - -function intersectTangentRays(start, end, incomingHeading, outgoingHeading) { - const incoming = headingVector(incomingHeading); - const outgoing = headingVector(outgoingHeading); - const delta = project(end, start); - const cross = incoming[0] * outgoing[1] - incoming[1] * outgoing[0]; - if (Math.abs(cross) < 1e-6) return null; - return { - incoming: (delta[0] * outgoing[1] - delta[1] * outgoing[0]) / cross, - outgoing: (delta[0] * incoming[1] - delta[1] * incoming[0]) / cross, - }; -} - -function lineCurve(start, end, segments) { - return Array.from({ length: segments + 1 }, (_, index) => interpolate(start, end, index / segments)); -} - -function cubicBezier(a, firstControl, secondControl, b, segments) { - const result = []; - for (let index = 0; index <= segments; index += 1) { - const t = index / segments; const u = 1 - t; - result.push([u ** 3 * a[0] + 3 * u * u * t * firstControl[0] + 3 * u * t * t * secondControl[0] + t ** 3 * b[0], u ** 3 * a[1] + 3 * u * u * t * firstControl[1] + 3 * u * t * t * secondControl[1] + t ** 3 * b[1]]); - } - return result; -} - -function quadraticCurve(a, control, b, segments) { - const result = []; - for (let index = 0; index <= segments; index += 1) { - const t = index / segments; const u = 1 - t; - result.push([u * u * a[0] + 2 * u * t * control[0] + t * t * b[0], u * u * a[1] + 2 * u * t * control[1] + t * t * b[1]]); - } - return result; -} - -function offsetLine(line, offsetMeters) { - if (line.length < 2) return null; - const origin = line[0]; const points = line.map((point) => project(point, origin)); const result = []; - for (let index = 0; index < points.length; index += 1) { - const previous = points[Math.max(0, index - 1)]; const next = points[Math.min(points.length - 1, index + 1)]; - const dx = next[0] - previous[0]; const dy = next[1] - previous[1]; const length = Math.hypot(dx, dy); - if (length < 0.01) return null; - result.push(unproject([points[index][0] - dy / length * offsetMeters, points[index][1] + dx / length * offsetMeters], origin)); - } - return result; -} - -function lineLengthMeters(line) { return line.slice(1).reduce((sum, point, index) => { const previous = line[index]; const dx = (point[0] - previous[0]) * 111320 * Math.cos(point[1] * Math.PI / 180); const dy = (point[1] - previous[1]) * 111320; return sum + Math.hypot(dx, dy); }, 0); } -function polygonAreaMeters(ring) { - if (ring.length < 3) return 0; - const origin = ring[0]; - const points = ring.map((point) => project(point, origin)); - let twiceArea = 0; - for (let index = 0; index < points.length; index += 1) { - const next = points[(index + 1) % points.length]; - twiceArea += points[index][0] * next[1] - next[0] * points[index][1]; - } - return Math.abs(twiceArea) / 2; -} - -function compileJunctionSurfaces(model, junctionPlans, connectors, movements, diagnostics, options = {}) { - const result = []; - const complexClusters = new Set((options.junctionTemplates?.enabled ? options.junctionTemplates.clusters || [] : []).filter((cluster) => cluster.template === "complex-junction-v1").map((cluster) => cluster.id)); - for (const [nodeId, plan] of junctionPlans) { - if (plan.clusterId && complexClusters.has(plan.clusterId)) continue; - const { segmentIds, node, approaches, cutbackMeters, boundary } = plan; - const junctionConnectors = connectors.filter((feature) => feature.properties.node_id === nodeId); - const junctionMovements = movements.filter((movement) => movement.nodeId === nodeId); - if (boundary.length < 3 || !junctionMovements.length) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-surface-deferred", "路口缺少足够的截面或转向路径,暂不生成路口面。", node)); - continue; - } - const approachAreaMeters = polygonAreaMeters(boundary); - let ring = [...boundary, boundary[0]]; - let boundaryMode = plan.boundaryMode || "approach-envelope"; - if (hasSelfIntersection(ring) || !junctionConnectors.every((feature) => feature.geometry.coordinates.every((point) => pointInOrNearPolygon(point, ring, CONNECTOR_BOUNDARY_TOLERANCE_METERS)))) { - const envelope = convexHull([...boundary, ...junctionConnectors.flatMap((feature) => feature.geometry.coordinates)]); - ring = [...envelope, envelope[0]]; - boundaryMode = "connector-convex-fallback"; - } - if (hasSelfIntersection(ring)) { - diagnostics.push(diagnostic("error", `junction:node/${nodeId}`, [nodeId], "invalid-junction-surface", "路口截面边界发生自相交,未发布路口面。请检查道路方向或路口拓扑。", node)); - continue; - } - const surfaceAreaMeters = polygonAreaMeters(ring); - const expansionRatio = approachAreaMeters > 0 ? surfaceAreaMeters / approachAreaMeters : null; - result.push({ type: "Feature", properties: { native_id: `junction:node/${nodeId}`, osm_node_id: nodeId, kind: segmentIds.size === 3 ? "t" : "cross", source_road_ids: approaches.flatMap((approach) => approach.roadIds).join(","), cutback_m: cutbackMeters, movement_count: junctionMovements.length, connector_count: junctionConnectors.length, boundary_mode: boundaryMode, template: plan.template || null, template_reference: plan.templateReference || null, approach_area_m2: Math.round(approachAreaMeters * 10) / 10, surface_area_m2: Math.round(surfaceAreaMeters * 10) / 10, expansion_ratio: expansionRatio === null ? null : Math.round(expansionRatio * 100) / 100, rule: plan.template ? "junction-cross-template/v1" : "junction-shared-cutback/v4-shared-node-split" }, geometry: { type: "Polygon", coordinates: [ring] } }); - if (boundaryMode === "connector-convex-fallback") diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-connector-envelope-fallback", "路口面需要按转向路径的凸包兜底生成;请检查外缘和路缘与步行带是否符合实际。", node)); - if (plan.boundaryFallbacks) diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-rounded-corner-fallback", "部分路口圆角无法按道路边缘切线安全构造,已对该角使用确定性的直线回退。", node)); - if (!plan.clusterId) diagnostics.push(diagnostic("info", `junction:node/${nodeId}`, [nodeId], "ordinary-junction-surface", "已按道路截面与转向路径生成普通路口面。", node)); - } - for (const cluster of options.junctionTemplates?.enabled ? options.junctionTemplates.clusters || [] : []) { - if (cluster.template === "complex-junction-v1") continue; - const clusterNodes = new Set(cluster.nodeIds.map(String)); - const members = result.filter((feature) => clusterNodes.has(String(feature.properties.osm_node_id))); - if (members.length < 2) continue; - const points = []; - const clusterCoordinates = [...clusterNodes].map((nodeId) => junctionPlans.get(nodeId)?.node).filter(Boolean); - const clusterCenter = clusterCoordinates.reduce((sum, point) => [sum[0] + point[0] / clusterCoordinates.length, sum[1] + point[1] / clusterCoordinates.length], [0, 0]); - for (let index = 0; index < 8; index += 1) points.push(offsetCoordinate(clusterCenter, index * 45, 12)); - for (const [nodeId, plan] of junctionPlans) { - if (!clusterNodes.has(String(nodeId))) continue; - for (const approach of plan.approaches) { - const end = approach.line.at(-1); - if ([...clusterNodes].some((candidate) => candidate !== String(nodeId) && distanceMeters(end, junctionPlans.get(candidate)?.node || [Infinity, Infinity]) < 3)) continue; - const cutback = pointAlongLine(approach.line, Math.min(plan.cutbackMeters, Math.max(12, cluster.approachLengthMeters * .5))); - const heading = headingAtEndpoint(approach.line); const half = approach.widthMeters / 2; - points.push(offsetCoordinate(cutback, heading + 90, half), offsetCoordinate(cutback, heading - 90, half)); - } - const node = plan.node; - for (let index = 0; index < 8; index += 1) points.push(offsetCoordinate(node, index * 45, 9)); - } - const hull = convexHull(points); - if (hull.length < 3) continue; - const ring = roundedHull(hull, 0.22); - const memberIds = new Set(members.map((feature) => feature.properties.native_id)); - for (let index = result.length - 1; index >= 0; index -= 1) if (memberIds.has(result[index].properties.native_id)) result.splice(index, 1); - result.push({ type: "Feature", properties: { native_id: `junction-cluster:${cluster.id}`, osm_node_ids: [...clusterNodes].join(","), kind: "cluster", template: cluster.template, boundary_mode: "cluster-import-core", center: clusterCenter, member_count: members.length, movement_count: movements.filter((movement) => clusterNodes.has(String(movement.nodeId))).length, connector_count: connectors.filter((feature) => clusterNodes.has(String(feature.properties.node_id))).length, surface_area_m2: Math.round(polygonAreaMeters(ring) * 10) / 10, rule: "junction-cluster-template/v2" }, geometry: { type: "Polygon", coordinates: [[...ring, ring[0]]] } }); - diagnostics.push(diagnostic("info", `junction-cluster:${cluster.id}`, [...clusterNodes], "junction-cluster-core-applied", "已按外部进口截面和簇节点核心生成受限复合路口面。", ring[0])); - } - return result; -} - -function activeComplexCluster(options, clusterId) { - return Boolean(clusterId && (options.junctionTemplates?.enabled ? options.junctionTemplates.clusters || [] : []).some((cluster) => cluster.id === clusterId && cluster.template === "complex-junction-v1")); -} - -function compileJunctionPlans(model, options = {}, diagnostics = []) { - const byNode = new Map(); - for (const endpoint of model.endpoints) { - if (!byNode.has(endpoint.nodeId)) byNode.set(endpoint.nodeId, []); - byNode.get(endpoint.nodeId).push(endpoint); - } - const plans = new Map(); - const clusters = options.junctionTemplates?.enabled ? (options.junctionTemplates.clusters || []) : []; - const clusterByNode = new Map(clusters.flatMap((cluster) => cluster.nodeIds.map((nodeId) => [String(nodeId), cluster]))); - for (const [nodeId, endpoints] of byNode) { - const segmentIds = new Set(endpoints.map((endpoint) => endpoint.roadId.replace(/:(forward|backward)$/, ""))); - if (segmentIds.size < 3 || segmentIds.size > 4) continue; - const approaches = junctionApproaches(model, endpoints); - if (approaches.length !== segmentIds.size) continue; - // Rounded curb corners need enough approach length to retain the full - // turning envelope after the corner is cut toward the junction. - const baseCutbackMeters = Math.max(...approaches.map((approach) => approach.widthMeters)) * 1.4; - const node = endpoints[0].coordinate; - const template = junctionTemplateFor(nodeId, segmentIds, options.junctionTemplates, diagnostics, node); - const cutbackMeters = baseCutbackMeters * (template?.cutbackMultiplier || 1); - const boundary = junctionBoundary(approaches, node, cutbackMeters, template?.cornerRadiusMultiplier || 1, template?.approachWidthMultiplier || 1); - if (boundary.points.length < 3) continue; - const cluster = clusterByNode.get(String(nodeId)); - plans.set(nodeId, { node, segmentIds, approaches, cutbackMeters, boundary: boundary.points, boundaryMode: boundary.mode, boundaryFallbacks: boundary.fallbacks, template: template?.template || null, templateReference: template?.referenceFile || null, approachWidthMultiplier: template?.approachWidthMultiplier || 1, approachLengthMeters: template?.approachLengthMeters || 0, clusterId: cluster?.id || null }); - } - return plans; -} - -function junctionApproaches(model, endpoints) { - const groups = new Map(); - for (const endpoint of endpoints) { - const road = model.roads.find((item) => item.id === endpoint.roadId); - if (!road) continue; - const key = road.segmentId; - if (!groups.has(key)) groups.set(key, []); - groups.get(key).push({ endpoint, road }); - } - return [...groups.values()].map((directions) => { - const { endpoint, road } = directions[0]; - return { segmentId: road.segmentId, sourceWayKey: road.osmWayIds.join(","), line: endpoint.side === "end" ? [...road.centerline].reverse() : road.centerline, roadIds: directions.map((item) => item.road.id), widthMeters: directions.reduce((sum, item) => sum + item.road.widthMeters, 0) }; - }); -} - -// `roadRing` builds the trimmed road's end edge from the direction at the -// cutback point, not at the node. Taking the node-side heading here instead -// leaves the two edges non-parallel whenever the way bends inside the cutback, -// and the junction surface then opens a wedge against the road it should meet. -function headingAtCutback(line, cutbackMeters) { - const point = pointAlongLine(line, cutbackMeters); - if (!point) return null; - let traversed = 0; - for (let index = 1; index < line.length; index += 1) { - traversed += distanceMeters(line[index - 1], line[index]); - // The first original vertex past the cutback is what the trimmed line - // carries as its second point, so match that pair exactly. - if (traversed > cutbackMeters + 1e-9) return headingDegrees(point, line[index]); - } - return headingAtEndpoint(line); -} - -// A dual carriageway reaches a node as two approaches on almost the same -// bearing. Their four side points interleave once sorted by angle, and because -// consecutive points then belong to different segments a rounded corner gets -// inserted between each interleaved pair. Those curves dive back toward the -// node and render as arch-shaped holes in the junction. Merge such approaches -// into one face and keep only its outermost edges. -const PARALLEL_APPROACH_DEGREES = 25; - -function signedHeadingDelta(value) { return ((value + 180) % 360 + 360) % 360 - 180; } - -function mergeParallelApproachPoints(points, node) { - const groups = []; - for (const item of points) { - const group = groups.find((candidate) => Math.abs(signedHeadingDelta(candidate.heading - item.outwardHeading)) < PARALLEL_APPROACH_DEGREES); - if (group) { group.points.push(item); continue; } - groups.push({ heading: item.outwardHeading, points: [item] }); - } - return groups.flatMap((group) => { - const segments = [...new Set(group.points.map((item) => item.segmentId))]; - if (segments.length < 2) return group.points; - // Order across the face by bearing measured from the group's own heading, - // so the comparison never straddles the +/-180 discontinuity. - const sorted = [...group.points].sort((first, second) => - signedHeadingDelta(headingDegrees(node, first.point) - group.heading) - signedHeadingDelta(headingDegrees(node, second.point) - group.heading)); - const merged = segments.sort().join("+"); - return [sorted[0], sorted.at(-1)].map((item) => ({ ...item, segmentId: merged, sourceWayKey: merged })); - }); -} - -function junctionBoundary(approaches, node, cutbackMeters, cornerRadiusMultiplier = 1, approachWidthMultiplier = 1) { - const points = []; - for (const approach of approaches) { - const cutback = pointAlongLine(approach.line, cutbackMeters); - if (!cutback) continue; - const heading = headingAtCutback(approach.line, cutbackMeters) ?? headingAtEndpoint(approach.line); - const half = approach.widthMeters * approachWidthMultiplier / 2; - points.push({ point: offsetCoordinate(cutback, heading + 90, half), segmentId: approach.segmentId, sourceWayKey: approach.sourceWayKey, outwardHeading: heading }); - points.push({ point: offsetCoordinate(cutback, heading - 90, half), segmentId: approach.segmentId, sourceWayKey: approach.sourceWayKey, outwardHeading: heading }); - } - const faces = mergeParallelApproachPoints(points, node); - const ordered = faces.sort((a, b) => angleAround(node, a.point) - angleAround(node, b.point)); - if (ordered.length < 3) return { points: [], mode: "approach-envelope" }; - const boundary = []; - let rounded = 0; - let fallbacks = 0; - for (let index = 0; index < ordered.length; index += 1) { - const first = ordered[index]; const second = ordered[(index + 1) % ordered.length]; - boundary.push(first.point); - // One physical OSM way is often split at an intersection node. Its two - // opposite approaches share a continuous road edge; rounding that edge - // bends the far side of a T junction and exposes junction asphalt beyond - // the pedestrian strip. - if (first.segmentId === second.segmentId || isStraightJunctionEdge(first, second)) continue; - const curve = roundedCorner(node, first.point, second.point, first.outwardHeading, second.outwardHeading, cornerRadiusMultiplier); - if (!curve) { fallbacks += 1; continue; } - boundary.push(...curve.slice(1, -1)); - rounded += 1; - } - return { points: boundary, mode: rounded ? "rounded-approach-envelope" : "approach-envelope", fallbacks }; -} - -function templateApproachRing(approach, plan) { - const innerDistance = plan.cutbackMeters; - const availableLength = lineLengthMeters(approach.line) - innerDistance - .5; - const lengthMeters = Math.min(plan.approachLengthMeters, availableLength); - if (lengthMeters < 10) return null; - const outerDistance = innerDistance + lengthMeters; - const inner = pointAlongLine(approach.line, innerDistance); - const outer = pointAlongLine(approach.line, outerDistance); - const heading = headingAtEndpoint(approach.line); - const innerHalf = approach.widthMeters * plan.approachWidthMultiplier / 2; - const outerHalf = approach.widthMeters / 2; - const ring = [ - offsetCoordinate(outer, heading + 90, outerHalf), - offsetCoordinate(inner, heading + 90, innerHalf), - offsetCoordinate(inner, heading - 90, innerHalf), - offsetCoordinate(outer, heading - 90, outerHalf), - offsetCoordinate(outer, heading + 90, outerHalf), - ]; - return ring.every((point) => point.every(Number.isFinite)) ? { ring, lengthMeters } : null; -} - -function roundedHull(hull, factor) { - const result = []; - for (let index = 0; index < hull.length; index += 1) { - const previous = hull[(index - 1 + hull.length) % hull.length]; - const current = hull[index]; - const next = hull[(index + 1) % hull.length]; - const entry = interpolate(previous, current, factor); - const exit = interpolate(current, next, factor); - result.push(entry); - const curve = quadraticCurve(entry, current, exit, 4); - result.push(...curve.slice(1, -1)); - result.push(exit); - } - return result; -} - -function isStraightJunctionEdge(first, second) { - if (first.sourceWayKey !== second.sourceWayKey) return false; - const radians = (first.outwardHeading - second.outwardHeading) * Math.PI / 180; - return Math.cos(radians) <= -0.98; -} - -function roundedCorner(node, first, second, firstHeading, secondHeading, radiusMultiplier = 1) { - const origin = node; - const a = project(first, origin); const b = project(second, origin); - const chord = Math.hypot(a[0] - b[0], a[1] - b[1]); - if (chord < .5 || !Number.isFinite(firstHeading) || !Number.isFinite(secondHeading)) return null; - const firstDirection = headingVector(firstHeading); - const secondDirection = headingVector(secondHeading); - const intersection = lineIntersection(a, firstDirection, b, secondDirection); - if (!intersection) return null; - const controlDistance = Math.hypot(...intersection); - const endpointDistance = Math.max(Math.hypot(...a), Math.hypot(...b)); - // Adjacent approach edge tangents should meet in the corner between the - // node and the cutback. Reject near-parallel or remote intersections rather - // than publishing a huge/self-crossing curve. - if (controlDistance < .01 || controlDistance > endpointDistance * 1.5 || controlDistance > 80) return null; - const scaledIntersection = [intersection[0] * radiusMultiplier, intersection[1] * radiusMultiplier]; - const control = unproject(scaledIntersection, origin); - return quadraticCurve(first, control, second, JUNCTION_CURVE_SEGMENTS); -} - -function junctionTemplateFor(nodeId, segmentIds, configured, diagnostics, node) { - if (!configured?.enabled) return null; - const entry = (configured.references || []).find((item) => String(item.nodeId) === String(nodeId)); - if (!entry) return null; - if (segmentIds.size !== 4) { - diagnostics.push(diagnostic("info", `junction:node/${nodeId}`, [nodeId], "junction-template-topology-skip", "cross 模板只应用于四臂路口,当前路口保留 native 几何。", node)); - return null; - } - if (entry.template !== "cross-v1") { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-unsupported", "路口模板名称不受支持,已回退 native 几何。", node)); - return null; - } - const multiplier = Number(entry.cornerRadiusMultiplier ?? 1); - if (!Number.isFinite(multiplier) || multiplier < 0.75 || multiplier > 1.25) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-invalid-parameter", "cross 模板圆角参数必须在 0.75 到 1.25 之间,已回退 native 几何。", node)); - return null; - } - const cutbackMultiplier = Number(entry.cutbackMultiplier ?? 1); - if (!Number.isFinite(cutbackMultiplier) || cutbackMultiplier < 1 || cutbackMultiplier > 1.35) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-invalid-parameter", "cross 模板进口过渡参数必须在 1 到 1.35 之间,已回退 native 几何。", node)); - return null; - } - const approachWidthMultiplier = Number(entry.approachWidthMultiplier ?? 1); - if (!Number.isFinite(approachWidthMultiplier) || approachWidthMultiplier < 1 || approachWidthMultiplier > 1.8) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-invalid-parameter", "cross 模板进口宽度参数必须在 1 到 1.8 之间,已回退 native 几何。", node)); - return null; - } - const approachLengthMeters = Number(entry.approachLengthMeters ?? 24); - if (!Number.isFinite(approachLengthMeters) || approachLengthMeters < 10 || approachLengthMeters > 50) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-invalid-parameter", "cross 模板进口过渡长度必须在 10 到 50 米之间,已回退 native 几何。", node)); - return null; - } - if (entry.referenceFile && !fs.existsSync(entry.referenceFile)) { - diagnostics.push(diagnostic("warning", `junction:node/${nodeId}`, [nodeId], "junction-template-reference-missing", "路口参考文件不存在,已回退 native 几何。", node)); - return null; - } - diagnostics.push(diagnostic("info", `junction:node/${nodeId}`, [nodeId], "junction-template-applied", "已按 cross-v1 模板规整路口面;道路、车道、连接器和控制设施保持 native 结果。", node)); - return { template: entry.template, referenceFile: entry.referenceFile || null, cornerRadiusMultiplier: multiplier, cutbackMultiplier, approachWidthMultiplier, approachLengthMeters }; -} - -function headingVector(degrees) { - const radians = degrees * Math.PI / 180; - return [Math.sin(radians), Math.cos(radians)]; -} - -function lineIntersection(firstPoint, firstDirection, secondPoint, secondDirection) { - const cross = firstDirection[0] * secondDirection[1] - firstDirection[1] * secondDirection[0]; - if (Math.abs(cross) < 1e-4) return null; - const delta = [secondPoint[0] - firstPoint[0], secondPoint[1] - firstPoint[1]]; - const firstDistance = (delta[0] * secondDirection[1] - delta[1] * secondDirection[0]) / cross; - return [firstPoint[0] + firstDirection[0] * firstDistance, firstPoint[1] + firstDirection[1] * firstDistance]; -} - -function pointAlongLine(line, meters) { - let remaining = meters; - for (let index = 1; index < line.length; index += 1) { - const length = distanceMeters(line[index - 1], line[index]); - if (length >= remaining) return interpolate(line[index - 1], line[index], remaining / length); - remaining -= length; - } - return line.at(-1); -} - -function pointAndAxisAlongLine(line, meters) { - let remaining = meters; - for (let index = 1; index < line.length; index += 1) { - const start = line[index - 1]; const end = line[index]; - const length = distanceMeters(start, end); - if (length < 0.01) continue; - if (length >= remaining) { - const vector = project(end, start); - return { point: interpolate(start, end, remaining / length), axis: [vector[0] / length, vector[1] / length] }; - } - remaining -= length; - } - return null; -} - -function trimLineAtJunctions(line, sourceNodeIds, junctionPlans) { - const startCutback = junctionPlans.get(sourceNodeIds[0])?.cutbackMeters || 0; - const endCutback = junctionPlans.get(sourceNodeIds.at(-1))?.cutbackMeters || 0; - if (!startCutback && !endCutback) return line; - const total = lineLengthMeters(line); - // Short OSM fragments cannot safely lose both ends. Keep their source - // geometry intact and let the junction diagnostic surface the ambiguity. - if (startCutback + endCutback >= total - 0.5) return line; - const result = []; - let traversed = 0; - const start = pointAlongLine(line, startCutback); - const end = pointAlongLine(line, total - endCutback); - result.push(start); - for (let index = 1; index < line.length - 1; index += 1) { - traversed += distanceMeters(line[index - 1], line[index]); - if (traversed > startCutback && traversed < total - endCutback) result.push(line[index]); - } - result.push(end); - return result; -} - -function trimLineAtComplexCluster(line, sourceNodeIds, junctionPlans, cluster, center) { - if (!center || line.length < 2) return trimLineAtJunctions(line, sourceNodeIds, junctionPlans); - const boundaryRadius = complexJunctionMetrics(cluster).approachOuterRadius; - const startInCluster = cluster.nodeIds.map(String).includes(String(sourceNodeIds[0])); - const endInCluster = cluster.nodeIds.map(String).includes(String(sourceNodeIds.at(-1))); - const available = lineLengthMeters(line); - const startDistance = startInCluster ? distanceAlongLineToRadius(line, center, boundaryRadius) : 0; - const endDistance = endInCluster ? distanceAlongLineToRadius([...line].reverse(), center, boundaryRadius) : 0; - const startCutback = startDistance > 0 && available > startDistance + 1 ? startDistance : 0; - const endCutback = endDistance > 0 && available > endDistance + 1 ? endDistance : 0; - if (!startCutback && !endCutback) return line; - return trimLineRange(line, startCutback, endCutback); -} - -function distanceAlongLineToRadius(line, center, radius) { - if (!center || line.length < 2) return 0; - const heading = headingAtEndpoint(line); - const vector = project(line[0], center); - const radians = heading * Math.PI / 180; - const startRadius = vector[0] * Math.sin(radians) + vector[1] * Math.cos(radians); - return Math.max(0, radius - startRadius); -} - -function trimLineRange(line, startCutback, endCutback) { - const total = lineLengthMeters(line); - if (startCutback + endCutback >= total - .5) return line; - const result = [pointAlongLine(line, startCutback)]; - let traversed = 0; - for (let index = 1; index < line.length - 1; index += 1) { - traversed += distanceMeters(line[index - 1], line[index]); - if (traversed > startCutback && traversed < total - endCutback) result.push(line[index]); - } - result.push(pointAlongLine(line, total - endCutback)); - return result; -} - -function headingAtEndpoint(line) { return headingDegrees(line[0], line[1]); } -function headingDegrees(a, b) { return Math.atan2((b[0] - a[0]) * Math.cos(a[1] * Math.PI / 180), b[1] - a[1]) * 180 / Math.PI; } -function offsetCoordinate(point, degrees, meters) { const radians = degrees * Math.PI / 180; return [point[0] + Math.sin(radians) * meters / (111320 * Math.cos(point[1] * Math.PI / 180)), point[1] + Math.cos(radians) * meters / 111320]; } -function angleAround(center, point) { return Math.atan2(point[1] - center[1], point[0] - center[0]); } -function sortAround(center, points) { return points.sort((a, b) => Math.atan2(a[1] - center[1], a[0] - center[0]) - Math.atan2(b[1] - center[1], b[0] - center[0])); } -function convexHull(points) { - const unique = [...new Map(points.map((point) => [`${point[0]},${point[1]}`, point])).values()].sort((a, b) => a[0] - b[0] || a[1] - b[1]); - if (unique.length < 3) return unique; - const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); - const lower = []; for (const point of unique) { while (lower.length >= 2 && cross(lower.at(-2), lower.at(-1), point) <= 0) lower.pop(); lower.push(point); } - const upper = []; for (const point of [...unique].reverse()) { while (upper.length >= 2 && cross(upper.at(-2), upper.at(-1), point) <= 0) upper.pop(); upper.push(point); } - return [...lower.slice(0, -1), ...upper.slice(0, -1)]; -} -function interpolate(a, b, ratio) { return [a[0] + (b[0] - a[0]) * ratio, a[1] + (b[1] - a[1]) * ratio]; } -function distanceMeters(a, b) { const dx = (b[0] - a[0]) * 111320 * Math.cos(a[1] * Math.PI / 180); const dy = (b[1] - a[1]) * 111320; return Math.hypot(dx, dy); } -function hasSelfIntersection(ring) { - for (let first = 0; first < ring.length - 1; first += 1) for (let second = first + 1; second < ring.length - 1; second += 1) { - if (Math.abs(first - second) <= 1 || first === 0 && second === ring.length - 2) continue; - if (segmentsIntersect(ring[first], ring[first + 1], ring[second], ring[second + 1])) return true; - } - return false; -} -function segmentsIntersect(a, b, c, d) { - const cross = (p, q, r) => (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]); - const abC = cross(a, b, c); const abD = cross(a, b, d); const cdA = cross(c, d, a); const cdB = cross(c, d, b); - return (abC > 0 && abD < 0 || abC < 0 && abD > 0) && (cdA > 0 && cdB < 0 || cdA < 0 && cdB > 0); -} - -function circleRing(center, radius, segments) { - const origin = center; - const ring = []; - for (let index = 0; index <= segments; index += 1) { - const angle = index / segments * Math.PI * 2; - ring.push(unproject([Math.cos(angle) * radius, Math.sin(angle) * radius], origin)); - } - return ring; -} - -// Offsetting every vertex by a fixed distance along its averaged normal has no -// miter limit: where the centerline turns and the neighbouring segment is short -// — typically the stub left after junction trimming — consecutive offset points -// swap order and the edge doubles back. The ring then self-intersects and the -// folded lobe renders as a hole. Drop the reversed vertices so each offset -// edge keeps travelling the same way as the centerline segment it follows. -function removeOffsetFolds(offset, centerline) { - let kept = offset.map((point, index) => ({ point, index })); - for (let guard = 0; guard < offset.length && kept.length > 2; guard += 1) { - let removed = false; - for (let position = 0; position < kept.length - 1; position += 1) { - const from = kept[position]; const to = kept[position + 1]; - const alongCenter = [centerline[to.index][0] - centerline[from.index][0], centerline[to.index][1] - centerline[from.index][1]]; - const alongOffset = [to.point[0] - from.point[0], to.point[1] - from.point[1]]; - if (alongCenter[0] * alongOffset[0] + alongCenter[1] * alongOffset[1] >= 0) continue; - // Keep both termini: they are where the surface meets its junctions. - kept.splice(position + 1 === kept.length - 1 ? position : position + 1, 1); - removed = true; - break; - } - if (!removed) break; - } - return kept.map((item) => item.point); -} - -function roadRing(line, width) { - if (line.length < 2 || !Number.isFinite(width)) return null; - const origin = line[0]; - const points = line.map((point) => project(point, origin)); - const left = []; const right = []; - const half = width / 2; - for (let i = 0; i < points.length; i += 1) { - const prior = points[Math.max(0, i - 1)]; const next = points[Math.min(points.length - 1, i + 1)]; - const dx = next[0] - prior[0]; const dy = next[1] - prior[1]; const length = Math.hypot(dx, dy); - if (length < 0.01) return null; - const nx = -dy / length * half; const ny = dx / length * half; - left.push([points[i][0] + nx, points[i][1] + ny]); - right.push([points[i][0] - nx, points[i][1] - ny]); - } - const leftEdge = removeOffsetFolds(left, points).map((point) => unproject(point, origin)); - const rightEdge = removeOffsetFolds(right, points).map((point) => unproject(point, origin)); - if (leftEdge.length < 2 || rightEdge.length < 2) return null; - const ring = [...leftEdge, ...rightEdge.reverse(), leftEdge[0]]; - return ring.every((point) => point.every(Number.isFinite)) ? ring : null; -} - -function sidewalkRing(line, innerOffset, outerOffset, side) { - const inner = offsetLine(line, innerOffset * side); - const outer = offsetLine(line, outerOffset * side); - if (!inner || !outer) return null; - const ring = [...inner, ...outer.reverse(), inner[0]]; - return ring.every((point) => point.every(Number.isFinite)) ? ring : null; -} - -function project(point, origin) { const scale = 111320; return [(point[0] - origin[0]) * scale * Math.cos(origin[1] * Math.PI / 180), (point[1] - origin[1]) * scale]; } -function unproject(point, origin) { const scale = 111320; return [point[0] / (scale * Math.cos(origin[1] * Math.PI / 180)) + origin[0], point[1] / scale + origin[1]]; } -function diagnostic(severity, subjectId, sourceIds, rule, message, coordinate) { return { id: `diagnostic:${rule}:${subjectId}`, severity, subjectId, sourceIds, rule, message, geometry: coordinate ? { type: "Point", coordinates: coordinate } : null }; } -function xmlAttrs(text) { const attrs = {}; for (const match of text.matchAll(/([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) attrs[match[1]] = match[2] ?? match[3]; return attrs; } -function parseTags(body) { const tags = {}; for (const match of body.matchAll(/]*)\/?\s*>/g)) { const attrs = xmlAttrs(match[1]); if (attrs.k) tags[attrs.k] = attrs.v || ""; } return tags; } -function positiveInteger(value) { const number = Number(value); return Number.isInteger(number) && number > 0 ? number : null; } -function positiveNumber(value) { const match = String(value ?? "").match(/^\s*(\d+(?:\.\d+)?)/); const number = match ? Number(match[1]) : null; return Number.isFinite(number) && number > 0 ? number : null; } -function writeJsonAtomic(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temporary = `${file}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`); fs.renameSync(temporary, file); } - -module.exports = { OVERRIDE_SCHEMA, compileRoadModel, compileGeometry, loadOverrides, validateOverrides, writeJsonAtomic }; diff --git a/packages/road-compiler/src/compile/turn-lane-arrows.js b/packages/road-compiler/src/compile/turn-lane-arrows.js deleted file mode 100644 index 4230679..0000000 --- a/packages/road-compiler/src/compile/turn-lane-arrows.js +++ /dev/null @@ -1,502 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const { laneCenterline } = require("../geometry/lane-geometry"); - -const ASSET_MANIFEST = path.resolve(__dirname, "..", "..", "..", "..", "assets", "lane-icons", "manifest.json"); -const LANE_WIDTH_METERS = 3.2; -const PLACEMENT_DISTANCE_METERS = 9; -const SPATIAL_MATCH_MAX_DISTANCE_METERS = 18; -const SPATIAL_MATCH_MIN_ALIGNMENT = Math.cos(Math.PI / 6); - -// Existing osm2streets lane arrows are approximately 1.4 m across. Keep the -// 25-unit upstream icon at the same on-road scale rather than at screen scale. -const SVG_METERS_PER_UNIT = 0.10; - -function loadManifest(file = ASSET_MANIFEST) { - const manifest = JSON.parse(fs.readFileSync(file, "utf8")); - if (!Array.isArray(manifest.assets)) throw new Error("turn-lane asset manifest has no assets array"); - return manifest; -} - -function supportedAssets(manifest = loadManifest()) { - return new Map(manifest.assets - .filter((asset) => asset.supported === true && asset.tested === true) - .map((asset) => [asset.id, asset])); -} - -function buildCustomTurnLaneArrows(osm, options = {}) { - const enabled = options.enabled === true; - const diagnostics = []; - if (!enabled) return { features: [], diagnostics: [{ reason: "disabled" }] }; - const assets = supportedAssets(options.manifest); - const endpointRoadCounts = roadCountsByNode(osm); - const networkIntersectionNodes = new Set((options.network?.intersections || []) - .flatMap(([, intersection]) => intersection.osm_ids || []).map(Number)); - const features = []; - const ways = [...osm.ways.values()].sort((a, b) => a.id - b.id); - for (const way of ways) { - for (const direction of ["forward", "backward"]) { - const tag = way.tags[`turn:lanes:${direction}`]; - if (!tag) continue; - const laneCount = directionalLaneCount(way, direction); - if (!laneCount) { - diagnostics.push(skip(way, direction, "missing_lane_count")); - continue; - } - const endpoint = endpointGeometry(osm, way, direction, endpointRoadCounts, networkIntersectionNodes); - if (!endpoint) { - diagnostics.push(skip(way, direction, "indeterminate_intersection_endpoint")); - continue; - } - const maneuvers = String(tag).split("|").map((value) => normalizeManeuver(value)); - for (let laneIndex = 0; laneIndex < maneuvers.length; laneIndex += 1) { - const maneuver = maneuvers[laneIndex]; - const asset = assets.get(maneuver); - if (!asset) { - diagnostics.push(skip(way, direction, "unsupported_or_untested_maneuver", { lane_index: laneIndex, maneuver })); - continue; - } - if (laneIndex >= laneCount) { - diagnostics.push(skip(way, direction, "lane_index_exceeds_lane_count", { lane_index: laneIndex, maneuver })); - continue; - } - const resolvedPlacement = lanePlacement(way, direction, laneIndex, endpoint, options.lanePolygons, options.crosswalkStripes, options.stopLines); - if (resolvedPlacement?.blocked) { - diagnostics.push(skip(way, direction, "no_safe_turn_arrow_position", { lane_index: laneIndex, maneuver })); - continue; - } - const placement = resolvedPlacement || fallbackLanePlacement(endpoint, direction, laneIndex, options.crosswalkStripes, options.stopLines); - if (!placement) { - diagnostics.push(skip(way, direction, "no_safe_turn_arrow_position", { lane_index: laneIndex, maneuver })); - continue; - } - const parts = templateFor(asset.id, options.manifest); - for (let partIndex = 0; partIndex < parts.length; partIndex += 1) { - features.push(makeFeature(way, direction, laneIndex, maneuver, asset, partIndex, parts[partIndex], placement.center, placement)); - } - } - } - } - return { features, diagnostics }; -} - -function normalizeManeuver(value) { - const parts = String(value || "").split(";").map((part) => part.trim()).filter(Boolean).sort(); - const supported = new Map([ - ["through", "through"], ["left", "left"], ["right", "right"], - ["left;through", "through;left"], ["right;through", "through;right"], - ["left;right;through", "through;left;right"], - ]); - return supported.get(parts.join(";")) || parts.join(";"); -} - -function directionalLaneCount(way, direction) { - const specific = Number(way.tags[`lanes:${direction}`]); - if (Number.isInteger(specific) && specific > 0) return specific; - const total = Number(way.tags.lanes); - if (Number.isInteger(total) && total > 0 && total % 2 === 0 && !isOneway(way)) return total / 2; - if (Number.isInteger(total) && total > 0 && isOneway(way)) return total; - return null; -} - -function roadCountsByNode(osm) { - const out = new Map(); - for (const way of osm.ways.values()) { - if (!way.tags.highway || way.tags.highway === "service") continue; - for (const ref of new Set(way.refs)) out.set(ref, (out.get(ref) || 0) + 1); - } - return out; -} - -function endpointGeometry(osm, way, direction, roadCounts, networkIntersectionNodes) { - const forward = direction === "forward"; - const endpointIndex = forward ? way.refs.length - 1 : 0; - const neighborIndex = forward ? endpointIndex - 1 : 1; - const node = osm.nodes.get(way.refs[endpointIndex]); - const neighbor = osm.nodes.get(way.refs[neighborIndex]); - if (!node || !neighbor) return null; - const networkSaysIntersection = networkIntersectionNodes && networkIntersectionNodes.size > 0 && networkIntersectionNodes.has(node.id); - if (!networkSaysIntersection && (roadCounts.get(node.id) || 0) < 3) return null; - const meters = metersForLat(node.lat); - // For both directions, point from the adjacent road node to the endpoint. - // At a forward endpoint this is the OSM-way direction; at a backward - // endpoint it is the reverse OSM-way direction, i.e. the actual travel - // direction used by turn:lanes:backward. - const raw = [node.lon - neighbor.lon, node.lat - neighbor.lat]; - const axis = normalizeMetersVector(raw, meters); - if (!axis) return null; - return { node, axis, right: [axis[1], -axis[0]], meters }; -} - -function laneCenter(endpoint, direction, laneIndex, meters) { - const lateral = (laneIndex + 0.5) * LANE_WIDTH_METERS; - // The local axis always follows travel, so moving back from either endpoint - // places the marking on its approach lane before the intersection. - return addMeters([endpoint.node.lon, endpoint.node.lat], endpoint.axis, -PLACEMENT_DISTANCE_METERS, endpoint.right, lateral, meters); -} - -function fallbackLanePlacement(endpoint, direction, laneIndex, crosswalkStripes, stopLines) { - const lateral = (laneIndex + 0.5) * LANE_WIDTH_METERS; - for (const distance of [PLACEMENT_DISTANCE_METERS, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]) { - const center = addMeters([endpoint.node.lon, endpoint.node.lat], endpoint.axis, -distance, endpoint.right, lateral, endpoint.meters); - if (!nearIntersectionMarking(center, endpoint.axis, crosswalkStripes, stopLines, endpoint.meters)) { - return { center, axis: endpoint.axis, right: endpoint.right, meters: endpoint.meters, placementDistance: distance, placementSource: "osm_way_fallback" }; - } - } - return null; -} - -function lanePlacement(way, direction, laneIndex, endpoint, lanePolygons, crosswalkStripes, stopLines) { - if (!Array.isArray(lanePolygons)) return null; - const expectedDirection = direction === "forward" ? "Fwd" : "Back"; - const directionalCandidates = lanePolygons.filter((feature) => - feature.properties?.type === "Driving" && - feature.properties.direction === expectedDirection - ); - let candidates = directionalCandidates.filter((feature) => - (feature.properties.osm_way_ids || []).map(Number).includes(way.id) - ); - let placementSource = "driving_lane_centerline"; - let spatialAnchors = null; - if (!candidates.length) { - const ranked = directionalCandidates - .map((feature) => ({ feature, anchor: spatialLaneAnchor(feature, endpoint) })) - .filter(({ anchor }) => anchor) - .filter(({ anchor }) => anchor.alignment >= SPATIAL_MATCH_MIN_ALIGNMENT && anchor.distance <= SPATIAL_MATCH_MAX_DISTANCE_METERS) - .sort((a, b) => a.anchor.distance - b.anchor.distance || a.anchor.lateral - b.anchor.lateral || Number(a.feature.properties.index) - Number(b.feature.properties.index)); - if (ranked.length) { - // JOSM may split a tagged OSM way into temporary negative IDs. Those IDs - // are absent from osm2streets' rendered polygons, so associate the full - // physical approach by endpoint proximity and road-axis alignment. - candidates = ranked.map(({ feature }) => feature); - placementSource = "spatial_driving_lane_centerline"; - spatialAnchors = new Map(ranked.map(({ feature, anchor }) => [feature, anchor])); - } - } - candidates.sort((a, b) => { - const lateralA = spatialAnchors?.get(a)?.lateral; - const lateralB = spatialAnchors?.get(b)?.lateral; - if (Number.isFinite(lateralA) && Number.isFinite(lateralB) && lateralA !== lateralB) return lateralA - lateralB; - return Number(a.properties.index) - Number(b.properties.index); - }); - const lane = candidates[laneIndex]; - const spatialAnchor = spatialAnchors?.get(lane); - if (spatialAnchor) { - const sampled = placementDistances().map((distance) => ({ - center: sampleCenterlineAwayFromEndpoint(spatialAnchor, distance, endpoint.meters), - distance, - })).find(({ center }) => center && !nearIntersectionMarking(center, spatialAnchor.axis, crosswalkStripes, stopLines, endpoint.meters)); - if (!sampled) return { blocked: true }; - return { center: sampled.center, axis: spatialAnchor.axis, right: [spatialAnchor.axis[1], -spatialAnchor.axis[0]], meters: endpoint.meters, placementDistance: sampled.distance, placementSource }; - } - const centerline = laneCenterline(lane); - if (!centerline) return null; - const startsAtEndpoint = direction === "backward"; - const ordered = startsAtEndpoint ? centerline : [...centerline].reverse(); - const sampled = [PLACEMENT_DISTANCE_METERS, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42] - .map((distance) => ({ center: samplePolyline(ordered, distance, endpoint.meters), distance })) - .find(({ center }) => center && !nearIntersectionMarking(center, axisForLane(ordered, endpoint.meters), crosswalkStripes, stopLines, endpoint.meters)); - if (!sampled) return { blocked: true }; - const axis = axisForLane(ordered, endpoint.meters); - if (!axis) return null; - return { center: sampled.center, axis, right: [axis[1], -axis[0]], meters: endpoint.meters, placementDistance: sampled.distance, placementSource }; -} - -function spatialLaneAnchor(lane, endpoint) { - const centerline = laneCenterline(lane); - if (!centerline) return null; - let best = null; - for (let index = 0; index < centerline.length - 1; index += 1) { - const start = centerline[index]; - const end = centerline[index + 1]; - const point = closestPointOnSegment([endpoint.node.lon, endpoint.node.lat], start, end, endpoint.meters); - const distance = Math.hypot((point[0] - endpoint.node.lon) * endpoint.meters.lon, (point[1] - endpoint.node.lat) * endpoint.meters.lat); - const tangent = normalizeMetersVector(subtractPoint(end, start), endpoint.meters); - if (!tangent || (best && distance >= best.distance)) continue; - const dot = tangent[0] * endpoint.axis[0] + tangent[1] * endpoint.axis[1]; - const axis = dot >= 0 ? tangent : [-tangent[0], -tangent[1]]; - const offset = subtractPoint(point, [endpoint.node.lon, endpoint.node.lat]); - best = { - point, - distance, - axis, - alignment: Math.abs(dot), - lateral: offset[0] * endpoint.right[0] * endpoint.meters.lon + offset[1] * endpoint.right[1] * endpoint.meters.lat, - centerline, - segmentIndex: index, - }; - } - return best; -} - -function placementDistances() { - return [PLACEMENT_DISTANCE_METERS, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]; -} - -function sampleCenterlineAwayFromEndpoint(anchor, distanceMeters, meters) { - const { centerline, segmentIndex } = anchor; - const start = centerline[segmentIndex]; - const end = centerline[segmentIndex + 1]; - const tangent = normalizeMetersVector(subtractPoint(end, start), meters); - if (!tangent) return null; - // Walk away from the junction along the rendered centerline. This preserves - // curved or split lane geometry instead of approximating it with a tangent. - const towardEnd = tangent[0] * anchor.axis[0] + tangent[1] * anchor.axis[1] < 0; - const points = [anchor.point]; - if (towardEnd) { - for (let index = segmentIndex + 1; index < centerline.length; index += 1) points.push(centerline[index]); - } else { - for (let index = segmentIndex; index >= 0; index -= 1) points.push(centerline[index]); - } - return samplePolyline(points, distanceMeters, meters); -} - -function closestPointOnSegment(point, start, end, meters) { - const dx = (end[0] - start[0]) * meters.lon; - const dy = (end[1] - start[1]) * meters.lat; - const px = (point[0] - start[0]) * meters.lon; - const py = (point[1] - start[1]) * meters.lat; - const lengthSquared = dx * dx + dy * dy; - const ratio = lengthSquared ? Math.max(0, Math.min(1, (px * dx + py * dy) / lengthSquared)) : 0; - return [start[0] + ratio * (end[0] - start[0]), start[1] + ratio * (end[1] - start[1])]; -} - -function axisForLane(ordered, meters) { - return normalizeMetersVector(subtractPoint(ordered[0], ordered[1]), meters); -} - -function nearIntersectionMarking(center, axis, stripes, stopLines, meters) { - if (!axis) return true; - const right = [axis[1], -axis[0]]; - const samples = []; - for (const forward of [-0.2, 0.5, 1.2, 1.9, 2.2]) { - for (const lateral of [-1.6, -0.8, 0, 0.8, 1.6]) { - samples.push(addMeters(center, axis, forward, right, lateral, meters)); - } - } - return [...(stripes || []), ...(stopLines || [])].some((feature) => samples.some((point) => nearFeature(point, feature, meters))); -} - -function nearFeature(point, feature, meters) { - const ring = feature.geometry?.type === "Polygon" ? feature.geometry.coordinates?.[0] : null; - if (!ring?.length) return false; - const xs = ring.map((coordinate) => coordinate[0]); - const ys = ring.map((coordinate) => coordinate[1]); - const clearance = 0.7; - const dx = Math.max((Math.min(...xs) - point[0]) * meters.lon, 0, (point[0] - Math.max(...xs)) * meters.lon); - const dy = Math.max((Math.min(...ys) - point[1]) * meters.lat, 0, (point[1] - Math.max(...ys)) * meters.lat); - return Math.hypot(dx, dy) < clearance; -} - -function subtractPoint([lon, lat], [otherLon, otherLat]) { - return [lon - otherLon, lat - otherLat]; -} - -function samplePolyline(points, distanceMeters, meters) { - let remaining = distanceMeters; - for (let index = 0; index < points.length - 1; index += 1) { - const start = points[index]; - const end = points[index + 1]; - const vector = normalizeMetersVector(subtractPoint(end, start), meters); - const length = Math.hypot((end[0] - start[0]) * meters.lon, (end[1] - start[1]) * meters.lat); - if (!vector || !length) continue; - if (remaining <= length) return addMeters(start, vector, remaining, [0, 0], 0, meters); - remaining -= length; - } - return null; -} - -function makeFeature(way, direction, laneIndex, maneuver, asset, partIndex, template, center, endpoint) { - const ring = template.map(([rightMeters, forwardMeters]) => addMeters(center, endpoint.axis, forwardMeters, endpoint.right, rightMeters, endpoint.meters)); - return { - type: "Feature", - properties: { - type: "lane arrow", - source: "osm_turn_lanes", - osm_way_id: way.id, - direction, - lane_index: laneIndex, - maneuver, - source_asset: asset.id, - source_asset_path: asset.source, - arrow_part: partIndex, - // SVG strokes and fills are expanded separately for GeoJSON validity. - // This stable key lets the QGIS normalizer restore one rendered arrow. - custom_arrow_id: `${way.id}:${direction}:${laneIndex}:${maneuver}`, - placement_distance_meters: endpoint.placementDistance ?? PLACEMENT_DISTANCE_METERS, - placement_source: endpoint.placementSource ?? "osm_way_fallback", - }, - geometry: { type: "Polygon", coordinates: [ring] }, - }; -} - -function skip(way, direction, reason, extra = {}) { - return { source: "osm_turn_lanes", osm_way_id: way.id, direction, reason, ...extra }; -} - -function isOneway(way) { - return ["yes", "true", "1"].includes(String(way.tags.oneway || "").toLowerCase()); -} - -function metersForLat(lat) { - return { lon: 111320 * Math.cos((lat * Math.PI) / 180), lat: 110540 }; -} - -function normalizeMetersVector([dxLon, dyLat], meters) { - const x = dxLon * meters.lon; - const y = dyLat * meters.lat; - const length = Math.hypot(x, y); - return length ? [x / length, y / length] : null; -} - -function addMeters(center, axis, axisDistance, right, rightDistance, meters) { - return [ - center[0] + (axis[0] * axisDistance + right[0] * rightDistance) / meters.lon, - center[1] + (axis[1] * axisDistance + right[1] * rightDistance) / meters.lat, - ]; -} - -function arrowRingsAt(maneuver, center, axis, manifest = loadManifest()) { - const normalized = normalizeManeuver(maneuver); - if (!supportedAssets(manifest).has(normalized) || !Array.isArray(center) || !Array.isArray(axis)) return []; - const meters = metersForLat(center[1]); - const length = Math.hypot(axis[0], axis[1]); - if (!Number.isFinite(length) || length < 0.001) return []; - const forward = [axis[0] / length, axis[1] / length]; - const right = [forward[1], -forward[0]]; - return templateFor(normalized, manifest).map((template) => template.map(([rightMeters, forwardMeters]) => - addMeters(center, forward, forwardMeters, right, rightMeters, meters))); -} - -function templateFor(assetId, manifest = loadManifest()) { - const asset = supportedAssets(manifest).get(assetId); - if (!asset) throw new Error(`Unsupported or untested turn-lane asset: ${assetId}`); - return angularTemplate(assetId); -} - -function angularTemplate(assetId) { - const shaftWidth = 0.30; - const shaftHalf = shaftWidth / 2; - const straightBase = 1.18; - const straightTip = 1.92; - const rectangle = (minX, minY, maxX, maxY) => [ - [minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY], [minX, minY], - ]; - const throughHead = () => [[0, straightTip], [-0.42, straightBase], [-shaftHalf, straightBase], [-shaftHalf, 0], [shaftHalf, 0], [shaftHalf, straightBase], [0.42, straightBase], [0, straightTip]]; - const diagonalShaft = (side) => { - const start = [0, 0.56]; - const end = [side * 0.72, 0.96]; - const length = Math.hypot(end[0] - start[0], end[1] - start[1]); - const normal = [-(end[1] - start[1]) / length * shaftHalf, (end[0] - start[0]) / length * shaftHalf]; - return [[start[0] + normal[0], start[1] + normal[1]], [end[0] + normal[0], end[1] + normal[1]], [end[0] - normal[0], end[1] - normal[1]], [start[0] - normal[0], start[1] - normal[1]], [start[0] + normal[0], start[1] + normal[1]]]; - }; - const diagonalHead = (side) => { - const base = [side * 0.60, 0.89]; - const tip = [side * 1.22, 1.24]; - const length = Math.hypot(tip[0] - base[0], tip[1] - base[1]); - const normal = [-(tip[1] - base[1]) / length * 0.36, (tip[0] - base[0]) / length * 0.36]; - return [tip, [base[0] + normal[0], base[1] + normal[1]], [base[0] - normal[0], base[1] - normal[1]], tip]; - }; - const turnStem = (side) => { - const cutMidpoint = 0.73; - const cutRise = side * 0.084; - return [ - [-shaftHalf, 0], [shaftHalf, 0], - [shaftHalf, cutMidpoint + cutRise], [-shaftHalf, cutMidpoint - cutRise], - [-shaftHalf, 0], - ]; - }; - if (assetId === "through") return [throughHead()]; - if (assetId === "right") return [turnStem(1), diagonalShaft(1), diagonalHead(1)]; - if (assetId === "left") return [turnStem(-1), diagonalShaft(-1), diagonalHead(-1)]; - if (assetId === "through;right") return [throughHead(), diagonalShaft(1), diagonalHead(1)]; - if (assetId === "through;left") return [throughHead(), diagonalShaft(-1), diagonalHead(-1)]; - if (assetId === "through;left;right") return [throughHead(), diagonalShaft(-1), diagonalHead(-1), diagonalShaft(1), diagonalHead(1)]; - throw new Error(`No angular turn-lane template: ${assetId}`); -} - -function sourceSvgTemplateFor(asset, assetId) { - const source = fs.readFileSync(path.resolve(__dirname, "..", "..", "assets", "lane-icons", asset.source), "utf8"); - const mirrorX = asset.mirror_x === true; - const anchorX = Number(asset.anchor_x); - if (!Number.isFinite(anchorX)) throw new Error(`turn-lane asset has invalid anchor_x: ${assetId}`); - const shapes = []; - for (const match of source.matchAll(/]*)\/>|]*)\/>/g)) { - const attrs = parseSvgAttrs(match[1] || match[2]); - const strokeWidth = Number(attrs["stroke-width"] || 0); - if (match[1]) { - shapes.push(strokePolygon([[Number(attrs.x1), Number(attrs.y1)], [Number(attrs.x2), Number(attrs.y2)]], strokeWidth)); - } else { - const points = parseSvgPath(attrs.d || ""); - if (attrs.fill !== "none") shapes.push(points); - if (strokeWidth > 0) shapes.push(strokePolygon(points, strokeWidth)); - } - } - return shapes.filter((ring) => ring.length >= 4).map((ring) => ring.map(([x, y]) => [ - (mirrorX ? anchorX - x : x - anchorX) * SVG_METERS_PER_UNIT, - (23 - y) * SVG_METERS_PER_UNIT, - ])); -} - -function parseSvgAttrs(text) { - const attrs = {}; - for (const match of text.matchAll(/([\w:-]+)=(['"])(.*?)\2/g)) attrs[match[1]] = match[3]; - return attrs; -} - -function parseSvgPath(value) { - const tokens = value.match(/[a-zA-Z]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g) || []; - let index = 0; - let command = ""; - let point = [0, 0]; - let start = null; - const points = []; - const number = () => Number(tokens[index++]); - const lineTo = (x, y) => { point = [x, y]; points.push(point); }; - while (index < tokens.length) { - if (/^[a-zA-Z]$/.test(tokens[index])) command = tokens[index++]; - const relative = command === command.toLowerCase(); - const op = command.toUpperCase(); - if (op === "Z") { if (start) points.push(start); command = ""; continue; } - if (op === "M" || op === "L") { - const x = number(); const y = number(); - const next = relative ? [point[0] + x, point[1] + y] : [x, y]; - if (op === "M" && !start) { start = next; point = next; points.push(point); command = relative ? "l" : "L"; } else lineTo(...next); - continue; - } - if (op === "H") { lineTo(relative ? point[0] + number() : number(), point[1]); continue; } - if (op === "V") { lineTo(point[0], relative ? point[1] + number() : number()); continue; } - if (op === "C") { - const values = [number(), number(), number(), number(), number(), number()]; - const controls = relative ? values.map((n, i) => n + point[i % 2]) : values; - const origin = point; - for (let step = 1; step <= 8; step += 1) { - const t = step / 8; const u = 1 - t; - lineTo(u ** 3 * origin[0] + 3 * u ** 2 * t * controls[0] + 3 * u * t ** 2 * controls[2] + t ** 3 * controls[4], u ** 3 * origin[1] + 3 * u ** 2 * t * controls[1] + 3 * u * t ** 2 * controls[3] + t ** 3 * controls[5]); - } - continue; - } - if (op === "A") { number(); number(); number(); number(); number(); const x = number(); const y = number(); lineTo(relative ? point[0] + x : x, relative ? point[1] + y : y); continue; } - throw new Error(`Unsupported SVG path command: ${command}`); - } - return points; -} - -function strokePolygon(points, width) { - if (points.length < 2) return []; - const half = width / 2; - const left = []; const right = []; - for (let index = 0; index < points.length; index += 1) { - const prev = points[Math.max(0, index - 1)]; - const next = points[Math.min(points.length - 1, index + 1)]; - const dx = next[0] - prev[0]; const dy = next[1] - prev[1]; const length = Math.hypot(dx, dy) || 1; - const nx = -dy / length * half; const ny = dx / length * half; - left.push([points[index][0] + nx, points[index][1] + ny]); - right.unshift([points[index][0] - nx, points[index][1] - ny]); - } - return [...left, ...right, left[0]]; -} - -module.exports = { arrowRingsAt, buildCustomTurnLaneArrows, loadManifest, normalizeManeuver, supportedAssets, templateFor }; diff --git a/packages/road-compiler/src/geometry/lane-geometry.js b/packages/road-compiler/src/geometry/lane-geometry.js deleted file mode 100644 index 20d36bc..0000000 --- a/packages/road-compiler/src/geometry/lane-geometry.js +++ /dev/null @@ -1,161 +0,0 @@ -"use strict"; - -const EARTH_RADIUS_METERS = 6371008.8; - -function laneCenterline(lane) { - const ring = lane?.geometry?.type === "Polygon" ? lane.geometry.coordinates?.[0] : null; - if (!Array.isArray(ring) || ring.length < 5 || !sameCoordinate(ring[0], ring.at(-1))) return null; - const vertices = ring.slice(0, -1); - if (!vertices.every(validCoordinate)) return null; - const half = vertices.length / 2; - if (!Number.isInteger(half) || half < 2) return null; - const centerline = vertices.slice(0, half).map((point, index) => [ - (point[0] + vertices[vertices.length - 1 - index][0]) / 2, - (point[1] + vertices[vertices.length - 1 - index][1]) / 2, - ]); - return polylineLength(centerline) > 0.01 ? centerline : null; -} - -function orientPolyline(polyline, reference) { - if (!polyline?.length || !reference?.length) return null; - const forward = projectedDistanceAlong(reference, polyline.at(-1)) - projectedDistanceAlong(reference, polyline[0]); - if (Math.abs(forward) < 0.01) return null; - return forward > 0 ? polyline.map(copyCoordinate) : [...polyline].reverse().map(copyCoordinate); -} - -function stitchPolylines(polylines, maxGapMeters) { - if (!polylines.length) return null; - const result = []; - for (const polyline of polylines) { - if (!polyline?.length) return null; - if (result.length && haversineMeters(result.at(-1), polyline[0]) > maxGapMeters) return null; - appendCoordinates(result, polyline); - } - return result; -} - -function projectedDistanceAlong(polyline, point) { - let traversed = 0; - let best = { distance: Infinity, along: 0, lateral: 0 }; - for (let index = 1; index < polyline.length; index += 1) { - const start = polyline[index - 1]; - const end = polyline[index]; - const meters = metersAt((start[1] + end[1]) / 2); - const dx = (end[0] - start[0]) * meters.lon; - const dy = (end[1] - start[1]) * meters.lat; - const px = (point[0] - start[0]) * meters.lon; - const py = (point[1] - start[1]) * meters.lat; - const length = Math.hypot(dx, dy); - if (length < 0.001) continue; - const ratio = Math.max(0, Math.min(1, (px * dx + py * dy) / (length * length))); - const offsetX = px - dx * ratio; - const offsetY = py - dy * ratio; - const distance = Math.hypot(offsetX, offsetY); - if (distance < best.distance) { - const rightX = dy / length; - const rightY = -dx / length; - best = { - distance, - along: traversed + length * ratio, - lateral: offsetX * rightX + offsetY * rightY, - }; - } - traversed += length; - } - return best.along; -} - -function lateralOffsetFrom(polyline, point) { - let best = null; - for (let index = 1; index < polyline.length; index += 1) { - const start = polyline[index - 1]; - const end = polyline[index]; - const meters = metersAt((start[1] + end[1]) / 2); - const dx = (end[0] - start[0]) * meters.lon; - const dy = (end[1] - start[1]) * meters.lat; - const px = (point[0] - start[0]) * meters.lon; - const py = (point[1] - start[1]) * meters.lat; - const length = Math.hypot(dx, dy); - if (length < 0.001) continue; - const ratio = Math.max(0, Math.min(1, (px * dx + py * dy) / (length * length))); - const offsetX = px - dx * ratio; - const offsetY = py - dy * ratio; - const distance = Math.hypot(offsetX, offsetY); - if (!best || distance < best.distance) { - best = { distance, lateral: offsetX * dy / length - offsetY * dx / length }; - } - } - return best; -} - -function polylineMidpoint(polyline) { - const target = polylineLength(polyline) / 2; - let traversed = 0; - for (let index = 1; index < polyline.length; index += 1) { - const length = haversineMeters(polyline[index - 1], polyline[index]); - if (traversed + length >= target) { - const ratio = length ? (target - traversed) / length : 0; - return [ - polyline[index - 1][0] + (polyline[index][0] - polyline[index - 1][0]) * ratio, - polyline[index - 1][1] + (polyline[index][1] - polyline[index - 1][1]) * ratio, - ]; - } - traversed += length; - } - return polyline.length ? copyCoordinate(polyline.at(-1)) : null; -} - -function polylineLength(polyline) { - let total = 0; - for (let index = 1; index < (polyline?.length || 0); index += 1) { - total += haversineMeters(polyline[index - 1], polyline[index]); - } - return total; -} - -function haversineMeters(a, b) { - const lat1 = degreesToRadians(a[1]); - const lat2 = degreesToRadians(b[1]); - const dLat = degreesToRadians(b[1] - a[1]); - const dLon = degreesToRadians(b[0] - a[0]); - const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; - return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h))); -} - -function appendCoordinates(target, coordinates) { - for (const coordinate of coordinates) { - if (!sameCoordinate(target.at(-1), coordinate)) target.push(copyCoordinate(coordinate)); - } -} - -function validCoordinate(value) { - return Array.isArray(value) && value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1]); -} - -function sameCoordinate(a, b) { - return Boolean(a && b && a[0] === b[0] && a[1] === b[1]); -} - -function copyCoordinate(coordinate) { - return [coordinate[0], coordinate[1]]; -} - -function metersAt(latitude) { - return { lon: 111320 * Math.cos(degreesToRadians(latitude)), lat: 111320 }; -} - -function degreesToRadians(value) { - return value * Math.PI / 180; -} - -module.exports = { - appendCoordinates, - haversineMeters, - laneCenterline, - lateralOffsetFrom, - orientPolyline, - polylineLength, - polylineMidpoint, - projectedDistanceAlong, - stitchPolylines, -}; diff --git a/packages/road-compiler/src/index.js b/packages/road-compiler/src/index.js deleted file mode 100644 index 6b58c4a..0000000 --- a/packages/road-compiler/src/index.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; - -module.exports = { - laneGeometry: require("./geometry/lane-geometry"), - gaodeReference: require("./reference/gaode"), - turnLaneArrows: require("./compile/turn-lane-arrows"), - complexJunction: require("./compile/complex-junction"), - osm: require("./osm"), - trafficSignals: require("./traffic-signals"), - nativeTrafficSignals: require("./native-traffic-signals"), - nativeRoad: require("./compile/native-road"), - compiler: require("./compile/compiler"), - check: require("./check"), -}; diff --git a/packages/road-compiler/src/native-traffic-signals.js b/packages/road-compiler/src/native-traffic-signals.js deleted file mode 100644 index 58069f5..0000000 --- a/packages/road-compiler/src/native-traffic-signals.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const { parseOsm } = require("./osm"); -const { - buildTrafficSignalFeatures, - buildTrafficSignalsFromFeatures, - validateTrafficSignalFeatures, - validateTrafficSignalSourceReferences, -} = require("./traffic-signals"); - -const SCHEMA = "native-traffic-signals/v1"; - -function loadOrGenerate(file, osmText, stopLines, intersections) { - if (fs.existsSync(file)) { - const document = JSON.parse(fs.readFileSync(file, "utf8")); - try { - return validateDocument(document, osmText); - } catch (error) { - // OSM edits can invalidate the stable identities in a document that was - // itself generated from OSM. User-authored documents must remain strict. - if (document?.provenance === "generated:osm-controls" && isStaleSourceReferenceError(error)) { - return generate(osmText, stopLines, intersections); - } - throw error; - } - } - return generate(osmText, stopLines, intersections); -} - -function isStaleSourceReferenceError(error) { - return error instanceof Error && /^traffic signal feature \d+: (approach_id .* is not present on OSM control|control_id .* is not present in the current OSM)/.test(error.message); -} - -function generate(osmText, stopLines, intersections) { - const controls = parseOsm(osmText).trafficSignalControls; - return { schema: SCHEMA, provenance: "generated:osm-controls", assemblies: buildTrafficSignalFeatures(stopLines, intersections, controls) }; -} - -function validateDocument(value, osmText) { - if (value?.schema !== SCHEMA) throw new Error(`Expected ${SCHEMA} signal document`); - const assemblies = validateTrafficSignalFeatures(value.assemblies); - if (osmText) validateTrafficSignalSourceReferences(assemblies, parseOsm(osmText).trafficSignalControls); - return { schema: SCHEMA, provenance: value.provenance || "native", assemblies }; -} - -function runtime(document) { return buildTrafficSignalsFromFeatures(document.assemblies); } - -module.exports = { SCHEMA, generate, loadOrGenerate, validateDocument, runtime }; diff --git a/packages/road-compiler/src/osm.js b/packages/road-compiler/src/osm.js deleted file mode 100644 index 03e8f75..0000000 --- a/packages/road-compiler/src/osm.js +++ /dev/null @@ -1,102 +0,0 @@ -"use strict"; - -function parseOsm(xml) { - const boundsMatch = xml.match(/]*)\/?\s*>/); - const boundsAttrs = boundsMatch ? xmlAttrs(boundsMatch[1]) : {}; - const candidateBounds = { - minLon: Number(boundsAttrs.minlon), minLat: Number(boundsAttrs.minlat), - maxLon: Number(boundsAttrs.maxlon), maxLat: Number(boundsAttrs.maxlat), - }; - const bounds = Object.values(candidateBounds).every(Number.isFinite) ? candidateBounds : null; - const nodes = new Map(); - const trafficSignalControls = []; - const nodePattern = /]*?)(?:\/>|>([\s\S]*?)<\/node>)/g; - for (const match of xml.matchAll(nodePattern)) { - const attrs = xmlAttrs(match[1]); - if (attrs.action === "delete" || !attrs.id || attrs.lon === undefined || attrs.lat === undefined) continue; - const coordinate = [Number(attrs.lon), Number(attrs.lat)]; - if (!coordinate.every(Number.isFinite)) continue; - nodes.set(attrs.id, coordinate); - const tags = parseTags(match[2] || ""); - if (tags.highway === "traffic_signals") { - trafficSignalControls.push({ id: attrs.id, longitude: coordinate[0], latitude: coordinate[1], tags }); - } - } - const ways = []; - for (const match of xml.matchAll(/]*)>([\s\S]*?)<\/way>/g)) { - const attrs = xmlAttrs(match[1]); - if (attrs.action === "delete") continue; - const body = match[2]; - const refs = []; - for (const ndMatch of body.matchAll(/]*)\/?\s*>/g)) { - const ref = xmlAttrs(ndMatch[1]).ref; - if (ref && nodes.has(ref)) refs.push(ref); - } - if (refs.length >= 2) ways.push({ id: attrs.id || `way-${ways.length + 1}`, refs, tags: parseTags(body) }); - } - for (const control of trafficSignalControls) { - const arms = []; - for (const way of ways) { - if (!isMotorRoad(way.tags)) continue; - for (let index = 0; index < way.refs.length; index += 1) { - if (way.refs[index] !== control.id) continue; - for (const neighborIndex of [index - 1, index + 1]) { - const neighbor = way.refs[neighborIndex]; - if (!neighbor || !nodes.has(neighbor)) continue; - const neighborPoint = nodes.get(neighbor); - arms.push({ - headingDegrees: headingBetween(control, neighborPoint), - wayId: String(way.id), - neighborNodeId: String(neighbor), - }); - } - } - } - control.arms = dedupeHeadings(arms); - control.junctionType = control.arms.length === 3 ? "T" : control.arms.length === 4 ? "cross" : "other"; - } - return { bounds, nodes, ways, trafficSignalControls }; -} - -function isMotorRoad(tags) { - const highway = tags.highway || ""; - return highway && tags.area !== "yes" && !new Set([ - "footway", "path", "pedestrian", "steps", "cycleway", "service", "track", - "bridleway", "corridor", "elevator", "platform", "construction", - ]).has(highway); -} - -function headingBetween(from, to) { - const latitude = (from.latitude + to[1]) / 2 * Math.PI / 180; - return Math.atan2((to[0] - from.longitude) * Math.cos(latitude), to[1] - from.latitude) * 180 / Math.PI; -} - -function dedupeHeadings(arms) { - const normalized = (value) => ((value % 360) + 360) % 360; - const distance = (a, b) => Math.abs(((a - b + 540) % 360) - 180); - const result = []; - for (const arm of arms) { - arm.headingDegrees = normalized(arm.headingDegrees); - if (!result.some((other) => distance(other.headingDegrees, arm.headingDegrees) <= 25)) result.push(arm); - } - return result.sort((a, b) => a.headingDegrees - b.headingDegrees); -} - -function xmlAttrs(text) { - const attrs = {}; - for (const match of text.matchAll(/([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) { - attrs[match[1]] = match[2] !== undefined ? match[2] : match[3]; - } - return attrs; -} - -function parseTags(body) { - const tags = {}; - for (const match of body.matchAll(/]*)\/?\s*>/g)) { - const tag = xmlAttrs(match[1]); - if (tag.k) tags[tag.k] = tag.v || ""; - } - return tags; -} - -module.exports = { parseOsm }; diff --git a/packages/road-compiler/src/reference/gaode.js b/packages/road-compiler/src/reference/gaode.js deleted file mode 100644 index 7d8eac1..0000000 --- a/packages/road-compiler/src/reference/gaode.js +++ /dev/null @@ -1,231 +0,0 @@ -"use strict"; - -const fs = require("fs"); - -const PI = Math.PI; -const EARTH_A = 6378245.0; -const EARTH_EE = 0.00669342162296594323; - -function transformLat(x, y) { - let value = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); - value += (20 * Math.sin(6 * x * PI) + 20 * Math.sin(2 * x * PI)) * 2 / 3; - value += (20 * Math.sin(y * PI) + 40 * Math.sin(y / 3 * PI)) * 2 / 3; - value += (160 * Math.sin(y / 12 * PI) + 320 * Math.sin(y * PI / 30)) * 2 / 3; - return value; -} - -function transformLon(x, y) { - let value = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); - value += (20 * Math.sin(6 * x * PI) + 20 * Math.sin(2 * x * PI)) * 2 / 3; - value += (20 * Math.sin(x * PI) + 40 * Math.sin(x / 3 * PI)) * 2 / 3; - value += (150 * Math.sin(x / 12 * PI) + 300 * Math.sin(x / 30 * PI)) * 2 / 3; - return value; -} - -// This is the standard local inverse approximation used for GCJ-02 reference -// data. It is intentionally kept separate from native road geometry, whose -// source coordinates remain WGS84. -function gcj02ToWgs84(coordinate) { - const [longitude, latitude] = coordinate; - if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) throw new Error("Reference coordinate must be finite"); - const dLat = transformLat(longitude - 105, latitude - 35); - const dLon = transformLon(longitude - 105, latitude - 35); - const radLat = latitude / 180 * PI; - const magic = 1 - EARTH_EE * Math.sin(radLat) ** 2; - const sqrtMagic = Math.sqrt(magic); - return [ - longitude - dLon * 180 / (EARTH_A / sqrtMagic * Math.cos(radLat) * PI), - latitude - dLat * 180 / (EARTH_A * (1 - EARTH_EE) / (magic * sqrtMagic) * PI), - ]; -} - -function mapCoordinates(coordinates, mapper) { - if (typeof coordinates[0] === "number") return mapper(coordinates); - return coordinates.map((value) => mapCoordinates(value, mapper)); -} - -function convertGeoJson(document) { - if (!document || document.type !== "FeatureCollection" || !Array.isArray(document.features)) { - throw new Error("Reference must be a GeoJSON FeatureCollection"); - } - return { - ...document, - crs: undefined, - features: document.features.map((feature) => { - if (!feature || !feature.geometry || !feature.geometry.coordinates) throw new Error("Reference feature is missing geometry"); - return { ...feature, geometry: { ...feature.geometry, coordinates: mapCoordinates(feature.geometry.coordinates, gcj02ToWgs84) } }; - }), - }; -} - -function coordinatesOf(document) { - const points = []; - for (const feature of document.features || []) walkCoordinates(feature.geometry?.coordinates, points); - return points; -} - -function walkCoordinates(value, points) { - if (!Array.isArray(value) || !value.length) return; - if (typeof value[0] === "number") { - points.push(value); - return; - } - for (const child of value) walkCoordinates(child, points); -} - -function boundsOf(document) { - const points = coordinatesOf(document); - if (!points.length) throw new Error("Reference contains no coordinates"); - return { - minLon: Math.min(...points.map((point) => point[0])), - minLat: Math.min(...points.map((point) => point[1])), - maxLon: Math.max(...points.map((point) => point[0])), - maxLat: Math.max(...points.map((point) => point[1])), - }; -} - -function centerOf(bounds) { - return [(bounds.minLon + bounds.maxLon) / 2, (bounds.minLat + bounds.maxLat) / 2]; -} - -function distanceMeters(first, second) { - const lonScale = 111320 * Math.cos(first[1] * PI / 180); - return Math.hypot((second[0] - first[0]) * lonScale, (second[1] - first[1]) * 111320); -} - -function parseOsmNodes(xml) { - const nodes = []; - for (const match of xml.matchAll(/]*?)(?:\/>|>([\s\S]*?)<\/node>)/g)) { - const attrs = {}; - for (const item of match[1].matchAll(/([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) attrs[item[1]] = item[2] ?? item[3]; - if (!attrs.id || !Number.isFinite(Number(attrs.lon)) || !Number.isFinite(Number(attrs.lat))) continue; - const tags = {}; - for (const item of (match[2] || "").matchAll(/]*)\/?\s*>/g)) { - const tag = {}; - for (const attr of item[1].matchAll(/([:\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) tag[attr[1]] = attr[2] ?? attr[3]; - if (tag.k) tags[tag.k] = tag.v || ""; - } - nodes.push({ id: String(attrs.id), coordinate: [Number(attrs.lon), Number(attrs.lat)], tags }); - } - return nodes; -} - -function nearestNode(nodes, coordinate, nodeId) { - if (nodeId) { - const exact = nodes.find((node) => node.id === String(nodeId)); - if (!exact) throw new Error(`OSM node not found: ${nodeId}`); - return { ...exact, distanceMeters: distanceMeters(exact.coordinate, coordinate), match: "node-id" }; - } - const candidates = nodes.map((node) => ({ ...node, distanceMeters: distanceMeters(node.coordinate, coordinate) })); - candidates.sort((first, second) => first.distanceMeters - second.distanceMeters); - if (!candidates[0]) throw new Error("OSM contains no usable nodes"); - return { ...candidates[0], match: "nearest-node" }; -} - -function bboxIntersectionRatio(first, second) { - const width = Math.max(0, Math.min(first.maxLon, second.maxLon) - Math.max(first.minLon, second.minLon)); - const height = Math.max(0, Math.min(first.maxLat, second.maxLat) - Math.max(first.minLat, second.minLat)); - const intersection = width * height; - const firstArea = Math.max(0, first.maxLon - first.minLon) * Math.max(0, first.maxLat - first.minLat); - const secondArea = Math.max(0, second.maxLon - second.minLon) * Math.max(0, second.maxLat - second.minLat); - return intersection / Math.max(firstArea + secondArea - intersection, Number.EPSILON); -} - -// A complex junction is compiled as one cluster of `complex_part` polygons in -// road_surface.geojson, not as a per-node feature in intersection_surface. -// Match it by cluster id, or by whichever cluster core sits nearest the node. -function complexClusterSurface(nativeRoadSurfaceFile, node, clusterId) { - if (!nativeRoadSurfaceFile || !fs.existsSync(nativeRoadSurfaceFile)) return null; - const surface = JSON.parse(fs.readFileSync(nativeRoadSurfaceFile, "utf8")); - const parts = (surface.features || []).filter((item) => item.properties?.cluster_id && item.properties?.complex_part); - const cores = parts.filter((item) => item.properties.complex_part === "core" && Array.isArray(item.properties.center)); - if (!cores.length) return null; - const core = clusterId - ? cores.find((item) => String(item.properties.cluster_id) === String(clusterId)) - : [...cores].sort((first, second) => distanceMeters(first.properties.center, node.coordinate) - distanceMeters(second.properties.center, node.coordinate))[0]; - if (!core) return null; - const features = parts.filter((item) => item.properties.cluster_id === core.properties.cluster_id); - return { clusterId: core.properties.cluster_id, core, features }; -} - -function inspectReference({ referenceFile, osmFile, nativeIntersectionFile, nativeRoadSurfaceFile, nodeId, clusterId }) { - const source = JSON.parse(fs.readFileSync(referenceFile, "utf8")); - const converted = convertGeoJson(source); - const referenceBounds = boundsOf(converted); - const referenceCenter = centerOf(referenceBounds); - const nodes = parseOsmNodes(fs.readFileSync(osmFile, "utf8")); - const matchedNode = nearestNode(nodes, referenceCenter, nodeId); - const native = JSON.parse(fs.readFileSync(nativeIntersectionFile, "utf8")); - const feature = (native.features || []).find((item) => item.properties?.osm_node_id === matchedNode.id); - const cluster = feature ? null : complexClusterSurface(nativeRoadSurfaceFile, matchedNode, clusterId); - const matchedFeatures = feature ? [feature] : cluster?.features || null; - const nativeBounds = matchedFeatures ? boundsOf({ features: matchedFeatures }) : null; - const diagnostics = []; - if (!matchedFeatures) diagnostics.push(nativeRoadSurfaceFile ? "No native intersection surface or complex cluster matched the OSM node" : "No native intersection surface matched the OSM node; pass --native-road-surface to also search complex junction clusters"); - return { - schema: "gaode-junction-reference-comparison/v2", - source: { file: referenceFile, coordinateSystem: "GCJ-02", featureCount: converted.features.length }, - conversion: { target: "WGS84", method: "gcj02-inverse-approximation" }, - reference: { bounds: referenceBounds, center: referenceCenter }, - matchedOsmNode: { id: matchedNode.id, coordinate: matchedNode.coordinate, tags: matchedNode.tags, match: matchedNode.match, centerDistanceMeters: matchedNode.distanceMeters }, - nativeIntersection: nativeBounds ? { - kind: feature ? "junction-node" : "complex-cluster", - clusterId: cluster?.clusterId || null, - featureCount: matchedFeatures.length, - bounds: nativeBounds, - bboxIoU: bboxIntersectionRatio(referenceBounds, nativeBounds), - centerOffsetMeters: distanceMeters(referenceCenter, centerOf(nativeBounds)), - featureProperties: feature ? feature.properties : cluster.core.properties, - } : null, - diagnostics, - converted, - matchedFeatures, - }; -} - -function localReferenceSvg({ converted, nativeIntersection, center, radiusMeters = 180 }) { - const width = 1000; - const height = 1000; - const lonScale = 111320 * Math.cos(center[1] * PI / 180); - const project = (point) => [ - width / 2 + (point[0] - center[0]) * lonScale * width / (radiusMeters * 2), - height / 2 - (point[1] - center[1]) * 111320 * height / (radiusMeters * 2), - ]; - const pathFor = (coordinates) => { - const parts = []; - const appendLine = (line, close) => { - if (!line?.length) return; - const [firstX, firstY] = project(line[0]); - parts.push(`M ${firstX.toFixed(1)} ${firstY.toFixed(1)}`); - for (const point of line.slice(1)) { - const [x, y] = project(point); - parts.push(`L ${x.toFixed(1)} ${y.toFixed(1)}`); - } - if (close) parts.push("Z"); - }; - const visit = (value) => { - if (!Array.isArray(value) || !value.length) return; - if (typeof value[0] === "number") return; - if (typeof value[0][0] === "number") appendLine(value, value.length > 2); - else value.forEach(visit); - }; - visit(coordinates); - return parts.join(" "); - }; - const color = { 1: "#2563eb", 2: "#0f766e", 3: "#7c3aed", 4: "#ea580c", 5: "#64748b" }; - const references = converted.features.map((feature) => { - const type = feature.properties?.type || "unknown"; - return ``; - }).join("\n"); - const nativePaths = (nativeIntersection?.features || []).map((feature) => ``).join("\n"); - return ` - - - ${references} - ${nativePaths} - - Gaode reference (type colors) / native intersection (red) -`; -} - -module.exports = { gcj02ToWgs84, convertGeoJson, boundsOf, parseOsmNodes, nearestNode, inspectReference, localReferenceSvg }; diff --git a/packages/road-compiler/src/traffic-signals.js b/packages/road-compiler/src/traffic-signals.js deleted file mode 100644 index e3f8d11..0000000 --- a/packages/road-compiler/src/traffic-signals.js +++ /dev/null @@ -1,330 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const crypto = require("crypto"); -const { parseOsm } = require("./osm"); - -const EARTH_RADIUS = 6371008.8; -const CURB_OFFSET_METERS = 5.2; -const MAST_REACH_METERS = 4.5; -const SIGNAL_LAYOUT = Object.freeze({ - poleHeightMeters: 6.7, poleRadiusMeters: 0.13, armWidthMeters: 0.21, - mastHeightMeters: 6.25, headCenterHeightMeters: 6.25, - headWidthMeters: 0.68, headDepthMeters: 0.30, headBodyHeightMeters: 1.62, - lensRadiusMeters: 0.22, lensDepthMeters: 0.07, lensFaceOffsetMeters: 0.18, - lensVerticalOffsetsMeters: [0.49, -0.01, -0.51], - countdownLateralMeters: 1.15, countdownFaceOffsetMeters: 0.05, - countdownWidthMeters: 0.82, countdownDepthMeters: 0.14, - countdownHeightMeters: 0.56, countdownVerticalOffsetMeters: 0.0, -}); - -function buildTrafficSignalFeatures(stopLines, intersections, controls = []) { - const centers = (intersections.features || []).map((feature, index) => { - const point = polygonCenter(feature.geometry); - return { id: `intersection-${index + 1}`, point, radius: polygonRadius(feature.geometry, point) }; - }).filter((entry) => entry.point); - const clusteredStops = new Map(); - for (const feature of stopLines.features || []) { - const clusterId = feature.properties?.cluster_id; - const point = polygonCenter(feature.geometry); - if (!clusterId || !point) continue; - if (!clusteredStops.has(clusterId)) clusteredStops.set(clusterId, []); - clusteredStops.get(clusterId).push(point); - } - for (const [clusterId, points] of clusteredStops) { - if (points.length < 3) continue; - const point = points.reduce((sum, item) => [sum[0] + item[0] / points.length, sum[1] + item[1] / points.length], [0, 0]); - centers.push({ id: `cluster-${clusterId}`, clusterId, point, radius: Math.max(...points.map((item) => metersBetween(point, item))) }); - } - const candidates = []; - for (const feature of stopLines.features || []) { - const center = polygonCenter(feature.geometry); - if (!center) continue; - const clusterId = feature.properties?.cluster_id; - const intersection = clusterId ? centers.find((entry) => entry.clusterId === clusterId) : nearestCenter(center, centers); - if (!intersection || metersBetween(center, intersection.point) > 32) continue; - const axis = roadAxis(feature.geometry, center, intersection.point); - if (!axis) continue; - const right = [axis[1], -axis[0]]; - candidates.push({ - intersectionId: intersection.id, center, axis, - point: intersection.clusterId - ? moveMeters(center, right, CURB_OFFSET_METERS) - : moveMeters(moveMeters(intersection.point, axis, intersection.radius + 3.2), right, CURB_OFFSET_METERS), - headingDegrees: normalizeDegrees(Math.atan2(axis[0], axis[1]) * 180 / Math.PI), - matchHeadingDegrees: intersection.clusterId - ? normalizeDegrees(Math.atan2(-axis[0], -axis[1]) * 180 / Math.PI) - : null, - }); - } - const features = []; - for (const control of controls) { - const controlPoint = [Number(control.longitude), Number(control.latitude)]; - if (!controlPoint.every(Number.isFinite) || !Array.isArray(control.arms) || control.arms.length < 3) continue; - const intersection = nearestCenter(controlPoint, centers); - if (!intersection || metersBetween(controlPoint, intersection.point) > 32) continue; - const arms = matchOsmArms(candidates.filter((item) => item.intersectionId === intersection.id), controlPoint, control.arms); - const groups = phaseGroups(arms); - arms.forEach((candidate, index) => { - const fallbackArmId = `heading-${Math.round(normalizeDegrees(candidate.osmArm?.headingDegrees || 0) * 1000)}`; - const sourceWayId = String(candidate.osmArm?.wayId || "legacy"); - const neighborNodeId = String(candidate.osmArm?.neighborNodeId || fallbackArmId); - const approachId = `${sourceWayId}:${neighborNodeId}`; - const signalUid = `osm-${String(control.id)}-${sourceWayId}-${neighborNodeId}`; - features.push({ - type: "Feature", - geometry: { type: "Point", coordinates: candidate.point.slice() }, - properties: { - signal_uid: signalUid, display_id: signalUid, control_id: String(control.id), - approach_id: approachId, source_way_id: sourceWayId, - // These are independent assembly controls. heading_deg remains a - // migration hint for older native documents only. - mast_heading_deg: normalizeDegrees(candidate.headingDegrees - 90), - face_heading_deg: normalizeDegrees(candidate.headingDegrees + 180), - phase_group: groups[index], - mast_reach_m: MAST_REACH_METERS, - stop_lon: candidate.center[0], stop_lat: candidate.center[1], - enabled: true, z_offset_m: 0, - }, - }); - }); - } - return validateTrafficSignalFeatures({ type: "FeatureCollection", features }); -} - -function validateTrafficSignalFeatures(collection) { - if (collection?.type !== "FeatureCollection" || !Array.isArray(collection.features)) { - throw new Error("Traffic signal assemblies must be a FeatureCollection"); - } - const uids = new Set(); - const displayIds = new Set(); - const features = collection.features.map((feature, index) => { - const label = `traffic signal feature ${index + 1}`; - if (feature?.geometry?.type !== "Point" || !Array.isArray(feature.geometry.coordinates) || - feature.geometry.coordinates.length < 2 || !feature.geometry.coordinates.slice(0, 2).every(Number.isFinite)) { - throw new Error(`${label}: geometry must be a finite Point`); - } - const input = feature.properties || {}; - const text = (key, required = true) => { - const value = input[key] == null ? "" : String(input[key]).trim(); - if (required && !value) throw new Error(`${label}: missing ${key}`); - return value; - }; - const number = (key, options = {}) => { - if (input[key] === null || input[key] === undefined || input[key] === "") { - throw new Error(`${label}: missing ${key}`); - } - const value = Number(input[key]); - if (!Number.isFinite(value) || (options.min != null && value < options.min) || (options.max != null && value > options.max)) { - throw new Error(`${label}: invalid ${key} '${input[key]}'`); - } - return value; - }; - const signalUid = text("signal_uid"); - if (!/^osm-[A-Za-z0-9_.:-]+$/.test(signalUid)) throw new Error(`${label}: invalid signal_uid '${signalUid}'`); - if (uids.has(signalUid)) throw new Error(`Duplicate signal_uid '${signalUid}'`); - uids.add(signalUid); - const displayId = text("display_id", false); - if (displayId && displayIds.has(displayId)) throw new Error(`Duplicate display_id '${displayId}'`); - if (displayId) displayIds.add(displayId); - const phaseGroup = number("phase_group", { min: 0, max: 1 }); - if (!Number.isInteger(phaseGroup)) throw new Error(`${label}: phase_group must be 0 or 1`); - const enabled = normalizeBoolean(input.enabled, label); - const controlId = text("control_id"); - const approachId = text("approach_id"); - const sourceWayId = text("source_way_id"); - if (!approachId.startsWith(`${sourceWayId}:`)) throw new Error(`${label}: approach_id does not match source_way_id`); - const expectedUid = `osm-${controlId}-${approachId.replace(":", "-")}`; - if (signalUid !== expectedUid) throw new Error(`${label}: signal_uid does not match source identity (expected '${expectedUid}')`); - const legacyHeading = input.heading_deg == null || input.heading_deg === "" ? null : normalizeDegrees(number("heading_deg")); - if (legacyHeading == null && (input.mast_heading_deg == null || input.mast_heading_deg === "")) { - throw new Error(`${label}: missing mast_heading_deg`); - } - if (legacyHeading == null && (input.face_heading_deg == null || input.face_heading_deg === "")) { - throw new Error(`${label}: missing face_heading_deg`); - } - const mastHeading = input.mast_heading_deg == null || input.mast_heading_deg === "" - ? normalizeDegrees((legacyHeading == null ? 0 : legacyHeading) - 90) - : normalizeDegrees(number("mast_heading_deg")); - const faceHeading = input.face_heading_deg == null || input.face_heading_deg === "" - ? normalizeDegrees((legacyHeading == null ? 0 : legacyHeading) + 180) - : normalizeDegrees(number("face_heading_deg")); - return { - type: "Feature", - geometry: { type: "Point", coordinates: feature.geometry.coordinates.slice(0, 2).map(Number) }, - properties: { - ...input, signal_uid: signalUid, display_id: displayId, - control_id: controlId, approach_id: approachId, - source_way_id: sourceWayId, - // Retain the legacy value only for migration compatibility. Runtime - // geometry is entirely defined by mast_heading_deg and face_heading_deg. - heading_deg: legacyHeading, - mast_heading_deg: mastHeading, face_heading_deg: faceHeading, - phase_group: phaseGroup, mast_reach_m: number("mast_reach_m", { min: 0.1, max: 30 }), - stop_lon: number("stop_lon", { min: -180, max: 180 }), - stop_lat: number("stop_lat", { min: -90, max: 90 }), - enabled, z_offset_m: number("z_offset_m", { min: -20, max: 100 }), - }, - }; - }); - return { type: "FeatureCollection", features }; -} - -function buildTrafficSignalsFromFeatures(collection) { - const normalized = validateTrafficSignalFeatures(collection); - const signals = normalized.features.filter((feature) => feature.properties.enabled).map((feature) => { - const p = feature.properties; - const point = feature.geometry.coordinates; - const mastAxis = headingVector(p.mast_heading_deg); - return { - id: p.signal_uid, signalUid: p.signal_uid, displayId: p.display_id, - nodeKey: signalNodeKey(p.signal_uid), - controlId: p.control_id, approachId: p.approach_id, sourceWayId: p.source_way_id, - phaseGroup: p.phase_group, longitude: point[0], latitude: point[1], - stopLongitude: p.stop_lon, stopLatitude: p.stop_lat, - // Existing Blender readers require headingDegrees. It is a compatibility - // alias only; the independent mast/face fields below define all geometry. - headingDegrees: p.heading_deg == null ? p.mast_heading_deg : p.heading_deg, - mastHeadingDegrees: p.mast_heading_deg, - faceHeadingDegrees: p.face_heading_deg, mastReachMeters: p.mast_reach_m, - zOffsetMeters: p.z_offset_m, - pose: buildSignalPose(point, mastAxis, p.face_heading_deg, p.mast_reach_m, p.z_offset_m), - }; - }); - return { version: 3, layout: SIGNAL_LAYOUT, signals }; -} - -function signalNodeKey(signalUid) { - return `ts_${crypto.createHash("sha256").update(signalUid).digest("hex").slice(0, 16)}`; -} - -function reconcileTrafficSignalSourceReferences(collection, controls) { - const normalized = validateTrafficSignalFeatures(collection); - const approachesByControl = new Map((controls || []).map((control) => [ - String(control.id), - new Set((control.arms || []).map((arm) => `${String(arm.wayId)}:${String(arm.neighborNodeId)}`)), - ])); - const kept = []; - const dropped = []; - for (const [index, feature] of normalized.features.entries()) { - const { control_id: controlId, approach_id: approachId, signal_uid: signalUid } = feature.properties; - const approaches = approachesByControl.get(controlId); - if (!approaches) { - dropped.push({ index: index + 1, signalUid, controlId, approachId, reason: "missing-control", message: `control_id '${controlId}' is not present in the current OSM` }); - continue; - } - if (!approaches.has(approachId)) { - dropped.push({ index: index + 1, signalUid, controlId, approachId, reason: "missing-approach", message: `approach_id '${approachId}' is not present on OSM control '${controlId}'` }); - continue; - } - kept.push(feature); - } - return { collection: { ...normalized, features: kept }, dropped }; -} - -function validateTrafficSignalSourceReferences(collection, controls) { - const { collection: reconciled, dropped } = reconcileTrafficSignalSourceReferences(collection, controls); - if (dropped.length) throw new Error(`traffic signal feature ${dropped[0].index}: ${dropped[0].message}`); - return reconciled; -} - -function buildTrafficSignals(stopLines, intersections, controls = []) { - return buildTrafficSignalsFromFeatures(buildTrafficSignalFeatures(stopLines, intersections, controls)); -} - -function readTrafficSignalFeatures(stopLinePath, intersectionPath, osmPath) { - const controls = parseOsm(fs.readFileSync(osmPath, "utf8")).trafficSignalControls; - return buildTrafficSignalFeatures( - JSON.parse(fs.readFileSync(stopLinePath, "utf8")), - JSON.parse(fs.readFileSync(intersectionPath, "utf8")), controls, - ); -} - -function readTrafficSignals(editablePath, osmPath = null) { - const collection = JSON.parse(fs.readFileSync(editablePath, "utf8")); - if (osmPath) { - const controls = parseOsm(fs.readFileSync(osmPath, "utf8")).trafficSignalControls; - validateTrafficSignalSourceReferences(collection, controls); - } - return buildTrafficSignalsFromFeatures(collection); -} - -function normalizeBoolean(value, label) { - if (value === true || value === 1 || value === "1" || String(value).toLowerCase() === "true" || String(value).toLowerCase() === "yes") return true; - if (value === false || value === 0 || value === "0" || String(value).toLowerCase() === "false" || String(value).toLowerCase() === "no") return false; - throw new Error(`${label}: invalid enabled '${value}'`); -} - -function uniqueApproachArms(candidates, controlPoint) { - const sorted = candidates.map((candidate) => ({ ...candidate, armHeading: normalizeDegrees(headingBetween(controlPoint, candidate.center)), controlDistance: metersBetween(controlPoint, candidate.center) })) - .sort((a, b) => a.armHeading - b.armHeading || a.controlDistance - b.controlDistance); - const arms = []; - for (const candidate of sorted) if (!arms.some((arm) => angularDistance(arm.armHeading, candidate.armHeading) <= 25)) arms.push(candidate); - return arms; -} - -function matchOsmArms(candidates, controlPoint, osmArms) { - const remaining = candidates.map((candidate) => ({ ...candidate, armHeading: candidate.matchHeadingDegrees ?? normalizeDegrees(headingBetween(controlPoint, candidate.center)) })); - if (!osmArms.length) return uniqueApproachArms(remaining, controlPoint); - return osmArms.map((osmArm) => { - let bestIndex = -1; let bestDistance = Infinity; - remaining.forEach((item, index) => { - const directedDistance = angularDistance(item.armHeading, osmArm.headingDegrees); - const distance = item.matchHeadingDegrees == null - ? directedDistance - : Math.min( - angularDistance(item.matchHeadingDegrees, osmArm.headingDegrees), - angularDistance(item.matchHeadingDegrees + 180, osmArm.headingDegrees), - ); - if (distance < bestDistance) { bestDistance = distance; bestIndex = index; } - }); - const candidate = bestIndex >= 0 && bestDistance <= 45 ? remaining.splice(bestIndex, 1)[0] : fallbackCandidate(controlPoint, osmArm); - return { ...candidate, osmArm }; - }); -} - -function fallbackCandidate(controlPoint, osmArm) { - const outward = headingVector(osmArm.headingDegrees); const axis = [-outward[0], -outward[1]]; - const center = moveMeters(controlPoint, outward, 8); const farSide = moveMeters(controlPoint, axis, 3.2); - return { center, axis, point: moveMeters(farSide, [axis[1], -axis[0]], CURB_OFFSET_METERS), armHeading: normalizeDegrees(osmArm.headingDegrees), headingDegrees: normalizeDegrees(Math.atan2(axis[0], axis[1]) * 180 / Math.PI), fallback: true }; -} - -function phaseGroups(arms) { - const groups = Array(arms.length).fill(1); if (arms.length < 2) return groups; - let main = [0, 1]; let best = -1; - for (let a = 0; a < arms.length; a += 1) for (let b = a + 1; b < arms.length; b += 1) { const opposition = angularDistance(arms[a].armHeading, arms[b].armHeading); if (opposition > best) { best = opposition; main = [a, b]; } } - groups[main[0]] = 0; groups[main[1]] = 0; return groups; -} - -function buildSignalPose(pole, mastAxis, faceHeadingDegrees, mastReach, zOffset = 0) { - const face = headingVector(faceHeadingDegrees); - const head = moveMeters(pole, mastAxis, mastReach); - const position = (point, height) => ({ longitude: point[0], latitude: point[1], height: height + zOffset }); - const lensPoint = moveMeters(head, face, SIGNAL_LAYOUT.lensFaceOffsetMeters); - const faceRight = [-face[1], face[0]]; - const board = moveMeters(moveMeters(head, faceRight, SIGNAL_LAYOUT.countdownLateralMeters), face, SIGNAL_LAYOUT.countdownFaceOffsetMeters); - return { pole: position(pole, 0), arm: { from: position(pole, SIGNAL_LAYOUT.mastHeightMeters), to: position(head, SIGNAL_LAYOUT.mastHeightMeters) }, head: { ...position(head, SIGNAL_LAYOUT.headCenterHeightMeters), faceHeadingDegrees }, lenses: ["red", "yellow", "green"].map((state, index) => ({ state, ...position(lensPoint, SIGNAL_LAYOUT.headCenterHeightMeters + SIGNAL_LAYOUT.lensVerticalOffsetsMeters[index]) })), countdown: { ...position(board, SIGNAL_LAYOUT.mastHeightMeters), faceHeadingDegrees } }; -} - -function polygonCenter(geometry) { const ring = geometry?.type === "Polygon" ? geometry.coordinates?.[0] : null; if (!ring || ring.length < 4) return null; const points = ring.slice(0, -1); return [points.reduce((s, p) => s + p[0], 0) / points.length, points.reduce((s, p) => s + p[1], 0) / points.length]; } -function polygonRadius(geometry, center) { const ring = geometry?.type === "Polygon" ? geometry.coordinates?.[0] : null; return ring && center ? Math.max(...ring.slice(0, -1).map((point) => metersBetween(center, point)), 0) : 0; } -function roadAxis(geometry, center, target) { const ring = geometry?.coordinates?.[0]; if (!ring || ring.length < 3) return null; let longest; for (let i = 0; i < ring.length - 1; i += 1) { const dx = (ring[i + 1][0] - ring[i][0]) * Math.cos(center[1] * Math.PI / 180); const dy = ring[i + 1][1] - ring[i][1]; const length = Math.hypot(dx, dy); if (!longest || length > longest.length) longest = { dx, dy, length }; } if (!longest?.length) return null; let axis = [-longest.dy / longest.length, longest.dx / longest.length]; const toward = [(target[0] - center[0]) * Math.cos(center[1] * Math.PI / 180), target[1] - center[1]]; if (axis[0] * toward[0] + axis[1] * toward[1] < 0) axis = [-axis[0], -axis[1]]; return axis; } -function nearestCenter(point, centers) { return centers.map((entry) => ({ ...entry, distance: metersBetween(point, entry.point) })).sort((a, b) => a.distance - b.distance)[0] || null; } -function metersBetween(a, b) { const lat = (a[1] + b[1]) / 2 * Math.PI / 180; return Math.hypot((a[0] - b[0]) * Math.cos(lat), a[1] - b[1]) * Math.PI / 180 * EARTH_RADIUS; } -function moveMeters(point, vector, meters) { const scale = 180 / Math.PI / EARTH_RADIUS; return [point[0] + vector[0] * meters * scale / Math.cos(point[1] * Math.PI / 180), point[1] + vector[1] * meters * scale]; } -function headingBetween(from, to) { const latitude = (from[1] + to[1]) / 2 * Math.PI / 180; return Math.atan2((to[0] - from[0]) * Math.cos(latitude), to[1] - from[1]) * 180 / Math.PI; } -function headingVector(degrees) { const radians = degrees * Math.PI / 180; return [Math.sin(radians), Math.cos(radians)]; } -function normalizeDegrees(value) { return ((value % 360) + 360) % 360; } -function angularDistance(a, b) { return Math.abs(((a - b + 540) % 360) - 180); } - -module.exports = { - SIGNAL_LAYOUT, - signalNodeKey, - buildTrafficSignalFeatures, - validateTrafficSignalFeatures, - validateTrafficSignalSourceReferences, - buildTrafficSignalsFromFeatures, - buildTrafficSignals, - readTrafficSignalFeatures, - readTrafficSignals, -}; diff --git a/packages/road-compiler/test/fixtures/fengshu-er-road.osm b/packages/road-compiler/test/fixtures/fengshu-er-road.osm deleted file mode 100644 index 300a4af..0000000 --- a/packages/road-compiler/test/fixtures/fengshu-er-road.osm +++ /dev/null @@ -1,11091 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/road-compiler/test/fixtures/fengshu-junction-reference.geojson b/packages/road-compiler/test/fixtures/fengshu-junction-reference.geojson deleted file mode 100644 index f5d5069..0000000 --- a/packages/road-compiler/test/fixtures/fengshu-junction-reference.geojson +++ /dev/null @@ -1 +0,0 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","id":"10","properties":{"type":1},"geometry":{"type":"MultiPolygon","coordinates":[[[[114.12670642509887,30.459719866954526],[114.12672480418713,30.459728657730796],[114.12674493919597,30.459737997306732],[114.1267833123607,30.459756158076946],[114.12680436982367,30.45976593212108],[114.12685397313732,30.459789508849415],[114.12690012817846,30.45981125331352],[114.12696581650293,30.459842297256657],[114.12703599666129,30.459875724524046],[114.1270498805918,30.45988229362388],[114.12705936578135,30.459887101893713],[114.12713738017123,30.459929772755643],[114.1272371446353,30.459984098991324],[114.12732032068887,30.460029332274345],[114.12756802667649,30.460156811375963],[114.1276272681774,30.46018476753933],[114.12767330666927,30.460206334960937],[114.12774798860086,30.460241752793856],[114.12781876709889,30.460274913778548],[114.12793737942023,30.46033070051492],[114.12802476152558,30.460371550079984],[114.12803377753897,30.460375985604387],[114.12804110941299,30.460380028660342],[114.12805101374497,30.460386354257707],[114.12806883527543,30.460398025244622],[114.12808654794983,30.46041118828605],[114.12809933710143,30.46042164630667],[114.1281073950421,30.460428573291047],[114.12811395128384,30.460434620557336],[114.12811938213294,30.460440066689017],[114.12812926407065,30.460450347349173],[114.12813473454605,30.46045657462467],[114.12814164327082,30.460464424462735],[114.12814730374363,30.460471255615666],[114.12815294371121,30.460478077312562],[114.12815879253623,30.460486283596325],[114.1281645602278,30.460494776825477],[114.12816883816927,30.460501272024807],[114.12817349886402,30.460509245696944],[114.1281771352045,30.460517071410813],[114.12817986770692,30.460522406328934],[114.12818358105403,30.460530441147434],[114.12818686670175,30.46053817825591],[114.12819018776598,30.460545819342492],[114.12819418910176,30.46055564186251],[114.12820380410353,30.460582194804786],[114.12820842032188,30.46060375471472],[114.12821202983478,30.46062954010985],[114.1282137381741,30.46064526447438],[114.12821400817043,30.460653439918755],[114.12821393731382,30.46067277098964],[114.12821257637805,30.46069672891237],[114.12821098332131,30.460710299619304],[114.12820721720577,30.460726258836115],[114.12819314233184,30.460756750044617],[114.12817805241743,30.46078387834311],[114.1281664689422,30.460803920591136],[114.12815723705758,30.46081876175518],[114.12813556102246,30.46085392543641],[114.12786187058398,30.461286782141404],[114.12767310863411,30.461588040608625],[114.12749605673096,30.46186848121499],[114.12771556983981,30.46197223706339],[114.12829921553568,30.46103318454979],[114.12834745214138,30.460953266211526],[114.12837228716977,30.460914859036752],[114.1283903573218,30.460889225666122],[114.12840962380116,30.46086537779413],[114.12842658919257,30.46084461385105],[114.12844027647458,30.460830720633155],[114.12845700359827,30.460817290594896],[114.12846927807756,30.460807952031754],[114.1284793259837,30.46080120044445],[114.12848732126338,30.46079615416839],[114.12849824982679,30.460790108078196],[114.12851735673152,30.46078057113085],[114.12853428994015,30.460773234382117],[114.12854767478771,30.460768204092016],[114.12856867125925,30.460760493145365],[114.12858706934223,30.460754937340415],[114.12861834861083,30.46074820545063],[114.12862756243905,30.46074661029612],[114.12864869683986,30.46074212232996],[114.12865920934019,30.460740441110147],[114.12866868493343,30.460739396487128],[114.12868036248904,30.460738380348605],[114.12868990874968,30.460737761184348],[114.12870180719327,30.460737363112344],[114.12871667998598,30.460737392256405],[114.1287292771692,30.46073780823301],[114.1287499886682,30.46073912084974],[114.12876640653077,30.460741578573987],[114.12877789632182,30.460743427010925],[114.12878992723334,30.460745602599992],[114.12880068089633,30.46074775119988],[114.12881210760563,30.460750689795557],[114.12882252563773,30.460753300372676],[114.12882949358202,30.460755215632997],[114.12884206581988,30.460759486418695],[114.12884823128418,30.460761483949376],[114.12890089866964,30.460782505183808],[114.12891485544512,30.460788816797255],[114.12895540785316,30.460808048501978],[114.12902501127174,30.46084044145126],[114.12908466293443,30.460868698056956],[114.12910546198879,30.46087847390208],[114.1291300006723,30.460890007409656],[114.12917162969823,30.4609095887503],[114.12927120714293,30.460956547967346],[114.12935900625274,30.460997903663344],[114.12943766593234,30.461035023980024],[114.12945728539037,30.461044170004904],[114.12947426624748,30.461052231980972],[114.12948015380047,30.46105484591913],[114.12948639545796,30.4610574312437],[114.1294967439802,30.461061699880304],[114.12951325767341,30.461068326698438],[114.12953869523535,30.4610785199137],[114.12956431897612,30.461088970193117],[114.12959304786149,30.461100566489673],[114.1296316067205,30.461116134315017],[114.12965073525396,30.461123943783882],[114.1296767364358,30.461134433008322],[114.12971617913543,30.461150365527534],[114.1297826382227,30.461177241498255],[114.12984084737079,30.46120092141013],[114.12985385983599,30.46120636036464],[114.12988107460019,30.46121346938985],[114.12988717793624,30.461215056315822],[114.1299011234562,30.461221666468255],[114.12991177953043,30.461226717093556],[114.12993382466588,30.46123713451079],[114.12998083593384,30.461259449163787],[114.13001973787617,30.46127751673334],[114.13006243727204,30.461297540583438],[114.13013580529461,30.461331867997014],[114.13020225528513,30.461363289096944],[114.13026373880149,30.461392632679807],[114.13029120386813,30.46140550178206],[114.13040786031041,30.461460431954237],[114.13046438304347,30.461487017104876],[114.13058639783064,30.46127753388595],[114.1305697754666,30.461269607588438],[114.13044610731077,30.461211548873724],[114.13036451796869,30.461173104230493],[114.13028045912006,30.46113356386891],[114.13023878831747,30.46111384483735],[114.1301812215938,30.46108665817515],[114.1301369597027,30.461066069392857],[114.13012052111407,30.461058307966308],[114.13010307916063,30.461050038266155],[114.13009311705432,30.461045354054807],[114.12976904001516,30.460871316765],[114.12973565501949,30.460853218474796],[114.12971961636799,30.460845148883948],[114.1295129460683,30.46074116541183],[114.12945722337031,30.46071487608635],[114.12926460885387,30.460624157320336],[114.12907783250328,30.460536187270748],[114.12906810320003,30.46053099337933],[114.12905584211939,30.460524970242773],[114.12902430272453,30.460508788612035],[114.12900645526051,30.460499046548748],[114.12899922449976,30.46049483639869],[114.12899068600481,30.460489375426576],[114.12897983929717,30.460481324246036],[114.12896495996615,30.460469828796573],[114.1289488405392,30.460456110632084],[114.12893156889058,30.460438092299785],[114.12889747265079,30.460394122293796],[114.12889453203489,30.46039033011819],[114.12888995839599,30.46038307458886],[114.12888289382553,30.46037047324551],[114.12887002948217,30.46033996405978],[114.12886289038934,30.460319750928605],[114.12885395471388,30.46029112420495],[114.1288521680376,30.46028171852131],[114.12884964067962,30.46026239223364],[114.1288483923799,30.46025102886467],[114.12884796986727,30.460232582397943],[114.12884875193501,30.460206802769104],[114.1288503392283,30.46019201466402],[114.12885801135928,30.460157905337283],[114.12886084427603,30.46014882845719],[114.12886636717148,30.46013113267193],[114.12887584793602,30.460110139555457],[114.1288838291907,30.460092466765246],[114.12890164662322,30.460061561716298],[114.12891494258247,30.460039083877408],[114.12892546862423,30.46002213124867],[114.12897421219743,30.459943575764843],[114.1289842110455,30.45992736538217],[114.12899391148486,30.459911638771093],[114.12900365647637,30.459895839915685],[114.1290135248707,30.459879840980385],[114.12902702305738,30.45985795729204],[114.1290760801111,30.459778424191825],[114.12928780195848,30.45945907207147],[114.1293908264265,30.459296790906148],[114.12951420054556,30.45909081702124],[114.12929465307654,30.45898788033217],[114.12887225771087,30.459683803494816],[114.12882348281829,30.45976346737858],[114.12881370466052,30.459779839929716],[114.12881212257278,30.45978237574835],[114.12880649093621,30.459791362993084],[114.12879568258535,30.459808611481332],[114.1287862339858,30.45982368999736],[114.12877733346733,30.459837893847396],[114.1287684006543,30.45985214922154],[114.12874671778104,30.459886751646973],[114.12870820465953,30.459948212292552],[114.12869629062735,30.459967225092058],[114.1286909504444,30.459975584332216],[114.1286733227205,30.460001654020463],[114.12866499322047,30.46001094922957],[114.1286409124706,30.460037821819622],[114.12862056779049,30.460055362448653],[114.12859942445834,30.460073591613387],[114.12859892982115,30.460074018074582],[114.12858006237614,30.46008735465943],[114.1285703977271,30.460094186172874],[114.12856173865131,30.46009965466516],[114.12855854817336,30.460101651529794],[114.12854977053016,30.460106600493948],[114.1285407570152,30.460111128749116],[114.1285349971284,30.460113638283904],[114.12853144757541,30.460115080735324],[114.12851676295261,30.460120842817474],[114.12849833445783,30.46012695982037],[114.12849320364326,30.460128659370767],[114.12848915726917,30.46012975301531],[114.12847990318281,30.46013225418443],[114.12846243300255,30.460136775580118],[114.12844856827508,30.460140200201295],[114.12842993780764,30.46014382686593],[114.12841310640472,30.46014658507335],[114.12839158428022,30.46014837514409],[114.12838418126694,30.460149173943524],[114.12837031195335,30.4601498098311],[114.12834724265122,30.460149881331255],[114.12832457028,30.460148815571962],[114.12830033627363,30.460146787633487],[114.12828606564942,30.460144447170077],[114.12826758884803,30.460140971343122],[114.1282376466944,30.460134095768744],[114.12822807094831,30.460131457652615],[114.12821968332197,30.46012905213806],[114.12821317234342,30.460126785540226],[114.12820518502357,30.460123838223822],[114.12819414681434,30.460119689764355],[114.12818404012039,30.460115828881552],[114.12818024400646,30.460114010332596],[114.12816840687336,30.460108401648185],[114.12810279127679,30.460077521889644],[114.12801392613177,30.460035481897506],[114.127875067572,30.45996989516379],[114.1277868310944,30.459928396957878],[114.12776774120755,30.459919418827678],[114.12765252636181,30.45986512851318],[114.12763346721754,30.45985561035686],[114.1274294888824,30.459774464597885],[114.12729689668743,30.459721334876495],[114.12725367350123,30.459704157756786],[114.12720564883506,30.459685197896864],[114.12713351632975,30.459651534720905],[114.12711081207827,30.459641142294075],[114.12704721321815,30.459610978372535],[114.12699816961752,30.45958834667066],[114.12693500878125,30.459558363528384],[114.12687186487535,30.459528720775257],[114.12682601995878,30.459507090823386],[114.12670642509887,30.459719866954526]]]]}},{"type":"Feature","id":"11","properties":{"type":2},"geometry":{"type":"MultiPolygon","coordinates":[[[[114.12667851705592,30.459769519300867],[114.12678669733096,30.459820404831465],[114.12696098364239,30.459902229147097],[114.12706246005538,30.459954887442564],[114.12713863456968,30.45999580532362],[114.1272445047874,30.460052759625377],[114.12729073755453,30.460077508855534],[114.1275086759357,30.460190139691004],[114.12793638723153,30.460393536847192],[114.1280091529264,30.460429436483583],[114.12807692852476,30.460488494764924],[114.12809608381721,30.46050926878318],[114.12812869945513,30.460564335635073],[114.12814437677969,30.460598855787218],[114.12815471974382,30.460634219292533],[114.12815795586218,30.46065232227354],[114.12816084802566,30.460666226523696],[114.12816297177484,30.460678205130506],[114.12816330989769,30.460697061140195],[114.1281615490448,30.460711335366184],[114.12815967333917,30.460725154080695],[114.12815412047559,30.460745801236786],[114.12813685164059,30.460778711292647],[114.12812526814679,30.46079875352665],[114.12811603624742,30.46081359467971],[114.1279496458915,30.46108040507848],[114.12746442959613,30.46185587039544],[114.12748164436566,30.461863076578037],[114.12797226529804,30.461083207482826],[114.12808936576536,30.460894645704666],[114.12813972501878,30.46081439412671],[114.1281489569059,30.46079955296438],[114.12816054038488,30.46077951071853],[114.1281755585996,30.46075295372288],[114.12818963347885,30.46072246251689],[114.12819258724711,30.460709382623207],[114.12819434809374,30.460695108394308],[114.12819591291681,30.46067465433775],[114.1281960248573,30.46066411627798],[114.12819561473438,30.460648068731086],[114.12819466169702,30.46063554207201],[114.12819279494028,30.460620418411153],[114.12818940492706,30.460607712318566],[114.12818563805288,30.460597393544138],[114.1281819505387,30.460586594127783],[114.12817989670708,30.460577862964488],[114.12817924467245,30.460573839455144],[114.12817949504986,30.46057124649343],[114.12818053476947,30.460570151389053],[114.12818221818578,30.46056899362691],[114.12818652570962,30.46056741402056],[114.12819690363179,30.46056423973758],[114.12819339565145,30.460554513371267],[114.12818938424755,30.460544357421085],[114.12818682298207,30.46053814209666],[114.12818353733651,30.460530404990013],[114.12817982399187,30.460522370173596],[114.12817709149125,30.460517035257006],[114.1281748449635,30.4605134072006],[114.12815999976104,30.460516411050083],[114.1281569844651,30.460516108596025],[114.12815427533317,30.46051473858637],[114.1281504066004,30.46051071863724],[114.12813888688173,30.460497459445044],[114.12813622851498,30.46049402946341],[114.12812933351096,30.460485710839954],[114.12812094472461,30.460476242046507],[114.12811173169072,30.460466178487792],[114.12809933858425,30.460453694910942],[114.12808964467422,30.460444747095103],[114.1280722884732,30.460430668424696],[114.12806373666007,30.460423929440257],[114.12805096285751,30.4604148188883],[114.12803869437997,30.460406548926965],[114.12802808197053,30.46039991968587],[114.12802370738075,30.46039743663877],[114.12801513967143,30.460393015625648],[114.12799381210561,30.46038291219492],[114.12797508311029,30.460374198797272],[114.12791771802227,30.460347195819843],[114.12767435823383,30.46023264012388],[114.12762175159291,30.460208054352417],[114.12750768942868,30.460153778331048],[114.12750968523949,30.46015305383419],[114.12751615320019,30.460151116056814],[114.12752101174054,30.460149847807664],[114.1275245010255,30.460149209070295],[114.12753385950025,30.46014791885199],[114.12753905352344,30.460147613899547],[114.12754442532368,30.460147980243853],[114.12732052000571,30.460030682280813],[114.12732096353244,30.460033522969436],[114.1273209437513,30.46003526354767],[114.12732042193159,30.460039600474627],[114.12731982187725,30.460044125178978],[114.12731900199067,30.46004906017859],[114.12731813378313,30.460053516056735],[114.12731760854854,30.460055567040012],[114.12731527476217,30.46005429975084],[114.12725782565802,30.460023057477574],[114.1271690692801,30.459974725730937],[114.12702132571069,30.459895863096776],[114.1270118405141,30.45989105481675],[114.12699795657305,30.459884485702027],[114.126927776362,30.45985105836026],[114.1268620879885,30.459820014347677],[114.1268159329123,30.459798269834298],[114.12676616873911,30.459774841761565],[114.12674527208262,30.4597649189869],[114.12672905649116,30.459757399324875],[114.12669446854811,30.459741139345454],[114.12667851705592,30.459769519300867]]],[[[114.12683688366135,30.459487762763597],[114.12694685034681,30.45953947097963],[114.12702821410709,30.459577593679693],[114.1271414291467,30.459631188953093],[114.12719322411404,30.459655676670067],[114.12726367595704,30.459686168438914],[114.1273874043263,30.45973747924787],[114.12745538294098,30.459766253026842],[114.12746249171516,30.459769261998932],[114.12746847497262,30.459771794566112],[114.12746506444269,30.45977297871145],[114.12745847670172,30.45977469440682],[114.12745466519337,30.459775504406007],[114.12744835030661,30.459775950289142],[114.12744153549609,30.459776037784387],[114.12743665669757,30.459775368827874],[114.12743317980241,30.459774519397744],[114.12763742270677,30.45985630983784],[114.1276294917997,30.45985222493772],[114.12762504577312,30.45984893496641],[114.12761871568127,30.45984280134446],[114.1276187147639,30.45984276277992],[114.12761871065264,30.45984272395687],[114.12761870683686,30.459842685110296],[114.1276187033168,30.45984264624262],[114.12761870009267,30.459842607355274],[114.1276186971644,30.45984256845031],[114.12761869453227,30.459842529528967],[114.12761869219653,30.45984249059319],[114.12761869062959,30.45984246138299],[114.12761868924792,30.45984243216579],[114.12761868801458,30.459842402943504],[114.12761868696664,30.459842373715418],[114.12761868606704,30.459842344483548],[114.12761868535262,30.459842315247634],[114.1276186846472,30.459842276262652],[114.12761868423841,30.4598422372737],[114.12761868411735,30.459842208031027],[114.12761868418148,30.459842178787778],[114.12761868439428,30.45984214954512],[114.12761868479193,30.459842120303886],[114.12761868533845,30.459842091064104],[114.12761868607039,30.459842061827327],[114.12761868729305,30.459842022849955],[114.12761868881246,30.459841983880256],[114.12761869013697,30.459841954658607],[114.12761869164726,30.459841925443666],[114.12761869330551,30.459841896234774],[114.1276186951485,30.459841867033727],[114.12761869785317,30.459841828111287],[114.12761870085355,30.459841789205232],[114.12761870415021,30.459841750316855],[114.12761870680785,30.45984172116234],[114.12761870964978,30.459841692021122],[114.1276187126398,30.459841662890913],[114.12761871581431,30.459841633775135],[114.12761871913659,30.45984160467167],[114.1276187226427,30.45984157558435],[114.12761872756414,30.45984153682401],[114.12761873143927,30.459841507771703],[114.1276187354987,30.459841478738042],[114.12761873970517,30.459841449720273],[114.1276187440955,30.459841420722743],[114.1276187501946,30.459841382087735],[114.12761875658737,30.459841343488495],[114.1276187615654,30.459841314561874],[114.12761876672647,30.459841285659866],[114.12761877385242,30.45984124715648],[114.12761878127093,30.459841208694883],[114.12761878898186,30.459841170276164],[114.1276187949478,30.459841141490177],[114.12761880109602,30.459841112732796],[114.12761880953707,30.459841074429427],[114.12761881826873,30.459841036174858],[114.12761882499953,30.459841007515504],[114.12761883191142,30.45984097888851],[114.12761883896847,30.459840950288267],[114.12761884620718,30.45984092172191],[114.12761885359049,30.45984089318333],[114.12761886115479,30.45984086468046],[114.12761887148058,30.45984082672498],[114.12761888209575,30.459840788829606],[114.12761889299816,30.45984075099531],[114.12761890418855,30.459840713224306],[114.12761891276041,30.459840684936363],[114.1276189215118,30.459840656689476],[114.12761893341859,30.45984061908359],[114.12761894252766,30.459840590921647],[114.12761895181512,30.459840562803826],[114.12761896443511,30.45984052537278],[114.12761897734006,30.459840488014866],[114.12761899052893,30.45984045073134],[114.12761900059769,30.45984042281616],[114.12761901084313,30.459840394949538],[114.12761902122963,30.459840367122013],[114.12761903179226,30.45984033934458],[114.12761904611081,30.459840302375543],[114.12761906070975,30.45984026548902],[114.12761907558946,30.45984022868712],[114.12761908692329,30.45984020113895],[114.12761909843222,30.45984017364542],[114.12761911008049,30.459840146196054],[114.12761912190228,30.459840118802354],[114.12761913386308,30.459840091454122],[114.12761914599669,30.459840064163327],[114.12761915826864,30.459840036918962],[114.12761917071323,30.45984000973357],[114.12761918753536,30.459839973566115],[114.12761920463223,30.459839937495637],[114.12761922200302,30.459839901523655],[114.12761923964709,30.459839865651997],[114.12761925305021,30.459839838811785],[114.12761926662367,30.459839812035433],[114.1276192849469,30.45983977642092],[114.12761930354125,30.459839740911907],[114.12761932237923,30.459839705557602],[114.12761934140767,30.4598396704538],[114.12761936070153,30.45983963545903],[114.12761937533706,30.45983960928203],[114.12761939013714,30.4598395831747],[114.12761940506908,30.459839557123757],[114.12761942016478,30.459839531144077],[114.12761943539132,30.459839505221865],[114.12761945078094,30.459839479372334],[114.12761947151769,30.459839445004132],[114.12761949251376,30.459839410754853],[114.12761951376811,30.459839376625645],[114.12761953528096,30.459839342618263],[114.1276195570496,30.459839308734132],[114.12761957907493,30.459839274974502],[114.12761960135458,30.459839241341257],[114.1276196238882,30.459839207835074],[114.12761964667455,30.45983917445846],[114.12761966971314,30.459839141211692],[114.12761969300254,30.45983910809728],[114.12761971062592,30.45983908334458],[114.12761972840475,30.459839058676106],[114.1276197523174,30.459839025897416],[114.12761977040617,30.45983900139877],[114.12761978864927,30.459838976986457],[114.12761980701526,30.459838952643675],[114.12761982553445,30.45983892838848],[114.12761985042985,30.459838896165554],[114.1276198755689,30.459838864085224],[114.12761990094943,30.4598388321488],[114.12761992013613,30.45983880828728],[114.12761993947205,30.459838784516933],[114.12761996545335,30.45983875294645],[114.12761999167229,30.45983872152413],[114.1276200181291,30.459838690251917],[114.1276200381189,30.459838666892264],[114.12762005825522,30.459838643627762],[114.12762007850922,30.459838620440074],[114.12762009890882,30.45983859734891],[114.12762012630185,30.459838566690046],[114.12762015392593,30.459838536188006],[114.12762018178003,30.45983850584421],[114.1276202098635,30.459838475659282],[114.12762023817392,30.45983844563511],[114.12762026671132,30.459838415772875],[114.12762028825482,30.459838393478226],[114.12762030993893,30.45983837128579],[114.12762033903648,30.459838341834047],[114.12762036835612,30.459838312548637],[114.12762039048407,30.45983829068925],[114.12762041279098,30.459838268894966],[114.12762044271615,30.459838239977927],[114.12762047285979,30.45983821123222],[114.12762050322027,30.45983818265933],[114.12762053379673,30.459838154260165],[114.1276205568633,30.459838133070182],[114.12762058006322,30.45983811199049],[114.12762061117319,30.45983808403135],[114.1276206424942,30.459838056250543],[114.12762067402507,30.45983802864933],[114.12762070576474,30.459838001228317],[114.12762073771073,30.459837973989174],[114.12762076986259,30.459837946933085],[114.12762079410406,30.459837926755934],[114.12762081847264,30.459837906694375],[114.12762085113233,30.459837880100626],[114.12762088399268,30.459837853693976],[114.12762091705243,30.459837827475287],[114.12762095030996,30.459837801446138],[114.12762098376406,30.45983777560717],[114.12762101741312,30.45983774995985],[114.12762105125513,30.45983772450498],[114.1276210767572,30.459837705535097],[114.12762110237843,30.45983768668677],[114.12762112809455,30.459837667936096],[114.12762115392856,30.45983764930772],[114.12762117985655,30.459837630778082],[114.12762120590098,30.459837612372162],[114.12762123203778,30.45983759406543],[114.12762125829046,30.459837575883395],[114.12762129344584,30.459837551807507],[114.12762131992537,30.459837533876286],[114.12762134651851,30.459837516071463],[114.127621382125,30.459837492500615],[114.12762140894068,30.45983747494968],[114.12762143586701,30.45983745752674],[114.12762147191441,30.45983743446725],[114.1276215081359,30.459837411614217],[114.12762153540983,30.45983739460385],[114.12762156279013,30.45983737772347],[114.12762159943968,30.459837355390285],[114.1276216270315,30.459837338771592],[114.12762165472797,30.459837322284315],[114.12762169179473,30.45983730047752],[114.12762172902579,30.459837278882418],[114.12762175705105,30.45983726281962],[114.12762178517671,30.45983724688988],[114.12762182281158,30.459837225829375],[114.12762186060465,30.459837204984552],[114.12762189844541,30.45983718441425],[114.12762193611204,30.45983716423502],[114.12762196445577,30.459837149233792],[114.12762199289281,30.459837134366264],[114.12762203093145,30.45983711472131],[114.1276220691167,30.45983709529137],[114.12762209784599,30.459837080854093],[114.12762212666482,30.459837066552055],[114.12762215555456,30.459837052358374],[114.12762218453251,30.45983703830107],[114.12762222328578,30.459837019739837],[114.12762225243738,30.459837005955855],[114.1276222816744,30.459836992309274],[114.12762231097932,30.45983697877282],[114.12762234036788,30.459836965374453],[114.12762237966366,30.4598369476944],[114.12762241909073,30.45983693023659],[114.12762245864755,30.459836913002057],[114.12762249833237,30.45983689599091],[114.12762253814398,30.45983687920406],[114.12762256808047,30.459836866754415],[114.12762259809404,30.459836854446106],[114.12762262816855,30.459836842250773],[114.12762265831932,30.45983683019728],[114.12762269861896,30.459836814314745],[114.12762273903685,30.459836798660557],[114.12762276942279,30.459836787062446],[114.12762279988081,30.459836775607666],[114.12762284058573,30.45983676052571],[114.1276228815797,30.459836745610463],[114.12762292274388,30.459836730906748],[114.12762296401716,30.45983671643723],[114.1276230053987,30.459836702202328],[114.12762304688614,30.45983668820277],[114.12762308847797,30.459836674439074],[114.12762313017268,30.45983666091164],[114.1276231719677,30.459836647621437],[114.12762321386248,30.459836634568866],[114.12762324534398,30.459836624928133],[114.12762327688561,30.45983661543659],[114.12762330847417,30.45983660606458],[114.12762334012137,30.459836596842106],[114.12762337181421,30.459836587739506],[114.12762340356346,30.45983657878724],[114.12762344597049,30.45983656705118],[114.1276234778297,30.459836558399925],[114.1276235097433,30.459836549899297],[114.12762355236475,30.45983653876698],[114.12762358438253,30.459836530569017],[114.12762361645154,30.459836522522764],[114.12762365927591,30.459836511996816],[114.12762370217928,30.45983650171443],[114.12762373440403,30.459836494154903],[114.12762376667597,30.459836486747943],[114.12762379898444,30.459836479463302],[114.12762383133824,30.45983647233185],[114.12762387453527,30.459836463027482],[114.12762391780132,30.45983645396862],[114.127623950293,30.459836447328122],[114.12762398282547,30.45983644084116],[114.1276240262554,30.459836432397626],[114.12762406974804,30.459836424201143],[114.12762411330148,30.459836416251473],[114.12762414600274,30.45983641044426],[114.12762417874067,30.459836404791663],[114.12762422243705,30.459836397461554],[114.12762426647649,30.45983639033405],[114.12762431066506,30.45983638344345],[114.12762435490482,30.459836376806383],[114.12762439919386,30.4598363704228],[114.12762444353012,30.459836364293214],[114.12762448791192,30.459836358417853],[114.12762453233674,30.459836352796604],[114.12762457680334,30.459836347430212],[114.12762462130945,30.45983634231879],[114.1276246547125,30.459836338644507],[114.12762468813864,30.45983633512997],[114.12762472158133,30.45983633174301],[114.12762475504547,30.45983632851574],[114.12762479969035,30.45983632442604],[114.12762483319266,30.45983632151856],[114.127624866713,30.45983631877129],[114.1276249114282,30.459836315321517],[114.12762494498104,30.459836312894442],[114.12762497854808,30.459836310627463],[114.12762502332265,30.45983630781871],[114.12762506811713,30.45983630526654],[114.12762511292976,30.459836302971063],[114.12762515775864,30.459836300932515],[114.12762520260196,30.459836299150833],[114.12762523624184,30.45983629797529],[114.12762526988877,30.459836296960308],[114.12762531475859,30.459836295821326],[114.12762535963614,30.459836294939453],[114.12762540451882,30.459836294314968],[114.1276254494054,30.45983629394731],[114.12762549429334,30.459836293837213],[114.12762552795887,30.4598362939152],[114.12762556162362,30.459836294154258],[114.12762559528632,30.459836294521654],[114.12762562880437,30.459836295046962],[114.12762567311198,30.459836295951487],[114.12762570633784,30.45983629678661],[114.12762573955806,30.45983629777844],[114.12762577277238,30.45983629889571],[114.127625805979,30.45983630016923],[114.127625850243,30.459836302076255],[114.1276258944916,30.459836304233814],[114.12762593872274,30.459836306641275],[114.1276259829345,30.459836309299394],[114.12762601608061,30.459836311449266],[114.12762604921208,30.459836313755456],[114.12762609336727,30.45983631703821],[114.12762613749685,30.459836320571167],[114.12762618159863,30.45983632435347],[114.12762622567084,30.45983632838541],[114.12762626971127,30.459836332666814],[114.1276263137185,30.459836337197064],[114.12762635769045,30.459836341976207],[114.12762640162542,30.459836347004373],[114.12762643455244,30.4598363509307],[114.12762646745382,30.45983635501222],[114.12762650033469,30.459836359217938],[114.12762653318843,30.459836363578727],[114.12762657695646,30.459836369599568],[114.12762662067755,30.459836375867443],[114.12762665343853,30.459836380723218],[114.12762668616885,30.459836385733446],[114.12762672976561,30.45983639261954],[114.12762677368023,30.45983639981385],[114.12762681766263,30.45983640727993],[114.1276268615865,30.45983641499749],[114.1276269054507,30.45983642296648],[114.1276269492532,30.459836431186268],[114.12762699299216,30.459836439656467],[114.12762702575566,30.459836446165387],[114.12762705847707,30.459836452830707],[114.1276271020486,30.45983646192548],[114.12762713468335,30.45983646890252],[114.12762716727306,30.45983647603511],[114.12762721066561,30.45983648575225],[114.12762724316296,30.459836493195315],[114.12762727561211,30.459836500793024],[114.12762731881337,30.45983651112993],[114.12762736193498,30.459836521713676],[114.12762740497458,30.45983653254432],[114.12762743720252,30.45983654082061],[114.12762746937713,30.459836549250873],[114.12762751220447,30.45983656069578],[114.1276275442704,30.459836569432436],[114.12762757628029,30.459836578321926],[114.1276276188839,30.459836590377932],[114.12762766139517,30.459836602677782],[114.12762770381161,30.459836615220507],[114.12762774613181,30.459836628005814],[114.12762778835355,30.459836641033036],[114.12762783047522,30.459836654301775],[114.12762787249478,30.45983666781129],[114.12762791441045,30.45983668156107],[114.12762795622102,30.459836695550614],[114.12762799792417,30.459836709778944],[114.12762802913298,30.459836720599355],[114.12762806027291,30.459836731568572],[114.12762810169936,30.459836746391844],[114.1276281430122,30.459836761451868],[114.12762818420902,30.459836776748535],[114.12762821503378,30.459836788367827],[114.12762824578444,30.45983680013411],[114.12762828668501,30.459836816018285],[114.12762832746405,30.459836832135693],[114.12762836811932,30.459836848486344],[114.12762840864899,30.45983686506971],[114.12763036571418,30.45983774033913],[114.12767012739805,30.45985553030195],[114.12777511998902,30.45990500231548],[114.12779109555976,30.459912477348286],[114.12791443773256,30.45997081696305],[114.12804831801135,30.460034140516466],[114.12812695665549,30.46007133529421],[114.12817294740331,30.46009199515834],[114.12820544743163,30.460104998116936],[114.12822395736802,30.460111308702093],[114.12825057198464,30.460118745368774],[114.12828752276148,30.460126209644613],[114.12830984742014,30.46012992768818],[114.12832069121107,30.46013139808863],[114.12833517285914,30.460132563528205],[114.1283542702872,30.46013382655505],[114.12836998873179,30.46013406342985],[114.12837846217042,30.460133860706673],[114.12838442976265,30.46013334116843],[114.12839168437102,30.46009575927462],[114.12834998644269,30.460093641808722],[114.12828941210206,30.460081463918776],[114.12824893713191,30.460074122081945],[114.128216315597,30.46006719885561],[114.12780441489443,30.459881181622045],[114.12769332402713,30.45982907167044],[114.12765769404636,30.45981272951338],[114.12742849592884,30.459716222464763],[114.1272364659745,30.459626098477848],[114.12718111350233,30.45960016361023],[114.12709825116885,30.459561251063718],[114.12699977087117,30.45951488076466],[114.12691108838536,30.45947279517776],[114.12685896736635,30.45944847273632],[114.12683688366135,30.459487762763597]]],[[[114.13061898402532,30.46122158770834],[114.13050093394426,30.4611659455433],[114.13011032314509,30.460982830355253],[114.12948509785771,30.460676729562238],[114.12915080419201,30.46052159776799],[114.12910905022575,30.460499808444705],[114.12908108043383,30.460480458154805],[114.12906609730352,30.46046825584629],[114.12904116432249,30.460440950193757],[114.1290302547096,30.46042482415912],[114.12902839379315,30.46042077514532],[114.12885361666702,30.460284356806863],[114.12885456445991,30.460289597977344],[114.12885699706965,30.460299440816254],[114.12886055715941,30.460311722664137],[114.1288644893895,30.460324983381874],[114.12886998531407,30.460339927505565],[114.12887792622648,30.46035876025066],[114.12888132456803,30.460365733766483],[114.12888470366988,30.46037169567034],[114.12888717307217,30.460376077774853],[114.12889053410137,30.460381787712556],[114.12889339870573,30.460386617190853],[114.12889489628813,30.460389215954844],[114.12889742846474,30.460394085724353],[114.12893152468206,30.460438055711222],[114.1289487963196,30.4604560740338],[114.12895919300571,30.46046399981868],[114.12896128462303,30.460460423906998],[114.1289652252416,30.46045728503223],[114.1289692683392,30.460455341689837],[114.12897714389003,30.460453956628445],[114.12898001822931,30.460454375339527],[114.12898246998583,30.460455143136826],[114.12898620474245,30.46045904078643],[114.12900059349039,30.460471954477043],[114.12901977307614,30.46048624243595],[114.12903720594392,30.460497358755884],[114.12904224205833,30.460499925867214],[114.12906283040171,30.460510409674747],[114.1290906202127,30.460524959988184],[114.1294632289959,30.460700150112054],[114.12947592210453,30.460706120207483],[114.12947208127396,30.46072074536206],[114.12951290148226,30.460741128494877],[114.12971957164879,30.460845111851004],[114.12973316140021,30.460850578955355],[114.12973317110324,30.46085054094337],[114.12973318087785,30.460850502945007],[114.12973319072499,30.460850464960792],[114.12973320064394,30.460850426990543],[114.12973321063416,30.460850389034093],[114.12973322069686,30.460850351092297],[114.12973323083084,30.460850313164645],[114.12973324103676,30.460850275251637],[114.12973325131463,30.460850237352837],[114.12973326166396,30.460850199468855],[114.12973327208536,30.4608501615997],[114.12973328257823,30.460850123745423],[114.12973329314272,30.460850085906138],[114.12973330377868,30.460850048081735],[114.12973331448659,30.460850010272384],[114.12973332526539,30.460849972478595],[114.12973333611637,30.46084993469997],[114.12973334703862,30.460849896937077],[114.12973335803225,30.460849859189462],[114.12973336909747,30.46084982145753],[114.12973338023383,30.46084978374155],[114.12973339144177,30.460849746041255],[114.12973340272111,30.46084970835714],[114.12973341407171,30.460849670689047],[114.12973342549365,30.460849633037082],[114.12973343698654,30.46084959540154],[114.12973344855115,30.460849557782296],[114.12973346018664,30.460849520179355],[114.12973347189309,30.46084948259334],[114.12973348367112,30.460849445023626],[114.12973349552028,30.4608494074709],[114.12973350743991,30.46084936993516],[114.12973351943081,30.4608493324164],[114.12973353149319,30.460849294914627],[114.12973354362605,30.46084925742984],[114.12973355582992,30.460849219962604],[114.12973356810481,30.460849182512636],[114.12973358045053,30.460849145080452],[114.12973359286681,30.46084910766571],[114.12973360535426,30.460849070268512],[114.12973361791227,30.46084903288939],[114.12973363054132,30.4608489955281],[114.12973364324083,30.4608489581847],[114.1297336560108,30.460848920859487],[114.12973366885136,30.460848883552394],[114.12973368176294,30.460848846263648],[114.12973369474476,30.460848808993372],[114.12973370779727,30.46084877174144],[114.12973372092013,30.460848734508314],[114.12973373411369,30.460848697294107],[114.12973374737749,30.460848660098307],[114.12973376071149,30.460848622921482],[114.12973377411633,30.460848585763742],[114.12973378759119,30.460848548625098],[114.12973380113627,30.460848511505596],[114.12973381475169,30.46084847440558],[114.12973382843747,30.460848437324884],[114.1297338421928,30.460848400263618],[114.12973385601907,30.460848363222006],[114.12973386991467,30.460848326200225],[114.12973388388117,30.460848289198104],[114.12973389791702,30.460848252215982],[114.12973391202287,30.460848215253858],[114.12973392619884,30.460848178311903],[114.1297339404446,30.460848141390173],[114.12973395476035,30.460848104488615],[114.12973396914592,30.460848067607564],[114.12973398360111,30.460848030747087],[114.12973399812635,30.460847993907294],[114.12973401272126,30.46084795708806],[114.12973402738538,30.460847920289403],[114.12973404211985,30.460847883512105],[114.12973405692318,30.460847846755435],[114.1297340717962,30.460847810019956],[114.12973408673935,30.460847773305897],[114.12973410175135,30.460847736613093],[114.1297341168327,30.460847699941535],[114.12973413198372,30.460847663291453],[114.12973414720406,30.460847626663142],[114.12973416249373,30.46084759005659],[114.12973417785264,30.46084755347163],[114.12973419328075,30.460847516908828],[114.12973420877796,30.46084748036779],[114.12973422434438,30.460847443848913],[114.12973423997992,30.460847407352198],[114.12973425568457,30.460847370877982],[114.12973427145819,30.46084733442604],[114.12973428730056,30.460847297996434],[114.12973430321183,30.46084726158967],[114.12973431919244,30.460847225205406],[114.12973433524168,30.460847188843985],[114.12973435135945,30.46084715250558],[114.12973436754645,30.460847116189903],[114.12973438380163,30.46084707989758],[114.1297344001257,30.460847043628274],[114.12973441651842,30.46084700738232],[114.12973443297979,30.460846971159608],[114.12973444950947,30.460846934960653],[114.1297344661079,30.46084689878505],[114.12973448277431,30.4608468626332],[114.12973449950948,30.460846826504934],[114.12973451631328,30.460846790400765],[114.12973453318486,30.46084675432063],[114.12973455012506,30.460846718264136],[114.12973456713335,30.460846682232365],[114.12973458420971,30.4608466462244],[114.12973460135419,30.460846610240875],[114.12973461856694,30.460846574281785],[114.12973463584757,30.460846538347475],[114.12973465319605,30.460846502437654],[114.12973467061272,30.460846466552436],[114.12973468809724,30.46084643069223],[114.1297347056494,30.460846394856965],[114.12973472326964,30.46084635904671],[114.1297347409575,30.46084632326146],[114.12973475871311,30.46084628750184],[114.12973477653622,30.460846251766938],[114.12973479442687,30.460846216057842],[114.12973481238535,30.460846180374087],[114.12973483041137,30.46084614471608],[114.12973484850464,30.460846109083704],[114.12973486666525,30.460846073477246],[114.12973488489334,30.460846037896466],[114.12973490318895,30.460846002341892],[114.12973492155162,30.4608459668134],[114.12973493998136,30.460845931310942],[114.12973495847815,30.460845895834915],[114.12973497704223,30.460845860385252],[114.12973499567349,30.46084582496201],[114.1297350143717,30.46084578956538],[114.12973503313673,30.460845754195446],[114.12973505196894,30.460845718852177],[114.12973507086788,30.460845683535787],[114.12973508983355,30.460845648246394],[114.12973510886603,30.460845612984055],[114.12973512796502,30.460845577748707],[114.12973514713129,30.460845542540756],[114.1297351663636,30.4608455073602],[114.12973518566251,30.460845472207044],[114.12973520502815,30.460845437081385],[114.12973522447447,30.460845401957243],[114.12973524398728,30.46084536686072],[114.12973526356659,30.460845331792104],[114.1297352832125,30.46084529675162],[114.12973530292444,30.46084526173927],[114.12973532270277,30.460845226754653],[114.12973534254714,30.46084519179834],[114.1297353624581,30.460845156870626],[114.12973538243492,30.460845121971378],[114.12973540247751,30.460845087100207],[114.12973542258638,30.460845052257852],[114.12973544276153,30.46084501744426],[114.12973546300248,30.46084498265943],[114.12973548330854,30.460844947903578],[114.12973550368125,30.46084491317649],[114.12973552411918,30.460844878478678],[114.12973554462316,30.460844843809905],[114.12973556519239,30.460844809170403],[114.1297355858273,30.46084477456029],[114.1297356065278,30.460844739979848],[114.12973562729402,30.460844705428787],[114.12973564812548,30.46084467090734],[114.12973566902218,30.460844636415676],[114.12973568998424,30.460844601953795],[114.12973571101132,30.460844567521697],[114.12973573210387,30.460844533120067],[114.12973575326133,30.460844498747935],[114.12973577448392,30.460844464406215],[114.12973579577152,30.46084443009507],[114.1297358171238,30.460844395814338],[114.12973583854122,30.46084436156379],[114.1297358600239,30.460844327343764],[114.12973588157078,30.460844293154544],[114.12973590318224,30.46084425899602],[114.12973592485882,30.460844224868357],[114.12973594659975,30.46084419077167],[114.12973596840513,30.460844156706084],[114.1297359902752,30.460844122671467],[114.12973601220969,30.460844088668065],[114.12973603420832,30.460844054696032],[114.12973605627127,30.46084402075561],[114.12973607839879,30.46084398684639],[114.12973610059021,30.460843952968883],[114.12973612284573,30.460843919123104],[114.12973614516581,30.460843885309092],[114.12973616754921,30.460843851526803],[114.12973618999683,30.460843817776457],[114.12973621250835,30.46084378405834],[114.12973623508363,30.460843750372167],[114.12973625772257,30.460843716718458],[114.12973628042562,30.460843683096805],[114.12973630319165,30.46084364950778],[114.1297363260219,30.460843615951042],[114.12973634891513,30.46084358242699],[114.12973637187213,30.46084354893551],[114.12973639489235,30.460843515477],[114.1297364179762,30.460843482051338],[114.12973644112304,30.46084344865843],[114.12973646433308,30.460843415298715],[114.12973648760621,30.46084338197197],[114.12973651094244,30.46084334867893],[114.12973653434175,30.460843315418522],[114.12973655780404,30.460843282192165],[114.1297365813292,30.460843248998835],[114.12973660491721,30.460843215839155],[114.12973662856798,30.46084318271313],[114.12973665228115,30.46084314962104],[114.12973667605745,30.460843116562604],[114.12973669989564,30.460843083538276],[114.12973672379708,30.46084305054811],[114.12973674776023,30.460843017591717],[114.12973677178626,30.46084298466965],[114.12973679587434,30.460842951782038],[114.12973682002496,30.460842918928645],[114.1297368442373,30.460842886109816],[114.12973686851215,30.46084285332538],[114.1297368928491,30.460842820575735],[114.12973691724753,30.460842787860877],[114.12973694170815,30.460842755180927],[114.12973696623061,30.460842722535595],[114.12973699081468,30.46084268992551],[114.12973701546082,30.460842657350554],[114.12973704016846,30.46084262481056],[114.12973706493784,30.46084259230604],[114.1297370897685,30.460842559836937],[114.12973711466043,30.460842527402853],[114.12973713961374,30.460842495004698],[114.12973716462866,30.46084246264207],[114.12973718970476,30.46084243031509],[114.12973721484202,30.46084239802409],[114.1297372400401,30.460842365768798],[114.12973726529978,30.46084233354943],[114.12973729061983,30.460842301366167],[114.12973731600107,30.460842269219334],[114.12973734144309,30.46084223710821],[114.12973736694595,30.46084220503398],[114.12973739250975,30.460842172995626],[114.12973741813359,30.460842140994103],[114.12973744381833,30.460842109028967],[114.1297374695639,30.460842077100555],[114.12973749536906,30.460842045209155],[114.12973752123504,30.460842013354196],[114.12973754716137,30.460841981536305],[114.12973757314764,30.46084194975559],[114.12973759919439,30.460841918012],[114.12973762530102,30.460841886305307],[114.1297376514678,30.460841854635913],[114.1297376776943,30.46084182300403],[114.12973770393836,30.460841791460155],[114.12973773024235,30.46084175995368],[114.12973775660524,30.460841728484848],[114.12973778302835,30.460841697053365],[114.12973780951026,30.460841665659746],[114.1297378360514,30.460841634303875],[114.12973786265248,30.460841602985642],[114.12973788931195,30.4608415717055],[114.12973791603115,30.46084154046334],[114.12973794280876,30.46084150925915],[114.12973796964584,30.460841478093283],[114.1297379965416,30.460841446965734],[114.12973802349589,30.46084141587616],[114.1297380505093,30.46084138482542],[114.12973807758071,30.460841353812825],[114.12973810471146,30.460841322838952],[114.12973813190021,30.460841291903677],[114.1297381591477,30.460841261007296],[114.12973818645354,30.46084123014957],[114.12973821381759,30.46084119933091],[114.12973824123986,30.460841168551244],[114.12973826872056,30.460841137810586],[114.12973829625902,30.46084110710921],[114.12973832385524,30.460841076447178],[114.12973835150966,30.46084104582449],[114.12973837922208,30.46084101524092],[114.1297384069918,30.460840984696862],[114.12973843481961,30.460840954192545],[114.12973846270495,30.460840923727798],[114.12973849064808,30.460840893302965],[114.12973851864791,30.460840862917873],[114.12973854670584,30.46084083257269],[114.1297385748211,30.460840802267484],[114.12973860299297,30.460840772002296],[114.1297386312228,30.460840741777428],[114.12973865950896,30.460840711592525],[114.12973868785284,30.460840681448328],[114.12973871625313,30.460840651344103],[114.1297387447104,30.460840621280703],[114.12973877322459,30.46084059125772],[114.12973880179534,30.460840561275567],[114.12973883042291,30.460840531333726],[114.12973885910677,30.46084050143316],[114.12973888784748,30.460840471573245],[114.12973891664413,30.460840441753987],[114.1297389454974,30.460840411976232],[114.12973897440673,30.46084038223947],[114.12973900337235,30.460840352543872],[114.1297390323938,30.460840322889613],[114.12973906147154,30.460840293276515],[114.12973909060543,30.460840263705265],[114.12973911979493,30.460840234175237],[114.1297391490399,30.460840204686885],[114.12973917834107,30.46084017524021],[114.12973920769737,30.46084014583527],[114.12973923710942,30.460840116472177],[114.12973926657692,30.460840087151162],[114.12973929609981,30.460840057871987],[114.12973932567807,30.46084002863506],[114.12973935531136,30.46083999944016],[114.1297393849997,30.46083997028755],[114.12973941474364,30.460839941177138],[114.12973944454214,30.46083991210931],[114.12973947439559,30.4608398830839],[114.12973950430361,30.460839854100907],[114.12973953426678,30.46083982516067],[114.12973956428452,30.46083979626319],[114.12973959435695,30.460839767408473],[114.1297396244833,30.4608397385964],[114.12973965466446,30.460839709827706],[114.12973968490007,30.460839681101834],[114.12973971518971,30.460839652418773],[114.12973974553346,30.46083962377926],[114.12973977593147,30.460839595182573],[114.12973980638337,30.460839566629723],[114.12973983688916,30.4608395381198],[114.12973986744888,30.460839509653717],[114.12973989806237,30.460839481231133],[114.12973992872953,30.46083945285216],[114.12973995945013,30.460839424516685],[114.1297399902242,30.46083939622522],[114.12974002105238,30.46083936797777],[114.12974005193287,30.460839339773983],[114.12974008286739,30.46083931161438],[114.12974011385488,30.460839283498785],[114.12974014489504,30.460839255427377],[114.12974017598887,30.460839227400314],[114.12974020713499,30.460839199417492],[114.12974023833446,30.460839171478966],[114.12974026958672,30.460839143585194],[114.12974030089127,30.460839115735656],[114.12974033224835,30.460839087930925],[114.12974036365799,30.460839060170827],[114.12974039512015,30.460839032455368],[114.12974042663507,30.46083900478506],[114.12974045820164,30.460838977159444],[114.12974048982052,30.460838949578918],[114.12974052149148,30.460838922043603],[114.1297405532143,30.46083889455304],[114.12974058498955,30.46083886710796],[114.12974061681597,30.46083883970826],[114.12974064869458,30.460838812353654],[114.12974068062474,30.460838785044647],[114.12974071260616,30.460838757781246],[114.12974074463935,30.46083873056328],[114.1297407767237,30.460838703391087],[114.12974080885945,30.46083867626456],[114.12974084104661,30.460838649183746],[114.12974087328485,30.460838622148994],[114.12974090557401,30.460838595160133],[114.12974093791401,30.460838568217273],[114.12974097030511,30.4608385413207],[114.12974100274688,30.46083851446996],[114.12974103523953,30.460838487665733],[114.12974106778245,30.460838460907624],[114.1297411003762,30.460838434195974],[114.12974113301986,30.460838407530893],[114.12974116571438,30.460838380912445],[114.12974119845916,30.46083835434051],[114.12974123125375,30.46083832781492],[114.12974126409861,30.460838301336356],[114.12974129699319,30.46083827490448],[114.12974132993791,30.46083824851963],[114.12974136293302,30.460838222181806],[114.12974139597692,30.46083819589084],[114.12974142907109,30.460838169647076],[114.12974146221428,30.460838143450506],[114.1297414954073,30.460838117301023],[114.12974152864933,30.46083809119908],[114.12974156194095,30.460838065144387],[114.12974159528159,30.460838039137183],[114.12974162867171,30.46083801317763],[114.12974166211028,30.460837987265446],[114.12974169559821,30.460837961400973],[114.12974172913461,30.46083793558444],[114.12974176271968,30.460837909815503],[114.12974179635388,30.46083788409445],[114.12974183017747,30.460837858313894],[114.12974186405006,30.460837832581735],[114.12974189797144,30.46083780689825],[114.12974193194164,30.460837781263045],[114.12974196596016,30.46083775567652],[114.12974200002715,30.4608377301389],[114.1297420341427,30.460837704649958],[114.12974206830681,30.46083767920969],[114.12974210251882,30.46083765381833],[114.12974213677917,30.460837628476046],[114.12974217108763,30.460837603182895],[114.1297422054433,30.46083757793871],[114.12974223984742,30.460837552743712],[114.12974227429932,30.46083752759807],[114.12974230879898,30.460837502501565],[114.12974234334588,30.460837477454763],[114.12974237794029,30.460837452457156],[114.12974241258215,30.460837427509244],[114.12974244727121,30.460837402610863],[114.12974248200759,30.46083737776185],[114.12974251679118,30.46083735296271],[114.12974255162163,30.460837328213604],[114.12974258649906,30.46083730351421],[114.1297426214229,30.460837278864684],[114.12974265639365,30.460837254265208],[114.12974269141121,30.460837229715832],[114.12974272647509,30.460837205216492],[114.12974276158572,30.460837180767374],[114.12974279674265,30.46083715636853],[114.12974283194544,30.460837132019893],[114.1297428671945,30.460837107722163],[114.12974290248987,30.460837083474356],[114.12974293783107,30.460837059277164],[114.1297429732178,30.460837035130705],[114.1297430086505,30.460837011034858],[114.12974304412889,30.460836986989623],[114.12974307965314,30.460836962995113],[114.12974311522257,30.460836939051614],[114.12974315083704,30.46083691515902],[114.12974318649712,30.460836891317378],[114.12974322220249,30.46083686752686],[114.12974325795291,30.460836843787302],[114.12974329374815,30.460836820099036],[114.12974332958821,30.46083679646207],[114.12974336547343,30.460836772876398],[114.12974340140303,30.460836749342025],[114.12974343737723,30.460836725859007],[114.1297434733959,30.460836702427624],[114.12974350945919,30.460836679047713],[114.12974354556673,30.460836655719433],[114.12974358171827,30.460836632443026],[114.12974361791409,30.460836609218145],[114.12974365415394,30.46083658604524],[114.12974369043783,30.460836562924147],[114.12974372676494,30.46083653985492],[114.12974376313619,30.460836516837844],[114.12974379955125,30.46083649387269],[114.12974383600931,30.46083647095963],[114.12974387251109,30.46083644809918],[114.12974390905619,30.460836425290427],[114.1297439456444,30.46083640253416],[114.12974398227597,30.460836379830443],[114.12974401895045,30.460836357179108],[114.12974405566725,30.460836334580154],[114.12974409242761,30.46083631203392],[114.12974412923009,30.460836289540236],[114.12974416607557,30.460836267099154],[114.12974420296361,30.460836244710915],[114.12974423989408,30.46083622237539],[114.12974427686679,30.46083620009298],[114.12974431388136,30.460836177863303],[114.12974435093882,30.460836155686337],[114.12974438803758,30.460836133563006],[114.12974442517869,30.460836111492398],[114.1297444623613,30.460836089474903],[114.12974449958558,30.46083606751082],[114.1297445368516,30.460836045599965],[114.12974457415919,30.460836023742743],[114.12974461150816,30.460836001938468],[114.12974464889825,30.46083598018794],[114.12974468632983,30.46083595849071],[114.12974472380219,30.460835936847275],[114.12974476131606,30.460835915257412],[114.12974479887022,30.460835893721246],[114.12974483646546,30.46083587223899],[114.1297448741012,30.460835850810366],[114.12974491177792,30.460835829435553],[114.12974494949492,30.460835808114876],[114.12974498725211,30.460835786848175],[114.12974502504957,30.46083576563545],[114.12974506288778,30.460835744476864],[114.12974510076567,30.460835723372433],[114.1297451386832,30.460835702322193],[114.12974517664112,30.46083568132656],[114.12974521463842,30.460835660384785],[114.12974525267579,30.460835639497663],[114.12974529075241,30.46083561866475],[114.12974532886874,30.46083559788666],[114.12974536702389,30.46083557716289],[114.12974540521888,30.46083555649383],[114.12974544345278,30.46083553587977],[114.12974548172573,30.46083551531991],[114.12974552003749,30.460835494815225],[114.12974555838827,30.46083547436525],[114.12974559677788,30.460835453970162],[114.12974563520561,30.46083543363007],[114.12974567367226,30.460835413344867],[114.1297457121775,30.460835393115058],[114.12974575072053,30.460835372939968],[114.1297457893018,30.460835352820215],[114.12974582792164,30.46083533275558],[114.12974586657919,30.46083531274645],[114.12974590527472,30.460835292792723],[114.12974594400761,30.46083527289411],[114.12974598277896,30.460835253051005],[114.1297460215873,30.46083523326381],[114.12974606043319,30.46083521353156],[114.12974609931645,30.46083519385533],[114.12974613823725,30.46083517423473],[114.12974617719517,30.4608351546697],[114.1297462161895,30.460835135160465],[114.12974625522142,30.460835115706914],[114.12974629429009,30.4608350963095],[114.12974633339519,30.460835076968],[114.12974637253728,30.46083505768229],[114.12974981756975,30.460834461321785],[114.12975222151356,30.460834512012003],[114.12975434025654,30.460834914699298],[114.12976090056145,30.46083708761061],[114.12982096010788,30.46086824150853],[114.1298175011319,30.46086928551613],[114.12981050537311,30.460871066987227],[114.12979936369597,30.460874063653247],[114.12979263196452,30.460875893396935],[114.12979259448348,30.460875904666846],[114.12979255697874,30.460875915876738],[114.12979251945042,30.460875927026613],[114.12979248189846,30.46087593811624],[114.12979244432323,30.460875949145738],[114.12979240672453,30.460875960115096],[114.12979236910267,30.460875971023988],[114.12979233145778,30.460875981872633],[114.12979229378993,30.460875992661375],[114.12979225609931,30.460876003389412],[114.12979221838631,30.46087601405692],[114.12979218065027,30.46087602466435],[114.129792142892,30.460876035211367],[114.12979210511146,30.46087604569751],[114.12979206730893,30.460876056123237],[114.12979202948424,30.4608760664886],[114.12979199163789,30.460876076793205],[114.12979195376964,30.46087608703705],[114.12979191587971,30.460876097220535],[114.12979187796786,30.460876107343093],[114.1297918400349,30.460876117405228],[114.12979180208131,30.460876127406152],[114.12979176410579,30.4608761373466],[114.12979172610963,30.460876147226003],[114.1297916880928,30.460876157044474],[114.12979165005488,30.460876166802358],[114.12979161199628,30.460876176498967],[114.12979157391773,30.460876186134705],[114.12979153581874,30.460876195709453],[114.12979149769932,30.460876205223098],[114.12979145955968,30.4608762146757],[114.12979142140031,30.460876224067487],[114.12979138322119,30.460876233397716],[114.12979134502208,30.460876242666952],[114.12979130680324,30.46087625187509],[114.12979126856567,30.460876261021607],[114.12979123030844,30.46087627010726],[114.12979119203195,30.460876279131575],[114.12979115373625,30.46087628809456],[114.1297911154223,30.460876296995988],[114.12979107708894,30.46087630583626],[114.12979103873741,30.460876314614968],[114.12979100036694,30.46087632333212],[114.12979096197827,30.460876331988217],[114.12979092357159,30.46087634058237],[114.12979088514618,30.460876349115182],[114.1297908467035,30.46087635758633],[114.12979080824222,30.460876365996082],[114.12979076976369,30.460876374343997],[114.12979073126733,30.46087638263046],[114.12979069275352,30.460876390855084],[114.12979065422287,30.460876399018087],[114.12979061567431,30.46087640711925],[114.12979057710872,30.460876415158793],[114.12979053852645,30.460876423136554],[114.12979049992747,30.46087643105247],[114.12979046131183,30.460876438906364],[114.12979042267939,30.460876446698474],[114.12979038403058,30.46087645442891],[114.12979034536556,30.460876462097218],[114.12979030668455,30.460876469703678],[114.12979026798719,30.46087647724801],[114.12979022927402,30.4608764847305],[114.12979019054524,30.46087649215097],[114.12979015180063,30.460876499509308],[114.12979011304073,30.460876506805462],[114.12979007426503,30.4608765140396],[114.12979003547437,30.46087652121161],[114.1297899966687,30.46087652832154],[114.1297899578484,30.4608765353694],[114.12978991901268,30.460876542354843],[114.12978988016253,30.46087654927827],[114.12978984129761,30.460876556139226],[114.12978980241817,30.460876562937994],[114.12978976352488,30.46087656967457],[114.1297897246173,30.46087657634862],[114.12978968569543,30.46087658296054],[114.12978964675968,30.460876589510043],[114.12978960781008,30.46087659599724],[114.12978956884733,30.460876602422026],[114.1297895298704,30.460876608784165],[114.12978949088074,30.460876615083944],[114.12978945187746,30.460876621321365],[114.12978941286111,30.460876627496027],[114.12978937383161,30.460876633608557],[114.12978933478963,30.460876639658554],[114.12978929974295,30.460876645027668],[114.12978926468301,30.460876650333397],[114.12978922961057,30.46087665557597],[114.12978919452598,30.460876660755215],[114.12978915942948,30.460876665871133],[114.12978912432025,30.460876670923497],[114.12978908919935,30.460876675912814],[114.12978905406663,30.460876680838524],[114.12978901892234,30.46087668570079],[114.12978898376646,30.46087669049967],[114.12978894859923,30.460876695234937],[114.12978891342065,30.460876699906763],[114.12978887823151,30.4608767045152],[114.12978884303136,30.460876709059974],[114.12978880782032,30.460876713541246],[114.12978877259859,30.460876717958843],[114.12978873736677,30.46087672231289],[114.12978870212451,30.460876726603264],[114.12978866687213,30.460876730830133],[114.12978863160942,30.460876734993338],[114.12978859633718,30.460876739092754],[114.1297885610554,30.460876743128615],[114.1297885257642,30.46087674710069],[114.12978849046311,30.46087675100932],[114.12978845515353,30.460876754853825],[114.1297884198344,30.4608767586346],[114.12978838450641,30.460876762351702],[114.12978834916967,30.46087676600508],[114.12978831382443,30.460876769594435],[114.12978827847101,30.46087677312041],[114.12978824310873,30.460876776582257],[114.12978820773874,30.460876779980143],[114.12978817236103,30.46087678331424],[114.12978813697501,30.460876786584727],[114.12978810158127,30.460876789790916],[114.12978806618005,30.46087679293337],[114.12978803077156,30.46087679601181],[114.12978799535614,30.460876799026465],[114.129787959933,30.46087680197716],[114.12978792450339,30.4608768048639],[114.1297878890673,30.460876807686564],[114.12978785362394,30.460876810445217],[114.12978781817445,30.460876813140075],[114.12978778271884,30.46087681577081],[114.12978774725674,30.460876818337578],[114.12978771178896,30.46087682084039],[114.12978767631517,30.46087682327874],[114.1297876408357,30.460876825653457],[114.1297876053509,30.46087682796388],[114.12978756986041,30.460876830210342],[114.12978753436492,30.460876832392962],[114.12978749886432,30.460876834510994],[114.12978746335861,30.460876836565344],[114.12978742784836,30.460876838555286],[114.12978739233343,30.460876840481205],[114.12978735681386,30.46087684234317],[114.12978732129055,30.460876844140657],[114.12978728576235,30.460876845874353],[114.1297872502305,30.460876847543748],[114.12978721469503,30.460876849148953],[114.12978717915546,30.460876850690084],[114.12978714361304,30.460876852167026],[114.12978710806676,30.460876853579666],[114.12978707251695,30.46087685492817],[114.12978703696456,30.460876856212714],[114.12978700140896,30.460876857432783],[114.12978696585073,30.46087685858849],[114.1297869302897,30.460876859680464],[114.12978689472659,30.460876860707906],[114.12978685916099,30.46087686167121],[114.12978682359321,30.46087686257039],[114.12978678802351,30.460876863405257],[114.12978675245176,30.460876864175933],[114.12978671687853,30.460876864882483],[114.12978668130383,30.46087686552455],[114.12978664572775,30.460876866102655],[114.12978661015008,30.46087686661628],[114.12978657457182,30.460876867065778],[114.12978653899222,30.460876867451077],[114.12978650341203,30.46087686777208],[114.12978646783127,30.460876868028766],[114.12978643224972,30.460876868221323],[114.12978639666827,30.460876868349516],[114.12978636108646,30.460876868413628],[114.12978632550501,30.460876868413372],[114.12978628992353,30.460876868348816],[114.12978625434218,30.46087686822024],[114.1297862187615,30.46087686802724],[114.12978618318125,30.460876867770214],[114.12978614760226,30.46087686744854],[114.1297861120236,30.4608768670629],[114.12978607644618,30.460876866613123],[114.12978604086999,30.460876866098697],[114.1297860052954,30.46087686552025],[114.1297859697224,30.460876864877665],[114.12978593415062,30.460876864170597],[114.1297858985809,30.460876863399566],[114.12978586301378,30.460876862564227],[114.12978582744789,30.460876861664687],[114.12978579188496,30.460876860700843],[114.12978575632417,30.46087685967292],[114.12978572076631,30.46087685858057],[114.12978568521085,30.460876857424477],[114.12978564965854,30.460876856203736],[114.12978561410942,30.460876854918965],[114.12978557856312,30.460876853569776],[114.12978097293913,30.46087611735825],[114.1300935983725,30.46104443000447],[114.13009199872872,30.46104334989119],[114.130090791552,30.461042350214225],[114.13008921072786,30.461040981833918],[114.13008820653799,30.461039436169777],[114.13008735704764,30.461037929848207],[114.13008647082899,30.461035547741947],[114.13008592970188,30.461032379083246],[114.13008728405228,30.461027226146687],[114.13008886666402,30.46102387032586],[114.13009347073576,30.461014625663626],[114.13011271804483,30.46102349111015],[114.13012760768065,30.461030570401856],[114.13015916719633,30.461045895195372],[114.13018302670666,30.46105712354523],[114.13024107267701,30.46108509338401],[114.13024120877141,30.46108524890782],[114.1302412605094,30.461085359787887],[114.13024127308707,30.461085493710335],[114.13024125252475,30.461085677628468],[114.13022792770946,30.461107473223144],[114.13031431801001,30.46114830931335],[114.13034715161534,30.46116368102704],[114.13035959409316,30.46114331674733],[114.13036020068445,30.4611428650941],[114.13036047855408,30.461142763543243],[114.1303608793548,30.461142688448973],[114.13036167174162,30.461142769382395],[114.13036864345355,30.461146033323974],[114.13045399634161,30.4611861869654],[114.13056643045437,30.46123894413378],[114.13059972652555,30.461254650283248],[114.13061898402532,30.46122158770834]]],[[[114.13045166013256,30.461508860656757],[114.13045113236419,30.46150861167757],[114.13038902579527,30.46147940019146],[114.13027236933345,30.461424469991137],[114.1302449042618,30.461411600882368],[114.13018342073529,30.461382257284672],[114.13011697073304,30.461350836168783],[114.13004360269801,30.46131650873754],[114.13000090329518,30.46129648487721],[114.12996200134592,30.461278417298047],[114.12993616252682,30.46126615249132],[114.12990291072697,30.46125045788717],[114.12988256235427,30.46124067768216],[114.12987179255991,30.461235567370608],[114.12986722261299,30.461233910100944],[114.12986195939581,30.461232654867565],[114.12985731372267,30.461231510175065],[114.12983345211502,30.461224729028125],[114.1298274737889,30.46122221362121],[114.12972906729911,30.46118220384203],[114.12968234577546,30.461163346656495],[114.12965518684442,30.461152377241042],[114.12962904070915,30.46114174420687],[114.12959327721632,30.461127199295024],[114.12956497393517,30.461115876272295],[114.12954623041804,30.461108177464922],[114.12951793337623,30.46109663971318],[114.12950043861774,30.4610896689159],[114.12948335261126,30.46108282129353],[114.12946834944965,30.461076664409667],[114.12946210779079,30.461074079083684],[114.12945622023722,30.46107146514424],[114.12943923937732,30.461063403164466],[114.12941961991667,30.461054257135935],[114.12934096022525,30.46101713680265],[114.1292531611024,30.460975781088365],[114.12915358364316,30.460928821850516],[114.12914446705692,30.4609245336199],[114.12911195461106,30.460909240500985],[114.12906661686613,30.460887931138817],[114.12900696519473,30.460859674520563],[114.12899472493436,30.46085400383098],[114.12897711937539,30.460845605200472],[114.12895897965275,30.46083701325433],[114.1289346889824,30.460825927957725],[114.12891435148165,30.460816098825497],[114.12888630584379,30.460803373586902],[114.12887836259212,30.46079988818031],[114.12886817096455,30.4607959182663],[114.12883288424162,30.46078344055786],[114.12880683904324,30.460775348266598],[114.12878095499761,30.46076926421222],[114.12876268288106,30.460766109565174],[114.12874165770062,30.46076308767697],[114.12872509369107,30.460761687541552],[114.12870763931349,30.460760650459722],[114.12868966534714,30.460759518850644],[114.12868556332796,30.460758775820878],[114.12868139785537,30.460757852444367],[114.12867930532934,30.460757079594252],[114.1286743946421,30.460755258055634],[114.12867217333921,30.460753659737904],[114.12866915506972,30.460748762815165],[114.12866651324336,30.460740080827907],[114.1286615349295,30.460740621283133],[114.12865159543583,30.460742072415314],[114.1286445447878,30.460743259994725],[114.12863472942703,30.460745282694166],[114.12862751842948,30.460746573884617],[114.12862310380396,30.460747364359552],[114.1286129285285,30.46074959393931],[114.1286036936273,30.460751644932678],[114.12859305335921,30.460754496127752],[114.12858272295519,30.460757439015904],[114.12858684681783,30.460756974895318],[114.1285896592975,30.460757211799937],[114.12859336695772,30.46075893258156],[114.12859483318482,30.46076096842862],[114.12859511158445,30.460762728564493],[114.12859446761085,30.46076500622275],[114.12859354876066,30.4607668126579],[114.12859116968379,30.460769196850375],[114.12858763843047,30.46077126133305],[114.12857545492906,30.460774983201105],[114.12856651479777,30.46077799213247],[114.12854889649874,30.46078481146621],[114.12853447052267,30.460790639959814],[114.12852557335621,30.460794471642462],[114.12851594301046,30.46079905130281],[114.12849904731934,30.46080789334833],[114.12848921767336,30.46081381903344],[114.12847995375431,30.460820365739195],[114.12845718211763,30.46083835215128],[114.12845199062264,30.460842482142652],[114.12843863190128,30.460856657982863],[114.12842208172918,30.460877284195742],[114.12840342322,30.460900828623902],[114.12838529881427,30.46092675780746],[114.1280351192241,30.4614916579163],[114.12772665457429,30.461977474726904],[114.12773666316124,30.46198267919212],[114.12838019747124,30.46096860236104],[114.12839862491309,30.460940485061798],[114.12841620397128,30.460914721940537],[114.12843438168466,30.460891047924747],[114.12845081874062,30.46087048759082],[114.12846385693705,30.460856156383116],[114.12846784607905,30.460852302594482],[114.12849139753372,30.46083382588418],[114.12850049625027,30.460827287615476],[114.12851003248754,30.460821443548152],[114.1285190183587,30.460816503231946],[114.128527396434,30.460812748170223],[114.1285377280812,30.460809171635077],[114.12854770191542,30.460805844107],[114.12856078586411,30.46080192595596],[114.12857238348518,30.460798490726418],[114.12858337520525,30.460794904254787],[114.12859945021059,30.460790470513647],[114.12861472608745,30.46078759112877],[114.12863298746211,30.460785640678257],[114.12864145553547,30.460784861359542],[114.12867263363336,30.46078576380548],[114.12869005218914,30.4607870865643],[114.12871293776365,30.460789479520425],[114.12873005711924,30.46079163128663],[114.1287544930627,30.46079647154317],[114.12877633262853,30.46080491582557],[114.12878327578939,30.46080767851991],[114.1289198979808,30.460870976248305],[114.12906165250779,30.460938179307167],[114.12938691893761,30.46109103651788],[114.12958281068029,30.461181831471286],[114.12970186691408,30.461237068649936],[114.12977138918183,30.461269340469094],[114.13000395955547,30.461377765191333],[114.1301615044232,30.46145164530947],[114.13037598615774,30.46155321754522],[114.13041321793216,30.461570825330455],[114.13041506318048,30.461571692773276],[114.13045166013256,30.461508860656757]]]]}},{"type":"Feature","id":"12","properties":{"type":4},"geometry":{"type":"MultiPolygon","coordinates":[[[[114.12765978321049,30.45988651522178],[114.12764383468487,30.459882807400575],[114.12765100334624,30.45988657107926],[114.12764139830975,30.459889506914333],[114.12761387234207,30.459876742501372],[114.1276124028338,30.459879128935576],[114.12764749557435,30.459895403858667],[114.12766126254193,30.45989195806849],[114.12766940339483,30.459896232150424],[114.12765978321049,30.45988651522178]]],[[[114.12763427339303,30.45991154790557],[114.12763291194328,30.459913757116947],[114.12759912524707,30.45989808805539],[114.12759765571056,30.45990047538993],[114.1276314403325,30.459916144409654],[114.1276300810117,30.45991835186179],[114.12765300104127,30.45992460771415],[114.12763427339303,30.45991154790557]]],[[[114.12761755190054,30.459938684458045],[114.12761611977218,30.459940858795086],[114.12758296657559,30.459925484570334],[114.12758149703767,30.45992787190393],[114.12761457127884,30.4599432101801],[114.12761311527514,30.459945419192206],[114.12763583774534,30.459951538273963],[114.12761755190054,30.459938684458045]]],[[[114.12760400423902,30.45996289423156],[114.12756944059383,30.45994678740718],[114.12756796389054,30.459949170979908],[114.12759459934937,30.45996158282404],[114.12759725797072,30.459971110870285],[114.12758979292224,30.459967603401395],[114.12760189361006,30.459977350333364],[114.12761687330165,30.459980327738513],[114.12760864362862,30.459976460395396],[114.12760400423902,30.45996289423156]]],[[[114.12799440218569,30.460048124375334],[114.12800243788405,30.460051976656732],[114.12799057087545,30.460042416186628],[114.12797537561978,30.460039000558577],[114.12798337851463,30.460042837719275],[114.1279726697087,30.460045822498866],[114.12794553803901,30.460032980209355],[114.12794404498791,30.460035355341343],[114.12797953758076,30.46005215693364],[114.12799440218569,30.460048124375334]]],[[[114.12796557032368,30.460067280495235],[114.12796418208411,30.46006958928911],[114.1279323869172,30.460054371474623],[114.12793088153516,30.460056741834425],[114.12796273821913,30.46007198801494],[114.12796131066906,30.460074361827907],[114.12798568849392,30.460081426697982],[114.12796557032368,30.460067280495235]]],[[[114.12794871296117,30.46009438283973],[114.12794741459626,30.46009663850804],[114.12791534084874,30.460081565421028],[114.12791385391927,30.460083944289185],[114.1279460209531,30.460099060847472],[114.1279447280439,30.460101307610305],[114.12796929043392,30.460108457589786],[114.12794871296117,30.46009438283973]]],[[[114.12793709442113,30.460120036352464],[114.12790195505336,30.460103411760084],[114.12790046199727,30.46010578688927],[114.12792761745402,30.460118635147563],[114.12793127303058,30.460128785938068],[114.12792429530099,30.460125601002048],[114.12793536229401,30.460134449981247],[114.12795051437212,30.460137567038498],[114.12794231951005,30.460133826836465],[114.12793709442113,30.460120036352464]]],[[[114.12835891047108,30.460191093536803],[114.1283690188825,30.460192677511287],[114.12835412543915,30.460187121572954],[114.12834021308942,30.460188162473116],[114.12834685654148,30.460189203212558],[114.1283384784417,30.460195350486952],[114.12830723173941,30.46019030660117],[114.12830665829725,30.460192965509346],[114.12834646485058,30.460199392851074],[114.12835891047108,30.460191093536803]]],[[[114.12870975662688,30.460212982914015],[114.12870665669686,30.460211400130223],[114.1286890621461,30.46023761963853],[114.12867780266502,30.4602396197635],[114.12868292426433,30.460232229061138],[114.12867230300633,30.460241891336665],[114.12866889768308,30.460253871059727],[114.12867427962401,30.46024567940838],[114.12868956421664,30.460242122565393],[114.12870975662688,30.460212982914015]]],[[[114.12873258742432,30.460223963610332],[114.12872855443425,30.46022230969746],[114.12871141229377,30.46025121432643],[114.12870839683895,30.460249798406643],[114.12870077475303,30.460270240679073],[114.1287167478244,30.46025309602353],[114.12871452320131,30.46025224437172],[114.12873258742432,30.460223963610332]]],[[[114.12837647935412,30.46026156409562],[114.1283752151951,30.46026329099487],[114.12834192553392,30.460247423299087],[114.12834042431034,30.460249794675402],[114.12837354178777,30.46026558115513],[114.12837196579537,30.4602677380636],[114.1283946904409,30.46027411807514],[114.12837647935412,30.46026156409562]]],[[[114.12876241641918,30.460237315744084],[114.12875882836029,30.460235903068593],[114.1287414802644,30.46026382191426],[114.12873876272113,30.460262771317108],[114.12873078746144,30.460283317092614],[114.12874715699776,30.460266187107642],[114.12874496976379,30.46026515854007],[114.12876241641918,30.460237315744084]]],[[[114.12880287063375,30.460269826219278],[114.12880057854322,30.46027534189185],[114.12879499567953,30.460267155271225],[114.12880594534293,30.460241237531278],[114.12880302093224,30.46024030792805],[114.12878906971548,30.460273325397395],[114.12879641747973,30.4602853492421],[114.12879292942804,30.460293741602786],[114.1288017286223,30.46028190815778],[114.12880287063375,30.460269826219278]]],[[[114.12834895482324,30.46028357000437],[114.12834761758594,30.460285734666073],[114.12831461477353,30.46027032493471],[114.12831313610232,30.460272706709155],[114.1283461440566,30.46028811835546],[114.12834469014169,30.46029047088728],[114.12836871610331,30.460297121998284],[114.12834895482324,30.46028357000437]]],[[[114.12833628018815,30.46030807470138],[114.12830123480336,30.4602914490679],[114.12829973767604,30.46029382233258],[114.12832709672114,30.460306803443775],[114.12833006918703,30.4603164363764],[114.12832213338147,30.46031268824945],[114.12833389186757,30.46032250966981],[114.12835043401269,30.460326052779433],[114.12834147430807,30.460321822050688],[114.12833628018815,30.46030807470138]]],[[[114.12874925720942,30.46058753420879],[114.12875633276082,30.460590743526613],[114.12874529087212,30.460582287850315],[114.12873214771808,30.460579773021713],[114.12873971340865,30.460583205588684],[114.12874193185147,30.46059058991506],[114.1287647929814,30.460601281663973],[114.12876627265992,30.46059889988208],[114.12874884830025,30.460590749575427],[114.12874925720942,30.46058753420879]]],[[[114.12871821605825,30.4606105640714],[114.12871959460246,30.460608229769374],[114.12869575525808,30.460601600988607],[114.12871534205641,30.460615428660677],[114.12871679376168,30.460612973222794],[114.12874984384793,30.460628424004636],[114.12875132459295,30.46062604134405],[114.12871821605825,30.4606105640714]]],[[[114.1282713090051,30.460612952415406],[114.12826086657832,30.460601581970952],[114.12826129431166,30.46059513218137],[114.1282566641438,30.46060741971045],[114.12825991855485,30.46061977475574],[114.12826022942248,30.460613803298962],[114.12826630595534,30.46061596778145],[114.12826866071323,30.46061965292968],[114.1282682144802,30.460646263638203],[114.12827089724746,30.460646906719386],[114.1282713090051,30.460612952415406]]],[[[114.12870076159,30.460637924067537],[114.12870236859342,30.46063552332987],[114.12867844351734,30.4606289829275],[114.12869772909524,30.46064245244094],[114.1286991937367,30.460640265193003],[114.12873219886704,30.460655761957565],[114.1287336826763,30.460653381166896],[114.12870076159,30.460637924067537]]],[[[114.12831797511244,30.46063243172318],[114.12832119644226,30.460632779482374],[114.12832893290955,30.46061305238447],[114.12831239255001,30.460630040454017],[114.12831582544989,30.460630970013842],[114.1282984920438,30.460658369257313],[114.12830113451777,30.46065967901653],[114.12831797511244,30.46063243172318]]],[[[114.12834864625601,30.460645501797423],[114.12835063030111,30.460646373655234],[114.12835899930107,30.460625234647328],[114.12834321867702,30.46064257256931],[114.1283455814283,30.46064374741235],[114.12832807040849,30.460672459045476],[114.12833113221508,30.460673961690624],[114.12834864625601,30.460645501797423]]],[[[114.12837912681817,30.460659686372537],[114.12837676038178,30.460664922481147],[114.12838729349056,30.460655438886963],[114.12839074447052,30.460642612219186],[114.12838590538631,30.46064925740684],[114.12837091827653,30.460652936711554],[114.1283527536239,30.460683177988436],[114.12835493179958,30.460684339907434],[114.12837001606486,30.46066033266817],[114.12837912681817,30.460659686372537]]],[[[114.12872896548119,30.460704124997644],[114.12876084568157,30.46070725039113],[114.12876119762366,30.46070456153281],[114.12871989417552,30.460700515380775],[114.12870691650812,30.46070992221255],[114.12869839289986,30.46070925589473],[114.12871279406163,30.46071423201666],[114.12872941048305,30.460711679332316],[114.12872029803995,30.460710967204506],[114.12872896548119,30.460704124997644]]],[[[114.12913444135532,30.460768798505974],[114.12914149179586,30.460772188498577],[114.12913097470313,30.460763556443407],[114.12911396829095,30.46075895585527],[114.12912571153127,30.460764601784295],[114.12912590284351,30.460775678762786],[114.12916231806324,30.460793185492257],[114.12916383051939,30.46079081881016],[114.12913598426056,30.46077743143822],[114.12913444135532,30.460768798505974]]],[[[114.12911697977484,30.460797693086274],[114.12911843007527,30.460795462210044],[114.12909439426096,30.46078881163049],[114.12911400178062,30.460802278592816],[114.1291154488873,30.46080005035548],[114.1291485118678,30.4608152951975],[114.12914997511503,30.460812905827964],[114.12911697977484,30.460797693086274]]],[[[114.12909986376616,30.460824861167204],[114.12910122728519,30.460822684388322],[114.12907774100891,30.460816072456357],[114.12909695474436,30.460829504056413],[114.12909837487132,30.460827239167585],[114.12913135974942,30.460842633030847],[114.12913283740411,30.460840248476778],[114.12909986376616,30.460824861167204]]],[[[114.1290832891964,30.460844033872448],[114.12906817778146,30.46084821163846],[114.1290595466183,30.460844157698915],[114.12907309910834,30.460854163685678],[114.1290881839199,30.460857607867585],[114.12907976595324,30.46085365405626],[114.12909012325989,30.460850345634984],[114.12911828965193,30.460863660437624],[114.12911978267327,30.460861283425245],[114.1290832891964,30.460844033872448]]],[[[114.1295170729681,30.46094868857821],[114.12952471659668,30.460952292136778],[114.12951319270734,30.46094273963868],[114.12949712644783,30.460939288545823],[114.12950565870332,30.460943309573725],[114.12950977301703,30.460956744341534],[114.1295459846389,30.460973346840195],[114.12954744170554,30.460970954609365],[114.12951970910399,30.46095823940797],[114.1295170729681,30.46094868857821]]],[[[114.12950069463872,30.460978320737603],[114.1295022112425,30.46097574033008],[114.12947838746287,30.460969275015792],[114.12949799054365,30.46098292199377],[114.12949927753735,30.460980731850242],[114.12953219914202,30.460996356573943],[114.12953369721849,30.4609939832506],[114.12950069463872,30.460978320737603]]],[[[114.12948462553341,30.461005586056444],[114.12948598100314,30.461003470426377],[114.12946220363271,30.460996518955422],[114.12948148441804,30.46101049291415],[114.12948311211464,30.461007950934665],[114.12951612229881,30.46102356762681],[114.12951761523743,30.461021192391577],[114.12948462553341,30.461005586056444]]],[[[114.1294685281634,30.461024581467186],[114.12945465384469,30.461029561357567],[114.12944516793,30.461025143123695],[114.12945798782363,30.461035146972872],[114.12947318283328,30.461038191819814],[114.12946503873893,30.46103439817314],[114.12947539290786,30.461030872121057],[114.12950386704058,30.461044204700624],[114.12950534876923,30.461041822013147],[114.1294685281634,30.461024581467186]]]]}},{"type":"Feature","id":"13","properties":{"type":3},"geometry":{"type":"MultiPolygon","coordinates":[[[[114.1266944684298,30.45974113955593],[114.12672905702632,30.45975739984173],[114.12674527261686,30.459764919502927],[114.12676616927213,30.459774842276527],[114.12681593344244,30.459798270346727],[114.12686208851598,30.459820014857755],[114.12692777688568,30.459851058867],[114.12699795709268,30.459884486205194],[114.12701184103294,30.45989105531921],[114.12702132622897,30.459895863598756],[114.12716906978987,30.45997472622542],[114.12725782616269,30.460023057967557],[114.12731527526354,30.460054300237914],[114.12731760904978,30.460055567526968],[114.12731813428435,30.460053516543667],[114.12731900249183,30.46004906066548],[114.12731982237837,30.460044125665828],[114.12732042243266,30.460039600961448],[114.12732094425235,30.46003526403447],[114.12732096403349,30.460033523456232],[114.12732052050677,30.460030682767634],[114.12731943525426,30.46003001809118],[114.12731561342733,30.46002781724937],[114.12730266390733,30.4600207953853],[114.1272944154204,30.460016316299836],[114.12723696629992,30.459985073980643],[114.12713720190709,30.459930747807572],[114.12705918757291,30.459888076994318],[114.12704970239025,30.459883268730508],[114.12703581846966,30.45987669963919],[114.12696563836118,30.459843272416002],[114.12689995008387,30.459812228514114],[114.12685379507562,30.459790484078905],[114.12680403097558,30.45976705609049],[114.12678313434951,30.459757133351058],[114.126766918782,30.45974961371671],[114.12671858919056,30.45972689376226],[114.12670588722376,30.459720823910242],[114.1266944684298,30.45974113955593]]],[[[114.12675814499713,30.459627849957624],[114.12688251033282,30.4596865974618],[114.12707051661417,30.459775086771863],[114.12719733974058,30.45983468642013],[114.12734472023581,30.45990416351866],[114.12742926333294,30.459943885087256],[114.12750732255947,30.45998070502695],[114.1275779458936,30.460013852051457],[114.12762297926939,30.46003518344493],[114.12768145827465,30.46006255464501],[114.12769094188808,30.460066929215547],[114.12777515220014,30.460106613454087],[114.12781781683972,30.460126637479288],[114.12786681803642,30.46014968461648],[114.12793397862626,30.460181425380462],[114.12796362762155,30.460195355841083],[114.1280563612488,30.460239138512893],[114.1280974603508,30.460258332389202],[114.12812174927116,30.460269841792726],[114.1281473346031,30.460281966551854],[114.12817332494056,30.460294337560583],[114.1281918699316,30.460303041631594],[114.12821516367895,30.460314187259602],[114.12822254996298,30.460317789390476],[114.12822908233838,30.460320390201996],[114.12823183884626,30.460320998763187],[114.12823522173838,30.460321336764043],[114.12823732737714,30.460321277212827],[114.1282396830356,30.460321077878376],[114.12824192577881,30.46032034723348],[114.12824466576511,30.460319159303868],[114.12824900487033,30.460316311803368],[114.12824903044557,30.460316293513653],[114.12824905597351,30.46031627517361],[114.12824908145355,30.460316256784147],[114.12824910688614,30.46031623834515],[114.12824913227085,30.460316219856164],[114.12824915760778,30.460316201317926],[114.12824918289648,30.460316182730047],[114.12824920813742,30.46031616409314],[114.12824923332965,30.460316145406875],[114.12824925847366,30.46031612667147],[114.128249283569,30.460316107887273],[114.12824930861576,30.460316089054],[114.12824933361371,30.460316070171817],[114.12824935856253,30.460316051240955],[114.12824938346256,30.46031603226147],[114.12824940831311,30.460316013233538],[114.1282494331145,30.460315994157263],[114.12824945786632,30.460315975032252],[114.12824948256899,30.460315955859418],[114.12824950722138,30.46031593663801],[114.12824953182431,30.460315917368842],[114.12824955637696,30.460315898051558],[114.1282495808798,30.460315878686508],[114.1282496053328,30.460315859273578],[114.12824962973511,30.46031583981322],[114.12824965408676,30.460315820304974],[114.12824967838806,30.460315800749473],[114.1282497026385,30.4603157811466],[114.12824972683855,30.46031576149665],[114.12824975098745,30.46031574179904],[114.12824977508514,30.46031572205468],[114.12824979913199,30.460315702263298],[114.12824982312755,30.460315682424888],[114.12824984707149,30.460315662539955],[114.12824987096366,30.46031564260817],[114.12824989480455,30.460315622629924],[114.12824991859347,30.46031560260516],[114.1282499423303,30.460315582534054],[114.12824996601559,30.460315562416774],[114.1282499896487,30.4603155422532],[114.12825001322925,30.46031552204351],[114.12825003675762,30.460315501787985],[114.12825006023343,30.46031548148669],[114.12825008365613,30.460315461139437],[114.12825010702663,30.4603154407467],[114.12825013034448,30.46031542030824],[114.128250153609,30.460315399824633],[114.12825017682074,30.460315379295302],[114.12825019997902,30.460315358720827],[114.12825022308364,30.46031533810148],[114.12825024613504,30.46031531743693],[114.12825026913309,30.4603152967274],[114.12825029207771,30.460315275973173],[114.12825031496807,30.460315255174255],[114.12825033780463,30.460315234330583],[114.12825036058753,30.46031521344267],[114.12825038331597,30.46031519250995],[114.12825040599014,30.460315171533157],[114.12825042860962,30.460315150512187],[114.12825045117509,30.460315129447203],[114.12825047368585,30.460315108338374],[114.12825049614159,30.460315087185197],[114.12825051854318,30.460315065988745],[114.1282505408893,30.460315044748455],[114.12825056318046,30.460315023464435],[114.1282505854165,30.4603150021372],[114.12825060759738,30.460314980766636],[114.12825062972276,30.460314959352914],[114.12825065179265,30.46031493789592],[114.12825067380695,30.460314916395994],[114.12825069576563,30.46031489485308],[114.12825071766825,30.46031487326764],[114.12825073951514,30.46031485163955],[114.12825076130575,30.46031482996859],[114.12825078304067,30.46031480825549],[114.12825080471872,30.46031478649952],[114.12825082634046,30.460314764701813],[114.1282508479057,30.460314742861687],[114.12825086941466,30.460314720979657],[114.12825089086688,30.460314699055836],[114.1282509122619,30.46031467709005],[114.12825093360064,30.46031465508258],[114.12825095488195,30.46031463303361],[114.12825097610583,30.460314610943474],[114.12825099727287,30.46031458881166],[114.12825101838263,30.460314566638505],[114.12825103943457,30.46031454442464],[114.12825106042901,30.460314522169323],[114.12825108136593,30.460314499873355],[114.12825110224462,30.460314477536734],[114.12825112306598,30.460314455159],[114.12825114382905,30.460314432740955],[114.12825116453386,30.460314410282596],[114.12825118518117,30.460314387783644],[114.12825120576936,30.460314365244603],[114.12825122629965,30.460314342665477],[114.12825124658667,30.46031432025074],[114.12825126681595,30.460314297796767],[114.1282512869879,30.46031427530379],[114.12825130710188,30.460314252771923],[114.12825132715821,30.46031423020105],[114.12825134715654,30.46031420759185],[114.12825136709644,30.46031418494365],[114.12825138697845,30.46031416225707],[114.12825140680238,30.460314139532223],[114.12825142656739,30.460314116768995],[114.12825144627443,30.460314093967845],[114.12825146592279,30.460314071128373],[114.12825148551238,30.460314048251092],[114.12825150504305,30.460314025336054],[114.12825152451508,30.46031400238338],[114.12825154392831,30.460313979393067],[114.12825156328276,30.460313956365166],[114.12825158257729,30.460313933300025],[114.12825160181306,30.46031391019776],[114.12825162098945,30.46031388705814],[114.12825164010638,30.46031386388156],[114.12825165916377,30.460313840668423],[114.1282516781613,30.46031381741793],[114.12825169709974,30.460313794131334],[114.12825171597783,30.460313770807726],[114.12825173479607,30.460313747447902],[114.12825175355442,30.460313724051915],[114.12825177225272,30.46031370061954],[114.12825179089103,30.46031367715129],[114.12825180946858,30.46031365364677],[114.12825182798602,30.46031363010654],[114.12825184644298,30.460313606530608],[114.12825186483931,30.460313582919255],[114.12825188317528,30.460313559272084],[114.1282519014501,30.460313535589776],[114.12825191966442,30.46031351187199],[114.1282519378177,30.460313488119244],[114.12825195591026,30.460313464331357],[114.12825197394119,30.46031344050873],[114.12825199191133,30.4603134166512],[114.12825200982003,30.460313392758874],[114.12825202766739,30.460313368832267],[114.12825204545332,30.460313344871146],[114.12825206317798,30.460313320875574],[114.12825208084024,30.46031329684595],[114.12825209844144,30.46031327278227],[114.12825211598069,30.460313248684702],[114.12825213345819,30.46031322455297],[114.12825215087362,30.460313200387805],[114.12825216822664,30.460313176188986],[114.12825218551781,30.460313151956797],[114.12825220274713,30.46031312769129],[114.12825221991372,30.46031310339207],[114.12825223701766,30.460313079060054],[114.1282522540595,30.460313054695394],[114.12825227103863,30.460313030297424],[114.12825228795496,30.460313005866766],[114.12825230480904,30.46031298140353],[114.12825232159955,30.460312956907835],[114.12825233832774,30.46031293237979],[114.12825235499263,30.460312907819343],[114.12825237159478,30.460312883227],[114.12825238813313,30.460312858602368],[114.12825240460852,30.460312833946126],[114.12825242102103,30.46031280925782],[114.12825243736954,30.460312784538143],[114.12825245365518,30.460312759787016],[114.1282524698768,30.460312735004063],[114.1282524860348,30.460312710190003],[114.12825250212911,30.46031268534474],[114.12825251815977,30.460312660468663],[114.12825253412632,30.460312635561493],[114.12825255002898,30.460312610623507],[114.12825256586763,30.460312585655],[114.12825258164227,30.460312560656188],[114.12825259735236,30.4603125356264],[114.12825261299821,30.46031251056659],[114.12825262857994,30.460312485476763],[114.12825264409734,30.4603124603567],[114.12825265954983,30.460312435206788],[114.12825267493797,30.460312410027147],[114.12825269026165,30.460312384817666],[114.1282527055202,30.46031235957879],[114.12825272071397,30.460312334310466],[114.12825273584305,30.460312309012924],[114.128252750907,30.460312283686225],[114.12825276590588,30.46031225833036],[114.12825278083979,30.460312232945732],[114.12825279570822,30.460312207532347],[114.12825281051138,30.460312182090135],[114.12825282524965,30.460312156619327],[114.12825283992197,30.460312131120332],[114.12825285452939,30.460312105593022],[114.12825286907076,30.46031208003764],[114.12825288354688,30.46031205445394],[114.12825289795707,30.46031202884273],[114.12825291230135,30.460312003203384],[114.12825463446963,30.460307266151542],[114.1282546958477,30.460302914042497],[114.12825359360119,30.46029924379911],[114.12825230343141,30.460296734308333],[114.1282500427877,30.46029431885763],[114.12824661664233,30.4602918978516],[114.12820494374954,30.460272114380142],[114.1281922841449,30.460265869404232],[114.12808402777658,30.460214870952296],[114.12766577985062,30.460017600721127],[114.12743693679198,30.459909957721468],[114.12725923541228,30.459826070251896],[114.12719650698922,30.45979676876443],[114.12694653473818,30.459678949412346],[114.12682802881133,30.45962329819676],[114.1267747664414,30.45959827806291],[114.12675814499713,30.459627849957624]]],[[[114.12682661116624,30.459506038981907],[114.12687232754989,30.459527608277465],[114.12699863219632,30.459587234089707],[114.12704767575964,30.459609865759525],[114.127111274572,30.459640029638972],[114.12713397060182,30.459650554557154],[114.12720629683558,30.459684360024074],[114.12725407711113,30.45970325251408],[114.12729733260204,30.45972041396493],[114.1273112377685,30.459726030135034],[114.1273387493548,30.45973707466334],[114.12741960052145,30.459769520858135],[114.12742953675937,30.459773454640874],[114.12743318029702,30.45977451987903],[114.12743665719199,30.459775369308982],[114.12744153599023,30.45977603826525],[114.12744835080035,30.459775950769664],[114.12745466568676,30.45977550488621],[114.12745847719489,30.459774694886836],[114.12746506493548,30.459772979191133],[114.12746847546522,30.459771795045626],[114.1274624922081,30.459769262478748],[114.12745538343432,30.459766253507013],[114.12738740482354,30.459737479731473],[114.1272636764614,30.45968616892877],[114.12719322462243,30.45965567716349],[114.12714142965808,30.45963118944914],[114.12702821462499,30.459577594181486],[114.12694685086942,30.45953947148556],[114.12683688354451,30.459487762971463],[114.12682661116624,30.459506038981907]]],[[[114.1305997264454,30.461254650420855],[114.13056643077365,30.46123894446055],[114.13045399666704,30.46118618729741],[114.13036864378365,30.461146033660274],[114.1303616720721,30.461142769719032],[114.13036087968533,30.46114268878565],[114.13036047888463,30.46114276387994],[114.13036020101502,30.461142865430812],[114.13035959442377,30.46114331708407],[114.13034715194661,30.461163681364365],[114.13035309354153,30.461166463038413],[114.1304384464855,30.46120661673209],[114.13055088067259,30.461259373969753],[114.13058704027621,30.46127643089205],[114.1305997264454,30.461254650420855]]],[[[114.13053418430762,30.461367177585128],[114.13049318476216,30.461347923823414],[114.1304353730375,30.4613206455615],[114.1303847801091,30.46129699218056],[114.13033804043904,30.461275160858154],[114.13031869977469,30.461266093584754],[114.13025878549531,30.461237868552704],[114.13022311086705,30.461221015009325],[114.13019982360476,30.461210114477765],[114.1301856974576,30.46120357451651],[114.13014370712925,30.461183827450988],[114.13007393532432,30.46115088390394],[114.13002322306909,30.461127028815913],[114.13000194661323,30.461117000383993],[114.12998034182208,30.4611068297025],[114.12996651839514,30.461100593680996],[114.1298697425707,30.461055209083533],[114.12984185474966,30.461042091356937],[114.12982176097213,30.461032475595395],[114.1298039961753,30.46102422594877],[114.12974578440762,30.460996730168535],[114.12969922356724,30.46097479316306],[114.1296529204122,30.46095292464968],[114.12960720785264,30.460931465418557],[114.12956090431067,30.460909605744693],[114.12951580005522,30.460888303507012],[114.12947708153946,30.46086998646089],[114.12940928452369,30.460838038896387],[114.12918331261375,30.46073195799231],[114.12913177782775,30.46070775896083],[114.12907421815683,30.460680663249114],[114.12898231868293,30.46063720932726],[114.1289153688462,30.460605747365257],[114.12885654370466,30.460578209460085],[114.1288389962118,30.46056985731374],[114.12882684847357,30.460563863524182],[114.1288141725599,30.4605581652759],[114.12880782774334,30.46055555054327],[114.1288057848866,30.46055505635136],[114.12880362948478,30.460554857465603],[114.12880095013205,30.460554809883952],[114.12879839023064,30.460555075444944],[114.12879690449662,30.460555292723512],[114.1287931846466,30.460556524724897],[114.12879028514973,30.460558073317237],[114.12878727750986,30.460560462065427],[114.1287849094132,30.46056331802322],[114.12878327828815,30.46056649000981],[114.12878233237424,30.460570669087307],[114.12878249837452,30.46057437526104],[114.12878361332838,30.460578040744846],[114.12878565826341,30.460581439852827],[114.12878879981872,30.460584578808444],[114.12882004927184,30.460598789178334],[114.12882910982678,30.46060304892338],[114.12883741646571,30.460606796599585],[114.12888886204941,30.460631219887322],[114.12907550677258,30.460718986866333],[114.12919481048306,30.460775052584722],[114.1292711416073,30.46081079817736],[114.1294100633873,30.460876387833594],[114.12953601405539,30.460935704296794],[114.12962270083028,30.460976511380327],[114.1300316137447,30.46116872249151],[114.13018471241936,30.461240365703688],[114.13042502797587,30.461353907089034],[114.13051678799235,30.461397044752903],[114.13053418430762,30.461367177585128]]],[[[114.13046385794472,30.46148791862986],[114.13040616042963,30.461460780940797],[114.13028950406114,30.461405850830815],[114.13026203901191,30.46139298174333],[114.13020055553451,30.461363638193205],[114.13013410558624,30.46133221712907],[114.13006073761063,30.461297889754782],[114.1300180382422,30.461277865927773],[114.1299791363247,30.461259798379114],[114.12995329752638,30.46124753359232],[114.12992004575335,30.46123183901411],[114.12989969739692,30.461222058825008],[114.12988892761159,30.461216948521958],[114.12988435766842,30.461215291255623],[114.12987909445536,30.46121403602663],[114.1298744487866,30.461212891337485],[114.12985058719934,30.461206110209485],[114.1298446088779,30.46120359480715],[114.12974620246875,30.46116358510477],[114.12969948098392,30.461144727955627],[114.12967232207535,30.46113375856144],[114.12964617596162,30.46112312554772],[114.12961041249778,30.461108580663897],[114.12958210924047,30.461097257663308],[114.1295633657393,30.461089558870512],[114.12953506872064,30.461078021140928],[114.12951757397664,30.461071050357365],[114.12950048798439,30.46106420274828],[114.12948548483459,30.461058045876143],[114.12947924318162,30.461055460555205],[114.12947335563234,30.46105284662048],[114.1294563747867,30.461044784653954],[114.12943675534166,30.461035638640777],[114.12935809571461,30.460998518369415],[114.12927029666353,30.460957162723787],[114.12917071928544,30.460910203564357],[114.12912909028749,30.46089062224755],[114.1290837525801,30.46086931292122],[114.12902410095711,30.460841056350123],[114.12901186070705,30.4608353856701],[114.1289942551628,30.46082698705355],[114.12897611545476,30.460818395121724],[114.12895182480428,30.460807309844117],[114.12893148732057,30.460797480728047],[114.12890344170553,30.46078475551124],[114.12889549846048,30.46078127011125],[114.12884170032781,30.460760314277042],[114.1288347519017,30.4607580098877],[114.12882607768718,30.460755306255887],[114.12881584291304,30.460752569654307],[114.12880273716975,30.46074948598957],[114.12879713995717,30.460748355768576],[114.12879353452269,30.460747554032856],[114.12878923020261,30.460746584780672],[114.12878632197628,30.46074600780591],[114.12878070054767,30.460744953445282],[114.12877733130436,30.46074434763582],[114.1287733065773,30.460743594320274],[114.12876752626345,30.46074270844498],[114.1287603003112,30.460741620180574],[114.12874927521233,30.46074033708395],[114.12873845929876,30.460739421186133],[114.12872899998375,30.460738743979512],[114.12872142087552,30.46073845018549],[114.12871229983101,30.460738238373125],[114.12870264177718,30.460738339806475],[114.12869255624352,30.460738538047984],[114.12868470145209,30.46073887723521],[114.12867494039402,30.46073949201651],[114.12866963265316,30.460739742643483],[114.12866651366794,30.46074008124729],[114.12866915549414,30.460748763234417],[114.12867217376335,30.460753660157007],[114.12867439506623,30.460755258474624],[114.1286793057532,30.460757080013],[114.12868139827911,30.460757852863008],[114.12868556375125,30.460758776239317],[114.12868966577041,30.460759519268876],[114.12870763973575,30.46076065087707],[114.12872509411235,30.460761687958037],[114.12874165812096,30.46076308809264],[114.12876268329988,30.460766109979808],[114.12878095541575,30.460769264625952],[114.12880683945993,30.46077534867905],[114.12883288465684,30.460783440969028],[114.1288681713778,30.460795918675725],[114.12887836300479,30.460799888589232],[114.12888630625602,30.46080337399543],[114.1289143518923,30.46081609923264],[114.12893468939191,30.460825928363864],[114.1289589800609,30.46083701365927],[114.12897711978253,30.460845605604515],[114.12899472534052,30.460854004234157],[114.12900696560021,30.460859674923135],[114.12906661726825,30.46088793153845],[114.12911195501067,30.460909240898385],[114.1291444674547,30.460924534015696],[114.12915358404044,30.460928822245865],[114.12925316149412,30.46097578147882],[114.12934096061207,30.46101713718879],[114.12941962029913,30.46105425751788],[114.12943923975868,30.46106340354579],[114.12945622061763,30.461071465524732],[114.12946210817088,30.461074079463888],[114.1294683498294,30.461076664789566],[114.12948335299018,30.461082821672694],[114.1295004389957,30.46108966929423],[114.12951793375322,30.461096640090652],[114.12954623079347,30.461108177841012],[114.12956497430956,30.461115876647472],[114.12959327758891,30.461127199668816],[114.12962904107998,30.461141744578914],[114.1296551872138,30.46115237761181],[114.12968234614334,30.46116334702594],[114.1297290676644,30.461182204209194],[114.12982747414874,30.461222213983582],[114.12983345247454,30.461224729390207],[114.12985731408087,30.46123151053599],[114.12986195975375,30.461232655228265],[114.12986722297065,30.461233910461385],[114.12987179291733,30.461235567730828],[114.12988256271107,30.461240678041854],[114.12990291108265,30.46125045824588],[114.12993616288067,30.46126615284841],[114.12996200169835,30.461278417653883],[114.13000090364547,30.461296485230985],[114.13004360304595,30.46131650908941],[114.13011697107694,30.4613508365171],[114.13018342107554,30.461382257629765],[114.13024490459868,30.461411601224487],[114.13027236966882,30.461424470331927],[114.13038902612425,30.461479400526617],[114.13045113268977,30.46150861200973],[114.13045166005098,30.461508860796823],[114.13046385794472,30.46148791862986]]],[[[114.12838443020314,30.460133341602067],[114.12837846261124,30.460133861140605],[114.1283699891731,30.460134063864203],[114.12835427072939,30.460133826990184],[114.12833517330242,30.460132563964283],[114.12832069165516,30.46013139852543],[114.12830984786484,30.46012992812529],[114.12828752320745,30.46012621008306],[114.12825057243269,30.46011874580906],[114.12822395781758,30.460111309143702],[114.12820544788224,30.46010499855947],[114.12817294785577,30.460091995602493],[114.12812695711021,30.460071335740665],[114.12804831847087,30.460034140966847],[114.1279144381997,30.45997081742014],[114.12779109603392,30.459912477811564],[114.1277751204641,30.45990500277956],[114.1276701278791,30.45985553077131],[114.12763036619752,30.45983774081049],[114.12762840913243,30.459836865541],[114.12762836860277,30.45983684895803],[114.1276283279475,30.459836832607156],[114.12762828716846,30.45983681648975],[114.12762824626789,30.459836800605576],[114.12762821551723,30.459836788839294],[114.12762818469247,30.45983677722],[114.12762814349567,30.45983676192334],[114.12762810218283,30.459836746863317],[114.12762806075638,30.459836732040046],[114.12762802961645,30.459836721070833],[114.12762799840706,30.459836710250425],[114.1276279567045,30.459836696022094],[114.12762791489392,30.459836682032556],[114.12762787297827,30.459836668282776],[114.1276278309587,30.459836654773262],[114.12762778883703,30.459836641504527],[114.12762774661529,30.459836628477305],[114.1276277042951,30.459836615691998],[114.12762766187866,30.45983660314928],[114.12762761936739,30.45983659084943],[114.12762757676379,30.459836578793425],[114.12762754475389,30.45983656990394],[114.12762751268797,30.459836561167286],[114.12762746986063,30.45983654972238],[114.12762743768602,30.459836541292116],[114.12762740545831,30.459836533015483],[114.1276273624185,30.459836522185185],[114.12762731929688,30.45983651160144],[114.12762727609562,30.45983650126454],[114.12762724364647,30.45983649366683],[114.12762721114912,30.459836486223768],[114.12762716775659,30.45983647650663],[114.12762713516688,30.459836469374043],[114.12762710253213,30.459836462397003],[114.1276270589606,30.459836453302234],[114.12762702623918,30.459836446636917],[114.12762699347569,30.459836440127994],[114.12762694973672,30.4598364316578],[114.12762690593424,30.459836423438013],[114.12762686207003,30.459836415469027],[114.12762681814617,30.459836407751467],[114.12762677416377,30.45983640028539],[114.12762673024915,30.45983639309108],[114.12762668665239,30.459836386204987],[114.12762665392206,30.459836381194762],[114.1276266211611,30.45983637633899],[114.12762657744001,30.459836370070946],[114.12762653367199,30.45983636405028],[114.12762650081824,30.45983635968949],[114.12762646793738,30.459836355483777],[114.12762643503599,30.45983635140226],[114.12762640210897,30.45983634747593],[114.127626358174,30.45983634244777],[114.12762631420206,30.459836337668627],[114.12762627019484,30.45983633313838],[114.1276262261544,30.459836328856976],[114.12762618208221,30.45983632482504],[114.12762613798041,30.45983632104274],[114.12762609385085,30.459836317509783],[114.12762604969566,30.459836314227033],[114.1276260165642,30.45983631192067],[114.1276259834181,30.459836309770974],[114.12762593920633,30.45983630711286],[114.12762589497518,30.459836304705398],[114.12762585072659,30.459836302547842],[114.12762580646259,30.459836300640816],[114.12762577325597,30.4598362993673],[114.12762574004165,30.459836298250032],[114.12762570682145,30.459836297258203],[114.12762567359557,30.45983629642308],[114.12762562928798,30.45983629551856],[114.12762559576993,30.459836294993252],[114.12762556210721,30.45983629462586],[114.12762552844248,30.4598362943868],[114.12762549477696,30.45983629430882],[114.12762544988901,30.459836294418913],[114.12762540500245,30.459836294786577],[114.12762536011977,30.459836295411062],[114.12762531524221,30.45983629629294],[114.12762527037239,30.459836297431924],[114.12762523672546,30.459836298446906],[114.12762520308536,30.459836299622452],[114.12762515824227,30.459836301404135],[114.12762511341339,30.459836303442685],[114.12762506860075,30.45983630573816],[114.12762502380629,30.459836308290168],[114.12762497903172,30.459836311099092],[114.12762494546469,30.459836313366072],[114.12762491191184,30.45983631579315],[114.12762486719666,30.459836319242925],[114.12762483367631,30.459836321990196],[114.127624800174,30.459836324897676],[114.12762475552913,30.45983632898738],[114.12762472206498,30.45983633221465],[114.12762468862229,30.459836335601615],[114.12762465519617,30.45983633911615],[114.1276246217931,30.45983634279044],[114.12762457728701,30.459836347901863],[114.1276245328204,30.45983635326826],[114.12762448839558,30.459836358889504],[114.12762444401379,30.459836364764868],[114.12762439967753,30.459836370894458],[114.1276243553885,30.459836377278044],[114.12762431114874,30.459836383915114],[114.12762426696017,30.459836390805716],[114.12762422292073,30.459836397933223],[114.12762417922436,30.459836405263164],[114.12762414648644,30.459836410915933],[114.12762411378517,30.45983641672315],[114.12762407023173,30.45983642467282],[114.1276240267391,30.459836432869306],[114.12762398330916,30.459836441312838],[114.1276239507767,30.45983644779963],[114.12762391828501,30.459836454440303],[114.12762387501897,30.45983646349917],[114.12762383182195,30.459836472803538],[114.12762379946815,30.45983647993499],[114.12762376715968,30.459836487219633],[114.12762373488773,30.459836494626593],[114.127623702663,30.459836502186125],[114.12762365975962,30.459836512468513],[114.12762361693527,30.45983652299429],[114.12762358486626,30.459836531040718],[114.12762355284848,30.45983653923868],[114.12762351022704,30.459836550371],[114.12762347831344,30.45983655887163],[114.12762344645421,30.459836567522885],[114.1276234040472,30.459836579258948],[114.12762337229795,30.459836588211218],[114.12762334060511,30.459836597313817],[114.1276233089579,30.459836606536296],[114.12762327736935,30.459836615908305],[114.12762324582772,30.45983662539985],[114.12762321434622,30.459836635040585],[114.12762317245144,30.45983664809316],[114.12762313065642,30.459836661383363],[114.12762308896173,30.4598366749108],[114.1276230473699,30.459836688674496],[114.12762300588246,30.459836702674057],[114.12762296450092,30.459836716908963],[114.12762292322763,30.45983673137848],[114.12762288206346,30.4598367460822],[114.1276228410695,30.459836760997447],[114.12762280036458,30.459836776079236],[114.12762276990655,30.459836787534186],[114.12762273952062,30.459836799132127],[114.12762269910273,30.459836814786488],[114.12762265880288,30.45983683066868],[114.12762262865232,30.45983684272252],[114.12762259857782,30.459836854917857],[114.12762256856425,30.459836867226166],[114.12762253862776,30.459836879675816],[114.12762249881615,30.45983689646266],[114.12762245913135,30.45983691347381],[114.12762241957452,30.45983693070835],[114.12762238014746,30.45983694816616],[114.12762234085166,30.459836965846215],[114.12762231146311,30.459836979244585],[114.1276222821582,30.45983699278104],[114.12762225292117,30.45983700642762],[114.12762222376958,30.459837020211605],[114.1276221850163,30.45983703877284],[114.12762215603836,30.459837052830146],[114.12762212714861,30.459837067023827],[114.1276220983298,30.459837081325865],[114.1276220696005,30.459837095763145],[114.12762203141526,30.45983711519309],[114.12762199337662,30.459837134838043],[114.12762196493958,30.45983714970557],[114.12762193659584,30.459837164706798],[114.12762189892922,30.459837184886037],[114.12762186108847,30.45983720545634],[114.1276218232954,30.45983722630116],[114.12762178566054,30.45983724736167],[114.12762175753487,30.45983726329141],[114.12762172950961,30.45983727935421],[114.12762169227855,30.459837300949314],[114.12762165521181,30.459837322756112],[114.12762162751534,30.45983733924339],[114.12762159992351,30.45983735586208],[114.12762156327396,30.45983737819527],[114.12762153589367,30.459837395075652],[114.12762150861974,30.45983741208602],[114.12762147239825,30.459837434939054],[114.12762143635086,30.459837457998546],[114.12762140942452,30.459837475421487],[114.12762138260884,30.459837492972426],[114.12762134700236,30.459837516543274],[114.12762132040922,30.4598375343481],[114.12762129392969,30.45983755227932],[114.12762125877431,30.45983757635521],[114.12762123252163,30.459837594537248],[114.12762120638483,30.45983761284398],[114.12762118034041,30.4598376312499],[114.12762115441242,30.45983764977954],[114.1276211285784,30.459837668407918],[114.12762110286229,30.459837687158597],[114.12762107724106,30.459837706006923],[114.127621051739,30.459837724976808],[114.127621017897,30.459837750431678],[114.12762098424793,30.459837776079],[114.12762095079383,30.45983780191797],[114.1276209175363,30.45983782794712],[114.12762088447656,30.459837854165812],[114.12762085161621,30.459837880572458],[114.12762081895652,30.45983790716621],[114.12762079458794,30.459837927227774],[114.12762077034647,30.459837947404928],[114.12762073819461,30.459837974461017],[114.1276207062484,30.45983800170016],[114.12762067450895,30.459838029121173],[114.12762064297809,30.45983805672239],[114.12762061165706,30.4598380845032],[114.1276205805471,30.45983811246234],[114.1276205573472,30.459838133542032],[114.12762053428062,30.459838154732015],[114.12762050370418,30.459838183131183],[114.12762047334368,30.459838211704074],[114.12762044320004,30.45983824044978],[114.12762041327487,30.459838269366823],[114.12762039096796,30.45983829116111],[114.12762036884001,30.459838313020498],[114.12762033952039,30.45983834230591],[114.12762031042284,30.45983837175765],[114.12762028873873,30.45983839395009],[114.12762026719523,30.45983841624474],[114.12762023865783,30.459838446106975],[114.12762021034742,30.45983847613115],[114.12762018226395,30.459838506316082],[114.12762015440984,30.459838536659873],[114.12762012678577,30.459838567161917],[114.12762009939274,30.45983859782078],[114.12762007899315,30.45983862091195],[114.12762005873914,30.459838644099637],[114.12762003860283,30.459838667364142],[114.12762001861302,30.459838690723796],[114.12761999215621,30.459838721995837],[114.12761996593728,30.459838753417987],[114.12761993995598,30.459838784988815],[114.12761992061982,30.459838808759166],[114.12761990143335,30.459838832620687],[114.12761987605282,30.459838864557106],[114.12761985091379,30.45983889663744],[114.12761982601839,30.459838928860368],[114.1276198074992,30.459838953115334],[114.1276197891332,30.459838977458347],[114.1276197708901,30.45983900187066],[114.12761975280124,30.45983902636931],[114.12761972888869,30.459839059147654],[114.12761971110986,30.459839083816473],[114.1276196934865,30.459839108569174],[114.12761967019709,30.459839141683588],[114.12761964715848,30.45983917493035],[114.12761962437214,30.45983920830697],[114.12761960183853,30.459839241813157],[114.12761957955888,30.459839275446402],[114.12761955753355,30.45983930920586],[114.12761953576491,30.459839343090163],[114.12761951425206,30.459839377097545],[114.12761949299771,30.459839411226756],[114.12761947200165,30.459839445476035],[114.12761945126489,30.45983947984424],[114.12761943587527,30.459839505693772],[114.12761942064874,30.459839531615984],[114.12761940555305,30.459839557595664],[114.1276193906211,30.45983958364661],[114.12761937582103,30.459839609753768],[114.12761936118548,30.459839635930937],[114.12761934189163,30.45983967092571],[114.1276193228632,30.459839706029513],[114.12761930402522,30.45983974138382],[114.12761928543088,30.459839776892835],[114.12761926710763,30.459839812507347],[114.12761925353418,30.4598398392837],[114.12761924013094,30.45983986612391],[114.127619222487,30.459839901995572],[114.12761920511619,30.459839937967555],[114.12761918801932,30.459839974038033],[114.1276191711972,30.45984001020549],[114.12761915875262,30.459840037390883],[114.12761914648067,30.459840064635245],[114.12761913434706,30.459840091926043],[114.12761912238625,30.459840119274276],[114.12761911056447,30.459840146667975],[114.12761909891618,30.459840174117343],[114.12761908740727,30.45984020161087],[114.12761907607344,30.459840229159045],[114.12761906119373,30.459840265960946],[114.12761904659479,30.459840302847468],[114.12761903227624,30.459840339816335],[114.12761902171361,30.459840367593937],[114.12761901132711,30.459840395421235],[114.12761900108167,30.459840423288085],[114.12761899101291,30.459840451203267],[114.12761897782404,30.459840488486794],[114.1276189649191,30.45984052584471],[114.1276189522991,30.459840563275755],[114.12761894301164,30.459840591393576],[114.12761893390257,30.459840619555518],[114.12761892199579,30.459840657161408],[114.1276189132444,30.459840685408295],[114.12761890467253,30.459840713696238],[114.12761889348214,30.45984075146724],[114.12761888257951,30.459840789301538],[114.12761887196457,30.459840827196913],[114.12761886163878,30.459840865152394],[114.12761885407448,30.459840893655265],[114.12761884669118,30.45984092219384],[114.12761883945247,30.459840950760203],[114.12761883239541,30.459840979360447],[114.12761882548295,30.45984100798744],[114.12761881875272,30.459841036646793],[114.12761881002072,30.459841074901362],[114.12761880158001,30.45984111320473],[114.12761879543179,30.459841141962116],[114.12761878946586,30.459841170748103],[114.12761878175492,30.45984120916665],[114.12761877433641,30.459841247628418],[114.12761876721046,30.459841286131805],[114.12761876204941,30.459841315033813],[114.12761875707136,30.459841343960434],[114.12761875067861,30.459841382559674],[114.12761874457951,30.45984142119468],[114.12761874018916,30.45984145019221],[114.1276187359827,30.45984147920998],[114.12761873192328,30.459841508243642],[114.12761872804813,30.45984153729595],[114.12761872312669,30.45984157605629],[114.12761871962003,30.459841605143612],[114.12761871629831,30.459841634247077],[114.1276187131238,30.45984166336285],[114.12761871013377,30.459841692493065],[114.12761870729186,30.459841721634284],[114.12761870463422,30.459841750788797],[114.12761870133754,30.459841789677174],[114.12761869833716,30.45984182858323],[114.12761869563249,30.45984186750567],[114.1276186937895,30.459841896706717],[114.12761869213125,30.459841925915605],[114.12761869062098,30.45984195513055],[114.12761868929645,30.4598419843522],[114.12761868777706,30.459842023321897],[114.1276186865544,30.45984206229927],[114.12761868582245,30.459842091536046],[114.12761868527593,30.45984212077583],[114.12761868487829,30.459842150017064],[114.12761868466548,30.45984217925972],[114.12761868460136,30.4598422085028],[114.12761868472242,30.459842237745644],[114.12761868513086,30.459842276734594],[114.12761868583661,30.459842315719577],[114.12761868655103,30.45984234495549],[114.12761868745064,30.45984237418736],[114.12761868849857,30.459842403415447],[114.12761868973193,30.459842432637902],[114.12761869111358,30.459842461854933],[114.12761869268054,30.45984249106513],[114.12761869501627,30.459842530000905],[114.12761869764839,30.459842568922024],[114.12761870057666,30.459842607827213],[114.1276187038008,30.45984264671456],[114.12761870732085,30.459842685582235],[114.12761871113665,30.459842724428814],[114.1276187152479,30.45984276325186],[114.12761871616526,30.4598428018164],[114.12762504625675,30.45984893543803],[114.12762949228308,30.459852225409115],[114.1276374231897,30.459856310308833],[114.1276616430547,30.459868103554726],[114.12776663568344,30.459917575602454],[114.12778261125963,30.45992505064024],[114.12813135914148,30.4600900038603],[114.12814639053336,30.460097240706176],[114.12816552011387,30.460106150285668],[114.1281841717783,30.46011497172923],[114.12820244821832,30.460121744953625],[114.12821657240998,30.460126959969266],[114.12822533712574,30.460129617014047],[114.12823552448107,30.460132304143233],[114.12825978483049,30.460138049224465],[114.12826945708734,30.460140229306568],[114.1282785540599,30.46014182891401],[114.12829062480424,30.460144068728933],[114.12829917773735,30.460145440326883],[114.12832593940803,30.460147729175638],[114.12834108083686,30.46014871687025],[114.1283521088651,30.4601489500489],[114.12835742918749,30.46014891256776],[114.12837417703372,30.460148560094062],[114.12838159291634,30.460148039918515],[114.12838443020314,30.460133341602067]]],[[[114.12848080370732,30.46021996386791],[114.1284606817147,30.460222550643277],[114.12842428170627,30.460225607748736],[114.1284355436386,30.460270462042168],[114.12847819403144,30.460289804892188],[114.12848303613838,30.460290623268943],[114.12848869445524,30.460290794092238],[114.12849910512165,30.46028920664694],[114.12850612224027,30.46028710502764],[114.12851218636065,30.46028186305056],[114.12851944887758,30.460272354453615],[114.12853544237866,30.460248200417247],[114.12848080370732,30.46021996386791]]],[[[114.12876098693175,30.460327242497126],[114.12871829029552,30.460330156172663],[114.12869807501227,30.460363024516155],[114.12869614675651,30.460371470071735],[114.1286958516941,30.46037890808962],[114.1286974221503,30.46038724711732],[114.12869941534363,30.460391301317554],[114.12870328283655,30.460395533591264],[114.12870918351372,30.460399367437283],[114.12871169761027,30.460401000920996],[114.12875440144785,30.460421163179134],[114.12877706273908,30.460376546593384],[114.12876098693175,30.460327242497126]]],[[[114.12753905401199,30.46014761437533],[114.1275338599891,30.46014791932803],[114.12752450151488,30.460149209546806],[114.12752101223012,30.46014984828435],[114.12751615369005,30.460151116533744],[114.12750968572972,30.46015305431144],[114.12750768991903,30.4601537788084],[114.12762175207672,30.460208054824022],[114.12767435871464,30.46023264059284],[114.12791771848921,30.46034719627658],[114.12797508357396,30.460374199251135],[114.12799381256822,30.460382912647844],[114.12801514013283,30.460393016077504],[114.12802370784166,30.460397437090197],[114.1280280824312,30.460399920137075],[114.12803869484003,30.460406549377637],[114.12805096331688,30.460414819338357],[114.1280637371187,30.46042392988968],[114.12807228893135,30.460430668873688],[114.1280896451314,30.46044474754322],[114.12809933904086,30.460453695358574],[114.12811173214664,30.4604661789348],[114.12812094518,30.460476242493055],[114.12812933396587,30.46048571128608],[114.1281362289695,30.460494029909185],[114.1281388873361,30.46049745989069],[114.12815040705412,30.460510719082308],[114.12815427578668,30.46051473903124],[114.12815698491843,30.46051610904076],[114.12816000021421,30.460516411494666],[114.12817484541584,30.46051340764445],[114.12817310991078,30.460510604870887],[114.12816915556422,30.46050441420295],[114.12816408899778,30.46049610720233],[114.12815907912268,30.46048870347132],[114.12815495677987,30.460482572393023],[114.1281496400492,30.460475712433443],[114.1281427450495,30.46046739381447],[114.12813435626802,30.460457925026244],[114.1281251432397,30.460447861473753],[114.12811275014104,30.460435377904666],[114.12810305623711,30.460426430095055],[114.12808570004728,30.460412351435764],[114.12807714823967,30.46040561245698],[114.12806437444586,30.460396501913202],[114.12805210597658,30.460388231959485],[114.12804149357405,30.460381602725292],[114.12803711898684,30.460379119681253],[114.12802855128386,30.46037469867336],[114.12800722373298,30.460364595256255],[114.12798849475082,30.460355881870463],[114.12768777008509,30.460214323390005],[114.12763516348114,30.46018973765222],[114.12758087853727,30.46016426831105],[114.127549333318,30.460149654253833],[114.1275469608798,30.460148701939403],[114.12754442581192,30.460147980719363],[114.12753905401199,30.46014761437533]]],[[[114.1283105764358,30.46046782138418],[114.1282703180798,30.46049109632768],[114.12827622553172,30.460502624397915],[114.12828152820767,30.460515645183932],[114.12828729169814,30.460530317484707],[114.12829079823528,30.46053987849389],[114.12834003760709,30.460556818168108],[114.12836063324018,30.46052378574948],[114.12836376369664,30.460518006512633],[114.12836459736319,30.46051508338462],[114.12836541369076,30.460512042620753],[114.12836514161567,30.46050926750724],[114.1283647553862,30.460505777343094],[114.12836374082613,30.460502454308315],[114.12836197313399,30.460498592147694],[114.12836034108322,30.460496131066375],[114.12835756235341,30.46049308645533],[114.12835458131129,30.460490208959772],[114.12835106750647,30.46048742844768],[114.12834542677822,30.46048453981788],[114.12832747170387,30.46047566264443],[114.1283105764358,30.46046782138418]]],[[[114.1285716711271,30.460593593058658],[114.12856745920965,30.460593720536945],[114.12856122741601,30.46059469837452],[114.12855772059082,30.460595941281333],[114.1285522979939,30.460598027727023],[114.12854839016643,30.460600191291086],[114.12854500787812,30.46060214946514],[114.12854230576508,30.460604654237667],[114.12854039690191,30.460607392273527],[114.12851933861432,30.460639796030616],[114.12858022522585,30.460671950374703],[114.1285895537819,30.460669925476665],[114.12860621810192,30.460667869126652],[114.12862116348589,30.46066596587378],[114.12863837299199,30.460664351103055],[114.12862658288586,30.460617500482353],[114.12858066923467,30.460596083515824],[114.12857532038161,30.460594229469987],[114.1285716711271,30.460593593058658]]],[[[114.12897714429718,30.460453957032687],[114.1289692687468,30.460455342094463],[114.1289652256492,30.46045728543705],[114.12896128503084,30.460460424312014],[114.12895919341386,30.460464000223794],[114.12896141988213,30.46046569754427],[114.12896562637829,30.460469304460965],[114.12897456271966,30.460476495050344],[114.12897886427122,30.460479812759715],[114.12897935757692,30.460480189934927],[114.12898217988712,30.46048225042216],[114.12899244448124,30.46048937726859],[114.12900031997047,30.460494519353627],[114.12900805299152,30.460499048680862],[114.1290194750882,30.46050498195191],[114.1290352411703,30.46051301856558],[114.12905582952001,30.460523502378468],[114.12908361933961,30.460538052699043],[114.12945622823806,30.46071324291898],[114.12947048490607,30.460719948422255],[114.1294720816535,30.46072074574196],[114.12947592248388,30.460706120587197],[114.12946322937594,30.46070015049239],[114.1290906206135,30.460524960386824],[114.12906283080406,30.460510410074754],[114.12904224246184,30.460499926268238],[114.1290372063477,30.460497359157156],[114.1290197734809,30.460486242838083],[114.12900059389624,30.460471954879953],[114.12898620514909,30.46045904119022],[114.12898247039269,30.460455143540806],[114.1289800186363,30.460454375743627],[114.12897714429718,30.460453957032687]]],[[[114.12819690408288,30.460564240180304],[114.1281865261613,30.4605674144638],[114.1281822186377,30.460568994070364],[114.12818053522149,30.460570151832588],[114.12817949550193,30.460571246937015],[114.12817924512454,30.460573839898743],[114.12817989715913,30.46057786340805],[114.12818195099064,30.46058659457124],[114.1281856385046,30.460597393987406],[114.12818940537856,30.460607712761643],[114.1281927953916,30.460620418854052],[114.12819466214823,30.460635542514808],[114.12819561518553,30.460648069173835],[114.12819602530844,30.4606641167207],[114.12819591336795,30.46067465478047],[114.12819434854497,30.460695108837097],[114.12819258769844,30.460709383066078],[114.12818963393035,30.460722462959897],[114.12817555905188,30.460752954166573],[114.12816054083802,30.460779511162954],[114.1281489573597,30.460799553409373],[114.1281397254731,30.460814394572154],[114.12808936576536,30.460894645704666],[114.12797226529804,30.461083207482826],[114.12748164436566,30.461863076578037],[114.12749605673096,30.46186848121499],[114.12801970600025,30.46103656746764],[114.12815589593656,30.46081798258573],[114.12816512781515,30.460803141416246],[114.12817671128303,30.460783099161485],[114.12819172948392,30.460756542154517],[114.128205804349,30.4607260509376],[114.1282087581131,30.460712971041602],[114.12821092672456,30.4606980520665],[114.12821208377527,30.460678242753254],[114.12821257112043,30.46066722633577],[114.1282122678114,30.460651493822446],[114.12821182592609,30.460639142547798],[114.12821021761332,30.460623642476424],[114.12820830338848,30.460610140794113],[114.12820676016533,30.460600871482857],[114.1282041467479,30.460588715452456],[114.12820072582716,30.460575969525987],[114.12819690408288,30.460564240180304]]],[[[114.12974981793391,30.46083446168812],[114.12974637290162,30.46083505804879],[114.12974633375953,30.4608350773345],[114.12974629465444,30.460835096676004],[114.12974625558576,30.46083511607342],[114.12974621655387,30.460835135526747],[114.12974617755953,30.46083515503621],[114.12974613860139,30.460835174601243],[114.12974609968082,30.460835194221847],[114.12974606079756,30.460835213898076],[114.12974602195166,30.460835233629986],[114.12974598314332,30.460835253417525],[114.12974594437198,30.46083527326063],[114.1297459056391,30.460835293159246],[114.12974586694355,30.460835313112977],[114.129745828286,30.460835333122105],[114.12974578966617,30.460835353186745],[114.12974575108491,30.460835373306328],[114.12974571254188,30.46083539348142],[114.12974567403664,30.4608354137114],[114.12974563556999,30.460835433996607],[114.12974559714226,30.46083545433653],[114.12974555875266,30.46083547473179],[114.12974552040188,30.460835495181765],[114.12974548209013,30.460835515686455],[114.12974544381717,30.460835536246314],[114.12974540558328,30.460835556860378],[114.1297453673883,30.460835577529437],[114.12974532923315,30.46083559825321],[114.12974529111682,30.460835619031304],[114.1297452530402,30.460835639864218],[114.12974521500283,30.460835660751343],[114.12974517700553,30.46083568169312],[114.12974513904761,30.460835702688755],[114.1297451011301,30.460835723738995],[114.12974506325219,30.46083574484343],[114.129745025414,30.460835766002017],[114.12974498761653,30.46083578721474],[114.12974494985934,30.460835808481445],[114.12974491214234,30.460835829802125],[114.12974487446563,30.46083585117694],[114.1297448368299,30.460835872605397],[114.12974479923466,30.460835894087822],[114.1297447616805,30.460835915623992],[114.12974472416663,30.460835937213854],[114.12974468669428,30.46083595885729],[114.12974464926269,30.460835980554528],[114.12974461187261,30.460836002305054],[114.12974457452363,30.46083602410933],[114.12974453721606,30.460836045966555],[114.12974449995004,30.46083606787741],[114.12974446272574,30.460836089841496],[114.12974442554314,30.460836111858995],[114.12974438840205,30.460836133929604],[114.12974435130327,30.460836156052935],[114.12974431424581,30.460836178229904],[114.12974427723125,30.460836200459585],[114.12974424025855,30.460836222741996],[114.12974420332807,30.46083624507735],[114.12974416644003,30.460836267465762],[114.12974412959456,30.460836289906847],[114.12974409279208,30.46083631240053],[114.12974405603171,30.460836334946766],[114.12974401931491,30.460836357545723],[114.12974398264045,30.460836380197062],[114.12974394600889,30.460836402900778],[114.12974390942067,30.460836425657046],[114.12974387287557,30.460836448465802],[114.12974383637379,30.460836471326257],[114.12974379991573,30.460836494239317],[114.12974376350067,30.46083651720447],[114.12974372712944,30.46083654022155],[114.12974369080233,30.460836563290776],[114.12974365451844,30.460836586411872],[114.1297436182786,30.460836609584778],[114.12974358208278,30.460836632809663],[114.12974354593123,30.46083665608607],[114.12974350982368,30.46083667941435],[114.12974347376041,30.460836702794264],[114.12974343774174,30.460836726225647],[114.12974340176754,30.46083674970867],[114.12974336583773,30.460836773243045],[114.12974332995272,30.46083679682872],[114.12974329411266,30.460836820465687],[114.12974325831743,30.460836844153953],[114.129743222567,30.460836867893516],[114.12974318686165,30.460836891684032],[114.12974315120157,30.46083691552568],[114.12974311558709,30.46083693941827],[114.12974308001766,30.460836963361775],[114.12974304449341,30.460836987356114],[114.12974300901502,30.460837011401523],[114.12974297358232,30.460837035497374],[114.1297429381956,30.460837059643833],[114.1297429028544,30.460837083841025],[114.12974286755903,30.46083710808849],[114.12974283230997,30.460837132386736],[114.12974279710718,30.460837156735202],[114.12974276195025,30.46083718113405],[114.12974272683962,30.46083720558317],[114.12974269177575,30.46083723008234],[114.1297426567582,30.46083725463189],[114.12974262178747,30.460837279231367],[114.12974258686361,30.460837303880897],[114.12974255198618,30.46083732858029],[114.12974251715573,30.460837353329396],[114.12974248237215,30.46083737812854],[114.12974244763576,30.460837402977557],[114.12974241294671,30.460837427875934],[114.12974237830485,30.46083745282385],[114.12974234371045,30.46083747782146],[114.12974230916356,30.460837502868262],[114.12974227466388,30.46083752796477],[114.129742240212,30.460837553110412],[114.12974220580787,30.460837578305412],[114.12974217145187,30.460837603549603],[114.12974213714375,30.460837628842754],[114.1297421028834,30.46083765418504],[114.1297420686714,30.4608376795764],[114.12974203450729,30.460837705016665],[114.12974200039173,30.460837730505613],[114.12974196632476,30.460837756043233],[114.12974193230623,30.46083778162976],[114.12974189833604,30.460837807264966],[114.12974186441465,30.460837832948457],[114.12974183054206,30.460837858680613],[114.12974179671848,30.46083788446117],[114.12974176308428,30.460837910182224],[114.12974172949909,30.460837935950995],[114.12974169596282,30.4608379617677],[114.12974166247488,30.460837987632175],[114.12974162903632,30.46083801354419],[114.1297415956462,30.460838039503916],[114.12974156230555,30.460838065511123],[114.12974152901394,30.46083809156581],[114.12974149577191,30.46083811766776],[114.1297414625789,30.460838143817245],[114.12974142943571,30.460838170013815],[114.12974139634154,30.460838196257583],[114.12974136329764,30.46083822254855],[114.12974133030254,30.460838248886375],[114.12974129735781,30.460838275271225],[114.12974126446325,30.460838301703102],[114.12974123161838,30.46083832818167],[114.12974119882378,30.46083835470726],[114.129741166079,30.460838381279196],[114.1297411333845,30.460838407897647],[114.12974110074083,30.460838434562728],[114.12974106814708,30.46083846127438],[114.12974103560418,30.460838488032493],[114.12974100311153,30.46083851483672],[114.12974097066974,30.460838541687462],[114.12974093827866,30.460838568584037],[114.12974090593866,30.460838595526898],[114.12974087364948,30.46083862251576],[114.12974084141126,30.460838649550514],[114.1297408092241,30.46083867663133],[114.12974077708834,30.460838703757858],[114.129740745004,30.460838730930053],[114.12974071297083,30.460838758147847],[114.1297406809894,30.460838785411422],[114.12974064905924,30.46083881272043],[114.12974061718063,30.460838840074867],[114.12974058535421,30.46083886747474],[114.12974055357897,30.46083889491982],[114.12974052185616,30.460838922410385],[114.12974049018518,30.460838949945703],[114.1297404585663,30.46083897752623],[114.12974042699975,30.46083900515185],[114.12974039548483,30.460839032822157],[114.12974036402267,30.460839060537616],[114.12974033261303,30.460839088297718],[114.12974030125595,30.46083911610245],[114.1297402699514,30.460839143951986],[114.12974023869893,30.460839171845763],[114.12974020749968,30.460839199784292],[114.12974017635355,30.460839227767114],[114.12974014525973,30.46083925579418],[114.12974011421957,30.46083928386559],[114.12974008323208,30.460839311981182],[114.12974005229756,30.46083934014079],[114.12974002141708,30.46083936834458],[114.12973999058889,30.460839396592032],[114.12973995981484,30.460839424883495],[114.12973992909423,30.46083945321897],[114.12973989842708,30.460839481597944],[114.12973986781358,30.46083951002053],[114.12973983725387,30.460839538486617],[114.12973980674808,30.46083956699654],[114.12973977629618,30.46083959554939],[114.12973974589816,30.460839624146082],[114.12973971555442,30.460839652785594],[114.1297396852648,30.460839681468485],[114.12973965502917,30.46083971019453],[114.12973962484803,30.460839738963223],[114.12973959472167,30.4608397677753],[114.12973956464924,30.460839796630022],[114.1297395346315,30.4608398255275],[114.12973950466834,30.46083985446774],[114.12973947476031,30.460839883450735],[114.12973944490686,30.460839912476146],[114.12973941510836,30.460839941543973],[114.12973938536442,30.46083997065439],[114.1297393556761,30.460839999807],[114.12973932604281,30.460840029001904],[114.12973929646455,30.46084005823883],[114.12973926694166,30.460840087518005],[114.12973923747415,30.460840116839023],[114.12973920806212,30.460840146202116],[114.1297391787058,30.46084017560706],[114.12973914940466,30.460840205053735],[114.12973912015968,30.460840234542086],[114.12973909097016,30.460840264072118],[114.12973906183629,30.46084029364337],[114.12973903275855,30.460840323256296],[114.1297390037371,30.46084035291073],[114.12973897477148,30.460840382606158],[114.12973894586216,30.46084041234309],[114.12973891700888,30.460840442120848],[114.12973888821224,30.46084047194011],[114.12973885947152,30.46084050180002],[114.12973883078767,30.46084053170059],[114.1297388021601,30.460840561642435],[114.12973877358935,30.46084059162459],[114.12973874507516,30.46084062164757],[114.1297387166179,30.460840651710974],[114.1297386882176,30.4608406818152],[114.12973865987372,30.460840711959396],[114.12973863158757,30.4608407421443],[114.12973860335774,30.46084077236917],[114.12973857518587,30.460840802634362],[114.12973854707062,30.460840832939567],[114.12973851901269,30.46084086328475],[114.12973849101286,30.460840893669847],[114.12973846306974,30.46084092409468],[114.1297384351844,30.460840954559426],[114.1297384073566,30.460840985063747],[114.12973837958685,30.460841015607635],[114.12973835187445,30.46084104619138],[114.12973832422003,30.460841076813896],[114.12973829662381,30.4608411074761],[114.12973826908535,30.46084113817748],[114.12973824160466,30.460841168918137],[114.1297382141824,30.460841199697803],[114.12973818681834,30.460841230516465],[114.1297381595125,30.460841261374195],[114.129738132265,30.460841292270576],[114.12973810507626,30.46084132320585],[114.12973807794552,30.460841354179724],[114.12973805087411,30.46084138519232],[114.1297380238607,30.460841416243063],[114.12973799690607,30.460841447332637],[114.12973797001065,30.46084147846019],[114.12973794317357,30.460841509626057],[114.12973791639597,30.46084154083025],[114.12973788967678,30.46084157207241],[114.12973786301728,30.460841603352552],[114.12973783641623,30.46084163467062],[114.12973780987508,30.46084166602666],[114.12973778339317,30.46084169742028],[114.12973775697006,30.46084172885176],[114.12973773060718,30.460841760320598],[114.12973770430318,30.460841791827075],[114.12973767805912,30.46084182337095],[114.12973765183263,30.460841855002833],[114.12973762566584,30.460841886672227],[114.1297375995592,30.460841918378925],[114.12973757351246,30.460841950122514],[114.1297375475262,30.460841981903233],[114.12973752159988,30.460842013721123],[114.12973749573389,30.460842045576083],[114.12973746992874,30.460842077467486],[114.12973744418316,30.460842109395898],[114.12973741849842,30.460842141361034],[114.12973739287459,30.460842173362558],[114.1297373673108,30.460842205400915],[114.12973734180794,30.460842237475145],[114.1297373163659,30.460842269586273],[114.12973729098468,30.460842301733106],[114.12973726566462,30.46084233391637],[114.12973724040495,30.46084236613574],[114.12973721520687,30.460842398391033],[114.1297371900696,30.460842430682032],[114.12973716499351,30.460842463009016],[114.12973713997859,30.460842495371644],[114.12973711502528,30.460842527769803],[114.12973709013336,30.460842560203886],[114.1297370653027,30.46084259267299],[114.12973704053333,30.460842625177513],[114.12973701582568,30.460842657717507],[114.12973699117954,30.460842690292466],[114.12973696659547,30.460842722902548],[114.12973694207301,30.46084275554771],[114.12973691761239,30.460842788227833],[114.12973689321396,30.460842820942695],[114.12973686887703,30.46084285369234],[114.12973684460216,30.460842886476776],[114.12973682038982,30.460842919295605],[114.12973679623921,30.460842952149],[114.12973677215113,30.460842985036614],[114.12973674812511,30.46084301795868],[114.12973672416196,30.460843050915077],[114.12973670026052,30.460843083905242],[114.12973667642233,30.46084311692957],[114.12973665264605,30.46084314998801],[114.12973662893286,30.4608431830801],[114.1297366052821,30.460843216206126],[114.12973658169409,30.460843249365634],[114.12973655816893,30.46084328255914],[114.12973653470664,30.460843315785496],[114.12973651130744,30.460843349045906],[114.1297364879711,30.460843382338947],[114.12973646469763,30.460843415665693],[114.12973644148794,30.460843449025408],[114.1297364183411,30.46084348241832],[114.12973639525725,30.46084351584398],[114.12973637223703,30.460843549302492],[114.12973634928004,30.46084358279397],[114.1297363263868,30.460843616318027],[114.12973630355656,30.460843649874768],[114.12973628079052,30.460843683463793],[114.12973625808748,30.46084371708522],[114.12973623544853,30.460843750739155],[114.12973621287325,30.460843784425332],[114.12973619036174,30.46084381814345],[114.12973616791412,30.460843851893795],[114.12973614553071,30.460843885675917],[114.12973612321063,30.460843919490095],[114.12973610095511,30.46084395333588],[114.12973607876371,30.460843987213384],[114.12973605663619,30.460844021122607],[114.12973603457323,30.46084405506303],[114.12973601257461,30.460844089035064],[114.1297359906401,30.460844123038466],[114.12973596877005,30.460844157073083],[114.12973594696467,30.460844191138673],[114.12973592522376,30.46084422523536],[114.12973590354716,30.46084425936302],[114.1297358819357,30.46084429352155],[114.12973586038882,30.46084432771077],[114.12973583890616,30.4608443619308],[114.12973581748874,30.460844396181177],[114.12973579613644,30.460844430462082],[114.12973577484884,30.460844464773228],[114.12973575362625,30.460844499114945],[114.1297357324688,30.46084453348708],[114.12973571137626,30.46084456788871],[114.12973569034918,30.46084460232064],[114.12973566938712,30.460844636782692],[114.12973564849041,30.460844671274355],[114.12973562765896,30.460844705795804],[114.12973560689274,30.460844740346865],[114.12973558619225,30.46084477492731],[114.12973556555733,30.460844809537424],[114.12973554498811,30.460844844176926],[114.12973552448413,30.460844878845702],[114.1297355040462,30.460844913543514],[114.12973548367349,30.460844948270605],[114.12973546336686,30.46084498302645],[114.12973544312648,30.460845017811288],[114.12973542295134,30.460845052624883],[114.12973540284247,30.460845087467234],[114.12973538279986,30.460845122338068],[114.12973536282306,30.460845157237486],[114.1297353429121,30.460845192165376],[114.12973532306773,30.460845227121684],[114.1297353032894,30.460845262106304],[114.12973528357745,30.460845297118656],[114.12973526393154,30.46084533215931],[114.12973524435202,30.46084536722776],[114.12973522483944,30.46084540232411],[114.12973520539312,30.460845437448253],[114.12973518602747,30.460845472574082],[114.12973516672857,30.460845507727242],[114.12973514749625,30.460845542907798],[114.12973512832998,30.460845578115748],[114.12973510923099,30.460845613351097],[114.12973509019851,30.460845648613436],[114.12973507123286,30.460845683902832],[114.12973505233391,30.460845719219222],[114.12973503350169,30.460845754562495],[114.12973501473667,30.460845789932424],[114.12973499603847,30.46084582532906],[114.12973497740721,30.4608458607523],[114.12973495884313,30.460845896201963],[114.12973494034634,30.460845931677994],[114.12973492191625,30.460845967180454],[114.12973490355392,30.460846002708944],[114.12973488525832,30.460846038263522],[114.12973486703022,30.4608460738443],[114.12973484886962,30.46084610945076],[114.12973483077634,30.46084614508314],[114.12973481275034,30.460846180741147],[114.12973479479184,30.460846216424898],[114.12973477690122,30.460846252133997],[114.12973475907809,30.4608462878689],[114.12973474132248,30.46084632362852],[114.12973472363463,30.46084635941377],[114.12973470601439,30.460846395224028],[114.12973468846224,30.46084643105929],[114.12973467097771,30.4608464669195],[114.12973465356104,30.460846502804547],[114.12973463621256,30.460846538714367],[114.12973461893193,30.460846574648848],[114.12973460171918,30.46084661060794],[114.1297345845747,30.46084664659147],[114.12973456749836,30.46084668259943],[114.12973455049007,30.460846718631206],[114.12973453354985,30.4608467546877],[114.12973451667828,30.460846790767835],[114.12973449987447,30.460846826872007],[114.12973448313932,30.460846863000274],[114.12973446647291,30.460846899152124],[114.12973444987446,30.460846935327726],[114.1297344333448,30.460846971526685],[114.12973441688342,30.460847007749397],[114.1297344004907,30.46084704399535],[114.12973438416665,30.460847080264656],[114.12973436791145,30.46084711655698],[114.12973435172445,30.460847152872656],[114.12973433560668,30.460847189211066],[114.12973431955744,30.460847225572486],[114.12973430357685,30.46084726195675],[114.12973428766557,30.460847298363515],[114.1297342718232,30.460847334793126],[114.12973425604935,30.460847371245066],[114.12973424034494,30.46084740771928],[114.1297342247094,30.460847444215997],[114.12973420914298,30.460847480734877],[114.12973419364577,30.460847517275745],[114.12973417821766,30.460847553838715],[114.12973416285875,30.46084759042368],[114.12973414756908,30.46084762703023],[114.12973413234874,30.460847663658544],[114.12973411719773,30.460847700308626],[114.12973410211626,30.46084773698001],[114.12973408710438,30.460847773672988],[114.12973407216123,30.460847810387047],[114.1297340572882,30.460847847122526],[114.12973404248488,30.4608478838792],[114.12973402775042,30.460847920656498],[114.12973401308629,30.460847957455154],[114.12973399849137,30.46084799427439],[114.12973398396615,30.460848031114182],[114.12973396951094,30.460848067974663],[114.12973395512537,30.460848104855714],[114.12973394080963,30.46084814175727],[114.12973392656387,30.460848178679],[114.1297339123879,30.460848215620956],[114.12973389828205,30.46084825258308],[114.1297338842462,30.460848289565206],[114.12973387027971,30.460848326567326],[114.1297338563841,30.46084836358911],[114.12973384255785,30.46084840063072],[114.12973382880251,30.460848437691986],[114.12973381511672,30.460848474772686],[114.1297338015013,30.4608485118727],[114.12973378795623,30.460848548992203],[114.12973377448137,30.460848586130847],[114.12973376107654,30.460848623288587],[114.12973374774252,30.460848660465413],[114.12973373447873,30.460848697661216],[114.12973372128518,30.460848734875423],[114.12973370816232,30.460848772108548],[114.1297336951098,30.46084880936048],[114.12973368212799,30.460848846630757],[114.1297336692164,30.460848883919507],[114.12973365637585,30.4608489212266],[114.12973364360587,30.460848958551814],[114.12973363090637,30.46084899589504],[114.12973361827731,30.4608490332565],[114.12973360571931,30.460849070635625],[114.12973359323186,30.46084910803265],[114.12973358081558,30.46084914544757],[114.12973356846986,30.460849182879752],[114.12973355619496,30.46084922032972],[114.1297335439911,30.46084925779696],[114.12973353185824,30.460849295281747],[114.12973351979586,30.46084933278318],[114.12973350780496,30.460849370302277],[114.12973349588535,30.46084940783802],[114.12973348403617,30.460849445390746],[114.12973347225812,30.460849482960118],[114.1297334605517,30.460849520546475],[114.1297334489162,30.46084955814942],[114.1297334373516,30.460849595768664],[114.12973342585872,30.460849633404205],[114.12973341443677,30.460849671056167],[114.12973340308616,30.460849708724265],[114.12973339180684,30.46084974640838],[114.12973338059889,30.460849784108678],[114.12973336946253,30.460849821824652],[114.12973335839732,30.460849859556586],[114.12973334740369,30.460849897304204],[114.12973333648145,30.460849935067095],[114.12973332563045,30.460849972845722],[114.12973331485165,30.46085001063951],[114.12973330414376,30.460850048448865],[114.12973329350778,30.46085008627327],[114.12973328294329,30.460850124112376],[114.12973327245044,30.460850161966828],[114.12973326202902,30.460850199835985],[114.1297332516797,30.460850237719967],[114.12973324140182,30.46085027561877],[114.12973323119591,30.460850313531775],[114.12973322106194,30.46085035145926],[114.12973321099923,30.460850389401223],[114.12973320100902,30.460850427357677],[114.12973319109005,30.460850465327923],[114.12973318124291,30.46085050331214],[114.1297331714683,30.460850541310503],[114.12973316176529,30.460850579322493],[114.12975322379191,30.46086161535706],[114.12977616132954,30.460873949171205],[114.12978097330156,30.460876117722883],[114.1297855789253,30.460876853934355],[114.1297856144716,30.46087685528354],[114.12978565002072,30.46087685656814],[114.12978568557301,30.460876857789053],[114.12978572112847,30.46087685894514],[114.12978575668633,30.460876860037487],[114.12978579224712,30.46087686106541],[114.12978582781005,30.460876862029256],[114.12978586337594,30.460876862928792],[114.12978589894306,30.460876863764128],[114.12978593451278,30.46087686453516],[114.12978597008455,30.460876865242227],[114.12978600565755,30.46087686588481],[114.12978604123214,30.460876866463256],[114.12978607680833,30.460876866977337],[114.12978611238574,30.460876867427455],[114.1297861479644,30.46087686781309],[114.1297861835434,30.460876868134424],[114.12978621912363,30.460876868391786],[114.12978625470431,30.460876868584783],[114.12978629028534,30.46087686871336],[114.12978632586714,30.460876868777916],[114.1297863614486,30.46087686877817],[114.1297863970304,30.460876868714053],[114.12978643261184,30.46087686858586],[114.1297864681934,30.4608768683933],[114.12978650377416,30.460876868136612],[114.129786539354,30.46087686781561],[114.12978657493395,30.460876867430308],[114.1297866105122,30.46087686698081],[114.12978664608987,30.46087686646718],[114.12978668166595,30.460876865889077],[114.12978671724063,30.460876865247005],[114.12978675281389,30.460876864540456],[114.12978678838552,30.46087686376978],[114.12978682395556,30.460876862934906],[114.1297868595231,30.46087686203573],[114.12978689508869,30.46087686107242],[114.12978693065179,30.46087686004498],[114.12978696621283,30.460876858953004],[114.12978700177105,30.46087685779729],[114.12978703732665,30.460876856577222],[114.12978707287905,30.46087685529268],[114.12978710842886,30.460876853944168],[114.12978714397514,30.46087685253153],[114.12978717951754,30.460876851054586],[114.1297872150571,30.460876849513454],[114.12978725059259,30.460876847908246],[114.12978728612443,30.46087684623885],[114.12978732165263,30.46087684450515],[114.12978735717594,30.46087684270749],[114.1297873926955,30.460876840845696],[114.12978742821043,30.460876838919773],[114.12978746372067,30.460876836929835],[114.1297874992264,30.46087683487548],[114.12978753472699,30.460876832757446],[114.12978757022248,30.460876830574826],[114.12978760571296,30.46087682832836],[114.12978764119777,30.460876826017937],[114.12978767667724,30.460876823643215],[114.12978771215103,30.460876821204867],[114.12978774761879,30.46087681870205],[114.12978778308089,30.46087681613528],[114.12978781853651,30.460876813504544],[114.12978785398599,30.460876810809683],[114.12978788942935,30.46087680805103],[114.12978792486543,30.460876805228366],[114.12978796029505,30.460876802341623],[114.12978799571819,30.460876799390928],[114.1297880311336,30.460876796376272],[114.12978806654209,30.460876793297828],[114.12978810194332,30.460876790155375],[114.12978813733704,30.460876786949182],[114.12978817272283,30.460876783678696],[114.12978820810076,30.460876780344595],[114.12978824347076,30.460876776946705],[114.12978827883303,30.46087677348486],[114.12978831418646,30.46087676995888],[114.1297883495317,30.460876766369523],[114.12978838486843,30.460876762716147],[114.12978842019642,30.460876758999042],[114.12978845551554,30.460876755218266],[114.12978849082513,30.460876751373757],[114.1297885261262,30.460876747465125],[114.12978856141741,30.46087674349305],[114.12978859669919,30.460876739457188],[114.12978863197142,30.46087673535777],[114.12978866723412,30.46087673119456],[114.12978870248652,30.46087672696769],[114.12978873772877,30.460876722677316],[114.1297887729606,30.460876718323266],[114.12978880818231,30.46087671390567],[114.12978884339336,30.460876709424394],[114.1297888785935,30.46087670487962],[114.12978891378265,30.46087670027118],[114.12978894896122,30.460876695599353],[114.12978898412845,30.460876690864083],[114.12978901928432,30.460876686065202],[114.12978905442861,30.460876681202933],[114.12978908956133,30.460876676277223],[114.12978912468225,30.460876671287902],[114.12978915979146,30.46087666623554],[114.12978919488796,30.46087666111962],[114.12978922997254,30.46087665594037],[114.12978926504498,30.460876650697795],[114.12978930010493,30.460876645392066],[114.1297893351516,30.46087664002295],[114.12978937419358,30.46087663397295],[114.12978941322308,30.46087662786042],[114.12978945223941,30.460876621685756],[114.12978949124259,30.460876615448335],[114.12978953023236,30.460876609148553],[114.12978956920928,30.46087660278641],[114.12978960817203,30.460876596361455],[114.12978964712163,30.460876589874424],[114.12978968605736,30.460876583324918],[114.12978972497925,30.460876576712998],[114.12978976388683,30.46087657003895],[114.12978980278011,30.460876563302367],[114.12978984165954,30.4608765565036],[114.12978988052447,30.46087654964264],[114.12978991937462,30.460876542719213],[114.12978995821034,30.46087653573377],[114.12978999703064,30.460876528685905],[114.1297900358363,30.460876521575972],[114.12979007462695,30.46087651440396],[114.12979011340265,30.46087650716982],[114.12979015216257,30.460876499873667],[114.12979019090714,30.460876492515325],[114.12979022963594,30.46087648509485],[114.12979026834911,30.460876477612363],[114.12979030704648,30.46087647006803],[114.12979034572749,30.460876462461563],[114.1297903843925,30.460876454793254],[114.1297904230413,30.46087644706282],[114.12979046167351,30.460876439270706],[114.12979050028937,30.46087643141681],[114.12979053888834,30.460876423500892],[114.12979057747062,30.46087641552313],[114.12979061603622,30.460876407483585],[114.12979065458444,30.46087639938242],[114.12979069311541,30.460876391219415],[114.12979073162923,30.460876382994787],[114.12979077012558,30.460876374708324],[114.12979080860411,30.460876366360406],[114.12979084706505,30.460876357950653],[114.12979088550806,30.460876349479506],[114.12979092393347,30.46087634094669],[114.12979096234015,30.460876332352537],[114.12979100072882,30.460876323696432],[114.12979103909929,30.46087631497928],[114.12979107745082,30.460876306200568],[114.12979111578417,30.4608762973603],[114.12979115409813,30.46087628845887],[114.12979119239381,30.46087627949588],[114.1297912306703,30.460876270471562],[114.12979126892753,30.460876261385913],[114.1297913071651,30.46087625223939],[114.12979134538395,30.460876243031255],[114.12979138358304,30.46087623376201],[114.12979142176216,30.460876224431782],[114.12979145992153,30.460876215039992],[114.12979149806117,30.46087620558739],[114.12979153618058,30.460876196073745],[114.12979157427958,30.460876186498993],[114.12979161235812,30.460876176863255],[114.12979165041672,30.460876167166642],[114.12979168845465,30.460876157408755],[114.12979172647147,30.460876147590113],[114.12979176446763,30.460876137710876],[114.12979180244315,30.46087612777043],[114.12979184039673,30.460876117769505],[114.1297918783297,30.460876107707364],[114.12979191624153,30.46087609758481],[114.12979195413146,30.46087608740132],[114.12979199199971,30.460876077157472],[114.12979202984606,30.460876066852865],[114.12979206767073,30.4608760564875],[114.12979210547329,30.460876046061774],[114.12979214325381,30.460876035575627],[114.12979218101198,30.46087602502861],[114.12979221874812,30.460876014421178],[114.12979225646112,30.46087600375367],[114.12979229415174,30.460875993025628],[114.12979233181957,30.460875982236885],[114.12979236946448,30.46087597138824],[114.12979240708634,30.460875960479346],[114.12979244468502,30.460875949509983],[114.12979248226026,30.460875938480484],[114.12979251981221,30.460875927390685],[114.12979255734054,30.460875916240976],[114.12979259484527,30.460875905031084],[114.1297926323263,30.460875893761173],[114.12979936405739,30.46087406401716],[114.12981050573391,30.4608710673506],[114.12981750149231,30.460869285879166],[114.1298209604681,30.460868241871395],[114.12976090092499,30.460837087976408],[114.12975434062045,30.460834915065185],[114.12975222187758,30.46083451237822],[114.12974981793391,30.46083446168812]]],[[[114.12858684724688,30.46075697531846],[114.12858272338447,30.46075743943942],[114.12856332046812,30.460763483455032],[114.12855289699057,30.46076738027048],[114.12853600009338,30.460774064010007],[114.12852229595235,30.46077968152313],[114.12851351920625,30.460783672797096],[114.12850361728512,30.460788535310336],[114.12848832696787,30.460796685018366],[114.12847775624438,30.460803263768174],[114.12846849232196,30.460809810470042],[114.12844572067664,30.460827796871854],[114.1284405291799,30.460831926860674],[114.12842717045298,30.46084610269508],[114.1284102909892,30.460866749129064],[114.12839123126408,30.460890499948565],[114.12837310477931,30.460916179875024],[114.12831646060333,30.46100674187852],[114.12827962900336,30.46106619288501],[114.12783498522268,30.461781744044416],[114.12771556983981,30.46197223706339],[114.12772665457429,30.461977474726904],[114.1280351192241,30.4614916579163],[114.1281772411588,30.4612674655686],[114.12838529925469,30.460926758240657],[114.1284034236594,30.460900829056214],[114.12842208216752,30.46087728462714],[114.12843863233869,30.46085665841345],[114.12845199105928,30.46084248257259],[114.12845718255365,30.460838352580964],[114.12847995418937,30.460820366167756],[114.12848921810793,30.46081381946155],[114.12849904775334,30.460807893775957],[114.1285159434435,30.460799051729605],[114.12852557378872,30.46079447206878],[114.12853447095468,30.460790640385696],[114.1285488969296,30.46078481189138],[114.12856651522796,30.460777992556775],[114.12857545535876,30.46077498362497],[114.12858763885949,30.460771261756314],[114.1285911701126,30.460769197273464],[114.12859354918935,30.460766813080873],[114.12859446803948,30.460765006645683],[114.12859511201303,30.46076272898739],[114.12859483361342,30.46076096885153],[114.12859336738642,30.460758933004715],[114.1285896597264,30.460757212223108],[114.12858684724688,30.46075697531846]]],[[[114.13009347108097,30.461014626013245],[114.13008886700948,30.461023870675692],[114.13008728439782,30.461027226496423],[114.1300859300475,30.461032379433217],[114.13008647117458,30.46103554809189],[114.13008735739318,30.461037930198106],[114.13008820688349,30.461039436519634],[114.1300892110733,30.46104098218373],[114.13009079189736,30.46104235056373],[114.130091999074,30.461043350240864],[114.13009359871769,30.461044430354065],[114.13009820163818,30.46104655701199],[114.13010687464767,30.46105055553842],[114.13012176428953,30.461057634833466],[114.13014504672978,30.46106834589903],[114.13016890625546,30.461079574262442],[114.13022792804726,30.461107473566233],[114.13024125286185,30.461085677970928],[114.13024127342416,30.461085494052796],[114.13024126084613,30.461085360130177],[114.1302412091085,30.46108524925028],[114.1302410730141,30.461085093726478],[114.13018302704694,30.46105712389051],[114.13015916753793,30.461045895541805],[114.13012760802397,30.46103057074982],[114.13011271838874,30.461023491458835],[114.13009347108097,30.461014626013245]]]]}},{"type":"Feature","id":"7","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12897724043302,30.459732524015177],[114.12891906233625,30.459825264551682],[114.12890990575438,30.45983986082598],[114.12889960140261,30.459856286714437],[114.12889000609071,30.459871582326734],[114.12888026176522,30.45988711546247],[114.12864624354339,30.460260301463716]]]}},{"type":"Feature","id":"8","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864239023739,30.460258399832206],[114.12887584219082,30.459885109720673],[114.12888531735717,30.45986997481169],[114.1288949542723,30.459854581522617],[114.12890533748563,30.459837996136145],[114.12891431475633,30.45982365648563],[114.1289726900806,30.45973041160152]]]}},{"type":"Feature","id":"9","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12798139225067,30.461314706510002],[114.12818869480081,30.460981353806087],[114.12826611586888,30.460858573455294],[114.12828593157752,30.460826843153637],[114.12830427901403,30.4607973730555],[114.1283271156673,30.46076105246407],[114.12835723198545,30.460712713413564],[114.1283905424493,30.460659501312453],[114.128417691069,30.46061609233018]]]}},{"type":"Feature","id":"133","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865536915062,30.460575768862906],[114.12860099077626,30.46055000215731]]]}},{"type":"Feature","id":"10","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842232979635,30.460618247013066],[114.12839561301983,30.460660968957175],[114.12836230256465,30.460714181065516],[114.1283321862545,30.460762520122845],[114.12830934960708,30.46079884071929],[114.12829100217542,30.46082831082157],[114.12827118647216,30.460860041127475],[114.12819326150827,30.460983494816098],[114.12798675932302,30.461316769922696]]]}},{"type":"Feature","id":"11","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.1276263870675,30.459859579177976],[114.12765470102313,30.459872401753966],[114.1277611168518,30.45992200439],[114.12785763040516,30.459967328638495],[114.12795992499306,30.460015354753946],[114.12806337194627,30.460064469260022],[114.1281687176785,30.46011402039716],[114.12817873041107,30.460118769600683],[114.12819169404607,30.460124230705205],[114.12820708953562,30.460130031899652],[114.12821613630753,30.460132992597295],[114.12823052993473,30.460137166900232],[114.12824385853544,30.460140452682115],[114.1282629296385,30.46014492434246],[114.12828147269322,30.460148266252308],[114.12829512641986,30.460150737996006],[114.12830596819813,30.46015236546864],[114.12832484330475,30.460153946306104],[114.12834015915607,30.460155009855193],[114.12835000355373,30.46015485717896],[114.1283672902542,30.46015497010353],[114.12838643674475,30.460154021808915],[114.12842096859087,30.46015197565487],[114.12845680056239,30.46014888906762],[114.1284761476081,30.460144326962837],[114.12849573392417,30.460138975734004],[114.1285041216086,30.460136520265024],[114.12851533122597,30.460132817087857],[114.12857414466598,30.460105119057804],[114.12859590035085,30.46009128145205],[114.12861675115045,30.460076494569353],[114.12863473366819,30.46006165603471],[114.12865044714968,30.460046543068422],[114.12867676973335,30.46001821876738],[114.12870434742325,30.45998209755369],[114.12871540287891,30.45996827591854],[114.1287635441821,30.459891209799093],[114.1288325033461,30.45977921371121],[114.12931873755855,30.45899917261138]]]}},{"type":"Feature","id":"12","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.1281739457557,30.460499207358286],[114.12816786865055,30.460490635424566],[114.12816119187117,30.460480896152717],[114.12814380378107,30.46045903197977],[114.1281301141407,30.46044429121061],[114.12811888705954,30.460433102565368],[114.12810904456339,30.460423954825124],[114.12810046818328,30.460415925371965],[114.12808968907709,30.46040718178407],[114.12808045375098,30.460400365201558],[114.12806850516638,30.460391461552604],[114.12805994843728,30.46038524566546],[114.12804986520692,30.460378997479467],[114.12803683191065,30.460371297683672],[114.12802338123912,30.460365077468524],[114.12800764492603,30.46035778052335],[114.1279762006981,30.460342925602617],[114.12792388558171,30.460318337642654],[114.12785992698542,30.460288262105767],[114.12777807934471,30.46024987852015],[114.12763956850337,30.46018455884436],[114.12756786932079,30.4601508907607],[114.1275107233993,30.46012411581605],[114.12748347584443,30.46011106306988],[114.12746972809653,30.460103979285517],[114.1274417805846,30.460089278917057],[114.12740655657191,30.46007005834914],[114.12737168965059,30.460051061792136],[114.12732726914756,30.460026883341335],[114.12728229494306,30.46000244951703],[114.12721682417789,30.459966837594138],[114.12715307732698,30.459931965645627],[114.12707893531862,30.4598919027675],[114.12706085704859,30.459881885428633],[114.12703969926034,30.459871640420573],[114.127018713248,30.459861561408193],[114.12698511844061,30.4598455951542],[114.12695360451545,30.45983060210644],[114.12689695521986,30.45980386328977],[114.12685392296001,30.459783709573067],[114.12677732862484,30.459747526781303],[114.12673059734941,30.459725228642583],[114.12670908584316,30.459715133114983]]]}},{"type":"Feature","id":"13","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.1300794624898,30.461044218499723],[114.13009057474109,30.461049720771378],[114.13009996732531,30.46105433885233],[114.13013979351264,30.461072768465435],[114.13032645466019,30.461160692824794],[114.13049466355547,30.461239693644842],[114.13058393128345,30.461281768620577]]]}},{"type":"Feature","id":"14","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12882277886841,30.460453322501046],[114.12881787573671,30.46044796947164],[114.12881158715328,30.460441153762385],[114.1288067493305,30.460433949312808],[114.1288001539798,30.46042339124335],[114.12879200707901,30.460408797342957],[114.12879030510373,30.46040557140097],[114.12877706273885,30.460376546593384],[114.12876098693141,30.460327242497126],[114.12876040839092,30.460323767807377],[114.12875876209284,30.46031388020817],[114.12875749610893,30.460306276759738],[114.12875726121462,30.460304281487893],[114.12875671862989,30.460299672590363],[114.12875557087689,30.46027086584409]]]}},{"type":"Feature","id":"15","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12949925637302,30.45908381036229],[114.12906863093419,30.4597741843955],[114.12891042032965,30.460030184134702],[114.12889413422447,30.460063075917926],[114.12887563750385,30.460103438899957],[114.12886082796645,30.46014235388471],[114.12885542999538,30.46015957257983],[114.12884703119313,30.46020354754154],[114.12884559342868,30.46023920152125],[114.12885026874397,30.46028359561936],[114.12885975256741,30.46032134141008],[114.12886846975947,30.46034453853017],[114.12888687051543,30.460381603499457],[114.12890777512125,30.460412498531188],[114.12892180163672,30.46042897047228],[114.12893971038247,30.460446944158406],[114.12895523364386,30.46046147406139],[114.12897886427122,30.460479812759715],[114.12899464865654,30.460491584118675],[114.12902440946912,30.460509604536686],[114.12907435794124,30.460534409868426],[114.12946898303335,30.460720122328183],[114.12951775750942,30.460744631244008],[114.12957155440026,30.46077032952545]]]}},{"type":"Feature","id":"16","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12832699736397,30.46022001983161],[114.12836404472645,30.460224633610444],[114.12837302917117,30.46022523007898],[114.12840022780429,30.460226284750153],[114.12842428170627,30.460225607748736],[114.12846068171436,30.46022255064311],[114.12848080370732,30.460219963867853],[114.12851469333377,30.46021348906026],[114.1285323730125,30.46020906884395],[114.1285457397978,30.460205305973776],[114.12857029988476,30.460196571231123],[114.1285571368483,30.460216067679987],[114.12854918983022,30.460227838404215],[114.12854690634074,30.460231220592163],[114.12853544237832,30.460248200417247],[114.12851944887736,30.460272354453615],[114.12851218636042,30.460281863050504],[114.12850612224015,30.46028710502764],[114.12849910512165,30.46028920664694],[114.12848869445502,30.460290794092067],[114.12848303613814,30.460290623268772],[114.12847819403144,30.460289804892017],[114.12843554363837,30.460270462042168],[114.1284132054234,30.460260081520367]]]}},{"type":"Feature","id":"17","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12834021985782,30.460376936480415],[114.12810549252377,30.46026661249821],[114.12793108680269,30.46018447827835],[114.12779346360745,30.460119746592724],[114.12766184114018,30.460057657953552],[114.12740989311544,30.459939040044063],[114.1272249764619,30.459851996295452],[114.12711248460293,30.459799160175752],[114.1270374599439,30.45976392176662],[114.12688333686737,30.459691203549152],[114.12683064376942,30.45966633457376],[114.12675626234494,30.459631199461516]]]}},{"type":"Feature","id":"18","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875557087689,30.46027086584409],[114.12873789098559,30.460298886010346],[114.12871981540793,30.460327731040778],[114.1287182902953,30.460330156172322],[114.12869807501215,30.4603630245161],[114.12869614675628,30.460371470071735],[114.12869585169376,30.46037890808945],[114.1286974221503,30.46038724711732],[114.1286994153434,30.460391301317557],[114.12870328283621,30.460395533591093],[114.12870918351338,30.460399367437283],[114.12871169761004,30.460401000920825],[114.1287744345245,30.460430621610467],[114.12878720135707,30.460436649346487],[114.12882277886841,30.460453322501046]]]}},{"type":"Feature","id":"19","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12882277886841,30.460453322501046],[114.12885346434857,30.460467799461636]]]}},{"type":"Feature","id":"20","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.13051485347947,30.461400366055145],[114.13047468454016,30.46138144875007],[114.13036160436964,30.46132802805536],[114.1302430504507,30.46127204691718],[114.1300838515681,30.46119719434247],[114.1299357044339,30.461127711511185],[114.12985598514405,30.461090321932144],[114.12967374660232,30.46100455302151],[114.1295007516756,30.460921568803485],[114.12932234861319,30.460836796072016],[114.12920795046435,30.460783250131836],[114.12909064982529,30.46072760238839],[114.12890469941287,30.460639511974996],[114.12871283204083,30.46054890605957]]]}},{"type":"Feature","id":"21","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.13046693610107,30.461482633843357],[114.13038606822798,30.46144436951979],[114.13027501012286,30.4613921366407],[114.13018847763566,30.46135108250479],[114.13007930919306,30.46129958315995],[114.12999872539311,30.461261874120172],[114.12994170529474,30.461235206755003],[114.12990102253175,30.46121585668478],[114.12988478386872,30.461209425370157],[114.12985747674861,30.461202030021965],[114.1297774932942,30.46116970927307],[114.12964453600448,30.46111622700083],[114.129524958337,30.46106744642272],[114.12948286936863,30.461050330200425],[114.1294301436001,30.46102565562422],[114.12934054948825,30.460983694989807],[114.12922067006116,30.460927076570584],[114.12911757403806,30.460878867417346],[114.12896104638719,30.460804857790933],[114.1289304419777,30.46079080664426],[114.12890193910415,30.4607777289812],[114.12885190818976,30.460757512192895],[114.12884261314063,30.46075451942377],[114.12883176802906,30.460750880498],[114.12881766336771,30.46074713662746],[114.1288054216991,30.460744352218846],[114.12878830599986,30.46074088750907],[114.12876715087545,30.460737337447174],[114.12874513289626,30.46073415702747],[114.12873048194281,30.46073274695193],[114.1287122944105,30.460731821427643],[114.12869397199555,30.460732309292833],[114.12867794830335,30.460733719129816]]]}},{"type":"Feature","id":"22","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12872278820035,30.460511370529066],[114.1289441332922,30.46061511953031],[114.12907372630534,30.46067607587177],[114.12915700261074,30.460715585767986],[114.12941289814538,30.46083589256067],[114.12948970387153,30.46087208950104],[114.12967491583197,30.460959248519227],[114.12984005696497,30.461036682635438],[114.13001956598467,30.461121247154537],[114.13002864825172,30.461125519620687],[114.13023762396803,30.461223825032437],[114.13049887038697,30.461346686944687],[114.13053596428729,30.461364121595935]]]}},{"type":"Feature","id":"23","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12744830877914,30.459787106092207],[114.127409509125,30.459771833036406],[114.12734184036161,30.459744943040576],[114.12727440106164,30.459718373439795],[114.12724792645402,30.459707664153637],[114.12720629223848,30.459690887389243],[114.12719205714096,30.459684830502855],[114.1271733457091,30.459676567893943],[114.12712578434419,30.45965425294694],[114.12701691017116,30.45960300772696],[114.126899589828,30.459548137001413],[114.12682318366205,30.459512136995478]]]}},{"type":"Feature","id":"24","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12677639632612,30.459595378268002],[114.1268063691351,30.45960957564021],[114.1269081789495,30.45965724416581],[114.12698359383919,30.459692526512907],[114.1270627904483,30.459729954529234],[114.12713925938512,30.459766133737045],[114.12714094893008,30.459766925349616],[114.12719342297875,30.459791511290355],[114.12725858217806,30.45982214625411],[114.12735578395005,30.45986815464524],[114.12745253202482,30.459913323126912],[114.12752961389185,30.459949829796287],[114.12760816252492,30.45998662887781],[114.12771488077304,30.460036914687965],[114.12782355844776,30.46008840517057],[114.12790583405823,30.460127068267425],[114.12796595398729,30.46015515907547],[114.12807572841606,30.460206779878195],[114.12819454217355,30.460262668735545],[114.12826353994915,30.46029521337293],[114.12833443017455,30.46032877206684],[114.12836213336487,30.46034185152361]]]}},{"type":"Feature","id":"25","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.1275157981083,30.46187798662634],[114.12797076401056,30.461152091581972],[114.12816304230289,30.460845174464335],[114.12818825060478,30.460800740137387],[114.12820316339932,30.46077096459124],[114.12821272159984,30.46074629342443],[114.12821752572157,30.460728878483152],[114.12822172932809,30.46071246440057],[114.12822433156069,30.46069344808538],[114.12822473618087,30.460681806547388],[114.12822444614423,30.460664208945],[114.12822246300462,30.460643601869652],[114.12821906341146,30.460622894878266],[114.12820991919551,30.46058905852358]]]}},{"type":"Feature","id":"26","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12859695125248,30.460749296000518],[114.1285641230873,30.460757102698334],[114.12854010247865,30.46076470922441],[114.1285110831788,30.46077431491476],[114.12848550415981,30.460785412429615],[114.1284664392788,30.460795735843927],[114.12843821506361,30.46081595318955],[114.12841409692503,30.460836788640815],[114.12839077436152,30.4608625932047],[114.12837556130937,30.46087940763076],[114.1283566115678,30.46090253222412],[114.12827917639183,30.461023774191535],[114.12769151696821,30.46196088920321]]]}},{"type":"Feature","id":"27","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864842897787,30.4606276302832],[114.12862898061579,30.460618618930347],[114.12862658288563,30.460617500482353],[114.12858066923444,30.460596083515824],[114.1285753203815,30.460594229469987],[114.12857167112699,30.460593593058658],[114.12856745920931,30.460593720536774],[114.12856122741601,30.460594698374464],[114.12855772059048,30.460595941281277],[114.1285522979939,30.460598027726796],[114.1285483901662,30.460600191291086],[114.12854500787789,30.460602149465135],[114.12854230576485,30.460604654237496],[114.12854039690191,30.460607392273527],[114.1285193386141,30.460639796030616],[114.1285079053109,30.46065799662477],[114.12850706592427,30.460659350460933],[114.12849941394543,30.460671692237295],[114.12849141514032,30.460684593394582],[114.12847872288397,30.460705064530792]]]}},{"type":"Feature","id":"28","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12847872288397,30.460705064530792],[114.1284732864388,30.460713834476344]]]}},{"type":"Feature","id":"29","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12823116917164,30.460430565779617],[114.12823513363749,30.460435047164694],[114.1282410985911,30.460442480098674],[114.12824699456962,30.460450849727245],[114.12825612343089,30.460464853507645],[114.1282621451196,30.460475318598228],[114.12827004285334,30.460490559238714],[114.12827622553161,30.460502624397915],[114.12828152820744,30.460515645183932],[114.12828729169814,30.460530317484707],[114.12829185294262,30.46054275428451],[114.12829655198448,30.46055864893324],[114.12830072615508,30.460573305664308],[114.12830350703699,30.460586218995534],[114.1283057058045,30.460595944723526],[114.12830651401676,30.460604188710853],[114.12829868477651,30.46062353496956],[114.12831145944958,30.460602786826506],[114.12832884115144,30.460574821538472],[114.1283379454297,30.460560173714523],[114.12836063323996,30.46052378574931],[114.12836376369641,30.460518006512462],[114.12836459736296,30.46051508338462],[114.12836541369053,30.460512042620753],[114.12836514161545,30.46050926750707],[114.12836475538597,30.460505777343094],[114.12836374082613,30.460502454308315],[114.12836197313365,30.460498592147694],[114.12836034108288,30.460496131066375],[114.12835756235341,30.46049308645533],[114.12835458131094,30.460490208959772],[114.12835106750626,30.46048742844768],[114.12834542677811,30.460484539817884],[114.12832747170351,30.46047566264443],[114.12828954958354,30.46045806260415],[114.12824607343725,30.46043758783169],[114.12819295080001,30.46041255940355]]]}},{"type":"Feature","id":"134","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12860622058159,30.46054207521988],[114.12866069424561,30.46056778078176]]]}},{"type":"Feature","id":"135","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12868075434154,30.460536668227384],[114.1286261815418,30.46051044643603]]]}},{"type":"Feature","id":"30","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12912278314475,30.46085068839886],[114.12904124105474,30.460812270419204],[114.12897783505122,30.460782561304338],[114.12889343028047,30.460742891641594],[114.128827187441,30.460711760087573],[114.12876201360413,30.460681020788293],[114.12871871350927,30.460660873763988],[114.12871540763784,30.46065931028169],[114.12871156753305,30.460659360647206],[114.12870155760558,30.460659491930894],[114.1286930532053,30.460660015850845],[114.12868308922111,30.460660707382488],[114.12867346315322,30.460661452813614],[114.12866670776984,30.460661975939868],[114.12866189334268,30.460662264222407],[114.12865149721733,30.46066288672742],[114.1286501161541,30.460662969423137],[114.12863837299199,30.460664351103055],[114.12862116348589,30.460665965873723],[114.12860621810181,30.460667869126482],[114.12858955378167,30.46066992547661],[114.12858022522573,30.460671950374874],[114.12857584464733,30.460672901241058],[114.1285661300287,30.46067430161067],[114.12855794056122,30.460675779314688],[114.12854994047656,30.46067753164059],[114.12853872035802,30.460680668239995],[114.1285338333218,30.460682142372615],[114.1285263397709,30.46068440273543],[114.12851723573179,30.46068725357128],[114.12851419616699,30.460688205378652],[114.12850163985091,30.460692973529728],[114.1285007257757,30.460693399910237],[114.12848441607913,30.46070100774001],[114.12847872288397,30.460705064530792]]]}},{"type":"Feature","id":"31","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12914007973365,30.460822483792516],[114.12887456443299,30.460697201305514],[114.1286858024831,30.46060822496752],[114.12867798025998,30.460604378204753]]]}},{"type":"Feature","id":"32","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12915765731448,30.460794860092545],[114.12869540616136,30.460576642138296]]]}},{"type":"Feature","id":"33","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12773503437458,30.460011515042158],[114.12781700454225,30.460050325325824],[114.12791906931785,30.460098400883528],[114.12801287260986,30.46014223287044],[114.12809008867563,30.460178722541972],[114.12818780640409,30.46022476554824],[114.12828835912829,30.460272275342994],[114.1283586035856,30.46030481900877],[114.12837928019152,30.460314398279753]]]}},{"type":"Feature","id":"34","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.1277518461424,30.459984503356356],[114.12794365282777,30.460075007194664],[114.1279982520951,30.460100716895834],[114.12806983858272,30.46013429109269],[114.1281313933395,30.460163404118905],[114.12819844034753,30.460195149932535],[114.12826302682811,30.460225400211414],[114.12832956263576,30.460257207017207],[114.1283852969419,30.460283165363396],[114.1283957314117,30.46028805870498]]]}},{"type":"Feature","id":"35","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12776888004775,30.459957134711782],[114.12776927905365,30.459957321868618],[114.12783777064836,30.459989793855886],[114.12785845211445,30.459999252293215],[114.12793323694133,30.460034680685403],[114.1280053462846,30.460068500674236],[114.12808125053584,30.460104367246164],[114.12813446219064,30.460129551143037],[114.12820666640502,30.460163309393074],[114.12828587593559,30.460200544213052],[114.12832699736397,30.46022001983161]]]}},{"type":"Feature","id":"36","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12832699736397,30.46022001983161],[114.1283270929556,30.460220065105005]]]}},{"type":"Feature","id":"37","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12884403956247,30.460007641707215],[114.12867760736032,30.46027479966971]]]}},{"type":"Feature","id":"38","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12887150044769,30.46001997208708],[114.12870731410098,30.46028872831528]]]}},{"type":"Feature","id":"39","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12818014087874,30.460874810459718],[114.12822144234136,30.460808716806646],[114.12824696465553,30.460767995861808],[114.12827292522499,30.460726389131928],[114.128304829952,30.46067542079121],[114.12833107999349,30.46063331293882],[114.12835891643661,30.460588791534825]]]}},{"type":"Feature","id":"40","properties":{"type":1},"geometry":{"type":"MultiLineString","coordinates":[[[114.12821012605546,30.46088886842086],[114.1282412682786,30.460839424206632],[114.12826592705372,30.46080002751735],[114.12829868801084,30.46074734443058],[114.1283396266124,30.460681891115996],[114.12836003370703,30.460649206631423],[114.12838906067488,30.46060279353459]]]}},{"type":"Feature","id":"41","properties":{"type":4},"geometry":{"type":"MultiLineString","coordinates":[[[114.12791250027578,30.461436033512047],[114.12798675932302,30.461316769922696]]]}},{"type":"Feature","id":"42","properties":{"type":4},"geometry":{"type":"MultiLineString","coordinates":[[[114.12798139225067,30.461314706510002],[114.12790721558684,30.461432066904347]]]}},{"type":"Feature","id":"43","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12791250027578,30.461436033512047],[114.12760695187441,30.461921826983442]]]}},{"type":"Feature","id":"44","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12790721558684,30.461432066904347],[114.12760124697984,30.46191922475083]]]}},{"type":"Feature","id":"45","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12802134345617,30.461185257079794],[114.12757284979007,30.461904903192504]]]}},{"type":"Feature","id":"46","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12799359944563,30.461170059048992],[114.12754318292802,30.461890906595524]]]}},{"type":"Feature","id":"47","properties":{"type":4},"geometry":{"type":"MultiLineString","coordinates":[[[114.1289726900806,30.45973041160152],[114.12905084067081,30.459610143655393],[114.12912860670679,30.459484907374254]]]}},{"type":"Feature","id":"48","properties":{"type":4},"geometry":{"type":"MultiLineString","coordinates":[[[114.12897724043302,30.459732524015177],[114.12908987415992,30.459554495911952],[114.12913176009636,30.45948693795003]]]}},{"type":"Feature","id":"49","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12912860670679,30.459484907374254],[114.12918860887022,30.459388703669106],[114.12940692117314,30.459040354800283]]]}},{"type":"Feature","id":"50","properties":{"type":3},"geometry":{"type":"MultiLineString","coordinates":[[[114.12913176009636,30.45948693795003],[114.1292186688281,30.459347626355402],[114.12928769388098,30.459236272889843],[114.1294088728476,30.45904135565898]]]}},{"type":"Feature","id":"51","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12900740340355,30.45974525957922],[114.12930185603157,30.459273004403947],[114.12943776550892,30.459054989774348]]]}},{"type":"Feature","id":"52","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12903468416134,30.459759191112973],[114.12910643837142,30.459647825985254],[114.12924000296428,30.459433141795156],[114.12939441460196,30.459185760032476],[114.12946726576742,30.45906881136676]]]}},{"type":"Feature","id":"53","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12679204348328,30.459567539767527],[114.1268360594006,30.459588194797206],[114.12694201630637,30.4596379537218],[114.1269954117388,30.459663315435186],[114.12705192223714,30.45968981652831],[114.12710391907243,30.459714318445624],[114.1271579633486,30.459739595227425],[114.12716006239806,30.459740576961867],[114.12720414426339,30.459761419895838],[114.1272685192033,30.45979186681696],[114.12737430459964,30.45984175550143],[114.12743273349024,30.459869432945784],[114.12748623372657,30.459894380831127],[114.12754058206845,30.459919815635516],[114.12759280774002,30.45994463706296],[114.12765154108888,30.459972194132174],[114.12773503437458,30.460011515042158]]]}},{"type":"Feature","id":"54","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12830640567694,30.46043107504182],[114.12818935621225,30.46037596750847],[114.12808795522145,30.460328313054408],[114.12798911993617,30.460281740825522],[114.1278981990045,30.46023882670201],[114.12772954924898,30.46015951002228],[114.12707926064358,30.459852396534487],[114.12672577159651,30.45968544680535]]]}},{"type":"Feature","id":"55","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12819295080001,30.46041255940355],[114.128150914052,30.46039288912343],[114.12805481170027,30.46034773709655],[114.12795560218164,30.460301165830813],[114.12782851745932,30.460241330062537],[114.12771865053372,30.46018999482562],[114.12760991752974,30.460138601911694],[114.12750038561464,30.460087155502507],[114.12739544994673,30.460037709331758],[114.12729047372582,30.459988216739237],[114.1271836077708,30.45993795295971],[114.12710503081357,30.459901016565624],[114.12706085704825,30.459881885428633]]]}},{"type":"Feature","id":"56","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12885346434857,30.460467799461636],[114.12888611478475,30.46048330114584],[114.12894212166319,30.460509498511577],[114.12899770191459,30.46053571380165],[114.12906103846264,30.46056530411545],[114.12933228326845,30.460692990021055],[114.12939922183315,30.460724450600186],[114.12945219117499,30.460749319804087],[114.1294949523801,30.460769381987685],[114.1295646720375,30.46080237116679],[114.12961044647972,30.460824201627865],[114.12966760787087,30.460851026077375],[114.12972489851822,30.460877690747047],[114.1297846468849,30.46090557979937],[114.12983406031677,30.460929092262713],[114.12989603575154,30.46095827285938],[114.12994965349259,30.46098332605729],[114.13000503482809,30.461009534879153],[114.13001051246,30.46101195061097],[114.1300794624898,30.461044218499893]]]}},{"type":"Feature","id":"57","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12988478386872,30.461209425370157],[114.12983517790201,30.461185033927062],[114.12980747191082,30.461172091385784],[114.12976235278241,30.461150951652858],[114.12965581771897,30.46110102144966],[114.12954634172344,30.461049675638765],[114.1294427140344,30.46100085224905],[114.12932650533043,30.46094627831662],[114.12921344859751,30.46089333997407],[114.12912278314475,30.46085068839886]]]}},{"type":"Feature","id":"58","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12857813510907,30.460244863218666],[114.12934615820019,30.459012029066628]]]}},{"type":"Feature","id":"59","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12861197266949,30.46025280528113],[114.12937586387014,30.45902595685755]]]}},{"type":"Feature","id":"60","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12744830877914,30.459787106092207],[114.12746862128559,30.459794978424974],[114.12748735101675,30.45980260974082],[114.12759346209813,30.45984560350452],[114.12761138449859,30.459853479228062],[114.1276263870675,30.459859579177976]]]}},{"type":"Feature","id":"61","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12776888004775,30.459957134711782],[114.12774231094184,30.459944672249055],[114.12763228066444,30.45989288214787],[114.12757785286539,30.459867328602357],[114.12745358071072,30.459808632927086],[114.12741197479716,30.45978912384476],[114.12735687524035,30.45976307747049],[114.12730078342108,30.459736847572746],[114.12725003300609,30.459713085134545],[114.12723471844387,30.459705922914285],[114.1271920571413,30.45968483050285]]]}},{"type":"Feature","id":"62","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12957155440026,30.46077032952545],[114.12962560879784,30.46079536052207],[114.12969807096734,30.460830190404653],[114.12973316176529,30.460850579322493],[114.12977092851051,30.460870448151194],[114.1298018736042,30.460887031239576],[114.12983899187161,30.46090765686766],[114.12988703308896,30.46093287850678],[114.12991570697065,30.460952002731872],[114.12994598007576,30.460972153130776],[114.12998268083776,30.46099194239249],[114.13001781280236,30.461010661152926],[114.1300438771927,30.461024660365503],[114.13006143062687,30.461034447929116],[114.1300794624898,30.461044218499723]]]}},{"type":"Feature","id":"63","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.1304828959087,30.461455232968806],[114.13044635911619,30.461438034725898],[114.13033843458149,30.461387374583097],[114.13023006248848,30.46133601896736],[114.13010079461621,30.461277151980173],[114.12914007973365,30.460822483792516]]]}},{"type":"Feature","id":"64","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.1304988990673,30.46142775766637],[114.13040188534445,30.46138206264984],[114.13029319275141,30.461331016288316],[114.13018197267012,30.461278274427475],[114.13011821926273,30.461249414854397],[114.1297996469823,30.461098948695362],[114.12958253083829,30.460996747637843],[114.12915765731448,30.460794860092545]]]}},{"type":"Feature","id":"65","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875753260568,30.46045704777092],[114.1288271635834,30.46048990394257],[114.12893137978958,30.46053891373819],[114.12898145903952,30.460562441979395],[114.12903942169002,30.460589618580357],[114.1290929564931,30.460615068807137],[114.12915022534027,30.46064215833168],[114.12925606054661,30.460691552731266],[114.1295927629289,30.460850115037506],[114.1297118410154,30.460906011102722],[114.12976728953336,30.460932068522915],[114.12981863325906,30.460956370440798],[114.12988131598836,30.46098588171369],[114.12998954955715,30.461036676630748],[114.13004608969733,30.46106340592618],[114.13006261309012,30.46107117703442],[114.1300997639906,30.46108864942799],[114.1302601720615,30.461164068601292],[114.13031469283304,30.46118984365158],[114.13037267427723,30.46121700849508],[114.13042454324432,30.461241437490035],[114.13048268457972,30.461268812921542],[114.13053567157775,30.46129408611399],[114.13056801034821,30.4613091027564]]]}},{"type":"Feature","id":"66","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12874030932966,30.46048397632716],[114.12880312320125,30.460513589668718],[114.12881408934,30.46051877370536],[114.12886034248127,30.460540615681648],[114.12901969078055,30.460615885242152],[114.12918879345622,30.46069546410517],[114.12950619915098,30.46084488819738],[114.12958058913902,30.460879834131823],[114.1297382830412,30.46095399968528],[114.1298989137917,30.46102932667027],[114.13004557426913,30.461098438591197],[114.13006054778565,30.46110549464093],[114.13012839752761,30.461137369360586],[114.13023304710003,30.461186698506765],[114.13040098147604,30.46126551377191],[114.13050675607363,30.461315424744768],[114.13055195920352,30.461336660444562]]]}},{"type":"Feature","id":"136","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12867558659867,30.4605446055135],[114.12862146241301,30.460518321024757]]]}},{"type":"Feature","id":"137","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12867066233986,30.460552033762347],[114.12861603989573,30.460526442348822]]]}},{"type":"Feature","id":"67","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12832328945066,30.460404043115755],[114.12821293483425,30.460352462075083],[114.12810882062381,30.46030343330221],[114.12800898333393,30.46025630773833],[114.12791284517074,30.460210946918565],[114.1278139899509,30.46016432845007],[114.12769539677514,30.460108307070723],[114.12758815968235,30.460057992138477],[114.12748280123796,30.460008447196017],[114.12737433762597,30.45995741073594],[114.12726317223137,30.4599049453874],[114.12715576088877,30.459854498539748],[114.1270956712156,30.45982610110306],[114.12704304909965,30.45980123263368],[114.12693907122653,30.45975203028999],[114.12683084070854,30.459701131617745],[114.12674074829447,30.459658801146414]]]}},{"type":"Feature","id":"68","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12680725956686,30.45954046820778],[114.12717449067327,30.459713047474622],[114.12759520741717,30.459910756278994],[114.1277516632006,30.459984417034843],[114.1277518461424,30.459984503356356]]]}},{"type":"Feature","id":"69","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12844958148462,30.460630905376078],[114.12841802968548,30.460681649005544],[114.12839198638993,30.46072327204628],[114.12836266917118,30.460769877974602],[114.12833395960327,30.460816395178412],[114.1283037234543,30.46086464582302],[114.1282911770312,30.460884715512925],[114.1282211342061,30.460996562363395],[114.12801243920661,30.461332549159803],[114.1277442196444,30.461759037316693],[114.12763446549414,30.461933972927216]]]}},{"type":"Feature","id":"70","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12848254571205,30.460644386019133],[114.12845049214515,30.460695881009226],[114.12839457762426,30.46078340026094],[114.12836346977788,30.460832468671114],[114.12825786032651,30.46100118711294],[114.12766413227311,30.46194796940813]]]}},{"type":"Feature","id":"71","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12900740340355,30.45974525957922],[114.128957960984,30.4598241272444],[114.12894004561335,30.459853352318294],[114.12893003702641,30.459869065799808],[114.12891992835358,30.459885680054146],[114.12891052233593,30.459900943156093],[114.12890109275257,30.45991602938203],[114.12889116629341,30.45993199281133],[114.12884403956247,30.460007641707215]]]}},{"type":"Feature","id":"72","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12903468416134,30.459759191112973],[114.12898507654947,30.459838468600687],[114.12897867293883,30.45984870212572],[114.12896815212967,30.459865515277198],[114.12895874916521,30.459880542002033],[114.1289500604375,30.459894427304334],[114.12893775904018,30.45991408592911],[114.12892769870186,30.459930192766993],[114.12891807753455,30.459945538511995],[114.12887150044769,30.46001997208708]]]}},{"type":"Feature","id":"73","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12799359944563,30.461170059048992],[114.12813058642354,30.460954110798646],[114.12818014087874,30.460874810459718]]]}},{"type":"Feature","id":"74","properties":{"type":2},"geometry":{"type":"MultiLineString","coordinates":[[[114.12802134345617,30.461185257079794],[114.12816026839353,30.460968026637453],[114.12821012605546,30.46088886842086]]]}},{"type":"Feature","id":"75","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12871659988954,30.460480973960003],[114.12866201732668,30.460455225703175]]]}},{"type":"Feature","id":"76","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12871181937012,30.4604891720155],[114.12865696068012,30.460463293495675]]]}},{"type":"Feature","id":"77","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12870625051855,30.460496613714092],[114.12865209001964,30.46047106454629]]]}},{"type":"Feature","id":"78","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12870119737104,30.460504896214353],[114.12864693030672,30.46047929677077]]]}},{"type":"Feature","id":"79","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12869621892378,30.46051275631796],[114.12864199196972,30.46048717579036]]]}},{"type":"Feature","id":"80","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12869144604645,30.460520692930068],[114.12863706353075,30.46049503901376]]]}},{"type":"Feature","id":"81","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875206317393,30.46042591203504],[114.12869730457149,30.46039987443165]]]}},{"type":"Feature","id":"82","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12835089014197,30.460401168581985],[114.12840556493653,30.460426761698077]]]}},{"type":"Feature","id":"83","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12841050808538,30.460419045241387],[114.12835610108812,30.460393178156856]]]}},{"type":"Feature","id":"84","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284204973323,30.460403289755668],[114.12836583665592,30.46037757068294]]]}},{"type":"Feature","id":"85","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12843095112684,30.460387282485826],[114.1283756424364,30.460361694068006]]]}},{"type":"Feature","id":"86","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12844040891842,30.460371578901405],[114.12838552459199,30.46034569277411]]]}},{"type":"Feature","id":"87","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12845530333595,30.460347628036693],[114.12840068992743,30.460322072418204]]]}},{"type":"Feature","id":"88","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284709841291,30.460324144822113],[114.12841544462637,30.460298280998398]]]}},{"type":"Feature","id":"89","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12846564956904,30.460331744768236],[114.12841033665775,30.46030594886737]]]}},{"type":"Feature","id":"90","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840565911587,30.460314176099065],[114.12846039800873,30.460340059104666]]]}},{"type":"Feature","id":"91","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12845085257727,30.46035556238818],[114.12839547046075,30.460329999527442]]]}},{"type":"Feature","id":"92","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12839045330449,30.46033775951954],[114.12844535891315,30.460363628043794]]]}},{"type":"Feature","id":"93","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12838043800423,30.460353694855385],[114.12843551638234,30.46037934153785]]]}},{"type":"Feature","id":"94","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1283709198198,30.460369685801584],[114.12842598062204,30.46039522388622]]]}},{"type":"Feature","id":"95","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12836108578529,30.460385462589116],[114.12841554195626,30.460411069100882]]]}},{"type":"Feature","id":"96","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12866685244558,30.460447511350914],[114.12872170540942,30.460473387158626]]]}},{"type":"Feature","id":"97","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12872659724891,30.460465290681256],[114.12867213942197,30.460439405192986]]]}},{"type":"Feature","id":"98","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12869225340944,30.460407869796136],[114.12874702860445,30.46043391529476]]]}},{"type":"Feature","id":"99","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12874224061314,30.460442013970503],[114.12868702900892,30.460415968259927]]]}},{"type":"Feature","id":"100","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12873675247232,30.460449538567893],[114.12868213874421,30.460424010637905]]]}},{"type":"Feature","id":"101","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12867707775104,30.46043185095154],[114.12873153978501,30.460457592185595]]]}},{"type":"Feature","id":"102","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12836533305023,30.46049065278955],[114.12831126438999,30.46046493708541]]]}},{"type":"Feature","id":"103","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12837057320787,30.460482374315422],[114.12831627884167,30.460456915451616]]]}},{"type":"Feature","id":"104","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.128375869697,30.460474656315874],[114.12832115618573,30.460448963083365]]]}},{"type":"Feature","id":"105","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840057586054,30.460434982343063],[114.12834581797811,30.4604093874583]]]}},{"type":"Feature","id":"106","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12839557850852,30.460443130646244],[114.1283409619393,30.46041732224048]]]}},{"type":"Feature","id":"107","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12839069502158,30.46045093856841],[114.12833610026247,30.460425094529544]]]}},{"type":"Feature","id":"108","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12838572529347,30.460458852915],[114.1283310701576,30.460433296250798]]]}},{"type":"Feature","id":"109","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12838079578135,30.46046681321431],[114.12832598302165,30.46044131635064]]]}},{"type":"Feature","id":"110","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12837649906545,30.4605135128412],[114.12834694946365,30.460560688846577]]]}},{"type":"Feature","id":"111","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12854178593888,30.46058975003992],[114.12851202635618,30.460637363832053]]]}},{"type":"Feature","id":"112","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12853243489933,30.460585826834613],[114.12850285552688,30.46063331813388]]]}},{"type":"Feature","id":"113","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12852298308462,30.460581441438844],[114.1284932007625,30.460628766063113]]]}},{"type":"Feature","id":"114","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12851419252213,30.46057753909397],[114.12848460959708,30.460624795756164]]]}},{"type":"Feature","id":"115","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12850517472779,30.460573235031077],[114.12847496389253,30.460620288973356]]]}},{"type":"Feature","id":"116","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12849593701645,30.460568989461134],[114.12846619724603,30.460616278867587]]]}},{"type":"Feature","id":"117","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12843089699246,30.46027479298183],[114.12848603051319,30.46029982760062]]]}},{"type":"Feature","id":"118","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842542716572,30.460282399079777],[114.12848046957723,30.460308814914708]]]}},{"type":"Feature","id":"119","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842025416148,30.46028980397502],[114.1284756669986,30.460316576604242]]]}},{"type":"Feature","id":"120","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12848666488827,30.460564499600935],[114.12845654247133,30.46061172678205]]]}},{"type":"Feature","id":"121","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12847753955164,30.46056031954453],[114.12844747388621,30.46060773734857]]]}},{"type":"Feature","id":"122","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284680946764,30.460556051543414],[114.12843832116972,30.460603782216417]]]}},{"type":"Feature","id":"123","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12845940547105,30.460551880699725],[114.12842920689394,30.46059922350488]]]}},{"type":"Feature","id":"124","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12845034586127,30.46054758475326],[114.12842010151311,30.460595070910582]]]}},{"type":"Feature","id":"125","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12844042699773,30.460543180431102],[114.12841116676191,30.460590768567833]]]}},{"type":"Feature","id":"126","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.128432077035,30.460539134018713],[114.12840149499199,30.460586441619128]]]}},{"type":"Feature","id":"127","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842263910166,30.460534983416075],[114.12839221993035,30.460582050888107]]]}},{"type":"Feature","id":"128","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.128413479861,30.460530541018002],[114.12838377174411,30.460578164755052]]]}},{"type":"Feature","id":"129","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840446028889,30.46052665183327],[114.12837433184895,30.460573725428528]]]}},{"type":"Feature","id":"130","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1283951588295,30.4605220981727],[114.12836525681436,30.460569600517413]]]}},{"type":"Feature","id":"131","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12838603082982,30.46051800823294],[114.12835560702274,30.460564877103256]]]}},{"type":"Feature","id":"132","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12863027713958,30.460615146289182],[114.12857543625488,30.460589559185415]]]}},{"type":"Feature","id":"138","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1286655605906,30.460559846150645],[114.12861113559894,30.460533898069162]]]}},{"type":"Feature","id":"139","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1286506066429,30.460583705686243],[114.12859608673774,30.460557448859458]]]}},{"type":"Feature","id":"140","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864519873487,30.460591682994284],[114.12859087146254,30.46056594440731]]]}},{"type":"Feature","id":"141","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1286402269583,30.46059966951767],[114.12858567860708,30.460573673666502]]]}},{"type":"Feature","id":"142","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12863514112153,30.460607292800816],[114.12858079067996,30.460581634887177]]]}},{"type":"Feature","id":"143","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12868567339142,30.460528707643714],[114.12863162666974,30.460502262442503]]]}},{"type":"Feature","id":"144","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12884532738447,30.460284588142553],[114.12883379933665,30.46028716606622],[114.12875726121462,30.460304281487893]]]}},{"type":"Feature","id":"145","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875726121462,30.460304281487893],[114.1287495824418,30.46029087357002],[114.12873789098559,30.460298886010346]]]}},{"type":"Feature","id":"146","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864239023739,30.460258399832206],[114.1286478584979,30.46026107747178],[114.12867760736032,30.46027479966971],[114.12870731410098,30.46028872831528],[114.12873586697903,30.460302115922662]]]}},{"type":"Feature","id":"147","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12873586697903,30.460302115922662],[114.12873930594586,30.460303728353384]]]}},{"type":"Feature","id":"148","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840022780429,30.460226284750153],[114.12838643674475,30.460154021808915]]]}},{"type":"Feature","id":"149","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12836213336523,30.46034185152361],[114.12837928019152,30.460314398279753],[114.1283957314117,30.46028805870498],[114.1284132054234,30.460260081520367]]]}},{"type":"Feature","id":"150","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12870837067852,30.46032404079281],[114.1286803577796,30.4603724503829]]]}},{"type":"Feature","id":"151","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12867109294197,30.460368381122688],[114.12869912869216,30.460319932077443]]]}},{"type":"Feature","id":"152","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12866183174938,30.46036430555569],[114.12868988865519,30.460315819985393]]]}},{"type":"Feature","id":"153","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865257055421,30.46036022998521],[114.12868064861574,30.46031170788965]]]}},{"type":"Feature","id":"154","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864330935608,30.460356154411024],[114.1286715030655,30.46030752306435]]]}},{"type":"Feature","id":"155","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12863404815567,30.460352078833306],[114.12866216852906,30.460303483687547]]]}},{"type":"Feature","id":"156","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12862478695267,30.460348003251934],[114.12865292848181,30.460299371581126]]]}},{"type":"Feature","id":"157","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12861548891867,30.46034392349733],[114.12864368843199,30.460295259471124]]]}},{"type":"Feature","id":"158","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12860626453867,30.460339852078516],[114.12863444837981,30.460291147357605]]]}},{"type":"Feature","id":"159","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12859700332784,30.460335776486477],[114.1286252083248,30.460287035240558]]]}},{"type":"Feature","id":"160","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12854143600869,30.460311322858644],[114.12856976794158,30.46026236246309]]]}},{"type":"Feature","id":"161","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12853138266078,30.46030689865123],[114.12856027238867,30.460258136623867]]]}},{"type":"Feature","id":"162","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285209155493,30.460302292354605],[114.12855120127928,30.460254099673847]]]}},{"type":"Feature","id":"163","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12851162280957,30.46029825874834],[114.1285419586371,30.46024998638347]]]}},{"type":"Feature","id":"164","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840807113457,30.460157725686944],[114.12847011220352,30.460151560245418]]]}},{"type":"Feature","id":"165","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284101654926,30.46016659318303],[114.12847220656202,30.460160427736348]]]}},{"type":"Feature","id":"166","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12841225985079,30.460175460679043],[114.12847430092049,30.460169295227086]]]}},{"type":"Feature","id":"167","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12841435420903,30.46018432817498],[114.12847639527922,30.46017816271769]]]}},{"type":"Feature","id":"168","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12841644856744,30.460193195670612],[114.12847848963814,30.46018703020827]]]}},{"type":"Feature","id":"169","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12841854292579,30.460202063166392],[114.1284805839972,30.4601958976989]]]}},{"type":"Feature","id":"170","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842063728452,30.460210930662107],[114.1284826783562,30.460204765189165]]]}},{"type":"Feature","id":"171","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842273164331,30.460219798157567],[114.12848477271548,30.46021363267947]]]}},{"type":"Feature","id":"172","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12876862589741,30.4603243180969],[114.12878429704632,30.460375964015952]]]}},{"type":"Feature","id":"173","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12877832341091,30.460321867552405],[114.1287941676972,30.460373502290036]]]}},{"type":"Feature","id":"174","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12878830215362,30.460319345936522],[114.12880415582616,30.460371011259845]]]}},{"type":"Feature","id":"175","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12879828089203,30.460316824316447],[114.1288141439507,30.46036852022551]]]}},{"type":"Feature","id":"176","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12883819580152,30.46030673779352],[114.12885409640481,30.460358556045676]]]}},{"type":"Feature","id":"177","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12886408450736,30.460356064989977],[114.12884817451787,30.460304216152167]]]}},{"type":"Feature","id":"178","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12857900801183,30.460266474601568],[114.128550697235,30.460315398472268]]]}},{"type":"Feature","id":"179","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12858824807958,30.460270586736574],[114.12855995845868,30.460319474082347]]]}},{"type":"Feature","id":"180","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12859748814465,30.460274698867888],[114.12856921967986,30.460323549688777]]]}},{"type":"Feature","id":"181","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12860672820732,30.460278810995728],[114.12857848089843,30.460327625291548]]]}},{"type":"Feature","id":"182","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1286159682674,30.46028292311988],[114.1285877421146,30.46033170089078]]]}},{"type":"Feature","id":"183","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1288083184198,30.460314494295],[114.12882418147751,30.460366190197742]]]}},{"type":"Feature","id":"184","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12881835594338,30.46031216426939],[114.128834219,30.46036386016563]]]}},{"type":"Feature","id":"185","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12882815828664,30.460309067827627],[114.12884405889112,30.460360886086296]]]}},{"type":"Feature","id":"186","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12857969415214,30.46067716294352],[114.12864065377806,30.460669210477786]]]}},{"type":"Feature","id":"187","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12821095471107,30.460587805847116],[114.12822264139515,30.460583823177657],[114.12829655198448,30.46055864893324]]]}},{"type":"Feature","id":"188","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12859393102332,30.46073889902016],[114.12865733633403,30.46073080933146]]]}},{"type":"Feature","id":"189","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865499232646,30.460722154294714],[114.12859194575512,30.460730290201745]]]}},{"type":"Feature","id":"190","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12859000904008,30.460721891924944],[114.12865258593759,30.460713268918468]]]}},{"type":"Feature","id":"191","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865010693449,30.460704115420466],[114.12858781952768,30.46071239743059]]]}},{"type":"Feature","id":"192","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864775433783,30.460695428666835],[114.1285859385222,30.46070424073049]]]}},{"type":"Feature","id":"193","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864547745343,30.46068702147333],[114.1285838040565,30.460694984936232]]]}},{"type":"Feature","id":"194","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12864312222841,30.460678325013305],[114.12858175677763,30.46068610721425]]]}},{"type":"Feature","id":"195","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12826540486421,30.46049167731894],[114.128285766533,30.460541251276027]]]}},{"type":"Feature","id":"196","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12827623056698,30.46054452442842],[114.12825556202769,30.46049482491383]]]}},{"type":"Feature","id":"197","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12826632154945,30.460547925622357],[114.12824607169709,30.460497859777327]]]}},{"type":"Feature","id":"198","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12820831432073,30.46056783604067],[114.12818747687564,30.460516597394804]]]}},{"type":"Feature","id":"199","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1282178745576,30.46056455459578],[114.12819694070524,30.460513571038067]]]}},{"type":"Feature","id":"200","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12822757560343,30.460561224814413],[114.12820677922646,30.46051042485647]]]}},{"type":"Feature","id":"201","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12823755308571,30.46055780014313],[114.12821652067532,30.460507234215115]]]}},{"type":"Feature","id":"202","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12824678210885,30.460554632367874],[114.12822641133666,30.46050414684501]]]}},{"type":"Feature","id":"203","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12825617266687,30.460551409142138],[114.12823638920834,30.460500956084466]]]}},{"type":"Feature","id":"204","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12842232979635,30.460618247013066],[114.128417691069,30.46061609233018],[114.12838906067488,30.46060279353459],[114.12835891643661,30.460588791534825],[114.12832884115144,30.460574821538472]]]}},{"type":"Feature","id":"205","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12866189334268,30.460662264222407],[114.12867794830335,30.460733719129816]]]}},{"type":"Feature","id":"206","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285007257757,30.460693399910237],[114.12850290961815,30.460684535480944],[114.12849141514032,30.460684593394582]]]}},{"type":"Feature","id":"207","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12849941394543,30.460671692237295],[114.12851919055488,30.460673065270363],[114.12851723573179,30.46068725357128]]]}},{"type":"Feature","id":"208","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12850706592427,30.460659350460933],[114.12853856699874,30.460659154402517],[114.1285338333218,30.460682142372615]]]}},{"type":"Feature","id":"209","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284732864388,30.460713834476344],[114.12847872288397,30.460705064530792]]]}},{"type":"Feature","id":"210","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12847872288397,30.460705064530792],[114.12849141514032,30.460684593394582]]]}},{"type":"Feature","id":"211","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12849141514032,30.460684593394582],[114.12849941394543,30.460671692237295]]]}},{"type":"Feature","id":"212","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12849941394543,30.460671692237295],[114.12850706592427,30.460659350460933]]]}},{"type":"Feature","id":"213","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12850706592427,30.460659350460933],[114.1285079053109,30.46065799662477],[114.1285193386141,30.460639796030616],[114.12858022522573,30.460671950374874],[114.12857584464733,30.460672901241058],[114.1285661300287,30.46067430161067],[114.12855794056122,30.460675779314688],[114.12854994047656,30.46067753164059],[114.12853872035802,30.460680668239995],[114.1285338333218,30.460682142372615]]]}},{"type":"Feature","id":"214","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285338333218,30.460682142372615],[114.1285263397709,30.46068440273543],[114.12851723573179,30.46068725357128]]]}},{"type":"Feature","id":"215","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12851723573179,30.46068725357128],[114.12851419616699,30.460688205378652],[114.12850163985091,30.460692973529728],[114.1285007257757,30.460693399910237]]]}},{"type":"Feature","id":"216","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285007257757,30.460693399910237],[114.12848441607913,30.46070100774001],[114.12847872288397,30.460705064530792]]]}},{"type":"Feature","id":"217","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12862658288552,30.460617500482353],[114.12862756382431,30.460621398461406]]]}},{"type":"Feature","id":"218","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12862756382431,30.460621398461406],[114.12863622422122,30.46065581248366],[114.12863622422124,30.460655812483694]]]}},{"type":"Feature","id":"219","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12863622422124,30.460655812483694],[114.12863837299199,30.460664351103055],[114.1286501161541,30.460662969423137],[114.12865149721733,30.46066288672742]]]}},{"type":"Feature","id":"220","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865149721733,30.46066288672742],[114.12866189334268,30.460662264222407]]]}},{"type":"Feature","id":"221","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12866189334268,30.460662264222407],[114.12866670776984,30.460661975939868],[114.12867346315322,30.460661452813614]]]}},{"type":"Feature","id":"222","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12867346315322,30.460661452813614],[114.12868308922111,30.460660707382488],[114.1286930532053,30.460660015850845],[114.12870155760558,30.460659491930894],[114.12871156753305,30.460659360647206],[114.12871540763784,30.46065931028169],[114.12870178263275,30.46065286645476],[114.12868143900795,30.46064307478373]]]}},{"type":"Feature","id":"223","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12868143900795,30.46064307478373],[114.12867312388286,30.460639072594365],[114.12865979441074,30.460632896425178]]]}},{"type":"Feature","id":"224","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865979441074,30.460632896425178],[114.12864842897787,30.4606276302832],[114.12862898061579,30.460618618930347],[114.12862658288563,30.460617500482353]]]}},{"type":"Feature","id":"225","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12862756382431,30.460621398461406],[114.1286536981893,30.460646726631666],[114.12863622422124,30.460655812483694]]]}},{"type":"Feature","id":"226","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12863622422124,30.460655812483694],[114.12863621216883,30.46065581875052]]]}},{"type":"Feature","id":"227","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12865979441074,30.460632896425178],[114.12867149838834,30.460650389640094],[114.12865149721733,30.46066288672742]]]}},{"type":"Feature","id":"228","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12868143900795,30.46064307478373],[114.12868869484318,30.46065366482945],[114.12867346315322,30.460661452813614]]]}},{"type":"Feature","id":"229","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12854918983022,30.460227838404215],[114.12852234392066,30.460227942017273],[114.1285323730125,30.46020906884395]]]}},{"type":"Feature","id":"230","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285457397978,30.460205305973776],[114.12854351565343,30.460216013087265],[114.1285571368483,30.460216067679987]]]}},{"type":"Feature","id":"231","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12857029988476,30.460196571231123],[114.1285571368483,30.460216067679987]]]}},{"type":"Feature","id":"232","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285571368483,30.460216067679987],[114.12854918983022,30.460227838404215]]]}},{"type":"Feature","id":"233","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12854918983022,30.460227838404215],[114.12853544237832,30.460248200417247],[114.12848080370732,30.460219963867853],[114.12851469333377,30.46021348906026],[114.1285323730125,30.46020906884395]]]}},{"type":"Feature","id":"234","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285323730125,30.46020906884395],[114.1285457397978,30.460205305973776]]]}},{"type":"Feature","id":"235","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1285457397978,30.460205305973776],[114.12857029988476,30.460196571231123]]]}},{"type":"Feature","id":"236","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12876040839092,30.460323767807377],[114.12874507413414,30.4603128520213],[114.12871981540793,30.460327731040778]]]}},{"type":"Feature","id":"237","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12872991929535,30.46031166450774],[114.12874684379823,30.46030173077994],[114.12875876209284,30.46031388020817]]]}},{"type":"Feature","id":"238","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875557087689,30.46027086584409],[114.12873789098559,30.460298886010346]]]}},{"type":"Feature","id":"239","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12873789098559,30.460298886010346],[114.12873586697903,30.460302115922662]]]}},{"type":"Feature","id":"240","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12873586697903,30.460302115922662],[114.12871981540793,30.460327731040778]]]}},{"type":"Feature","id":"241","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12871981540793,30.460327731040778],[114.1287182902953,30.460330156172322],[114.12876098693141,30.460327242497126],[114.12876040839092,30.460323767807377]]]}},{"type":"Feature","id":"242","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12876040839092,30.460323767807377],[114.12875876209284,30.46031388020817]]]}},{"type":"Feature","id":"243","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875876209284,30.46031388020817],[114.12875749610893,30.460306276759738],[114.12875726121462,30.460304281487893]]]}},{"type":"Feature","id":"244","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12875726121462,30.460304281487893],[114.12875671862989,30.460299672590363],[114.12875557087689,30.46027086584409]]]}},{"type":"Feature","id":"245","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12879200707901,30.460408797342957],[114.12877165734105,30.460407878627482],[114.12877202801093,30.460429485397974]]]}},{"type":"Feature","id":"246","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12878636449918,30.46043625423225],[114.12878583346638,30.460423139484174],[114.1288001539798,30.46042339124335]]]}},{"type":"Feature","id":"247","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1288067493305,30.460433949312637],[114.12880674933038,30.460433949312637]]]}},{"type":"Feature","id":"248","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12880674933038,30.460433949312637],[114.12879787001776,30.46043388887097],[114.12879753025778,30.460441489701243]]]}},{"type":"Feature","id":"249","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1288535470595,30.46046745842134],[114.12882277886841,30.460453322501046]]]}},{"type":"Feature","id":"250","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12882277886841,30.460453322501046],[114.12879753025778,30.460441489701243]]]}},{"type":"Feature","id":"251","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12879753025778,30.460441489701243],[114.12878636449918,30.46043625423225]]]}},{"type":"Feature","id":"252","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12878636449918,30.46043625423225],[114.12877202801093,30.460429485397974]]]}},{"type":"Feature","id":"253","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12877202801093,30.460429485397974],[114.12875440144785,30.460421163179134],[114.12877706273885,30.460376546593384],[114.12879030510373,30.46040557140097],[114.12879200707901,30.460408797342957]]]}},{"type":"Feature","id":"254","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12879200707901,30.460408797342957],[114.1288001539798,30.46042339124335]]]}},{"type":"Feature","id":"255","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1288001539798,30.46042339124335],[114.12880674933038,30.460433949312637]]]}},{"type":"Feature","id":"256","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12880674933038,30.460433949312637],[114.1288067493305,30.460433949312808],[114.12881158715328,30.460441153762385],[114.12881787573671,30.46044796947164],[114.12882277886841,30.460453322501046]]]}},{"type":"Feature","id":"257","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12837302917117,30.46022523007898],[114.12836095507677,30.46022907665788],[114.12836617769591,30.460238227787542]]]}},{"type":"Feature","id":"258","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1283270929556,30.460220065105005],[114.12836404472645,30.460224633610444],[114.12837302917117,30.46022523007898]]]}},{"type":"Feature","id":"259","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12837302917117,30.46022523007898],[114.12840022780429,30.460226284750153]]]}},{"type":"Feature","id":"260","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12840022780429,30.460226284750153],[114.12842428170627,30.460225607748736],[114.12843554363837,30.460270462042168],[114.1284132054234,30.460260081520367]]]}},{"type":"Feature","id":"261","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1284132054234,30.460260081520367],[114.12836617769591,30.460238227787542]]]}},{"type":"Feature","id":"262","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12836617769591,30.460238227787542],[114.12834456974755,30.460228342316125],[114.1283270929556,30.460220065105005]]]}},{"type":"Feature","id":"263","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1282594361881,30.460470610747606],[114.12828158879684,30.460469862575287],[114.12827668978332,30.460452006380965]]]}},{"type":"Feature","id":"264","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12824988935719,30.460455290365648],[114.1282626235959,30.46045470735359],[114.12825957569038,30.460443946624476]]]}},{"type":"Feature","id":"265","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12831057643545,30.460467821384352],[114.12828954958354,30.46045806260415],[114.12827668978332,30.460452006380965]]]}},{"type":"Feature","id":"266","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12827668978332,30.460452006380965],[114.12825957569038,30.460443946624476]]]}},{"type":"Feature","id":"267","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12825957569038,30.460443946624476],[114.12823116917164,30.460430565779617],[114.12823513363749,30.460435047164694],[114.1282410985911,30.460442480098674],[114.12824699456962,30.460450849727245],[114.12824988935719,30.460455290365648]]]}},{"type":"Feature","id":"268","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12824988935719,30.460455290365648],[114.12825612343089,30.460464853507645],[114.1282594361881,30.460470610747606]]]}},{"type":"Feature","id":"269","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1282594361881,30.460470610747606],[114.1282621451196,30.460475318598228],[114.12827004285334,30.460490559238714],[114.12827031807959,30.460491096327623],[114.12831057643545,30.460467821384352]]]}},{"type":"Feature","id":"270","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12829079823528,30.46053987849389],[114.12834003760709,30.46055681816805]]]}},{"type":"Feature","id":"271","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12830107585357,30.460574929528217],[114.12831134133391,30.4605855186706],[114.1283308918113,30.460571522243782]]]}},{"type":"Feature","id":"272","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1283308918113,30.460571522243782],[114.12833090616948,30.460571511964588]]]}},{"type":"Feature","id":"273","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12829079823528,30.46053987849389],[114.12829185294262,30.46054275428451],[114.12829352173044,30.460548399008697]]]}},{"type":"Feature","id":"274","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12829352173044,30.460548399008697],[114.12829352173046,30.460548399008758],[114.12829655198448,30.46055864893324]]]}},{"type":"Feature","id":"275","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12829655198448,30.46055864893324],[114.12830072615508,30.460573305664308],[114.12830107585357,30.460574929528217]]]}},{"type":"Feature","id":"276","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12830107585357,30.460574929528217],[114.12830350703699,30.460586218995534],[114.1283057058045,30.460595944723526],[114.12830651401676,30.460604188710853],[114.12829868477651,30.46062353496956],[114.12831145944958,30.460602786826506],[114.12832884115144,30.460574821538472]]]}},{"type":"Feature","id":"277","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12832884115144,30.460574821538472],[114.1283308918113,30.460571522243782]]]}},{"type":"Feature","id":"278","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.1283308918113,30.460571522243782],[114.12833089181142,30.460571522243605],[114.1283379454297,30.460560173714523],[114.12834003760709,30.46055681816805]]]}},{"type":"Feature","id":"279","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12834003760709,30.46055681816805],[114.12831411299864,30.460568000808475],[114.12829352173044,30.460548399008697]]]}},{"type":"Feature","id":"280","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12829352173044,30.460548399008697],[114.12829350753343,30.460548385493894]]]}},{"type":"Feature","id":"281","properties":{"type":5},"geometry":{"type":"MultiLineString","coordinates":[[[114.12871283204083,30.46054890605957],[114.12869540616136,30.460576642138296],[114.12867798025998,30.460604378204753],[114.12866062846345,30.460631996274866]]]}}]} \ No newline at end of file diff --git a/packages/road-compiler/test/index.js b/packages/road-compiler/test/index.js deleted file mode 100644 index e3311bf..0000000 --- a/packages/road-compiler/test/index.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -const assert = require("assert/strict"); -const fs = require("fs"); -const path = require("path"); -const compiler = require("../src"); - -assert.equal(typeof compiler.laneGeometry.laneCenterline, "function"); -assert.equal(typeof compiler.gaodeReference.convertGeoJson, "function"); -assert.equal(typeof compiler.turnLaneArrows.buildCustomTurnLaneArrows, "function"); -assert.equal(typeof compiler.complexJunction.buildComplexJunctionGeometry, "function"); -assert.equal(typeof compiler.nativeRoad.compileRoadModel, "function"); -assert.equal(typeof compiler.check.checkOutput, "function"); -const fixture = path.join(__dirname, "fixtures", "fengshu-er-road.osm"); -assert.ok(fs.existsSync(fixture)); -assert.throws(() => compiler.compiler.validateInput({ id: "area" }), /RoadCompilerInput/); -const model = compiler.nativeRoad.compileRoadModel(fs.readFileSync(fixture, "utf8"), { schema: "native-road-overrides/v1", overrides: [] }); -assert.equal(model.roads.length, 24); -console.log("road compiler package tests passed"); diff --git a/packages/road-compiler/workbench/client/app.css b/packages/road-compiler/workbench/client/app.css deleted file mode 100644 index 4c58311..0000000 --- a/packages/road-compiler/workbench/client/app.css +++ /dev/null @@ -1 +0,0 @@ -*{box-sizing:border-box}body{margin:0;background:#eef1ef;color:#202523;font:14px system-ui,sans-serif}header{height:50px;display:flex;gap:12px;align-items:center;padding:0 16px;background:#183a32;color:#fff}header span{color:#c9d8d2}#dirty-state.dirty{color:#ffe08a;font-weight:700}button{border:1px solid #82988f;background:#fff;color:#1d392f;padding:7px 10px;border-radius:3px;cursor:pointer}button:disabled{cursor:default;opacity:.55}header button:first-of-type{margin-left:auto}main{display:grid;grid-template-columns:260px minmax(0,1fr) 320px;height:calc(100vh - 50px)}aside{overflow:auto;background:#fff;padding:16px}.issues{border-right:1px solid #d5dfda}.inspector{border-left:1px solid #d5dfda}.map{position:relative;background:#d7e2de;min-height:400px}canvas{width:100%;height:100%;display:block}.legend{position:absolute;bottom:12px;left:12px;background:#fff;padding:8px;box-shadow:0 1px 4px #0003}.legend i{display:inline-block;width:18px;height:7px;margin:0 4px -1px 10px}.reference{background:#a5b0b5}.native{background:#296654}.line{height:3px!important;background:#263630}.junction{width:10px!important;height:10px!important;background:#0e7860;border-radius:50%}.warning{width:10px!important;height:10px!important;background:#d49318;border-radius:50%}h1{font-size:16px;margin:0 0 8px}h2{font-size:14px;margin:12px 0 8px}.muted,output,pre{color:#52615b}.issues ul{list-style:none;padding:0;margin:0}.issues button{width:100%;text-align:left;margin:4px 0;background:#fff7e5;border-color:#e7c67b;color:#693c00}.issues button.error{background:#fff0ee;border-color:#e3a49b;color:#8d261a}.segmented{display:flex;margin:0 0 8px}.segmented button{flex:1;border-radius:0;padding:6px 4px;font-size:12px}.segmented button+button{border-left:0}.segmented button:first-child{border-radius:3px 0 0 3px}.segmented button:last-child{border-radius:0 3px 3px 0}.segmented button.active{background:#286956;border-color:#286956;color:#fff}label{display:block;margin:10px 0}input[type=number]{display:block;width:100%;padding:7px;border:1px solid #aab8b2;border-radius:2px}output,pre{display:block;white-space:pre-wrap;overflow-wrap:anywhere}form button{margin-top:8px;background:#286956;color:white;border:0}dl{display:grid;grid-template-columns:1fr auto;gap:5px 10px;margin:0}dt{color:#52615b}dd{margin:0;font-variant-numeric:tabular-nums}hr{border:0;border-top:1px solid #dde4e1;margin:16px 0}details{margin-top:16px}summary{cursor:pointer;font-weight:600}@media(max-width:900px){main{grid-template-columns:minmax(0,1fr)}.issues{display:none}.inspector{position:absolute;right:0;bottom:0;width:min(360px,100%);max-height:55vh;border-top:1px solid #d5dfda}} diff --git a/packages/road-compiler/workbench/client/app.js b/packages/road-compiler/workbench/client/app.js deleted file mode 100644 index 45885e7..0000000 --- a/packages/road-compiler/workbench/client/app.js +++ /dev/null @@ -1,459 +0,0 @@ -import Map from "/vendor/ol/Map.js"; -import View from "/vendor/ol/View.js"; -import VectorLayer from "/vendor/ol/layer/Vector.js"; -import VectorSource from "/vendor/ol/source/Vector.js"; -import GeoJSON from "/vendor/ol/format/GeoJSON.js"; -import Feature from "/vendor/ol/Feature.js"; -import LineString from "/vendor/ol/geom/LineString.js"; -import Point from "/vendor/ol/geom/Point.js"; -import Style from "/vendor/ol/style/Style.js"; -import Fill from "/vendor/ol/style/Fill.js"; -import Stroke from "/vendor/ol/style/Stroke.js"; -import CircleStyle from "/vendor/ol/style/Circle.js"; -import Text from "/vendor/ol/style/Text.js"; -import Polygon from "/vendor/ol/geom/Polygon.js"; -import RegularShape from "/vendor/ol/style/RegularShape.js"; -import Select from "/vendor/ol/interaction/Select.js"; -import { click } from "/vendor/ol/events/condition.js"; -import { fromLonLat } from "/vendor/ol/proj.js"; - -const geojson = new GeoJSON(); -const areaLabel = document.querySelector("#area"); -const status = document.querySelector("#status"); -const form = document.querySelector("#road-form"); -const hint = document.querySelector("#hint"); -const roadName = document.querySelector("#road-name"); -const movementSummary = document.querySelector("#movement-summary"); -const laneConvention = document.querySelector("#lane-convention"); -const selectedMovementPanel = document.querySelector("#selected-movement"); -const movementDetail = document.querySelector("#movement-detail"); -const directionSwitch = document.querySelector("#direction-switch"); -const widthInput = document.querySelector("#width"); -const lanesInput = document.querySelector("#lanes"); -const leftInput = document.querySelector("#left"); -const rightInput = document.querySelector("#right"); -const centerLineForm = document.querySelector("#center-line-form"); -const centerLineSegment = document.querySelector("#center-line-segment"); -const markingStyleHeading = document.querySelector("#marking-style-heading"); -const centerLineStyleInput = document.querySelector("#center-line-style"); -const doubleYellowOption = document.createElement("option"); -doubleYellowOption.value = "double-yellow-solid"; doubleYellowOption.textContent = "双黄实线"; centerLineStyleInput.append(doubleYellowOption); -const evidence = document.querySelector("#evidence"); -const diagnostics = document.querySelector("#diagnostics"); -const diagnosticFilters = document.querySelector("#diagnostic-filters"); -const summary = document.querySelector("#summary"); -const connectionsBox = document.querySelector("#connections"); -const addConnectionButton = document.querySelector("#add-connection"); -const saveButton = document.querySelector("#save"); -const compileButton = document.querySelector("#compile"); -const dirtyState = document.querySelector("#dirty-state"); -const scenePreviewToggle = document.querySelector("#scene-preview"); -const selectedJunctionPanel = document.querySelector("#selected-junction"); -const junctionDetail = document.querySelector("#junction-detail"); -const directionArrowsToggle = document.createElement("label"); -directionArrowsToggle.innerHTML = ' 道路方向箭头'; -const markingsToggle = document.createElement("label"); -markingsToggle.innerHTML = ' 车道分隔线与路口转向箭头'; -const centerLinesToggle = document.createElement("label"); -centerLinesToggle.innerHTML = ' 道路中心线'; -const edgeLinesToggle = document.createElement("label"); -edgeLinesToggle.innerHTML = ' 道路外缘线'; -const controlsToggle = document.createElement("label"); -controlsToggle.innerHTML = ' 斑马线与停止线'; -const signalsToggle = document.createElement("label"); -signalsToggle.innerHTML = ' 红绿灯设施'; -document.querySelector('[data-layer="lanes"]').closest("label").after(directionArrowsToggle, markingsToggle, centerLinesToggle, edgeLinesToggle, controlsToggle, signalsToggle); -// Only offered when the server was started with --debug; without it the state -// carries no candidates and an empty toggle would just be confusing. -const candidateAction = document.createElement("section"); -document.querySelector(".inspector").insertBefore(candidateAction, document.querySelector(".inspector details")); -const candidatesToggle = document.createElement("label"); -candidatesToggle.hidden = true; -candidatesToggle.innerHTML = ' 复杂路口候选(debug)'; -signalsToggle.after(candidatesToggle); -const gaodeReferenceColors = { 1: "#2563eb", 2: "#0f766e", 3: "#7c3aed", 4: "#ea580c", 5: "#64748b" }; - -let state; -let selectedRoad = null; -let selectedMovement = null; -let selectedJunction = null; -let staged = []; -let manualFromEndpoint = null; -let diagnosticFilter = "all"; -let scenePreview = false; -let selectedCenterLineSegment = null; -let selectedLaneSeparator = null; -let selectedEdgeLine = null; -let selectedSignal = null; -const signalPanel = document.createElement("section"); -signalPanel.innerHTML = '

原生红绿灯

'; -document.querySelector(".inspector").insertBefore(signalPanel, document.querySelector(".inspector details")); -const signalForm = signalPanel.querySelector("form"); -const signalOutput = signalForm.querySelector("output"); -const signalPicker = signalPanel.querySelector('[name="signal-picker"]'); -const source = () => new VectorSource(); -const layers = { - reference: new VectorLayer({ source: source(), visible: false, style: new Style({ fill: new Fill({ color: "rgba(123, 140, 148, .28)" }), stroke: new Stroke({ color: "#8999a0", width: 1 }) }) }), - gaodeReference: new VectorLayer({ source: source(), visible: true, zIndex: 1, style: (feature) => { const color = gaodeReferenceColors[feature.get("type")] || "#475569"; return new Style({ fill: new Fill({ color: `${color}26` }), stroke: new Stroke({ color, width: 1.5 }) }); } }), - native: new VectorLayer({ source: source(), style: nativeSurfaceStyle }), - edgeLines: new VectorLayer({ source: source(), visible: false, style: markingStyle }), - sidewalks: new VectorLayer({ source: source(), style: sidewalkSurfaceStyle }), - osm: new VectorLayer({ source: source(), style: (feature) => new Style({ stroke: new Stroke({ color: feature.get("road_id") === selectedRoad?.id ? "#006e91" : "#263630", width: feature.get("road_id") === selectedRoad?.id ? 5 : 2 }) }) }), - lanes: new VectorLayer({ source: source(), style: laneStyle }), - directionArrows: 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 }), - signals: new VectorLayer({ source: source(), style: signalAssemblyStyle, zIndex: 30 }), - 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 }), - junctionCandidates: new VectorLayer({ source: source(), visible: true, zIndex: 25, style: (feature) => [ - new Style({ fill: new Fill({ color: "rgba(219, 39, 119, .12)" }), stroke: new Stroke({ color: "#db2777", width: 2, lineDash: [8, 5] }) }), - new Style({ text: new Text({ text: `#${feature.get("index")} ${feature.get("nodeCount")}节点`, font: "bold 13px system-ui, sans-serif", fill: new Fill({ color: "#831843" }), stroke: new Stroke({ color: "#fff", width: 3 }), offsetY: -14 }) }), - ] }), - diagnostics: new VectorLayer({ source: source(), style: (feature) => new Style({ image: new CircleStyle({ radius: 6, fill: new Fill({ color: feature.get("severity") === "error" ? "#bf3b2e" : "#d49318" }), stroke: new Stroke({ color: "#fff", width: 1 }) }) }) }), - 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 }), -}; -const map = new Map({ target: "map", layers: [layers.gaodeReference, layers.reference, layers.native, layers.edgeLines, layers.sidewalks, layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.signals, layers.connectors, layers.junctionCandidates, 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.centerLines, layers.edgeLines, layers.controls, layers.signals, layers.connectors, layers.native, layers.junctionCandidates, layers.diagnostics].includes(layer), hitTolerance: 12, style: null }); -map.addInteraction(select); -select.on("select", ({ selected }) => { - const feature = selected[0]; - if (!feature) return; - if (feature.get("candidate_index")) return selectJunctionCandidate(feature); - if (feature.get("signal_uid")) return selectSignal(poleFeatureForSignal(feature.get("signal_uid")) || feature); - if (manualFromEndpoint) return chooseManualTarget(roadForFeature(feature)); - const junction = junctionForFeature(feature); - if (junction) return selectJunction(junction); - const provenance = feature.get("provenance"); - if (provenance?.startsWith("native-road-")) { - const road = roadForFeature(feature); - const directionArrow = provenance === "native-road-direction-arrow/v1"; - const turnArrow = provenance === "native-road-turn-arrow/v1"; - const centerLine = provenance === "native-road-center-line/v1"; - const edgeLine = provenance === "native-road-edge-line/v1"; - const crosswalk = provenance === "native-road-crosswalk/v1"; - const stopLine = provenance === "native-road-stop-line/v1"; - const markingType = centerLine ? "道路中心线" : edgeLine ? "道路外缘线" : crosswalk ? "斑马线" : stopLine ? "停止线" : directionArrow ? "道路方向箭头" : turnArrow ? "路口转向箭头" : "车道分隔线"; - selectRoad(road); - if (centerLine) selectCenterLine(feature); else if (edgeLine) selectEdgeLine(feature); else if (provenance === "native-road-lane-separator/v1") selectLaneSeparator(feature); else clearCenterLineSelection(); - evidence.textContent = JSON.stringify({ - 标线类型: markingType, - 人行横道节点: crosswalk || stopLine ? feature.get("crossing_node_id") : null, - OSM道路: feature.get("osm_way_ids"), - 原生道路: feature.get("road_id"), - 道路段: centerLine ? feature.get("segment_id") : null, - 方向: feature.get("direction"), - 车道: feature.get("lane_id") || feature.get("lane_index") || `${feature.get("left_lane_index")} 与 ${feature.get("right_lane_index")} 之间`, - 转向: turnArrow ? feature.get("maneuver") : null, - 样式: centerLine || edgeLine || provenance === "native-road-lane-separator/v1" ? feature.get("effective_style") : null, - 放置方法: feature.get("placement_method") || null, - 道路内距离米: feature.get("distance_along_lane_meters") || null, - 路口前距离米: feature.get("placement_distance_meters") || null, - 来源: provenance, - }, null, 2); - return message(`已选中${markingType}`); - } - const movement = state.compiled.movements?.find((item) => item.id === feature.get("movement_id")) || null; - selectRoad(roadForFeature(feature), undefined, movement); -}); -map.on("pointermove", (event) => { map.getTargetElement().style.cursor = map.hasFeatureAtPixel(event.pixel, { hitTolerance: 8 }) ? "pointer" : ""; }); - -function message(text) { status.textContent = text; } -function updateDirtyState() { - const count = staged.length; - dirtyState.textContent = count ? `未保存修改 ${count} 项` : "所有修改已保存"; - dirtyState.classList.toggle("dirty", count > 0); - saveButton.disabled = count === 0; -} -function roadLabel(road) { return road.tags.name || `${road.highway}(OSM ${road.osmWayIds.join(", ")})`; } -function osmDirectionLabel(road) { return road?.direction === "forward" ? "沿 OSM 方向" : "逆 OSM 方向"; } -function roadIdFromLane(laneId) { return typeof laneId === "string" ? laneId.slice(5, laneId.lastIndexOf(":")) : ""; } -function laneIndex(laneId) { return Number(String(laneId).split(":").at(-1)); } -function lanePositionLabel(road, index) { return road?.laneCount === 1 ? "唯一车道" : `左起第 ${index} 车道`; } -function laneStyle(feature) { const roadId = feature.get("road_id"); const selected = Boolean(roadId && selectedRoad && roadId === selectedRoad.id); const composite = feature.get("cluster_preview"); return new Style({ stroke: new Stroke({ color: selected ? "#006e91" : "#f5f6ee", width: selected ? 3 : composite ? 1.6 : 1.3, lineDash: composite ? [7, 5] : [5, 4] }) }); } -function markingStyle(feature) { const yellow = feature?.get("color") === "yellow"; return new Style({ fill: new Fill({ color: yellow ? "#f5be2a" : "#f5f6ee" }), stroke: new Stroke({ color: yellow ? "#d29d16" : "#d9dacf", width: 1 }) }); } -function signalAssemblyStyle(feature) { const component = feature.get("signal_component"); if (component === "mast") return [new Style({ stroke: new Stroke({ color: "#fff", width: 9 }) }), new Style({ stroke: new Stroke({ color: "#007f99", width: 5 }) })]; if (component === "face") return [new Style({ stroke: new Stroke({ color: "#fff", width: 7 }) }), new Style({ stroke: new Stroke({ color: "#df2435", width: 3 }) })]; if (component === "head") { const heading = Number(feature.get("face_heading_deg")) || 0; return new Style({ image: new RegularShape({ points: 3, radius: 8, rotation: heading * Math.PI / 180, fill: new Fill({ color: "#df2435" }), stroke: new Stroke({ color: "#fff", width: 2 }) }) }); } return new Style({ image: new RegularShape({ points: 4, radius: 6, angle: Math.PI / 4, fill: new Fill({ color: "#263630" }), stroke: new Stroke({ color: "#fff", width: 2 }) }) }); } -function centerLineStyle(feature) { const white = feature.get("color") === "white"; const color = white ? "#faf9ee" : "#f5be2a"; return new Style({ fill: new Fill({ color }), stroke: new Stroke({ color: feature.get("pattern") === "solid" ? color : white ? "#aeb0aa" : "#d29d16", width: feature.get("pattern") === "solid" ? .25 : .8 }) }); } -function nativeSurfaceStyle(feature) { - // 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. - if (scenePreview) return new Style({ fill: new Fill({ color: "#3f4b50" }) }); - if (feature.get("cluster_id") && feature.get("complex_part")) return new Style({ fill: new Fill({ color: "#6f948a" }) }); - if (feature.get("kind") === "cluster") return new Style({ fill: new Fill({ color: "rgba(20, 132, 112, .5)" }), stroke: new Stroke({ color: "#075e4f", width: 4, lineDash: [10, 5] }) }); - if (feature.get("template")) return new Style({ fill: new Fill({ color: "rgba(20, 132, 112, .46)" }), stroke: new Stroke({ color: "#087c67", width: 3, lineDash: [7, 4] }) }); - return feature.get("native_id")?.startsWith("junction:") - ? new Style({ fill: new Fill({ color: "rgba(12, 116, 91, .38)" }), stroke: new Stroke({ color: "#0e785f", width: 1.5 }) }) - : new Style({ fill: new Fill({ color: "rgba(40, 105, 86, .35)" }), stroke: new Stroke({ color: "#296956", width: 1 }) }); -} -function sidewalkSurfaceStyle() { - return scenePreview - ? new Style({ fill: new Fill({ color: "#b7b9ad" }) }) - : new Style({ fill: new Fill({ color: "rgba(218, 191, 137, .6)" }), stroke: new Stroke({ color: "#9b7c40", width: 1 }) }); -} -function directionArrowFeature(geometry) { const middle = geometry.getCoordinateAt(.5); const before = geometry.getCoordinateAt(.48); const after = geometry.getCoordinateAt(.52); const length = Math.hypot(after[0] - before[0], after[1] - before[1]); if (length < .01) return null; return new Feature({ geometry: new Point(middle), rotation: Math.atan2(after[1] - before[1], after[0] - before[0]) }); } -function refreshOsmDirection() { const directionSource = layers.osmDirection.getSource(); directionSource.clear(); if (!selectedRoad) return; const centerline = new LineString(selectedRoad.centerline).transform("EPSG:4326", "EPSG:3857"); const arrow = directionArrowFeature(centerline); if (arrow) directionSource.addFeature(arrow); } -function refreshSelectedMovement() { const movementSource = layers.selectedMovement.getSource(); movementSource.clear(); if (!selectedMovement?.geometryPublished || !effectiveConnectorEnabled({ connection_id: selectedMovement.connectionId, fromLaneId: selectedMovement.fromLaneId, toLaneId: selectedMovement.toLaneId })) return; const feature = layers.connectors.getSource().getFeatures().find((candidate) => candidate.get("movement_id") === selectedMovement.id); if (feature) movementSource.addFeature(new Feature({ geometry: feature.getGeometry().clone() })); } -function roadForFeature(feature) { - const properties = feature.getProperties(); - const roadId = properties.road_id || properties.subjectId || roadIdFromLane(properties.from_lane_id) || properties.directional_road_ids?.split(",")[0]; - return state.compiled.model.roads.find((road) => road.id === roadId) || null; -} -function junctionForFeature(feature) { const id = feature.get("native_id"); return id?.startsWith("junction:") || id?.startsWith("junction-cluster:") ? layers.native.getSource().getFeatures().find((item) => item.get("native_id") === id) : null; } -function endpointFor(road, side) { return state.compiled.model.endpoints.find((endpoint) => endpoint.roadId === road?.id && endpoint.side === side) || null; } -function endpointsCompatible(from, to) { if (!from || !to || from.roadId === to.roadId || from.side !== "end" || to.side !== "start") return false; if (from.nodeId === to.nodeId) return true; const dx = (from.coordinate[0] - to.coordinate[0]) * 111320 * Math.cos(from.coordinate[1] * Math.PI / 180); const dy = (from.coordinate[1] - to.coordinate[1]) * 111320; return Math.hypot(dx, dy) <= 35; } -function readFeatures(collection, predicate = null) { const source = collection || { type: "FeatureCollection", features: [] }; const filtered = predicate ? { ...source, features: (source.features || []).filter(predicate) } : source; return geojson.readFeatures(filtered, { dataProjection: "EPSG:4326", featureProjection: "EPSG:3857" }); } -function rawRoadFeatures() { return state.compiled.model.roads.map((road) => new Feature({ geometry: new LineString(road.centerline).transform("EPSG:4326", "EPSG:3857"), road_id: road.id })); } -function updateSources() { - layers.reference.getSource().clear(); layers.reference.getSource().addFeatures(readFeatures(state.layers.osm2streetsRoadSurface)); - layers.gaodeReference.getSource().clear(); layers.gaodeReference.getSource().addFeatures(readFeatures(state.junctionReference?.converted)); - layers.native.getSource().clear(); layers.native.getSource().addFeatures([...readFeatures(state.layers.nativeRoadSurface), ...readFeatures(state.layers.nativeIntersectionSurface)]); - layers.sidewalks.getSource().clear(); layers.sidewalks.getSource().addFeatures(readFeatures(state.layers.nativeSidewalkSurface)); - layers.osm.getSource().clear(); layers.osm.getSource().addFeatures(rawRoadFeatures()); - layers.lanes.getSource().clear(); layers.lanes.getSource().addFeatures(readFeatures(state.layers.laneCenterlines, (feature) => !feature.properties?.cluster_internal && !feature.properties?.cluster_preview_hidden)); - layers.edgeLines.getSource().clear(); layers.edgeLines.getSource().addFeatures(readFeatures(state.layers.edgeLines)); - layers.directionArrows.getSource().clear(); layers.directionArrows.getSource().addFeatures(readFeatures(state.layers.directionArrows, (feature) => !feature.properties?.cluster_preview_hidden)); - layers.markings.getSource().clear(); layers.markings.getSource().addFeatures([...readFeatures(state.layers.laneSeparators, (feature) => !feature.properties?.cluster_preview_hidden), ...readFeatures(state.layers.turnArrows, (feature) => !feature.properties?.cluster_preview_hidden)]); - layers.centerLines.getSource().clear(); layers.centerLines.getSource().addFeatures(readFeatures(state.layers.centerLines, (feature) => !feature.properties?.cluster_preview_hidden)); - layers.controls.getSource().clear(); layers.controls.getSource().addFeatures([...readFeatures(state.layers.crosswalks), ...readFeatures(state.layers.vehicleStopLines)]); - const signalFeatures = readFeatures(state.trafficSignals?.assemblies || { type: "FeatureCollection", features: [] }); const armFeatures = []; const faceFeatures = []; const headFeatures = []; for (const signal of state.trafficRuntime?.signals || []) { const arm = signal.pose?.arm; const head = signal.pose?.head; if (!arm || !head) continue; const properties = { signal_uid: signal.id }; const headPoint = fromLonLat([head.longitude, head.latitude]); const radians = Number(head.faceHeadingDegrees) * Math.PI / 180; const faceEnd = [headPoint[0] + Math.sin(radians) * 2.5, headPoint[1] + Math.cos(radians) * 2.5]; armFeatures.push(new Feature({ geometry: new LineString([fromLonLat([arm.from.longitude, arm.from.latitude]), fromLonLat([arm.to.longitude, arm.to.latitude])]), signal_component: "mast", ...properties })); faceFeatures.push(new Feature({ geometry: new LineString([headPoint, faceEnd]), signal_component: "face", ...properties })); headFeatures.push(new Feature({ geometry: new Point(faceEnd), signal_component: "head", face_heading_deg: head.faceHeadingDegrees, ...properties })); } layers.signals.getSource().clear(); layers.signals.getSource().addFeatures([...armFeatures, ...faceFeatures, ...signalFeatures, ...headFeatures]); const pickerValue = signalPicker.value; signalPicker.replaceChildren(new Option("选择设施", "")); signalFeatures.forEach((feature) => signalPicker.add(new Option(feature.get("display_id") || feature.get("signal_uid"), feature.get("signal_uid")))); signalPicker.value = pickerValue; - layers.connectors.getSource().clear(); layers.connectors.getSource().addFeatures(readFeatures(state.layers.connectors, (feature) => !feature.properties?.cluster_internal)); - candidatesToggle.hidden = !(state.debug?.junctionCandidates || []).length; - renderJunctionCandidates(); - 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 })) })); - const extent = layers.osm.getSource().getExtent(); if (Number.isFinite(extent[0])) map.getView().fit(extent, { padding: [48, 48, 48, 48], maxZoom: 19 }); -} -// Draw one convex-ish hull per candidate cluster so the whole intersection is -// outlined, not just its centre, and label it with the same index the inspector -// and the console listing use. -function renderJunctionCandidates() { - const source = layers.junctionCandidates.getSource(); - source.clear(); - const list = state.debug?.junctionCandidates || []; - if (!list.length) return; - for (const candidate of list) { - const nodes = candidate.nodeIds.map((nodeId) => nodeCoordinate(nodeId)).filter(Boolean); - if (!nodes.length) continue; - const ring = candidateRing(nodes, Math.max(12, candidate.coreRadiusMeters * .6)); - source.addFeature(new Feature({ geometry: new Polygon([ring]), candidate_index: candidate.index, index: candidate.index, nodeCount: candidate.nodeCount, candidate_id: candidate.id })); - } -} -function nodeCoordinate(nodeId) { - for (const road of state.compiled.model.roads) { - const at = road.sourceNodeIds.findIndex((item) => String(item) === String(nodeId)); - if (at === 0) return road.centerline[0]; - if (at === road.sourceNodeIds.length - 1) return road.centerline.at(-1); - } - return null; -} -// A rounded envelope around the member nodes: sample a circle of `padMeters` -// around each node and take the outer boundary by angle from the centroid. -function candidateRing(nodes, padMeters) { - const centre = nodes.reduce((sum, point) => [sum[0] + point[0] / nodes.length, sum[1] + point[1] / nodes.length], [0, 0]); - const metresPerLon = 111320 * Math.cos(centre[1] * Math.PI / 180); - const points = []; - for (let degrees = 0; degrees < 360; degrees += 12) { - const radians = degrees * Math.PI / 180; - let best = null; - for (const node of nodes) { - const point = [node[0] + Math.sin(radians) * padMeters / metresPerLon, node[1] + Math.cos(radians) * padMeters / 111320]; - const reach = (point[0] - centre[0]) * metresPerLon * Math.sin(radians) + (point[1] - centre[1]) * 111320 * Math.cos(radians); - if (!best || reach > best.reach) best = { point, reach }; - } - points.push(fromLonLat(best.point)); - } - return [...points, points[0]]; -} -function selectJunctionCandidate(feature) { - const candidate = (state.debug?.junctionCandidates || []).find((item) => item.index === feature.get("candidate_index")); - if (!candidate) return; - selectedRoad = null; selectedMovement = null; selectedJunction = null; - form.hidden = true; hint.hidden = false; selectedJunctionPanel.hidden = true; - hint.textContent = `复杂路口候选 #${candidate.index}:${candidate.nodeCount} 个节点,直径 ${candidate.diameterMeters} 米。下方是可直接粘贴到 config 的片段。`; - evidence.textContent = JSON.stringify({ - 说明: "复制到区域配置文件的 nativeRoad.junctionTemplates.clusters 数组,然后重新编译", - 片段: { - id: `cluster-${candidate.nodeIds[0]}`, - template: candidate.template, - nodeIds: candidate.nodeIds, - coreRadiusMeters: candidate.coreRadiusMeters, - cornerRadiusMeters: 12, - outerRadiusExtraMeters: 18, - }, - 实测: { 节点数: candidate.nodeCount, 直径米: candidate.diameterMeters, 最长内部连接米: candidate.longestLinkMeters, 最宽进口米: candidate.widestApproachMeters }, - 提示: "coreRadiusMeters 是按节点跨度估的起点,配好后按实际效果调整;有高德参考几何时它会被校准值覆盖。", - }, null, 2); - renderCandidateAction(candidate); - message(`已选中复杂路口候选 #${candidate.index}`); -} -// The accept button lives beside the snippet so the manual path stays available -// if the write is refused; both describe the same cluster. -function renderCandidateAction(candidate) { - candidateAction.replaceChildren(); - const button = document.createElement("button"); - button.type = "button"; - button.textContent = `把候选 #${candidate.index} 加入配置并重新编译`; - button.onclick = async () => { - button.disabled = true; - message(`正在把候选 #${candidate.index} 写入区域配置...`); - try { - const response = await fetch("/api/junction-clusters", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ index: candidate.index }), cache: "no-store" }); - const result = await response.json(); - if (!response.ok || result.ok === false) throw new Error(result.error || `HTTP ${response.status}`); - state = result; - staged = []; - updateDirtyState(); updateSources(); renderDiagnostics(); renderSummary(); - candidateAction.replaceChildren(); - hint.textContent = `已加入复杂路口 ${result.added.id}(${result.added.nodeIds.length} 个节点),配置已更新并重新编译。`; - message(`已加入 ${result.added.id};如需撤销可 git checkout 区域配置`); - } catch (error) { - button.disabled = false; - message(`加入失败:${error.message}`); - } - }; - candidateAction.append(button); -} -function laneId(connection, side) { return connection[side === "from" ? "fromLaneId" : "toLaneId"] || connection[side === "from" ? "from_lane_id" : "to_lane_id"]; } -function effectiveLaneEnabled(connector) { const id = `车道连接:${laneId(connector, "from")}->${laneId(connector, "to")}`; const override = [...staged, ...state.overrides.overrides].find((item) => item.id === id); return override ? override.enabled : connector.enabled !== false; } -function effectiveConnectionEnabled(connection) { const id = `连接:${connection.id}`; const override = [...staged, ...state.overrides.overrides].find((item) => item.id === id); return override ? override.enabled : connection.enabled; } -function effectiveConnectorEnabled(connector) { const connection = state?.compiled.model.connections.find((item) => item.id === connector.connection_id); return effectiveLaneEnabled(connector) && (!connection || effectiveConnectionEnabled(connection)); } - -function selectRoad(road, note, movement = null) { - candidateAction.replaceChildren(); - selectedRoad = road; selectedMovement = movement; selectedJunction = null; selectedJunctionPanel.hidden = true; layers.osm.changed(); layers.lanes.changed(); layers.connectors.changed(); refreshOsmDirection(); refreshSelectedMovement(); - layers.selectedRoad.getSource().clear(); if (road) layers.selectedRoad.getSource().addFeature(new Feature({ geometry: new LineString(road.centerline).transform("EPSG:4326", "EPSG:3857") })); - form.hidden = !road; hint.hidden = Boolean(road); if (!road) return; - roadName.textContent = `${roadLabel(road)}(${osmDirectionLabel(road)})`; - widthInput.value = road.widthMeters; lanesInput.value = road.laneCount; leftInput.checked = road.sidewalkLeft; rightInput.checked = road.sidewalkRight; - evidence.textContent = JSON.stringify({ OSM道路: road.osmWayIds, 当前方向节点顺序: road.sourceNodeIds, 参数来源: road.provenance, 已应用修改: road.appliedOverrideIds, 原始标签: road.tags }, null, 2); - laneConvention.textContent = road.laneCount === 1 ? "蓝色箭头在 OSM 原始中心线上,表示当前方向;“沿 OSM 方向”即节点顺序。本方向只有一条车道。" : "蓝色箭头在 OSM 原始中心线上,表示当前方向;“沿 OSM 方向”即节点顺序。车道按行驶方向从左向右编号。"; - renderDirectionSwitch(road); renderMovementSummary(road); renderSelectedMovement(); renderConnections(road); message(note || (selectedMovement ? `已选中行驶动作:${turnLabel(selectedMovement.turn)}` : `已选中:${roadLabel(road)}`)); -} -function selectJunction(feature) { - candidateAction.replaceChildren(); - selectedJunction = feature; selectedRoad = null; selectedMovement = null; form.hidden = true; hint.hidden = true; selectedJunctionPanel.hidden = false; - const properties = feature.getProperties(); const roadIds = String(properties.source_road_ids || "").split(",").filter(Boolean); - const roads = roadIds.map((id) => state.compiled.model.roads.find((road) => road.id === id)).filter(Boolean); - junctionDetail.textContent = JSON.stringify({ OSM节点: properties.osm_node_id || properties.osm_node_ids, 类型: properties.kind === "cluster" ? "复合路口簇" : properties.kind === "t" ? "T字路口" : "十字路口", 参与方向道路: roads.map((road) => ({ 道路: roadLabel(road), OSM道路: road.osmWayIds, 节点顺序: road.sourceNodeIds })), 构面规则: properties.rule, 模板: properties.template, 边界策略: properties.boundary_mode, 基础截面面积平方米: properties.approach_area_m2, 最终路口面积平方米: properties.surface_area_m2, 外缘扩张倍率: properties.expansion_ratio, 路口退让距离米: properties.cutback_m, 行驶动作数: properties.movement_count, 已绘制连接数: properties.connector_count }, null, 2); - message(`已选中路口:OSM 节点 ${properties.osm_node_id || properties.osm_node_ids}`); -} -function selectSignal(feature) { - selectedSignal = feature.get("signal_uid"); const p = feature.getProperties(); signalPicker.value = selectedSignal; - const [x, y] = feature.getGeometry().getCoordinates(); - map.getView().fit([x - 25, y - 25, x + 25, y + 25], { padding: [80, 80, 80, 360], maxZoom: 22, duration: 250 }); - signalForm.hidden = false; signalOutput.textContent = `${p.display_id || p.signal_uid}(${p.signal_uid})`; - signalForm.lon.value = feature.getGeometry().clone().transform("EPSG:3857", "EPSG:4326").getCoordinates()[0]; - signalForm.lat.value = feature.getGeometry().clone().transform("EPSG:3857", "EPSG:4326").getCoordinates()[1]; - signalForm.mastHeading.value = p.mast_heading_deg; signalForm.mastReach.value = p.mast_reach_m; signalForm.faceHeading.value = p.face_heading_deg; signalForm.phase.value = p.phase_group; signalForm.enabled.checked = p.enabled; - evidence.textContent = JSON.stringify({ 信号灯: p.signal_uid, 控制节点: p.control_id, 路口方向: p.approach_id, 来源: state.trafficSignals.provenance }, null, 2); message("已选中原生红绿灯"); -} -function poleFeatureForSignal(signalUid) { return layers.signals.getSource().getFeatures().find((item) => item.get("signal_uid") === signalUid && !item.get("signal_component")); } -async function saveSignals(document) { const response = await fetch("/api/traffic-signals", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(document) }); const result = await response.json(); if (!result.ok) throw new Error(result.error); state.trafficSignals = result.trafficSignals; state.trafficRuntime = result.runtime; updateSources(); renderSummary(); } -function signalDocumentWithChange(change) { const document = structuredClone(state.trafficSignals); document.assemblies.features = change(document.assemblies.features); return document; } -signalForm.onsubmit = async (event) => { event.preventDefault(); try { await saveSignals(signalDocumentWithChange((features) => features.map((feature) => feature.properties.signal_uid !== selectedSignal ? feature : { ...feature, geometry: { type: "Point", coordinates: [Number(signalForm.lon.value), Number(signalForm.lat.value)] }, properties: { ...feature.properties, mast_heading_deg: Number(signalForm.mastHeading.value), mast_reach_m: Number(signalForm.mastReach.value), face_heading_deg: Number(signalForm.faceHeading.value), phase_group: Number(signalForm.phase.value), enabled: signalForm.enabled.checked } }))); message("红绿灯已保存"); } catch (error) { message(error.message); } }; -signalPanel.querySelector('[data-signal="delete"]').onclick = async () => { try { await saveSignals(signalDocumentWithChange((features) => features.filter((feature) => feature.properties.signal_uid !== selectedSignal))); signalForm.hidden = true; selectedSignal = null; message("红绿灯已删除"); } catch (error) { message(error.message); } }; -signalPanel.querySelector('[data-signal="generate"]').onclick = async () => { try { const response = await fetch("/api/traffic-signals/generate", { method: "POST" }); const result = await response.json(); if (!result.ok) throw new Error(result.error); state.trafficSignals = result.trafficSignals; state.trafficRuntime = result.runtime; updateSources(); message("已补充 OSM 信号灯"); } catch (error) { message(error.message); } }; -signalPicker.onchange = () => { const feature = poleFeatureForSignal(signalPicker.value); if (feature) selectSignal(feature); }; -function turnLabel(turn) { return { left: "左转", through: "直行", right: "右转", uturn: "掉头" }[turn] || turn; } -function renderSelectedMovement() { selectedMovementPanel.hidden = !selectedMovement; if (!selectedMovement) return; const targetRoad = state.compiled.model.roads.find((road) => road.id === selectedMovement.toRoadId); const geometry = selectedMovement.geometryStatus === "connector" ? "已绘制路径" : selectedMovement.geometryStatus === "continuous" ? "节点连续" : "路径过长未绘制"; movementDetail.textContent = `${turnLabel(selectedMovement.turn)}:${lanePositionLabel(selectedRoad, laneIndex(selectedMovement.fromLaneId))} → ${lanePositionLabel(targetRoad, laneIndex(selectedMovement.toLaneId))}\n目标:${roadLabel(targetRoad)}(${osmDirectionLabel(targetRoad)})\n来源端点:${selectedRoad.sourceNodeIds.at(-1)};目标端点:${targetRoad.sourceNodeIds[0]}\n路口节点:${selectedMovement.nodeId}\n状态:${geometry}\n来源:${selectedMovement.provenance}`; } -function renderDirectionSwitch(road) { - directionSwitch.innerHTML = ""; const alternatives = state.compiled.model.roads.filter((item) => item.osmWayIds.join(",") === road.osmWayIds.join(",")); - if (alternatives.length < 2) { directionSwitch.textContent = "单向道路"; return; } - for (const item of alternatives) { const button = document.createElement("button"); button.type = "button"; button.textContent = osmDirectionLabel(item); button.disabled = item.id === road.id; button.onclick = () => selectRoad(item); directionSwitch.append(button); } -} -function renderMovementSummary(road) { const movements = state.compiled.movements?.filter((movement) => movement.fromRoadId === road.id && effectiveConnectorEnabled({ connection_id: movement.connectionId, fromLaneId: movement.fromLaneId, toLaneId: movement.toLaneId })) || []; const turns = movements.reduce((result, item) => { result[item.turn] = (result[item.turn] || 0) + 1; return result; }, {}); const labels = { left: "左转", through: "直行", right: "右转", uturn: "掉头" }; const published = movements.filter((movement) => movement.geometryPublished).length; movementSummary.textContent = movements.length ? `已识别 ${movements.length} 个行驶动作,${published} 条已绘制路径:${Object.entries(turns).map(([key, value]) => `${labels[key] || key} ${value}`).join(",")}` : "当前方向没有已识别的行驶动作"; } -function renderConnections(road) { - connectionsBox.innerHTML = ""; const endpoint = state.compiled.model.endpoints.find((item) => item.roadId === road.id && item.side === "end"); addConnectionButton.hidden = !endpoint; const rows = state.compiled.model.connections.filter((connection) => connection.fromEndpointId === endpoint?.id); - if (!rows.length) connectionsBox.textContent = "当前方向到达终点后没有已识别的驶出道路。"; - for (const connection of rows) { const target = state.compiled.model.roads.find((item) => item.id === state.compiled.model.endpoints.find((endpointItem) => endpointItem.id === connection.toEndpointId)?.roadId); if (!target) continue; const label = document.createElement("label"); const input = document.createElement("input"); input.type = "checkbox"; input.checked = effectiveConnectionEnabled(connection); input.onchange = () => { stageConnection(connection, input.checked); selectRoad(road, "有未保存修改:转向路径已即时更新"); }; label.append(input, ` ${turnName(road, target)}:${roadLabel(target)}(${osmDirectionLabel(target)})`); connectionsBox.append(label); renderLaneControls(connection); } - renderManualCandidates(endpoint, road); -} -function renderManualCandidates(endpoint, road) { const candidates = state.compiled.diagnostics.find((item) => item.endpointId === endpoint?.id)?.manualCandidates || []; if (!candidates.length) return; const title = document.createElement("p"); title.textContent = "附近可手工连接的驶出方向"; connectionsBox.append(title); for (const candidate of candidates) { const target = state.compiled.model.roads.find((item) => item.id === candidate.roadId); if (!target) continue; const button = document.createElement("button"); button.type = "button"; button.textContent = `${roadLabel(target)}(${candidate.distanceMeters} 米)`; button.onclick = () => { stageConnection({ id: `connection:${endpoint.id}:${candidate.toEndpointId}`, fromEndpointId: endpoint.id, toEndpointId: candidate.toEndpointId }, true); selectRoad(road, "有未保存修改:手工连接已暂存;保存并重新生成后会出现转向路径"); }; connectionsBox.append(button); } } -function renderLaneControls(connection) { const rows = state.compiled.movements?.filter((movement) => movement.connectionId === connection.id) || []; for (const row of rows) { const targetRoad = state.compiled.model.roads.find((road) => road.id === row.toRoadId); const label = document.createElement("label"); const input = document.createElement("input"); input.type = "checkbox"; input.checked = effectiveLaneEnabled(row); input.onchange = () => { stageLaneConnection(row, input.checked); selectRoad(selectedRoad, "有未保存修改:转向路径已即时更新"); }; const geometryNote = row.geometryStatus === "continuous" ? ",节点连续" : row.geometryStatus === "deferred-too-long" ? ",路径过长未绘制" : ""; label.append(input, ` ${lanePositionLabel(selectedRoad, laneIndex(row.fromLaneId))} → ${lanePositionLabel(targetRoad, laneIndex(row.toLaneId))}(${osmDirectionLabel(targetRoad)}${geometryNote})`); connectionsBox.append(label); } } -function turnName(from, to) { const heading = (a, b) => Math.atan2(b[0] - a[0], b[1] - a[1]) * 180 / Math.PI; const delta = ((heading(to.centerline[0], to.centerline[1]) - heading(from.centerline.at(-2), from.centerline.at(-1)) + 540) % 360) - 180; return Math.abs(delta) >= 150 ? "掉头" : Math.abs(delta) <= 30 ? "直行" : delta > 0 ? "右转" : "左转"; } -function stageConnection(connection, enabled) { const id = `连接:${connection.id}`; staged = staged.filter((item) => item.id !== id); staged.push({ id, kind: "junction-connection", fromEndpointId: connection.fromEndpointId, toEndpointId: connection.toEndpointId, enabled }); layers.connectors.changed(); updateDirtyState(); } -function stageLaneConnection(connector, enabled) { const fromLaneId = laneId(connector, "from"); const toLaneId = laneId(connector, "to"); const id = `车道连接:${fromLaneId}->${toLaneId}`; staged = staged.filter((item) => item.id !== id); staged.push({ id, kind: "lane-connection", fromLaneId, toLaneId, enabled }); layers.connectors.changed(); updateDirtyState(); } -function chooseManualTarget(targetRoad) { const toEndpoint = endpointFor(targetRoad, "start"); if (!endpointsCompatible(manualFromEndpoint, toEndpoint)) return message("该方向的起点与当前道路终点不兼容:必须是同一路口,或相距不超过 35 米。"); const connection = { id: `connection:${manualFromEndpoint.id}:${toEndpoint.id}`, fromEndpointId: manualFromEndpoint.id, toEndpointId: toEndpoint.id }; manualFromEndpoint = null; stageConnection(connection, true); selectRoad(selectedRoad, "有未保存修改:手工连接已暂存;保存并重新生成后会出现转向路径"); } -addConnectionButton.onclick = () => { const endpoint = endpointFor(selectedRoad, "end"); if (!endpoint) return; manualFromEndpoint = endpoint; select.getFeatures().clear(); message("请在地图上点击目标方向的 OSM 中心线;仅同一路口或 35 米内的驶出方向可连接。"); }; -function focusDiagnostic(item) { const feature = layers.diagnostics.getSource().getFeatures().find((candidate) => candidate.get("id") === item.id); if (feature) map.getView().fit(feature.getGeometry().getExtent(), { padding: [80, 80, 80, 360], maxZoom: 18, duration: 250 }); const junction = layers.native.getSource().getFeatures().find((candidate) => candidate.get("native_id") === item.subjectId); if (junction) return selectJunction(junction); selectRoad(state.compiled.model.roads.find((road) => road.id === item.subjectId), `已定位:${item.message}`); } -function diagnosticLabel(item) { const road = state.compiled.model.roads.find((candidate) => candidate.id === item.subjectId); if (item.rule !== "unconnected-interior-road-end" || !road) return item.message; const candidateCount = item.manualCandidates?.length || 0; return `${roadLabel(road)}(${osmDirectionLabel(road)},节点 ${item.sourceIds[0]}):内部端点未连接${candidateCount ? `,附近有 ${candidateCount} 个可手工连接候选` : ""}`; } -function renderDiagnostics() { const all = state.compiled.diagnostics.filter((diagnostic) => diagnostic.rule !== "ordinary-junction-surface"); const counts = { all: all.length, candidates: all.filter((item) => item.manualCandidates?.length).length, other: all.filter((item) => !item.manualCandidates?.length).length }; for (const button of diagnosticFilters.querySelectorAll("button")) { const filter = button.dataset.diagnosticFilter; button.classList.toggle("active", filter === diagnosticFilter); button.textContent = `${filter === "all" ? "全部" : filter === "candidates" ? "可连接" : "其他"}(${counts[filter]})`; } const visible = all.filter((item) => diagnosticFilter === "all" || diagnosticFilter === "candidates" ? Boolean(item.manualCandidates?.length) : !item.manualCandidates?.length).sort((a, b) => (b.manualCandidates?.length || 0) - (a.manualCandidates?.length || 0)); diagnostics.innerHTML = ""; for (const item of visible) { const button = document.createElement("button"); button.textContent = diagnosticLabel(item); button.onclick = () => focusDiagnostic(item); diagnostics.append(button); } } -function renderSummary() { - const comparison = state.comparison; - const rows = [ - ["方向道路", comparison.nativeRoadCount], - ["路缘与步行带", comparison.nativeSidewalkSurfaceFeatures], - ["路口面", comparison.nativeJunctionSurfaceFeatures], - ["普通构面路口", comparison.nativeApproachEnvelopeJunctions], - ["兜底构面路口", comparison.nativeFallbackJunctions], - ["最大外缘扩张", comparison.nativeMaxJunctionExpansionRatio], - ["道路中心虚线", comparison.nativeCenterLineFeatures], - ["道路方向箭头", comparison.nativeDirectionArrowFeatures], - ["路口转向箭头", comparison.nativeTurnArrowFeatures], - ["斑马线条带", comparison.nativeCrosswalkFeatures], - ["停止线", comparison.nativeVehicleStopLineFeatures], - ["红绿灯设施", state.trafficSignals?.assemblies?.features?.length || 0], - ["行驶动作", comparison.nativeMovementCount], - ["已绘制路径", comparison.nativePublishedMovementCount], - ["可手工复核", comparison.unconnectedEndsWithManualCandidates], - ["内部断头", comparison.unconnectedInteriorRoadEnds], - ["osm2streets 参考", comparison.osm2streetsAvailable ? comparison.osm2streetsRoadSurfaceFeatures : "无"], - ]; - summary.innerHTML = ""; - for (const [label, value] of rows) { - const term = document.createElement("dt"); const detail = document.createElement("dd"); - term.textContent = label; detail.textContent = value; summary.append(term, detail); - } -} -function stageRoadOverride(road, changes) { const id = `道路:${road.id}`; const existing = staged.find((item) => item.id === id) || state.overrides.overrides.find((item) => item.id === id); staged = staged.filter((item) => item.id !== id); staged.push({ ...existing, id, kind: "road", roadId: road.id, ...changes }); } -function setMarkingStyleForm(title, target, style, allowsDouble) { centerLineForm.hidden = false; markingStyleHeading.textContent = title; centerLineSegment.textContent = target; doubleYellowOption.hidden = !allowsDouble; centerLineStyleInput.value = style || "yellow-dashed"; } -function selectCenterLine(feature) { selectedCenterLineSegment = feature.get("segment_id"); selectedLaneSeparator = null; setMarkingStyleForm("道路中心线样式", `道路段:${selectedCenterLineSegment}`, feature.get("effective_style"), true); } -function selectLaneSeparator(feature) { selectedCenterLineSegment = null; selectedLaneSeparator = feature.getProperties(); setMarkingStyleForm("车道分隔线样式", `第 ${selectedLaneSeparator.left_lane_index} 与第 ${selectedLaneSeparator.right_lane_index} 车道之间`, selectedLaneSeparator.effective_style || "white-dashed", false); } -function selectEdgeLine(feature) { selectedCenterLineSegment = null; selectedLaneSeparator = null; selectedEdgeLine = feature.getProperties(); setMarkingStyleForm("道路外缘线样式", `${selectedEdgeLine.side === "left" ? "左" : "右"}侧外缘`, selectedEdgeLine.effective_style || "white-solid", false); } -function clearCenterLineSelection() { selectedCenterLineSegment = null; selectedLaneSeparator = null; selectedEdgeLine = null; centerLineForm.hidden = true; } -function stageCenterLineStyle(segmentId, style) { const parts = style.split("-"); const double = parts[0] === "double"; const [color, pattern] = double ? parts.slice(1) : parts; const id = `道路中心线:${segmentId}`; const existing = staged.find((item) => item.id === id) || state.overrides.overrides.find((item) => item.id === id); staged = staged.filter((item) => item.id !== id); staged.push({ ...existing, id, kind: "center-line-style", segmentId, color, pattern, double }); } -form.onsubmit = (event) => { event.preventDefault(); const roadChanges = { widthMeters: Number(widthInput.value), laneCount: Number(lanesInput.value), sidewalkLeft: leftInput.checked, sidewalkRight: rightInput.checked }; stageRoadOverride(selectedRoad, roadChanges); const opposite = state.compiled.model.roads.find((road) => road.id !== selectedRoad.id && road.segmentId === selectedRoad.segmentId); if (opposite) stageRoadOverride(opposite, { sidewalkLeft: rightInput.checked, sidewalkRight: leftInput.checked }); updateDirtyState(); message(opposite ? "有未保存修改:双向道路的路缘与步行带已按实际侧边同步" : "有未保存修改"); }; -function stageSelectedCenterLineStyle() { if (!selectedCenterLineSegment && !selectedLaneSeparator && !selectedEdgeLine) return; if (selectedCenterLineSegment) stageCenterLineStyle(selectedCenterLineSegment, centerLineStyleInput.value); else { const [color, pattern] = centerLineStyleInput.value.split("-"); const item = selectedLaneSeparator || selectedEdgeLine; const id = selectedLaneSeparator ? `车道分隔线:${item.road_id}:${item.left_lane_index}-${item.right_lane_index}` : `道路外缘线:${item.road_id}:${item.side}`; staged = staged.filter((change) => change.id !== id); staged.push(selectedLaneSeparator ? { id, kind: "lane-separator-style", roadId: item.road_id, leftLaneIndex: item.left_lane_index, rightLaneIndex: item.right_lane_index, color, pattern } : { id, kind: "edge-line-style", roadId: item.road_id, side: item.side, color, pattern }); } updateDirtyState(); message("有未保存修改:线样式"); } -centerLineForm.onsubmit = (event) => { event.preventDefault(); stageSelectedCenterLineStyle(); }; -centerLineStyleInput.onchange = stageSelectedCenterLineStyle; -async function saveStagedChanges() { if (!staged.length) return true; const existing = state.overrides.overrides.filter((item) => !staged.some((change) => change.id === item.id)); const response = await fetch("/api/overrides", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ schema: "native-road-overrides/v1", overrides: [...existing, ...staged] }) }); const result = await response.json(); if (!result.ok) { message(result.error); return false; } state.overrides = result.overrides; staged = []; updateDirtyState(); return true; } -saveButton.onclick = async () => { if (await saveStagedChanges()) message("已保存,点击“保存并重新生成”写入几何"); }; -compileButton.onclick = async () => { - if (!await saveStagedChanges()) return; - message("正在保存修改并重新生成..."); - const response = await fetch("/api/compile", { method: "POST", cache: "no-store" }); - const nextState = await response.json(); - if (!response.ok || nextState.ok === false || !nextState.compiled?.model || !nextState.layers) { - message(`重新生成失败:${nextState.error || `HTTP ${response.status}`}`); - return; - } - state = nextState; - staged = []; - updateDirtyState(); - updateSources(); - renderDiagnostics(); - renderSummary(); - selectRoad(selectedRoad ? state.compiled.model.roads.find((road) => road.id === selectedRoad.id) : null); - message("已保存并重新生成"); -}; -for (const input of document.querySelectorAll("[data-layer]")) input.onchange = () => { const visible = input.checked; layers[input.dataset.layer].setVisible(visible); if (input.dataset.layer === "osm") layers.osmDirection.setVisible(visible); }; -scenePreviewToggle.onchange = () => { - scenePreview = scenePreviewToggle.checked; - for (const input of document.querySelectorAll("[data-layer]")) { - const layer = input.dataset.layer; - if (["osm", "lanes", "reference", "gaodeReference"].includes(layer)) layers[layer].setVisible(!scenePreview && input.checked); - } - layers.osmDirection.setVisible(!scenePreview && document.querySelector('[data-layer="osm"]').checked); - layers.connectors.setVisible(!scenePreview && document.querySelector('[data-layer="lanes"]').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); - const signalsVisible = document.querySelector('[data-layer="signals"]').checked; - layers.signals.setVisible(signalsVisible); - layers.diagnostics.setVisible(!scenePreview); - layers.native.changed(); layers.sidewalks.changed(); - message(scenePreview ? "场景效果预览:当前编译面" : "编辑图层预览"); -}; -for (const button of diagnosticFilters.querySelectorAll("button")) button.onclick = () => { diagnosticFilter = button.dataset.diagnosticFilter; renderDiagnostics(); }; -fetch("/api/state").then((response) => response.json()).then((value) => { state = value; updateDirtyState(); updateSources(); renderDiagnostics(); renderSummary(); areaLabel.textContent = state.areaId; const signalUid = new URLSearchParams(location.search).get("signal"); const signal = signalUid && poleFeatureForSignal(signalUid); if (signal) selectSignal(signal); message(`已加载 ${state.compiled.model.roads.length} 条方向道路`); }).catch((error) => message(error.message)); diff --git a/packages/road-compiler/workbench/client/index.html b/packages/road-compiler/workbench/client/index.html deleted file mode 100644 index 528b28e..0000000 --- a/packages/road-compiler/workbench/client/index.html +++ /dev/null @@ -1,4 +0,0 @@ - -道路编译工作台 -
道路编译工作台
-
diff --git a/packages/road-compiler/workbench/server.js b/packages/road-compiler/workbench/server.js deleted file mode 100644 index 9957fe0..0000000 --- a/packages/road-compiler/workbench/server.js +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs"); -const http = require("http"); -const path = require("path"); -const { loadOverrides, validateOverrides, writeJsonAtomic } = require("../src/compile/native-road"); -const { generate, validateDocument, runtime } = require("../src/native-traffic-signals"); -const { convertGeoJson } = require("../src/reference/gaode"); - -function startWorkbench({ area, configPath, repoRoot, compileFresh, readAreaConfig, junctionReference = null, debug = false, port = 8787 }) { - if (typeof junctionReference === "string") junctionReference = readJunctionReference(junctionReference); - // `--debug` surfaces advisory compiler findings that have no geometry layer of - // their own — currently the complex-junction candidates. Off by default so the - // normal editing view stays uncluttered. - if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("--port must be an integer in [1024, 65535]."); - const context = { repoRoot, configPath, compileFresh, readAreaConfig }; - const server = http.createServer((request, response) => handle(request, response, area, context, junctionReference, debug)); - server.on("error", (error) => { - console.error(`Road Workbench failed to listen: ${error.message}`); - process.exitCode = 1; - }); - server.listen(port, "127.0.0.1", () => console.log(`Road Workbench: http://127.0.0.1:${port}/${debug ? " (debug: 复杂路口候选已开启)" : ""}`)); - return server; -} - -function handle(request, response, area, context, junctionReference, debug = false) { - const url = new URL(request.url, "http://127.0.0.1"); - if (request.method === "GET" && url.pathname === "/") return sendFile(response, path.join(__dirname, "client", "index.html"), "text/html; charset=utf-8"); - if (request.method === "GET" && url.pathname === "/app.js") return sendFile(response, path.join(__dirname, "client", "app.js"), "text/javascript; charset=utf-8"); - if (request.method === "GET" && url.pathname === "/app.css") return sendFile(response, path.join(__dirname, "client", "app.css"), "text/css; charset=utf-8"); - if (request.method === "GET" && url.pathname.startsWith("/vendor/")) return sendVendorFile(response, url.pathname, context.repoRoot); - if (request.method === "GET" && url.pathname === "/api/state") return sendJson(response, 200, state(area, junctionReference, debug)); - if (request.method === "POST" && url.pathname === "/api/traffic-signals") return readBody(request).then((body) => { - const document = validateDocument(body, fs.readFileSync(area.input, "utf8")); - writeJsonAtomic(area.outputs.nativeTrafficSignals, document); - sendJson(response, 200, { ok: true, trafficSignals: document, runtime: runtime(document) }); - }).catch((error) => sendJson(response, 400, { ok: false, error: error.message })); - if (request.method === "POST" && url.pathname === "/api/traffic-signals/generate") return Promise.resolve().then(() => { - const compiled = readCompiled(area); - const generated = generate(fs.readFileSync(area.input, "utf8"), readLayer(path.join(area.outputs.nativeRoadDir, "layers", "vehicle_stop_lines.geojson")), readLayer(path.join(area.outputs.nativeRoadDir, "layers", "intersection_surface.geojson"))); - const current = validateDocument(readJson(area.outputs.nativeTrafficSignals), fs.readFileSync(area.input, "utf8")); - const present = new Set(current.assemblies.features.map((feature) => feature.properties.signal_uid)); - current.assemblies.features.push(...generated.assemblies.features.filter((feature) => !present.has(feature.properties.signal_uid))); - writeJsonAtomic(area.outputs.nativeTrafficSignals, current); - sendJson(response, 200, { ok: true, trafficSignals: current, runtime: runtime(current), generated: generated.assemblies.features.length, compiled: Boolean(compiled) }); - }).catch((error) => sendJson(response, 400, { ok: false, error: error.message })); - if (request.method === "POST" && url.pathname === "/api/overrides") return readBody(request).then((body) => { - const compiled = readCompiled(area); - const overrides = validateOverrides(body, { roads: compiled.model.roads, endpoints: compiled.model.endpoints }); - writeJsonAtomic(area.outputs.nativeRoadOverrides, overrides); - sendJson(response, 200, { ok: true, overrides }); - }).catch((error) => sendJson(response, 400, { ok: false, error: error.message })); - if (request.method === "POST" && url.pathname === "/api/junction-clusters") return readBody(request).then((body) => { - if (!debug) throw new Error("该接口仅在 --debug 模式下可用。"); - const added = addJunctionCluster(context.configPath, body, readCompiled(area), context.readAreaConfig, context.repoRoot); - context.compileFresh(); - const refreshed = context.readAreaConfig(context.configPath, { repoRoot: context.repoRoot }); - sendJson(response, 200, { ok: true, added, ...state(refreshed, junctionReference, debug) }); - }).catch((error) => sendJson(response, 400, { ok: false, error: error.message })); - if (request.method === "POST" && url.pathname === "/api/compile") return Promise.resolve().then(() => { - context.compileFresh(); - sendJson(response, 200, state(area, junctionReference)); - }).catch((error) => sendJson(response, 500, { ok: false, error: error.message })); - sendJson(response, 404, { error: "Not found" }); -} - -function state(area, junctionReference = null, debug = false) { - const nativeDir = area.outputs.nativeRoadDir; - const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson"); - const trafficSignals = fs.existsSync(area.outputs.nativeTrafficSignals) - ? validateDocument(readJson(area.outputs.nativeTrafficSignals), fs.readFileSync(area.input, "utf8")) - : { schema: "native-traffic-signals/v1", provenance: "empty", assemblies: { type: "FeatureCollection", features: [] } }; - const trafficRuntime = runtime(trafficSignals); - const compiled = readCompiled(area); - return { areaId: area.id, debug: debug ? { junctionCandidates: junctionCandidates(compiled) } : null, compiled, overrides: loadOverrides(area.outputs.nativeRoadOverrides), trafficSignals, trafficRuntime, comparison: readJson(path.join(nativeDir, "comparison.json")), junctionReference, layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), edgeLines: readLayer(path.join(nativeDir, "layers", "edge_lines.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 } }; -} -// The compiler reports candidates as advisory diagnostics. Lift them into their -// own payload with a stable index so the map can label them "#1, #2, ..." and -// the inspector can offer a ready-to-paste cluster配置. -// Append one detected cluster to the hand-authored area config. The candidate -// must still be present in the latest compile, so a stale browser tab cannot -// write a cluster that no longer exists. The edited config is validated by the -// real loader before it replaces the file: an invalid write would break every -// later command, and the file is git-tracked so a bad accept stays revertible. -function addJunctionCluster(configPath, body, compiled, readAreaConfig, repoRoot) { - const index = Number(body?.index); - if (!Number.isInteger(index)) throw new Error("请求缺少候选编号 index。"); - const candidate = junctionCandidates(compiled).find((item) => item.index === index); - if (!candidate) throw new Error(`候选 #${index} 不在最新一次编译结果里,请刷新页面后重试。`); - const raw = readJson(configPath); - const templates = raw.nativeRoad?.junctionTemplates; - if (!templates) throw new Error("区域配置缺少 nativeRoad.junctionTemplates,请先手工建立该节点。"); - const clusters = Array.isArray(templates.clusters) ? templates.clusters : []; - const taken = new Set(clusters.flatMap((cluster) => (cluster.nodeIds || []).map(String))); - const clash = candidate.nodeIds.filter((nodeId) => taken.has(String(nodeId))); - if (clash.length) throw new Error(`节点 ${clash.join("、")} 已属于其他复杂路口配置。`); - const id = uniqueClusterId(`cluster-${candidate.nodeIds[0]}`, new Set(clusters.map((cluster) => cluster.id))); - const cluster = { - id, - template: candidate.template, - coreRadiusMeters: candidate.coreRadiusMeters, - cornerRadiusMeters: 12, - outerRadiusExtraMeters: 18, - nodeIds: candidate.nodeIds.map(String), - }; - const next = { ...raw, nativeRoad: { ...raw.nativeRoad, junctionTemplates: { ...templates, enabled: true, clusters: [...clusters, cluster] } } }; - const staging = `${configPath}.candidate-${process.pid}.json`; - fs.writeFileSync(staging, `${JSON.stringify(next, null, 2)}\n`); - try { - readAreaConfig(staging, { repoRoot }); - } catch (error) { - fs.unlinkSync(staging); - throw new Error(`写入后的配置无法通过校验,已放弃:${error.message}`); - } - fs.unlinkSync(staging); - writeJsonAtomic(configPath, next); - return cluster; -} -function uniqueClusterId(base, taken) { - if (!taken.has(base)) return base; - for (let suffix = 2; suffix < 100; suffix += 1) if (!taken.has(`${base}-${suffix}`)) return `${base}-${suffix}`; - throw new Error("无法生成唯一的 cluster id。"); -} - -function junctionCandidates(compiled) { - return (compiled?.diagnostics || []) - .filter((item) => item.rule === "complex-junction-candidate" && item.suggestedCluster) - .sort((first, second) => second.suggestedCluster.nodeCount - first.suggestedCluster.nodeCount || first.suggestedCluster.diameterMeters - second.suggestedCluster.diameterMeters) - .map((item, index) => ({ index: index + 1, id: item.id, message: item.message, coordinate: item.geometry?.coordinates || null, ...item.suggestedCluster })); -} - -function readJunctionReference(file) { - if (!fs.existsSync(file)) throw new Error(`Junction reference not found: ${file}`); - const converted = convertGeoJson(JSON.parse(fs.readFileSync(file, "utf8"))); - return { source: file, coordinateSystem: "GCJ-02", converted }; -} -function readCompiled(area) { return readJson(path.join(area.outputs.nativeRoadDir, "compiled.json")); } -function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } -function readLayer(file) { return fs.existsSync(file) ? readJson(file) : { type: "FeatureCollection", features: [] }; } -function readBody(request) { return new Promise((resolve, reject) => { let body = ""; request.setEncoding("utf8"); request.on("data", (part) => { body += part; if (body.length > 1024 * 1024) request.destroy(); }); request.on("end", () => { try { resolve(JSON.parse(body)); } catch (_) { reject(new Error("Request body must be JSON.")); } }); request.on("error", reject); }); } -function sendFile(response, file, type) { response.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" }); fs.createReadStream(file).pipe(response); } -function sendVendorFile(response, pathname, repoRoot) { - const match = /^\/vendor\/(ol|rbush|quickselect)\/(.+)$/.exec(pathname); - if (!match) return sendJson(response, 404, { error: "Not found" }); - const root = path.join(repoRoot, "node_modules", match[1]); - const file = path.resolve(root, match[2]); - if (!file.startsWith(`${root}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) return sendJson(response, 404, { error: "Not found" }); - return sendFile(response, file, file.endsWith(".css") ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8"); -} -function sendJson(response, status, value) { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }); response.end(`${JSON.stringify(value)}\n`); } -module.exports = { startWorkbench }; diff --git a/scripts/build-area.js b/scripts/build-area.js index f19eb1b..04322f5 100755 --- a/scripts/build-area.js +++ b/scripts/build-area.js @@ -4,7 +4,7 @@ const fs = require("fs"); const path = require("path"); const { spawnSync } = require("child_process"); const { readAreaConfig } = require("./lib/area-config"); -const { compileArea: compileNativeRoads } = require("./compile-native-roads"); +const { compileArea: compileNativeRoads } = require("./lib/road-compiler-cli"); const { resolveStages } = require("./lib/build-stages"); const { validateManifest, addIntegrity } = require("./lib/package-contract"); const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths"); @@ -226,7 +226,7 @@ function buildBlenderScene(area, roadProvider) { fs.mkdirSync(path.dirname(area.outputs.blend), { recursive: true }); fs.mkdirSync(path.dirname(area.outputs.render), { recursive: true }); if (roadProvider === "native") { - compileNativeRoads(configPath); + compileNativeRoads(area); ensureNativeRoadLayers(area); } diff --git a/scripts/build-osm2streets-qgis.js b/scripts/build-osm2streets-qgis.js index b265a09..05b3972 100755 --- a/scripts/build-osm2streets-qgis.js +++ b/scripts/build-osm2streets-qgis.js @@ -6,7 +6,7 @@ const os = require("os"); const { execFileSync } = require("child_process"); const { JsStreetNetwork } = require("osm2streets-js-node"); const { qgisPaths } = require("./lib/tool-paths"); -const { buildCustomTurnLaneArrows } = require("../packages/road-compiler/src/compile/turn-lane-arrows"); +const { turnLaneArrows: { buildCustomTurnLaneArrows } } = require("@osm-asset/road-compiler"); const { readTrafficSignalFeatures } = require("./lib/traffic-signals"); const { SCENE_LAYERS, diff --git a/scripts/check-native-roads.js b/scripts/check-native-roads.js index c51eac3..18b6da1 100644 --- a/scripts/check-native-roads.js +++ b/scripts/check-native-roads.js @@ -4,14 +4,14 @@ const path = require("path"); const { readAreaConfig } = require("./lib/area-config"); const { compileArea, parseArgs } = require("./compile-native-roads"); -const { checkOutput } = require("../packages/road-compiler/src/check"); +const { check } = require("@osm-asset/road-compiler"); const repoRoot = path.resolve(__dirname, ".."); function checkArea(configPath, options = {}) { if (options.compile) compileArea(configPath); const area = readAreaConfig(configPath, { repoRoot }); - return checkOutput({ areaId: area.id, outDir: area.outputs.nativeRoadDir }); + return check.checkOutput({ areaId: area.id, outDir: area.outputs.nativeRoadDir }); } function main() { diff --git a/scripts/compile-native-roads.js b/scripts/compile-native-roads.js index 60c9739..83675cf 100644 --- a/scripts/compile-native-roads.js +++ b/scripts/compile-native-roads.js @@ -2,8 +2,8 @@ "use strict"; const path = require("path"); -const { readAreaConfig, toRoadCompilerInput } = require("./lib/area-config"); -const { compileInput } = require("../packages/road-compiler/src/compile/compiler"); +const { readAreaConfig } = require("./lib/area-config"); +const { compileArea: compileWithCli } = require("./lib/road-compiler-cli"); const repoRoot = path.resolve(__dirname, ".."); @@ -19,15 +19,13 @@ function parseArgs(argv) { function compileArea(configPath) { const area = readAreaConfig(configPath, { repoRoot }); - const compiled = compileInput(toRoadCompilerInput(area)); - return { ...compiled, area }; + return { area, marker: compileWithCli(area) }; } function main() { const args = parseArgs(process.argv.slice(2)); const configPath = path.resolve(args.config || path.join(repoRoot, "config", "areas", "nantaizi-lake-innovation-valley.json")); - const { area, result, comparison } = compileArea(configPath); - console.log(`NATIVE_ROAD_COMPILE_DONE ${JSON.stringify({ areaId: area.id, roads: result.model.roads.length, endpoints: result.model.endpoints.length, diagnostics: result.diagnostics.length, output: area.outputs.nativeRoadDir, comparison })}`); + compileArea(configPath); } if (require.main === module) main(); diff --git a/scripts/inspect-junction-reference.js b/scripts/inspect-junction-reference.js index 8ba6e2a..0661bbc 100644 --- a/scripts/inspect-junction-reference.js +++ b/scripts/inspect-junction-reference.js @@ -3,7 +3,7 @@ const fs = require("fs"); const path = require("path"); -const { inspectReference, localReferenceSvg } = require("../packages/road-compiler/src/reference/gaode"); +const { gaodeReference: { inspectReference, localReferenceSvg } } = require("@osm-asset/road-compiler"); function parseArgs(argv) { const result = {}; diff --git a/scripts/lib/road-compiler-cli.js b/scripts/lib/road-compiler-cli.js new file mode 100644 index 0000000..f242d56 --- /dev/null +++ b/scripts/lib/road-compiler-cli.js @@ -0,0 +1,47 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const { toRoadCompilerInput } = require("./area-config"); + +function compilerCli() { + const packageFile = require.resolve("@osm-asset/road-compiler/package.json"); + const packageRoot = path.dirname(packageFile); + const manifest = require(packageFile); + return path.join(packageRoot, typeof manifest.bin === "string" ? manifest.bin : manifest.bin["road-compiler"]); +} + +function writeInput(area) { + const input = toRoadCompilerInput(area); + fs.mkdirSync(area.outputs.pipelineDir, { recursive: true }); + const file = path.join(area.outputs.pipelineDir, "road-compiler.input.json"); + fs.writeFileSync(file, `${JSON.stringify(input, null, 2)}\n`); + return { file, input }; +} + +function parseCompletionMarker(stdout, input) { + const lines = String(stdout).split(/\r?\n/).filter((line) => line.startsWith("NATIVE_ROAD_COMPILE_DONE ")); + if (lines.length !== 1) throw new Error(`Expected exactly one NATIVE_ROAD_COMPILE_DONE marker, found ${lines.length}`); + let marker; + try { marker = JSON.parse(lines[0].slice("NATIVE_ROAD_COMPILE_DONE ".length)); } + catch (error) { throw new Error(`Invalid NATIVE_ROAD_COMPILE_DONE JSON: ${error.message}`); } + if (!marker || typeof marker !== "object") throw new Error("NATIVE_ROAD_COMPILE_DONE payload must be an object"); + if (marker.areaId !== input.areaId) throw new Error(`NATIVE_ROAD_COMPILE_DONE areaId mismatch: expected ${input.areaId}, got ${marker.areaId}`); + if (path.resolve(marker.output || "") !== path.resolve(input.outDir)) throw new Error(`NATIVE_ROAD_COMPILE_DONE output mismatch: expected ${input.outDir}, got ${marker.output}`); + return marker; +} + +function compileArea(area) { + const { file, input } = writeInput(area); + const result = spawnSync(process.execPath, [compilerCli(), "--input", file], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }); + if (result.stdout) process.stdout.write(result.stdout); + if (result.error) throw result.error; + if (result.status !== 0) { + const signal = result.signal ? ` signal=${result.signal}` : ""; + throw new Error(`Native road compiler failed with status=${result.status}${signal}`); + } + return parseCompletionMarker(result.stdout, input); +} + +module.exports = { compileArea, compilerCli, parseCompletionMarker, writeInput }; diff --git a/scripts/lib/traffic-signals.js b/scripts/lib/traffic-signals.js index a203351..389e075 100644 --- a/scripts/lib/traffic-signals.js +++ b/scripts/lib/traffic-signals.js @@ -1,8 +1,7 @@ "use strict"; const fs = require("fs"); -const { parseOsm } = require("../../packages/road-compiler/src/osm"); -const trafficSignals = require("../../packages/road-compiler/src/traffic-signals"); +const { osm: { parseOsm }, trafficSignals } = require("@osm-asset/road-compiler"); function readTrafficSignalFeatures(stopLinePath, intersectionPath, osmPath) { const controls = parseOsm(fs.readFileSync(osmPath, "utf8")).trafficSignalControls; diff --git a/scripts/lib/vehicle-route.js b/scripts/lib/vehicle-route.js index b2270a8..0880629 100644 --- a/scripts/lib/vehicle-route.js +++ b/scripts/lib/vehicle-route.js @@ -1,8 +1,7 @@ "use strict"; const fs = require("fs"); -const { parseOsm } = require("../../packages/road-compiler/src/osm"); -const { +const { osm: { parseOsm }, laneGeometry: { appendCoordinates, haversineMeters, laneCenterline, @@ -10,7 +9,7 @@ const { orientPolyline, polylineLength, polylineMidpoint, -} = require("../../packages/road-compiler/src/geometry/lane-geometry"); +} } = require("@osm-asset/road-compiler"); const MAX_ROUTES = 5; const MAX_PATH_EDGES = 7; diff --git a/scripts/reimport-gpkg.js b/scripts/reimport-gpkg.js index b9adfa0..6003d1c 100755 --- a/scripts/reimport-gpkg.js +++ b/scripts/reimport-gpkg.js @@ -17,7 +17,7 @@ const path = require("path"); const os = require("os"); const { execFileSync } = require("child_process"); const { qgisPaths } = require("./lib/tool-paths"); -const { parseOsm } = require("../packages/road-compiler/src/osm"); +const { osm: { parseOsm } } = require("@osm-asset/road-compiler"); const { validateTrafficSignalSourceReferences, } = require("./lib/traffic-signals"); diff --git a/scripts/render-turn-lane-arrow-samples.js b/scripts/render-turn-lane-arrow-samples.js index 8c72da5..9fdd5d0 100644 --- a/scripts/render-turn-lane-arrow-samples.js +++ b/scripts/render-turn-lane-arrow-samples.js @@ -3,7 +3,7 @@ const fs = require("fs"); const path = require("path"); -const { templateFor } = require("../packages/road-compiler/src/compile/turn-lane-arrows"); +const { turnLaneArrows: { templateFor } } = require("@osm-asset/road-compiler"); const output = path.resolve(process.argv[2] || path.join("outputs", "turn-lane-arrow-samples.svg")); const DISPLAY_SCALE = 60; diff --git a/scripts/road-workbench.js b/scripts/road-workbench.js index 03f87d3..0a7d933 100644 --- a/scripts/road-workbench.js +++ b/scripts/road-workbench.js @@ -5,7 +5,7 @@ const { execFileSync } = require("child_process"); const path = require("path"); const { readAreaConfig } = require("./lib/area-config"); const { parseArgs } = require("./compile-native-roads"); -const { startWorkbench } = require("../packages/road-compiler/workbench/server"); +const { startWorkbench } = require("@osm-asset/road-compiler/workbench/server"); const repoRoot = path.resolve(__dirname, ".."); const args = parseArgs(process.argv.slice(2)); diff --git a/scripts/test-gaode-junction-reference.js b/scripts/test-gaode-junction-reference.js index f33f9b0..367789c 100644 --- a/scripts/test-gaode-junction-reference.js +++ b/scripts/test-gaode-junction-reference.js @@ -5,7 +5,7 @@ const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); -const { gcj02ToWgs84, convertGeoJson, inspectReference } = require("../packages/road-compiler/src/reference/gaode"); +const { gaodeReference: { gcj02ToWgs84, convertGeoJson, inspectReference } } = require("@osm-asset/road-compiler"); const converted = gcj02ToWgs84([114.12864875054062, 30.460485279762146]); assert.ok(Math.abs(converted[0] - 114.1229659) < 0.00001); diff --git a/scripts/test-native-road.js b/scripts/test-native-road.js index fd1bb55..97abb04 100644 --- a/scripts/test-native-road.js +++ b/scripts/test-native-road.js @@ -5,7 +5,7 @@ const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); -const { compileRoadModel, compileGeometry, validateOverrides } = require("../packages/road-compiler/src/compile/native-road"); +const { nativeRoad: { compileRoadModel, compileGeometry, validateOverrides } } = require("@osm-asset/road-compiler"); const { compileArea } = require("./compile-native-roads"); const { checkArea } = require("./check-native-roads"); @@ -164,7 +164,7 @@ assert.equal(clusterApproaches.filter((feature) => feature.properties.kind === " assert.ok(clusterApproachGeometry.diagnostics.some((item) => item.rule === "complex-junction-insufficient-nodes")); assert.equal(clusterApproachGeometry.intersectionSurface.features.length, 0); -const fixtureDir = path.join(__dirname, "..", "packages", "road-compiler", "test", "fixtures"); +const fixtureDir = path.join(path.dirname(require.resolve("@osm-asset/road-compiler/package.json")), "test", "fixtures"); const fengshuOsm = fs.readFileSync(path.join(fixtureDir, "fengshu-er-road.osm"), "utf8"); const fengshuModel = compileRoadModel(fengshuOsm, empty); const fengshuCluster = { @@ -402,20 +402,22 @@ try { fs.writeFileSync(input, osm); fs.writeFileSync(config, JSON.stringify({ id: "fresh", input, outputRoot })); const compiledArea = compileArea(config); - assert.equal(compiledArea.result.areaId, "fresh"); + assert.equal(compiledArea.marker.areaId, "fresh"); assert.ok(fs.existsSync(path.join(outputRoot, "fresh", "native-road", "compiled.json"))); + const compiled = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "compiled.json"), "utf8")); + const comparison = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "comparison.json"), "utf8")); const centerLineLayer = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "layers", "center_lines.geojson"), "utf8")); const edgeLineLayer = JSON.parse(fs.readFileSync(path.join(outputRoot, "fresh", "native-road", "layers", "edge_lines.geojson"), "utf8")); assert.equal(centerLineLayer.type, "FeatureCollection"); - assert.equal(centerLineLayer.features.length, compiledArea.comparison.nativeCenterLineFeatures); + assert.equal(centerLineLayer.features.length, comparison.nativeCenterLineFeatures); assert.equal(edgeLineLayer.features.length, 0); - assert.equal(compiledArea.comparison.schema, "native-road-comparison/v2"); - 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.nativeCrosswalkFeatures, 0); - assert.equal(compiledArea.comparison.nativeVehicleStopLineFeatures, 0); - assert.equal(compiledArea.comparison.nativeApproachEnvelopeJunctions + compiledArea.comparison.nativeFallbackJunctions, compiledArea.comparison.nativeJunctionSurfaceFeatures); - assert.ok(compiledArea.comparison.nativeMaxJunctionExpansionRatio >= 0); + assert.equal(comparison.schema, "native-road-comparison/v2"); + assert.equal(comparison.nativeRoadCount, compiled.model.roads.length); + assert.equal(comparison.nativePublishedMovementCount, compiled.movements.filter((movement) => movement.geometryPublished).length); + assert.equal(comparison.nativeCrosswalkFeatures, 0); + assert.equal(comparison.nativeVehicleStopLineFeatures, 0); + assert.equal(comparison.nativeApproachEnvelopeJunctions + comparison.nativeFallbackJunctions, comparison.nativeJunctionSurfaceFeatures); + assert.ok(comparison.nativeMaxJunctionExpansionRatio >= 0); assert.equal(checkArea(config).ok, true); } finally { fs.rmSync(freshArea, { recursive: true, force: true }); diff --git a/scripts/test-preview-assets.js b/scripts/test-preview-assets.js index 37b6fda..349ea2a 100644 --- a/scripts/test-preview-assets.js +++ b/scripts/test-preview-assets.js @@ -22,8 +22,7 @@ const { uTurnConnector, } = require("./lib/vehicle-route"); const { buildTrafficSignals } = require("./lib/traffic-signals"); -const { haversineMeters, laneCenterline } = require("../packages/road-compiler/src/geometry/lane-geometry"); -const { parseOsm } = require("../packages/road-compiler/src/osm"); +const { laneGeometry: { haversineMeters, laneCenterline }, osm: { parseOsm } } = require("@osm-asset/road-compiler"); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "preview-assets-")); const osmPath = path.join(tempDir, "fixture.osm"); diff --git a/scripts/test-road-compiler-cli.js b/scripts/test-road-compiler-cli.js new file mode 100644 index 0000000..f8d81b1 --- /dev/null +++ b/scripts/test-road-compiler-cli.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node +"use strict"; + +const assert = require("assert/strict"); +const { parseCompletionMarker } = require("./lib/road-compiler-cli"); + +const input = { areaId: "fixture", outDir: "/tmp/fixture/native-road" }; +const marker = `NATIVE_ROAD_COMPILE_DONE ${JSON.stringify({ areaId: input.areaId, output: input.outDir, roads: 1, endpoints: 2, diagnostics: 0 })}`; + +assert.equal(parseCompletionMarker(marker, input).areaId, input.areaId); +assert.throws(() => parseCompletionMarker("compiler output", input), /exactly one/); +assert.throws(() => parseCompletionMarker(`${marker}\n${marker}`, input), /exactly one/); +assert.throws(() => parseCompletionMarker("NATIVE_ROAD_COMPILE_DONE {", input), /Invalid/); +assert.throws(() => parseCompletionMarker("NATIVE_ROAD_COMPILE_DONE {\"areaId\":\"other\",\"output\":\"/tmp/fixture/native-road\"}", input), /areaId mismatch/); +assert.throws(() => parseCompletionMarker("NATIVE_ROAD_COMPILE_DONE {\"areaId\":\"fixture\",\"output\":\"/tmp/other\"}", input), /output mismatch/); +console.log("road compiler CLI tests passed"); diff --git a/scripts/test-road-workbench.js b/scripts/test-road-workbench.js index 0486dbb..7e9057e 100644 --- a/scripts/test-road-workbench.js +++ b/scripts/test-road-workbench.js @@ -5,7 +5,8 @@ const assert = require("assert"); const fs = require("fs"); const path = require("path"); -const client = path.join(__dirname, "..", "packages", "road-compiler", "workbench", "client"); +const packageRoot = path.dirname(require.resolve("@osm-asset/road-compiler/package.json")); +const client = path.join(packageRoot, "workbench", "client"); const html = fs.readFileSync(path.join(client, "index.html"), "utf8"); assert.match(html, /id="width" type="number" min="1" step="0\.01"/); assert.match(html, /data-layer="sidewalks" type="checkbox" checked> 路缘与步行带/); @@ -81,7 +82,7 @@ assert.match(app, /fromLonLat/); assert.match(app, /armFeatures\.push\(new Feature/); assert.match(app, /headFeatures\.push\(new Feature/); assert.match(app, /faceFeatures\.push\(new Feature/); -const server = fs.readFileSync(path.join(__dirname, "..", "packages", "road-compiler", "workbench", "server.js"), "utf8"); +const server = fs.readFileSync(path.join(packageRoot, "workbench", "server.js"), "utf8"); assert.match(server, /\/api\/traffic-signals\/generate/); assert.match(server, /function startWorkbench\(/); assert.match(server, /context\.compileFresh\(\)/, "workbench regeneration must use the host-provided fresh compiler callback"); diff --git a/scripts/test-traffic-signals.js b/scripts/test-traffic-signals.js index d579b66..001bf13 100644 --- a/scripts/test-traffic-signals.js +++ b/scripts/test-traffic-signals.js @@ -11,8 +11,8 @@ const { buildTrafficSignalsFromFeatures, validateTrafficSignalFeatures, validateTrafficSignalSourceReferences, -} = require("../packages/road-compiler/src/traffic-signals"); -const { SCHEMA, loadOrGenerate } = require("../packages/road-compiler/src/native-traffic-signals"); +} = require("@osm-asset/road-compiler").trafficSignals; +const { SCHEMA, loadOrGenerate } = require("@osm-asset/road-compiler").nativeTrafficSignals; function rectangle(lon, lat, dx = 0.00003, dy = 0.000006) { return { type: "Feature", geometry: { type: "Polygon", coordinates: [[ diff --git a/scripts/test-turn-lane-arrows.js b/scripts/test-turn-lane-arrows.js index b281a76..c47ce4c 100644 --- a/scripts/test-turn-lane-arrows.js +++ b/scripts/test-turn-lane-arrows.js @@ -2,7 +2,7 @@ "use strict"; const assert = require("assert"); -const { arrowRingsAt, buildCustomTurnLaneArrows, loadManifest, normalizeManeuver, supportedAssets, templateFor } = require("../packages/road-compiler/src/compile/turn-lane-arrows"); +const { turnLaneArrows: { arrowRingsAt, buildCustomTurnLaneArrows, loadManifest, normalizeManeuver, supportedAssets, templateFor } } = require("@osm-asset/road-compiler"); function node(id, lon, lat) { return { id, lon, lat, tags: {} }; } function way(id, refs, tags) { return { id, refs, tags }; } diff --git a/scripts/test-v2x-cesium-overlay.js b/scripts/test-v2x-cesium-overlay.js index e4db437..1550e57 100644 --- a/scripts/test-v2x-cesium-overlay.js +++ b/scripts/test-v2x-cesium-overlay.js @@ -5,7 +5,7 @@ const assert = require("assert"); const fs = require("fs"); const path = require("path"); const vm = require("vm"); -const { gcj02ToWgs84: referenceGcj02ToWgs84 } = require("../packages/road-compiler/src/reference/gaode"); +const { gaodeReference: { gcj02ToWgs84: referenceGcj02ToWgs84 } } = require("@osm-asset/road-compiler"); const source = fs.readFileSync(path.join(__dirname, "lib", "v2x-cesium-overlay.js"), "utf8"); const context = {