test(06-02): add failing D-16 drive replay suite R1-R12 + four committed drive fixtures (RED)
- guidance-replay.test.mjs: corridor/excursion/missed-turn/jitter replays, storm guard, shortened grace, haptic edges, start gate, projection - fixtures derived from golden pair central-market-to-merdeka-118 (ODbL attribution in each _notice; Fix-shaped streams per D-16)
This commit is contained in:
+1892
File diff suppressed because it is too large
Load Diff
+1892
File diff suppressed because it is too large
Load Diff
+462
@@ -0,0 +1,462 @@
|
|||||||
|
{
|
||||||
|
"_notice": "Synthetic drive replay derived from golden pair central-market-to-merdeka-118 (routing/src/data/route-fixtures.json). Route geometry © OpenStreetMap contributors, ODbL v1.0 — https://opendatacommons.org/licenses/odbl/1-0/. Drive samples are synthetic (D-16 replay fixture; Fix-shaped per web/src/location/types.ts).",
|
||||||
|
"pairId": "central-market-to-merdeka-118",
|
||||||
|
"maneuverIdx": [
|
||||||
|
17,
|
||||||
|
31,
|
||||||
|
47,
|
||||||
|
57,
|
||||||
|
93
|
||||||
|
],
|
||||||
|
"kinds": [
|
||||||
|
"left",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"right",
|
||||||
|
"left"
|
||||||
|
],
|
||||||
|
"note": "straight past maneuver #17 (135 m); reroute requested under the shortened missed-turn grace",
|
||||||
|
"missedTurnIdx": 17,
|
||||||
|
"fixes": [
|
||||||
|
{
|
||||||
|
"lat": 3.145045,
|
||||||
|
"lng": 101.69526,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 96.5,
|
||||||
|
"ts": 1760000000000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450365,
|
||||||
|
"lng": 101.6953346,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 96.8,
|
||||||
|
"ts": 1760000001000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450277,
|
||||||
|
"lng": 101.6954091,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000002000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450175,
|
||||||
|
"lng": 101.6954835,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000003000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450073,
|
||||||
|
"lng": 101.6955578,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000004000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449971,
|
||||||
|
"lng": 101.6956322,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 159.5,
|
||||||
|
"ts": 1760000005000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449382,
|
||||||
|
"lng": 101.6956542,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 184,
|
||||||
|
"ts": 1760000006000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1448635,
|
||||||
|
"lng": 101.6956489,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 193.5,
|
||||||
|
"ts": 1760000007000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447916,
|
||||||
|
"lng": 101.6956316,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 217.5,
|
||||||
|
"ts": 1760000008000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447321,
|
||||||
|
"lng": 101.6955859,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 218.8,
|
||||||
|
"ts": 1760000009000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446737,
|
||||||
|
"lng": 101.695539,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 220.7,
|
||||||
|
"ts": 1760000010000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446169,
|
||||||
|
"lng": 101.69549,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 222.6,
|
||||||
|
"ts": 1760000011000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1445619,
|
||||||
|
"lng": 101.6954394,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 232,
|
||||||
|
"ts": 1760000012000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1445158,
|
||||||
|
"lng": 101.6953803,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 210.2,
|
||||||
|
"ts": 1760000013000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1444577,
|
||||||
|
"lng": 101.6953465,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 154.8,
|
||||||
|
"ts": 1760000014000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443918,
|
||||||
|
"lng": 101.6953776,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 133.5,
|
||||||
|
"ts": 1760000015000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443402,
|
||||||
|
"lng": 101.695432,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 53.2,
|
||||||
|
"ts": 1760000016000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.14428,
|
||||||
|
"lng": 101.695497,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000017000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1442289,
|
||||||
|
"lng": 101.6955519,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000018000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1441779,
|
||||||
|
"lng": 101.6956069,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000019000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1441269,
|
||||||
|
"lng": 101.6956619,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000020000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1440758,
|
||||||
|
"lng": 101.6957168,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000021000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1440248,
|
||||||
|
"lng": 101.6957718,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000022000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1439737,
|
||||||
|
"lng": 101.6958267,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000023000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1439227,
|
||||||
|
"lng": 101.6958817,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000024000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1438717,
|
||||||
|
"lng": 101.6959367,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000025000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1438206,
|
||||||
|
"lng": 101.6959916,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 132.9,
|
||||||
|
"ts": 1760000026000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1441042,
|
||||||
|
"lng": 101.6960488,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000027000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443879,
|
||||||
|
"lng": 101.6961061,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000028000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446715,
|
||||||
|
"lng": 101.6961633,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000029000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449552,
|
||||||
|
"lng": 101.6962206,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000030000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452388,
|
||||||
|
"lng": 101.6962778,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000031000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1455224,
|
||||||
|
"lng": 101.696335,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000032000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458061,
|
||||||
|
"lng": 101.6963923,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000033000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460897,
|
||||||
|
"lng": 101.6964495,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000034000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1463734,
|
||||||
|
"lng": 101.6965068,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000035000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.146657,
|
||||||
|
"lng": 101.696564,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 11.4,
|
||||||
|
"ts": 1760000036000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1466448,
|
||||||
|
"lng": 101.6965684,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 346.6,
|
||||||
|
"ts": 1760000037000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1467166,
|
||||||
|
"lng": 101.6965512,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 3.6,
|
||||||
|
"ts": 1760000038000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1467914,
|
||||||
|
"lng": 101.6965558,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 72.1,
|
||||||
|
"ts": 1760000039000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.14681,
|
||||||
|
"lng": 101.6966134,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 77.1,
|
||||||
|
"ts": 1760000040000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468267,
|
||||||
|
"lng": 101.6966865,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 74.7,
|
||||||
|
"ts": 1760000041000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468465,
|
||||||
|
"lng": 101.6967588,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 74.7,
|
||||||
|
"ts": 1760000042000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468662,
|
||||||
|
"lng": 101.6968312,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 73,
|
||||||
|
"ts": 1760000043000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468881,
|
||||||
|
"lng": 101.696903,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 69.7,
|
||||||
|
"ts": 1760000044000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469141,
|
||||||
|
"lng": 101.6969734,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 65.6,
|
||||||
|
"ts": 1760000045000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469449,
|
||||||
|
"lng": 101.6970416,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 50.9,
|
||||||
|
"ts": 1760000046000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.146992,
|
||||||
|
"lng": 101.6970995,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 40,
|
||||||
|
"ts": 1760000047000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470494,
|
||||||
|
"lng": 101.6971478,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 74.6,
|
||||||
|
"ts": 1760000048000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470639,
|
||||||
|
"lng": 101.6972006,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 127.9,
|
||||||
|
"ts": 1760000049000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470179,
|
||||||
|
"lng": 101.6972598,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 128.3,
|
||||||
|
"ts": 1760000050000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469714,
|
||||||
|
"lng": 101.6973187,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 128.4,
|
||||||
|
"ts": 1760000051000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469249,
|
||||||
|
"lng": 101.6973775,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 128,
|
||||||
|
"ts": 1760000052000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468787,
|
||||||
|
"lng": 101.6974366,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 127.4,
|
||||||
|
"ts": 1760000053000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468332,
|
||||||
|
"lng": 101.6974963,
|
||||||
|
"accuracyM": 8,
|
||||||
|
"headingDeg": 127.4,
|
||||||
|
"ts": 1760000054000,
|
||||||
|
"source": "webview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+950
@@ -0,0 +1,950 @@
|
|||||||
|
{
|
||||||
|
"_notice": "Synthetic drive replay derived from golden pair central-market-to-merdeka-118 (routing/src/data/route-fixtures.json). Route geometry © OpenStreetMap contributors, ODbL v1.0 — https://opendatacommons.org/licenses/odbl/1-0/. Drive samples are synthetic (D-16 replay fixture; Fix-shaped per web/src/location/types.ts).",
|
||||||
|
"pairId": "central-market-to-merdeka-118",
|
||||||
|
"maneuverIdx": [
|
||||||
|
17,
|
||||||
|
31,
|
||||||
|
47,
|
||||||
|
57,
|
||||||
|
93
|
||||||
|
],
|
||||||
|
"kinds": [
|
||||||
|
"left",
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"right",
|
||||||
|
"left"
|
||||||
|
],
|
||||||
|
"note": "excursion 1 detected at fix ts+93s; excursion 2 within STORM_GUARD_MS — suppressed",
|
||||||
|
"excursion1DetectionS": 93,
|
||||||
|
"fixes": [
|
||||||
|
{
|
||||||
|
"lat": 3.145045,
|
||||||
|
"lng": 101.69526,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 96.5,
|
||||||
|
"ts": 1760000000000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450365,
|
||||||
|
"lng": 101.6953346,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 96.8,
|
||||||
|
"ts": 1760000001000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450277,
|
||||||
|
"lng": 101.6954091,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000002000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450175,
|
||||||
|
"lng": 101.6954835,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000003000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450073,
|
||||||
|
"lng": 101.6955578,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 97.8,
|
||||||
|
"ts": 1760000004000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449971,
|
||||||
|
"lng": 101.6956322,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 159.5,
|
||||||
|
"ts": 1760000005000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449382,
|
||||||
|
"lng": 101.6956542,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 184,
|
||||||
|
"ts": 1760000006000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1448635,
|
||||||
|
"lng": 101.6956489,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 193.5,
|
||||||
|
"ts": 1760000007000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447916,
|
||||||
|
"lng": 101.6956316,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 217.5,
|
||||||
|
"ts": 1760000008000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447321,
|
||||||
|
"lng": 101.6955859,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 218.8,
|
||||||
|
"ts": 1760000009000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446737,
|
||||||
|
"lng": 101.695539,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 220.7,
|
||||||
|
"ts": 1760000010000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446169,
|
||||||
|
"lng": 101.69549,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 222.6,
|
||||||
|
"ts": 1760000011000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1445619,
|
||||||
|
"lng": 101.6954394,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 232,
|
||||||
|
"ts": 1760000012000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1445158,
|
||||||
|
"lng": 101.6953803,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 210.2,
|
||||||
|
"ts": 1760000013000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1444577,
|
||||||
|
"lng": 101.6953465,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 154.8,
|
||||||
|
"ts": 1760000014000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443918,
|
||||||
|
"lng": 101.6953776,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 133.5,
|
||||||
|
"ts": 1760000015000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443402,
|
||||||
|
"lng": 101.695432,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 53.2,
|
||||||
|
"ts": 1760000016000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1443776,
|
||||||
|
"lng": 101.695482,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 40.6,
|
||||||
|
"ts": 1760000017000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1444345,
|
||||||
|
"lng": 101.6955308,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 40.6,
|
||||||
|
"ts": 1760000018000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1444914,
|
||||||
|
"lng": 101.6955797,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 41.5,
|
||||||
|
"ts": 1760000019000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1445475,
|
||||||
|
"lng": 101.6956294,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 43.4,
|
||||||
|
"ts": 1760000020000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.144602,
|
||||||
|
"lng": 101.6956809,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 43.4,
|
||||||
|
"ts": 1760000021000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1446565,
|
||||||
|
"lng": 101.6957325,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 45.8,
|
||||||
|
"ts": 1760000022000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447087,
|
||||||
|
"lng": 101.6957861,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 50.7,
|
||||||
|
"ts": 1760000023000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1447562,
|
||||||
|
"lng": 101.6958442,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 53.7,
|
||||||
|
"ts": 1760000024000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1448005,
|
||||||
|
"lng": 101.6959047,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 54.4,
|
||||||
|
"ts": 1760000025000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1448442,
|
||||||
|
"lng": 101.6959657,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 56.6,
|
||||||
|
"ts": 1760000026000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1448854,
|
||||||
|
"lng": 101.6960283,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 57.1,
|
||||||
|
"ts": 1760000027000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449261,
|
||||||
|
"lng": 101.6960914,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 57.1,
|
||||||
|
"ts": 1760000028000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1449668,
|
||||||
|
"lng": 101.6961544,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 57.1,
|
||||||
|
"ts": 1760000029000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450074,
|
||||||
|
"lng": 101.6962175,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 63.5,
|
||||||
|
"ts": 1760000030000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450409,
|
||||||
|
"lng": 101.6962846,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 65.6,
|
||||||
|
"ts": 1760000031000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450718,
|
||||||
|
"lng": 101.6963529,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 68.5,
|
||||||
|
"ts": 1760000032000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1450993,
|
||||||
|
"lng": 101.6964227,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 68.5,
|
||||||
|
"ts": 1760000033000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1451268,
|
||||||
|
"lng": 101.6964925,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 69.1,
|
||||||
|
"ts": 1760000034000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1451536,
|
||||||
|
"lng": 101.6965626,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 69.9,
|
||||||
|
"ts": 1760000035000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1451793,
|
||||||
|
"lng": 101.6966331,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 72.1,
|
||||||
|
"ts": 1760000036000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452023,
|
||||||
|
"lng": 101.6967045,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 74,
|
||||||
|
"ts": 1760000037000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.145223,
|
||||||
|
"lng": 101.6967766,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 76.3,
|
||||||
|
"ts": 1760000038000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452407,
|
||||||
|
"lng": 101.6968496,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 76.7,
|
||||||
|
"ts": 1760000039000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452579,
|
||||||
|
"lng": 101.6969226,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 76.8,
|
||||||
|
"ts": 1760000040000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.145275,
|
||||||
|
"lng": 101.6969957,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 77.1,
|
||||||
|
"ts": 1760000041000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452917,
|
||||||
|
"lng": 101.6970689,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 77.1,
|
||||||
|
"ts": 1760000042000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453085,
|
||||||
|
"lng": 101.697142,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 77.1,
|
||||||
|
"ts": 1760000043000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453252,
|
||||||
|
"lng": 101.6972152,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 79,
|
||||||
|
"ts": 1760000044000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453394,
|
||||||
|
"lng": 101.6972888,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 81.8,
|
||||||
|
"ts": 1760000045000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453501,
|
||||||
|
"lng": 101.6973631,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 33.8,
|
||||||
|
"ts": 1760000046000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453846,
|
||||||
|
"lng": 101.6973862,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 328.1,
|
||||||
|
"ts": 1760000047000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1454482,
|
||||||
|
"lng": 101.6973465,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 324.2,
|
||||||
|
"ts": 1760000048000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1455089,
|
||||||
|
"lng": 101.6973027,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 323.6,
|
||||||
|
"ts": 1760000049000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1455693,
|
||||||
|
"lng": 101.6972582,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 324.3,
|
||||||
|
"ts": 1760000050000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1456302,
|
||||||
|
"lng": 101.6972144,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 324.3,
|
||||||
|
"ts": 1760000051000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.145691,
|
||||||
|
"lng": 101.6971707,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 326,
|
||||||
|
"ts": 1760000052000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1457531,
|
||||||
|
"lng": 101.6971287,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 326,
|
||||||
|
"ts": 1760000053000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458153,
|
||||||
|
"lng": 101.6970867,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 326,
|
||||||
|
"ts": 1760000054000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458774,
|
||||||
|
"lng": 101.6970447,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 325.7,
|
||||||
|
"ts": 1760000055000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459393,
|
||||||
|
"lng": 101.6970024,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 323.6,
|
||||||
|
"ts": 1760000056000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459996,
|
||||||
|
"lng": 101.696958,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 316.5,
|
||||||
|
"ts": 1760000057000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460539,
|
||||||
|
"lng": 101.6969064,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 316.5,
|
||||||
|
"ts": 1760000058000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1461083,
|
||||||
|
"lng": 101.6968548,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 317.2,
|
||||||
|
"ts": 1760000059000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1461633,
|
||||||
|
"lng": 101.6968038,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 321.7,
|
||||||
|
"ts": 1760000060000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1462221,
|
||||||
|
"lng": 101.6967573,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 328.5,
|
||||||
|
"ts": 1760000061000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.146286,
|
||||||
|
"lng": 101.6967181,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 332,
|
||||||
|
"ts": 1760000062000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1463522,
|
||||||
|
"lng": 101.6966829,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 334.5,
|
||||||
|
"ts": 1760000063000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1464198,
|
||||||
|
"lng": 101.6966506,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 336.7,
|
||||||
|
"ts": 1760000064000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1464886,
|
||||||
|
"lng": 101.6966209,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 340.3,
|
||||||
|
"ts": 1760000065000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1465591,
|
||||||
|
"lng": 101.6965957,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 342.7,
|
||||||
|
"ts": 1760000066000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1466307,
|
||||||
|
"lng": 101.6965734,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 342,
|
||||||
|
"ts": 1760000067000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1467017,
|
||||||
|
"lng": 101.6965502,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 3.6,
|
||||||
|
"ts": 1760000068000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1467765,
|
||||||
|
"lng": 101.6965549,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 52.4,
|
||||||
|
"ts": 1760000069000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.14681,
|
||||||
|
"lng": 101.6965984,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 80.2,
|
||||||
|
"ts": 1760000070000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468227,
|
||||||
|
"lng": 101.696672,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 74.7,
|
||||||
|
"ts": 1760000071000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468425,
|
||||||
|
"lng": 101.6967444,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 74.7,
|
||||||
|
"ts": 1760000072000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468623,
|
||||||
|
"lng": 101.6968168,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 73.7,
|
||||||
|
"ts": 1760000073000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468833,
|
||||||
|
"lng": 101.6968888,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 70.5,
|
||||||
|
"ts": 1760000074000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469084,
|
||||||
|
"lng": 101.6969595,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 67.6,
|
||||||
|
"ts": 1760000075000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469369,
|
||||||
|
"lng": 101.6970289,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 53.4,
|
||||||
|
"ts": 1760000076000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469814,
|
||||||
|
"lng": 101.6970889,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 41,
|
||||||
|
"ts": 1760000077000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470379,
|
||||||
|
"lng": 101.6971381,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 55.1,
|
||||||
|
"ts": 1760000078000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470731,
|
||||||
|
"lng": 101.6971887,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.9,
|
||||||
|
"ts": 1760000079000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1470271,
|
||||||
|
"lng": 101.697248,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 128.2,
|
||||||
|
"ts": 1760000080000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469808,
|
||||||
|
"lng": 101.6973069,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 128.4,
|
||||||
|
"ts": 1760000081000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1469342,
|
||||||
|
"lng": 101.6973658,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 128.2,
|
||||||
|
"ts": 1760000082000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1468878,
|
||||||
|
"lng": 101.6974247,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.4,
|
||||||
|
"ts": 1760000083000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1466994,
|
||||||
|
"lng": 101.6973749,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.4,
|
||||||
|
"ts": 1760000084000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1465116,
|
||||||
|
"lng": 101.6973243,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.6,
|
||||||
|
"ts": 1760000085000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1463247,
|
||||||
|
"lng": 101.6972722,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.8,
|
||||||
|
"ts": 1760000086000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1461362,
|
||||||
|
"lng": 101.6972217,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 127.7,
|
||||||
|
"ts": 1760000087000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460655,
|
||||||
|
"lng": 101.6973153,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 124.4,
|
||||||
|
"ts": 1760000088000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460231,
|
||||||
|
"lng": 101.6973772,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 124.4,
|
||||||
|
"ts": 1760000089000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459803,
|
||||||
|
"lng": 101.6974399,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 124.3,
|
||||||
|
"ts": 1760000090000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459311,
|
||||||
|
"lng": 101.6975123,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.3,
|
||||||
|
"ts": 1760000091000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458888,
|
||||||
|
"lng": 101.6975768,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.1,
|
||||||
|
"ts": 1760000092000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458478,
|
||||||
|
"lng": 101.6976397,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.1,
|
||||||
|
"ts": 1760000093000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458068,
|
||||||
|
"lng": 101.6977026,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.1,
|
||||||
|
"ts": 1760000094000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1457659,
|
||||||
|
"lng": 101.6977654,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.1,
|
||||||
|
"ts": 1760000095000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458755,
|
||||||
|
"lng": 101.6979267,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.1,
|
||||||
|
"ts": 1760000096000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459866,
|
||||||
|
"lng": 101.6980859,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.5,
|
||||||
|
"ts": 1760000097000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460954,
|
||||||
|
"lng": 101.6982475,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.7,
|
||||||
|
"ts": 1760000098000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1462034,
|
||||||
|
"lng": 101.6984099,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.7,
|
||||||
|
"ts": 1760000099000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458626,
|
||||||
|
"lng": 101.6982723,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 123.7,
|
||||||
|
"ts": 1760000100000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1456888,
|
||||||
|
"lng": 101.6982101,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 126.9,
|
||||||
|
"ts": 1760000101000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458381,
|
||||||
|
"lng": 101.697914,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 160.7,
|
||||||
|
"ts": 1760000102000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460739,
|
||||||
|
"lng": 101.6979011,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 185.4,
|
||||||
|
"ts": 1760000103000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1460026,
|
||||||
|
"lng": 101.6978944,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 185.7,
|
||||||
|
"ts": 1760000104000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1459009,
|
||||||
|
"lng": 101.6980654,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 184.7,
|
||||||
|
"ts": 1760000105000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1458033,
|
||||||
|
"lng": 101.6982382,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 183.3,
|
||||||
|
"ts": 1760000106000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1457001,
|
||||||
|
"lng": 101.6984135,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 177.6,
|
||||||
|
"ts": 1760000107000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1456327,
|
||||||
|
"lng": 101.6985966,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 176.9,
|
||||||
|
"ts": 1760000108000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1455579,
|
||||||
|
"lng": 101.6986007,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 176.8,
|
||||||
|
"ts": 1760000109000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1454831,
|
||||||
|
"lng": 101.6986048,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 176.5,
|
||||||
|
"ts": 1760000110000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1454083,
|
||||||
|
"lng": 101.6986094,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 176.9,
|
||||||
|
"ts": 1760000111000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1453334,
|
||||||
|
"lng": 101.6986134,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 178.6,
|
||||||
|
"ts": 1760000112000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1452585,
|
||||||
|
"lng": 101.6986153,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 178.9,
|
||||||
|
"ts": 1760000113000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1451836,
|
||||||
|
"lng": 101.6986167,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 178.9,
|
||||||
|
"ts": 1760000114000,
|
||||||
|
"source": "webview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 3.1451087,
|
||||||
|
"lng": 101.6986182,
|
||||||
|
"accuracyM": 10,
|
||||||
|
"headingDeg": 178.6,
|
||||||
|
"ts": 1760000115000,
|
||||||
|
"source": "webview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
* guidance-replay.test.mjs — GUID-02/GUID-06: THE D-04 reroute state machine,
|
||||||
|
* replay-proven (D-16) + the D-15 distance throttle.
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* The four committed drive fixtures (web/test/fixtures/drives/*.json) are
|
||||||
|
* Fix-shaped JSON streams derived from the committed golden pair
|
||||||
|
* central-market-to-merdeka-118 (public ODbL data; attribution travels in
|
||||||
|
* each file's _notice). Each drive is folded fix-by-fix through the pure
|
||||||
|
* reducer (createGuidanceSession + stepGuidance) — no DOM, no maplibre, no
|
||||||
|
* device, no network — and every D-04 transition, the storm guard, the
|
||||||
|
* missed-turn shortened grace, the haptic edge stream, and arrival are
|
||||||
|
* asserted on the recorded frames.
|
||||||
|
*
|
||||||
|
* Cases:
|
||||||
|
* R1 named constants pinned (the D-04/D-07/D-08/D-14/D-15 table)
|
||||||
|
* R2 corridor-follow: start gate, on_route throughout, zero reroutes,
|
||||||
|
* haptic approach/maneuver edges, arrival (D-16 corridor)
|
||||||
|
* R3 off-route excursion: exactly ONE reroute-requested; storm guard
|
||||||
|
* holds under an immediate second excursion (RESEARCH Pitfall 6)
|
||||||
|
* R4 missed-turn excursion: shortened grace — the request fires well
|
||||||
|
* inside OFF_ROUTE_GRACE_MS
|
||||||
|
* R5 GPS jitter: never leaves on_route, zero reroute effects
|
||||||
|
* R6 guidance start gate (GUIDANCE_START_ALONG_M; PX-03 binds in plan 05)
|
||||||
|
* R7 reroute-failed -> honest no_reroute, then self-heal to on_route
|
||||||
|
* R8 self-heal: off_route -> resuming -> on_route without a reroute
|
||||||
|
* R9 storm-guard mechanics: a second request only after STORM_GUARD_MS
|
||||||
|
* R10 projectAlongRoute: forward-only projection, cumulative remaining
|
||||||
|
* R11 accuracy-aware threshold: max(35, 2xaccuracy) capped 100
|
||||||
|
* R12 structural: the module is pure (no console/document/maplibre/
|
||||||
|
* localStorage) and haversineM is THE distance authority
|
||||||
|
* T1..T3 (task 3) the D-15 distance throttle + watchPosition wrapper
|
||||||
|
*/
|
||||||
|
import { strict as assert } from "node:assert";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { test } from "./harness.mjs";
|
||||||
|
|
||||||
|
const HERE = path.dirname(fileURLToPath(new URL(import.meta.url)));
|
||||||
|
|
||||||
|
const g = await import("../src/guidance/guidance.ts");
|
||||||
|
const {
|
||||||
|
createGuidanceSession,
|
||||||
|
stepGuidance,
|
||||||
|
projectAlongRoute,
|
||||||
|
OFF_ROUTE_THRESHOLD_M,
|
||||||
|
OFF_ROUTE_CONSECUTIVE_FIXES,
|
||||||
|
OFF_ROUTE_GRACE_MS,
|
||||||
|
STORM_GUARD_MS,
|
||||||
|
MISSED_TURN_PAST_MANEUVER_M,
|
||||||
|
MISSED_TURN_GRACE_MS,
|
||||||
|
ARRIVAL_THRESHOLD_M,
|
||||||
|
APPROACH_HAPTIC_M,
|
||||||
|
GUIDANCE_START_ALONG_M,
|
||||||
|
} = g;
|
||||||
|
const { offRouteThresholdM } = g;
|
||||||
|
|
||||||
|
const routing = await import("@araharah/routing/src/index.ts");
|
||||||
|
const { decodePolyline6, encodePolyline6, deriveCumulativeDistanceM, haversineM } = routing;
|
||||||
|
|
||||||
|
const ROUTE_FIXTURES = JSON.parse(
|
||||||
|
readFileSync(path.join(HERE, "..", "..", "routing", "src", "data", "route-fixtures.json"), "utf8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ————— replay helpers ————— */
|
||||||
|
|
||||||
|
function loadDrive(name) {
|
||||||
|
return JSON.parse(readFileSync(path.join(HERE, "fixtures", "drives", `${name}.json`), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GuidanceRouteModel from the SAME golden-pair route the drive was derived
|
||||||
|
* from: decode -> points -> cumulative -> steps synthesized at the fixture's
|
||||||
|
* recorded maneuver indices (OSRM-style step distances: a step runs from its
|
||||||
|
* maneuver TO the next boundary; the maneuver point rides in step.geometry). */
|
||||||
|
function buildModel(fx) {
|
||||||
|
const pair = ROUTE_FIXTURES.pairs.find((p) => p.pairId === fx.pairId);
|
||||||
|
const points = decodePolyline6(pair.routes[0].geometry);
|
||||||
|
const cumulative = deriveCumulativeDistanceM(points);
|
||||||
|
const totalM = cumulative[cumulative.length - 1];
|
||||||
|
const steps = [{ kind: "depart", distanceM: cumulative[fx.maneuverIdx[0]] }];
|
||||||
|
fx.maneuverIdx.forEach((idx, n) => {
|
||||||
|
const nextAlong = n + 1 < fx.maneuverIdx.length ? cumulative[fx.maneuverIdx[n + 1]] : totalM;
|
||||||
|
steps.push({
|
||||||
|
kind: fx.kinds[n],
|
||||||
|
distanceM: nextAlong - cumulative[idx],
|
||||||
|
geometry: encodePolyline6(points.slice(idx, idx + 2)),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
steps.push({ kind: "arrive", distanceM: 0 });
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
cumulative,
|
||||||
|
steps,
|
||||||
|
destinationId: pair.destinationId,
|
||||||
|
destinationName: "Merdeka 118",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A fresh engine-A-style route: origin at the requesting fix, a merge leg
|
||||||
|
* along the drive's own next fixes, then the destination line onward. */
|
||||||
|
function buildRerouteModel(original, fixes, atIdx) {
|
||||||
|
const proj = projectAlongRoute(original.points, original.cumulative, fixes[atIdx], 0);
|
||||||
|
const startIdx = Math.min(proj.idx + 1, original.points.length - 1);
|
||||||
|
const pts = [
|
||||||
|
{ lat: fixes[atIdx].lat, lng: fixes[atIdx].lng },
|
||||||
|
{ lat: fixes[Math.min(atIdx + 4, fixes.length - 1)].lat, lng: fixes[Math.min(atIdx + 4, fixes.length - 1)].lng },
|
||||||
|
...original.points.slice(startIdx).map((p) => ({ lat: p.lat, lng: p.lng })),
|
||||||
|
];
|
||||||
|
const cumulative = deriveCumulativeDistanceM(pts);
|
||||||
|
const totalM = cumulative[cumulative.length - 1];
|
||||||
|
return {
|
||||||
|
points: pts,
|
||||||
|
cumulative,
|
||||||
|
steps: [
|
||||||
|
{ kind: "depart", distanceM: totalM },
|
||||||
|
{ kind: "arrive", distanceM: 0 },
|
||||||
|
],
|
||||||
|
destinationId: original.destinationId,
|
||||||
|
destinationName: original.destinationName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function totalOf(model) {
|
||||||
|
return model.cumulative[model.cumulative.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fold a drive through the machine; records {state, deviationM, alongM,
|
||||||
|
* remainingM, ts, effects} per fix. handler(e, ctx) runs controller logic
|
||||||
|
* between steps (it may mutate pendingEvents, folded before the next fix). */
|
||||||
|
function replayDrive(fx, handler = null) {
|
||||||
|
const model = buildModel(fx);
|
||||||
|
let snap = createGuidanceSession(model, fx.fixes[0], fx.fixes[0].ts);
|
||||||
|
const frames = [{ state: snap.state, ts: fx.fixes[0].ts, effects: [] }];
|
||||||
|
let pendingEvents = [];
|
||||||
|
for (let i = 1; i < fx.fixes.length; i++) {
|
||||||
|
for (const ev of pendingEvents) {
|
||||||
|
snap = stepGuidance(snap, ev, fx.fixes[i].ts).snap;
|
||||||
|
}
|
||||||
|
pendingEvents = [];
|
||||||
|
const fix = fx.fixes[i];
|
||||||
|
const out = stepGuidance(snap, { type: "fix", fix }, fix.ts);
|
||||||
|
snap = out.snap;
|
||||||
|
frames.push({
|
||||||
|
state: snap.state,
|
||||||
|
deviationM: snap.deviationM,
|
||||||
|
alongM: snap.alongM,
|
||||||
|
remainingM: snap.remainingM,
|
||||||
|
ts: fix.ts,
|
||||||
|
effects: out.effects,
|
||||||
|
});
|
||||||
|
if (handler) handler(out.effects, { i, fix, snap, model, fixes: fx.fixes, pendingEvents });
|
||||||
|
}
|
||||||
|
return { snap, frames, model };
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectFrames(frames, type) {
|
||||||
|
return frames.flatMap((f) => f.effects.filter((e) => e.type === type).map((e) => ({ ...e, ts: f.ts })));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ————— R1 — the constants table (RS4 style: exported values pinned) ————— */
|
||||||
|
|
||||||
|
test("R1 guidance constants are exported at their pinned D-04/D-07/D-08/D-14/D-15 values", () => {
|
||||||
|
assert.equal(OFF_ROUTE_THRESHOLD_M, 50);
|
||||||
|
assert.equal(OFF_ROUTE_CONSECUTIVE_FIXES, 3);
|
||||||
|
assert.equal(OFF_ROUTE_GRACE_MS, 8000);
|
||||||
|
assert.equal(STORM_GUARD_MS, 20000);
|
||||||
|
assert.equal(MISSED_TURN_PAST_MANEUVER_M, 40);
|
||||||
|
assert.equal(MISSED_TURN_GRACE_MS, 3000);
|
||||||
|
assert.equal(ARRIVAL_THRESHOLD_M, 25);
|
||||||
|
assert.equal(APPROACH_HAPTIC_M, 300);
|
||||||
|
assert.equal(GUIDANCE_START_ALONG_M, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R2 — corridor-follow (D-16) ————— */
|
||||||
|
|
||||||
|
test("R2 corridor-follow: start gate then on_route throughout, zero reroutes, haptic edges, arrival", () => {
|
||||||
|
const fx = loadDrive("corridor-follow");
|
||||||
|
const { snap, frames, model } = replayDrive(fx);
|
||||||
|
|
||||||
|
// start gate: first fix < 30 m along -> idle; guidance starts by ~33 m along
|
||||||
|
assert.equal(frames[0].state, "idle");
|
||||||
|
assert.ok(frames.slice(1, 6).some((f) => f.state === "on_route"), "guidance starts within the first 5 fixes");
|
||||||
|
const routeFrames = frames.filter((f) => f.state !== "idle");
|
||||||
|
assert.ok(routeFrames.length > 100, "the drive is mostly guided");
|
||||||
|
assert.ok(routeFrames.every((f) => f.state === "on_route"), "state stays on_route for the whole corridor");
|
||||||
|
|
||||||
|
// ZERO reroute-requested effects on a clean corridor
|
||||||
|
assert.equal(effectFrames(frames, "reroute-requested").length, 0);
|
||||||
|
|
||||||
|
// haptic edges: one approach per maneuver (+ the arrival approach),
|
||||||
|
// one maneuver impact per recorded turn, one arrival
|
||||||
|
const approaches = effectFrames(frames, "haptic").filter((e) => e.cue === "approach");
|
||||||
|
const impacts = effectFrames(frames, "haptic").filter((e) => e.cue === "maneuver");
|
||||||
|
const arrives = effectFrames(frames, "haptic").filter((e) => e.cue === "arrive");
|
||||||
|
const arrivals = effectFrames(frames, "arrival");
|
||||||
|
assert.equal(fx.maneuverIdx.length, 5);
|
||||||
|
assert.equal(impacts.length, 5, "one maneuver haptic per recorded turn");
|
||||||
|
assert.equal(approaches.length, 6, "one approach haptic per maneuver edge + arrival");
|
||||||
|
assert.equal(arrivals.length, 1, "arrival fires exactly once");
|
||||||
|
assert.equal(arrives.length, 1, "arrive haptic fires exactly once");
|
||||||
|
|
||||||
|
// the FINAL approach fires ~300 m before the final maneuver (arrival)
|
||||||
|
const lastApproachFrame = frames.find((f) => f.effects.some((e) => e.type === "haptic" && e.cue === "approach" && f.remainingM <= APPROACH_HAPTIC_M && f.alongM > 1500));
|
||||||
|
assert.ok(lastApproachFrame, "the arrival approach exists late in the drive");
|
||||||
|
assert.ok(
|
||||||
|
lastApproachFrame.remainingM > 250 && lastApproachFrame.remainingM <= APPROACH_HAPTIC_M,
|
||||||
|
`final approach at ~300 m before arrival (got ${lastApproachFrame.remainingM.toFixed(1)} m)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// arrival happens at the end-of-route threshold (D-14)
|
||||||
|
const arrivalFrame = frames.find((f) => f.effects.some((e) => e.type === "arrival"));
|
||||||
|
assert.ok(arrivalFrame.remainingM <= ARRIVAL_THRESHOLD_M, "arrival at snapped progress within the threshold");
|
||||||
|
assert.equal(snap.arrived, true);
|
||||||
|
assert.equal(snap.state, "on_route");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R3 — off-route excursion + storm guard (Pitfall 6) ————— */
|
||||||
|
|
||||||
|
test("R3 off-route excursion: exactly ONE reroute-requested; storm guard holds under the second excursion", () => {
|
||||||
|
const fx = loadDrive("off-route-excursion");
|
||||||
|
const requests = [];
|
||||||
|
let phase = "idle";
|
||||||
|
const { snap, frames } = replayDrive(fx, (effects, ctx) => {
|
||||||
|
for (const e of effects) {
|
||||||
|
if (e.type === "reroute-requested") {
|
||||||
|
requests.push({ origin: e.origin, ts: ctx.fix.ts });
|
||||||
|
phase = "starting";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the controller: reroute-started the next fix, reroute-applied the one after
|
||||||
|
if (phase === "starting") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-started" });
|
||||||
|
phase = "routing";
|
||||||
|
} else if (phase === "routing") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-applied", model: buildRerouteModel(ctx.model, ctx.fixes, ctx.i) });
|
||||||
|
phase = "done";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// exactly ONE reroute request across BOTH excursions (the storm guard holds)
|
||||||
|
assert.equal(requests.length, 1, `expected 1 reroute request, got ${requests.length}`);
|
||||||
|
// the request origin is the honest excursion position (off the line)
|
||||||
|
assert.equal(typeof requests[0].origin.lat, "number");
|
||||||
|
assert.equal(typeof requests[0].origin.lng, "number");
|
||||||
|
|
||||||
|
const states = frames.map((f) => f.state);
|
||||||
|
assert.ok(states.includes("on_route"), "starts on the route");
|
||||||
|
assert.ok(states.includes("rerouting"), "the machine tracks the in-flight reroute");
|
||||||
|
const applyIdx = states.lastIndexOf("rerouting");
|
||||||
|
assert.equal(states[applyIdx + 1], "on_route", "reroute-applied lands on_route (the new route)");
|
||||||
|
|
||||||
|
// after the apply: the SECOND excursion never re-requests and never re-enters off_route
|
||||||
|
const afterApply = frames.slice(applyIdx + 1);
|
||||||
|
assert.equal(effectFrames(afterApply, "reroute-requested").length, 0, "no second reroute-requested within the storm guard");
|
||||||
|
assert.ok(afterApply.every((f) => f.state !== "off_route"), "the banner stays calm under the excursion storm");
|
||||||
|
assert.equal(snap.state, "on_route", "the drive ends back on the (new) route");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R4 — missed-turn shortened grace ————— */
|
||||||
|
|
||||||
|
test("R4 missed-turn: reroute fires under the shortened grace, far inside OFF_ROUTE_GRACE_MS", () => {
|
||||||
|
const fx = loadDrive("missed-turn-excursion");
|
||||||
|
const requests = [];
|
||||||
|
let phase = "idle";
|
||||||
|
const { snap, frames } = replayDrive(fx, (effects, ctx) => {
|
||||||
|
for (const e of effects) {
|
||||||
|
if (e.type === "reroute-requested") {
|
||||||
|
requests.push({ ts: ctx.fix.ts });
|
||||||
|
phase = "starting";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phase === "starting") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-started" });
|
||||||
|
phase = "routing";
|
||||||
|
} else if (phase === "routing") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-applied", model: buildRerouteModel(ctx.model, ctx.fixes, ctx.i) });
|
||||||
|
phase = "done";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(requests.length, 1, "exactly one reroute request");
|
||||||
|
// the excursion window: first fix beyond the accuracy-aware threshold ...
|
||||||
|
const firstOff = frames.find((f) => f.deviationM !== undefined && f.deviationM > 35);
|
||||||
|
assert.ok(firstOff, "the drive goes off-threshold");
|
||||||
|
// ... to the request: well inside the STANDARD grace (shortened by the
|
||||||
|
// missed-turn detector: the car is past the maneuver point)
|
||||||
|
const span = requests[0].ts - firstOff.ts;
|
||||||
|
assert.ok(span >= 0, "sanity");
|
||||||
|
assert.ok(
|
||||||
|
span < OFF_ROUTE_GRACE_MS,
|
||||||
|
`request fired ${span} ms after the first off-threshold fix — inside the standard ${OFF_ROUTE_GRACE_MS} ms grace`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
span >= MISSED_TURN_GRACE_MS,
|
||||||
|
`the shortened grace was still honored (>= ${MISSED_TURN_GRACE_MS} ms, got ${span} ms)`,
|
||||||
|
);
|
||||||
|
// 3 consecutive off-threshold fixes preceded the request (the D-04 gate)
|
||||||
|
const offRun = frames.filter((f) => f.ts <= requests[0].ts && f.deviationM > 35);
|
||||||
|
assert.ok(offRun.length >= OFF_ROUTE_CONSECUTIVE_FIXES, "at least 3 consecutive off-threshold fixes before acting");
|
||||||
|
assert.equal(snap.state, "on_route", "the drive ends on the rerouted route");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R5 — GPS jitter absorption ————— */
|
||||||
|
|
||||||
|
test("R5 gps-jitter: the accuracy-aware threshold + 3 fixes + grace absorb ±15 m noise — never off_route", () => {
|
||||||
|
const fx = loadDrive("gps-jitter");
|
||||||
|
const { snap, frames } = replayDrive(fx);
|
||||||
|
const guided = frames.filter((f) => f.state !== "idle");
|
||||||
|
assert.ok(guided.length > 100);
|
||||||
|
assert.ok(guided.every((f) => f.state === "on_route"), "state NEVER leaves on_route under jitter");
|
||||||
|
assert.equal(effectFrames(frames, "reroute-requested").length, 0, "zero reroute requests");
|
||||||
|
assert.equal(effectFrames(frames, "arrival").length, 1, "the drive still arrives");
|
||||||
|
// deviations stay inside the honest threshold the whole drive
|
||||||
|
assert.ok(guided.every((f) => f.deviationM <= offRouteThresholdM(20)), "deviation inside the accuracy-aware threshold");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R6 — guidance start gate ————— */
|
||||||
|
|
||||||
|
test("R6 start gate: a fix < 30 m along does not start guidance; >= 30 m along does", () => {
|
||||||
|
const fx = loadDrive("corridor-follow");
|
||||||
|
const model = buildModel(fx);
|
||||||
|
const fixes = fx.fixes;
|
||||||
|
// first recorded fix sits ~4 m along — not started
|
||||||
|
const snap0 = createGuidanceSession(model, fixes[0], fixes[0].ts);
|
||||||
|
assert.equal(snap0.state, "idle");
|
||||||
|
assert.ok(snap0.alongM < GUIDANCE_START_ALONG_M);
|
||||||
|
// a fix >= 30 m along starts guidance (PX-03 close protections bind in plan 05)
|
||||||
|
const started = createGuidanceSession(model, fixes[5], fixes[5].ts);
|
||||||
|
assert.ok(started.alongM >= GUIDANCE_START_ALONG_M);
|
||||||
|
assert.equal(started.state, "on_route");
|
||||||
|
// and an idle session starts on the first qualifying fix
|
||||||
|
const stepped = stepGuidance(snap0, { type: "fix", fix: fixes[5] }, fixes[5].ts);
|
||||||
|
assert.equal(stepped.snap.state, "on_route");
|
||||||
|
assert.deepEqual(stepped.effects, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R7 — reroute-failed: the honest degradation (D-03) ————— */
|
||||||
|
|
||||||
|
test("R7 reroute-failed while rerouting -> no_reroute; a manual rejoin still recovers", () => {
|
||||||
|
const fx = loadDrive("off-route-excursion");
|
||||||
|
let phase = "idle";
|
||||||
|
const { snap, frames } = replayDrive(fx, (effects, ctx) => {
|
||||||
|
for (const e of effects) {
|
||||||
|
if (e.type === "reroute-requested") phase = "starting";
|
||||||
|
}
|
||||||
|
if (phase === "starting") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-started" });
|
||||||
|
phase = "failing";
|
||||||
|
} else if (phase === "failing") {
|
||||||
|
ctx.pendingEvents.push({ type: "reroute-failed" });
|
||||||
|
phase = "done";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const states = frames.map((f) => f.state);
|
||||||
|
assert.ok(states.includes("rerouting"), "the reroute was in flight");
|
||||||
|
assert.ok(states.includes("no_reroute"), "the failure lands in the honest no_reroute state");
|
||||||
|
// the car rejoins the old line as the excursion decays — self-heal proceeds
|
||||||
|
assert.equal(snap.state, "on_route", "a manual rejoin recovers to on_route");
|
||||||
|
assert.ok(states.includes("resuming"), "recovery walks through resuming");
|
||||||
|
assert.equal(effectFrames(frames, "reroute-requested").length, 1, "the failed attempt is still exactly one request");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R8 — self-heal without any reroute ————— */
|
||||||
|
|
||||||
|
test("R8 off_route -> resuming -> on_route on a clean rejoin, no reroute events at all", () => {
|
||||||
|
const fx = loadDrive("off-route-excursion");
|
||||||
|
// replay WITHOUT any controller: excursion 1 goes off_route; excursion 2's
|
||||||
|
// suppression keeps the banner calm; the rejoin heals through resuming
|
||||||
|
const { snap, frames } = replayDrive(fx);
|
||||||
|
const states = frames.map((f) => f.state);
|
||||||
|
assert.ok(states.includes("off_route"), "the excursion is detected honestly");
|
||||||
|
assert.ok(states.includes("resuming"), "the rejoin enters resuming");
|
||||||
|
const resumeIdx = states.indexOf("resuming");
|
||||||
|
assert.equal(states[resumeIdx + 1], "on_route", "resuming resolves to on_route on the next good fix");
|
||||||
|
assert.equal(effectFrames(frames, "reroute-requested").length, 1, "the un-acted request stays single");
|
||||||
|
assert.equal(snap.state, "on_route");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R9 — storm-guard mechanics (unit: controlled stream) ————— */
|
||||||
|
|
||||||
|
// A synthetic 4 km straight line east of KL center — pure storm-guard probing.
|
||||||
|
function straightModel(lengthM, segM = 39) {
|
||||||
|
const n = Math.ceil(lengthM / segM);
|
||||||
|
const lat0 = 3.15;
|
||||||
|
const lng0 = 101.69;
|
||||||
|
const dLng = segM / (111194.93 * Math.cos(lat0 * (Math.PI / 180)));
|
||||||
|
const points = Array.from({ length: n + 1 }, (_, i) => ({ lat: lat0, lng: lng0 + i * dLng }));
|
||||||
|
const cumulative = deriveCumulativeDistanceM(points);
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
cumulative,
|
||||||
|
steps: [
|
||||||
|
{ kind: "depart", distanceM: lengthM },
|
||||||
|
{ kind: "arrive", distanceM: 0 },
|
||||||
|
],
|
||||||
|
destinationId: "storm-probe",
|
||||||
|
destinationName: "Storm Probe",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetFix = (p, offM, t) => {
|
||||||
|
const M_PER_DEG = 111194.93;
|
||||||
|
return {
|
||||||
|
lat: p.lat,
|
||||||
|
lng: p.lng + offM / (M_PER_DEG * Math.cos(p.lat * (Math.PI / 180))),
|
||||||
|
accuracyM: 10,
|
||||||
|
headingDeg: 90,
|
||||||
|
ts: 1760000000000 + t * 1000,
|
||||||
|
source: "webview",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test("R9 storm guard: a re-confirmed excursion re-requests only after STORM_GUARD_MS", () => {
|
||||||
|
const model = straightModel(4000);
|
||||||
|
const base = model.points[0];
|
||||||
|
// on-line fixes t=0..5 (start gate), then a permanent 80 m offset from t=6
|
||||||
|
let snap = createGuidanceSession(model, offsetFix(base, 0, 0), 1760000000000);
|
||||||
|
const requests = [];
|
||||||
|
for (let t = 1; t <= 60; t++) {
|
||||||
|
const fix = offsetFix(model.points[Math.min(t, model.points.length - 1)], t < 6 ? 0 : 80, t);
|
||||||
|
const out = stepGuidance(snap, { type: "fix", fix }, fix.ts);
|
||||||
|
snap = out.snap;
|
||||||
|
for (const e of out.effects) {
|
||||||
|
if (e.type === "reroute-requested") requests.push({ t, ts: fix.ts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.equal(requests.length, 2, `excursion confirmed once, then once more after the guard (got ${requests.length})`);
|
||||||
|
assert.equal(requests[0].t, 14, "first request: 3 consecutive fixes + 8 s grace after going off at t=6");
|
||||||
|
assert.ok(
|
||||||
|
requests[1].ts - requests[0].ts >= STORM_GUARD_MS,
|
||||||
|
`second request waits the full storm guard (>= ${STORM_GUARD_MS} ms)`,
|
||||||
|
);
|
||||||
|
assert.equal(requests[1].t, 34, "second request lands exactly when the 20 s guard expires");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R10 — projectAlongRoute ————— */
|
||||||
|
|
||||||
|
test("R10 projection: forward-only walk, snapped point, cumulative remaining", () => {
|
||||||
|
const fx = loadDrive("corridor-follow");
|
||||||
|
const model = buildModel(fx);
|
||||||
|
const totalM = totalOf(model);
|
||||||
|
const mid = 50;
|
||||||
|
const at = model.points[mid];
|
||||||
|
const fixAt = { lat: at.lat, lng: at.lng, accuracyM: 8, headingDeg: null, ts: 0, source: "webview" };
|
||||||
|
const p1 = projectAlongRoute(model.points, model.cumulative, fixAt, 0);
|
||||||
|
assert.ok(haversineM(p1.snapped, at) < 1, "snapped to the route point itself");
|
||||||
|
assert.ok(Math.abs(p1.alongM - model.cumulative[mid]) < 1, "along reads the cumulative field");
|
||||||
|
assert.ok(Math.abs(p1.remainingM - (totalM - model.cumulative[mid])) < 1, "remaining is the cumulative difference (D-07)");
|
||||||
|
assert.ok(p1.idx >= mid - 1 && p1.idx <= mid, "the owning segment sits at the point");
|
||||||
|
|
||||||
|
// forward-only: a fix far BEHIND fromIdx cannot drag progress backward
|
||||||
|
const behind = model.points[10];
|
||||||
|
const fixBehind = { lat: behind.lat, lng: behind.lng, accuracyM: 8, headingDeg: null, ts: 0, source: "webview" };
|
||||||
|
const p2 = projectAlongRoute(model.points, model.cumulative, fixBehind, 60);
|
||||||
|
assert.ok(p2.idx >= 60, "the walk never goes backward");
|
||||||
|
assert.ok(haversineM(p2.snapped, behind) > 100, "the backward fix reads as a large deviation, not a snap");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R11 — the accuracy-aware threshold ————— */
|
||||||
|
|
||||||
|
test("R11 offRouteThresholdM: max(35, 2 x accuracyM) capped at 100; null uses the nominal", () => {
|
||||||
|
assert.equal(offRouteThresholdM(null), OFF_ROUTE_THRESHOLD_M, "no accuracy -> the nominal 50");
|
||||||
|
assert.equal(offRouteThresholdM(8), 35, "small accuracy floors at 35");
|
||||||
|
assert.equal(offRouteThresholdM(20), 40, "2x accuracy inside the band");
|
||||||
|
assert.equal(offRouteThresholdM(60), 100, "huge accuracy caps at 100");
|
||||||
|
assert.equal(offRouteThresholdM(25), 50, "the nominal accuracy reads the named constant");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ————— R12 — structural: THE pure module ————— */
|
||||||
|
|
||||||
|
function stripComments(src) {
|
||||||
|
return src.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("R12 guidance.ts is pure: no console/document/maplibre/localStorage; haversineM is the distance authority", () => {
|
||||||
|
const src = stripComments(readFileSync(path.join(HERE, "..", "src", "guidance", "guidance.ts"), "utf8"));
|
||||||
|
assert.doesNotMatch(src, /console\./, "no console in the machine (state tokens are the controller's job)");
|
||||||
|
assert.doesNotMatch(src, /\bdocument\b/, "no DOM");
|
||||||
|
assert.doesNotMatch(src, /maplibre/, "no maplibre");
|
||||||
|
assert.doesNotMatch(src, /localStorage|sessionStorage|fetch\(/, "no storage, no network (T-06-05)");
|
||||||
|
assert.match(src, /haversineM/, "one distance authority");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user