From b879e56bb10f60a12fe1c76c6b2a01185dfbc3c1 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 12:35:32 +0800 Subject: [PATCH] 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) --- web/test/fixtures/drives/corridor-follow.json | 1892 +++++++++++++++++ web/test/fixtures/drives/gps-jitter.json | 1892 +++++++++++++++++ .../drives/missed-turn-excursion.json | 462 ++++ .../fixtures/drives/off-route-excursion.json | 950 +++++++++ web/test/guidance-replay.test.mjs | 483 +++++ 5 files changed, 5679 insertions(+) create mode 100644 web/test/fixtures/drives/corridor-follow.json create mode 100644 web/test/fixtures/drives/gps-jitter.json create mode 100644 web/test/fixtures/drives/missed-turn-excursion.json create mode 100644 web/test/fixtures/drives/off-route-excursion.json create mode 100644 web/test/guidance-replay.test.mjs diff --git a/web/test/fixtures/drives/corridor-follow.json b/web/test/fixtures/drives/corridor-follow.json new file mode 100644 index 0000000..518b34b --- /dev/null +++ b/web/test/fixtures/drives/corridor-follow.json @@ -0,0 +1,1892 @@ +{ + "_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" + ], + "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.1443776, + "lng": 101.695482, + "accuracyM": 8, + "headingDeg": 40.6, + "ts": 1760000017000, + "source": "webview" + }, + { + "lat": 3.1444345, + "lng": 101.6955308, + "accuracyM": 8, + "headingDeg": 40.6, + "ts": 1760000018000, + "source": "webview" + }, + { + "lat": 3.1444914, + "lng": 101.6955797, + "accuracyM": 8, + "headingDeg": 41.5, + "ts": 1760000019000, + "source": "webview" + }, + { + "lat": 3.1445475, + "lng": 101.6956294, + "accuracyM": 8, + "headingDeg": 43.4, + "ts": 1760000020000, + "source": "webview" + }, + { + "lat": 3.144602, + "lng": 101.6956809, + "accuracyM": 8, + "headingDeg": 43.4, + "ts": 1760000021000, + "source": "webview" + }, + { + "lat": 3.1446565, + "lng": 101.6957325, + "accuracyM": 8, + "headingDeg": 45.8, + "ts": 1760000022000, + "source": "webview" + }, + { + "lat": 3.1447087, + "lng": 101.6957861, + "accuracyM": 8, + "headingDeg": 50.7, + "ts": 1760000023000, + "source": "webview" + }, + { + "lat": 3.1447562, + "lng": 101.6958442, + "accuracyM": 8, + "headingDeg": 53.7, + "ts": 1760000024000, + "source": "webview" + }, + { + "lat": 3.1448005, + "lng": 101.6959047, + "accuracyM": 8, + "headingDeg": 54.4, + "ts": 1760000025000, + "source": "webview" + }, + { + "lat": 3.1448442, + "lng": 101.6959657, + "accuracyM": 8, + "headingDeg": 56.6, + "ts": 1760000026000, + "source": "webview" + }, + { + "lat": 3.1448854, + "lng": 101.6960283, + "accuracyM": 8, + "headingDeg": 57.1, + "ts": 1760000027000, + "source": "webview" + }, + { + "lat": 3.1449261, + "lng": 101.6960914, + "accuracyM": 8, + "headingDeg": 57.1, + "ts": 1760000028000, + "source": "webview" + }, + { + "lat": 3.1449668, + "lng": 101.6961544, + "accuracyM": 8, + "headingDeg": 57.1, + "ts": 1760000029000, + "source": "webview" + }, + { + "lat": 3.1450074, + "lng": 101.6962175, + "accuracyM": 8, + "headingDeg": 63.5, + "ts": 1760000030000, + "source": "webview" + }, + { + "lat": 3.1450409, + "lng": 101.6962846, + "accuracyM": 8, + "headingDeg": 65.6, + "ts": 1760000031000, + "source": "webview" + }, + { + "lat": 3.1450718, + "lng": 101.6963529, + "accuracyM": 8, + "headingDeg": 68.5, + "ts": 1760000032000, + "source": "webview" + }, + { + "lat": 3.1450993, + "lng": 101.6964227, + "accuracyM": 8, + "headingDeg": 68.5, + "ts": 1760000033000, + "source": "webview" + }, + { + "lat": 3.1451268, + "lng": 101.6964925, + "accuracyM": 8, + "headingDeg": 69.1, + "ts": 1760000034000, + "source": "webview" + }, + { + "lat": 3.1451536, + "lng": 101.6965626, + "accuracyM": 8, + "headingDeg": 69.9, + "ts": 1760000035000, + "source": "webview" + }, + { + "lat": 3.1451793, + "lng": 101.6966331, + "accuracyM": 8, + "headingDeg": 72.1, + "ts": 1760000036000, + "source": "webview" + }, + { + "lat": 3.1452023, + "lng": 101.6967045, + "accuracyM": 8, + "headingDeg": 74, + "ts": 1760000037000, + "source": "webview" + }, + { + "lat": 3.145223, + "lng": 101.6967766, + "accuracyM": 8, + "headingDeg": 76.3, + "ts": 1760000038000, + "source": "webview" + }, + { + "lat": 3.1452407, + "lng": 101.6968496, + "accuracyM": 8, + "headingDeg": 76.7, + "ts": 1760000039000, + "source": "webview" + }, + { + "lat": 3.1452579, + "lng": 101.6969226, + "accuracyM": 8, + "headingDeg": 76.8, + "ts": 1760000040000, + "source": "webview" + }, + { + "lat": 3.145275, + "lng": 101.6969957, + "accuracyM": 8, + "headingDeg": 77.1, + "ts": 1760000041000, + "source": "webview" + }, + { + "lat": 3.1452917, + "lng": 101.6970689, + "accuracyM": 8, + "headingDeg": 77.1, + "ts": 1760000042000, + "source": "webview" + }, + { + "lat": 3.1453085, + "lng": 101.697142, + "accuracyM": 8, + "headingDeg": 77.1, + "ts": 1760000043000, + "source": "webview" + }, + { + "lat": 3.1453252, + "lng": 101.6972152, + "accuracyM": 8, + "headingDeg": 79, + "ts": 1760000044000, + "source": "webview" + }, + { + "lat": 3.1453394, + "lng": 101.6972888, + "accuracyM": 8, + "headingDeg": 81.8, + "ts": 1760000045000, + "source": "webview" + }, + { + "lat": 3.1453501, + "lng": 101.6973631, + "accuracyM": 8, + "headingDeg": 33.8, + "ts": 1760000046000, + "source": "webview" + }, + { + "lat": 3.1453846, + "lng": 101.6973862, + "accuracyM": 8, + "headingDeg": 328.1, + "ts": 1760000047000, + "source": "webview" + }, + { + "lat": 3.1454482, + "lng": 101.6973465, + "accuracyM": 8, + "headingDeg": 324.2, + "ts": 1760000048000, + "source": "webview" + }, + { + "lat": 3.1455089, + "lng": 101.6973027, + "accuracyM": 8, + "headingDeg": 323.6, + "ts": 1760000049000, + "source": "webview" + }, + { + "lat": 3.1455693, + "lng": 101.6972582, + "accuracyM": 8, + "headingDeg": 324.3, + "ts": 1760000050000, + "source": "webview" + }, + { + "lat": 3.1456302, + "lng": 101.6972144, + "accuracyM": 8, + "headingDeg": 324.3, + "ts": 1760000051000, + "source": "webview" + }, + { + "lat": 3.145691, + "lng": 101.6971707, + "accuracyM": 8, + "headingDeg": 326, + "ts": 1760000052000, + "source": "webview" + }, + { + "lat": 3.1457531, + "lng": 101.6971287, + "accuracyM": 8, + "headingDeg": 326, + "ts": 1760000053000, + "source": "webview" + }, + { + "lat": 3.1458153, + "lng": 101.6970867, + "accuracyM": 8, + "headingDeg": 326, + "ts": 1760000054000, + "source": "webview" + }, + { + "lat": 3.1458774, + "lng": 101.6970447, + "accuracyM": 8, + "headingDeg": 325.7, + "ts": 1760000055000, + "source": "webview" + }, + { + "lat": 3.1459393, + "lng": 101.6970024, + "accuracyM": 8, + "headingDeg": 323.6, + "ts": 1760000056000, + "source": "webview" + }, + { + "lat": 3.1459996, + "lng": 101.696958, + "accuracyM": 8, + "headingDeg": 316.5, + "ts": 1760000057000, + "source": "webview" + }, + { + "lat": 3.1460539, + "lng": 101.6969064, + "accuracyM": 8, + "headingDeg": 316.5, + "ts": 1760000058000, + "source": "webview" + }, + { + "lat": 3.1461083, + "lng": 101.6968548, + "accuracyM": 8, + "headingDeg": 317.2, + "ts": 1760000059000, + "source": "webview" + }, + { + "lat": 3.1461633, + "lng": 101.6968038, + "accuracyM": 8, + "headingDeg": 321.7, + "ts": 1760000060000, + "source": "webview" + }, + { + "lat": 3.1462221, + "lng": 101.6967573, + "accuracyM": 8, + "headingDeg": 328.5, + "ts": 1760000061000, + "source": "webview" + }, + { + "lat": 3.146286, + "lng": 101.6967181, + "accuracyM": 8, + "headingDeg": 332, + "ts": 1760000062000, + "source": "webview" + }, + { + "lat": 3.1463522, + "lng": 101.6966829, + "accuracyM": 8, + "headingDeg": 334.5, + "ts": 1760000063000, + "source": "webview" + }, + { + "lat": 3.1464198, + "lng": 101.6966506, + "accuracyM": 8, + "headingDeg": 336.7, + "ts": 1760000064000, + "source": "webview" + }, + { + "lat": 3.1464886, + "lng": 101.6966209, + "accuracyM": 8, + "headingDeg": 340.3, + "ts": 1760000065000, + "source": "webview" + }, + { + "lat": 3.1465591, + "lng": 101.6965957, + "accuracyM": 8, + "headingDeg": 342.7, + "ts": 1760000066000, + "source": "webview" + }, + { + "lat": 3.1466307, + "lng": 101.6965734, + "accuracyM": 8, + "headingDeg": 342, + "ts": 1760000067000, + "source": "webview" + }, + { + "lat": 3.1467017, + "lng": 101.6965502, + "accuracyM": 8, + "headingDeg": 3.6, + "ts": 1760000068000, + "source": "webview" + }, + { + "lat": 3.1467765, + "lng": 101.6965549, + "accuracyM": 8, + "headingDeg": 52.4, + "ts": 1760000069000, + "source": "webview" + }, + { + "lat": 3.14681, + "lng": 101.6965984, + "accuracyM": 8, + "headingDeg": 80.2, + "ts": 1760000070000, + "source": "webview" + }, + { + "lat": 3.1468227, + "lng": 101.696672, + "accuracyM": 8, + "headingDeg": 74.7, + "ts": 1760000071000, + "source": "webview" + }, + { + "lat": 3.1468425, + "lng": 101.6967444, + "accuracyM": 8, + "headingDeg": 74.7, + "ts": 1760000072000, + "source": "webview" + }, + { + "lat": 3.1468623, + "lng": 101.6968168, + "accuracyM": 8, + "headingDeg": 73.7, + "ts": 1760000073000, + "source": "webview" + }, + { + "lat": 3.1468833, + "lng": 101.6968888, + "accuracyM": 8, + "headingDeg": 70.5, + "ts": 1760000074000, + "source": "webview" + }, + { + "lat": 3.1469084, + "lng": 101.6969595, + "accuracyM": 8, + "headingDeg": 67.6, + "ts": 1760000075000, + "source": "webview" + }, + { + "lat": 3.1469369, + "lng": 101.6970289, + "accuracyM": 8, + "headingDeg": 53.4, + "ts": 1760000076000, + "source": "webview" + }, + { + "lat": 3.1469814, + "lng": 101.6970889, + "accuracyM": 8, + "headingDeg": 41, + "ts": 1760000077000, + "source": "webview" + }, + { + "lat": 3.1470379, + "lng": 101.6971381, + "accuracyM": 8, + "headingDeg": 55.1, + "ts": 1760000078000, + "source": "webview" + }, + { + "lat": 3.1470731, + "lng": 101.6971887, + "accuracyM": 8, + "headingDeg": 127.9, + "ts": 1760000079000, + "source": "webview" + }, + { + "lat": 3.1470271, + "lng": 101.697248, + "accuracyM": 8, + "headingDeg": 128.2, + "ts": 1760000080000, + "source": "webview" + }, + { + "lat": 3.1469808, + "lng": 101.6973069, + "accuracyM": 8, + "headingDeg": 128.4, + "ts": 1760000081000, + "source": "webview" + }, + { + "lat": 3.1469342, + "lng": 101.6973658, + "accuracyM": 8, + "headingDeg": 128.2, + "ts": 1760000082000, + "source": "webview" + }, + { + "lat": 3.1468878, + "lng": 101.6974247, + "accuracyM": 8, + "headingDeg": 127.4, + "ts": 1760000083000, + "source": "webview" + }, + { + "lat": 3.1468423, + "lng": 101.6974843, + "accuracyM": 8, + "headingDeg": 127.4, + "ts": 1760000084000, + "source": "webview" + }, + { + "lat": 3.1467968, + "lng": 101.6975439, + "accuracyM": 8, + "headingDeg": 127.6, + "ts": 1760000085000, + "source": "webview" + }, + { + "lat": 3.1467511, + "lng": 101.6976034, + "accuracyM": 8, + "headingDeg": 127.8, + "ts": 1760000086000, + "source": "webview" + }, + { + "lat": 3.1467051, + "lng": 101.6976627, + "accuracyM": 8, + "headingDeg": 127.7, + "ts": 1760000087000, + "source": "webview" + }, + { + "lat": 3.1466593, + "lng": 101.6977221, + "accuracyM": 8, + "headingDeg": 124.4, + "ts": 1760000088000, + "source": "webview" + }, + { + "lat": 3.1466169, + "lng": 101.697784, + "accuracyM": 8, + "headingDeg": 124.4, + "ts": 1760000089000, + "source": "webview" + }, + { + "lat": 3.1465746, + "lng": 101.697846, + "accuracyM": 8, + "headingDeg": 124.3, + "ts": 1760000090000, + "source": "webview" + }, + { + "lat": 3.1465324, + "lng": 101.697908, + "accuracyM": 8, + "headingDeg": 123.3, + "ts": 1760000091000, + "source": "webview" + }, + { + "lat": 3.1464912, + "lng": 101.6979707, + "accuracyM": 8, + "headingDeg": 123.1, + "ts": 1760000092000, + "source": "webview" + }, + { + "lat": 3.1464503, + "lng": 101.6980336, + "accuracyM": 8, + "headingDeg": 123.1, + "ts": 1760000093000, + "source": "webview" + }, + { + "lat": 3.1464093, + "lng": 101.6980964, + "accuracyM": 8, + "headingDeg": 123.1, + "ts": 1760000094000, + "source": "webview" + }, + { + "lat": 3.1463683, + "lng": 101.6981593, + "accuracyM": 8, + "headingDeg": 123.1, + "ts": 1760000095000, + "source": "webview" + }, + { + "lat": 3.1463274, + "lng": 101.6982221, + "accuracyM": 8, + "headingDeg": 123.1, + "ts": 1760000096000, + "source": "webview" + }, + { + "lat": 3.1462864, + "lng": 101.698285, + "accuracyM": 8, + "headingDeg": 123.5, + "ts": 1760000097000, + "source": "webview" + }, + { + "lat": 3.146245, + "lng": 101.6983475, + "accuracyM": 8, + "headingDeg": 123.7, + "ts": 1760000098000, + "source": "webview" + }, + { + "lat": 3.1462034, + "lng": 101.6984099, + "accuracyM": 8, + "headingDeg": 123.7, + "ts": 1760000099000, + "source": "webview" + }, + { + "lat": 3.1461618, + "lng": 101.6984724, + "accuracyM": 8, + "headingDeg": 123.7, + "ts": 1760000100000, + "source": "webview" + }, + { + "lat": 3.1461201, + "lng": 101.6985348, + "accuracyM": 8, + "headingDeg": 126.9, + "ts": 1760000101000, + "source": "webview" + }, + { + "lat": 3.1460756, + "lng": 101.6985941, + "accuracyM": 8, + "headingDeg": 160.7, + "ts": 1760000102000, + "source": "webview" + }, + { + "lat": 3.1460062, + "lng": 101.6986184, + "accuracyM": 8, + "headingDeg": 185.4, + "ts": 1760000103000, + "source": "webview" + }, + { + "lat": 3.1459316, + "lng": 101.6986114, + "accuracyM": 8, + "headingDeg": 185.7, + "ts": 1760000104000, + "source": "webview" + }, + { + "lat": 3.1458571, + "lng": 101.698604, + "accuracyM": 8, + "headingDeg": 184.7, + "ts": 1760000105000, + "source": "webview" + }, + { + "lat": 3.1457824, + "lng": 101.6985979, + "accuracyM": 8, + "headingDeg": 183.3, + "ts": 1760000106000, + "source": "webview" + }, + { + "lat": 3.1457076, + "lng": 101.6985935, + "accuracyM": 8, + "headingDeg": 177.6, + "ts": 1760000107000, + "source": "webview" + }, + { + "lat": 3.1456327, + "lng": 101.6985966, + "accuracyM": 8, + "headingDeg": 176.9, + "ts": 1760000108000, + "source": "webview" + }, + { + "lat": 3.1455579, + "lng": 101.6986007, + "accuracyM": 8, + "headingDeg": 176.8, + "ts": 1760000109000, + "source": "webview" + }, + { + "lat": 3.1454831, + "lng": 101.6986048, + "accuracyM": 8, + "headingDeg": 176.5, + "ts": 1760000110000, + "source": "webview" + }, + { + "lat": 3.1454083, + "lng": 101.6986094, + "accuracyM": 8, + "headingDeg": 176.9, + "ts": 1760000111000, + "source": "webview" + }, + { + "lat": 3.1453334, + "lng": 101.6986134, + "accuracyM": 8, + "headingDeg": 178.6, + "ts": 1760000112000, + "source": "webview" + }, + { + "lat": 3.1452585, + "lng": 101.6986153, + "accuracyM": 8, + "headingDeg": 178.9, + "ts": 1760000113000, + "source": "webview" + }, + { + "lat": 3.1451836, + "lng": 101.6986167, + "accuracyM": 8, + "headingDeg": 178.9, + "ts": 1760000114000, + "source": "webview" + }, + { + "lat": 3.1451087, + "lng": 101.6986182, + "accuracyM": 8, + "headingDeg": 178.6, + "ts": 1760000115000, + "source": "webview" + }, + { + "lat": 3.1450337, + "lng": 101.6986201, + "accuracyM": 8, + "headingDeg": 178.2, + "ts": 1760000116000, + "source": "webview" + }, + { + "lat": 3.1449588, + "lng": 101.6986224, + "accuracyM": 8, + "headingDeg": 178.2, + "ts": 1760000117000, + "source": "webview" + }, + { + "lat": 3.1448839, + "lng": 101.6986248, + "accuracyM": 8, + "headingDeg": 178.2, + "ts": 1760000118000, + "source": "webview" + }, + { + "lat": 3.144809, + "lng": 101.6986272, + "accuracyM": 8, + "headingDeg": 178.1, + "ts": 1760000119000, + "source": "webview" + }, + { + "lat": 3.1447341, + "lng": 101.6986296, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000120000, + "source": "webview" + }, + { + "lat": 3.1446592, + "lng": 101.6986323, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000121000, + "source": "webview" + }, + { + "lat": 3.1445843, + "lng": 101.698635, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000122000, + "source": "webview" + }, + { + "lat": 3.1445094, + "lng": 101.6986377, + "accuracyM": 8, + "headingDeg": 178.6, + "ts": 1760000123000, + "source": "webview" + }, + { + "lat": 3.1444345, + "lng": 101.6986395, + "accuracyM": 8, + "headingDeg": 177.8, + "ts": 1760000124000, + "source": "webview" + }, + { + "lat": 3.1443596, + "lng": 101.6986424, + "accuracyM": 8, + "headingDeg": 178, + "ts": 1760000125000, + "source": "webview" + }, + { + "lat": 3.1442847, + "lng": 101.6986451, + "accuracyM": 8, + "headingDeg": 178, + "ts": 1760000126000, + "source": "webview" + }, + { + "lat": 3.1442098, + "lng": 101.6986477, + "accuracyM": 8, + "headingDeg": 177.4, + "ts": 1760000127000, + "source": "webview" + }, + { + "lat": 3.144135, + "lng": 101.6986512, + "accuracyM": 8, + "headingDeg": 179.4, + "ts": 1760000128000, + "source": "webview" + }, + { + "lat": 3.1440601, + "lng": 101.6986519, + "accuracyM": 8, + "headingDeg": 178, + "ts": 1760000129000, + "source": "webview" + }, + { + "lat": 3.1439852, + "lng": 101.6986545, + "accuracyM": 8, + "headingDeg": 177.4, + "ts": 1760000130000, + "source": "webview" + }, + { + "lat": 3.1439103, + "lng": 101.698658, + "accuracyM": 8, + "headingDeg": 176.7, + "ts": 1760000131000, + "source": "webview" + }, + { + "lat": 3.1438355, + "lng": 101.6986622, + "accuracyM": 8, + "headingDeg": 176.7, + "ts": 1760000132000, + "source": "webview" + }, + { + "lat": 3.1437607, + "lng": 101.6986665, + "accuracyM": 8, + "headingDeg": 176.7, + "ts": 1760000133000, + "source": "webview" + }, + { + "lat": 3.1436858, + "lng": 101.6986707, + "accuracyM": 8, + "headingDeg": 176.7, + "ts": 1760000134000, + "source": "webview" + }, + { + "lat": 3.143611, + "lng": 101.698675, + "accuracyM": 8, + "headingDeg": 177.2, + "ts": 1760000135000, + "source": "webview" + }, + { + "lat": 3.1435362, + "lng": 101.6986786, + "accuracyM": 8, + "headingDeg": 177.1, + "ts": 1760000136000, + "source": "webview" + }, + { + "lat": 3.1434613, + "lng": 101.6986825, + "accuracyM": 8, + "headingDeg": 177.3, + "ts": 1760000137000, + "source": "webview" + }, + { + "lat": 3.1433864, + "lng": 101.698686, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000138000, + "source": "webview" + }, + { + "lat": 3.1433116, + "lng": 101.6986888, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000139000, + "source": "webview" + }, + { + "lat": 3.1432367, + "lng": 101.6986915, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000140000, + "source": "webview" + }, + { + "lat": 3.1431618, + "lng": 101.6986942, + "accuracyM": 8, + "headingDeg": 177.9, + "ts": 1760000141000, + "source": "webview" + }, + { + "lat": 3.1430869, + "lng": 101.698697, + "accuracyM": 8, + "headingDeg": 178.3, + "ts": 1760000142000, + "source": "webview" + }, + { + "lat": 3.143012, + "lng": 101.6986992, + "accuracyM": 8, + "headingDeg": 179.6, + "ts": 1760000143000, + "source": "webview" + }, + { + "lat": 3.142937, + "lng": 101.6986997, + "accuracyM": 8, + "headingDeg": 180.8, + "ts": 1760000144000, + "source": "webview" + }, + { + "lat": 3.1428621, + "lng": 101.6986987, + "accuracyM": 8, + "headingDeg": 152.4, + "ts": 1760000145000, + "source": "webview" + }, + { + "lat": 3.142811, + "lng": 101.6987254, + "accuracyM": 8, + "headingDeg": 91, + "ts": 1760000146000, + "source": "webview" + }, + { + "lat": 3.1428098, + "lng": 101.6988003, + "accuracyM": 8, + "headingDeg": 89.3, + "ts": 1760000147000, + "source": "webview" + }, + { + "lat": 3.1428107, + "lng": 101.6988753, + "accuracyM": 8, + "headingDeg": 89.3, + "ts": 1760000148000, + "source": "webview" + }, + { + "lat": 3.1428116, + "lng": 101.6989504, + "accuracyM": 8, + "headingDeg": 89, + "ts": 1760000149000, + "source": "webview" + }, + { + "lat": 3.1428128, + "lng": 101.6990254, + "accuracyM": 8, + "headingDeg": 88.8, + "ts": 1760000150000, + "source": "webview" + }, + { + "lat": 3.1428144, + "lng": 101.6991005, + "accuracyM": 8, + "headingDeg": 88.9, + "ts": 1760000151000, + "source": "webview" + }, + { + "lat": 3.1428159, + "lng": 101.6991755, + "accuracyM": 8, + "headingDeg": 90.1, + "ts": 1760000152000, + "source": "webview" + }, + { + "lat": 3.1428157, + "lng": 101.6992506, + "accuracyM": 8, + "headingDeg": 90.2, + "ts": 1760000153000, + "source": "webview" + }, + { + "lat": 3.1428155, + "lng": 101.6993256, + "accuracyM": 8, + "headingDeg": 90.2, + "ts": 1760000154000, + "source": "webview" + }, + { + "lat": 3.1428152, + "lng": 101.6994007, + "accuracyM": 8, + "headingDeg": 89.7, + "ts": 1760000155000, + "source": "webview" + }, + { + "lat": 3.1428156, + "lng": 101.6994757, + "accuracyM": 8, + "headingDeg": 88.9, + "ts": 1760000156000, + "source": "webview" + }, + { + "lat": 3.1428171, + "lng": 101.6995508, + "accuracyM": 8, + "headingDeg": 87.5, + "ts": 1760000157000, + "source": "webview" + }, + { + "lat": 3.1428204, + "lng": 101.6996258, + "accuracyM": 8, + "headingDeg": 85.3, + "ts": 1760000158000, + "source": "webview" + }, + { + "lat": 3.1428266, + "lng": 101.6997005, + "accuracyM": 8, + "headingDeg": 84.9, + "ts": 1760000159000, + "source": "webview" + }, + { + "lat": 3.1428332, + "lng": 101.6997753, + "accuracyM": 8, + "headingDeg": 84.1, + "ts": 1760000160000, + "source": "webview" + }, + { + "lat": 3.142841, + "lng": 101.6998499, + "accuracyM": 8, + "headingDeg": 86.3, + "ts": 1760000161000, + "source": "webview" + }, + { + "lat": 3.1428458, + "lng": 101.6999248, + "accuracyM": 8, + "headingDeg": 87.5, + "ts": 1760000162000, + "source": "webview" + }, + { + "lat": 3.1428492, + "lng": 101.6999998, + "accuracyM": 8, + "headingDeg": 87.8, + "ts": 1760000163000, + "source": "webview" + }, + { + "lat": 3.142852, + "lng": 101.7000748, + "accuracyM": 8, + "headingDeg": 94.1, + "ts": 1760000164000, + "source": "webview" + }, + { + "lat": 3.1428467, + "lng": 101.7001497, + "accuracyM": 8, + "headingDeg": 94.1, + "ts": 1760000165000, + "source": "webview" + }, + { + "lat": 3.1428414, + "lng": 101.7002246, + "accuracyM": 8, + "headingDeg": 94.1, + "ts": 1760000166000, + "source": "webview" + }, + { + "lat": 3.142836, + "lng": 101.7002994, + "accuracyM": 8, + "headingDeg": 96.2, + "ts": 1760000167000, + "source": "webview" + }, + { + "lat": 3.1428279, + "lng": 101.700374, + "accuracyM": 8, + "headingDeg": 96.5, + "ts": 1760000168000, + "source": "webview" + }, + { + "lat": 3.1428194, + "lng": 101.7004486, + "accuracyM": 8, + "headingDeg": 97, + "ts": 1760000169000, + "source": "webview" + }, + { + "lat": 3.1428103, + "lng": 101.7005231, + "accuracyM": 8, + "headingDeg": 98.1, + "ts": 1760000170000, + "source": "webview" + }, + { + "lat": 3.1427997, + "lng": 101.7005974, + "accuracyM": 8, + "headingDeg": 101.5, + "ts": 1760000171000, + "source": "webview" + }, + { + "lat": 3.1427848, + "lng": 101.7006709, + "accuracyM": 8, + "headingDeg": 105.3, + "ts": 1760000172000, + "source": "webview" + }, + { + "lat": 3.1427651, + "lng": 101.700743, + "accuracyM": 8, + "headingDeg": 112.4, + "ts": 1760000173000, + "source": "webview" + }, + { + "lat": 3.1427365, + "lng": 101.7008124, + "accuracyM": 8, + "headingDeg": 95.2, + "ts": 1760000174000, + "source": "webview" + }, + { + "lat": 3.1427297, + "lng": 101.700887, + "accuracyM": 8, + "headingDeg": 94.2, + "ts": 1760000175000, + "source": "webview" + }, + { + "lat": 3.1427242, + "lng": 101.7009618, + "accuracyM": 8, + "headingDeg": 94.1, + "ts": 1760000176000, + "source": "webview" + }, + { + "lat": 3.1427189, + "lng": 101.7010367, + "accuracyM": 8, + "headingDeg": 94.1, + "ts": 1760000177000, + "source": "webview" + }, + { + "lat": 3.1427135, + "lng": 101.7011116, + "accuracyM": 8, + "headingDeg": 94.3, + "ts": 1760000178000, + "source": "webview" + }, + { + "lat": 3.1427079, + "lng": 101.7011864, + "accuracyM": 8, + "headingDeg": 95.7, + "ts": 1760000179000, + "source": "webview" + }, + { + "lat": 3.1427004, + "lng": 101.7012611, + "accuracyM": 8, + "headingDeg": 95.7, + "ts": 1760000180000, + "source": "webview" + }, + { + "lat": 3.142693, + "lng": 101.7013358, + "accuracyM": 8, + "headingDeg": 100.9, + "ts": 1760000181000, + "source": "webview" + }, + { + "lat": 3.1426789, + "lng": 101.7014091, + "accuracyM": 8, + "headingDeg": 107, + "ts": 1760000182000, + "source": "webview" + }, + { + "lat": 3.142657, + "lng": 101.7014809, + "accuracyM": 8, + "headingDeg": 112.9, + "ts": 1760000183000, + "source": "webview" + }, + { + "lat": 3.1426279, + "lng": 101.7015498, + "accuracyM": 8, + "headingDeg": 116.2, + "ts": 1760000184000, + "source": "webview" + }, + { + "lat": 3.1425948, + "lng": 101.7016172, + "accuracyM": 8, + "headingDeg": 115.2, + "ts": 1760000185000, + "source": "webview" + }, + { + "lat": 3.1425629, + "lng": 101.7016851, + "accuracyM": 8, + "headingDeg": 120.2, + "ts": 1760000186000, + "source": "webview" + }, + { + "lat": 3.1425252, + "lng": 101.7017499, + "accuracyM": 8, + "headingDeg": 120.3, + "ts": 1760000187000, + "source": "webview" + }, + { + "lat": 3.1424874, + "lng": 101.7018147, + "accuracyM": 8, + "headingDeg": 120.8, + "ts": 1760000188000, + "source": "webview" + }, + { + "lat": 3.142449, + "lng": 101.7018792, + "accuracyM": 8, + "headingDeg": 121.1, + "ts": 1760000189000, + "source": "webview" + }, + { + "lat": 3.1424103, + "lng": 101.7019435, + "accuracyM": 8, + "headingDeg": 121.1, + "ts": 1760000190000, + "source": "webview" + }, + { + "lat": 3.1423716, + "lng": 101.7020078, + "accuracyM": 8, + "headingDeg": 121.1, + "ts": 1760000191000, + "source": "webview" + }, + { + "lat": 3.1423329, + "lng": 101.702072, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000192000, + "source": "webview" + }, + { + "lat": 3.1422943, + "lng": 101.7021364, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000193000, + "source": "webview" + }, + { + "lat": 3.1422556, + "lng": 101.7022007, + "accuracyM": 8, + "headingDeg": 120.1, + "ts": 1760000194000, + "source": "webview" + }, + { + "lat": 3.142218, + "lng": 101.7022656, + "accuracyM": 8, + "headingDeg": 119.3, + "ts": 1760000195000, + "source": "webview" + }, + { + "lat": 3.1421813, + "lng": 101.702331, + "accuracyM": 8, + "headingDeg": 119.3, + "ts": 1760000196000, + "source": "webview" + }, + { + "lat": 3.1421446, + "lng": 101.7023964, + "accuracyM": 8, + "headingDeg": 119.3, + "ts": 1760000197000, + "source": "webview" + }, + { + "lat": 3.1421079, + "lng": 101.7024619, + "accuracyM": 8, + "headingDeg": 120, + "ts": 1760000198000, + "source": "webview" + }, + { + "lat": 3.1420705, + "lng": 101.7025269, + "accuracyM": 8, + "headingDeg": 121.5, + "ts": 1760000199000, + "source": "webview" + }, + { + "lat": 3.1420313, + "lng": 101.7025908, + "accuracyM": 8, + "headingDeg": 121.5, + "ts": 1760000200000, + "source": "webview" + }, + { + "lat": 3.1419921, + "lng": 101.7026548, + "accuracyM": 8, + "headingDeg": 121.5, + "ts": 1760000201000, + "source": "webview" + }, + { + "lat": 3.1419529, + "lng": 101.7027188, + "accuracyM": 8, + "headingDeg": 121.5, + "ts": 1760000202000, + "source": "webview" + }, + { + "lat": 3.1419136, + "lng": 101.7027827, + "accuracyM": 8, + "headingDeg": 121.5, + "ts": 1760000203000, + "source": "webview" + }, + { + "lat": 3.1418744, + "lng": 101.7028467, + "accuracyM": 8, + "headingDeg": 121.2, + "ts": 1760000204000, + "source": "webview" + }, + { + "lat": 3.1418356, + "lng": 101.7029109, + "accuracyM": 8, + "headingDeg": 119.9, + "ts": 1760000205000, + "source": "webview" + }, + { + "lat": 3.1417982, + "lng": 101.7029759, + "accuracyM": 8, + "headingDeg": 119.9, + "ts": 1760000206000, + "source": "webview" + }, + { + "lat": 3.1417608, + "lng": 101.703041, + "accuracyM": 8, + "headingDeg": 123.4, + "ts": 1760000207000, + "source": "webview" + }, + { + "lat": 3.1417195, + "lng": 101.7031036, + "accuracyM": 8, + "headingDeg": 125.8, + "ts": 1760000208000, + "source": "webview" + }, + { + "lat": 3.1416757, + "lng": 101.7031645, + "accuracyM": 8, + "headingDeg": 125.4, + "ts": 1760000209000, + "source": "webview" + }, + { + "lat": 3.1416323, + "lng": 101.7032256, + "accuracyM": 8, + "headingDeg": 124.4, + "ts": 1760000210000, + "source": "webview" + }, + { + "lat": 3.14159, + "lng": 101.7032876, + "accuracyM": 8, + "headingDeg": 124.4, + "ts": 1760000211000, + "source": "webview" + }, + { + "lat": 3.1415476, + "lng": 101.7033495, + "accuracyM": 8, + "headingDeg": 124.8, + "ts": 1760000212000, + "source": "webview" + }, + { + "lat": 3.1415048, + "lng": 101.7034111, + "accuracyM": 8, + "headingDeg": 124.4, + "ts": 1760000213000, + "source": "webview" + }, + { + "lat": 3.1414626, + "lng": 101.703473, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000214000, + "source": "webview" + }, + { + "lat": 3.1414239, + "lng": 101.7035374, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000215000, + "source": "webview" + }, + { + "lat": 3.1413853, + "lng": 101.7036017, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000216000, + "source": "webview" + }, + { + "lat": 3.1413467, + "lng": 101.703666, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000217000, + "source": "webview" + }, + { + "lat": 3.1413081, + "lng": 101.7037303, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000218000, + "source": "webview" + }, + { + "lat": 3.1412695, + "lng": 101.7037946, + "accuracyM": 8, + "headingDeg": 121, + "ts": 1760000219000, + "source": "webview" + }, + { + "lat": 3.1412308, + "lng": 101.703859, + "accuracyM": 8, + "headingDeg": 119.8, + "ts": 1760000220000, + "source": "webview" + }, + { + "lat": 3.1411936, + "lng": 101.7039241, + "accuracyM": 8, + "headingDeg": 121.4, + "ts": 1760000221000, + "source": "webview" + }, + { + "lat": 3.1411546, + "lng": 101.7039881, + "accuracyM": 8, + "headingDeg": 119.7, + "ts": 1760000222000, + "source": "webview" + }, + { + "lat": 3.1411175, + "lng": 101.7040533, + "accuracyM": 8, + "headingDeg": 119.7, + "ts": 1760000223000, + "source": "webview" + }, + { + "lat": 3.1410804, + "lng": 101.7041185, + "accuracyM": 8, + "headingDeg": 123.6, + "ts": 1760000224000, + "source": "webview" + }, + { + "lat": 3.141039, + "lng": 101.7041809, + "accuracyM": 8, + "headingDeg": 127, + "ts": 1760000225000, + "source": "webview" + }, + { + "lat": 3.1409938, + "lng": 101.7042408, + "accuracyM": 8, + "headingDeg": 127, + "ts": 1760000226000, + "source": "webview" + }, + { + "lat": 3.1409487, + "lng": 101.7043007, + "accuracyM": 8, + "headingDeg": 127, + "ts": 1760000227000, + "source": "webview" + }, + { + "lat": 3.1409035, + "lng": 101.7043606, + "accuracyM": 8, + "headingDeg": 127, + "ts": 1760000228000, + "source": "webview" + }, + { + "lat": 3.1408584, + "lng": 101.7044205, + "accuracyM": 8, + "headingDeg": 127, + "ts": 1760000229000, + "source": "webview" + }, + { + "lat": 3.1408132, + "lng": 101.7044804, + "accuracyM": 8, + "headingDeg": 80.5, + "ts": 1760000230000, + "source": "webview" + }, + { + "lat": 3.1408245, + "lng": 101.7045479, + "accuracyM": 8, + "headingDeg": 78.4, + "ts": 1760000231000, + "source": "webview" + }, + { + "lat": 3.140839, + "lng": 101.7046187, + "accuracyM": 8, + "headingDeg": 107.5, + "ts": 1760000232000, + "source": "webview" + }, + { + "lat": 3.1408168, + "lng": 101.7046892, + "accuracyM": 8, + "headingDeg": 128.7, + "ts": 1760000233000, + "source": "webview" + } + ] +} diff --git a/web/test/fixtures/drives/gps-jitter.json b/web/test/fixtures/drives/gps-jitter.json new file mode 100644 index 0000000..d21dcd7 --- /dev/null +++ b/web/test/fixtures/drives/gps-jitter.json @@ -0,0 +1,1892 @@ +{ + "_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" + ], + "fixes": [ + { + "lat": 3.144911, + "lng": 101.6952447, + "accuracyM": 20, + "headingDeg": 96.5, + "ts": 1760000000000, + "source": "webview" + }, + { + "lat": 3.1451705, + "lng": 101.6953506, + "accuracyM": 20, + "headingDeg": 96.8, + "ts": 1760000001000, + "source": "webview" + }, + { + "lat": 3.144894, + "lng": 101.6953907, + "accuracyM": 20, + "headingDeg": 97.8, + "ts": 1760000002000, + "source": "webview" + }, + { + "lat": 3.1451511, + "lng": 101.6955018, + "accuracyM": 20, + "headingDeg": 97.8, + "ts": 1760000003000, + "source": "webview" + }, + { + "lat": 3.1448736, + "lng": 101.6955394, + "accuracyM": 20, + "headingDeg": 97.8, + "ts": 1760000004000, + "source": "webview" + }, + { + "lat": 3.1450443, + "lng": 101.6957587, + "accuracyM": 20, + "headingDeg": 159.5, + "ts": 1760000005000, + "source": "webview" + }, + { + "lat": 3.1449477, + "lng": 101.6955194, + "accuracyM": 20, + "headingDeg": 184, + "ts": 1760000006000, + "source": "webview" + }, + { + "lat": 3.1448319, + "lng": 101.6957803, + "accuracyM": 20, + "headingDeg": 193.5, + "ts": 1760000007000, + "source": "webview" + }, + { + "lat": 3.1448737, + "lng": 101.6955244, + "accuracyM": 20, + "headingDeg": 217.5, + "ts": 1760000008000, + "source": "webview" + }, + { + "lat": 3.1446476, + "lng": 101.6956913, + "accuracyM": 20, + "headingDeg": 218.8, + "ts": 1760000009000, + "source": "webview" + }, + { + "lat": 3.1447616, + "lng": 101.6954365, + "accuracyM": 20, + "headingDeg": 220.7, + "ts": 1760000010000, + "source": "webview" + }, + { + "lat": 3.1445256, + "lng": 101.6955895, + "accuracyM": 20, + "headingDeg": 222.6, + "ts": 1760000011000, + "source": "webview" + }, + { + "lat": 3.1446682, + "lng": 101.6953563, + "accuracyM": 20, + "headingDeg": 232, + "ts": 1760000012000, + "source": "webview" + }, + { + "lat": 3.1444479, + "lng": 101.6954971, + "accuracyM": 20, + "headingDeg": 210.2, + "ts": 1760000013000, + "source": "webview" + }, + { + "lat": 3.1444003, + "lng": 101.6952243, + "accuracyM": 20, + "headingDeg": 154.8, + "ts": 1760000014000, + "source": "webview" + }, + { + "lat": 3.1444897, + "lng": 101.6954705, + "accuracyM": 20, + "headingDeg": 133.5, + "ts": 1760000015000, + "source": "webview" + }, + { + "lat": 3.1442323, + "lng": 101.695513, + "accuracyM": 20, + "headingDeg": 53.2, + "ts": 1760000016000, + "source": "webview" + }, + { + "lat": 3.1444654, + "lng": 101.6953794, + "accuracyM": 20, + "headingDeg": 40.6, + "ts": 1760000017000, + "source": "webview" + }, + { + "lat": 3.1443467, + "lng": 101.6956334, + "accuracyM": 20, + "headingDeg": 40.6, + "ts": 1760000018000, + "source": "webview" + }, + { + "lat": 3.1445808, + "lng": 101.6954785, + "accuracyM": 20, + "headingDeg": 41.5, + "ts": 1760000019000, + "source": "webview" + }, + { + "lat": 3.1444549, + "lng": 101.6957276, + "accuracyM": 20, + "headingDeg": 43.4, + "ts": 1760000020000, + "source": "webview" + }, + { + "lat": 3.1446946, + "lng": 101.6955827, + "accuracyM": 20, + "headingDeg": 43.4, + "ts": 1760000021000, + "source": "webview" + }, + { + "lat": 3.1445598, + "lng": 101.6958267, + "accuracyM": 20, + "headingDeg": 45.8, + "ts": 1760000022000, + "source": "webview" + }, + { + "lat": 3.144813, + "lng": 101.6957005, + "accuracyM": 20, + "headingDeg": 50.7, + "ts": 1760000023000, + "source": "webview" + }, + { + "lat": 3.1446475, + "lng": 101.6959242, + "accuracyM": 20, + "headingDeg": 53.7, + "ts": 1760000024000, + "source": "webview" + }, + { + "lat": 3.1449102, + "lng": 101.695826, + "accuracyM": 20, + "headingDeg": 54.4, + "ts": 1760000025000, + "source": "webview" + }, + { + "lat": 3.1447316, + "lng": 101.6960401, + "accuracyM": 20, + "headingDeg": 56.6, + "ts": 1760000026000, + "source": "webview" + }, + { + "lat": 3.1449988, + "lng": 101.695955, + "accuracyM": 20, + "headingDeg": 57.1, + "ts": 1760000027000, + "source": "webview" + }, + { + "lat": 3.1448128, + "lng": 101.6961647, + "accuracyM": 20, + "headingDeg": 57.1, + "ts": 1760000028000, + "source": "webview" + }, + { + "lat": 3.1450801, + "lng": 101.6960811, + "accuracyM": 20, + "headingDeg": 57.1, + "ts": 1760000029000, + "source": "webview" + }, + { + "lat": 3.1448868, + "lng": 101.6962778, + "accuracyM": 20, + "headingDeg": 63.5, + "ts": 1760000030000, + "source": "webview" + }, + { + "lat": 3.1451638, + "lng": 101.6962288, + "accuracyM": 20, + "headingDeg": 65.6, + "ts": 1760000031000, + "source": "webview" + }, + { + "lat": 3.1449463, + "lng": 101.6964025, + "accuracyM": 20, + "headingDeg": 68.5, + "ts": 1760000032000, + "source": "webview" + }, + { + "lat": 3.1452248, + "lng": 101.6963731, + "accuracyM": 20, + "headingDeg": 68.5, + "ts": 1760000033000, + "source": "webview" + }, + { + "lat": 3.1450008, + "lng": 101.6965407, + "accuracyM": 20, + "headingDeg": 69.1, + "ts": 1760000034000, + "source": "webview" + }, + { + "lat": 3.1452802, + "lng": 101.6965162, + "accuracyM": 20, + "headingDeg": 69.9, + "ts": 1760000035000, + "source": "webview" + }, + { + "lat": 3.145051, + "lng": 101.6966746, + "accuracyM": 20, + "headingDeg": 72.1, + "ts": 1760000036000, + "source": "webview" + }, + { + "lat": 3.145332, + "lng": 101.6966672, + "accuracyM": 20, + "headingDeg": 74, + "ts": 1760000037000, + "source": "webview" + }, + { + "lat": 3.1450919, + "lng": 101.6968085, + "accuracyM": 20, + "headingDeg": 76.3, + "ts": 1760000038000, + "source": "webview" + }, + { + "lat": 3.145372, + "lng": 101.6968186, + "accuracyM": 20, + "headingDeg": 76.7, + "ts": 1760000039000, + "source": "webview" + }, + { + "lat": 3.1451266, + "lng": 101.6969534, + "accuracyM": 20, + "headingDeg": 76.8, + "ts": 1760000040000, + "source": "webview" + }, + { + "lat": 3.1454065, + "lng": 101.6969655, + "accuracyM": 20, + "headingDeg": 77.1, + "ts": 1760000041000, + "source": "webview" + }, + { + "lat": 3.1451602, + "lng": 101.697099, + "accuracyM": 20, + "headingDeg": 77.1, + "ts": 1760000042000, + "source": "webview" + }, + { + "lat": 3.1454399, + "lng": 101.6971118, + "accuracyM": 20, + "headingDeg": 77.1, + "ts": 1760000043000, + "source": "webview" + }, + { + "lat": 3.1451928, + "lng": 101.6972409, + "accuracyM": 20, + "headingDeg": 79, + "ts": 1760000044000, + "source": "webview" + }, + { + "lat": 3.145473, + "lng": 101.6972695, + "accuracyM": 20, + "headingDeg": 81.8, + "ts": 1760000045000, + "source": "webview" + }, + { + "lat": 3.1452752, + "lng": 101.6974754, + "accuracyM": 20, + "headingDeg": 33.8, + "ts": 1760000046000, + "source": "webview" + }, + { + "lat": 3.1453133, + "lng": 101.6972715, + "accuracyM": 20, + "headingDeg": 328.1, + "ts": 1760000047000, + "source": "webview" + }, + { + "lat": 3.145527, + "lng": 101.6974561, + "accuracyM": 20, + "headingDeg": 324.2, + "ts": 1760000048000, + "source": "webview" + }, + { + "lat": 3.145429, + "lng": 101.6971939, + "accuracyM": 20, + "headingDeg": 323.6, + "ts": 1760000049000, + "source": "webview" + }, + { + "lat": 3.1456479, + "lng": 101.697368, + "accuracyM": 20, + "headingDeg": 324.3, + "ts": 1760000050000, + "source": "webview" + }, + { + "lat": 3.1455515, + "lng": 101.6971047, + "accuracyM": 20, + "headingDeg": 324.3, + "ts": 1760000051000, + "source": "webview" + }, + { + "lat": 3.1457666, + "lng": 101.6972826, + "accuracyM": 20, + "headingDeg": 326, + "ts": 1760000052000, + "source": "webview" + }, + { + "lat": 3.1456777, + "lng": 101.6970167, + "accuracyM": 20, + "headingDeg": 326, + "ts": 1760000053000, + "source": "webview" + }, + { + "lat": 3.1458907, + "lng": 101.6971986, + "accuracyM": 20, + "headingDeg": 326, + "ts": 1760000054000, + "source": "webview" + }, + { + "lat": 3.1458015, + "lng": 101.696933, + "accuracyM": 20, + "headingDeg": 325.7, + "ts": 1760000055000, + "source": "webview" + }, + { + "lat": 3.1460193, + "lng": 101.6971112, + "accuracyM": 20, + "headingDeg": 323.6, + "ts": 1760000056000, + "source": "webview" + }, + { + "lat": 3.1459067, + "lng": 101.69686, + "accuracyM": 20, + "headingDeg": 316.5, + "ts": 1760000057000, + "source": "webview" + }, + { + "lat": 3.1461467, + "lng": 101.6970044, + "accuracyM": 20, + "headingDeg": 316.5, + "ts": 1760000058000, + "source": "webview" + }, + { + "lat": 3.1460167, + "lng": 101.6967556, + "accuracyM": 20, + "headingDeg": 317.2, + "ts": 1760000059000, + "source": "webview" + }, + { + "lat": 3.1462469, + "lng": 101.6969098, + "accuracyM": 20, + "headingDeg": 321.7, + "ts": 1760000060000, + "source": "webview" + }, + { + "lat": 3.1461516, + "lng": 101.6966421, + "accuracyM": 20, + "headingDeg": 328.5, + "ts": 1760000061000, + "source": "webview" + }, + { + "lat": 3.1463493, + "lng": 101.6968374, + "accuracyM": 20, + "headingDeg": 332, + "ts": 1760000062000, + "source": "webview" + }, + { + "lat": 3.1462942, + "lng": 101.6965609, + "accuracyM": 20, + "headingDeg": 334.5, + "ts": 1760000063000, + "source": "webview" + }, + { + "lat": 3.1464731, + "lng": 101.6967747, + "accuracyM": 20, + "headingDeg": 336.7, + "ts": 1760000064000, + "source": "webview" + }, + { + "lat": 3.1464432, + "lng": 101.6964937, + "accuracyM": 20, + "headingDeg": 340.3, + "ts": 1760000065000, + "source": "webview" + }, + { + "lat": 3.1465992, + "lng": 101.6967247, + "accuracyM": 20, + "headingDeg": 342.7, + "ts": 1760000066000, + "source": "webview" + }, + { + "lat": 3.1465889, + "lng": 101.6964449, + "accuracyM": 20, + "headingDeg": 342, + "ts": 1760000067000, + "source": "webview" + }, + { + "lat": 3.1466933, + "lng": 101.6966851, + "accuracyM": 20, + "headingDeg": 3.6, + "ts": 1760000068000, + "source": "webview" + }, + { + "lat": 3.1468833, + "lng": 101.6964724, + "accuracyM": 20, + "headingDeg": 52.4, + "ts": 1760000069000, + "source": "webview" + }, + { + "lat": 3.1466771, + "lng": 101.6966215, + "accuracyM": 20, + "headingDeg": 80.2, + "ts": 1760000070000, + "source": "webview" + }, + { + "lat": 3.1469529, + "lng": 101.6966363, + "accuracyM": 20, + "headingDeg": 74.7, + "ts": 1760000071000, + "source": "webview" + }, + { + "lat": 3.1467124, + "lng": 101.69678, + "accuracyM": 20, + "headingDeg": 74.7, + "ts": 1760000072000, + "source": "webview" + }, + { + "lat": 3.1469917, + "lng": 101.6967788, + "accuracyM": 20, + "headingDeg": 73.7, + "ts": 1760000073000, + "source": "webview" + }, + { + "lat": 3.1467562, + "lng": 101.696934, + "accuracyM": 20, + "headingDeg": 70.5, + "ts": 1760000074000, + "source": "webview" + }, + { + "lat": 3.1470331, + "lng": 101.696908, + "accuracyM": 20, + "headingDeg": 67.6, + "ts": 1760000075000, + "source": "webview" + }, + { + "lat": 3.1468286, + "lng": 101.6971094, + "accuracyM": 20, + "headingDeg": 53.4, + "ts": 1760000076000, + "source": "webview" + }, + { + "lat": 3.14707, + "lng": 101.696987, + "accuracyM": 20, + "headingDeg": 41, + "ts": 1760000077000, + "source": "webview" + }, + { + "lat": 3.1469272, + "lng": 101.6972153, + "accuracyM": 20, + "headingDeg": 55.1, + "ts": 1760000078000, + "source": "webview" + }, + { + "lat": 3.1471796, + "lng": 101.6972717, + "accuracyM": 20, + "headingDeg": 127.9, + "ts": 1760000079000, + "source": "webview" + }, + { + "lat": 3.1469211, + "lng": 101.6971644, + "accuracyM": 20, + "headingDeg": 128.2, + "ts": 1760000080000, + "source": "webview" + }, + { + "lat": 3.1470865, + "lng": 101.6973909, + "accuracyM": 20, + "headingDeg": 128.4, + "ts": 1760000081000, + "source": "webview" + }, + { + "lat": 3.1468282, + "lng": 101.6972821, + "accuracyM": 20, + "headingDeg": 128.2, + "ts": 1760000082000, + "source": "webview" + }, + { + "lat": 3.146995, + "lng": 101.6975068, + "accuracyM": 20, + "headingDeg": 127.4, + "ts": 1760000083000, + "source": "webview" + }, + { + "lat": 3.1467351, + "lng": 101.6974023, + "accuracyM": 20, + "headingDeg": 127.4, + "ts": 1760000084000, + "source": "webview" + }, + { + "lat": 3.1469037, + "lng": 101.6976263, + "accuracyM": 20, + "headingDeg": 127.6, + "ts": 1760000085000, + "source": "webview" + }, + { + "lat": 3.1466445, + "lng": 101.6975206, + "accuracyM": 20, + "headingDeg": 127.8, + "ts": 1760000086000, + "source": "webview" + }, + { + "lat": 3.1468118, + "lng": 101.6977454, + "accuracyM": 20, + "headingDeg": 127.7, + "ts": 1760000087000, + "source": "webview" + }, + { + "lat": 3.1465479, + "lng": 101.6976458, + "accuracyM": 20, + "headingDeg": 124.4, + "ts": 1760000088000, + "source": "webview" + }, + { + "lat": 3.1467283, + "lng": 101.6978603, + "accuracyM": 20, + "headingDeg": 124.4, + "ts": 1760000089000, + "source": "webview" + }, + { + "lat": 3.1464632, + "lng": 101.6977698, + "accuracyM": 20, + "headingDeg": 124.3, + "ts": 1760000090000, + "source": "webview" + }, + { + "lat": 3.1466451, + "lng": 101.6979822, + "accuracyM": 20, + "headingDeg": 123.3, + "ts": 1760000091000, + "source": "webview" + }, + { + "lat": 3.1463783, + "lng": 101.6978969, + "accuracyM": 20, + "headingDeg": 123.1, + "ts": 1760000092000, + "source": "webview" + }, + { + "lat": 3.1465632, + "lng": 101.6981074, + "accuracyM": 20, + "headingDeg": 123.1, + "ts": 1760000093000, + "source": "webview" + }, + { + "lat": 3.1462963, + "lng": 101.6980226, + "accuracyM": 20, + "headingDeg": 123.1, + "ts": 1760000094000, + "source": "webview" + }, + { + "lat": 3.1464813, + "lng": 101.6982331, + "accuracyM": 20, + "headingDeg": 123.1, + "ts": 1760000095000, + "source": "webview" + }, + { + "lat": 3.1462144, + "lng": 101.6981483, + "accuracyM": 20, + "headingDeg": 123.1, + "ts": 1760000096000, + "source": "webview" + }, + { + "lat": 3.1463988, + "lng": 101.6983596, + "accuracyM": 20, + "headingDeg": 123.5, + "ts": 1760000097000, + "source": "webview" + }, + { + "lat": 3.1461328, + "lng": 101.6982725, + "accuracyM": 20, + "headingDeg": 123.7, + "ts": 1760000098000, + "source": "webview" + }, + { + "lat": 3.1463156, + "lng": 101.698485, + "accuracyM": 20, + "headingDeg": 123.7, + "ts": 1760000099000, + "source": "webview" + }, + { + "lat": 3.1460496, + "lng": 101.6983973, + "accuracyM": 20, + "headingDeg": 123.7, + "ts": 1760000100000, + "source": "webview" + }, + { + "lat": 3.146228, + "lng": 101.6986159, + "accuracyM": 20, + "headingDeg": 126.9, + "ts": 1760000101000, + "source": "webview" + }, + { + "lat": 3.1460311, + "lng": 101.6984666, + "accuracyM": 20, + "headingDeg": 160.7, + "ts": 1760000102000, + "source": "webview" + }, + { + "lat": 3.1459936, + "lng": 101.6987529, + "accuracyM": 20, + "headingDeg": 185.4, + "ts": 1760000103000, + "source": "webview" + }, + { + "lat": 3.1459449, + "lng": 101.6984769, + "accuracyM": 20, + "headingDeg": 185.7, + "ts": 1760000104000, + "source": "webview" + }, + { + "lat": 3.1458461, + "lng": 101.6987386, + "accuracyM": 20, + "headingDeg": 184.7, + "ts": 1760000105000, + "source": "webview" + }, + { + "lat": 3.1457902, + "lng": 101.698463, + "accuracyM": 20, + "headingDeg": 183.3, + "ts": 1760000106000, + "source": "webview" + }, + { + "lat": 3.1457132, + "lng": 101.6987285, + "accuracyM": 20, + "headingDeg": 177.6, + "ts": 1760000107000, + "source": "webview" + }, + { + "lat": 3.1456254, + "lng": 101.6984617, + "accuracyM": 20, + "headingDeg": 176.9, + "ts": 1760000108000, + "source": "webview" + }, + { + "lat": 3.1455653, + "lng": 101.6987356, + "accuracyM": 20, + "headingDeg": 176.8, + "ts": 1760000109000, + "source": "webview" + }, + { + "lat": 3.1454749, + "lng": 101.69847, + "accuracyM": 20, + "headingDeg": 176.5, + "ts": 1760000110000, + "source": "webview" + }, + { + "lat": 3.1454156, + "lng": 101.6987443, + "accuracyM": 20, + "headingDeg": 176.9, + "ts": 1760000111000, + "source": "webview" + }, + { + "lat": 3.1453302, + "lng": 101.6984784, + "accuracyM": 20, + "headingDeg": 178.6, + "ts": 1760000112000, + "source": "webview" + }, + { + "lat": 3.1452612, + "lng": 101.6987503, + "accuracyM": 20, + "headingDeg": 178.9, + "ts": 1760000113000, + "source": "webview" + }, + { + "lat": 3.145181, + "lng": 101.6984816, + "accuracyM": 20, + "headingDeg": 178.9, + "ts": 1760000114000, + "source": "webview" + }, + { + "lat": 3.145112, + "lng": 101.6987532, + "accuracyM": 20, + "headingDeg": 178.6, + "ts": 1760000115000, + "source": "webview" + }, + { + "lat": 3.1450295, + "lng": 101.698485, + "accuracyM": 20, + "headingDeg": 178.2, + "ts": 1760000116000, + "source": "webview" + }, + { + "lat": 3.1449631, + "lng": 101.6987575, + "accuracyM": 20, + "headingDeg": 178.2, + "ts": 1760000117000, + "source": "webview" + }, + { + "lat": 3.1448797, + "lng": 101.6984898, + "accuracyM": 20, + "headingDeg": 178.2, + "ts": 1760000118000, + "source": "webview" + }, + { + "lat": 3.1448134, + "lng": 101.6987622, + "accuracyM": 20, + "headingDeg": 178.1, + "ts": 1760000119000, + "source": "webview" + }, + { + "lat": 3.1447293, + "lng": 101.6984946, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000120000, + "source": "webview" + }, + { + "lat": 3.1446641, + "lng": 101.6987674, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000121000, + "source": "webview" + }, + { + "lat": 3.1445795, + "lng": 101.6985, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000122000, + "source": "webview" + }, + { + "lat": 3.1445127, + "lng": 101.6987728, + "accuracyM": 20, + "headingDeg": 178.6, + "ts": 1760000123000, + "source": "webview" + }, + { + "lat": 3.1444293, + "lng": 101.6985045, + "accuracyM": 20, + "headingDeg": 177.8, + "ts": 1760000124000, + "source": "webview" + }, + { + "lat": 3.1443644, + "lng": 101.6987774, + "accuracyM": 20, + "headingDeg": 178, + "ts": 1760000125000, + "source": "webview" + }, + { + "lat": 3.14428, + "lng": 101.6985101, + "accuracyM": 20, + "headingDeg": 178, + "ts": 1760000126000, + "source": "webview" + }, + { + "lat": 3.144216, + "lng": 101.6987827, + "accuracyM": 20, + "headingDeg": 177.4, + "ts": 1760000127000, + "source": "webview" + }, + { + "lat": 3.1441337, + "lng": 101.6985161, + "accuracyM": 20, + "headingDeg": 179.4, + "ts": 1760000128000, + "source": "webview" + }, + { + "lat": 3.1440648, + "lng": 101.6987869, + "accuracyM": 20, + "headingDeg": 178, + "ts": 1760000129000, + "source": "webview" + }, + { + "lat": 3.143979, + "lng": 101.6985196, + "accuracyM": 20, + "headingDeg": 177.4, + "ts": 1760000130000, + "source": "webview" + }, + { + "lat": 3.1439179, + "lng": 101.6987928, + "accuracyM": 20, + "headingDeg": 176.7, + "ts": 1760000131000, + "source": "webview" + }, + { + "lat": 3.1438278, + "lng": 101.6985273, + "accuracyM": 20, + "headingDeg": 176.7, + "ts": 1760000132000, + "source": "webview" + }, + { + "lat": 3.1437683, + "lng": 101.6988013, + "accuracyM": 20, + "headingDeg": 176.7, + "ts": 1760000133000, + "source": "webview" + }, + { + "lat": 3.1436782, + "lng": 101.6985358, + "accuracyM": 20, + "headingDeg": 176.7, + "ts": 1760000134000, + "source": "webview" + }, + { + "lat": 3.1436176, + "lng": 101.6988099, + "accuracyM": 20, + "headingDeg": 177.2, + "ts": 1760000135000, + "source": "webview" + }, + { + "lat": 3.1435293, + "lng": 101.6985437, + "accuracyM": 20, + "headingDeg": 177.1, + "ts": 1760000136000, + "source": "webview" + }, + { + "lat": 3.1434677, + "lng": 101.6988174, + "accuracyM": 20, + "headingDeg": 177.3, + "ts": 1760000137000, + "source": "webview" + }, + { + "lat": 3.1433815, + "lng": 101.698551, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000138000, + "source": "webview" + }, + { + "lat": 3.1433165, + "lng": 101.6988238, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000139000, + "source": "webview" + }, + { + "lat": 3.1432317, + "lng": 101.6985565, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000140000, + "source": "webview" + }, + { + "lat": 3.1431667, + "lng": 101.6988293, + "accuracyM": 20, + "headingDeg": 177.9, + "ts": 1760000141000, + "source": "webview" + }, + { + "lat": 3.143083, + "lng": 101.6985619, + "accuracyM": 20, + "headingDeg": 178.3, + "ts": 1760000142000, + "source": "webview" + }, + { + "lat": 3.143013, + "lng": 101.6988342, + "accuracyM": 20, + "headingDeg": 179.6, + "ts": 1760000143000, + "source": "webview" + }, + { + "lat": 3.1429389, + "lng": 101.6985646, + "accuracyM": 20, + "headingDeg": 180.8, + "ts": 1760000144000, + "source": "webview" + }, + { + "lat": 3.1429246, + "lng": 101.6988184, + "accuracyM": 20, + "headingDeg": 152.4, + "ts": 1760000145000, + "source": "webview" + }, + { + "lat": 3.1426761, + "lng": 101.6987231, + "accuracyM": 20, + "headingDeg": 91, + "ts": 1760000146000, + "source": "webview" + }, + { + "lat": 3.1429447, + "lng": 101.6987987, + "accuracyM": 20, + "headingDeg": 89.3, + "ts": 1760000147000, + "source": "webview" + }, + { + "lat": 3.1426758, + "lng": 101.698877, + "accuracyM": 20, + "headingDeg": 89.3, + "ts": 1760000148000, + "source": "webview" + }, + { + "lat": 3.1429464, + "lng": 101.6989481, + "accuracyM": 20, + "headingDeg": 89, + "ts": 1760000149000, + "source": "webview" + }, + { + "lat": 3.142678, + "lng": 101.6990283, + "accuracyM": 20, + "headingDeg": 88.8, + "ts": 1760000150000, + "source": "webview" + }, + { + "lat": 3.1429493, + "lng": 101.6990978, + "accuracyM": 20, + "headingDeg": 88.9, + "ts": 1760000151000, + "source": "webview" + }, + { + "lat": 3.142681, + "lng": 101.6991752, + "accuracyM": 20, + "headingDeg": 90.1, + "ts": 1760000152000, + "source": "webview" + }, + { + "lat": 3.1429506, + "lng": 101.6992511, + "accuracyM": 20, + "headingDeg": 90.2, + "ts": 1760000153000, + "source": "webview" + }, + { + "lat": 3.1426806, + "lng": 101.6993251, + "accuracyM": 20, + "headingDeg": 90.2, + "ts": 1760000154000, + "source": "webview" + }, + { + "lat": 3.1429501, + "lng": 101.6993999, + "accuracyM": 20, + "headingDeg": 89.7, + "ts": 1760000155000, + "source": "webview" + }, + { + "lat": 3.1426807, + "lng": 101.6994784, + "accuracyM": 20, + "headingDeg": 88.9, + "ts": 1760000156000, + "source": "webview" + }, + { + "lat": 3.1429518, + "lng": 101.6995448, + "accuracyM": 20, + "headingDeg": 87.5, + "ts": 1760000157000, + "source": "webview" + }, + { + "lat": 3.142686, + "lng": 101.6996369, + "accuracyM": 20, + "headingDeg": 85.3, + "ts": 1760000158000, + "source": "webview" + }, + { + "lat": 3.142961, + "lng": 101.6996886, + "accuracyM": 20, + "headingDeg": 84.9, + "ts": 1760000159000, + "source": "webview" + }, + { + "lat": 3.1426991, + "lng": 101.6997892, + "accuracyM": 20, + "headingDeg": 84.1, + "ts": 1760000160000, + "source": "webview" + }, + { + "lat": 3.1429756, + "lng": 101.6998412, + "accuracyM": 20, + "headingDeg": 86.3, + "ts": 1760000161000, + "source": "webview" + }, + { + "lat": 3.1427111, + "lng": 101.6999308, + "accuracyM": 20, + "headingDeg": 87.5, + "ts": 1760000162000, + "source": "webview" + }, + { + "lat": 3.142984, + "lng": 101.6999947, + "accuracyM": 20, + "headingDeg": 87.8, + "ts": 1760000163000, + "source": "webview" + }, + { + "lat": 3.1427174, + "lng": 101.7000653, + "accuracyM": 20, + "headingDeg": 94.1, + "ts": 1760000164000, + "source": "webview" + }, + { + "lat": 3.1429812, + "lng": 101.7001593, + "accuracyM": 20, + "headingDeg": 94.1, + "ts": 1760000165000, + "source": "webview" + }, + { + "lat": 3.1427068, + "lng": 101.700215, + "accuracyM": 20, + "headingDeg": 94.1, + "ts": 1760000166000, + "source": "webview" + }, + { + "lat": 3.1429701, + "lng": 101.7003141, + "accuracyM": 20, + "headingDeg": 96.2, + "ts": 1760000167000, + "source": "webview" + }, + { + "lat": 3.1426939, + "lng": 101.7003587, + "accuracyM": 20, + "headingDeg": 96.5, + "ts": 1760000168000, + "source": "webview" + }, + { + "lat": 3.1429533, + "lng": 101.7004651, + "accuracyM": 20, + "headingDeg": 97, + "ts": 1760000169000, + "source": "webview" + }, + { + "lat": 3.1426767, + "lng": 101.7005041, + "accuracyM": 20, + "headingDeg": 98.1, + "ts": 1760000170000, + "source": "webview" + }, + { + "lat": 3.1429319, + "lng": 101.7006243, + "accuracyM": 20, + "headingDeg": 101.5, + "ts": 1760000171000, + "source": "webview" + }, + { + "lat": 3.1426546, + "lng": 101.7006353, + "accuracyM": 20, + "headingDeg": 105.3, + "ts": 1760000172000, + "source": "webview" + }, + { + "lat": 3.1428898, + "lng": 101.7007946, + "accuracyM": 20, + "headingDeg": 112.4, + "ts": 1760000173000, + "source": "webview" + }, + { + "lat": 3.1426021, + "lng": 101.7008002, + "accuracyM": 20, + "headingDeg": 95.2, + "ts": 1760000174000, + "source": "webview" + }, + { + "lat": 3.1428642, + "lng": 101.7008968, + "accuracyM": 20, + "headingDeg": 94.2, + "ts": 1760000175000, + "source": "webview" + }, + { + "lat": 3.1425897, + "lng": 101.7009522, + "accuracyM": 20, + "headingDeg": 94.1, + "ts": 1760000176000, + "source": "webview" + }, + { + "lat": 3.1428534, + "lng": 101.7010463, + "accuracyM": 20, + "headingDeg": 94.1, + "ts": 1760000177000, + "source": "webview" + }, + { + "lat": 3.142579, + "lng": 101.7011014, + "accuracyM": 20, + "headingDeg": 94.3, + "ts": 1760000178000, + "source": "webview" + }, + { + "lat": 3.1428421, + "lng": 101.7011998, + "accuracyM": 20, + "headingDeg": 95.7, + "ts": 1760000179000, + "source": "webview" + }, + { + "lat": 3.1425662, + "lng": 101.7012477, + "accuracyM": 20, + "headingDeg": 95.7, + "ts": 1760000180000, + "source": "webview" + }, + { + "lat": 3.1428255, + "lng": 101.7013613, + "accuracyM": 20, + "headingDeg": 100.9, + "ts": 1760000181000, + "source": "webview" + }, + { + "lat": 3.1425499, + "lng": 101.7013696, + "accuracyM": 20, + "headingDeg": 107, + "ts": 1760000182000, + "source": "webview" + }, + { + "lat": 3.1427813, + "lng": 101.7015334, + "accuracyM": 20, + "headingDeg": 112.9, + "ts": 1760000183000, + "source": "webview" + }, + { + "lat": 3.1425069, + "lng": 101.7014901, + "accuracyM": 20, + "headingDeg": 116.2, + "ts": 1760000184000, + "source": "webview" + }, + { + "lat": 3.1427169, + "lng": 101.7016747, + "accuracyM": 20, + "headingDeg": 115.2, + "ts": 1760000185000, + "source": "webview" + }, + { + "lat": 3.1424463, + "lng": 101.7016171, + "accuracyM": 20, + "headingDeg": 120.2, + "ts": 1760000186000, + "source": "webview" + }, + { + "lat": 3.1426416, + "lng": 101.7018182, + "accuracyM": 20, + "headingDeg": 120.3, + "ts": 1760000187000, + "source": "webview" + }, + { + "lat": 3.1423715, + "lng": 101.7017456, + "accuracyM": 20, + "headingDeg": 120.8, + "ts": 1760000188000, + "source": "webview" + }, + { + "lat": 3.1425645, + "lng": 101.701949, + "accuracyM": 20, + "headingDeg": 121.1, + "ts": 1760000189000, + "source": "webview" + }, + { + "lat": 3.1422948, + "lng": 101.7018737, + "accuracyM": 20, + "headingDeg": 121.1, + "ts": 1760000190000, + "source": "webview" + }, + { + "lat": 3.1424872, + "lng": 101.7020775, + "accuracyM": 20, + "headingDeg": 121.1, + "ts": 1760000191000, + "source": "webview" + }, + { + "lat": 3.1422174, + "lng": 101.7020024, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000192000, + "source": "webview" + }, + { + "lat": 3.1424099, + "lng": 101.702206, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000193000, + "source": "webview" + }, + { + "lat": 3.142139, + "lng": 101.7021329, + "accuracyM": 20, + "headingDeg": 120.1, + "ts": 1760000194000, + "source": "webview" + }, + { + "lat": 3.1423356, + "lng": 101.7023318, + "accuracyM": 20, + "headingDeg": 119.3, + "ts": 1760000195000, + "source": "webview" + }, + { + "lat": 3.1420637, + "lng": 101.7022648, + "accuracyM": 20, + "headingDeg": 119.3, + "ts": 1760000196000, + "source": "webview" + }, + { + "lat": 3.1422622, + "lng": 101.7024625, + "accuracyM": 20, + "headingDeg": 119.3, + "ts": 1760000197000, + "source": "webview" + }, + { + "lat": 3.1419911, + "lng": 101.7023944, + "accuracyM": 20, + "headingDeg": 120, + "ts": 1760000198000, + "source": "webview" + }, + { + "lat": 3.1421854, + "lng": 101.7025975, + "accuracyM": 20, + "headingDeg": 121.5, + "ts": 1760000199000, + "source": "webview" + }, + { + "lat": 3.1419163, + "lng": 101.7025202, + "accuracyM": 20, + "headingDeg": 121.5, + "ts": 1760000200000, + "source": "webview" + }, + { + "lat": 3.142107, + "lng": 101.7027255, + "accuracyM": 20, + "headingDeg": 121.5, + "ts": 1760000201000, + "source": "webview" + }, + { + "lat": 3.1418379, + "lng": 101.7026481, + "accuracyM": 20, + "headingDeg": 121.5, + "ts": 1760000202000, + "source": "webview" + }, + { + "lat": 3.1420286, + "lng": 101.7028534, + "accuracyM": 20, + "headingDeg": 121.5, + "ts": 1760000203000, + "source": "webview" + }, + { + "lat": 3.1417591, + "lng": 101.7027767, + "accuracyM": 20, + "headingDeg": 121.2, + "ts": 1760000204000, + "source": "webview" + }, + { + "lat": 3.1419525, + "lng": 101.7029783, + "accuracyM": 20, + "headingDeg": 119.9, + "ts": 1760000205000, + "source": "webview" + }, + { + "lat": 3.1416813, + "lng": 101.7029085, + "accuracyM": 20, + "headingDeg": 119.9, + "ts": 1760000206000, + "source": "webview" + }, + { + "lat": 3.1418734, + "lng": 101.7031154, + "accuracyM": 20, + "headingDeg": 123.4, + "ts": 1760000207000, + "source": "webview" + }, + { + "lat": 3.1416101, + "lng": 101.7030246, + "accuracyM": 20, + "headingDeg": 125.8, + "ts": 1760000208000, + "source": "webview" + }, + { + "lat": 3.1417857, + "lng": 101.7032427, + "accuracyM": 20, + "headingDeg": 125.4, + "ts": 1760000209000, + "source": "webview" + }, + { + "lat": 3.1415211, + "lng": 101.7031493, + "accuracyM": 20, + "headingDeg": 124.4, + "ts": 1760000210000, + "source": "webview" + }, + { + "lat": 3.1417013, + "lng": 101.7033639, + "accuracyM": 20, + "headingDeg": 124.4, + "ts": 1760000211000, + "source": "webview" + }, + { + "lat": 3.1414369, + "lng": 101.7032724, + "accuracyM": 20, + "headingDeg": 124.8, + "ts": 1760000212000, + "source": "webview" + }, + { + "lat": 3.1416162, + "lng": 101.7034874, + "accuracyM": 20, + "headingDeg": 124.4, + "ts": 1760000213000, + "source": "webview" + }, + { + "lat": 3.141347, + "lng": 101.7034034, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000214000, + "source": "webview" + }, + { + "lat": 3.1415396, + "lng": 101.703607, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000215000, + "source": "webview" + }, + { + "lat": 3.1412697, + "lng": 101.7035321, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000216000, + "source": "webview" + }, + { + "lat": 3.1414623, + "lng": 101.7037356, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000217000, + "source": "webview" + }, + { + "lat": 3.1411925, + "lng": 101.7036607, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000218000, + "source": "webview" + }, + { + "lat": 3.1413851, + "lng": 101.7038643, + "accuracyM": 20, + "headingDeg": 121, + "ts": 1760000219000, + "source": "webview" + }, + { + "lat": 3.1411137, + "lng": 101.7037919, + "accuracyM": 20, + "headingDeg": 119.8, + "ts": 1760000220000, + "source": "webview" + }, + { + "lat": 3.1413087, + "lng": 101.7039946, + "accuracyM": 20, + "headingDeg": 121.4, + "ts": 1760000221000, + "source": "webview" + }, + { + "lat": 3.1410374, + "lng": 101.7039212, + "accuracyM": 20, + "headingDeg": 119.7, + "ts": 1760000222000, + "source": "webview" + }, + { + "lat": 3.1412347, + "lng": 101.7041202, + "accuracyM": 20, + "headingDeg": 119.7, + "ts": 1760000223000, + "source": "webview" + }, + { + "lat": 3.140968, + "lng": 101.7040437, + "accuracyM": 20, + "headingDeg": 123.6, + "ts": 1760000224000, + "source": "webview" + }, + { + "lat": 3.1411466, + "lng": 101.7042623, + "accuracyM": 20, + "headingDeg": 127, + "ts": 1760000225000, + "source": "webview" + }, + { + "lat": 3.1408861, + "lng": 101.7041594, + "accuracyM": 20, + "headingDeg": 127, + "ts": 1760000226000, + "source": "webview" + }, + { + "lat": 3.1410563, + "lng": 101.7043821, + "accuracyM": 20, + "headingDeg": 127, + "ts": 1760000227000, + "source": "webview" + }, + { + "lat": 3.1407958, + "lng": 101.7042792, + "accuracyM": 20, + "headingDeg": 127, + "ts": 1760000228000, + "source": "webview" + }, + { + "lat": 3.140966, + "lng": 101.7045019, + "accuracyM": 20, + "headingDeg": 127, + "ts": 1760000229000, + "source": "webview" + }, + { + "lat": 3.1406802, + "lng": 101.7045027, + "accuracyM": 20, + "headingDeg": 80.5, + "ts": 1760000230000, + "source": "webview" + }, + { + "lat": 3.1409566, + "lng": 101.7045207, + "accuracyM": 20, + "headingDeg": 78.4, + "ts": 1760000231000, + "source": "webview" + }, + { + "lat": 3.1407103, + "lng": 101.7045781, + "accuracyM": 20, + "headingDeg": 107.5, + "ts": 1760000232000, + "source": "webview" + }, + { + "lat": 3.1409221, + "lng": 101.7047737, + "accuracyM": 20, + "headingDeg": 128.7, + "ts": 1760000233000, + "source": "webview" + } + ] +} diff --git a/web/test/fixtures/drives/missed-turn-excursion.json b/web/test/fixtures/drives/missed-turn-excursion.json new file mode 100644 index 0000000..fb5aa27 --- /dev/null +++ b/web/test/fixtures/drives/missed-turn-excursion.json @@ -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" + } + ] +} diff --git a/web/test/fixtures/drives/off-route-excursion.json b/web/test/fixtures/drives/off-route-excursion.json new file mode 100644 index 0000000..1a79522 --- /dev/null +++ b/web/test/fixtures/drives/off-route-excursion.json @@ -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" + } + ] +} diff --git a/web/test/guidance-replay.test.mjs b/web/test/guidance-replay.test.mjs new file mode 100644 index 0000000..9a30997 --- /dev/null +++ b/web/test/guidance-replay.test.mjs @@ -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"); +});