From 3db0f29a52f925cbdc3df61f7d61024b0bcbfd7a Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 04:46:04 +0900 Subject: [PATCH 1/3] =?UTF-8?q?GPX=E3=82=92=E5=88=B0=E7=9D=80=E6=99=82?= =?UTF-8?q?=E9=96=93=E6=8E=A8=E5=AE=9A=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B?= =?UTF-8?q?=E3=81=A6=E8=B5=B0=E3=82=89=E3=81=9B=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=80=81=E5=90=8C=E6=A2=B1=E3=81=AEGPX?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E3=82=8A=E7=9B=B4=E3=81=97=E3=81=9F=20(#7157?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GPX生成スクリプトで駅間に線路の長さを使い、種別グループでは到着時間推定の見込みに合わせて走らせるようにした Co-Authored-By: Claude Opus 5.5 * 同梱のGPXを到着時間推定に合わせて作り直し、GPXの回帰テストを複数の測位条件で測るようにした Co-Authored-By: Claude Opus 5.5 * 同梱のGPXをtrainRouteのEstimatedの見込みで作り直し、GPXの回帰テストとlocation.tsのコメントをdevの元の形に戻した Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .claude/skills/replay-gpx/SKILL.md | 11 +- assets/gpx/FLinerSeibu.gpx | 18100 ++++++++-------- assets/gpx/KatamachiRapid.gpx | 2038 +- assets/gpx/KeioSpecialExpress.gpx | 5351 +++-- assets/gpx/SampleTohokuShinkansen.gpx | 4244 ++-- assets/gpx/SobuRapid.gpx | 2583 ++- docs/location-simulation.md | 76 +- scripts/generate-location-gpx.mjs | 298 +- scripts/generate-location-gpx.test.mjs | 230 + src/store/atoms/location.gpxEtaAssist.test.ts | 8 +- src/store/atoms/location.subwayGpx.test.ts | 6 +- 11 files changed, 18103 insertions(+), 14842 deletions(-) diff --git a/.claude/skills/replay-gpx/SKILL.md b/.claude/skills/replay-gpx/SKILL.md index 33e30ea13e..a4fe8b5026 100644 --- a/.claude/skills/replay-gpx/SKILL.md +++ b/.claude/skills/replay-gpx/SKILL.md @@ -32,8 +32,8 @@ Argent の MCP ツールには位置情報を注入するものが無いため | `assets/gpx/SampleJY.gpx` | 山手線。実走行ログ | | `assets/gpx/SampleTohokuShinkansen.gpx` | 東北新幹線 盛岡→仙台。最高 320km/h | | `assets/gpx/KeioSpecialExpress.gpx` | 京王線 特急 新宿→京王八王子。種別グループから生成 | -| `assets/gpx/KatamachiRapid.gpx` | 片町線 快速 京田辺→木津。駅間 2.3km・最高 95km/h | -| `assets/gpx/SobuRapid.gpx` | 総武快速線 錦糸町→津田沼。最高 120km/h | +| `assets/gpx/KatamachiRapid.gpx` | 片町線 快速 京田辺→木津。駅間 2.3km 前後の全駅停車 | +| `assets/gpx/SobuRapid.gpx` | 総武快速線 錦糸町→津田沼。駅間 3.6〜7.6km の全駅停車 | | `assets/gpx/FLinerSeibu.gpx` | Fライナー相当 元町・中華街→飯能。地下鉄の電波環境入り | 新しい経路は `npm run gpx:generate` で作る。詳細は `docs/location-simulation.md`。 @@ -52,7 +52,7 @@ npm run gpx:generate -- --line 1004 --from 100418 --to 100411 --max-speed 320 \ 寄せる。詳細と数値の根拠は `docs/location-simulation.md`。 ```bash -npm run gpx:generate -- --line-group 152 --max-speed 80 \ +npm run gpx:generate -- --line-group 152 \ --signal-profile subway --subway-lines 99310,22003 \ --out assets/gpx/FLinerSeibu.gpx ``` @@ -62,11 +62,12 @@ npm run gpx:generate -- --line-group 152 --max-speed 80 \ 列車種別の停車パターンをそのまま走らせたいときは `--line-group` を使う。通過駅は `stopCondition` から自動判定されるので `--skip` を手で並べなくてよく、直通で複数 -路線にまたがる経路もそのまま扱える。`lineGroupId` は駅から辿る。 +路線にまたがる経路もそのまま扱える。`lineGroupId` は駅から辿る。区間の時間は +StationAPI の到着時間推定 (`trainRoute` の `model: Estimated`) に合わせる。 ```bash npm run gpx:generate -- --list-train-types 2400101 -npm run gpx:generate -- --line-group 71 --max-speed 110 --out assets/gpx/KeioSpecialExpress.gpx +npm run gpx:generate -- --line-group 71 --out assets/gpx/KeioSpecialExpress.gpx ``` ### 2. アプリを対象の路線に入れる diff --git a/assets/gpx/FLinerSeibu.gpx b/assets/gpx/FLinerSeibu.gpx index 2733841098..3c1552f668 100644 --- a/assets/gpx/FLinerSeibu.gpx +++ b/assets/gpx/FLinerSeibu.gpx @@ -4,17732 +4,17952 @@ 48 - + 429 - + 589 - + 333 - + 325 - + 528 - + 611 - + 286 - + 546 - + 490 - + 542 - + 389 - + 375 - + 262 - + 466 - + 294 - + 443 - + 510 - + 360 - + 369 - + 487 - - + + 564 - - + + 521 - - + + 569 - - + + 582 - - + + 518 - - + + 342 - - + + 370 - - + + 509 - - + + 417 - - + + 513 - - + + 494 - - + + 557 - - + + 375 - - + + 467 - - + + 386 - - -32 + + +337 - - -47 + + +486 - - -30 + + +308 - - -28 + + +290 - -51 + +527 - + 58 - + 27 - + 60 - + 33 - + 43 - + 37 - + 42 - + 51 - + 32 - + 50 - + 53 - + 47 - + 26 - + 41 - + 55 - + 27 - + 34 - + 31 - + 35 - + 58 - + 48 - + 43 - + 39 - + 47 - + 38 - + 51 - + 45 - + 42 - + 35 - + 60 - + 45 - + 38 - + 37 - + 41 - + 38 - + 55 - + 52 - - -48 + + +493 - - -51 + + +526 - - -37 + + +382 - - -30 + + +316 - - -50 + + +513 - - -50 + + +513 - - -46 + + +481 - - -28 + + +291 - - -51 + + +531 - - -39 + + +405 - - -42 + + +431 - - -48 + + +495 - - -53 + + +545 - - -32 + + +329 - - -40 + + +411 - - -28 + + +292 - - -38 + + +397 - - -58 + + +598 - - -45 + + +469 - - -32 + + +335 - - -38 + + +399 - - -53 + + +543 - - -38 + + +397 - - -46 + + +471 - - + + 405 - - + + 598 - - + + 473 - - + + 456 - - + + 489 - - + + 318 - - + + 534 - - + + 402 - - + + 384 - - + + 298 - - + + 333 - - + + 463 - - + + 327 - - + + 486 - - + + 260 - - + + 425 - - -267 - - - -513 + + +26 - - -534 + + +50 - - -355 + + +52 - - -616 + + +34 - - -485 + + +60 - - -266 + + +47 - - -273 + + +26 - - -261 + + +26 - - -467 + + +25 + + + +45 - - -445 + + +43 - - -516 + + +50 - - -586 + + +57 - - -536 + + +52 - - -567 + + +55 - - -609 + + +59 - - -586 + + +57 - - -522 + + +50 - -309 + +30 - + 27 - + 30 - + 41 - + 47 - + 59 - + 41 - + 42 - + 46 - + 48 - + 44 - + 27 - + 53 - + 27 - + 40 - + 29 - + 35 - + 33 - + 45 - -47 + +15 - -49 + +16 - -56 + +19 - -34 + +11 - -35 + +11 - -46 + +15 - -60 + +20 - -31 + +10 - -32 + +10 - -41 + +13 - -37 + +12 - -51 + +17 - -47 + +15 - -45 + +15 - -30 + +10 - -45 + +15 - -50 + +17 - -32 + +10 - -30 + +10 - -33 + +11 - -38 + +12 - -28 + +9 - -59 + +20 - -47 + +16 - -37 + +12 - -44 + +15 - -36 + +12 - -53 + +18 - -31 + +10 - -36 + +12 - -39 + +13 - -35 + +11 - -45 + +15 - -41 + +13 - -60 + +20 - -34 + +11 - -47 + +16 - - -37 + + +12 - - -41 + + +14 - - -42 + + +14 - - -48 + + +16 - - -34 + + +11 - - -31 + + +10 - - -56 + + +19 - - -41 + + +13 - - -35 + + +11 - - -53 + + +18 - - -38 + + +12 - - + + 18 - - + + 11 - - + + 14 - - + + 9 - - + + 20 - - + + 15 - - + + 11 - - + + 17 - - + + 18 - - + + 13 - - + + 10 - - + + 17 - - + + 14 - - + + 18 - - + + 14 - - + + 18 - - + + 12 - - + + 13 - - + + 17 - - + + 11 - - + + 19 - - + + 15 - - + + 17 - - + + 16 - - + + 15 - - + + 8 - - + + 13 - - + + 20 - - + + 10 - - + + 14 - - + + 18 - - + + 20 - - + + 19 - - + + 18 - - + + 17 - - + + 17 - - + + 9 - - + + 13 - - + + 14 - - + + 15 - - + + 14 - - + + 11 - - + + 9 - - + + 13 - - + + 18 - - + + 18 - - + + 18 - - + + 20 - - + + 20 - - + + 18 - - + + 15 - - + + 17 - - + + 19 - - + + 13 - - + + 18 - - + + 18 - - + + 12 - - + + 20 - - + + 14 - - + + 13 - - + + 19 - - + + 11 - - + + 18 - - + + 18 - - + + 17 - - + + 17 - - + + 14 - - + + 19 - - + + 10 - - + + 8 - - + + 10 - - + + 13 - - + + 16 - - + + 10 - - + + 9 - - + + 19 - - + + 10 - - + + 19 - - + + 17 - - + + 11 - - + + 11 - - + + 19 - - + + 19 - - + + 14 - - + + 12 - - + + 12 - - + + 19 - - + + 18 - - + + 11 - - + + 20 - - + + 14 - - + + 10 - - + + 18 - - + + 13 - - + + 9 - - + + 11 - - + + 13 - - + + 9 - - + + 14 - - + + 17 - - + + 12 - - + + 12 - - + + 17 - - + + 19 - - + + 8 - - + + 10 - - + + 16 - - + + 17 - - + + 15 - - + + 17 - - + + 9 - - + + 11 - - + + 20 - - + + 20 - - + + 10 - - + + 12 - - + + 10 - - + + 11 - - + + 12 - - + + 16 - - + + 12 - - + + 13 - - + + 18 - - + + 9 - - + + 13 - - + + 17 - - + + 16 - - + + 16 - - + + 9 - - + + 9 - - + + 13 - - + + 14 - - + + 11 - - + + 16 - - + + 20 - - + + 9 - - + + 14 - - + + 17 - - + + 17 - - + + 13 - - + + 10 - - + + 14 - - + + 15 - - + + 15 - - + + 16 - - + + 14 - - + + 18 - - + + 13 - - + + 11 - - + + 10 - - + + 20 - - + + 19 - - + + 15 - - + + 13 - - + + 15 - - + + 16 - - + + 10 - - + + 16 - - + + 13 - - + + 9 - - + + 18 - - + + 19 - - + + 19 - - + + 15 - - + + 12 - - + + 13 - - + + 10 - - + + 14 - - + + 18 - - + + 18 - - + + 20 - - + + 20 - - + + 13 - - + + 15 - - + + 16 - - + + 16 - - + + 19 - - + + 20 - - + + 13 - - + + 9 - - + + 9 - - + + 15 - - + + 16 - - + + 16 - - + + 9 - - + + 13 - - + + 11 - - + + 19 - - + + 17 - - + + 16 - - + + 19 - - + + 20 - - + + 16 - - + + 18 - - + + 17 - - + + 16 - - + + 16 - - + + 15 - - + + 14 - - + + 12 - - + + 15 - - + + 10 - - + + 12 - - + + 9 - - + + 17 - - + + 18 - - + + 17 - - + + 19 - - + + 20 - - + + 20 - - + + 11 - - + + 16 - - + + 17 - - + + 12 - - + + 18 - - + + 13 - - + + 14 - - + + 17 - - + + 19 - - + + 19 - - + + 19 - - + + 18 - - + + 17 - - + + 12 - - + + 12 - - + + 10 - - + + 17 - - + + 16 - - + + 17 - - + + 12 - - + + 16 - - + + 18 - - + + 12 - - + + 19 - - + + 13 - - + + 12 - - + + 17 - - + + 11 - - + + 17 - - + + 17 - - + + 12 - - + + 10 - - + + 19 - - + + 9 - - + + 18 - - + + 15 - - + + 18 - - + + 9 - - + + 15 - - + + 20 - - + + 10 - - + + 9 - - + + 16 - - + + 11 - - + + 18 - - + + 12 - - + + 16 - - + + 13 - - + + 14 - - + + 14 - - + + 8 - - + + 10 - - + + 16 - - + + 9 - - + + 13 - - + + 18 - - + + 11 - - + + 19 - - + + 11 - - + + 14 - - + + 13 - - + + 15 - - + + 20 - - + + 12 - - + + 8 - - + + 10 - - + + 19 - - + + 16 - - + + 14 - - + + 8 - - + + 19 - - + + 14 - - + + 8 - - + + 17 - - + + 9 - - + + 17 - - + + 10 - - + + 9 - - + + 9 - - + + 10 - - + + 20 - - + + 16 - - + + 12 - - + + 8 - - + + 15 - - + + 12 - - + + 9 - - + + 12 - - + + 14 - - + + 15 - - + + 10 - - + + 20 - - + + 12 - - + + 15 - - + + 10 - - + + 10 - - + + 8 - - + + 8 - - + + 19 - - + + 13 - + 20 - + 10 - + 20 - + 11 - + 9 - + 20 - + 13 - + 19 - + 13 - + 15 - + 13 - + 20 - + 18 - + 13 - + 12 - + 9 - + 18 - + 14 - + 15 - + 11 - + 11 - + 9 - + 14 - + 9 - + 17 - + 10 - + 16 - + 16 - + 13 - + 8 - + 13 - + 19 - + 14 - + 17 - + 9 - + 15 - + 15 - + 12 - - + + 9 - - + + 13 - - + + 9 - - + + 18 - - + + 18 - - + + 14 - - + + 19 - - + + 15 - - + + 10 - - + + 17 - - + + 10 - - + + 18 - - + + 19 - - + + 10 - - + + 8 - - + + 15 - - + + 14 - - + + 9 - - + + 19 - - + + 13 - - + + 14 - - + + 17 - - + + 18 - - + + 16 - - + + 17 - - + + 16 - - + + 15 - - + + 15 - - + + 15 - - + + 16 - - + + 20 - - + + 12 - - + + 13 - - + + 13 - - + + 12 - - + + 16 - - + + 17 - - + + 14 - - + + 9 - - + + 20 - - + + 17 - - + + 8 - - + + 15 - - + + 8 - - + + 8 - - + + 10 - - + + 9 - - + + 9 - - + + 13 - - + + 17 - - + + 14 - - + + 13 - - + + 11 - - + + 14 - - + + 19 - - + + 16 - - + + 19 - - + + 12 - - + + 11 - - + + 18 - - + + 15 - - + + 10 - - + + 15 - - + + 19 - - + + 11 - - + + 10 - - + + 10 - - + + 9 - - + + 10 - - + + 18 - - + + 10 - - + + 16 - - + + 15 - - + + 8 - - + + 10 - - + + 12 - - + + 18 - - + + 20 - - + + 9 - - + + 12 - - + + 17 - - + + 9 - - + + 10 - - + + 14 - - + + 16 - - + + 20 - - + + 10 - - + + 19 - - + + 12 - - + + 9 - - + + 13 - - + + 10 - - + + 9 - - + + 10 - - + + 19 - - + + 8 - - + + 11 - - + + 15 - - + + 12 - - + + 18 - - + + 13 - - + + 13 - - + + 19 - - + + 20 - - + + 16 - - + + 11 - - + + 15 - - + + 12 - - + + 8 - - + + 17 - - + + 9 - - + + 17 - - + + 10 - - + + 17 - - + + 12 - - + + 15 - - + + 10 - - + + 14 - - + + 10 - - + + 16 - - + + 12 - - + + 12 - - + + 16 - - + + 17 - - + + 9 - - + + 14 - - + + 11 - - + + 9 - - + + 13 - - + + 15 - - + + 11 - - + + 18 - - + + 11 - - + + 11 - - + + 12 - - + + 12 - - + + 8 - - + + 14 - - + + 13 - - + + 10 - - + + 11 - - + + 12 - - + + 18 - - + + 12 - - + + 12 - - + + 9 - - + + 10 - - + + 16 - - + + 11 - - + + 11 - - + + 15 - - + + 11 - - + + 17 - - + + 17 - - + + 13 - - + + 9 - - + + 13 - - + + 15 - - + + 11 - - + + 11 - - + + 18 - - + + 16 - - + + 12 - - + + 16 - - + + 16 - - + + 13 - - + + 10 - - + + 14 - - + + 18 - - + + 20 - - + + 19 - - + + 12 - - + + 20 - - + + 11 - - + + 15 - - + + 20 - - + + 11 - - + + 13 - - + + 13 - - + + 17 - - + + 10 - - + + 16 - - + + 9 - - + + 9 - - + + 9 - - + + 14 - - + + 18 - - + + 10 - - + + 16 - - + + 12 - - + + 9 - - + + 15 - - + + 16 - - + + 13 - - + + 14 - - + + 10 - - + + 11 - - + + 9 - - + + 14 - - + + 12 - - + + 12 - - + + 10 - - + + 10 - - + + 18 - - + + 16 - - + + 12 - - + + 20 - - + + 10 - - + + 18 - - + + 8 - - + + 19 - - + + 18 - - + + 10 - - + + 10 - - + + 12 - - + + 8 - - + + 13 - - + + 19 - - + + 18 - - + + 15 - - + + 15 - - + + 11 - - + + 15 - - + + 14 - - + + 12 - - + + 10 - - + + 15 - - + + 18 - - + + 19 - - + + 13 - - + + 9 - - + + 19 - - + + 10 - - + + 19 - - + + 18 - - + + 10 - - + + 15 - - + + 10 - - + + 11 - - + + 9 - - + + 12 - - + + 17 - - + + 20 - - + + 17 - - + + 10 - - + + 11 - - + + 8 - - + + 20 - - + + 14 - - + + 18 - - + + 9 - - + + 10 - - + + 15 - - + + 13 - - + + 18 - - + + 14 - - + + 11 - - + + 19 - - + + 10 - - + + 13 - - + + 18 - - + + 16 - - + + 19 - - + + 16 - - + + 16 - - + + 12 - - + + 17 - - + + 11 - - + + 20 - - + + 19 - - + + 11 - - + + 16 - - + + 9 - - + + 12 - - + + 19 - - + + 11 - - + + 13 - - + + 14 - - + + 17 - - + + 9 - - + + 18 - - + + 10 - - + + 10 - - + + 12 - - + + 8 - - + + 15 - - + + 17 - - + + 14 - - + + 10 - - + + 16 - - + + 15 - - + + 13 - - + + 17 - - + + 13 - - + + 19 - - + + 19 - - + + 18 - - + + 18 - - + + 19 - - + + 12 - - + + 18 - - + + 12 - - + + 9 - - + + 14 - - + + 14 - - + + 15 - - + + 15 - - + + 10 - - + + 14 - - + + 15 - - + + 11 - - + + 17 - - + + 13 - - + + 13 - - + + 8 - - + + 19 - - + + 11 - - + + 19 - - + + 14 - - + + 10 - - + + 16 - - + + 11 - - + + 8 - - + + 12 - - + + 17 - - + + 20 - - + + 9 - - + + 13 - - + + 11 - - + + 12 - - + + 17 - - + + 12 - - + + 10 - - + + 16 - - + + 16 - - + + 8 - - + + 10 - - + + 17 - - + + 18 - - + + 12 - - + + 14 - - + + 12 - - + + 12 - - + + 14 - - + + 20 - - + + 11 - - + + 20 - - + + 11 - - + + 13 - - + + 12 - - + + 19 - - + + 20 - - + + 19 - - + + 10 - - + + 10 - - + + 16 - - + + 11 - - + + 19 - - + + 13 - - + + 10 - - + + 10 - - + + 19 - - + + 19 - - + + 13 - - + + 19 - - + + 18 - - + + 11 - - + + 9 - - + + 18 - - + + 10 - - + + 10 - - + + 11 - - + + 16 - - + + 14 - - + + 16 - - + + 9 - - + + 18 - - + + 17 - - + + 14 - - + + 19 - - + + 14 - - + + 10 - - + + 12 - - + + 9 - - + + 17 - - + + 20 - - + + 18 - - + + 18 - - + + 8 - - + + 19 - - + + 19 - - + + 10 - - + + 15 - - + + 20 - - + + 11 - - + + 13 - - + + 16 - - + + 11 - - + + 15 - - + + 14 - - + + 13 - - + + 20 - - + + 16 - - + + 17 - - + + 18 - - + + 8 - - + + 20 - - + + 14 - - + + 19 - - + + 11 - - + + 13 - - + + 12 - - + + 15 - - + + 14 - - + + 14 - - + + 12 - - + + 14 - - + + 19 - - + + 12 - - + + 9 - - + + 13 - - + + 15 - - + + 14 - - + + 18 - - + + 8 - - + + 12 - - + + 19 - - + + 13 - - + + 10 - - + + 19 - - + + 9 - - + + 17 - - + + 19 - - + + 9 - - + + 15 - - + + 11 - - + + 11 - - + + 19 - - + + 9 - - + + 18 - - + + 19 - - + + 13 - - + + 16 - - + + 19 - - + + 13 - - + + 18 - - + + 17 - - + + 10 - - + + 13 - - + + 20 - - + + 18 - - + + 19 - - + + 20 - - + + 17 - - + + 11 - - + + 16 - - + + 15 - - + + 10 - - + + 12 - - + + 9 - - + + 19 - - + + 14 - - + + 15 - - + + 18 - - + + 13 - - + + 11 - - + + 16 - - + + 14 - - + + 13 - - + + 10 - - + + 16 - - + + 15 - - + + 8 - - + + 12 - - + + 15 - - + + 19 - - + + 16 - - + + 12 - - + + 19 - - + + 18 - - + + 9 - - + + 11 - - + + 11 - - + + 13 - - + + 17 - - + + 11 - - + + 10 - - + + 11 - - + + 11 - - + + 19 - - + + 10 - - + + 13 - - + + 13 - - + + 11 - - + + 12 - - + + 17 - - + + 12 - - + + 9 - - + + 12 - - + + 18 - - + + 12 - - + + 20 - - + + 17 - - + + 11 - - + + 10 - - + + 13 - - + + 17 - - + + 19 - - + + 9 - - + + 16 - - + + 14 - - + + 13 - - + + 9 - - + + 12 - - + + 11 - - + + 8 - - + + 9 - - + + 8 - - + + 13 - - + + 17 - - + + 12 - - + + 11 - - + + 12 - - + + 8 - - + + 18 - - + + 17 - - + + 12 - - + + 19 - - + + 8 - - + + 20 - - + + 9 - - + + 17 - - + + 12 - - + + 15 - - + + 19 - - + + 15 - - + + 19 - - + + 10 - - + + 8 - - + + 17 - - + + 10 - - + + 11 - - + + 11 - - + + 12 - - + + 9 - - + + 19 - - + + 13 - - + + 17 - - + + 18 - - + + 9 - - + + 20 - - + + 9 - - + + 19 - - + + 12 - - + + 10 - - + + 19 - - + + 14 - - + + 14 - - + + 19 - - + + 15 - - + + 9 - - + + 10 - - + + 19 - - + + 16 - - + + 10 - - + + 11 - - + + 9 - - + + 16 - - + + 13 - - + + 18 - - + + 13 - - + + 8 - - + + 10 - - + + 19 - - + + 15 - - + + 9 - - + + 10 - - + + 12 - - + + 19 - - + + 17 - - + + 10 - - + + 14 - - + + 20 - - + + 16 - - + + 19 - - + + 16 - - + + 20 - - + + 13 - - + + 14 - - + + 11 - - + + 20 - - + + 17 - - + + 12 - - + + 16 - - + + 17 - - + + 19 - - + + 19 - - + + 12 - - + + 9 - - + + 14 - - + + 14 - - + + 14 - - + + 13 - - + + 15 - - + + 14 - - + + 10 - - + + 13 - - + + 16 - - + + 10 - - + + 18 - - + + 16 - - + + 11 - - + + 16 - - + + 14 - - + + 14 - - + + 9 - - + + 16 - - + + 18 - - + + 16 - - + + 14 - - + + 17 - - + + 17 - - + + 20 - - + + 8 - - + + 14 - - + + 14 - - + + 9 - - + + 19 - - + + 9 - - + + 12 - - + + 18 - - + + 16 - - + + 15 - - + + 8 - - + + 14 - - + + 18 - - + + 12 - - + + 19 - - + + 11 - - + + 18 - - + + 13 - - + + 14 - - + + 14 - - + + 9 - - + + 12 - - + + 11 - - + + 15 - - + + 20 - - + + 10 - - + + 8 - - + + 10 - - + + 13 - - + + 10 - - + + 18 - - + + 18 - - + + 17 - - + + 15 - - + + 12 - - + + 14 - - + + 18 - - + + 16 - - + + 17 - - + + 10 - - + + 16 - - + + 18 - - + + 12 - - + + 17 - - + + 15 - - + + 17 - - + + 19 - - + + 19 - - + + 20 - - + + 10 - - + + 10 - - + + 19 - - + + 13 - - + + 13 - - + + 9 - - + + 14 - - + + 12 - - + + 9 - - + + 16 - - + + 18 - - + + 9 - - + + 14 - - + + 12 - - + + 19 - - + + 8 - - + + 18 - - + + 17 - - + + 8 - - + + 19 - - + + 20 - - + + 11 - - + + 19 - - + + 11 - - + + 19 - - + + 17 - - + + 10 - - + + 9 - - + + 16 - - + + 11 - - + + 20 - - + + 12 - - + + 20 - - + + 16 - - + + 11 - - + + 10 - - + + 17 - - + + 14 - - + + 13 - - + + 12 - - + + 16 - - + + 13 - - + + 9 - - + + 17 - - + + 13 - - + + 12 - - + + 12 - - + + 15 - - + + 14 - - + + 8 - - + + 15 - - + + 12 - - + + 15 - - + + 14 - - + + 9 - - + + 14 - - + + 12 - - + + 20 - - + + 10 - - + + 9 - - + + 20 - - + + 17 - - + + 12 - - + + 18 - - + + 12 - - + + 14 - - + + 14 - - + + 10 - - + + 13 - - + + 18 - - + + 12 - - + + 11 - - + + 8 - - + + 13 - - + + 18 - - + + 19 - - + + 14 - - + + 17 - - + + 14 - - + + 18 - - + + 20 - - + + 14 - - + + 20 - - + + 10 - - + + 17 - - + + 11 - - + + 15 - - + + 15 - - + + 20 - - + + 19 - - + + 15 - - + + 17 - - + + 13 - - + + 17 - - + + 14 - - + + 18 - - + + 8 - - + + 19 - - + + 14 - - + + 11 - - + + 8 - - + + 17 - - + + 15 - - + + 16 - - + + 16 - - + + 20 - - + + 9 - - + + 20 - - + + 9 - - + + 19 - - + + 13 - - + + 9 - - + + 18 - - + + 10 - - + + 14 - - + + 15 - - + + 17 - - + + 12 - - + + 12 - - + + 19 - - + + 15 - - + + 20 - - + + 11 - - + + 16 - - + + 14 - - + + 11 - - + + 14 - - + + 17 - - + + 19 - - + + 12 - - + + 19 - - + + 12 - - + + 13 - - + + 18 - - + + 12 - - + + 12 - - + + 17 - - + + 19 - - + + 11 - - + + 13 - - + + 16 - - + + 19 - - + + 15 - - + + 8 - - + + 17 - - + + 18 - - + + 14 - - + + 15 - - + + 15 - - + + 16 - - + + 12 - - + + 9 - - + + 13 - - + + 16 - - + + 14 - - + + 19 - - + + 19 - - + + 16 - - + + 18 - - + + 14 - - + + 10 - - + + 16 - - + + 19 - - + + 10 - - + + 20 - - + + 18 - - + + 16 - - + + 11 - - + + 12 - - + + 11 - - + + 19 - - + + 12 - - + + 11 - - + + 9 - - + + 8 - - + + 12 - - + + 16 - - + + 12 - - + + 18 - - + + 10 - - + + 13 - - + + 12 - - + + 13 - - + + 18 - - + + 13 - - + + 9 - - + + 17 - - + + 10 - - + + 9 - - + + 9 - - + + 14 - - + + 18 - - + + 17 - - + + 17 - - + + 18 - - + + 12 - - + + 12 - - + + 14 - - + + 8 - - + + 14 - - + + 17 - - + + 13 - - + + 18 - - + + 11 - - + + 10 - - + + 17 - - + + 12 - - + + 11 - - + + 14 - - + + 9 - - + + 13 - - + + 11 - - + + 8 - - + + 12 - - + + 16 - - + + 11 - - + + 11 - - + + 13 - - + + 17 - - + + 12 - - + + 10 - - + + 10 - - + + 9 - - + + 20 - - + + 9 - - + + 12 - - + + 12 - - + + 15 - - + + 15 - - + + 17 - - + + 14 - - + + 14 - - + + 10 - - + + 20 - - + + 12 - - + + 9 - - + + 11 - - + + 14 - - + + 14 - - + + 18 - - + + 13 - - + + 13 - - + + 15 - - + + 16 - - + + 12 - - + + 18 - - + + 10 - - + + 15 - - + + 9 - - + + 12 - - + + 14 - - + + 17 - - + + 14 - - + + 10 - - + + 17 - - + + 9 - - + + 14 - - + + 19 - - + + 11 - - + + 15 - - + + 11 - - + + 13 - - + + 13 - - + + 18 - - + + 15 - - + + 9 - - + + 16 - - + + 11 - - + + 14 - - + + 15 - - + + 10 - - + + 15 - - + + 16 - - + + 16 - - + + 17 - - + + 13 - - + + 17 - - + + 9 - - + + 18 - - + + 10 - - + + 17 - - + + 18 - - + + 12 - - + + 14 - - + + 9 - - + + 20 - - + + 19 - - + + 8 - - + + 18 - - + + 17 - - + + 19 - - + + 17 - - + + 10 - - + + 16 - - + + 20 - - + + 10 - - + + 10 - - + + 9 - - + + 15 - - + + 16 - - + + 16 - - + + 8 - - + + 12 - - + + 9 - - + + 15 - - + + 16 - - + + 19 - - + + 10 - - + + 11 - - + + 14 - - + + 16 - - + + 16 - - + + 18 - - + + 9 - - + + 17 - - + + 17 - - + + 19 - - + + 19 - - + + 18 - - + + 10 - - + + 15 - - + + 10 - - + + 14 - - + + 18 - - + + 16 - - + + 15 - - + + 15 - - + + 15 - - + + 10 - - + + 15 - - + + 8 - - + + 15 - - + + 13 - - + + 9 - - + + 17 - - + + 11 - - + + 18 - - + + 12 - - + + 19 - - + + 16 - - + + 18 - - + + 11 - - + + 17 - - + + 19 - - + + 13 - - + + 17 - - + + 16 - - + + 14 - - + + 19 - - + + 20 - - + + 20 - - + + 16 - - + + 16 - - + + 9 - - + + 20 - - + + 9 - - + + 12 - - + + 11 - - + + 13 - - + + 11 - - + + 11 - - + + 17 - - + + 9 - - + + 19 - - + + 11 - - + + 18 - - + + 13 - - + + 20 - - + + 9 - - + + 11 - - + + 11 - - + + 16 - - + + 11 - - + + 8 - - + + 10 - - + + 19 - - + + 12 - - + + 18 - - + + 9 - - + + 18 - - + + 17 - - + + 17 - - + + 15 - - + + 15 - - + + 10 - - + + 12 - - + + 8 - - + + 9 - - + + 10 - - + + 8 - - + + 19 - - + + 14 - - + + 18 - - + + 18 - - + + 12 - - + + 18 - - + + 16 - - + + 19 - - + + 9 - - + + 16 - - + + 9 - - + + 9 - - + + 12 - - + + 19 - - + + 13 - - + + 11 - - + + 9 - - + + 18 - - + + 20 - - + + 14 - - + + 12 - - + + 14 - - + + 9 - - + + 11 - - + + 11 - - + + 12 - - + + 10 - - + + 15 - - + + 10 - - + + 11 - - + + 17 - - + + 11 - - + + 13 - - + + 19 - - + + 18 - - + + 14 - - + + 14 - - + + 20 - - + + 9 - - + + 13 - - + + 18 - - -16 + + +16 - - + + 19 - - + + 20 - - + + 12 - - + + 18 - - + + 17 - - + + 15 - - + + 13 - - + + 11 - - + + 13 - - + + 12 - - + + 9 - - + + 17 - - + + 15 - - + + 12 - - + + 10 - - + + 11 - - + + 9 - - + + 12 - - + + 16 - - + + 17 - - + + 17 - - + + 11 - - + + 10 - - + + 11 - - + + 19 - - + + 18 - - + + 14 - - + + 14 - - + + 16 - - + + 10 - - + + 11 - - + + 17 - - + + 15 - - + + 10 - - + + 20 - - + + 9 - - + + 20 - - + + 17 - - + + 13 - - + + 17 - - + + 13 - - + + 11 - - + + 10 - - + + 9 - - + + 14 - - + + 10 - - + + 19 - - + + 13 - - + + 10 - - + + 18 - - + + 13 - - + + 20 - - + + 14 - - + + 15 - - + + 15 - - + + 15 - - + + 12 - - + + 10 - - + + 17 - - + + 8 - - + + 15 - - + + 14 - - + + 16 - - + + 14 - - + + 13 - - + + 16 - - + + 8 - - + + 11 - - + + 12 - - + + 14 - - -29 + + +9 - - -27 + + +9 - - -35 + + +11 - - -47 + + +16 - - -51 + + +17 - - -47 + + +15 - - -36 + + +12 - - -49 + + +16 - - -56 + + +18 - - -43 + + +14 - - -27 + + +9 - - -35 + + +11 - - -34 + + +11 - - -32 + + +10 - - -43 + + +14 - - -54 + + +18 - - -28 + + +9 - - -28 + + +9 - - -27 + + +9 - - -26 + + +8 - - -45 + + +15 - - -45 + + +15 - - -53 + + +17 - - -32 + + +10 - - -52 + + +17 - - -36 + + +12 - - -54 + + +18 - - -31 + + +10 - - -54 + + +18 - - -48 + + +16 - - -35 + + +12 - - -51 + + +17 - - -50 + + +16 - - -37 + + +12 - - -60 + + +20 - - -57 + + +19 - - -34 + + +11 - - -56 + + +19 - - -30 + + +10 - - -37 + + +12 - - -46 + + +15 - - -56 + + +19 - - -43 + + +14 - - -47 + + +16 - - -52 + + +17 - - -44 + + +15 - - -55 + + +18 - - -46 + + +15 - - -27 + + +9 - - -28 + + +9 - - -47 + + +16 - - -43 + + +14 - - -56 + + +18 - - -48 + + +16 - - -33 + + +11 - - -55 + + +18 - - -49 + + +16 - - -30 + + +10 - - -35 + + +12 - - -38 + + +13 - - -45 + + +15 - - -556 + + +18 - - -562 + + +18 - - -389 + + +12 - - -389 + + +12 - - -557 + + +18 - - -332 + + +10 - - -271 + + +8 - - -389 + + +12 - - -570 + + +18 - - -300 + + +9 - - -300 + + +9 - - -399 + + +13 - - -510 + + +16 - - -391 + + +12 - - -508 + + +16 - - -336 + + +11 - - -339 + + +11 - - -584 + + +19 - - -504 + + +16 - - -587 + + +19 - - -343 + + +11 - - -351 + + +11 - - -592 + + +19 - - -515 + + +17 - - -338 + + +11 - - -507 + + +16 - - -435 + + +14 - - -339 + + +11 - - -517 + + +17 - - -441 + + +14 - - -536 + + +17 - - -330 + + +10 - - -299 + + +9 - - -366 + + +12 - - -407 + + +13 - - -341 + + +11 - - -58 + + +19 - - -31 + + +10 - - -33 + + +11 - - -59 + + +20 - - -58 + + +19 - - -32 + + +11 - - -47 + + +15 - - -58 + + +19 - - -50 + + +17 - - -46 + + +15 - - -27 + + +9 - - -48 + + +16 - - -35 + + +12 - - -40 + + +13 - - -54 + + +18 - - -27 + + +9 - - -36 + + +12 - - -52 + + +17 - - -56 + + +19 - - -39 + + +13 - - + + 48 - - + + 55 - - + + 51 - - + + 46 - - + + 40 - - + + 35 - - + + 46 - - + + 39 - - + + 54 - - + + 39 - - + + 59 - - + + 43 - - + + 36 - - + + 54 - - + + 27 - - + + 37 - - + + 27 - - + + 36 - - + + 27 - - + + 44 - - + + 43 - - + + 38 - - + + 33 - - + + 58 - - + + 55 - - + + 45 - - + + 58 - - + + 59 - - + + 36 - - + + 48 - - + + 39 - - + + 43 - - + + 54 - - + + 37 - - + + 55 - - + + 45 - - + + 40 - - -45 + + +465 - - -60 + + +619 - - -48 + + +493 - - -36 + + +368 - - -33 + + +343 - - -33 + + +345 - - -39 + + +401 - - -55 + + +569 - - + + 519 - - + + 413 - - + + 590 - - + + 437 - - + + 344 - - + + 310 - - + + 597 - - + + 348 - - + + 597 - - + + 421 - - + + 508 - - + + 574 - - + + 602 - - + + 263 - - + + 303 - - + + 435 - - + + 285 - - + + 534 - - + + 598 - - + + 287 - - + + 463 - - + + 484 - - + + 415 - - + + 435 - - + + 433 - - + + 588 - - + + 326 - - + + 521 - - + + 377 - - + + 419 - - + + 302 - - + + 473 - + + +26 + + + +43 + + + +40 + + + +27 + + + +46 + + + +33 + + + +40 + + -274 +56 - + -447 +50 - + -40 +54 - + -27 +33 - + -46 +37 - + -33 +41 - + -40 +26 - + -56 +59 - + -50 +32 - + -54 +51 - + -33 +45 - + -37 +52 - + -41 +29 - + -26 +57 - + -59 +39 - + -32 +59 - + -51 +31 - + -45 +34 - + -52 +29 - + -29 +25 - + -57 +36 - + -39 +45 - + -59 +57 - + -31 +57 - + -34 +53 - + -29 +47 - + -25 +56 - + -36 +57 - + -45 +59 - + -57 +59 - + -57 +352 - + -53 +557 - + -47 +264 - + -56 +361 - + -57 +382 - + -59 +526 - + -59 +316 - + -34 +433 - + -54 +320 - + -25 +617 - + -35 +290 - + -37 +287 - + -51 +558 - + -30 +409 - + -42 +331 - + -31 +494 - + -60 +304 - + -28 +446 - + -28 +508 - + -54 - - - -39 - - - -32 - - - -48 - - - -29 - - - -43 - - - -49 - - - -34 +348 - - -32 + + +331 - - -56 + + +575 - - -31 + + +323 - - -34 + + +351 - - -28 + + +293 - - -26 + + +271 - - -57 + + +586 - - -58 + + +600 - - -40 + + +415 - - -46 + + +474 - - -59 + + +609 - - -54 + + +553 - - + + 506 - - + + 417 - - + + 615 - - + + 374 - - + + 517 - - + + 290 - - + + 331 - - + + 519 - - -472 + + +46 - - -523 + + +51 - - -602 + + +58 - - -307 + + +30 - - -544 + + +53 - - -442 + + +43 - - -610 + + +59 - - -583 + + +56 - - -387 + + +37 - - -575 + + +56 - - -611 + + +59 - - -618 + + +60 - - -477 + + +46 - - -378 + + +36 - - -417 + + +40 - - -278 + + +27 - - -415 + + +40 - - -526 + + +51 - - -482 + + +47 - - -304 + + +29 - - -373 + + +36 - - -274 + + +26 - - -317 + + +31 - - -422 + + +41 - - -471 + + +45 - - -295 + + +28 - - -560 + + +54 - - -499 + + +48 - - + + 45 - - + + 58 - - + + 35 - - + + 36 - - + + 45 - - + + 49 - - + + 54 - - + + 46 - - + + 28 - - -59 - - - -45 + + +610 - - -49 + + +465 - - -31 + + +510 - - -39 + + +326 - - -48 + + +402 - - -30 + + +497 - - -46 + + +316 - - -48 + + +472 - - -30 + + +497 - - -26 + + +310 - - -43 + + +269 - - -36 + + +442 - - -53 + + +375 - - -57 + + +545 - - -28 + + +589 - - -26 + + +289 - - -30 + + +275 - - -29 + + +314 - - -46 + + +299 - - -31 + + +481 - - -35 + + +317 - - -36 + + +362 - - -36 + + +370 - - -51 + + +368 - - -32 + + +522 - - -51 + + +331 - - -42 + + +532 - - -40 + + +438 - - -57 + + +418 - - -52 + + +589 - - -47 + + +541 - - -60 + + +486 - - -45 + + +618 - - -59 + + +463 - - -36 + + +607 - - -58 + + +371 - - -28 + + +595 - - -43 + + +288 + + + +449 - -33 + +346 - + 58 - + 35 - + 47 - + 59 - + 28 - + 43 - + 50 - + 45 - + 48 - + 60 - + 57 - + 60 - + 42 - + 52 - + 38 - + 31 - - -496 + + +48 - - -496 + + +48 - - -609 + + +59 - - -535 + + +52 - - -442 + + +43 - - -496 + + +48 - - -502 + + +49 - - -519 + + +50 - - -269 + + +26 - - -615 + + +59 - - -447 + + +43 - - -614 + + +59 - - -570 + + +55 - - -357 + + +34 - - -277 + + +27 - - -428 + + +41 - - -610 + + +59 - - -555 + + +54 - - -527 + + +51 - - -446 + + +43 - - -375 + + +36 - - + + 271 - - + + 465 - - + + 263 - - + + 349 - - + + 335 - - + + 570 - - + + 448 - - + + 424 - - + + 420 - - + + 292 - - + + 363 - - + + 505 - - + + 384 - - + + 346 - - -57 + + +584 - - -42 + + +431 - - -49 + + +505 - - -56 + + +574 - - -58 + + +602 - - -52 + + +539 - - -36 + + +374 - - -27 + + +283 - - -27 + + +281 - - -27 + + +286 - - -59 + + +611 - - -53 + + +549 - - -26 + + +266 - - -38 + + +396 - - -48 + + +498 - - -45 + + +466 - - -59 + + +611 - - -38 + + +396 - - -55 + + +567 - - -30 + + +313 - - -55 + + +573 - - -38 + + +397 - - -51 + + +525 - - -55 + + +571 - - -46 + + +471 - -59 + +610 - + 30 - + 56 - + 43 - + 43 - + 56 - + 34 - + 42 - + 58 - + 53 - + 47 - + 33 - + 33 - + 42 - + 32 - + 58 - + 59 - + 47 - + 25 - + 41 - + 43 - + 48 - + 52 - + 32 - + 46 - + 48 - + 52 - + 25 - + 58 - + 60 - + 50 - + 30 - + 41 - + 39 - + 36 - + 47 - + 49 - + 57 - + 28 - + 32 - + 42 - + 28 - + 54 - + 55 - + 47 - + 56 - + 39 - + 48 - + 41 - + 39 - + 26 - + 32 - + 42 - + 48 - + 36 - + 31 - + 37 - + 36 - + 39 - + 45 - + 26 - + 55 - + 28 - + 51 - + 53 - + 42 - + 26 - + 30 - + 40 - + 57 - + 32 - + 52 - + 51 - + 51 - + 25 - - -38 + + +390 - - -42 + + +436 - - -38 + + +396 - - -26 + + +273 - - -51 + + +531 - - -25 + + +262 - - -30 + + +312 - - -38 + + +397 - - -41 + + +422 - - -32 + + +330 - - -38 + + +397 - - -49 + + +503 - - -57 + + +589 - - -25 + + +261 - - -57 + + +594 - - -48 + + +493 - - -28 + + +293 - - -27 + + +284 - - -47 + + +486 - - -44 + + +453 - - -42 + + +432 - - -42 + + +433 - - -37 + + +386 - - -56 + + +578 - - -42 + + +434 - - -51 + + +529 - - -38 + + +399 - - + + 426 - - + + 278 - - + + 386 - - + + 346 - - + + 384 - - + + 505 - - + + 471 - - + + 342 - - + + 402 - - + + 404 - - + + 453 - - + + 400 - - + + 507 - - -360 + + +35 - - -370 + + +36 - - -468 + + +45 - - -483 + + +47 - - -451 + + +44 - - -601 + + +58 - - -544 + + +53 - - -526 + + +51 - - -394 + + +38 - - -514 + + +50 - - -290 + + +28 - - -458 + + +44 - - -281 + + +27 - - -422 + + +41 - - -490 + + +47 - - -554 + + +54 - - -570 + + +55 - - -426 + + +41 - - -549 + + +53 - - -298 + + +29 - -586 + +57 - + 58 - + 32 - + 47 - + 27 - + 59 - + 32 - + 47 - + 36 - + 43 - + 39 - + 51 - + 45 - + 33 - + 57 - + 37 - + 33 - -46 + +15 - -39 + +13 - -45 + +15 - -45 + +15 - -55 + +18 - -41 + +13 - -42 + +14 - -51 + +17 - -31 + +10 - -50 + +17 - -58 + +19 - -36 + +12 - -29 + +9 - -44 + +15 - -52 + +17 - -37 + +12 - -33 + +11 - -41 + +14 - -37 + +12 - -26 + +8 - -58 + +19 - -42 + +14 - -52 + +17 - -55 + +18 - -59 + +20 - -30 + +10 - -41 + +14 - -26 + +8 - -31 + +10 - -25 + +8 - -40 + +13 - -33 + +11 - -38 + +12 - -40 + +13 - -47 + +16 - -31 + +10 - -51 + +17 - - -39 + + +13 - - -44 + + +15 - - -39 + + +13 - - -48 + + +16 - - -47 + + +16 - - -43 + + +14 - - -51 + + +17 - - -26 + + +8 - - -60 + + +20 - - -43 + + +14 - - -38 + + +13 - - -54 + + +18 - - -28 + + +9 - - -44 + + +15 - - + + 14 - - + + 9 - - + + 17 - - + + 17 - - + + 18 - - + + 12 - - + + 12 - - + + 19 - - + + 10 - - + + 9 - - + + 13 - - + + 18 - - + + 19 - - + + 17 - - + + 18 - - + + 10 - - + + 20 - - + + 18 - - + + 9 - - + + 19 - - + + 14 - - + + 13 - - + + 10 - - + + 19 - - + + 18 - - + + 12 - - + + 13 - - + + 16 - - + + 10 - - + + 14 - - + + 12 - - + + 20 - - + + 17 - - + + 10 - - + + 9 - - + + 14 - - + + 18 - - + + 16 - - + + 10 - - + + 12 - - + + 13 - - + + 15 - - + + 17 - - + + 8 - - + + 17 - - + + 9 - - + + 16 - - + + 18 - - + + 9 - - + + 17 - - + + 9 - - + + 15 - - + + 19 - - + + 13 - - + + 18 - - + + 16 - - + + 10 - - + + 17 - - + + 9 - - + + 10 - - + + 20 - - + + 19 - - + + 16 - - + + 9 - - + + 12 - - + + 13 - - + + 8 - - + + 15 - - + + 17 - - + + 14 - - + + 19 - - + + 9 - - + + 19 - - + + 12 - - + + 14 - - + + 15 - - + + 10 - - + + 19 - - + + 11 - - + + 11 - - + + 9 - - + + 15 - - + + 12 - - + + 14 - - + + 12 - - + + 11 - - + + 16 - - + + 13 - - + + 20 - - + + 8 - - + + 11 - - + + 10 - - + + 9 - - + + 17 - - + + 17 - - + + 17 - - + + 20 - - + + 10 - - + + 16 - - + + 16 - - + + 12 - - + + 18 - - + + 11 - - + + 14 - - + + 17 - - + + 14 - - + + 20 - - + + 18 - - + + 18 - - + + 17 - - + + 9 - - + + 12 - - + + 16 - - + + 15 - - + + 14 - - + + 13 - - + + 19 - - + + 19 - - + + 16 - - + + 20 - - + + 15 - - + + 14 - - + + 8 - - + + 10 - - + + 17 - - + + 19 - - + + 13 - - + + 16 - - + + 9 - - + + 8 - - + + 13 - - + + 12 - - + + 16 - - + + 11 - - + + 16 - - + + 13 - - + + 17 - - + + 9 - - + + 16 - - + + 10 - - + + 11 - - + + 9 - - + + 19 - - + + 19 - - + + 19 - - + + 11 - - + + 13 - - + + 9 - - + + 13 - - + + 9 - - + + 13 - - + + 12 - - + + 19 - - + + 12 - - + + 15 - - + + 11 - - + + 14 - - + + 8 - - + + 17 - - + + 11 - - + + 8 - - + + 18 - - + + 10 - - + + 18 - - + + 16 - - + + 18 - - + + 12 - - + + 14 - - + + 14 - - + + 18 - - + + 19 - - + + 15 - - + + 12 - - + + 13 - - + + 16 - - + + 13 - - + + 13 - - + + 12 - - + + 14 - - + + 8 - - + + 9 - - + + 17 - - + + 10 - - + + 14 - - + + 18 - - + + 13 - - + + 19 - - + + 18 - - + + 19 - - + + 19 - - + + 8 - - + + 12 - - + + 17 - - + + 19 - - + + 12 - - + + 15 - - + + 15 - - + + 10 - - + + 10 - - + + 15 - - + + 15 - - + + 12 - - + + 19 - - + + 12 - - + + 18 - - + + 11 - - + + 13 - - + + 16 - - + + 18 - - + + 19 - - + + 15 - - + + 17 - - + + 18 - - + + 14 - - + + 18 - - + + 18 - - + + 9 - - + + 13 - - + + 9 - - + + 14 - - + + 14 - - + + 12 - - + + 16 - - + + 14 - - + + 14 - - + + 15 - - + + 17 - - + + 16 - - + + 13 - - + + 12 - - + + 17 - - + + 19 - - + + 13 - - + + 19 - - + + 9 - - + + 11 - - + + 15 - - + + 10 - - + + 14 - - + + 14 - - + + 11 - - + + 12 - - + + 20 - - + + 16 - - + + 10 - - + + 14 - - + + 10 - - + + 15 - - + + 9 - - + + 18 - - + + 10 - - + + 15 - - + + 8 - - + + 9 - - + + 13 - - + + 19 - - + + 16 - - + + 15 - - + + 16 - - + + 10 - - + + 16 - - + + 9 - - + + 12 - - + + 14 - - + + 9 - - + + 16 - - + + 15 - - + + 12 - - + + 15 - - + + 17 - - + + 14 - - + + 9 - - + + 11 - - + + 9 - - + + 14 - - + + 8 - - + + 10 - - + + 13 - - + + 14 - - + + 12 - - + + 10 - - + + 18 - - + + 19 - - + + 14 - - + + 10 - - + + 9 - - + + 10 - - + + 15 - - + + 10 - - + + 18 - - + + 18 - - + + 9 - - + + 13 - - + + 13 - - + + 19 - - + + 17 - - + + 14 - - - + + + 13 - - + + 14 - - + + 9 - - + + 19 - - + + 15 - - + + 9 - - + + 12 - - + + 14 - - + + 10 - - + + 11 - - + + 19 - - + + 13 - - + + 12 - - + + 9 - - + + 17 - - + + 15 - - + + 13 - - + + 12 - - + + 12 - - + + 18 - - + + 9 - - + + 19 - - + + 19 - - + + 11 - - + + 16 - - + + 14 - - + + 11 - - + + 12 - - + + 20 - - + + 10 - - + + 10 - - + + 16 - - + + 17 - - + + 19 - - + + 13 - - + + 17 - - + + 10 - - + + 10 - - + + 19 - - + + 9 - - + + 14 - - + + 19 - - + + 13 - - + + 10 - - + + 19 - - + + 14 - - + + 14 - - + + 10 - - + + 12 - - + + 11 - - + + 11 - - + + 19 - - + + 19 - - + + 8 - - + + 8 - - + + 11 - - + + 13 - - + + 10 - - + + 12 - - + + 15 - - + + 9 - - + + 15 - - + + 11 - - + + 9 - - + + 9 - - + + 9 - - + + 15 - - + + 10 - - + + 11 - - + + 20 - - + + 8 - - + + 13 - - + + 16 - - + + 17 - - + + 14 - - + + 10 - - + + 12 - - + + 13 - - + + 16 - - + + 19 - - + + 9 - - + + 12 - - + + 8 - - + + 12 - - + + 15 - - + + 19 - - + + 17 - - + + 10 - - + + 8 - - + + 8 - - + + 8 - - + + 10 - - + + 11 - - + + 10 - - + + 19 - - + + 11 - - + + 14 - - + + 18 - - + + 11 - - + + 14 - - + + 12 - - + + 17 - - + + 13 - - + + 11 - - + + 10 - - + + 9 - - + + 19 - - + + 17 - - + + 11 - - + + 8 - - + + 10 - - + + 11 - - + + 19 - - + + 15 - - + + 17 - - + + 12 - - + + 18 - - + + 12 - - + + 13 - - + + 9 - - + + 13 - - + + 15 - - + + 18 - - + + 13 - - + + 17 - - + + 17 - - + + 19 - - + + 15 - - + + 19 - - + + 10 - - + + 13 - - + + 10 - - + + 10 - - + + 9 - - + + 16 - - + + 16 - - + + 19 - - + + 10 - - + + 13 - - + + 19 - - + + 8 - - + + 9 - - + + 13 - - + + 18 - - + + 18 - - + + 13 - - + + 20 - - + + 19 - - + + 15 - - + + 9 - - + + 11 - - + + 14 - - + + 19 - - + + 18 - - + + 9 - - + + 11 - - + + 12 - - + + 18 - - + + 12 - - + + 9 - - + + 9 - - + + 9 - - + + 16 - - + + 15 - - + + 12 - - + + 17 - - + + 19 - - + + 9 - - + + 11 - - + + 12 - - + + 15 - - + + 10 - - + + 11 - - + + 12 - - + + 15 - - + + 19 - - + + 17 - - + + 16 - - + + 15 - - + + 12 - - + + 17 - - + + 13 - - + + 17 - - + + 11 - - + + 8 - - + + 11 - - + + 11 - - + + 12 - - + + 11 - - + + 16 - - + + 12 - - + + 17 - - + + 12 - - + + 13 - - + + 16 - - + + 11 - - + + 14 - - + + 15 - - + + 18 - - + + 11 - - + + 13 - - + + 15 - - + + 19 - - + + 16 - - + + 10 - - + + 9 - - + + 11 - - + + 14 - - + + 10 - - + + 18 - - + + 17 - - + + 19 - - + + 13 - - + + 14 - - + + 13 - - + + 13 - - + + 15 - - + + 16 - - + + 12 - - + + 17 - - + + 9 - - + + 10 - - + + 12 - - + + 13 - - + + 20 - - + + 19 - - + + 16 - - + + 17 - - + + 14 - - + + 19 - - + + 19 - - + + 10 - - + + 10 - - + + 10 - - + + 17 - - + + 20 - - + + 15 - - + + 12 - - + + 8 - - + + 16 - - + + 13 - - + + 15 - - + + 12 - - + + 9 - - + + 12 - - + + 19 - - + + 11 - - + + 14 - - + + 18 - - + + 15 - - + + 14 - - + + 10 - - + + 12 - - + + 18 - - + + 15 - - + + 16 - - + + 19 - - + + 15 - - + + 13 - - + + 17 - - + + 18 - - + + 19 - - + + 11 - - + + 11 - - + + 15 - - + + 9 - - + + 14 - - + + 13 - - + + 17 - - + + 17 - - + + 17 - - + + 10 - - + + 16 - - + + 12 - - + + 10 - - + + 11 - - + + 14 - - + + 12 - - + + 17 - - + + 13 - - + + 19 - - + + 16 - - + + 17 - - + + 19 - - + + 15 - - + + 10 - - + + 19 - - + + 19 - - + + 18 - - + + 19 - - + + 17 - - + + 9 - - + + 12 - - + + 12 - - + + 14 - - + + 11 - - + + 17 - - + + 15 - - + + 10 - - + + 19 - - + + 13 - - + + 17 - - + + 11 - - + + 16 - - + + 16 - - + + 15 - - + + 19 - - + + 8 - - + + 8 - - + + 16 - - + + 11 - - + + 9 - - + + 14 - + 11 - - + + 19 - - + + 19 - - + + 11 - - + + 9 - - + + 18 - - + + 14 - - + + 12 - - + + 17 - - + + 11 - - + + 14 - - + + 17 - - + + 17 - - + + 19 - - + + 10 - - + + 11 - - + + 12 - - + + 19 - - + + 16 - - + + 19 - - + + 15 - - + + 15 - - + + 14 - - + + 10 - - + + 17 - - + + 11 - - + + 19 - - + + 17 - - + + 8 - - + + 15 - - + + 9 - - + + 15 - - + + 9 - - + + 17 - - + + 16 - - + + 9 - - + + 12 - - + + 13 - - + + 14 - - + + 15 - - + + 16 - - + + 14 - - + + 19 - - + + 18 - - + + 10 - - + + 15 - - + + 12 - - + + 19 - - + + 15 - - + + 16 - - + + 18 - - + + 10 - - + + 19 - - + + 9 - - + + 19 - - + + 14 - - + + 10 - - + + 14 - - + + 17 - - + + 17 - - + + 17 - - + + 13 - - + + 11 - - + + 11 - - + + 11 - - + + 18 - - + + 8 - - + + 13 - - + + 11 - - + + 15 - - + + 18 - - + + 17 - - + + 11 - - + + 18 - - + + 12 - - + + 11 - - + + 18 - - + + 9 - - + + 15 - - + + 16 - - + + 19 - - + + 11 - - + + 19 - - + + 12 - - + + 13 - - + + 19 - - + + 10 - - + + 18 - - + + 10 - - + + 11 - - + + 10 - - + + 9 - - + + 12 - - + + 19 - - + + 15 - - + + 19 - - + + 13 - - + + 17 - - + + 17 - - + + 10 - - + + 10 - - + + 14 - - + + 11 - - + + 14 - - + + 9 - - + + 17 - - + + 18 - - + + 19 - - + + 9 - - + + 16 - - + + 17 - - + + 18 - - + + 15 - - + + 13 - - + + 9 - - + + 12 - - + + 10 - - + + 16 - - + + 8 - - + + 9 - - + + 8 - - + + 14 - - + + 9 - - + + 16 - - + + 14 - - + + 18 - - + + 16 - - + + 11 - - + + 16 - - + + 20 - - + + 19 - - + + 15 - - + + 17 - - + + 13 - - + + 12 - - + + 14 - - + + 16 - - + + 10 - - + + 15 - - + + 14 - - + + 14 - - + + 11 - - + + 13 - - + + 9 - - + + 19 - - + + 17 - - + + 9 - - + + 15 - - + + 12 - - + + 19 - - + + 12 - - + + 8 - - + + 8 - - + + 16 - - + + 14 - - + + 17 - - + + 10 - - + + 15 - - + + 9 - - + + 11 - - + + 15 - - + + 17 - - + + 12 - - + + 15 - - + + 15 - - + + 19 - - + + 15 - - + + 19 - - + + 13 - - + + 14 - - + + 17 - - + + 20 - - + + 18 - - + + 12 - - + + 9 - - + + 9 - - + + 9 - - + + 17 - - + + 10 - - + + 19 - - + + 12 - - + + 14 - - + + 15 - - + + 9 - - + + 12 - - + + 15 - - + + 12 - - + + 11 - - + + 13 - - + + 14 - - + + 9 - - + + 9 - - + + 20 - - + + 12 - - + + 13 - - + + 14 - - + + 19 - - + + 14 - - + + 9 - - + + 9 - - + + 18 - - + + 14 - - + + 15 - - + + 8 - - + + 9 - - + + 10 - - + + 18 - - + + 9 - - + + 14 - - + + 18 - - + + 11 - - + + 17 - - + + 15 - - + + 10 - - + + 11 - - + + 13 - - + + 20 - - + + 13 - - + + 16 - - + + 12 - - + + 17 - - + + 9 - - + + 8 - - + + 11 - - + + 13 - - + + 17 - - + + 20 - - + + 13 - - + + 17 - - + + 18 - - + + 9 - - + + 15 - - + + 10 - - + + 11 - - + + 15 - - + + 15 - - + + 13 - - + + 13 - - + + 20 - - + + 12 - - + + 9 - - + + 11 - - + + 18 - - + + 19 - - + + 10 - - + + 9 - - + + 15 - - + + 9 - - + + 10 - - + + 16 - - + + 17 - - + + 8 - - + + 14 - - + + 19 - - + + 16 - - + + 14 - - + + 18 - - + + 8 - - + + 15 - - + + 14 - - + + 16 - - + + 18 - - + + 12 - - + + 12 - - + + 18 - - + + 11 - - + + 10 - - + + 14 - - + + 10 - - + + 17 - - + + 11 - - + + 14 - - + + 10 - - + + 12 - - + + 18 - - + + 20 - - + + 18 - - + + 13 - - + + 15 - - + + 18 - - + + 19 - - + + 13 - - + + 17 - - + + 14 - - + + 15 - - + + 19 - - + + 10 - - + + 18 - - + + 13 - - + + 8 - - + + 12 - - + + 19 - - + + 20 - - + + 14 - - + + 16 - - + + 14 - - + + 20 - - + + 14 - - + + 18 - - + + 15 - - + + 9 - - + + 9 - - + + 15 - - + + 14 - - + + 15 - - + + 19 - - + + 20 - - + + 17 - - + + 20 - - + + 14 - - + + 10 - - + + 10 - - + + 10 - - + + 19 - - + + 9 - - + + 19 - - + + 18 - - + + 18 - - + + 17 - - + + 14 - - + + 9 - - + + 14 - - + + 17 - - + + 19 - - + + 18 - - + + 8 - - + + 15 - - + + 11 - - + + 19 - - + + 11 - - + + 10 - - + + 18 - - + + 17 - - + + 12 - - + + 12 - - + + 14 - - + + 16 - - + + 10 - - + + 13 - - + + 17 - - + + 18 - - + + 16 - - + + 9 - - + + 13 - - + + 19 - - + + 8 - - + + 8 - - + + 13 - - + + 12 - - + + 19 - - + + 13 - - + + 10 - - + + 9 - - + + 10 - - + + 10 - - + + 16 - - + + 19 - - + + 15 - - + + 13 - - + + 16 - - + + 18 - - + + 10 - - + + 20 - - + + 9 - - + + 12 - - + + 16 - - + + 10 - - + + 19 - - + + 8 - - + + 19 - - + + 19 - - + + 8 - - + + 10 - - + + 17 - - + + 18 - - + + 15 - - + + 9 - - + + 18 - - + + 10 - - + + 9 - - + + 15 - - + + 9 - - + + 8 - - + + 13 - - + + 20 - - + + 14 - - + + 13 - - + + 14 - - + + 17 - - + + 16 - - + + 14 - - + + 12 - - + + 17 - - + + 13 - - + + 19 - - + + 11 - - + + 20 - - + + 12 - - + + 20 - - + + 13 - - + + 19 - - + + 17 - - + + 18 - - + + 19 - - + + 15 - - + + 11 - - + + 18 - - + + 15 - - + + 13 - - + + 17 - - + + 20 - - + + 11 - - + + 9 - - + + 8 - - + + 8 - - + + 15 - - + + 9 - - + + 8 - - + + 19 - - + + 14 - - + + 14 - - + + 19 - - + + 17 - - + + 16 - - + + 18 - - + + 12 - - + + 18 - - + + 12 - - + + 9 - - + + 18 - - + + 10 - + 19 - + 19 - + 13 - + 10 - + 15 - + 12 - + 11 - + 11 - + 18 - + 18 - + 19 - + 10 - + 19 - + 15 - + 19 - + 10 - + 18 - + 19 - + 8 - + 14 - + 9 - + 19 - + 19 - + 10 - + 20 - + 9 - + 19 - + 17 - + 10 - + 11 - + 14 - + 13 - + 17 - + 10 - + 14 - - + + 11 - - + + 13 - - + + 14 - - + + 16 - - + + 10 - - + + 18 - - + + 9 - - + + 13 - - + + 12 - - + + 14 - - + + 19 - - + + 15 - - + + 14 - - + + 18 - - + + 15 - - + + 16 - - + + 17 - - + + 12 - - + + 17 - - + + 14 - - + + 12 - - + + 10 - - + + 15 - - + + 13 - - + + 15 - - + + 19 - - + + 11 - - + + 12 - - + + 15 - - + + 12 - - + + 11 - - + + 11 - - + + 12 - - + + 14 - - + + 15 - - + + 17 - - + + 12 - - + + 16 - - + + 17 - - + + 10 - - + + 13 - - + + 9 - - + + 13 - - + + 16 - - + + 13 - - + + 13 - - + + 15 - - + + 10 - - + + 18 - - + + 18 - - + + 9 - - + + 17 - - + + 18 - - + + 8 - - + + 9 - - + + 12 - - + + 14 - - + + 13 - - + + 10 - - + + 19 - - + + 19 - - + + 13 - - + + 8 - - + + 10 - - + + 8 - - + + 13 - - + + 9 - - + + 16 - - + + 15 - - + + 10 - - + + 16 - - + + 9 - - + + 11 - - + + 20 - - + + 13 - - + + 11 - - + + 15 - - + + 17 - - + + 18 - - + + 13 - - + + 14 - - + + 16 - - + + 8 - - + + 17 - - + + 9 - - + + 9 - - + + 16 - - + + 18 - - + + 14 - - + + 17 - - + + 10 - - + + 13 - - + + 11 - - + + 9 - - + + 20 - - + + 11 - - + + 14 - - + + 10 - - + + 20 - - + + 8 - - + + 12 - - + + 9 - - + + 13 - - + + 17 - - + + 16 - - + + 13 - - + + 12 - - + + 10 - - + + 8 - - + + 11 - - + + 18 - - + + 13 - - + + 19 - - + + 12 - - + + 11 - - + + 17 - - + + 9 - - + + 15 - - + + 8 - - + + 12 - - + + 20 - - + + 18 - - + + 16 - - + + 17 - - + + 12 - - + + 15 - - + + 9 - - + + 17 - - + + 16 - - + + 13 - - + + 10 - - + + 12 - - + + 19 - - + + 16 - - + + 14 - - + + 20 - - + + 18 - - + + 16 - - + + 10 - - + + 17 - - + + 10 - - + + 14 - - + + 13 - - + + 18 - - + + 9 - - + + 19 - - + + 8 - - + + 11 - - + + 19 - - + + 16 - - + + 8 - - + + 15 - - + + 10 - - + + 18 - - + + 12 - - + + 13 - - + + 14 - - + + 13 - - + + 15 - - + + 14 - - + + 19 - - + + 11 - - + + 20 - - + + 11 - - + + 15 - - + + 11 - - + + 14 - - + + 9 - - + + 17 - - + + 9 - - + + 16 - - + + 11 - - + + 16 - - + + 15 - - + + 19 - - + + 13 - - + + 11 - - + + 8 - - + + 15 - - + + 16 - - + + 14 - - + + 20 - - + + 13 - - + + 19 - - + + 14 - - + + 16 - - + + 19 - - + + 14 - - + + 13 - - + + 13 - - + + 12 - - + + 14 - - + + 11 - - + + 10 - - + + 17 - - + + 18 - - + + 12 - - + + 17 - - + + 18 - - + + 16 - - + + 17 - - + + 19 - - + + 10 - - + + 10 - - + + 14 - - + + 15 - - + + 9 - - + + 17 - - + + 15 - - + + 18 - - + + 15 - - + + 18 - - + + 13 - - + + 11 - - + + 19 - - + + 18 - - + + 13 - - + + 14 - - + + 14 - - + + 19 - - + + 19 - - + + 17 - - + + 15 - - + + 14 - - + + 9 - - + + 14 - - + + 11 - - + + 19 - - + + 19 - - + + 18 - - + + 8 - - + + 18 - - + + 19 - - + + 14 - - + + 8 - - + + 9 - - + + 9 - - + + 8 - - + + 10 - - + + 10 - - + + 15 - + 8 - + 16 - + 13 - + 11 - + 16 - + 11 - + 11 - + 14 - + 13 - + 11 - + 20 - + 12 - + 16 - + 8 - + 8 - + 16 - + 17 - + 11 - + 17 - + 9 - + 20 - + 13 - + 20 - + 15 - + 10 - + 17 - + 18 - + 9 - + 15 - + 19 - + 10 - + 14 - + 15 - + 19 - - + + 14 - - + + 10 - - + + 15 - - + + 18 - - + + 17 - - + + 13 - - + + 13 - - + + 17 - - + + 18 - - + + 11 - - + + 18 - - + + 18 - - + + 17 - - + + 16 - - + + 9 - - + + 18 - - + + 15 - - + + 11 - - + + 13 - - + + 11 - - + + 19 - - + + 12 - - + + 11 - - + + 10 - - + + 9 - - + + 14 - - + + 11 - - + + 12 - - + + 19 - - + + 9 - - + + 13 - - + + 10 - - + + 10 - - + + 13 - - + + 15 - - + + 13 - - + + 19 - - + + 17 - - + + 16 - - + + 13 - - + + 9 - - + + 15 - - + + 10 - - + + 16 - - + + 20 - - + + 13 - - + + 11 - - + + 9 - - + + 19 - - + + 17 - - + + 11 - - + + 20 - - + + 18 - - + + 15 - - + + 10 - - + + 11 - - + + 10 - - + + 16 - - + + 13 - - + + 10 - - + + 14 - - + + 15 - - + + 14 - - + + 11 - - + + 17 - - + + 8 - - + + 15 - - + + 13 - - + + 8 - - + + 12 - - + + 11 - - + + 12 - - + + 10 - - + + 15 - - + + 16 - - + + 15 - - + + 10 - - + + 17 - - + + 13 - - + + 17 - - + + 11 - - + + 16 - - + + 10 - - + + 17 - - + + 14 - - + + 19 - - + + 11 - - + + 11 - - + + 8 - - + + 11 - - + + 13 - - + + 12 - - + + 8 - - + + 19 - - + + 15 - - + + 11 - - + + 14 - - + + 16 - - + + 17 - - + + 12 - - + + 15 - - + + 9 - - + + 13 - - + + 12 - - + + 9 - - + + 18 - - + + 19 - - + + 20 - - + + 13 - - + + 9 - - + + 11 - - + + 10 - - + + 11 - - + + 9 - - + + 16 - - + + 17 - - + + 16 - - + + 19 - - + + 20 - - + + 13 - - + + 10 - - + + 10 - - + + 10 - - + + 12 - - + + 12 - - + + 15 - - + + 19 - - + + 11 - - + + 20 - - + + 9 - - + + 13 - - + + 15 - - + + 15 - - + + 18 - - + + 11 - - + + 17 - - + + 14 - - + + 10 - - + + 13 - - + + 20 - - + + 19 - - + + 14 - - + + 9 - - + + 15 - - + + 19 - - + + 16 - - + + 10 - - + + 10 - - + + 19 - - + + 12 - - + + 16 - - + + 14 - - + + 17 - - + + 18 - - + + 17 - - + + 11 - - + + 11 - - + + 16 - - + + 11 - - + + 19 - - + + 20 - - + + 17 - - + + 14 - - + + 15 - - + + 11 - - + + 17 - - + + 19 - - + + 9 - - + + 14 - - + + 19 - - + + 11 - - + + 20 - - + + 15 - - + + 14 - - + + 14 - - + + 15 - - + + 10 - - + + 10 - - + + 16 - - + + 10 - - + + 20 - - + + 8 - - + + 18 - - + + 11 - - + + 17 - - + + 18 - - + + 13 - - + + 12 - - + + 13 - - + + 18 - - + + 18 - - + + 18 - - + + 10 - - + + 18 - - + + 16 - - + + 15 - - + + 12 - - + + 18 - - + + 16 - - + + 18 - - + + 11 - - + + 14 - - + + 15 - - + + 17 - - + + 13 - - + + 13 - - + + 18 - - + + 18 - - + + 9 - - + + 8 - - + + 8 - - + + 16 - - + + 20 - - + + 18 - - + + 19 - - + + 16 - - + + 14 - - + + 10 - - + + 17 - - + + 15 - - + + 13 - - + + 10 - - + + 13 - - + + 19 - - + + 16 - - + + 14 - - + + 12 - - + + 19 - - + + 9 - - + + 11 - - + + 16 - - + + 8 - - + + 9 - - + + 20 - - + + 19 - - + + 18 - - + + 8 - - + + 18 - - + + 19 - - + + 15 - - + + 8 - - + + 13 - - + + 18 - - + + 17 - - + + 14 - - + + 17 - - + + 12 - - + + 20 - - + + 11 - - + + 10 - - + + 16 - - + + 10 - - + + 18 - - + + 16 - - + + 11 - - + + 9 - - + + 13 - - + + 17 - - + + 18 - - + + 19 - - + + 18 - - + + 11 - - + + 9 - - + + 15 - - + + 12 - - + + 14 - - + + 19 - - + + 18 - - + + 10 - - + + 10 - - + + 19 - - + + 16 - - + + 13 - - + + 15 - - + + 14 - - + + 13 - - + + 14 - - + + 18 - - + + 15 - - + + 15 - - + + 16 - - + + 13 - - + + 11 - - + + 19 - - + + 18 - - + + 14 - - + + 9 - - + + 18 - - + + 16 - - + + 12 - - + + 9 - - + + 15 - - + + 16 - - + + 11 - - + + 11 - - + + 19 - - + + 17 - - + + 14 - - + + 16 - - + + 8 - - + + 13 - - + + 13 - - + + 17 - - + + 17 - - + + 17 - - + + 12 - - + + 9 - - + + 14 - - + + 11 - - + + 10 - - + + 10 - - + + 8 - - + + 11 - - + + 18 - - + + 18 - - + + 9 - - + + 19 - - + + 17 - - + + 13 - - + + 12 - - + + 19 - - + + 18 - - + + 19 - - + + 10 - - + + 16 - - + + 15 - - + + 12 - - + + 12 - - + + 19 - - + + 8 - - + + 12 - - + + 9 - - + + 11 - - + + 19 - - + + 15 - - + + 9 - - + + 16 - - + + 15 - - + + 14 - - + + 19 - - + + 16 - - + + 19 - - + + 15 - - + + 10 - - + + 19 - - + + 17 - - + + 11 - - + + 12 - - + + 20 - - + + 19 - - + + 18 - - + + 16 - - + + 15 - - + + 17 - - + + 19 - - + + 16 - - + + 15 - - + + 17 - - + + 16 - - + + 15 - - + + 18 - - + + 10 - - + + 14 - - + + 12 - - + + 18 - - + + 18 - - + + 13 - - - + + + 18 - - + + 10 - - + + 17 - - + + 14 - - + + 14 - - + + 16 - - + + 14 - - + + 12 - - + + 9 - - + + 14 - - + + 18 - - + + 10 - - + + 16 - - + + 11 - - + + 9 - + 18 - + 12 - + 13 - + 11 - + 10 - + 12 - + 14 - + 11 - + 14 - + 14 - + 12 - + 14 - + 9 - + 9 - + 15 - + 18 - + 15 - + 16 - + 10 - + 13 - + 13 - - + + 20 - - + + 19 - - + + 9 - - + + 8 - - + + 19 - - + + 11 - - + + 19 - - + + 20 - - + + 18 - - + + 14 - - + + 8 - - + + 11 - - + + 17 - - + + 9 - - + + 13 - - + + 20 - - + + 19 - - + + 19 - - + + 18 - - + + 9 - - + + 11 - - + + 18 - - + + 18 - - + + 19 - - + + 16 - - + + 15 - - + + 8 - - + + 18 - - + + 20 - - + + 18 - - + + 16 - - + + 13 - - + + 9 - - + + 10 - - + + 10 - - + + 10 - - + + 10 - - + + 10 - - + + 13 - - + + 9 - - + + 17 - - + + 15 - - + + 18 - - + + 14 - - + + 18 - - + + 16 - - + + 11 - - + + 8 - - + + 15 - - + + 16 - - + + 11 - - + + 18 - - + + 20 - - + + 11 - - + + 12 - - + + 15 - - + + 13 - - + + 17 - - + + 12 - - + + 16 - - + + 19 - - + + 11 - - + + 17 - - + + 15 - - + + 15 - - + + 18 - - + + 12 - - + + 12 - - + + 13 - - + + 17 - - + + 17 - - + + 9 - - + + 19 - - + + 11 - - + + 14 - - + + 12 - - + + 14 - - + + 9 - - + + 13 - - + + 19 - - + + 10 - - + + 19 - - + + 10 - - + + 9 - - + + 11 - - + + 13 - - + + 15 - - + + 14 - - + + 12 - - + + 8 - - + + 9 - - + + 18 - - + + 13 - - + + 8 - - + + 14 - - + + 15 - - + + 8 - - + + 15 - - + + 9 - - + + 9 - - + + 13 - - + + 13 - - + + 20 - - + + 9 - - + + 11 - - + + 9 - - + + 13 - - + + 11 - - + + 9 - - + + 17 - - + + 14 - - + + 9 - - + + 14 - - + + 12 - - + + 15 - - + + 9 - - + + 17 - - + + 14 - - + + 13 - - + + 15 - - + + 18 - - + + 11 - - + + 11 - - + + 19 - - + + 19 - - + + 15 - - + + 16 - - + + 19 - - + + 8 - - + + 13 - - + + 10 - - + + 9 - - + + 13 - - + + 20 - - + + 12 - - + + 11 - - + + 11 - - + + 15 - - + + 14 - - + + 10 - - + + 17 - - + + 18 - - + + 19 - - + + 15 - - + + 12 - - + + 8 - - + + 12 - - + + 10 - - + + 11 - - + + 15 - - + + 8 - - + + 8 - - + + 17 - - + + 9 - - + + 12 - - + + 17 - - + + 17 - - + + 15 - - + + 18 - - + + 17 - - + + 9 - - + + 19 - - + + 11 - - + + 20 - - + + 13 - - + + 19 - - + + 12 - - + + 11 - - + + 15 - - + + 9 - - + + 14 - - + + 14 - - + + 12 - - + + 8 - - + + 19 - - + + 12 - - + + 15 - - + + 11 - - + + 14 - - + + 19 - - + + 8 - - + + 10 - - + + 18 - - + + 9 - - + + 14 - - + + 8 - - + + 13 - - + + 17 - - + + 18 - - + + 20 - - + + 11 - - + + 11 - - + + 17 - - + + 18 - - + + 10 - - + + 12 - - + + 9 - - + + 14 - - + + 19 - - + + 16 - - + + 16 - - + + 18 - - + + 12 - - + + 10 - - + + 12 - - + + 18 - - + + 14 - - + + 20 - - + + 19 - - + + 19 - - + + 11 - - + + 14 - - + + 11 - - + + 15 - - + + 16 - - + + 15 - - + + 17 - - + + 15 - - + + 12 - - + + 20 - - + + 18 - - + + 16 - - + + 12 - - + + 10 - - + + 14 - - + + 14 - - + + 19 - - + + 17 - - + + 18 - - + + 14 - - + + 13 - - + + 15 - - + + 11 - - + + 9 - - + + 19 - - + + 17 - - + + 10 - - + + 9 - - + + 15 - - + + 19 - - + + 20 - - + + 10 - - + + 17 - - + + 14 - - + + 13 - - + + 16 - - + + 16 - - + + 12 - - + + 17 - - + + 10 - - + + 12 - - + + 18 - - + + 9 - - + + 16 - - + + 20 - - + + 20 - - + + 16 - - + + 14 - - + + 12 - - + + 9 - - + + 16 - - + + 11 - - + + 11 - - + + 13 - - + + 14 - - + + 17 - - + + 15 - - + + 9 - - + + 17 - - + + 16 - - + + 19 - - + + 13 - - + + 13 - - + + 15 - - + + 11 - - + + 10 - - + + 11 - - + + 20 - - + + 13 - - + + 14 - - + + 11 - - + + 20 - - + + 9 - - + + 12 - - + + 16 - - + + 12 - - + + 10 - - + + 9 - - + + 15 - - + + 18 - - + + 15 - - + + 18 - - + + 15 - - + + 9 - - + + 13 - - + + 15 - - + + 9 - - + + 19 - - + + 8 - - + + 17 - - + + 15 - - + + 16 - - + + 20 - - + + 12 - - + + 17 - - + + 11 - - + + 9 - - + + 9 - - + + 16 - - + + 16 - - + + 16 - - + + 16 - - + + 11 - - + + 9 - - + + 11 - - + + 19 - - + + 12 - - + + 8 - - + + 14 - - + + 14 - - + + 15 - - + + 8 - - + + 16 - - + + 17 - - + + +19 + + + +14 + + + +9 + + + +9 + + + +8 + + + +11 + + + +19 + + + +18 + + + +11 + + + +13 + + + +10 + + + +14 + + + +15 + + + +12 + + + +16 + + + +16 + + + +18 + + + +14 + + + +15 + + + +14 + + + +13 + + + +16 + + + +16 + + + +17 + + + +14 + + + +11 + + + +16 + + + +10 + + + +20 + + + +17 + + + +20 + + + +14 + + + +20 + + + +8 + + + +19 + + + +19 + + + +8 + + + +16 + + + +10 + + + +16 + + + +16 + + + +9 + + + +16 + + + +12 + + + +13 + + + +17 + + + +15 + + + +12 + + + +18 + + + 19 + + +14 + + + +19 + + + +17 + + + +14 + + + +8 + + + +11 + - + 14 - -9 + +17 diff --git a/assets/gpx/KatamachiRapid.gpx b/assets/gpx/KatamachiRapid.gpx index 39f3a8dd27..811dbee18d 100644 --- a/assets/gpx/KatamachiRapid.gpx +++ b/assets/gpx/KatamachiRapid.gpx @@ -3,406 +3,406 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -480,283 +480,283 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -837,442 +837,442 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1317,1366 +1317,1750 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/gpx/KeioSpecialExpress.gpx b/assets/gpx/KeioSpecialExpress.gpx index 97ea30f26f..b8e310226a 100644 --- a/assets/gpx/KeioSpecialExpress.gpx +++ b/assets/gpx/KeioSpecialExpress.gpx @@ -3,580 +3,580 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -600,5668 +600,6859 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/gpx/SampleTohokuShinkansen.gpx b/assets/gpx/SampleTohokuShinkansen.gpx index 2d01912ab1..2b5774a52a 100644 --- a/assets/gpx/SampleTohokuShinkansen.gpx +++ b/assets/gpx/SampleTohokuShinkansen.gpx @@ -3,3262 +3,3262 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -3396,2851 +3396,3025 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/gpx/SobuRapid.gpx b/assets/gpx/SobuRapid.gpx index 341a6985e1..dd6b585be1 100644 --- a/assets/gpx/SobuRapid.gpx +++ b/assets/gpx/SobuRapid.gpx @@ -3,2722 +3,3493 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/location-simulation.md b/docs/location-simulation.md index 4137a09aac..c655e7c28d 100644 --- a/docs/location-simulation.md +++ b/docs/location-simulation.md @@ -30,8 +30,13 @@ useSimulationMode ──────────────────── ## GPX を生成する `scripts/generate-location-gpx.mjs` が StationAPI の実際の駅座標から GPX を生成する。 -速度プロファイルはアプリ本体の `generateTrainSpeedProfile` をそのまま使うため、 -加減速の挙動はオートモードと同じモデルになる。 +駅間の長さは StationAPI の `trackDistanceFromPrevious` (線路の長さ) を使い、返らない +区間だけ駅座標の直線距離で代える。点は駅座標を結んだ直線の上に置くので、カーブの +区間では実際の線路から横にずれる。 + +速度プロファイルはアプリ本体の `generateTrainSpeedProfile` で作る (加速→巡航→減速の +台形)。`--line-group` では、区間の時間を StationAPI の到着時間推定に合わせる +(「列車種別グループから生成する」を参照)。 まず路線の駅 ID を調べる。 @@ -55,9 +60,9 @@ npm run gpx:generate -- \ | `--line` | 路線 ID。東北新幹線は `1004` | | `--line-group` | 列車種別グループ ID。`--line` と排他 | | `--from` / `--to` | 始点・終点の駅 ID。API の並び順に関わらず指定した向きで走る | -| `--max-speed` | 最高速度 (km/h)。既定は `320` | +| `--max-speed` | 最高速度 (km/h)。`--line` の既定は `320`。`--line-group` で指定すると、到着時間推定を使わずにこの速度で走る | | `--skip` | 通過駅の駅 ID (カンマ区切り)。停車しないだけで経路上は通過する | -| `--dwell` | 停車駅での停車時間 (秒)。既定は `60` | +| `--dwell` | 停車駅での停車時間 (秒)。`--line-group` の既定は到着時間推定の停車時間、それ以外の既定は `60` | | `--api` | StationAPI の URL。既定は `$GQL_API_URL` か本番エンドポイント | 出力は 1 秒間隔の `` 列になる。iOS は `distanceInterval` 基準で概ね 1Hz 配信のため、 @@ -77,6 +82,25 @@ npm run gpx:generate -- \ - `--from` / `--to` を省略すると経路の全区間を走る。 - `--skip` は自動判定に**追加**される。臨時の通過を足すときに使う。 +区間の時間は、StationAPI の `trainRoute` に `model: Estimated` を渡して取る到着・出発の +見込みに合わせる。見込みは、同じ区間の `estimateArrivalTimes` と同じ値で、路線・種別 +ごとの速度を実ダイヤで較正した到着時間推定から出る。 + +- 停車駅から次の停車駅までの時間は「次の停車駅の到着 − この駅の出発」。推定では停まるが + GPX では通過する駅 (平日運転の駅を休日に走る、`--skip` など) は、その駅の停車時間を + 走行から引く。 +- その時間で走り切る巡航速度を台形の式から求め、線路の長さの上を走らせる。加減速は + `trainRoute` の値 (推定のモデルの値) を使う。区間ごとの 1 秒未満の端数は、次の区間で + 計画した到着時刻へ戻すので、終点までに積み上がらない。 +- 途中の停車駅では、推定の停車時間 (出発 − 到着) だけ停まる。GPX では停まるのに推定では + 通過する駅があると、停車時間の見込みが無いので `--dwell` を求めて止まる。 +- 見込みには運転余裕のぶんの時間が入っているので、巡航速度は路線の最高速度より低くなる。 + 駅間の中ほどでは、実際の列車より遅く走る。到着圏に入るときの速度は、減速度で決まり + 巡航速度にはほとんど依存しない (駅の手前 x m での速度は √(2·減速度·x))。 +- 推定に較正の無い路線・種別は、実ダイヤとの差が大きいことがある。 + +`trainRoute` の `model` 引数が無い StationAPI では、`--line-group` はエラーになる。 + `lineGroupId` は駅から辿る。`--list-train-types` にその駅の駅 ID を渡すと、 `groupId` と種別名の対応が出る。 @@ -89,7 +113,7 @@ npm run gpx:generate -- --list-train-types 2400101 npm run gpx:generate -- --line-group 71 --list # 新宿→京王八王子を特急の停車パターンで生成する -npm run gpx:generate -- --line-group 71 --max-speed 110 --out assets/gpx/KeioSpecialExpress.gpx +npm run gpx:generate -- --line-group 71 --out assets/gpx/KeioSpecialExpress.gpx ``` 生成時、標準エラーに同じ経路をアプリで開くためのディープリンクのクエリ部を出す。 @@ -209,7 +233,7 @@ GPX 1.1 にはメートル単位の水平精度を書く標準要素が無い (` ```bash # Fライナー相当 (元町・中華街→飯能) npm run gpx:generate -- \ - --line-group 152 --max-speed 80 --signal-profile subway \ + --line-group 152 --signal-profile subway \ --subway-lines 99310,22003 --out assets/gpx/FLinerSeibu.gpx ``` @@ -219,9 +243,9 @@ npm run gpx:generate -- \ | ---- | ---- | | `assets/gpx/SampleJY.gpx` | 山手線。外部ツールで記録した実走行ログ | | `assets/gpx/SampleTohokuShinkansen.gpx` | 東北新幹線 盛岡→仙台。最高 320km/h、一ノ関に停車、通過駅 5 駅を経由 | -| `assets/gpx/KeioSpecialExpress.gpx` | 京王線 特急 新宿→京王八王子。最高 110km/h | -| `assets/gpx/KatamachiRapid.gpx` | 片町線 快速 京田辺→木津。駅間 2.3km・最高 95km/h | -| `assets/gpx/SobuRapid.gpx` | 総武快速線 錦糸町→津田沼。駅間 3.4〜7.5km・最高 120km/h | +| `assets/gpx/KeioSpecialExpress.gpx` | 京王線 特急 新宿→京王八王子。到着時間推定に合わせて走る | +| `assets/gpx/KatamachiRapid.gpx` | 片町線 快速 京田辺→木津。駅間 2.3km 前後の全駅停車 | +| `assets/gpx/SobuRapid.gpx` | 総武快速線 錦糸町→津田沼。駅間 3.6〜7.6km (線路の長さ) の全駅停車 | | `assets/gpx/FLinerSeibu.gpx` | Fライナー相当 元町・中華街→飯能。地下鉄の電波環境付き | `SampleTohokuShinkansen.gpx` は盛岡以南の 320km/h 区間を再現するためのもの。 @@ -229,8 +253,9 @@ npm run gpx:generate -- \ 各駅の到着判定・通過判定をそのまま観測できる。 `KeioSpecialExpress.gpx` は在来線側のサンプルで、種別グループの停車パターン -(経路 32 駅 / うち停車 12 駅) をそのまま走る。同じコマンドを再実行すれば -バイト単位で同じ内容が得られる。 +(経路 32 駅 / うち停車 12 駅) をそのまま走る。StationAPI のデータが変わらなければ、 +同じコマンドを再実行するとバイト単位で同じ内容が得られる。京王線の特急は到着時間 +推定の較正テーブルに無いので、所要時間は一般則による推定値になる。 `FLinerSeibu.gpx` は地下鉄の電波環境を持つ唯一のサンプル。いわゆる F ライナーだが、 StationAPI に「F ライナー」という種別は無く、みなとみらい線・東急東横線が特急、 @@ -240,24 +265,21 @@ StationAPI に「F ライナー」という種別は無く、みなとみらい 地下 (みなとみらい線) → 地上 (東急東横線) → 地下 (副都心線 → 西武有楽町線) → 地上 (西武池袋線) と 5 路線 4 社を跨ぎ、地下 ↔ 地上が 3 回切り替わる (うち地下から地上への 復帰が 2 回)。副都心線から西武有楽町線は地下から地下なので切り替わりに数えない。 -欠測は 7 件・最長 195 秒で、ETA 棄却の保険 (`ETA_BOUND_MAX_HOLD_MS` = 90 秒) を超える -区間を 4 箇所含む。 +欠測は 7 件・最長 326 秒で、ETA 棄却の保険 (`ETA_BOUND_MAX_HOLD_MS` = 90 秒) を超える +区間を 5 箇所含む。 地下鉄分岐と、地上へ復帰したときの基準の張り直しを 1 本で繰り返し踏める -(ただし実機では上記のとおり副都心線の区間だけ。Jest 側は全区間)。84 分・4433 点と +(ただし実機では上記のとおり副都心線の区間だけ。Jest 側は全区間)。約 92 分・4488 点と 他のサンプルより大きいが、この経路は他に代えがない。 -`--max-speed` は路線ごとに変えられないので、全区間を副都心線の 80km/h で走らせて -いる。地上側は本来もっと速い (東横線特急 110km/h、西武池袋線快速急行 105km/h) が、 -このサンプルの被検体はトンネル内の欠測長であり、速度を上げるとそこが縮む。110km/h で -生成すると `ETA_BOUND_MAX_HOLD_MS` (90 秒) を超える欠測が 4 箇所から 1 箇所へ減る。 -地上区間の速度はこの検証に効かないため、地下側を実速度に合わせている。 +区間の時間は、路線ごとの到着時間推定に合わせている。トンネル内の欠測の長さは走行時間で +決まるので、地下区間の所要時間が推定どおりであることが、このサンプルの前提になる。 `KatamachiRapid.gpx` と `SobuRapid.gpx` は、EMA の追従遅れが到着判定へ効く条件を 狙ったサンプル。到着圏は駅間 800m 以上でどれも 200m にクランプされるので、 -遅れ (速度に比例) が到着圏を食い潰しやすい**高速・全駅停車**の区間を選んでいる。 -`SobuRapid.gpx` は 120km/h と最も条件が厳しく、東京から乗るだけで実機確認できる -区間でもある (地上区間は錦糸町以東)。 +遅れ (速度に比例) が到着圏を食い潰しやすい**全駅停車**の区間を選んでいる。 +到着圏に入るときの速度は減速度で決まり、減速度 0.9m/s² では約 68km/h になる。 +`SobuRapid.gpx` は東京から乗るだけで実機確認できる区間でもある (地上区間は錦糸町以東)。 ## iOS シミュレータで再生する @@ -334,8 +356,12 @@ T は旧実装の固定 α が 1 秒間隔で持っていた遅れ時間なの 10 秒間隔・σ=250m では許容遅れをどこに置いても両立しない。 この挙動は `src/store/atoms/location.gpxLag.test.ts` が GPX を実パイプラインへ -流して検証している (`assets/gpx/KatamachiRapid.gpx` は駅間 2.3km・95km/h と、到着圏が -最小クランプに張り付く最も不利な条件)。 +流して検証している (`assets/gpx/KatamachiRapid.gpx` は駅間 2.3km と、到着圏が +最小クランプに張り付く最も不利な条件)。10 秒間隔の測位は、駅に対して当たる位置が +1 通りではない。テストは最初の測位の遅れとノイズの種を変えた複数の条件で測る。 +ばたつきは、`useRefreshStation` と同じく停車時のグレース期間 (`ARRIVED_GRACE_PERIOD_MS`) +を含めて数える。到着直後に 1 回だけ圏外へ押し出された測位は、表示を変えないためである。 +精度 250m で α を正規化すると、グレース期間を含めてもほとんどの条件でばたつく。 ## Jest から GPX を流すテスト @@ -394,7 +420,7 @@ GPX の解析・停車駅の検出・真の位置の補間は `src/utils/test/gp 止めるだけなので、Play 開発者サービスの Fused が最後の測位を保持して配り続けるのでは ないか」という懸念を実機で確かめたが、**そうはならなかった**。 -`FLinerSeibu.gpx` の 195 秒の欠測を Galaxy SCG13 (Android 16) へ流し、`dumpsys location` +線路の長さを反映する前の `FLinerSeibu.gpx` の 195 秒の欠測を Galaxy SCG13 (Android 16) へ流し、`dumpsys location` を 3 秒間隔で採った結果、欠測のあいだ fused の `last location` は座標・`hAcc` だけでなく **測位自体の時刻 (`et=`) も 1ms も進まなかった** (26 サンプル)。Fused は測位を合成も 再配信もせず、新しい測位が 1 つも生まれない。つまり GPX の穴はそのまま測位の途絶になる。 diff --git a/scripts/generate-location-gpx.mjs b/scripts/generate-location-gpx.mjs index 82db44d6c0..e5d5fbe6e0 100644 --- a/scripts/generate-location-gpx.mjs +++ b/scripts/generate-location-gpx.mjs @@ -86,8 +86,12 @@ const usage = `使い方: node scripts/generate-location-gpx.mjs [options] (--line と排他。どちらか一方が必須) --from 始点の駅 ID (--line では必須。--line-group では既定で経路の先頭) --to 終点の駅 ID (--line では必須。--line-group では既定で経路の末尾) - --max-speed 最高速度 (既定: 320) - --dwell 各停車駅での停車時間 (既定: ${DEFAULT_DWELL_SEC}) + --max-speed 最高速度。--line-group で省略すると、trainRoute(model: Estimated) が + 返す到着・出発の見込み (estimateArrivalTimes と同じ到着時間推定) に + 合わせて区間ごとの速度を決める。指定すると見込みを使わず、この速度で + 走らせる (加減速は trainRoute の値)。--line では省略時 320 + --dwell 各停車駅での停車時間。--line-group で --max-speed を省略したときの + 既定は推定の停車時間、それ以外の既定は ${DEFAULT_DWELL_SEC} --skip 通過駅の ID をカンマ区切りで指定 (停車せず素通りする) --line-group では stopCondition による判定に追加される --start 先頭 waypoint の時刻。タイムゾーン(Z または ±HH:MM)必須 @@ -138,8 +142,8 @@ export const isValidIso8601WithTimezone = (value) => { export const parseArgs = (argv) => { const args = { - maxSpeed: 320, - dwell: DEFAULT_DWELL_SEC, + maxSpeed: undefined, + dwell: undefined, skip: [], signalProfile: DEFAULT_SIGNAL_PROFILE, subwayLines: [], @@ -243,6 +247,7 @@ const fetchLineStations = async (apiUrl, lineId) => { nameRoman latitude longitude + trackDistanceFromPrevious line { id nameShort @@ -270,6 +275,7 @@ const fetchLineGroupStations = async (apiUrl, lineGroupId) => { nameRoman latitude longitude + trackDistanceFromPrevious stopCondition line { id @@ -287,6 +293,36 @@ const fetchLineGroupStations = async (apiUrl, lineGroupId) => { return stations; }; +// 区間の値を到着時間推定のモデル (model: Estimated) で取る。見込みは、同じ区間の +// estimateArrivalTimes と同じ値になる。アプリのオートモード (useSimulationMode) は +// model を渡さない (Legacy) ので、そちらの走り方とは一致しない。 +const fetchTrainRoute = async (apiUrl, fromStationId, toStationId, lineGroupId) => { + const data = await queryStationApi( + apiUrl, + `query GenerateGpxTrainRoute($fromStationId: Int!, $toStationId: Int!, $lineGroupId: Int) { + trainRoute( + fromStationId: $fromStationId + toStationId: $toStationId + lineGroupId: $lineGroupId + model: Estimated + ) { + segments { + station { + id + } + maxSpeed + maxAcceleration + maxDeceleration + arrivalCumulativeMinutes + departureCumulativeMinutes + } + } + }`, + { fromStationId, toStationId, lineGroupId } + ); + return data.trainRoute?.segments ?? []; +}; + // lineGroupId は駅からしか辿れないため、--list-train-types の裏側で使う。 const fetchStationTrainTypes = async (apiUrl, stationId) => { const data = await queryStationApi( @@ -327,17 +363,30 @@ export const distanceBetween = (a, b) => { // 隣り合う駅の間は直線補間する。実際の線形とは異なるが、折れ点が実際の駅座標 // なので、各駅の到着判定(ARRIVED_MAX_THRESHOLD)は現実と同じ位置で成立する。 +// 区間の長さは cumulativeDistances が決めるため、線路の長さで伸ばした区間でも +// 進んだ割合で直線上の位置へ対応づける(座標は直線上、時間は線路の長さで進む)。 const interpolate = (from, to, ratio) => ({ latitude: from.latitude + (to.latitude - from.latitude) * ratio, longitude: from.longitude + (to.longitude - from.longitude) * ratio, }); +// 隣り合う駅 prev -> station の区間の長さ(m)。StationAPI が線路の長さ +// (trackDistanceFromPrevious)を返す区間はそれを使い、返さない区間 +// (線路データが無い、直通の境界で同じ駅が 2 回並ぶなど)は直線距離で代える。 +// 直線距離のままだとカーブ分だけ短くなり、所要時間が実際より短い GPX になる。 +export const segmentLength = (prev, station) => { + const track = station.trackDistanceFromPrevious; + return Number.isFinite(track) && track > 0 + ? track + : distanceBetween(prev, station); +}; + // 折れ線(全駅)の始点からの累積距離を求める export const cumulativeDistances = (polyline) => { const cumulative = [0]; for (let i = 1; i < polyline.length; i++) { cumulative.push( - cumulative[i - 1] + distanceBetween(polyline[i - 1], polyline[i]) + cumulative[i - 1] + segmentLength(polyline[i - 1], polyline[i]) ); } return cumulative; @@ -428,6 +477,113 @@ export const resolveIsHoliday = async (date) => { return (holidayJp.default ?? holidayJp).isHoliday(jstCalendarDate); }; +// trainRoute の segments を駅へ対応づける。useSimulationMode と同じく、駅 ID ではなく +// 配列の位置で対応づける。同じ駅 ID が離れた位置に現れる経路(接続駅の再登場)で、 +// ID をキーにすると別の位置の値を拾ってしまうため。並びが食い違っているときは、 +// 別の駅の値で走らせてしまうより止める。 +export const attachTrainRoute = (route, segments) => { + if (segments.length !== route.length) { + throw new Error( + `trainRoute の駅数(${segments.length})が経路の駅数(${route.length})と一致しません` + ); + } + return route.map((station, i) => { + const segment = segments[i]; + if (segment.station?.id !== station.id) { + throw new Error( + `trainRoute の ${i + 1} 駅目(${segment.station?.id})が経路の駅(${station.id} ${station.name})と一致しません` + ); + } + return { + ...station, + motion: { + maxSpeed: segment.maxSpeed, + accel: segment.maxAcceleration, + decel: segment.maxDeceleration, + }, + // バスの駅を含む経路では見込みが返らない (null) + timing: + Number.isFinite(segment.arrivalCumulativeMinutes) && + Number.isFinite(segment.departureCumulativeMinutes) + ? { + arrivalSec: segment.arrivalCumulativeMinutes * 60, + departureSec: segment.departureCumulativeMinutes * 60, + } + : null, + }; + }); +}; + +// 停車駅から次の停車駅までを加速度 accel・減速度 decel の台形(短い区間は三角形)で +// seconds 秒かけて走る巡航速度(m/s)。台形の所要時間は T(v) = D/v + k·v +// (k = 1/(2a) + 1/(2b)) なので、T(v) = seconds の 2 根のうち遅い方(巡航のある +// 台形)を返す。最短時間 2√(D·k) より短い時間を求められたときは解が無いので、 +// 最短時間で走る速度 √(D/k) を返し、shortened を立てる。 +export const cruiseSpeedForDuration = (distance, seconds, accel, decel) => { + const valid = (x) => Number.isFinite(x) && x > 0; + if (!(valid(distance) && valid(accel) && valid(decel))) { + return null; + } + const k = 0.5 / accel + 0.5 / decel; + const fastest = Math.sqrt(distance / k); + const discriminant = seconds * seconds - 4 * k * distance; + if (!Number.isFinite(seconds) || discriminant <= 0) { + return { speed: fastest, shortened: true }; + } + return { + speed: (seconds - Math.sqrt(discriminant)) / (2 * k), + shortened: false, + }; +}; + +// 停車駅ごとの走行時間と停車時間(秒)を、trainRoute の見込みから組み立てる。 +// legs[j] は stopIndices[j] から stopIndices[j + 1] までの走行 (runSec) と、 +// 着いた駅での停車 (dwellSec。終点は 0)。見込みの無い駅があれば null。 +// +// GPX の停車駅は、アプリと同じ規則 (isPassStopCondition と --skip) で決める。 +// 推定は平日/休日運転の駅を常に停車扱いにし、--skip も知らないので、GPX では +// 通過する駅に推定だけが停まることがある。その駅の停車時間 (出発 − 到着) は +// 走行時間から引く。逆に GPX で停まる駅を推定が通過していると、その駅の停車時間が +// 見込みに無いので、dwellOverrideSec が無ければ止める (黙って 0 秒で通らないため)。 +export const planLegTimings = (route, stopIndices, dwellOverrideSec) => { + if (route.some((station) => station.timing == null)) { + return null; + } + const dwellAt = (i) => route[i].timing.departureSec - route[i].timing.arrivalSec; + return stopIndices.slice(1).map((to, j) => { + const from = stopIndices[j]; + let runSec = route[to].timing.arrivalSec - route[from].timing.departureSec; + for (let i = from + 1; i < to; i++) { + runSec -= dwellAt(i); + } + const isFinal = j === stopIndices.length - 2; + let dwellSec = 0; + if (!isFinal) { + dwellSec = dwellOverrideSec ?? Math.round(dwellAt(to)); + if (dwellSec <= 0) { + throw new Error( + `${route[to].name} は推定では通過扱いで、停車時間の見込みがありません。--dwell で停車時間を指定してください` + ); + } + } + return { runSec, dwellSec }; + }); +}; + +// trackDistanceFromPrevious は API の並びで直前にある駅からの長さ。走らせる向きが +// API の並びと逆のときは、走る向きで直前になる駅(API の並びで直後の駅)が持つ値へ +// 付け替える。先頭の駅は区間の起点なので値を持たせない。 +export const alignTrackDistances = (ordered, reversed) => + ordered.map((station, i) => ({ + ...station, + trackDistanceFromPrevious: + i === 0 + ? null + : reversed + ? (ordered[i - 1].trackDistanceFromPrevious ?? null) + : (station.trackDistanceFromPrevious ?? null), + })); + // route 上で実際に停車する駅の index を返す。始点と終点は必ず停車する // (その駅から発車し、その駅で終着するため)。 export const resolveStopIndices = ({ route, skippedIds, isHoliday }) => @@ -469,14 +625,21 @@ export const isUndergroundStation = (station, subwayLineIds) => station?.line?.lineType === 'Subway' || (station?.line?.id != null && subwayLineIds.has(station.line.id)); +// 区間の走らせ方は次のどちらか。どちらも加減速は区間の到着駅の motion +// (trainRoute の値。無ければ generateTrainSpeedProfile の既定値) を使う。 +// - legTimings (planLegTimings の結果) があれば、区間ごとの走行時間に合う巡航速度で +// 走り、停車時間もそれに従う。区間ごとの端数は、計画した到着時刻に合わせて +// 次の区間で吸収するので、終点までに積み上がらない。 +// - 無ければ maxSpeedKmh (無ければ到着駅の motion.maxSpeed) で走り、dwellSec 停まる。 +// 走行中の点には速度 (speed, m/s) を持たせる。GPX には書き出さない。 export const buildWaypoints = ({ route, stopIndices, maxSpeedKmh, dwellSec, + legTimings = null, subwayLineIds = new Set(), }) => { - const maxSpeed = maxSpeedKmh / 3.6; // m/s // stopped は「駅に止まっている点」。電波プロファイル(applySignalProfile)が // 駅からの距離を時間で測るために使う。始発駅は停車時間を持たないが、 // 発車前の 1 点なので駅にいる扱いにする。 @@ -489,16 +652,51 @@ export const buildWaypoints = ({ }, ]; let elapsed = 0; + // legTimings に従ったときの、計画上の到着時刻 (始点からの秒) + let plannedDepartureSec = 0; + const shortenedLegs = []; for (let leg = 0; leg < stopIndices.length - 1; leg++) { const polyline = route.slice(stopIndices[leg], stopIndices[leg + 1] + 1); const cumulative = cumulativeDistances(polyline); const distance = cumulative.at(-1); - // 出力を再現可能にするため惰行のランダム性は無効にする + const arrivalStation = route[stopIndices[leg + 1]]; + const motion = arrivalStation.motion; + const timing = legTimings?.[leg]; + const legDwellSec = timing ? timing.dwellSec : dwellSec; + let maxSpeed; + if (timing) { + const plannedArrivalSec = plannedDepartureSec + timing.runSec; + plannedDepartureSec = plannedArrivalSec + timing.dwellSec; + // 到着点は速度プロファイルの最後の点の 1 秒後に置くので、その 1 秒を除いた + // 時間でプロファイルを作る + const fitted = cruiseSpeedForDuration( + distance, + plannedArrivalSec - elapsed - INTERVAL_SEC, + motion.accel, + motion.decel + ); + maxSpeed = fitted?.speed; + if (fitted?.shortened) { + shortenedLegs.push(arrivalStation.name); + } + } else { + maxSpeed = maxSpeedKmh !== undefined ? maxSpeedKmh / 3.6 : motion?.maxSpeed; // m/s + } + if (distance >= 1 && (!Number.isFinite(maxSpeed) || maxSpeed <= 0)) { + throw new Error( + `${arrivalStation.name} 行きの区間の最高速度が決まりません。--max-speed を指定するか、--line-group で trainRoute の値を使ってください` + ); + } + + // 出力を再現可能にするため惰行のランダム性は無効にする。 + // 加減速が undefined のときは generateTrainSpeedProfile の既定値になる const speedProfile = generateTrainSpeedProfile({ distance, maxSpeed, + accel: motion?.accel, + decel: motion?.decel, interval: INTERVAL_SEC, enableRandomCoast: false, }); @@ -511,7 +709,7 @@ export const buildWaypoints = ({ // 停車扱いのまま次へ進める。この分岐を通る境界駅は、旧実装より 1 点(1 秒)短くなる。 if (distance < 1) { const here = polyline.at(-1); - const dwellPoints = leg === stopIndices.length - 2 ? 0 : dwellSec; + const dwellPoints = leg === stopIndices.length - 2 ? 0 : legDwellSec; for (let t = -INTERVAL_SEC; t < dwellPoints; t += INTERVAL_SEC) { elapsed += INTERVAL_SEC; waypoints.push({ @@ -524,13 +722,23 @@ export const buildWaypoints = ({ continue; } + // 見込みに合わせて走るときは、速度列の合計距離を区間の距離にそろえる + // (useSimulationMode と同じ補正)。1 秒ごとの速度を足すと区間の距離より長く + // 進むため、そのままだとプロファイルが終わる前に駅へ着き、残りの点が駅の上で + // 止まって、見込みより早く着いたことになる。 + const profileDistance = speedProfile.reduce((sum, v) => sum + v, 0); + const scale = + timing && profileDistance > 0 ? distance / profileDistance : 1; + let travelled = 0; - for (const speed of speedProfile) { + for (const rawSpeed of speedProfile) { + const speed = rawSpeed * scale; travelled = Math.min(distance, travelled + speed * INTERVAL_SEC); elapsed += INTERVAL_SEC; waypoints.push({ ...pointAtDistance(polyline, cumulative, travelled), elapsed, + speed, stopped: false, underground: isUndergroundStation( stationAtDistance(polyline, cumulative, travelled), @@ -552,7 +760,7 @@ export const buildWaypoints = ({ // 終点以外は停車する。停車中も測位は届き続けるので同じ座標を並べる const isFinalStop = leg === stopIndices.length - 2; if (!isFinalStop) { - for (let t = 0; t < dwellSec; t += INTERVAL_SEC) { + for (let t = 0; t < legDwellSec; t += INTERVAL_SEC) { elapsed += INTERVAL_SEC; waypoints.push({ ...arrival, @@ -564,6 +772,11 @@ export const buildWaypoints = ({ } } + if (shortenedLegs.length > 0) { + process.stderr.write( + `警告: ${shortenedLegs.join('、')} 行きの区間は、見込みの時間では走り切れないため最短時間で走らせました\n` + ); + } return waypoints; }; @@ -765,12 +978,18 @@ const main = async () => { // 数値オプションは Number() の結果をそのまま使うため、ここで弾かないと // NaN や 0 が速度プロファイル・停車ループへ流れ込み、列車が一切進まない // GPX が終了コード 0 で出力されてしまう(壊れていることに気付けない) - if (!Number.isFinite(args.maxSpeed) || args.maxSpeed <= 0) { + if ( + args.maxSpeed !== undefined && + (!Number.isFinite(args.maxSpeed) || args.maxSpeed <= 0) + ) { throw new Error( `--max-speed には正の数値を指定してください: ${args.maxSpeed}` ); } - if (!Number.isFinite(args.dwell) || args.dwell < 0) { + if ( + args.dwell !== undefined && + (!Number.isFinite(args.dwell) || args.dwell < 0) + ) { throw new Error( `--dwell には 0 以上の数値を指定してください: ${args.dwell}` ); @@ -802,10 +1021,13 @@ const main = async () => { } // API の並び順に関わらず、指定された向きで走らせる - const ordered = - fromIndex < toIndex - ? allStations.slice(fromIndex, toIndex + 1) - : allStations.slice(toIndex, fromIndex + 1).reverse(); + const reversed = fromIndex > toIndex; + const ordered = alignTrackDistances( + reversed + ? allStations.slice(toIndex, fromIndex + 1).reverse() + : allStations.slice(fromIndex, toIndex + 1), + reversed + ); const missingCoords = ordered.filter( (s) => s.latitude == null || s.longitude == null @@ -862,11 +1084,37 @@ const main = async () => { ); } + // --line-group では trainRoute(model: Estimated) の見込みで走らせる。 + // trainRoute は lineGroupId を要るので、--line では従来どおり --max-speed(既定 320)。 + const route = useLineGroup + ? attachTrainRoute( + ordered, + await fetchTrainRoute( + apiUrl, + ordered[0].id, + ordered.at(-1).id, + args.lineGroup + ) + ) + : ordered; + const maxSpeedKmh = args.maxSpeed ?? (useLineGroup ? undefined : 320); + // --max-speed を指定したときは見込みを使わない (速度と時間の両方は守れないため) + const legTimings = + useLineGroup && args.maxSpeed === undefined + ? planLegTimings(route, stopIndices, args.dwell) + : null; + if (useLineGroup && args.maxSpeed === undefined && legTimings == null) { + process.stderr.write( + '警告: trainRoute が到着・出発の見込みを返さなかったため (バスの駅を含む経路)、区間ごとの最高速度で走らせます\n' + ); + } + const dense = buildWaypoints({ - route: ordered, + route, stopIndices, - maxSpeedKmh: args.maxSpeed, - dwellSec: args.dwell, + maxSpeedKmh, + dwellSec: args.dwell ?? DEFAULT_DWELL_SEC, + legTimings, subwayLineIds, }); const waypoints = applySignalProfile(dense, args.signalProfile); @@ -874,10 +1122,20 @@ const main = async () => { if (args.out) { writeFileSync(args.out, gpx, 'utf8'); + // 区間ごとに速度を決めたときは、実際に走った最高速度を出す + const topSpeedKmh = Math.round( + Math.max(...dense.map((wp) => wp.speed ?? 0)) * 3.6 + ); const minutes = (waypoints.at(-1).elapsed / 60).toFixed(1); process.stderr.write( - `${args.out} を出力しました (経路 ${ordered.length} 駅 / うち停車 ${stopIndices.length} 駅 / ${waypoints.length} 点 / 約 ${minutes} 分 / 最高 ${args.maxSpeed}km/h)\n` + `${args.out} を出力しました (経路 ${ordered.length} 駅 / うち停車 ${stopIndices.length} 駅 / ${waypoints.length} 点 / 約 ${minutes} 分 / 最高 ${topSpeedKmh}km/h)\n` ); + if (legTimings) { + const estimatedMinutes = (route.at(-1).timing.arrivalSec / 60).toFixed(1); + process.stderr.write( + `到着時間推定の終点到着: ${estimatedMinutes} 分 (GPX で通過にした駅の停車時間は除く)\n` + ); + } if (args.signalProfile === 'subway') { // どの路線を地下として扱ったかは生成物から読み取れないので、ここで残す。 const byLine = new Map(); diff --git a/scripts/generate-location-gpx.test.mjs b/scripts/generate-location-gpx.test.mjs index 71fe5f7ad6..1d894e3975 100644 --- a/scripts/generate-location-gpx.test.mjs +++ b/scripts/generate-location-gpx.test.mjs @@ -11,11 +11,17 @@ import { execFileSync } from 'node:child_process'; import { test } from 'node:test'; import { + alignTrackDistances, applySignalProfile, + attachTrainRoute, buildDeepLinkQuery, + buildWaypoints, + cruiseSpeedForDuration, cumulativeDistances, isPassStopCondition, isUndergroundStation, + planLegTimings, + pointAtDistance, resolveIsHoliday, resolveStopIndices, stationAtDistance, @@ -431,3 +437,227 @@ test('坑口窓は境界駅が地上側でもその駅を起点に測る', () => ); assert.ok(result[3].accuracy > 200, '発車直後は坑口帯'); }); + +test('区間の長さは線路の長さがあればそれを使い、無い区間は直線距離で代える', () => { + const a = { latitude: 35.0, longitude: 139.0 }; + const b = { latitude: 35.01, longitude: 139.0 }; + const straight = cumulativeDistances([a, b]).at(-1); + + // 線路の長さは直線距離より長い + assert.equal( + cumulativeDistances([a, { ...b, trackDistanceFromPrevious: 1500 }]).at(-1), + 1500 + ); + // null・0・未定義は直線距離。直通の境界で同じ駅が 2 回並ぶ組は 0 が返る + for (const track of [null, 0, undefined]) { + assert.equal( + cumulativeDistances([a, { ...b, trackDistanceFromPrevious: track }]).at( + -1 + ), + straight + ); + } +}); + +test('線路の長さで伸ばした区間でも、進んだ割合で直線上の位置へ対応づく', () => { + const a = { latitude: 35.0, longitude: 139.0 }; + const b = { + latitude: 35.01, + longitude: 139.0, + trackDistanceFromPrevious: 2000, + }; + const polyline = [a, b]; + const cumulative = cumulativeDistances(polyline); + const mid = pointAtDistance(polyline, cumulative, 1000); + assert.ok(Math.abs(mid.latitude - 35.005) < 1e-9); + // 終端は駅の座標に届く + assert.deepEqual(pointAtDistance(polyline, cumulative, 2000), { + latitude: b.latitude, + longitude: b.longitude, + trackDistanceFromPrevious: 2000, + }); +}); + +test('走る向きが API の並びと逆でも、線路の長さは同じ駅の組に付く', () => { + // API の並び A -> B -> C。B は A から 100m、C は B から 200m + const api = [ + { id: 1, trackDistanceFromPrevious: null }, + { id: 2, trackDistanceFromPrevious: 100 }, + { id: 3, trackDistanceFromPrevious: 200 }, + ]; + const forward = alignTrackDistances(api, false); + assert.deepEqual( + forward.map((s) => s.trackDistanceFromPrevious), + [null, 100, 200] + ); + + // C -> B -> A で走る。B は C から 200m、A は B から 100m + const reversed = alignTrackDistances([...api].reverse(), true); + assert.deepEqual( + reversed.map((s) => [s.id, s.trackDistanceFromPrevious]), + [ + [3, null], + [2, 200], + [1, 100], + ] + ); +}); + +test('区間の起点にした駅は、API の並びで前にあった駅からの長さを持ち越さない', () => { + // 3 駅目から走り始める。3 駅目の値は 2 駅目からの長さで、走行には使わない + const sliced = alignTrackDistances( + [ + { id: 3, trackDistanceFromPrevious: 200 }, + { id: 4, trackDistanceFromPrevious: 300 }, + ], + false + ); + assert.equal(sliced[0].trackDistanceFromPrevious, null); + assert.equal(sliced[1].trackDistanceFromPrevious, 300); +}); + +// 台形(短い区間は三角形)の所要時間(秒) +const trapezoidSeconds = (distance, v, a, b) => { + const k = 0.5 / a + 0.5 / b; + return distance >= v * v * k + ? distance / v + v * k + : 2 * Math.sqrt(distance * k); +}; + +test('巡航速度は、台形で走ったときに求めた時間になる速度', () => { + for (const [distance, seconds] of [ + [4843, 300], + [1627, 150], + [300, 60], + ]) { + const { speed, shortened } = cruiseSpeedForDuration( + distance, + seconds, + 0.7, + 0.9 + ); + assert.equal(shortened, false); + assert.ok( + Math.abs(trapezoidSeconds(distance, speed, 0.7, 0.9) - seconds) < 1e-6 + ); + } +}); + +test('最短時間より短い時間を求められたら、最短時間で走る速度にする', () => { + const distance = 2000; + const k = 0.5 / 0.7 + 0.5 / 0.9; + const { speed, shortened } = cruiseSpeedForDuration(distance, 10, 0.7, 0.9); + assert.equal(shortened, true); + assert.ok(Math.abs(speed - Math.sqrt(distance / k)) < 1e-9); + // 時間が負や非有限でも最短時間で走る + assert.equal(cruiseSpeedForDuration(distance, -1, 0.7, 0.9).shortened, true); + // 距離と加減速が正でなければ決められない + assert.equal(cruiseSpeedForDuration(0, 60, 0.7, 0.9), null); + assert.equal(cruiseSpeedForDuration(1000, 60, 0, 0.9), null); +}); + +const timed = (id, arrivalMin, departureMin) => ({ + id, + name: `駅${id}`, + timing: { arrivalSec: arrivalMin * 60, departureSec: departureMin * 60 }, +}); + +test('区間の時間は、次の停車駅の到着とこの駅の出発の差', () => { + const route = [timed(1, 0, 0), timed(2, 2, 2.6), timed(3, 5, 5)]; + assert.deepEqual(planLegTimings(route, [0, 1, 2]), [ + { runSec: 120, dwellSec: 36 }, + { runSec: 144, dwellSec: 0 }, + ]); +}); + +test('GPX で通過にした駅に推定だけが停まるときは、その停車時間を走行から引く', () => { + // 2 駅目は推定では 0.6 分停まるが、GPX では通過 (平日運転の駅を休日に走るなど) + const route = [timed(1, 0, 0), timed(2, 2, 2.6), timed(3, 5, 5)]; + assert.deepEqual(planLegTimings(route, [0, 2]), [ + { runSec: 264, dwellSec: 0 }, + ]); +}); + +test('GPX で停まる駅を推定が通過していたら、--dwell が無ければ止める', () => { + const route = [timed(1, 0, 0), timed(2, 2, 2), timed(3, 5, 5)]; + assert.throws(() => planLegTimings(route, [0, 1, 2]), /--dwell/); + assert.deepEqual(planLegTimings(route, [0, 1, 2], 30), [ + { runSec: 120, dwellSec: 30 }, + { runSec: 180, dwellSec: 0 }, + ]); +}); + +test('見込みの無い駅があれば (バスの経路)、見込みでは走らせない', () => { + const route = [timed(1, 0, 0), { id: 2, name: '駅2', timing: null }]; + assert.equal(planLegTimings(route, [0, 1]), null); +}); + +test('trainRoute の見込みを駅へ付け、並びが違えば止める', () => { + const route = [{ id: 1 }, { id: 2 }]; + const segment = (id, arrival, departure) => ({ + station: { id }, + maxSpeed: 25, + maxAcceleration: 0.7, + maxDeceleration: 0.9, + arrivalCumulativeMinutes: arrival, + departureCumulativeMinutes: departure, + }); + const attached = attachTrainRoute(route, [ + segment(1, 0, 0), + segment(2, 1.5, 1.5), + ]); + assert.deepEqual(attached[1].timing, { arrivalSec: 90, departureSec: 90 }); + assert.deepEqual(attached[1].motion, { + maxSpeed: 25, + accel: 0.7, + decel: 0.9, + }); + // Legacy 相当 (見込みが null) なら timing も null + assert.equal( + attachTrainRoute(route, [segment(1, null, null), segment(2, null, null)])[1] + .timing, + null + ); + assert.throws( + () => attachTrainRoute(route, [segment(2, 0, 0), segment(1, 1, 1)]), + /一致しません/ + ); + assert.throws( + () => attachTrainRoute(route, [segment(1, 0, 0)]), + /一致しません/ + ); +}); + +test('見込みで走らせると、停車駅に見込みの時刻どおりに着き、見込みの時間だけ停まる', () => { + const station = (id, latitude, arrivalMin, departureMin) => ({ + id, + name: `駅${id}`, + latitude, + longitude: 139.0, + motion: { maxSpeed: 30, accel: 0.7, decel: 0.9 }, + timing: { arrivalSec: arrivalMin * 60, departureSec: departureMin * 60 }, + }); + // 3.3km と 2.2km の区間 + const route = [ + station(1, 35.0, 0, 0), + station(2, 35.03, 4, 4.6), + station(3, 35.05, 8, 8), + ]; + const stopIndices = [0, 1, 2]; + const waypoints = buildWaypoints({ + route, + stopIndices, + dwellSec: 60, + legTimings: planLegTimings(route, stopIndices), + }); + const firstAt = (s) => + waypoints.find((wp) => wp.latitude === s.latitude && wp.elapsed > 0) + .elapsed; + // 1 秒刻みなので、着く時刻は見込みから 1 秒以内 + assert.ok(Math.abs(firstAt(route[1]) - 240) <= 1, `${firstAt(route[1])}`); + assert.ok(Math.abs(firstAt(route[2]) - 480) <= 1, `${firstAt(route[2])}`); + assert.equal(waypoints.at(-1).elapsed, 480); + // 2 駅目では 36 秒停まる (到着点の後に 36 点) + const atSecond = waypoints.filter((wp) => wp.latitude === route[1].latitude); + assert.ok(atSecond.length >= 37, `${atSecond.length}`); +}); diff --git a/src/store/atoms/location.gpxEtaAssist.test.ts b/src/store/atoms/location.gpxEtaAssist.test.ts index 0e7dd357eb..4e48c49b25 100644 --- a/src/store/atoms/location.gpxEtaAssist.test.ts +++ b/src/store/atoms/location.gpxEtaAssist.test.ts @@ -309,9 +309,15 @@ describe('eta_assist_enabled=true でGPXを実パイプラインへ流したと // 遅れが解けないままトラックが終わるため検知できないことがある。 const undetectableTerminal = endsAtStop && cond.accuracy > BAD_ACCURACY_THRESHOLD ? 1 : 0; - expect(detectedCount(on.firstDetection)).toBe( + // 終着駅は検知できないことがあるだけで、できてはいけないわけではない。 + // 検知できるかは GPX の軌跡で変わる(到着時間推定に合わせて走らせた片町線・ + // 京王線の GPX では、精度劣化帯でも終着駅を検知する)ため、下限と上限で見る。 + expect(detectedCount(on.firstDetection)).toBeGreaterThanOrEqual( stops.length - undetectableTerminal ); + expect(detectedCount(on.firstDetection)).toBeLessThanOrEqual( + stops.length + ); // ETAは到着判定へ介入しない(棄却が起きなければ入力も到着圏も同一)ため、 // 検知位置は一致する。R1を持っていた頃の「早まる側にだけずれる」許容ではなく、 // 完全一致を要求してETAが到着判定へ戻ってきたことを検出できるようにする。 diff --git a/src/store/atoms/location.subwayGpx.test.ts b/src/store/atoms/location.subwayGpx.test.ts index b65555517e..26278f5cbc 100644 --- a/src/store/atoms/location.subwayGpx.test.ts +++ b/src/store/atoms/location.subwayGpx.test.ts @@ -249,11 +249,11 @@ describe.each(GPX_FILES)('%s の電波環境', (file) => { describe('FLinerSeibu.gpx の形', () => { const track = parseGpx(join(GPX_DIR, 'FLinerSeibu.gpx')); - it('欠測は7件、うち4件がETA棄却の保険を超える', () => { + it('欠測は7件、うち5件がETA棄却の保険を超える', () => { const gaps = gapsOf(track); expect(gaps.length).toBe(7); - expect(gaps.filter((g) => g > ETA_BOUND_MAX_HOLD_MS).length).toBe(4); - expect(Math.max(...gaps)).toBe(195_000); + expect(gaps.filter((g) => g > ETA_BOUND_MAX_HOLD_MS).length).toBe(5); + expect(Math.max(...gaps)).toBe(326_000); }); it('地下から地上への復帰を2回含む', () => { From 94e721bde6f08da93a548dd8b222198582a914aa Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 10:28:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=88=E3=83=A2?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E5=88=B0=E7=9D=80=E6=99=82=E9=96=93?= =?UTF-8?q?=E6=8E=A8=E5=AE=9A=E3=81=AE=E8=A6=8B=E8=BE=BC=E3=81=BF=E3=81=A7?= =?UTF-8?q?=E8=B5=B0=E3=82=89=E3=81=9B=E3=80=81ETA=E3=81=A8GPX=E3=81=A8?= =?UTF-8?q?=E5=90=8C=E3=81=98=E6=99=82=E9=96=93=E3=81=A7=E9=A7=85=E3=81=AB?= =?UTF-8?q?=E7=9D=80=E3=81=84=E3=81=A6=E5=81=9C=E3=81=BE=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F=20(#7158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/gpx/FLinerSeibu.gpx | 7882 ++++++++++---------- assets/gpx/KatamachiRapid.gpx | 1660 ++--- assets/gpx/KeioSpecialExpress.gpx | 4211 ++++++----- assets/gpx/SobuRapid.gpx | 2101 +++--- docs/location-simulation.md | 11 +- scripts/generate-location-gpx.mjs | 127 +- src/hooks/useSimulationMode.test.tsx | 266 +- src/hooks/useSimulationMode.ts | 259 +- src/lib/graphql/queries.ts | 61 + src/store/atoms/location.subwayGpx.test.ts | 2 +- src/utils/currentLineGroupStations.test.ts | 23 + src/utils/currentLineGroupStations.ts | 9 +- src/utils/trainRouteTiming.test.ts | 141 + src/utils/trainRouteTiming.ts | 186 + 14 files changed, 8869 insertions(+), 8070 deletions(-) create mode 100644 src/utils/trainRouteTiming.test.ts create mode 100644 src/utils/trainRouteTiming.ts diff --git a/assets/gpx/FLinerSeibu.gpx b/assets/gpx/FLinerSeibu.gpx index 3c1552f668..c677d00a68 100644 --- a/assets/gpx/FLinerSeibu.gpx +++ b/assets/gpx/FLinerSeibu.gpx @@ -4,467 +4,467 @@ 48 - + 429 - + 589 - + 333 - + 325 - + 528 - + 611 - + 286 - + 546 - + 490 - + 542 - + 389 - + 375 - + 262 - + 466 - + 294 - + 443 - + 510 - + 360 - + 369 - + 487 - - + + 564 - - + + 521 - - + + 569 - - + + 582 - - + + 518 - - + + 342 - - + + 370 - - + + 509 - - + + 417 - - + + 513 - - + + 494 - - + + 557 - - + + 375 - - + + 467 - - + + 386 - - + + 337 - - + + 486 - - + + 308 - - + + 290 - + 527 - + 58 - + 27 - + 60 - + 33 - + 43 - + 37 - + 42 - + 51 - + 32 - + 50 - + 53 - + 47 - + 26 - + 41 - + 55 - + 27 - + 34 - + 31 - + 35 - + 58 - + 48 - + 43 - + 39 - + 47 - + 38 - + 51 - + 45 - + 42 - + 35 - + 60 - + 45 - + 38 - + 37 - + 41 - + 38 - + 55 - + 52 - - + + 493 - - + + 526 - - + + 382 - - + + 316 - - + + 513 - - + + 513 - - + + 481 - - + + 291 - - + + 531 - - + + 405 - - + + 431 - - + + 495 - - + + 545 - - + + 329 - - + + 411 - - + + 292 - - + + 397 - - + + 598 - - + + 469 - - + + 335 - + 399 - + 543 - + 397 - + 471 - + 405 - + 598 - + 473 - + 456 - + 489 - + 318 - + 534 - + 402 - + 384 - + 298 - + 333 - + 463 - + 327 - + 486 - + 260 @@ -768,1287 +768,1287 @@ 16 - + 12 - + 14 - + 14 - + 16 - + 11 - + 10 - + 19 - + 13 - + 11 - + 18 - + 12 - + 18 - + 11 - + 14 - + 9 - + 20 - + 15 - + 11 - + 17 - + 18 - + 13 - + 10 - + 17 - + 14 - + 18 - + 14 - + 18 - + 12 - + 13 - + 17 - + 11 - + 19 - + 15 - + 17 - + 16 - + 15 - + 8 - + 13 - + 20 - + 10 - + 14 - + 18 - + 20 - + 19 - + 18 - + 17 - + 17 - + 9 - + 13 - + 14 - + 15 - + 14 - + 11 - + 9 - + 13 - + 18 - + 18 - + 18 - + 20 - + 20 - + 18 - + 15 - + 17 - + 19 - + 13 - + 18 - + 18 - + 12 - + 20 - + 14 - + 13 - + 19 - + 11 - + 18 - + 18 - + 17 - + 17 - + 14 - + 19 - + 10 - + 8 - + 10 - + 13 - + 16 - + 10 - + 9 - + 19 - + 10 - + 19 - + 17 - + 11 - + 11 - + 19 - + 19 - + 14 - + 12 - + 12 - + 19 - + 18 - + 11 - + 20 - + 14 - + 10 - + 18 - + 13 - + 9 - + 11 - + 13 - + 9 - + 14 - + 17 - + 12 - + 12 - + 17 - + 19 - + 8 - + 10 - + 16 - + 17 - + 15 - + 17 - + 9 - + 11 - + 20 - + 20 - + 10 - + 12 - + 10 - + 11 - + 12 - + 16 - + 12 - + 13 - + 18 - + 9 - + 13 - + 17 - + 16 - + 16 - + 9 - + 9 - + 13 - + 14 - + 11 - + 16 - + 20 - + 9 - + 14 - + 17 - + 17 - + 13 - + 10 - + 14 - + 15 - + 15 - + 16 - + 14 - + 18 - + 13 - + 11 - + 10 - + 20 - + 19 - + 15 - + 13 - + 15 - + 16 - + 10 - + 16 - + 13 - + 9 - + 18 - + 19 - + 19 - + 15 - + 12 - + 13 - + 10 - + 14 - + 18 - + 18 - + 20 - + 20 - + 13 - + 15 - + 16 - + 16 - + 19 - + 20 - + 13 - + 9 - + 9 - + 15 - + 16 - + 16 - + 9 - + 13 - + 11 - + 19 - + 17 - + 16 - + 19 - + 20 - + 16 - + 18 - + 17 - + 16 - + 16 - + 15 - + 14 - + 12 - + 15 - + 10 - + 12 - + 9 - + 17 - + 18 - + 17 - + 19 - + 20 - + 20 - + 11 - + 16 - + 17 - + 12 - + 18 - + 13 - + 14 - + 17 - + 19 - + 19 - + 19 - + 18 - + 17 - + 12 - + 12 - + 10 - + 17 - + 16 - + 17 - + 12 - + 16 - + 18 - + 12 - + 19 - + 13 - + 12 - + 17 - + 11 - + 17 - + 17 - + 12 - + 10 - + 19 - + 9 - + 18 - + 15 - + 18 - + 9 - + 15 - + 20 - + 10 - + 9 - + 16 - + 11 - + 18 - + 12 - + 16 - + 13 - + 14 - + 14 - + 8 - + 10 - + 16 - + 9 - + 13 - + 18 - + 11 - + 19 - + 11 - + 14 - + 13 - + 15 - + 20 - + 12 - + 8 - + 10 - + 19 - + 16 - + 14 - + 8 - + 19 - + 14 - + 8 - + 17 - + 9 - + 17 - + 10 - + 9 - + 9 - + 10 - + 20 - + 16 - + 12 - + 8 - + 15 - + 12 - + 9 - + 12 - + 14 - + 15 - + 10 - + 20 - + 12 - + 15 - + 10 - + 10 - + 8 - + 8 - + 19 - + 13 @@ -2204,1843 +2204,1843 @@ 12 - + 9 - + 13 - + 9 - + 18 - + 18 - + 14 - + 19 - + 15 - + 10 - + 17 - + 10 - + 18 - + 19 - + 10 - + 8 - + 15 - + 14 - + 9 - + 19 - + 13 - + 14 - + 17 - + 18 - + 16 - + 17 - + 16 - + 15 - + 15 - + 15 - + 16 - + 20 - + 12 - + 13 - + 13 - + 12 - + 16 - + 17 - + 14 - + 9 - + 20 - + 17 - + 8 - + 15 - + 8 - + 8 - + 10 - + 9 - + 9 - + 13 - + 17 - + 14 - + 13 - + 11 - + 14 - + 19 - + 16 - + 19 - + 12 - + 11 - + 18 - + 15 - + 10 - + 15 - + 19 - + 11 - + 10 - + 10 - + 9 - + 10 - + 18 - + 10 - + 16 - + 15 - + 8 - + 10 - + 12 - + 18 - + 20 - + 9 - + 12 - + 17 - + 9 - + 10 - + 14 - + 16 - + 20 - + 10 - + 19 - + 12 - + 9 - + 13 - + 10 - + 9 - + 10 - + 19 - + 8 - + 11 - + 15 - + 12 - + 18 - + 13 - + 13 - + 19 - + 20 - + 16 - + 11 - + 15 - + 12 - + 8 - + 17 - + 9 - + 17 - + 10 - + 17 - + 12 - + 15 - + 10 - + 14 - + 10 - + 16 - + 12 - + 12 - + 16 - + 17 - + 9 - + 14 - + 11 - + 9 - + 13 - + 15 - + 11 - + 18 - + 11 - + 11 - + 12 - + 12 - + 8 - + 14 - + 13 - + 10 - + 11 - + 12 - + 18 - + 12 - + 12 - + 9 - + 10 - + 16 - + 11 - + 11 - + 15 - + 11 - + 17 - + 17 - + 13 - + 9 - + 13 - + 15 - + 11 - + 11 - + 18 - + 16 - + 12 - + 16 - + 16 - + 13 - + 10 - + 14 - + 18 - + 20 - + 19 - + 12 - + 20 - + 11 - + 15 - + 20 - + 11 - + 13 - + 13 - + 17 - + 10 - + 16 - + 9 - + 9 - + 9 - + 14 - + 18 - + 10 - + 16 - + 12 - + 9 - + 15 - + 16 - + 13 - + 14 - + 10 - + 11 - + 9 - + 14 - + 12 - + 12 - + 10 - + 10 - + 18 - + 16 - + 12 - + 20 - + 10 - + 18 - + 8 - + 19 - + 18 - + 10 - + 10 - + 12 - + 8 - + 13 - + 19 - + 18 - + 15 - + 15 - + 11 - + 15 - + 14 - + 12 - + 10 - + 15 - + 18 - + 19 - + 13 - + 9 - + 19 - + 10 - + 19 - + 18 - + 10 - + 15 - + 10 - + 11 - + 9 - + 12 - + 17 - + 20 - + 17 - + 10 - + 11 - + 8 - + 20 - + 14 - + 18 - + 9 - + 10 - + 15 - + 13 - + 18 - + 14 - + 11 - + 19 - + 10 - + 13 - + 18 - + 16 - + 19 - + 16 - + 16 - + 12 - + 17 - + 11 - + 20 - + 19 - + 11 - + 16 - + 9 - + 12 - + 19 - + 11 - + 13 - + 14 - + 17 - + 9 - + 18 - + 10 - + 10 - + 12 - + 8 - + 15 - + 17 - + 14 - + 10 - + 16 - + 15 - + 13 - + 17 - + 13 - + 19 - + 19 - + 18 - + 18 - + 19 - + 12 - + 18 - + 12 - + 9 - + 14 - + 14 - + 15 - + 15 - + 10 - + 14 - + 15 - + 11 - + 17 - + 13 - + 13 - + 8 - + 19 - + 11 - + 19 - + 14 - + 10 - + 16 - + 11 - + 8 - + 12 - + 17 - + 20 - + 9 - + 13 - + 11 - + 12 - + 17 - + 12 - + 10 - + 16 - + 16 - + 8 - + 10 - + 17 - + 18 - + 12 - + 14 - + 12 - + 12 - + 14 - + 20 - + 11 - + 20 - + 11 - + 13 - + 12 - + 19 - + 20 - + 19 - + 10 - + 10 - + 16 - + 11 - + 19 - + 13 - + 10 - + 10 - + 19 - + 19 - + 13 - + 19 - + 18 - + 11 - + 9 - + 18 - + 10 - + 10 - + 11 - + 16 - + 14 - + 16 - + 9 - + 18 - + 17 - + 14 - + 19 - + 14 - + 10 - + 12 - + 9 - + 17 - + 20 - + 18 - + 18 - + 8 - + 19 - + 19 - + 10 - + 15 - + 20 - + 11 - + 13 - + 16 - + 11 - + 15 - + 14 - + 13 - + 20 - + 16 - + 17 - + 18 - + 8 - + 20 - + 14 - + 19 - + 11 - + 13 - + 12 - + 15 - + 14 - + 14 - + 12 - + 14 - + 19 - + 12 - + 9 - + 13 - + 15 - + 14 - + 18 - + 8 - + 12 - + 19 - + 13 - + 10 - + 19 - + 9 - + 17 - + 19 - + 9 - + 15 - + 11 - + 11 - + 19 - + 9 - + 18 - + 19 - + 13 - + 16 - + 19 - + 13 - + 18 - + 17 - + 10 - + 13 - + 20 - + 18 - + 19 - + 20 - + 17 - + 11 - + 16 - + 15 - + 10 - + 12 - + 9 @@ -4192,955 +4192,955 @@ 12 - + 9 - + 12 - + 18 - + 12 - + 20 - + 17 - + 11 - + 10 - + 13 - + 17 - + 19 - + 9 - + 16 - + 14 - + 13 - + 9 - + 12 - + 11 - + 8 - + 9 - + 8 - + 13 - + 17 - + 12 - + 11 - + 12 - + 8 - + 18 - + 17 - + 12 - + 19 - + 8 - + 20 - + 9 - + 17 - + 12 - + 15 - + 19 - + 15 - + 19 - + 10 - + 8 - + 17 - + 10 - + 11 - + 11 - + 12 - + 9 - + 19 - + 13 - + 17 - + 18 - + 9 - + 20 - + 9 - + 19 - + 12 - + 10 - + 19 - + 14 - + 14 - + 19 - + 15 - + 9 - + 10 - + 19 - + 16 - + 10 - + 11 - + 9 - + 16 - + 13 - + 18 - + 13 - + 8 - + 10 - + 19 - + 15 - + 9 - + 10 - + 12 - + 19 - + 17 - + 10 - + 14 - + 20 - + 16 - + 19 - + 16 - + 20 - + 13 - + 14 - + 11 - + 20 - + 17 - + 12 - + 16 - + 17 - + 19 - + 19 - + 12 - + 9 - + 14 - + 14 - + 14 - + 13 - + 15 - + 14 - + 10 - + 13 - + 16 - + 10 - + 18 - + 16 - + 11 - + 16 - + 14 - + 14 - + 9 - + 16 - + 18 - + 16 - + 14 - + 17 - + 17 - + 20 - + 8 - + 14 - + 14 - + 9 - + 19 - + 9 - + 12 - + 18 - + 16 - + 15 - + 8 - + 14 - + 18 - + 12 - + 19 - + 11 - + 18 - + 13 - + 14 - + 14 - + 9 - + 12 - + 11 - + 15 - + 20 - + 10 - + 8 - + 10 - + 13 - + 10 - + 18 - + 18 - + 17 - + 15 - + 12 - + 14 - + 18 - + 16 - + 17 - + 10 - + 16 - + 18 - + 12 - + 17 - + 15 - + 17 - + 19 - + 19 - + 20 - + 10 - + 10 - + 19 - + 13 - + 13 - + 9 - + 14 - + 12 - + 9 - + 16 - + 18 - + 9 - + 14 - + 12 - + 19 - + 8 - + 18 - + 17 - + 8 - + 19 - + 20 - + 11 - + 19 - + 11 - + 19 - + 17 - + 10 - + 9 - + 16 - + 11 - + 20 - + 12 - + 20 - + 16 - + 11 - + 10 - + 17 - + 14 - + 13 - + 12 - + 16 - + 13 - + 9 - + 17 - + 13 - + 12 - + 12 - + 15 - + 14 - + 8 - + 15 - + 12 - + 15 - + 14 - + 9 - + 14 - + 12 - + 20 - + 10 - + 9 - + 20 - + 17 - + 12 @@ -5292,1191 +5292,1191 @@ 11 - + 8 - + 17 - + 15 - + 16 - + 16 - + 20 - + 9 - + 20 - + 9 - + 19 - + 13 - + 9 - + 18 - + 10 - + 14 - + 15 - + 17 - + 12 - + 12 - + 19 - + 15 - + 20 - + 11 - + 16 - + 14 - + 11 - + 14 - + 17 - + 19 - + 12 - + 19 - + 12 - + 13 - + 18 - + 12 - + 12 - + 17 - + 19 - + 11 - + 13 - + 16 - + 19 - + 15 - + 8 - + 17 - + 18 - + 14 - + 15 - + 15 - + 16 - + 12 - + 9 - + 13 - + 16 - + 14 - + 19 - + 19 - + 16 - + 18 - + 14 - + 10 - + 16 - + 19 - + 10 - + 20 - + 18 - + 16 - + 11 - + 12 - + 11 - + 19 - + 12 - + 11 - + 9 - + 8 - + 12 - + 16 - + 12 - + 18 - + 10 - + 13 - + 12 - + 13 - + 18 - + 13 - + 9 - + 17 - + 10 - + 9 - + 9 - + 14 - + 18 - + 17 - + 17 - + 18 - + 12 - + 12 - + 14 - + 8 - + 14 - + 17 - + 13 - + 18 - + 11 - + 10 - + 17 - + 12 - + 11 - + 14 - + 9 - + 13 - + 11 - + 8 - + 12 - + 16 - + 11 - + 11 - + 13 - + 17 - + 12 - + 10 - + 10 - + 9 - + 20 - + 9 - + 12 - + 12 - + 15 - + 15 - + 17 - + 14 - + 14 - + 10 - + 20 - + 12 - + 9 - + 11 - + 14 - + 14 - + 18 - + 13 - + 13 - + 15 - + 16 - + 12 - + 18 - + 10 - + 15 - + 9 - + 12 - + 14 - + 17 - + 14 - + 10 - + 17 - + 9 - + 14 - + 19 - + 11 - + 15 - + 11 - + 13 - + 13 - + 18 - + 15 - + 9 - + 16 - + 11 - + 14 - + 15 - + 10 - + 15 - + 16 - + 16 - + 17 - + 13 - + 17 - + 9 - + 18 - + 10 - + 17 - + 18 - + 12 - + 14 - + 9 - + 20 - + 19 - + 8 - + 18 - + 17 - + 19 - + 17 - + 10 - + 16 - + 20 - + 10 - + 10 - + 9 - + 15 - + 16 - + 16 - + 8 - + 12 - + 9 - + 15 - + 16 - + 19 - + 10 - + 11 - + 14 - + 16 - + 16 - + 18 - + 9 - + 17 - + 17 - + 19 - + 19 - + 18 - + 10 - + 15 - + 10 - + 14 - + 18 - + 16 - + 15 - + 15 - + 15 - + 10 - + 15 - + 8 - + 15 - + 13 - + 9 - + 17 - + 11 - + 18 - + 12 - + 19 - + 16 - + 18 - + 11 - + 17 - + 19 - + 13 - + 17 - + 16 - + 14 - + 19 - + 20 - + 20 - + 16 - + 16 - + 9 - + 20 - + 9 - + 12 - + 11 - + 13 - + 11 - + 11 - + 17 - + 9 - + 19 - + 11 - + 18 - + 13 - + 20 - + 9 - + 11 - + 11 - + 16 - + 11 - + 8 - + 10 - + 19 - + 12 - + 18 - + 9 - + 18 - + 17 - + 17 - + 15 - + 15 - + 10 - + 12 - + 8 - + 9 - + 10 - + 8 - + 19 - + 14 - + 18 - + 18 - + 12 - + 18 - + 16 @@ -6632,587 +6632,587 @@ 12 - + 18 - + 17 - + 15 - + 13 - + 11 - + 13 - + 12 - + 9 - + 17 - + 15 - + 12 - + 10 - + 11 - + 9 - + 12 - + 16 - + 17 - + 17 - + 11 - + 10 - + 11 - + 19 - + 18 - + 14 - + 14 - + 16 - + 10 - + 11 - + 17 - + 15 - + 10 - + 20 - + 9 - + 20 - + 17 - + 13 - + 17 - + 13 - + 11 - + 10 - + 9 - + 14 - + 10 - + 19 - + 13 - + 10 - + 18 - + 13 - + 20 - + 14 - + 15 - + 15 - + 15 - + 12 - + 10 - + 17 - + 8 - + 15 - + 14 - + 16 - + 14 - + 13 - + 16 - + 8 - + 11 - + 12 - + 14 - + 9 - + 9 - + 11 - + 16 - + 17 - + 15 - + 12 - + 16 - + 18 - + 14 - + 9 - + 11 - + 11 - + 10 - + 14 - + 18 - + 9 - + 9 - + 9 - + 8 - + 15 - + 15 - + 17 - + 10 - + 17 - + 12 - + 18 - + 10 - + 18 - + 16 - + 12 - + 17 - + 16 - + 12 - + 20 - + 19 - + 11 - + 19 - + 10 - + 12 - + 15 - + 19 - + 14 - + 16 - + 17 - + 15 - + 18 - + 15 - + 9 - + 9 - + 16 - + 14 - + 18 - + 16 - + 11 - + 18 - + 16 - + 10 - + 12 - + 13 - + 15 - + 18 - + 18 - + 12 - + 12 - + 18 - + 10 - + 8 - + 12 - + 18 - + 9 - + 9 - + 13 - + 16 - + 12 - + 16 - + 11 - + 11 - + 19 @@ -7366,7 +7366,7 @@ -13 +39 @@ -7512,289 +7512,285 @@ 45 - + -40 +409 - + 465 - + 619 - + 493 - + 368 - + 343 - + 345 - + 401 - + 569 - + 519 - + 413 - + 590 - + 437 - + 344 - + 310 - + 597 - + 348 - + 597 - + 421 - + 508 - - -574 - - + -602 +574 - + -263 +602 - + -303 +263 - + -435 +303 - + -285 +435 - + -534 +285 - + -598 +534 - + -287 +598 - + -463 +287 - + -484 +463 - + -415 +484 - + -435 +415 - + -433 +435 - + -588 +433 - + -326 +588 - + -521 +326 - + -377 +521 - + -419 +377 - + -302 +419 -473 +302 -26 +46 -43 +26 -40 +43 -27 +40 -46 +27 -33 +46 -40 +33 -56 +40 -50 +56 -54 +50 -33 +54 -37 +33 -41 +37 -26 +41 -59 +26 -32 +59 -51 +32 -45 +51 -52 +45 -29 +52 -57 +29 -39 +57 -59 +39 -31 +59 -34 +31 -29 +34 -25 +29 -36 +25 -45 +36 -57 +45 @@ -7802,187 +7798,191 @@ -53 +57 -47 +53 -56 +47 -57 +56 -59 +57 59 - + -352 +612 - + -557 +352 - + -264 +557 - + -361 +264 - + -382 +361 - + -526 +382 - + -316 +526 - + -433 +316 - + -320 +433 - + -617 +320 - + -290 +617 - + -287 +290 - + -558 +287 - + -409 +558 - + -331 +409 - + -494 +331 - + -304 +494 - + -446 +304 - + -508 +446 - + +508 + + + 348 - + 331 - + 575 - + 323 - + 351 - + 293 - + 271 - + 586 - + 600 - + 415 - + 474 - + 609 - + 553 - + 506 - + 417 - + 615 - + 374 - + 517 - + 290 - + 331 -519 +50 @@ -8128,237 +8128,233 @@ 46 - + -28 +286 - + 610 - + 465 - + 510 - + 326 - + 402 - + 497 - + 316 - + 472 - + 497 - + 310 - + 269 - + 442 - + 375 - + 545 - + 589 - + 289 - + 275 - + 314 - + 299 - - -481 - - + -317 +481 - + -362 +317 - + -370 +362 - + -368 +370 - + -522 +368 - + -331 +522 - + -532 +331 - + -438 +532 - + -418 +438 - + -589 +418 - + -541 +589 - + -486 +541 - + -618 +486 - + -463 +618 - + -607 +463 - + -371 +607 - + -595 +371 - + -288 +595 - + -449 +288 -346 +449 -58 +33 -35 +58 -47 +35 -59 +47 -28 +59 -43 +28 -50 +43 -45 +50 -48 +45 -60 +48 -57 +60 -60 +57 -42 +60 -52 +42 -38 +52 -31 +38 -48 +31 @@ -8366,239 +8362,243 @@ -59 +48 -52 +59 -43 +52 -48 +43 -49 +48 -50 +49 -26 +50 -59 +26 -43 +59 -59 +43 -55 +59 -34 +55 -27 +34 -41 +27 -59 +41 -54 +59 -51 +54 -43 +51 -36 +43 - + -271 +375 - + -465 +271 - + -263 +465 - + -349 +263 - + -335 +349 - + -570 +335 - + -448 +570 - + -424 +448 - + -420 +424 - + -292 +420 - + -363 +292 - + -505 +363 - + -384 +505 - + -346 +384 - + -584 +346 - + -431 +584 - + -505 +431 - + -574 +505 - + -602 +574 - + +602 + + + 539 - + 374 - + 283 - + 281 - + 286 - + 611 - + 549 - + 266 - + 396 - + 498 - + 466 - + 611 - + 396 - + 567 - + 313 - + 573 - + 397 - + 525 - + 571 - + 471 -610 +59 @@ -8892,169 +8892,169 @@ 51 - + -25 +265 - + 390 - + 436 - + 396 - + 273 - + 531 - + 262 - + 312 - + 397 - + 422 - + 330 - + 397 - + 503 - + 589 - + 261 - + 594 - + 493 - + 293 - + 284 - + 486 - - + + 453 - + 432 - + 433 - + 386 - + 578 - + 434 - + 529 - + 399 - + 426 - + 278 - + 386 - + 346 - + 384 - + 505 - + 471 - + 342 - + 402 - + 404 - + 453 - + 400 -507 +49 @@ -9202,7 +9202,7 @@ -33 +11 @@ -9348,995 +9348,995 @@ 10 - + 17 - + 13 - + 15 - + 13 - + 16 - + 16 - + 14 - + 17 - + 8 - + 20 - + 14 - + 13 - + 18 - + 9 - + 15 - + 14 - + 9 - + 17 - + 17 - + 18 - + 12 - + 12 - + 19 - + 10 - + 9 - + 13 - + 18 - + 19 - + 17 - + 18 - + 10 - + 20 - + 18 - + 9 - + 19 - + 14 - + 13 - + 10 - + 19 - + 18 - + 12 - + 13 - + 16 - + 10 - + 14 - + 12 - + 20 - + 17 - + 10 - + 9 - + 14 - + 18 - + 16 - + 10 - + 12 - + 13 - + 15 - + 17 - + 8 - + 17 - + 9 - + 16 - + 18 - + 9 - + 17 - + 9 - + 15 - + 19 - + 13 - + 18 - + 16 - + 10 - + 17 - + 9 - + 10 - + 20 - + 19 - + 16 - + 9 - + 12 - + 13 - + 8 - + 15 - + 17 - + 14 - + 19 - + 9 - + 19 - + 12 - + 14 - + 15 - + 10 - + 19 - + 11 - + 11 - + 9 - + 15 - + 12 - + 14 - + 12 - + 11 - + 16 - + 13 - + 20 - + 8 - + 11 - + 10 - + 9 - + 17 - + 17 - + 17 - + 20 - + 10 - + 16 - + 16 - + 12 - + 18 - + 11 - + 14 - + 17 - + 14 - + 20 - + 18 - + 18 - + 17 - + 9 - + 12 - + 16 - + 15 - + 14 - + 13 - + 19 - + 19 - + 16 - + 20 - + 15 - + 14 - + 8 - + 10 - + 17 - + 19 - + 13 - + 16 - + 9 - + 8 - + 13 - + 12 - + 16 - + 11 - + 16 - + 13 - + 17 - + 9 - + 16 - + 10 - + 11 - + 9 - + 19 - + 19 - + 19 - + 11 - + 13 - + 9 - + 13 - + 9 - + 13 - + 12 - + 19 - + 12 - + 15 - + 11 - + 14 - + 8 - + 17 - + 11 - + 8 - + 18 - + 10 - + 18 - + 16 - + 18 - + 12 - + 14 - + 14 - + 18 - + 19 - + 15 - + 12 - + 13 - + 16 - + 13 - + 13 - + 12 - + 14 - + 8 - + 9 - + 17 - + 10 - + 14 - + 18 - + 13 - + 19 - + 18 - + 19 - + 19 - + 8 - + 12 - + 17 - + 19 - + 12 - + 15 - + 15 - + 10 - + 10 - + 15 - + 15 - + 12 - + 19 - + 12 - + 18 - + 11 - + 13 - + 16 - + 18 - + 19 - + 15 - + 17 - + 18 - + 14 - + 18 - + 18 - + 9 - + 13 - + 9 - + 14 - + 14 - + 12 - + 16 - + 14 - + 14 - + 15 - + 17 - + 16 - + 13 - + 12 - + 17 - + 19 - + 13 @@ -10492,1211 +10492,1211 @@ 14 - + 9 - + 11 - + 9 - + 14 - + 8 - + 10 - + 13 - + 14 - + 12 - + 10 - + 18 - + 19 - + 14 - + 10 - + 9 - + 10 - + 15 - + 10 - + 18 - + 18 - + 9 - + 13 - + 13 - + 19 - + 17 - + 14 - + 13 - + 14 - + 9 - + 19 - + 15 - + 9 - + 12 - + 14 - + 10 - + 11 - + 19 - + 13 - + 12 - + 9 - + 17 - + 15 - + 13 - + 12 - + 12 - + 18 - + 9 - + 19 - + 19 - + 11 - + 16 - + 14 - + 11 - + 12 - + 20 - + 10 - + 10 - + 16 - + 17 - + 19 - + 13 - + 17 - + 10 - + 10 - + 19 - + 9 - + 14 - + 19 - + 13 - + 10 - + 19 - + 14 - + 14 - + 10 - + 12 - + 11 - + 11 - + 19 - + 19 - + 8 - + 8 - + 11 - + 13 - + 10 - + 12 - + 15 - + 9 - + 15 - + 11 - + 9 - + 9 - + 9 - + 15 - + 10 - + 11 - + 20 - + 8 - + 13 - + 16 - + 17 - + 14 - + 10 - + 12 - + 13 - + 16 - + 19 - + 9 - + 12 - + 8 - + 12 - + 15 - + 19 - + 17 - + 10 - + 8 - + 8 - + 8 - + 10 - + 11 - + 10 - + 19 - + 11 - + 14 - + 18 - + 11 - + 14 - + 12 - + 17 - + 13 - + 11 - + 10 - + 9 - + 19 - + 17 - + 11 - + 8 - + 10 - + 11 - + 19 - + 15 - + 17 - + 12 - + 18 - + 12 - + 13 - + 9 - + 13 - + 15 - + 18 - + 13 - + 17 - + 17 - + 19 - + 15 - + 19 - + 10 - + 13 - + 10 - + 10 - + 9 - + 16 - + 16 - + 19 - + 10 - + 13 - + 19 - + 8 - + 9 - + 13 - + 18 - + 18 - + 13 - + 20 - + 19 - + 15 - + 9 - + 11 - + 14 - + 19 - + 18 - + 9 - + 11 - + 12 - + 18 - + 12 - + 9 - + 9 - + 9 - + 16 - + 15 - + 12 - + 17 - + 19 - + 9 - + 11 - + 12 - + 15 - + 10 - + 11 - + 12 - + 15 - + 19 - + 17 - + 16 - + 15 - + 12 - + 17 - + 13 - + 17 - + 11 - + 8 - + 11 - + 11 - + 12 - + 11 - + 16 - + 12 - + 17 - + 12 - + 13 - + 16 - + 11 - + 14 - + 15 - + 18 - + 11 - + 13 - + 15 - + 19 - + 16 - + 10 - + 9 - + 11 - + 14 - + 10 - + 18 - + 17 - + 19 - + 13 - + 14 - + 13 - + 13 - + 15 - + 16 - + 12 - + 17 - + 9 - + 10 - + 12 - + 13 - + 20 - + 19 - + 16 - + 17 - + 14 - + 19 - + 19 - + 10 - + 10 - + 10 - + 17 - + 20 - + 15 - + 12 - + 8 - + 16 - + 13 - + 15 - + 12 - + 9 - + 12 - + 19 - + 11 - + 14 - + 18 - + 15 - + 14 - + 10 - + 12 - + 18 - + 15 - + 16 - + 19 - + 15 - + 13 - + 17 - + 18 - + 19 - + 11 - + 11 - + 15 - + 9 - + 14 - + 13 - + 17 - + 17 - + 17 - + 10 - + 16 - + 12 - + 10 - + 11 @@ -11852,1711 +11852,1711 @@ 11 - + 19 - + 19 - + 11 - + 9 - + 18 - + 14 - + 12 - + 17 - + 11 - + 14 - + 17 - + 17 - + 19 - + 10 - + 11 - + 12 - + 19 - + 16 - + 19 - + 15 - + 15 - + 14 - + 10 - + 17 - + 11 - + 19 - + 17 - + 8 - + 15 - + 9 - + 15 - + 9 - + 17 - + 16 - + 9 - + 12 - + 13 - + 14 - + 15 - + 16 - + 14 - + 19 - + 18 - + 10 - + 15 - + 12 - + 19 - + 15 - + 16 - + 18 - + 10 - + 19 - + 9 - + 19 - + 14 - + 10 - + 14 - + 17 - + 17 - + 17 - + 13 - + 11 - + 11 - + 11 - + 18 - + 8 - + 13 - + 11 - + 15 - + 18 - + 17 - + 11 - + 18 - + 12 - + 11 - + 18 - + 9 - + 15 - + 16 - + 19 - + 11 - + 19 - + 12 - + 13 - + 19 - + 10 - + 18 - + 10 - + 11 - + 10 - + 9 - + 12 - + 19 - + 15 - + 19 - + 13 - + 17 - + 17 - + 10 - + 10 - + 14 - + 11 - + 14 - + 9 - + 17 - + 18 - + 19 - + 9 - + 16 - + 17 - + 18 - + 15 - + 13 - + 9 - + 12 - + 10 - + 16 - + 8 - + 9 - + 8 - + 14 - + 9 - + 16 - + 14 - + 18 - + 16 - + 11 - + 16 - + 20 - + 19 - + 15 - + 17 - + 13 - + 12 - + 14 - + 16 - + 10 - + 15 - + 14 - + 14 - + 11 - + 13 - + 9 - + 19 - + 17 - + 9 - + 15 - + 12 - + 19 - + 12 - + 8 - + 8 - + 16 - + 14 - + 17 - + 10 - + 15 - + 9 - + 11 - + 15 - + 17 - + 12 - + 15 - + 15 - + 19 - + 15 - + 19 - + 13 - + 14 - + 17 - + 20 - + 18 - + 12 - + 9 - + 9 - + 9 - + 17 - + 10 - + 19 - + 12 - + 14 - + 15 - + 9 - + 12 - + 15 - + 12 - + 11 - + 13 - + 14 - + 9 - + 9 - + 20 - + 12 - + 13 - + 14 - + 19 - + 14 - + 9 - + 9 - + 18 - + 14 - + 15 - + 8 - + 9 - + 10 - + 18 - + 9 - + 14 - + 18 - + 11 - + 17 - + 15 - + 10 - + 11 - + 13 - + 20 - + 13 - + 16 - + 12 - + 17 - + 9 - + 8 - + 11 - + 13 - + 17 - + 20 - + 13 - + 17 - + 18 - + 9 - + 15 - + 10 - + 11 - + 15 - + 15 - + 13 - + 13 - + 20 - + 12 - + 9 - + 11 - + 18 - + 19 - + 10 - + 9 - + 15 - + 9 - + 10 - + 16 - + 17 - + 8 - + 14 - + 19 - + 16 - + 14 - + 18 - + 8 - + 15 - + 14 - + 16 - + 18 - + 12 - + 12 - + 18 - + 11 - + 10 - + 14 - + 10 - + 17 - + 11 - + 14 - + 10 - + 12 - + 18 - + 20 - + 18 - + 13 - + 15 - + 18 - + 19 - + 13 - + 17 - + 14 - + 15 - + 19 - + 10 - + 18 - + 13 - + 8 - + 12 - + 19 - + 20 - + 14 - + 16 - + 14 - + 20 - + 14 - + 18 - + 15 - + 9 - + 9 - + 15 - + 14 - + 15 - + 19 - + 20 - + 17 - + 20 - + 14 - + 10 - + 10 - + 10 - + 19 - + 9 - + 19 - + 18 - + 18 - + 17 - + 14 - + 9 - + 14 - + 17 - + 19 - + 18 - + 8 - + 15 - + 11 - + 19 - + 11 - + 10 - + 18 - + 17 - + 12 - + 12 - + 14 - + 16 - + 10 - + 13 - + 17 - + 18 - + 16 - + 9 - + 13 - + 19 - + 8 - + 8 - + 13 - + 12 - + 19 - + 13 - + 10 - + 9 - + 10 - + 10 - + 16 - + 19 - + 15 - + 13 - + 16 - + 18 - + 10 - + 20 - + 9 - + 12 - + 16 - + 10 - + 19 - + 8 - + 19 - + 19 - + 8 - + 10 - + 17 - + 18 - + 15 - + 9 - + 18 - + 10 - + 9 - + 15 - + 9 - + 8 - + 13 - + 20 - + 14 - + 13 - + 14 - + 17 - + 16 - + 14 - + 12 - + 17 - + 13 - + 19 - + 11 - + 20 - + 12 - + 20 - + 13 - + 19 - + 17 - + 18 - + 19 - + 15 - + 11 - + 18 - + 15 - + 13 - + 17 - + 20 - + 11 - + 9 - + 8 - + 8 - + 15 - + 9 - + 8 - + 19 - + 14 - + 14 - + 19 - + 17 - + 16 - + 18 - + 12 - + 18 - + 12 @@ -13708,971 +13708,971 @@ 10 - + 14 - + 11 - + 13 - + 14 - + 16 - + 10 - + 18 - + 9 - + 13 - + 12 - + 14 - + 19 - + 15 - + 14 - + 18 - + 15 - + 16 - + 17 - + 12 - + 17 - + 14 - + 12 - + 10 - + 15 - + 13 - + 15 - + 19 - + 11 - + 12 - + 15 - + 12 - + 11 - + 11 - + 12 - + 14 - + 15 - + 17 - + 12 - + 16 - + 17 - + 10 - + 13 - + 9 - + 13 - + 16 - + 13 - + 13 - + 15 - + 10 - + 18 - + 18 - + 9 - + 17 - + 18 - + 8 - + 9 - + 12 - + 14 - + 13 - + 10 - + 19 - + 19 - + 13 - + 8 - + 10 - + 8 - + 13 - + 9 - + 16 - + 15 - + 10 - + 16 - + 9 - + 11 - + 20 - + 13 - + 11 - + 15 - + 17 - + 18 - + 13 - + 14 - + 16 - + 8 - + 17 - + 9 - + 9 - + 16 - + 18 - + 14 - + 17 - + 10 - + 13 - + 11 - + 9 - + 20 - + 11 - + 14 - + 10 - + 20 - + 8 - + 12 - + 9 - + 13 - + 17 - + 16 - + 13 - + 12 - + 10 - + 8 - + 11 - + 18 - + 13 - + 19 - + 12 - + 11 - + 17 - + 9 - + 15 - + 8 - + 12 - + 20 - + 18 - + 16 - + 17 - + 12 - + 15 - + 9 - + 17 - + 16 - + 13 - + 10 - + 12 - + 19 - + 16 - + 14 - + 20 - + 18 - + 16 - + 10 - + 17 - + 10 - + 14 - + 13 - + 18 - + 9 - + 19 - + 8 - + 11 - + 19 - + 16 - + 8 - + 15 - + 10 - + 18 - + 12 - + 13 - + 14 - + 13 - + 15 - + 14 - + 19 - + 11 - + 20 - + 11 - + 15 - + 11 - + 14 - + 9 - + 17 - + 9 - + 16 - + 11 - + 16 - + 15 - + 19 - + 13 - + 11 - + 8 - + 15 - + 16 - + 14 - + 20 - + 13 - + 19 - + 14 - + 16 - + 19 - + 14 - + 13 - + 13 - + 12 - + 14 - + 11 - + 10 - + 17 - + 18 - + 12 - + 17 - + 18 - + 16 - + 17 - + 19 - + 10 - + 10 - + 14 - + 15 - + 9 - + 17 - + 15 - + 18 - + 15 - + 18 - + 13 - + 11 - + 19 - + 18 - + 13 - + 14 - + 14 - + 19 - + 19 - + 17 - + 15 - + 14 - + 9 - + 14 - + 11 - + 19 - + 19 - + 18 - + 8 - + 18 - + 19 - + 14 - + 8 - + 9 - + 9 - + 8 - + 10 - + 10 - + 15 @@ -14828,1515 +14828,1515 @@ 18 - + 17 - + 13 - + 13 - + 17 - + 18 - + 11 - + 18 - + 18 - + 17 - + 16 - + 9 - + 18 - + 15 - + 11 - + 13 - + 11 - + 19 - + 12 - + 11 - + 10 - + 9 - + 14 - + 11 - + 12 - + 19 - + 9 - + 13 - + 10 - + 10 - + 13 - + 15 - + 13 - + 19 - + 17 - + 16 - + 13 - + 9 - + 15 - + 10 - + 16 - + 20 - + 13 - + 11 - + 9 - + 19 - + 17 - + 11 - + 20 - + 18 - + 15 - + 10 - + 11 - + 10 - + 16 - + 13 - + 10 - + 14 - + 15 - + 14 - + 11 - + 17 - + 8 - + 15 - + 13 - + 8 - + 12 - + 11 - + 12 - + 10 - + 15 - + 16 - + 15 - + 10 - + 17 - + 13 - + 17 - + 11 - + 16 - + 10 - + 17 - + 14 - + 19 - + 11 - + 11 - + 8 - + 11 - + 13 - + 12 - + 8 - + 19 - + 15 - + 11 - + 14 - + 16 - + 17 - + 12 - + 15 - + 9 - + 13 - + 12 - + 9 - + 18 - + 19 - + 20 - + 13 - + 9 - + 11 - + 10 - + 11 - + 9 - + 16 - + 17 - + 16 - + 19 - + 20 - + 13 - + 10 - + 10 - + 10 - + 12 - + 12 - + 15 - + 19 - + 11 - + 20 - + 9 - + 13 - + 15 - + 15 - + 18 - + 11 - + 17 - + 14 - + 10 - + 13 - + 20 - + 19 - + 14 - + 9 - + 15 - + 19 - + 16 - + 10 - + 10 - + 19 - + 12 - + 16 - + 14 - + 17 - + 18 - + 17 - + 11 - + 11 - + 16 - + 11 - + 19 - + 20 - + 17 - + 14 - + 15 - + 11 - + 17 - + 19 - + 9 - + 14 - + 19 - + 11 - + 20 - + 15 - + 14 - + 14 - + 15 - + 10 - + 10 - + 16 - + 10 - + 20 - + 8 - + 18 - + 11 - + 17 - + 18 - + 13 - + 12 - + 13 - + 18 - + 18 - + 18 - + 10 - + 18 - + 16 - + 15 - + 12 - + 18 - + 16 - + 18 - + 11 - + 14 - + 15 - + 17 - + 13 - + 13 - + 18 - + 18 - + 9 - + 8 - + 8 - + 16 - + 20 - + 18 - + 19 - + 16 - + 14 - + 10 - + 17 - + 15 - + 13 - + 10 - + 13 - + 19 - + 16 - + 14 - + 12 - + 19 - + 9 - + 11 - + 16 - + 8 - + 9 - + 20 - + 19 - + 18 - + 8 - + 18 - + 19 - + 15 - + 8 - + 13 - + 18 - + 17 - + 14 - + 17 - + 12 - + 20 - + 11 - + 10 - + 16 - + 10 - + 18 - + 16 - + 11 - + 9 - + 13 - + 17 - + 18 - + 19 - + 18 - + 11 - + 9 - + 15 - + 12 - + 14 - + 19 - + 18 - + 10 - + 10 - + 19 - + 16 - + 13 - + 15 - + 14 - + 13 - + 14 - + 18 - + 15 - + 15 - + 16 - + 13 - + 11 - + 19 - + 18 - + 14 - + 9 - + 18 - + 16 - + 12 - + 9 - + 15 - + 16 - + 11 - + 11 - + 19 - + 17 - + 14 - + 16 - + 8 - + 13 - + 13 - + 17 - + 17 - + 17 - + 12 - + 9 - + 14 - + 11 - + 10 - + 10 - + 8 - + 11 - + 18 - + 18 - + 9 - + 19 - + 17 - + 13 - + 12 - + 19 - + 18 - + 19 - + 10 - + 16 - + 15 - + 12 - + 12 - + 19 - + 8 - + 12 - + 9 - + 11 - + 19 - + 15 - + 9 - + 16 - + 15 - + 14 - + 19 - + 16 - + 19 - + 15 - + 10 - + 19 - + 17 - + 11 - + 12 - + 20 - + 19 - + 18 - + 16 - + 15 - + 17 - + 19 - + 16 - + 15 - + 17 - + 16 - + 15 - + 18 - + 10 - + 14 - + 12 - + 18 - + 18 - + 13 - + 18 - + 10 - + 17 - + 14 - + 14 - + 16 - + 14 - + 12 - + 9 - + 14 - + 18 - + 10 - + 16 - + 11 - + 9 @@ -16492,1455 +16492,1455 @@ 19 - + 19 - + 18 - + 9 - + 11 - + 18 - + 18 - + 19 - + 16 - + 15 - + 8 - + 18 - + 20 - + 18 - + 16 - + 13 - + 9 - + 10 - + 10 - + 10 - + 10 - + 10 - + 13 - + 9 - + 17 - + 15 - + 18 - + 14 - + 18 - + 16 - + 11 - + 8 - + 15 - + 16 - + 11 - + 18 - + 20 - + 11 - + 12 - + 15 - + 13 - + 17 - + 12 - + 16 - + 19 - + 11 - + 17 - + 15 - + 15 - + 18 - + 12 - + 12 - + 13 - + 17 - + 17 - + 9 - + 19 - + 11 - + 14 - + 12 - + 14 - + 9 - + 13 - + 19 - + 10 - + 19 - + 10 - + 9 - + 11 - + 13 - + 15 - + 14 - + 12 - + 8 - + 9 - + 18 - + 13 - + 8 - + 14 - + 15 - + 8 - + 15 - + 9 - + 9 - + 13 - + 13 - + 20 - + 9 - + 11 - + 9 - + 13 - + 11 - + 9 - + 17 - + 14 - + 9 - + 14 - + 12 - + 15 - + 9 - + 17 - + 14 - + 13 - + 15 - + 18 - + 11 - + 11 - + 19 - + 19 - + 15 - + 16 - + 19 - + 8 - + 13 - + 10 - + 9 - + 13 - + 20 - + 12 - + 11 - + 11 - + 15 - + 14 - + 10 - + 17 - + 18 - + 19 - + 15 - + 12 - + 8 - + 12 - + 10 - + 11 - + 15 - + 8 - + 8 - + 17 - + 9 - + 12 - + 17 - + 17 - + 15 - + 18 - + 17 - + 9 - + 19 - + 11 - + 20 - + 13 - + 19 - + 12 - + 11 - + 15 - + 9 - + 14 - + 14 - + 12 - + 8 - + 19 - + 12 - + 15 - + 11 - + 14 - + 19 - + 8 - + 10 - + 18 - + 9 - + 14 - + 8 - + 13 - + 17 - + 18 - + 20 - + 11 - + 11 - + 17 - + 18 - + 10 - + 12 - + 9 - + 14 - + 19 - + 16 - + 16 - + 18 - + 12 - + 10 - + 12 - + 18 - + 14 - + 20 - + 19 - + 19 - + 11 - + 14 - + 11 - + 15 - + 16 - + 15 - + 17 - + 15 - + 12 - + 20 - + 18 - + 16 - + 12 - + 10 - + 14 - + 14 - + 19 - + 17 - + 18 - + 14 - + 13 - + 15 - + 11 - + 9 - + 19 - + 17 - + 10 - + 9 - + 15 - + 19 - + 20 - + 10 - + 17 - + 14 - + 13 - + 16 - + 16 - + 12 - + 17 - + 10 - + 12 - + 18 - + 9 - + 16 - + 20 - + 20 - + 16 - + 14 - + 12 - + 9 - + 16 - + 11 - + 11 - + 13 - + 14 - + 17 - + 15 - + 9 - + 17 - + 16 - + 19 - + 13 - + 13 - + 15 - + 11 - + 10 - + 11 - + 20 - + 13 - + 14 - + 11 - + 20 - + 9 - + 12 - + 16 - + 12 - + 10 - + 9 - + 15 - + 18 - + 15 - + 18 - + 15 - + 9 - + 13 - + 15 - + 9 - + 19 - + 8 - + 17 - + 15 - + 16 - + 20 - + 12 - + 17 - + 11 - + 9 - + 9 - + 16 - + 16 - + 16 - + 16 - + 11 - + 9 - + 11 - + 19 - + 12 - + 8 - + 14 - + 14 - + 15 - + 8 - + 16 - + 17 - + 19 - + 14 - + 9 - + 9 - + 8 - + 11 - + 19 - + 18 - + 11 - + 13 - + 10 - + 14 - + 15 - + 12 - + 16 - + 16 - + 18 - + 14 - + 15 - + 14 - + 13 - + 16 - + 16 - + 17 - + 14 - + 11 - + 16 - + 10 - + 20 - + 17 - + 20 - + 14 - + 20 - + 8 - + 19 - + 19 - + 8 - + 16 - + 10 - + 16 - + 16 - + 9 - + 16 - + 12 - + 13 - + 17 - + 15 - + 12 - + 18 - + 19 - + 14 - + 19 - + 17 - + 14 - + 8 - + 11 @@ -17948,8 +17948,4 @@ 14 - - -17 - diff --git a/assets/gpx/KatamachiRapid.gpx b/assets/gpx/KatamachiRapid.gpx index 811dbee18d..5ac015dc4d 100644 --- a/assets/gpx/KatamachiRapid.gpx +++ b/assets/gpx/KatamachiRapid.gpx @@ -3,406 +3,406 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -519,244 +519,244 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -870,409 +870,409 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1389,454 +1389,454 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1950,553 +1950,553 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2610,451 +2610,451 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/assets/gpx/KeioSpecialExpress.gpx b/assets/gpx/KeioSpecialExpress.gpx index b8e310226a..9414cb4f9f 100644 --- a/assets/gpx/KeioSpecialExpress.gpx +++ b/assets/gpx/KeioSpecialExpress.gpx @@ -3,580 +3,580 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -690,334 +690,334 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1134,769 +1134,769 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2013,901 +2013,901 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -3024,775 +3024,775 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -3909,304 +3909,304 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -4320,286 +4320,286 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -4716,532 +4716,532 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -5358,571 +5358,571 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -6039,952 +6039,952 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -7098,352 +7098,352 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -7452,7 +7452,4 @@ - - - diff --git a/assets/gpx/SobuRapid.gpx b/assets/gpx/SobuRapid.gpx index dd6b585be1..6995392e2c 100644 --- a/assets/gpx/SobuRapid.gpx +++ b/assets/gpx/SobuRapid.gpx @@ -3,721 +3,721 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -834,805 +834,805 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1746,1072 +1746,1072 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -2925,571 +2925,568 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - diff --git a/docs/location-simulation.md b/docs/location-simulation.md index c655e7c28d..9ff2e72c3c 100644 --- a/docs/location-simulation.md +++ b/docs/location-simulation.md @@ -84,13 +84,16 @@ npm run gpx:generate -- \ 区間の時間は、StationAPI の `trainRoute` に `model: Estimated` を渡して取る到着・出発の 見込みに合わせる。見込みは、同じ区間の `estimateArrivalTimes` と同じ値で、路線・種別 -ごとの速度を実ダイヤで較正した到着時間推定から出る。 +ごとの速度を実ダイヤで較正した到着時間推定から出る。アプリのオートモード +(`useSimulationMode`) も同じ見込みで走るので、GPX・オートモード・ETA の時間がそろう。 - 停車駅から次の停車駅までの時間は「次の停車駅の到着 − この駅の出発」。推定では停まるが GPX では通過する駅 (平日運転の駅を休日に走る、`--skip` など) は、その駅の停車時間を 走行から引く。 - その時間で走り切る巡航速度を台形の式から求め、線路の長さの上を走らせる。加減速は - `trainRoute` の値 (推定のモデルの値) を使う。区間ごとの 1 秒未満の端数は、次の区間で + `trainRoute` の値 (推定のモデルの値) を使う。1 秒ごとの点は台形の位置の式から置くので、 + 区間の点の数は求めた時間ちょうどになる。計算はオートモードと共通で、 + `src/utils/trainRouteTiming.ts` にある。区間ごとの 1 秒未満の端数は、次の区間で 計画した到着時刻へ戻すので、終点までに積み上がらない。 - 途中の停車駅では、推定の停車時間 (出発 − 到着) だけ停まる。GPX では停まるのに推定では 通過する駅があると、停車時間の見込みが無いので `--dwell` を求めて止まる。 @@ -265,11 +268,11 @@ StationAPI に「F ライナー」という種別は無く、みなとみらい 地下 (みなとみらい線) → 地上 (東急東横線) → 地下 (副都心線 → 西武有楽町線) → 地上 (西武池袋線) と 5 路線 4 社を跨ぎ、地下 ↔ 地上が 3 回切り替わる (うち地下から地上への 復帰が 2 回)。副都心線から西武有楽町線は地下から地下なので切り替わりに数えない。 -欠測は 7 件・最長 326 秒で、ETA 棄却の保険 (`ETA_BOUND_MAX_HOLD_MS` = 90 秒) を超える +欠測は 7 件・最長 327 秒で、ETA 棄却の保険 (`ETA_BOUND_MAX_HOLD_MS` = 90 秒) を超える 区間を 5 箇所含む。 地下鉄分岐と、地上へ復帰したときの基準の張り直しを 1 本で繰り返し踏める -(ただし実機では上記のとおり副都心線の区間だけ。Jest 側は全区間)。約 92 分・4488 点と +(ただし実機では上記のとおり副都心線の区間だけ。Jest 側は全区間)。約 92 分・4487 点と 他のサンプルより大きいが、この経路は他に代えがない。 区間の時間は、路線ごとの到着時間推定に合わせている。トンネル内の欠測の長さは走行時間で diff --git a/scripts/generate-location-gpx.mjs b/scripts/generate-location-gpx.mjs index e5d5fbe6e0..181b6edbe8 100644 --- a/scripts/generate-location-gpx.mjs +++ b/scripts/generate-location-gpx.mjs @@ -9,7 +9,8 @@ // に登録すると、シミュレータが実際の測位として再生する。 // // 速度プロファイルはアプリ本体の generateTrainSpeedProfile をそのまま使う。 -// 独自の加減速モデルを持たせるとアプリの想定と乖離するため。 +// 独自の加減速モデルを持たせるとアプリの想定と乖離するため。到着時間推定の見込みに +// 合わせて走る区間は、オートモードと同じ src/utils/trainRouteTiming.ts で作る。 // // 使い方: // node scripts/generate-location-gpx.mjs --line 1004 --from 100418 --to 100411 \ @@ -24,8 +25,15 @@ import { realpathSync, writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; +import { + cruiseSpeedForDuration, + planLegTimings as planStopLegTimings, + speedProfileForDuration, +} from '../src/utils/trainRouteTiming.ts'; import { generateTrainSpeedProfile } from '../src/utils/trainSpeed.ts'; +export { cruiseSpeedForDuration }; + const DEFAULT_API_URL = 'https://gql.trainlcd.app/'; // 1 秒間隔。iOS は distanceInterval 基準で概ね 1Hz 配信のため実機に近い。 const INTERVAL_SEC = 1; @@ -294,8 +302,8 @@ const fetchLineGroupStations = async (apiUrl, lineGroupId) => { }; // 区間の値を到着時間推定のモデル (model: Estimated) で取る。見込みは、同じ区間の -// estimateArrivalTimes と同じ値になる。アプリのオートモード (useSimulationMode) は -// model を渡さない (Legacy) ので、そちらの走り方とは一致しない。 +// estimateArrivalTimes と同じ値になる。アプリのオートモード (useSimulationMode) も +// 同じ見込みで走るので、GPX・オートモード・ETA の時間がそろう。 const fetchTrainRoute = async (apiUrl, fromStationId, toStationId, lineGroupId) => { const data = await queryStationApi( apiUrl, @@ -514,28 +522,6 @@ export const attachTrainRoute = (route, segments) => { }); }; -// 停車駅から次の停車駅までを加速度 accel・減速度 decel の台形(短い区間は三角形)で -// seconds 秒かけて走る巡航速度(m/s)。台形の所要時間は T(v) = D/v + k·v -// (k = 1/(2a) + 1/(2b)) なので、T(v) = seconds の 2 根のうち遅い方(巡航のある -// 台形)を返す。最短時間 2√(D·k) より短い時間を求められたときは解が無いので、 -// 最短時間で走る速度 √(D/k) を返し、shortened を立てる。 -export const cruiseSpeedForDuration = (distance, seconds, accel, decel) => { - const valid = (x) => Number.isFinite(x) && x > 0; - if (!(valid(distance) && valid(accel) && valid(decel))) { - return null; - } - const k = 0.5 / accel + 0.5 / decel; - const fastest = Math.sqrt(distance / k); - const discriminant = seconds * seconds - 4 * k * distance; - if (!Number.isFinite(seconds) || discriminant <= 0) { - return { speed: fastest, shortened: true }; - } - return { - speed: (seconds - Math.sqrt(discriminant)) / (2 * k), - shortened: false, - }; -}; - // 停車駅ごとの走行時間と停車時間(秒)を、trainRoute の見込みから組み立てる。 // legs[j] は stopIndices[j] から stopIndices[j + 1] までの走行 (runSec) と、 // 着いた駅での停車 (dwellSec。終点は 0)。見込みの無い駅があれば null。 @@ -546,28 +532,21 @@ export const cruiseSpeedForDuration = (distance, seconds, accel, decel) => { // 走行時間から引く。逆に GPX で停まる駅を推定が通過していると、その駅の停車時間が // 見込みに無いので、dwellOverrideSec が無ければ止める (黙って 0 秒で通らないため)。 export const planLegTimings = (route, stopIndices, dwellOverrideSec) => { - if (route.some((station) => station.timing == null)) { - return null; - } - const dwellAt = (i) => route[i].timing.departureSec - route[i].timing.arrivalSec; - return stopIndices.slice(1).map((to, j) => { - const from = stopIndices[j]; - let runSec = route[to].timing.arrivalSec - route[from].timing.departureSec; - for (let i = from + 1; i < to; i++) { - runSec -= dwellAt(i); - } - const isFinal = j === stopIndices.length - 2; - let dwellSec = 0; - if (!isFinal) { - dwellSec = dwellOverrideSec ?? Math.round(dwellAt(to)); - if (dwellSec <= 0) { + const legs = planStopLegTimings( + route.map((station) => station.timing), + stopIndices, + dwellOverrideSec + ); + return ( + legs?.map((leg, j) => { + if (leg.dwellSec == null) { throw new Error( - `${route[to].name} は推定では通過扱いで、停車時間の見込みがありません。--dwell で停車時間を指定してください` + `${route[stopIndices[j + 1]].name} は推定では通過扱いで、停車時間の見込みがありません。--dwell で停車時間を指定してください` ); } - } - return { runSec, dwellSec }; - }); + return leg; + }) ?? null + ); }; // trackDistanceFromPrevious は API の並びで直前にある駅からの長さ。走らせる向きが @@ -665,26 +644,40 @@ export const buildWaypoints = ({ const motion = arrivalStation.motion; const timing = legTimings?.[leg]; const legDwellSec = timing ? timing.dwellSec : dwellSec; - let maxSpeed; + let timedProfile = null; if (timing) { const plannedArrivalSec = plannedDepartureSec + timing.runSec; plannedDepartureSec = plannedArrivalSec + timing.dwellSec; // 到着点は速度プロファイルの最後の点の 1 秒後に置くので、その 1 秒を除いた - // 時間でプロファイルを作る - const fitted = cruiseSpeedForDuration( - distance, - plannedArrivalSec - elapsed - INTERVAL_SEC, - motion.accel, - motion.decel - ); - maxSpeed = fitted?.speed; - if (fitted?.shortened) { - shortenedLegs.push(arrivalStation.name); + // 時間でプロファイルを作る。列の長さはその秒数ちょうどになる + if (distance >= 1) { + const fitted = speedProfileForDuration({ + distance, + seconds: plannedArrivalSec - elapsed - INTERVAL_SEC, + accel: motion.accel, + decel: motion.decel, + }); + if (!fitted) { + throw new Error( + `${arrivalStation.name} 行きの区間の加減速が決まりません` + ); + } + timedProfile = fitted.profile; + if (fitted.shortened) { + shortenedLegs.push(arrivalStation.name); + } } - } else { - maxSpeed = maxSpeedKmh !== undefined ? maxSpeedKmh / 3.6 : motion?.maxSpeed; // m/s } - if (distance >= 1 && (!Number.isFinite(maxSpeed) || maxSpeed <= 0)) { + const maxSpeed = timing + ? undefined + : maxSpeedKmh !== undefined + ? maxSpeedKmh / 3.6 + : motion?.maxSpeed; // m/s + if ( + !timing && + distance >= 1 && + (!Number.isFinite(maxSpeed) || maxSpeed <= 0) + ) { throw new Error( `${arrivalStation.name} 行きの区間の最高速度が決まりません。--max-speed を指定するか、--line-group で trainRoute の値を使ってください` ); @@ -692,14 +685,16 @@ export const buildWaypoints = ({ // 出力を再現可能にするため惰行のランダム性は無効にする。 // 加減速が undefined のときは generateTrainSpeedProfile の既定値になる - const speedProfile = generateTrainSpeedProfile({ - distance, - maxSpeed, - accel: motion?.accel, - decel: motion?.decel, - interval: INTERVAL_SEC, - enableRandomCoast: false, - }); + const speedProfile = + timedProfile ?? + generateTrainSpeedProfile({ + distance, + maxSpeed, + accel: motion?.accel, + decel: motion?.decel, + interval: INTERVAL_SEC, + enableRandomCoast: false, + }); // 直通運転では乗り入れの境界駅が路線ごとに 2 回並ぶ(和光市が東京メトロ // 副都心線と東武東上線の両方に現れるなど)。同じ地点なので区間長が 0 になり、 diff --git a/src/hooks/useSimulationMode.test.tsx b/src/hooks/useSimulationMode.test.tsx index 9979d7b374..c56a66c369 100644 --- a/src/hooks/useSimulationMode.test.tsx +++ b/src/hooks/useSimulationMode.test.tsx @@ -15,6 +15,8 @@ import { useLoopLine } from '~/hooks/useLoopLine'; import { useSimulationMode } from '~/hooks/useSimulationMode'; import { GET_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_TRAIN_ROUTE, GET_TRAIN_ROUTE, } from '~/lib/graphql/queries'; import { store } from '~/store'; @@ -188,9 +190,17 @@ const mockTrainRoute = ( decel?: number; // 駅ごとの trackDistanceFromPrevious。省略時は station を返さない(直線距離だけ) trackDistances?: (number | null)[]; + // 駅ごとの [到着, 出発] の見込み(分)。model: Estimated の応答にあたる + timings?: [number, number][]; } = {} ) => { - const { maxSpeed = 30, accel = 1.0, decel = 1.5, trackDistances } = overrides; + const { + maxSpeed = 30, + accel = 1.0, + decel = 1.5, + trackDistances, + timings, + } = overrides; const segments = stationsInWalkOrder.map((s, i) => { const prev = stationsInWalkOrder[i - 1]; @@ -218,6 +228,8 @@ const mockTrainRoute = ( maxAcceleration: accel, maxDeceleration: decel, maxSpeed, + arrivalCumulativeMinutes: timings?.[i]?.[0] ?? null, + departureCumulativeMinutes: timings?.[i]?.[1] ?? null, }; }); @@ -1112,7 +1124,7 @@ describe('useSimulationMode', () => { }); expect(useGraphQLQuery).toHaveBeenCalledWith( - GET_TRAIN_ROUTE, + GET_ESTIMATED_TRAIN_ROUTE, expect.objectContaining({ variables: expect.objectContaining({ fromStationId: stations[1].id, @@ -1154,7 +1166,7 @@ describe('useSimulationMode', () => { }); expect(useGraphQLQuery).toHaveBeenCalledWith( - GET_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_CONNECTED_TRAIN_ROUTE, expect.objectContaining({ variables: { fromStationId: 9930138, @@ -1172,7 +1184,7 @@ describe('useSimulationMode', () => { }) ); expect(useGraphQLQuery).toHaveBeenCalledWith( - GET_TRAIN_ROUTE, + GET_ESTIMATED_TRAIN_ROUTE, expect.objectContaining({ skip: true }) ); }); @@ -1209,13 +1221,13 @@ describe('useSimulationMode', () => { maxDeceleration: 1, maxSpeed, }); - // legs を渡した trainRoute にだけ応答する。API は乗換駅を両方の区間に含めるので、 + // legs を渡した trainRoute(model: Estimated)にだけ応答する。API は乗換駅を両方の区間に含めるので、 // 新宿は大江戸線の到着(2000m)と埼京線の起点(0m)の 2 回現れる const mockConnectedTrainRoute = ( segments: ReturnType[] ) => (useGraphQLQuery as jest.Mock).mockImplementation((document) => - document === GET_CONNECTED_TRAIN_ROUTE + document === GET_ESTIMATED_CONNECTED_TRAIN_ROUTE ? { data: { trainRoute: { __typename: 'TrainRouteResponse', segments }, @@ -1323,7 +1335,7 @@ describe('useSimulationMode', () => { }); expect(useGraphQLQuery).toHaveBeenCalledWith( - GET_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_CONNECTED_TRAIN_ROUTE, expect.objectContaining({ variables: { fromStationId: 1132103, @@ -1599,4 +1611,244 @@ describe('useSimulationMode', () => { expect(cdDistance).toBeGreaterThan(15000); }); }); + + describe('到着時間推定の見込みに合わせた走行', () => { + // 東京駅から北へ約 2.1km・約 2.2km の 3 駅 + const timedStations = () => [ + mockStation(1, 1, 35.681, 139.767), + mockStation(2, 2, 35.7, 139.767), + mockStation(3, 3, 35.72, 139.767), + ]; + + // 各駅に初めて書いた時刻(始点を出てからの秒)と、そこを離れた時刻 + const stationVisits = (stations: Station[]) => { + const writes = (store.set as jest.Mock).mock.calls + .filter((call) => call[0] === locationAtom) + .map((call) => call[1] as ReturnType); + return stations.map((station) => { + const at = (loc: ReturnType) => + loc.coords.latitude === station.latitude && + loc.coords.longitude === station.longitude; + const first = writes.findIndex(at); + const left = writes.findIndex((loc, i) => i > first && !at(loc)); + const seconds = (i: number) => + i < 0 ? null : (writes[i].timestamp - 100000) / 1000; + return { arrivedSec: seconds(first), leftSec: seconds(left) }; + }); + }; + + it('停車駅には見込みの到着時刻に着き、推定の停車時間だけ止まる', () => { + const stations = timedStations(); + setupAtomMocks( + { station: stations[0], stations, selectedDirection: 'INBOUND' }, + { autoModeEnabled: true } + ); + // 2 分で次の駅、30 秒停車、さらに 2.5 分で終点 + mockTrainRoute(stations, { + accel: 0.7, + decel: 0.9, + timings: [ + [0, 0], + [2, 2.5], + [5, 5], + ], + }); + + renderHook(() => useSimulationMode(), { + wrapper: ({ children }) => {children}, + }); + jest.advanceTimersByTime(320 * 1000); + + const [, second, terminal] = stationVisits(stations); + expect(second.arrivedSec).toBe(120); + // 停車した 30 秒の次のティックで動き出す + expect(second.leftSec).toBe(151); + expect(terminal.arrivedSec).toBe(300); + }); + + it('推定が通過扱いの駅に停まるときは下限だけ止まり、終点の時刻はずらさない', () => { + const stations = timedStations(); + setupAtomMocks( + { station: stations[0], stations, selectedDirection: 'INBOUND' }, + { autoModeEnabled: true } + ); + // 2 駅目の到着と出発が同じ(推定では通過) + mockTrainRoute(stations, { + accel: 0.7, + decel: 0.9, + timings: [ + [0, 0], + [2, 2], + [5, 5], + ], + }); + + renderHook(() => useSimulationMode(), { + wrapper: ({ children }) => {children}, + }); + jest.advanceTimersByTime(320 * 1000); + + const [, second, terminal] = stationVisits(stations); + expect(second.arrivedSec).toBe(120); + expect(second.leftSec).toBe(123); + expect(terminal.arrivedSec).toBe(300); + }); + + it('乗換経路では、乗換駅の到着から次の区間の出発までをその駅での停車にする', () => { + const OEDO = 99301; + const SAIKYO = 11321; + const withTrainType = (station: Station, groupId: number): Station => ({ + ...station, + trainType: { groupId } as Station['trainType'], + }); + const stations = [ + withTrainType(mockStation(9930138, 9930138, 35.76, 139.63, OEDO), 7), + withTrainType(mockStation(9930100, 1130225, 35.69, 139.69, OEDO), 7), + withTrainType(mockStation(9930128, 1130208, 35.69, 139.7, OEDO), 7), + withTrainType(mockStation(1132104, 1130208, 35.69, 139.7, SAIKYO), 170), + withTrainType( + mockStation(1132103, 1130205, 35.658, 139.701, SAIKYO), + 170 + ), + ]; + setupAtomMocks( + { station: stations[0], stations, selectedDirection: 'INBOUND' }, + { autoModeEnabled: true } + ); + const segment = ( + groupId: number, + distanceFromPrevious: number, + [arrival, departure]: [number, number] + ) => ({ + __typename: 'TrainRouteSegment' as const, + station: { groupId, trackDistanceFromPrevious: null }, + distanceFromPrevious, + maxAcceleration: 1, + maxDeceleration: 1, + maxSpeed: 30, + arrivalCumulativeMinutes: arrival, + departureCumulativeMinutes: departure, + }); + // 新宿は大江戸線の降車(5 分着)と埼京線の乗車(8 分着・10 分発)の 2 行。 + // 着いてから 10 分の発車まで、徒歩と待ち時間のぶん新宿に止まる + const segments = [ + segment(9930138, 0, [0, 0]), + segment(1130225, 1000, [2, 2.5]), + segment(1130208, 2000, [5, 5]), + segment(1130208, 0, [8, 10]), + segment(1130205, 3000, [13, 13]), + ]; + (useGraphQLQuery as jest.Mock).mockImplementation((document) => + document === GET_ESTIMATED_CONNECTED_TRAIN_ROUTE + ? { + data: { + trainRoute: { __typename: 'TrainRouteResponse', segments }, + }, + loading: false, + error: undefined, + } + : { data: undefined, loading: false, error: undefined } + ); + + renderHook(() => useSimulationMode(), { + wrapper: ({ children }) => {children}, + }); + jest.advanceTimersByTime(800 * 1000); + + const [, tochomae, shinjuku, , shibuya] = stationVisits(stations); + expect(tochomae.arrivedSec).toBe(120); + expect(shinjuku.arrivedSec).toBe(300); + expect(shinjuku.leftSec).toBe(601); + expect(shibuya.arrivedSec).toBe(780); + }); + + it('乗換経路でも、model: Estimated の問い合わせがエラーなら従来の問い合わせに戻す', () => { + const withTrainType = (station: Station, groupId: number): Station => ({ + ...station, + trainType: { groupId } as Station['trainType'], + }); + const stations = [ + withTrainType(mockStation(9930138, 9930138, 35.76, 139.63, 99301), 7), + withTrainType(mockStation(9930128, 1130208, 35.69, 139.7, 99301), 7), + withTrainType(mockStation(1132104, 1130208, 35.69, 139.7, 11321), 170), + withTrainType( + mockStation(1132103, 1130205, 35.658, 139.701, 11321), + 170 + ), + ]; + setupAtomMocks( + { station: stations[0], stations, selectedDirection: 'INBOUND' }, + { autoModeEnabled: true } + ); + (useGraphQLQuery as jest.Mock).mockImplementation((document) => + document === GET_ESTIMATED_CONNECTED_TRAIN_ROUTE + ? { + data: undefined, + loading: false, + error: new Error('Unknown argument "model"'), + } + : { data: undefined, loading: false, error: undefined } + ); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + renderHook(() => useSimulationMode(), { + wrapper: ({ children }) => {children}, + }); + + expect(useGraphQLQuery).toHaveBeenCalledWith( + GET_CONNECTED_TRAIN_ROUTE, + expect.objectContaining({ skip: false }) + ); + expect(useGraphQLQuery).toHaveBeenCalledWith( + GET_TRAIN_ROUTE, + expect.objectContaining({ skip: true }) + ); + }); + + it('model: Estimated の問い合わせがエラーなら、従来の問い合わせの値で走る', () => { + const stations = timedStations(); + setupAtomMocks( + { station: stations[0], stations, selectedDirection: 'INBOUND' }, + { autoModeEnabled: true } + ); + mockTrainRoute(stations); + const legacyResult = (useGraphQLQuery as jest.Mock)(); + (useGraphQLQuery as jest.Mock).mockReset(); + (useGraphQLQuery as jest.Mock).mockImplementation((document) => + document === GET_ESTIMATED_TRAIN_ROUTE + ? { + data: undefined, + loading: false, + error: new Error('Unknown argument "model"'), + } + : document === GET_TRAIN_ROUTE + ? legacyResult + : { data: undefined, loading: false, error: undefined } + ); + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const error = jest.spyOn(console, 'error').mockImplementation(() => {}); + const generateSpy = jest.spyOn( + trainSpeedModule, + 'generateTrainSpeedProfile' + ); + + renderHook(() => useSimulationMode(), { + wrapper: ({ children }) => {children}, + }); + + expect(useGraphQLQuery).toHaveBeenCalledWith( + GET_TRAIN_ROUTE, + expect.objectContaining({ skip: false }) + ); + // 従来の値(区間の最高速度)で速度プロファイルを作る + expect(generateSpy).toHaveBeenCalledWith( + expect.not.objectContaining({ enableRandomCoast: false }) + ); + expect(generateSpy).toHaveBeenCalledWith( + expect.objectContaining({ maxSpeed: 30 }) + ); + expect(warn).toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/hooks/useSimulationMode.ts b/src/hooks/useSimulationMode.ts index aa9e127b77..9f0866845a 100644 --- a/src/hooks/useSimulationMode.ts +++ b/src/hooks/useSimulationMode.ts @@ -2,13 +2,12 @@ import * as Location from 'expo-location'; import getDistance from 'geolib/es/getDistance'; import { useAtomValue } from 'jotai'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import type { - GetTrainRouteQuery, - GetTrainRouteQueryVariables, -} from '~/@types/graphql'; +import type { GetTrainRouteQueryVariables } from '~/@types/graphql'; import { LOCATION_TASK_NAME } from '~/constants'; import { GET_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_CONNECTED_TRAIN_ROUTE, + GET_ESTIMATED_TRAIN_ROUTE, GET_TRAIN_ROUTE, } from '~/lib/graphql/queries'; import { store } from '~/store'; @@ -20,6 +19,12 @@ import { buildRouteLegInputs, type RouteLegInput, } from '~/utils/currentLineGroupStations'; +import { + planLegTimings, + type StopTiming, + speedProfileForDuration, + toStopTiming, +} from '~/utils/trainRouteTiming'; import { generateTrainSpeedProfile } from '~/utils/trainSpeed'; import { selectedBoundAtom, @@ -37,6 +42,43 @@ import { useLoopLine } from './useLoopLine'; // step のインターバルが1秒間隔のため、ティック数(≒秒数)としてそのまま扱う。 const TERMINAL_DWELL_TICKS = 60; +// 途中の停車駅で止まるティック数の下限。到着の判定を通すため、駅に着いたら +// 少なくともこれだけ止まる。見込みの無い区間(Legacy の値で走る区間や、推定が +// 通過扱いにしている駅)では、この値だけ止まる(見込みに合わせる前と同じ長さ)。 +const MIN_DWELL_TICKS = 2; + +type TrainRouteSegment = { + station?: { + id?: number | null; + groupId?: number | null; + trackDistanceFromPrevious?: number | null; + } | null; + distanceFromPrevious?: number | null; + maxAcceleration?: number | null; + maxDeceleration?: number | null; + maxSpeed?: number | null; + arrivalCumulativeMinutes?: number | null; + departureCumulativeMinutes?: number | null; +}; +type TrainRouteData = { + trainRoute: { segments?: TrainRouteSegment[] | null }; +}; +type ConnectedTrainRouteVariables = { + fromStationId: number; + toStationId: number; + legs: RouteLegInput[]; +}; + +// 乗換駅は前の区間の降車と次の区間の乗車の 2 行で返る。駅リストには降車の行を残し、 +// 出発の見込みだけを乗車の行から取る。乗換の徒歩と待ち時間は、その駅での停車になる +const mergeJunctionSegments = ( + first: TrainRouteSegment, + last: TrainRouteSegment +): TrainRouteSegment => ({ + ...first, + departureCumulativeMinutes: last.departureCumulativeMinutes, +}); + // locationAtom は書くたびに新しいオブジェクトへ置き換わり、座標を購読する走行画面の // フックが一斉に再評価される。終点での停車や経路の取得待ちの間は毎ティック同じ座標・ // speed=0 を書くことになるため、座標・速度・方位・精度が前回と同じなら書かない。 @@ -72,6 +114,8 @@ export const useSimulationMode = (): void => { const segmentIndexRef = useRef(0); const childIndexRef = useRef(0); const speedProfilesRef = useRef([]); + // 区間ごとの、着いた駅で止まるティック数(終点は使わない) + const dwellTicksRef = useRef([]); const segmentProgressDistanceRef = useRef(0); const dwellPendingRef = useRef(false); // 終点到着後、方面を逆転して折り返すまで終点で停車し続けたティック数 @@ -136,59 +180,73 @@ export const useSimulationMode = (): void => { toStationId != null && fromStationId !== toStationId; - const { data: singleTrainRouteData, error: singleTrainRouteError } = - useGraphQLQuery( - GET_TRAIN_ROUTE, + // 区間の値は到着時間推定のモデル(model: Estimated)で取り、ETA と同じ時間で走らせる。 + // model を知らない StationAPI ではクエリ全体がエラーになるので、そのときだけ + // 従来の問い合わせ(Legacy)に戻す + const singleVariables = { + fromStationId: fromStationId ?? 0, + toStationId: toStationId ?? 0, + lineGroupId: trainType?.groupId, + }; + const connectedVariables: ConnectedTrainRouteVariables = { + fromStationId: routeLegs?.[0]?.fromStationId ?? 0, + toStationId: routeLegs?.at(-1)?.toStationId ?? 0, + legs: routeLegs ?? [], + }; + const skipSingle = !canFetchTrainRoute || !!routeLegs; + const skipConnected = !canFetchTrainRoute || !routeLegs; + + const { data: estimatedSingleData, error: estimatedSingleError } = + useGraphQLQuery( + GET_ESTIMATED_TRAIN_ROUTE, + { variables: singleVariables, skip: skipSingle } + ); + const { data: legacySingleData, error: legacySingleError } = useGraphQLQuery< + TrainRouteData, + GetTrainRouteQueryVariables + >(GET_TRAIN_ROUTE, { + variables: singleVariables, + skip: skipSingle || !estimatedSingleError, + }); + const { data: estimatedConnectedData, error: estimatedConnectedError } = + useGraphQLQuery( + GET_ESTIMATED_CONNECTED_TRAIN_ROUTE, + { variables: connectedVariables, skip: skipConnected } + ); + const { data: legacyConnectedData, error: legacyConnectedError } = + useGraphQLQuery( + GET_CONNECTED_TRAIN_ROUTE, { - variables: { - fromStationId: fromStationId ?? 0, - toStationId: toStationId ?? 0, - lineGroupId: trainType?.groupId, - }, - skip: !canFetchTrainRoute || !!routeLegs, + variables: connectedVariables, + skip: skipConnected || !estimatedConnectedError, } ); - const { data: connectedTrainRouteData, error: connectedTrainRouteError } = - useGraphQLQuery< - { - trainRoute: { - segments: - | (Omit< - NonNullable< - GetTrainRouteQuery['trainRoute']['segments'] - >[number], - 'station' - > & { - station: { - id?: number | null; - groupId?: number | null; - trackDistanceFromPrevious?: number | null; - } | null; - })[] - | null; - }; - }, - { fromStationId: number; toStationId: number; legs: RouteLegInput[] } - >(GET_CONNECTED_TRAIN_ROUTE, { - variables: { - fromStationId: routeLegs?.[0]?.fromStationId ?? 0, - toStationId: routeLegs?.at(-1)?.toStationId ?? 0, - legs: routeLegs ?? [], - }, - skip: !canFetchTrainRoute || !routeLegs, - }); - const trainRouteError = routeLegs - ? connectedTrainRouteError - : singleTrainRouteError; + + const singleTrainRouteData: TrainRouteData | undefined = estimatedSingleError + ? legacySingleData + : estimatedSingleData; + const connectedTrainRouteData = estimatedConnectedError + ? legacyConnectedData + : estimatedConnectedData; + const estimatedTrainRouteError = routeLegs + ? estimatedConnectedError + : estimatedSingleError; + const legacyTrainRouteError = routeLegs + ? legacyConnectedError + : legacySingleError; // 区間を渡した trainRoute は乗換駅などを 2 回含むので、進行順の駅リストに揃えてから使う - const trainRouteSegments = useMemo(() => { + const trainRouteSegments = useMemo((): TrainRouteSegment[] | null => { if (!routeLegs) { return singleTrainRouteData?.trainRoute?.segments ?? null; } const segments = connectedTrainRouteData?.trainRoute?.segments; return segments - ? alignConnectedTrainRouteSegments(segments, maybeRevsersedStations) + ? alignConnectedTrainRouteSegments( + segments, + maybeRevsersedStations, + mergeJunctionSegments + ) : null; }, [ routeLegs, @@ -251,15 +309,24 @@ export const useSimulationMode = (): void => { stopLocationUpdates(); }, [enabled]); - // trainRoute クエリのエラーをログに記録 + // trainRoute クエリのエラーをログに記録。Estimated のエラーは Legacy に戻して + // 走り続けられるので警告に留める useEffect(() => { - if (trainRouteError) { + if (estimatedTrainRouteError) { + console.warn( + '[useSimulationMode] trainRoute(model: Estimated) query error; falling back to Legacy:', + estimatedTrainRouteError + ); + } + }, [estimatedTrainRouteError]); + useEffect(() => { + if (legacyTrainRouteError) { console.error( '[useSimulationMode] trainRoute query error:', - trainRouteError + legacyTrainRouteError ); } - }, [trainRouteError]); + }, [legacyTrainRouteError]); useEffect(() => { const segments = trainRouteSegments; @@ -268,6 +335,7 @@ export const useSimulationMode = (): void => { // 旧経路のプロファイル/ジオメトリを残すと、新しい駅リストの位置で旧経路を // 走ってしまう。消しておけば、タイマーは新しいプロファイルが揃うまで停車して待つ speedProfilesRef.current = []; + dwellTicksRef.current = []; segmentGeometryCacheRef.current = []; return; } @@ -280,12 +348,39 @@ export const useSimulationMode = (): void => { const segmentGeometry: SegmentGeometry[] = new Array( maybeRevsersedStations.length ); + const dwellTicks: number[] = new Array(maybeRevsersedStations.length).fill( + MIN_DWELL_TICKS + ); const emptyGeometry: SegmentGeometry = { waypoints: [], cumulativeDistances: [], totalDistance: 0, }; + // 到着・出発の見込み(model: Estimated)があれば、停車駅から次の停車駅までを + // その時間で走り切る巡航速度で走り、推定の停車時間だけ止まる。ETA と同じ時間に + // なる。1 駅でも見込みが無ければ(Legacy の値・バスの経路)、従来どおり + // 区間の最高速度で走る + const stopIndices: number[] = []; + maybeRevsersedStations.forEach((s, i) => { + if (s && !getIsPass(s)) { + stopIndices.push(i); + } + }); + const timings: (StopTiming | null)[] = segments.map(toStopTiming); + const legTimings = + timings.length === maybeRevsersedStations.length + ? planLegTimings(timings, stopIndices) + : null; + const legTimingByStart = new Map( + (legTimings ?? []).map((leg, j) => [stopIndices[j], leg]) + ); + // 始点の出発からの、計画上の出発時刻と、実際に走らせた時間(秒 = ティック)。 + // 区間ごとの 1 秒未満の端数は、計画上の到着時刻に合わせて次の区間で吸収するので、 + // 終点までに積み上がらない + let plannedDepartureSec = 0; + let elapsedSec = 0; + for ( let curMapIndex = 0; curMapIndex < maybeRevsersedStations.length; @@ -327,6 +422,12 @@ export const useSimulationMode = (): void => { ) { speedProfiles[curMapIndex] = []; segmentGeometry[curMapIndex] = emptyGeometry; + // 走らせない区間のぶん、計画の時刻を進めて次の区間の基準にする + const skippedLeg = legTimingByStart.get(curMapIndex); + if (skippedLeg) { + plannedDepartureSec += skippedLeg.runSec + (skippedLeg.dwellSec ?? 0); + elapsedSec = plannedDepartureSec; + } continue; } @@ -367,13 +468,39 @@ export const useSimulationMode = (): void => { totalDistance: distanceForNextStation, }; - const speedProfile = generateTrainSpeedProfile({ - distance: distanceForNextStation, - maxSpeed: arrivalSegment.maxSpeed, - accel: arrivalSegment.maxAcceleration, - decel: arrivalSegment.maxDeceleration, - interval: 1, - }); + const legTiming = legTimingByStart.get(curMapIndex); + const isFinalLeg = nextStationIndex === stopIndices.at(-1); + let timedProfile: number[] | null = null; + if (legTiming) { + const plannedArrivalSec = plannedDepartureSec + legTiming.runSec; + plannedDepartureSec = plannedArrivalSec + (legTiming.dwellSec ?? 0); + timedProfile = + speedProfileForDuration({ + distance: distanceForNextStation, + seconds: plannedArrivalSec - elapsedSec, + accel: arrivalSegment.maxAcceleration, + decel: arrivalSegment.maxDeceleration, + })?.profile ?? null; + // 推定が通過扱いにしている駅では停車時間の見込みが無いので、下限だけ止まる + if (!isFinalLeg) { + dwellTicks[curMapIndex] = Math.max( + MIN_DWELL_TICKS, + legTiming.dwellSec ?? 0 + ); + } + } + + const speedProfile = + timedProfile ?? + generateTrainSpeedProfile({ + distance: distanceForNextStation, + maxSpeed: arrivalSegment.maxSpeed, + accel: arrivalSegment.maxAcceleration, + decel: arrivalSegment.maxDeceleration, + interval: 1, + }); + elapsedSec += + speedProfile.length + (isFinalLeg ? 0 : dwellTicks[curMapIndex]); let profileDistance = 0; for (let i = 0; i < speedProfile.length; i++) { @@ -393,6 +520,7 @@ export const useSimulationMode = (): void => { segmentIndexRef.current = resolveStartIndex(); speedProfilesRef.current = speedProfiles; + dwellTicksRef.current = dwellTicks; segmentGeometryCacheRef.current = segmentGeometry; childIndexRef.current = 0; segmentProgressDistanceRef.current = 0; @@ -645,6 +773,21 @@ export const useSimulationMode = (): void => { } if (i >= speeds.length) { + // 駅に着いてから dwellTicks だけ止まる。最後の 2 ティックは、停車の印を立てる + // ティックと次の区間へ移るティック(上の dwellPendingRef の分岐)にあたる + const dwell = + dwellTicksRef.current[segmentIndexRef.current] ?? MIN_DWELL_TICKS; + if (i - speeds.length < dwell - MIN_DWELL_TICKS) { + const prev = store.get(locationAtom); + if (prev) { + setSimulatedLocation({ + timestamp: Date.now(), + coords: { ...prev.coords, speed: 0, heading: null }, + }); + } + childIndexRef.current += 1; + return; + } dwellPendingRef.current = true; return; } diff --git a/src/lib/graphql/queries.ts b/src/lib/graphql/queries.ts index 98a4a42502..af4144c0b6 100644 --- a/src/lib/graphql/queries.ts +++ b/src/lib/graphql/queries.ts @@ -581,6 +581,67 @@ export const GET_CONNECTED_TRAIN_ROUTE = gql` } `; +// Same as GET_TRAIN_ROUTE / GET_CONNECTED_TRAIN_ROUTE but with the arrival estimation's +// model (model: Estimated): the arrival/departure estimates equal estimateArrivalTimes, +// so the auto mode runs on the same times as the ETA. Kept as separate documents because +// declaring model makes the whole query fail validation on an API that does not know +// TrainRouteModel; useSimulationMode falls back to the queries above on an error. +export const GET_ESTIMATED_TRAIN_ROUTE = gql` + query GetEstimatedTrainRoute( + $fromStationId: Int! + $toStationId: Int! + $lineGroupId: Int + ) { + trainRoute( + fromStationId: $fromStationId + toStationId: $toStationId + lineGroupId: $lineGroupId + model: Estimated + ) { + segments { + station { + trackDistanceFromPrevious + } + distanceFromPrevious + maxAcceleration + maxDeceleration + maxSpeed + arrivalCumulativeMinutes + departureCumulativeMinutes + } + } + } +`; + +export const GET_ESTIMATED_CONNECTED_TRAIN_ROUTE = gql` + query GetEstimatedConnectedTrainRoute( + $fromStationId: Int! + $toStationId: Int! + $legs: [RouteLegInput!]! + ) { + trainRoute( + fromStationId: $fromStationId + toStationId: $toStationId + legs: $legs + model: Estimated + ) { + segments { + station { + id + groupId + trackDistanceFromPrevious + } + distanceFromPrevious + maxAcceleration + maxDeceleration + maxSpeed + arrivalCumulativeMinutes + departureCumulativeMinutes + } + } + } +`; + // Query for estimating arrival times between two stations export const ESTIMATE_ARRIVAL_TIMES = gql` query EstimateArrivalTimes( diff --git a/src/store/atoms/location.subwayGpx.test.ts b/src/store/atoms/location.subwayGpx.test.ts index 26278f5cbc..8cf596386f 100644 --- a/src/store/atoms/location.subwayGpx.test.ts +++ b/src/store/atoms/location.subwayGpx.test.ts @@ -253,7 +253,7 @@ describe('FLinerSeibu.gpx の形', () => { const gaps = gapsOf(track); expect(gaps.length).toBe(7); expect(gaps.filter((g) => g > ETA_BOUND_MAX_HOLD_MS).length).toBe(5); - expect(Math.max(...gaps)).toBe(326_000); + expect(Math.max(...gaps)).toBe(327_000); }); it('地下から地上への復帰を2回含む', () => { diff --git a/src/utils/currentLineGroupStations.test.ts b/src/utils/currentLineGroupStations.test.ts index d363531cb6..975bf4d30a 100644 --- a/src/utils/currentLineGroupStations.test.ts +++ b/src/utils/currentLineGroupStations.test.ts @@ -200,6 +200,29 @@ describe('乗換経路の区間指定', () => { ).toEqual(['東京', '京都', '新大阪(東海道)', '新神戸']); }); + it('merge を渡すと、同じ駅グループが続く 1 つ目と最後の 1 つから 1 つを作る', () => { + // 乗換駅の出発の見込みは、次の区間の乗車側(最後)の行にある + const stations = [group(1), group(2), group(3), group(4)]; + const aligned = alignConnectedTrainRouteSegments( + [ + segment('東京', 1), + segment('京都', 2), + segment('新大阪(東海道)', 3), + segment('新大阪(山陽、系統の中)', 3), + segment('新大阪(山陽、次の区間)', 3), + segment('新神戸', 4), + ], + stations, + (first, last) => ({ ...first, name: `${first.name}→${last.name}` }) + ); + expect(names(aligned)).toEqual([ + '東京', + '京都', + '新大阪(東海道)→新大阪(山陽、次の区間)', + '新神戸', + ]); + }); + it('駅リストと突き合わせられなければ null を返す', () => { const stations = [group(1), group(2), group(3)]; // 途中の駅が欠けている diff --git a/src/utils/currentLineGroupStations.ts b/src/utils/currentLineGroupStations.ts index b72f609212..961f6d95fa 100644 --- a/src/utils/currentLineGroupStations.ts +++ b/src/utils/currentLineGroupStations.ts @@ -114,13 +114,16 @@ export const buildRouteLegInputs = ( * 件数の推定で揃えると、区間の切り出し方が API とずれたときに位置がずれる * @param segments trainRoute の segments(進行順の区間ごとに連結されたもの) * @param stations 進行順の駅リスト(同じ駅グループが続かないもの) + * @param merge 同じ駅グループが続くとき、残す 1 つ目と最後の 1 つから 1 つを作る。 + * 乗換駅の出発の見込みは乗車側(最後)の行にあるので、それを拾うために使う * @returns 駅リストと同じ長さの segments。突き合わせられなければ null */ export const alignConnectedTrainRouteSegments = < T extends { station?: { groupId?: number | null } | null }, >( segments: T[], - stations: Station[] + stations: Station[], + merge?: (first: T, last: T) => T ): T[] | null => { const aligned: T[] = []; let cursor = 0; @@ -132,11 +135,13 @@ export const alignConnectedTrainRouteSegments = < ) { return null; } - aligned.push(segment); + let last = segment; cursor++; while (segments[cursor]?.station?.groupId === station.groupId) { + last = segments[cursor]; cursor++; } + aligned.push(merge && last !== segment ? merge(segment, last) : segment); } return cursor === segments.length ? aligned : null; }; diff --git a/src/utils/trainRouteTiming.test.ts b/src/utils/trainRouteTiming.test.ts new file mode 100644 index 0000000000..66e70024e3 --- /dev/null +++ b/src/utils/trainRouteTiming.test.ts @@ -0,0 +1,141 @@ +import { + cruiseSpeedForDuration, + planLegTimings, + speedProfileForDuration, + toStopTiming, +} from './trainRouteTiming'; + +const trapezoidSeconds = ( + distance: number, + v: number, + a: number, + b: number +) => { + const k = 0.5 / a + 0.5 / b; + return distance >= v * v * k + ? distance / v + v * k + : 2 * Math.sqrt(distance * k); +}; + +const at = (arrivalMin: number, departureMin: number) => ({ + arrivalSec: arrivalMin * 60, + departureSec: departureMin * 60, +}); + +describe('cruiseSpeedForDuration', () => { + it('台形で走ったときに求めた時間になる速度を返す', () => { + for (const [distance, seconds] of [ + [4843, 300], + [1627, 150], + [300, 60], + ]) { + const fitted = cruiseSpeedForDuration(distance, seconds, 0.7, 0.9); + expect(fitted?.shortened).toBe(false); + expect( + trapezoidSeconds(distance, fitted?.speed ?? 0, 0.7, 0.9) + ).toBeCloseTo(seconds, 6); + } + }); + + it('最短時間より短い時間なら最短時間で走る速度にする', () => { + const k = 0.5 / 0.7 + 0.5 / 0.9; + const fitted = cruiseSpeedForDuration(2000, 10, 0.7, 0.9); + expect(fitted?.shortened).toBe(true); + expect(fitted?.speed).toBeCloseTo(Math.sqrt(2000 / k), 9); + }); + + it('距離と加減速が正でなければ決められない', () => { + expect(cruiseSpeedForDuration(0, 60, 0.7, 0.9)).toBeNull(); + expect(cruiseSpeedForDuration(1000, 60, 0, 0.9)).toBeNull(); + }); +}); + +describe('planLegTimings', () => { + it('区間の時間は、次の停車駅の到着とこの駅の出発の差', () => { + expect(planLegTimings([at(0, 0), at(2, 2.6), at(5, 5)], [0, 1, 2])).toEqual( + [ + { runSec: 120, dwellSec: 36 }, + { runSec: 144, dwellSec: 0 }, + ] + ); + }); + + it('呼び出し側で通過する駅に推定だけが停まるときは、その停車時間を走行から引く', () => { + expect(planLegTimings([at(0, 0), at(2, 2.6), at(5, 5)], [0, 2])).toEqual([ + { runSec: 264, dwellSec: 0 }, + ]); + }); + + it('呼び出し側で停まる駅を推定が通過していたら、停車時間は null', () => { + expect(planLegTimings([at(0, 0), at(2, 2), at(5, 5)], [0, 1, 2])).toEqual([ + { runSec: 120, dwellSec: null }, + { runSec: 180, dwellSec: 0 }, + ]); + expect( + planLegTimings([at(0, 0), at(2, 2), at(5, 5)], [0, 1, 2], 30) + ).toEqual([ + { runSec: 120, dwellSec: 30 }, + { runSec: 180, dwellSec: 0 }, + ]); + }); + + it('見込みの無い駅があれば null', () => { + expect(planLegTimings([at(0, 0), null], [0, 1])).toBeNull(); + }); +}); + +describe('toStopTiming', () => { + it('累積分を秒にし、どちらかが無ければ null', () => { + expect( + toStopTiming({ + arrivalCumulativeMinutes: 1.5, + departureCumulativeMinutes: 2, + }) + ).toEqual({ arrivalSec: 90, departureSec: 120 }); + expect( + toStopTiming({ + arrivalCumulativeMinutes: 1.5, + departureCumulativeMinutes: null, + }) + ).toBeNull(); + expect(toStopTiming(null)).toBeNull(); + }); +}); + +describe('speedProfileForDuration', () => { + it('走り切れる時間なら、速度列の長さを求めた秒数に合わせ、距離も走り切る', () => { + for (const [distance, accel, decel] of [ + [800, 1.0, 1.0], + [2100, 0.7, 0.9], + [5000, 0.7, 0.9], + [30000, 0.4, 0.7], + ]) { + const k = 0.5 / accel + 0.5 / decel; + const fastest = Math.ceil(2 * Math.sqrt(distance * k)); + for (const extra of [3, 10, 37, 120]) { + const seconds = fastest + extra; + const result = speedProfileForDuration({ + distance, + seconds, + accel, + decel, + }); + expect(result?.shortened).toBe(false); + expect(result?.profile).toHaveLength(seconds); + const total = (result?.profile ?? []).reduce((sum, v) => sum + v, 0); + expect(total).toBeCloseTo(distance, 6); + } + } + }); + + it('最短時間より短い時間なら最短の列を返して shortened を立てる', () => { + const result = speedProfileForDuration({ + distance: 2000, + seconds: 10, + accel: 0.7, + decel: 0.9, + }); + expect(result?.shortened).toBe(true); + expect(result?.profile.length).toBeGreaterThan(10); + }); +}); diff --git a/src/utils/trainRouteTiming.ts b/src/utils/trainRouteTiming.ts new file mode 100644 index 0000000000..a5b3c2b7a7 --- /dev/null +++ b/src/utils/trainRouteTiming.ts @@ -0,0 +1,186 @@ +// trainRoute の Estimated が返す到着・出発の見込みに合わせて走らせるための計算。 +// オートモード (useSimulationMode) と GPX の生成 (scripts/generate-location-gpx.mjs) が +// 同じ関数を使い、どちらも ETA (estimateArrivalTimes) と同じ時間で走る。 + +// scripts/generate-location-gpx.mjs が Node から直接読むので、このファイルは何も import しない +// (Node は拡張子の無い相対 import を解決できない)。 + +export type StopTiming = { + /** 始点からの到着の見込み (秒) */ + arrivalSec: number; + /** 始点からの出発の見込み (秒) */ + departureSec: number; +}; + +export type LegTiming = { + /** 停車駅から次の停車駅までの走行時間 (秒) */ + runSec: number; + /** + * 着いた駅での停車時間 (秒)。終点は 0。推定がその駅を通過扱いにしていて + * 見込みが無いときは null + */ + dwellSec: number | null; +}; + +/** + * 距離 distance(m) を加速度 accel・減速度 decel の台形 (短い区間は三角形) で + * seconds 秒かけて走る巡航速度 (m/s)。台形の所要時間は T(v) = D/v + k·v + * (k = 1/(2a) + 1/(2b)) なので、T(v) = seconds の 2 根のうち遅い方 (巡航のある + * 台形) を返す。最短時間 2√(D·k) より短い時間を求められたときは解が無いので、 + * 最短時間で走る速度 √(D/k) を返し、shortened を立てる。 + */ +export const cruiseSpeedForDuration = ( + distance: number, + seconds: number, + accel: number, + decel: number +): { speed: number; shortened: boolean } | null => { + const valid = (x: number) => Number.isFinite(x) && x > 0; + if (!(valid(distance) && valid(accel) && valid(decel))) { + return null; + } + const k = 0.5 / accel + 0.5 / decel; + const fastest = Math.sqrt(distance / k); + const discriminant = seconds * seconds - 4 * k * distance; + if (!Number.isFinite(seconds) || discriminant <= 0) { + return { speed: fastest, shortened: true }; + } + return { + speed: (seconds - Math.sqrt(discriminant)) / (2 * k), + shortened: false, + }; +}; + +/** + * 停車駅ごとの走行時間と停車時間 (秒) を、駅ごとの見込みから組み立てる。 + * 戻り値の [j] は stopIndices[j] から stopIndices[j + 1] までの走行と、着いた駅での停車。 + * 見込みの無い駅があれば null (バスの経路など、推定のモデルの対象外)。 + * + * 停車駅は呼び出し側の規則 (アプリの isPass など) で決める。推定は平日/休日運転の + * 駅を常に停車扱いにするので、呼び出し側では通過する駅に推定だけが停まることがある。 + * その駅の停車時間 (出発 − 到着) は走行時間から引く。逆に、呼び出し側で停まる駅を + * 推定が通過していると停車時間の見込みが無いので、dwellSec を null にする。 + * dwellOverrideSec を渡すと、途中の停車駅の停車時間をすべてその値にする。 + */ +export const planLegTimings = ( + timings: (StopTiming | null | undefined)[], + stopIndices: number[], + dwellOverrideSec?: number +): LegTiming[] | null => { + const known: StopTiming[] = []; + for (const timing of timings) { + if (timing == null) { + return null; + } + known.push(timing); + } + const dwellAt = (i: number) => known[i].departureSec - known[i].arrivalSec; + return stopIndices.slice(1).map((to, j) => { + const from = stopIndices[j]; + let runSec = known[to].arrivalSec - known[from].departureSec; + for (let i = from + 1; i < to; i++) { + runSec -= dwellAt(i); + } + const isFinal = j === stopIndices.length - 2; + if (isFinal) { + return { runSec, dwellSec: 0 }; + } + if (dwellOverrideSec !== undefined) { + return { runSec, dwellSec: dwellOverrideSec }; + } + const dwellSec = Math.round(dwellAt(to)); + return { runSec, dwellSec: dwellSec > 0 ? dwellSec : null }; + }); +}; + +/** + * trainRoute の segment の累積分 (arrivalCumulativeMinutes / departureCumulativeMinutes) + * を秒の見込みにする。どちらかが無ければ null (Legacy の値や、バスの経路)。 + */ +export const toStopTiming = ( + segment: + | { + arrivalCumulativeMinutes?: number | null; + departureCumulativeMinutes?: number | null; + } + | null + | undefined +): StopTiming | null => { + const arrival = segment?.arrivalCumulativeMinutes; + const departure = segment?.departureCumulativeMinutes; + if ( + arrival == null || + departure == null || + !Number.isFinite(arrival) || + !Number.isFinite(departure) + ) { + return null; + } + return { arrivalSec: arrival * 60, departureSec: departure * 60 }; +}; + +/** + * 距離 distance(m) を加速度 accel・減速度 decel の台形で seconds 秒かけて走るときの、 + * 1 秒ごとの進み (m。1 秒あたりなので速度 m/s と同じ値)。 + * + * 台形の位置 x(t) から x(t) − x(t − 1) を並べるので、列の長さは seconds (整数に丸めた値) + * ちょうどで、合計は distance になる。generateTrainSpeedProfile は加速・巡航・減速を + * それぞれ 1 秒単位で切り上げて並べるため、同じ巡航速度でも列の長さが式の時間から + * 1〜2 秒ずれ、巡航速度を選び直しても合わないことがある。 + * 最短時間より短い時間は走り切れないので、最短時間 (三角形) の列を返して shortened を + * 立てる。加減速が決まらないときや距離が 0 のときは null。 + */ +export const speedProfileForDuration = ({ + distance, + seconds, + accel, + decel, +}: { + distance: number; + seconds: number; + accel: number; + decel: number; +}): { profile: number[]; shortened: boolean } | null => { + const fitted = cruiseSpeedForDuration( + distance, + Math.round(seconds), + accel, + decel + ); + if (!fitted) { + return null; + } + const v = fitted.speed; + const accelTime = v / accel; + const decelTime = v / decel; + const accelDistance = (v * v) / (2 * accel); + const decelDistance = (v * v) / (2 * decel); + const cruiseTime = Math.max( + 0, + (distance - accelDistance - decelDistance) / v + ); + const total = accelTime + cruiseTime + decelTime; + const positionAt = (t: number): number => { + if (t <= 0) { + return 0; + } + if (t >= total) { + return distance; + } + if (t <= accelTime) { + return (accel * t * t) / 2; + } + if (t <= accelTime + cruiseTime) { + return accelDistance + v * (t - accelTime); + } + const remaining = total - t; + return distance - (decel * remaining * remaining) / 2; + }; + // 浮動小数の誤差で total が整数をわずかに超えても、1 秒ぶんの点を増やさない + const length = Math.max(1, Math.ceil(total - 1e-6)); + const profile: number[] = []; + for (let t = 1; t <= length; t++) { + profile.push(positionAt(t) - positionAt(t - 1)); + } + return { profile, shortened: fitted.shortened }; +}; From 1b711e8e8b9d20b6a5d4fb9ea5df7131858548f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:30:54 +0900 Subject: [PATCH 3/3] Bump version for canary release (#7160) --- android/app/build.gradle | 6 ++--- android/wearable/build.gradle.kts | 6 ++--- app.config.ts | 4 +-- ios/TrainLCD.xcodeproj/project.pbxproj | 36 +++++++++++++------------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e7834d1b48..c349207777 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -170,12 +170,12 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.dev" versionNameSuffix "-dev" - versionCode 100000831 + versionCode 100000833 versionName "10.17.0" } prod { dimension "environment" - versionCode 100000831 + versionCode 100000833 versionName "10.17.0" } // ストア配信する dev (Canary) と applicationId を分けた、ローカル検証専用のフレーバー。 @@ -186,7 +186,7 @@ android { dimension "environment" applicationId "me.tinykitten.trainlcd.local" versionNameSuffix "-local" - versionCode 100000831 + versionCode 100000833 versionName "10.17.0" } } diff --git a/android/wearable/build.gradle.kts b/android/wearable/build.gradle.kts index 95ccd4fa4b..03baf4fda2 100644 --- a/android/wearable/build.gradle.kts +++ b/android/wearable/build.gradle.kts @@ -85,12 +85,12 @@ android { dimension = "environment" applicationIdSuffix = ".dev" versionNameSuffix = "-dev" - versionCode = 100000832 + versionCode = 100000834 versionName = "10.17.0" } create("prod") { dimension = "environment" - versionCode = 100000832 + versionCode = 100000834 versionName = "10.17.0" } // :app の local フレーバーと対になるローカル検証専用フレーバー。Wear の Data Layer は @@ -100,7 +100,7 @@ android { dimension = "environment" applicationIdSuffix = ".local" versionNameSuffix = "-local" - versionCode = 100000832 + versionCode = 100000834 versionName = "10.17.0" } } diff --git a/app.config.ts b/app.config.ts index b017658d84..95bfa7e484 100644 --- a/app.config.ts +++ b/app.config.ts @@ -64,7 +64,7 @@ export default { userInterfaceStyle: 'automatic', // Expo SDK 57 の各モジュール(expo / expo-modules-core ほか)は podspec で iOS 16.4 以上を要求する deploymentTarget: '16.4', - buildNumber: '2945', + buildNumber: '2946', scheme: IS_DEV ? 'CanaryTrainLCD' : 'ProdTrainLCD', bundleIdentifier: IS_DEV ? 'me.tinykitten.trainlcd.dev' @@ -78,7 +78,7 @@ export default { ? 'me.tinykitten.trainlcd.dev' : 'me.tinykitten.trainlcd', permissions: [], - versionCode: 100000831, + versionCode: 100000833, }, owner: 'trainlcd', experiments: { diff --git a/ios/TrainLCD.xcodeproj/project.pbxproj b/ios/TrainLCD.xcodeproj/project.pbxproj index 360f0a5200..f23b05ca47 100644 --- a/ios/TrainLCD.xcodeproj/project.pbxproj +++ b/ios/TrainLCD.xcodeproj/project.pbxproj @@ -2562,7 +2562,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; @@ -2602,7 +2602,7 @@ CODE_SIGN_ENTITLEMENTS = ProdTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Prod/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TrainLCD; @@ -2661,7 +2661,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2768,7 +2768,7 @@ CODE_SIGN_ENTITLEMENTS = TrainLCD/trainlcd.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2849,7 +2849,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -2889,7 +2889,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryTrainLCD.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; INFOPLIST_FILE = TrainLCD/Schemes/Dev/Info.plist; @@ -3101,7 +3101,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/CanaryRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3153,7 +3153,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3204,7 +3204,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/ProdWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3262,7 +3262,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3313,7 +3313,7 @@ CODE_SIGN_ENTITLEMENTS = WatchWidget/CanaryWatchWidget.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3370,7 +3370,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -3419,7 +3419,7 @@ CODE_SIGN_ENTITLEMENTS = RideSessionActivity/ProdRideSessionActivity.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3471,7 +3471,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -3690,7 +3690,7 @@ CODE_SIGN_ENTITLEMENTS = ProdAppClip/ProdAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3746,7 +3746,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3796,7 +3796,7 @@ CODE_SIGN_ENTITLEMENTS = CanaryAppClip/CanaryAppClip.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -3854,7 +3854,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2945; + CURRENT_PROJECT_VERSION = 2946; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = E6R2G33Z36; ENABLE_USER_SCRIPT_SANDBOXING = NO;