@ingissa/navcore-simulator 1.0.4 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -0
- package/README.md +10 -18
- package/dist/core/src/NavCore.d.ts +142 -0
- package/dist/core/src/NavCore.d.ts.map +1 -0
- package/dist/core/src/NavCore.js +390 -0
- package/dist/core/src/bearing/BearingEngine.d.ts +24 -0
- package/dist/core/src/bearing/BearingEngine.d.ts.map +1 -0
- package/dist/core/src/bearing/BearingEngine.js +50 -0
- package/dist/core/src/constants.d.ts +34 -0
- package/dist/core/src/constants.d.ts.map +1 -0
- package/dist/core/src/constants.js +41 -0
- package/dist/core/src/corridor/DeviationDetector.d.ts +33 -0
- package/dist/core/src/corridor/DeviationDetector.d.ts.map +1 -0
- package/dist/core/src/corridor/DeviationDetector.js +83 -0
- package/dist/core/src/corridor/DynamicCorridor.d.ts +13 -0
- package/dist/core/src/corridor/DynamicCorridor.d.ts.map +1 -0
- package/dist/core/src/corridor/DynamicCorridor.js +26 -0
- package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +35 -0
- package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +1 -0
- package/dist/core/src/dead-reckoning/DeadReckoning.js +49 -0
- package/dist/core/src/directions/DirectionsProvider.d.ts +70 -0
- package/dist/core/src/directions/DirectionsProvider.d.ts.map +1 -0
- package/dist/core/src/directions/DirectionsProvider.js +7 -0
- package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +26 -0
- package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +1 -0
- package/dist/core/src/directions/OSRMDirectionsProvider.js +90 -0
- package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +30 -0
- package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +1 -0
- package/dist/core/src/directions/OpenRouteServiceProvider.js +111 -0
- package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +33 -0
- package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +1 -0
- package/dist/core/src/directions/ValhallaDirectionsProvider.js +155 -0
- package/dist/core/src/eta/ETAEngine.d.ts +61 -0
- package/dist/core/src/eta/ETAEngine.d.ts.map +1 -0
- package/dist/core/src/eta/ETAEngine.js +74 -0
- package/dist/core/src/features.d.ts +76 -0
- package/dist/core/src/features.d.ts.map +1 -0
- package/dist/core/src/features.js +76 -0
- package/dist/core/src/geo/bearing.d.ts +11 -0
- package/dist/core/src/geo/bearing.d.ts.map +1 -0
- package/dist/core/src/geo/bearing.js +25 -0
- package/dist/core/src/geo/cumulative.d.ts +9 -0
- package/dist/core/src/geo/cumulative.d.ts.map +1 -0
- package/dist/core/src/geo/cumulative.js +20 -0
- package/dist/core/src/geo/haversine.d.ts +13 -0
- package/dist/core/src/geo/haversine.d.ts.map +1 -0
- package/dist/core/src/geo/haversine.js +39 -0
- package/dist/core/src/geo/projection.d.ts +7 -0
- package/dist/core/src/geo/projection.d.ts.map +1 -0
- package/dist/core/src/geo/projection.js +17 -0
- package/dist/core/src/geofencing/GeofencingEngine.d.ts +93 -0
- package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +1 -0
- package/dist/core/src/geofencing/GeofencingEngine.js +154 -0
- package/dist/core/src/index.d.ts +66 -0
- package/dist/core/src/index.d.ts.map +1 -0
- package/dist/core/src/index.js +62 -0
- package/dist/core/src/kalman/KalmanFilter2D.d.ts +46 -0
- package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +1 -0
- package/dist/core/src/kalman/KalmanFilter2D.js +77 -0
- package/dist/core/src/license/LicenseManager.d.ts +40 -0
- package/dist/core/src/license/LicenseManager.d.ts.map +1 -0
- package/dist/core/src/license/LicenseManager.js +119 -0
- package/dist/core/src/license/verifyP256Signature.d.ts +2 -0
- package/dist/core/src/license/verifyP256Signature.d.ts.map +1 -0
- package/dist/core/src/license/verifyP256Signature.js +26 -0
- package/dist/core/src/off-route/NavigationFSM.d.ts +42 -0
- package/dist/core/src/off-route/NavigationFSM.d.ts.map +1 -0
- package/dist/core/src/off-route/NavigationFSM.js +56 -0
- package/dist/core/src/off-route/OffRouteFSM.d.ts +70 -0
- package/dist/core/src/off-route/OffRouteFSM.d.ts.map +1 -0
- package/dist/core/src/off-route/OffRouteFSM.js +151 -0
- package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +24 -0
- package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +1 -0
- package/dist/core/src/predictor/RouteOffsetPredictor.js +55 -0
- package/dist/core/src/progress/InstructionEditor.d.ts +77 -0
- package/dist/core/src/progress/InstructionEditor.d.ts.map +1 -0
- package/dist/core/src/progress/InstructionEditor.js +151 -0
- package/dist/core/src/progress/InstructionResolver.d.ts +25 -0
- package/dist/core/src/progress/InstructionResolver.d.ts.map +1 -0
- package/dist/core/src/progress/InstructionResolver.js +78 -0
- package/dist/core/src/progress/ProgressTracker.d.ts +12 -0
- package/dist/core/src/progress/ProgressTracker.d.ts.map +1 -0
- package/dist/core/src/progress/ProgressTracker.js +22 -0
- package/dist/core/src/renderer/MapRendererAdapter.d.ts +72 -0
- package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +1 -0
- package/dist/core/src/renderer/MapRendererAdapter.js +19 -0
- package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +83 -0
- package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +1 -0
- package/dist/core/src/route-builder/CustomRouteBuilder.js +160 -0
- package/dist/core/src/route-plan/RoutePlan.d.ts +38 -0
- package/dist/core/src/route-plan/RoutePlan.d.ts.map +1 -0
- package/dist/core/src/route-plan/RoutePlan.js +102 -0
- package/dist/core/src/session/NavigationSession.d.ts +81 -0
- package/dist/core/src/session/NavigationSession.d.ts.map +1 -0
- package/dist/core/src/session/NavigationSession.js +115 -0
- package/dist/core/src/snapper/ParallelRoadResolver.d.ts +86 -0
- package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +1 -0
- package/dist/core/src/snapper/ParallelRoadResolver.js +83 -0
- package/dist/core/src/snapper/RouteSnapper.d.ts +38 -0
- package/dist/core/src/snapper/RouteSnapper.d.ts.map +1 -0
- package/dist/core/src/snapper/RouteSnapper.js +80 -0
- package/dist/core/src/voice/VoiceTriggerEngine.d.ts +80 -0
- package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +1 -0
- package/dist/core/src/voice/VoiceTriggerEngine.js +108 -0
- package/dist/simulator/src/core/NoiseEngine.d.ts +46 -0
- package/dist/simulator/src/core/NoiseEngine.d.ts.map +1 -0
- package/dist/simulator/src/core/NoiseEngine.js +106 -0
- package/dist/simulator/src/core/RouteWalker.d.ts +78 -0
- package/dist/simulator/src/core/RouteWalker.d.ts.map +1 -0
- package/dist/simulator/src/core/RouteWalker.js +180 -0
- package/dist/simulator/src/core/SimulatorEngine.d.ts +53 -0
- package/dist/simulator/src/core/SimulatorEngine.d.ts.map +1 -0
- package/dist/simulator/src/core/SimulatorEngine.js +298 -0
- package/dist/simulator/src/core/types.d.ts +157 -0
- package/dist/simulator/src/core/types.d.ts.map +1 -0
- package/dist/simulator/src/core/types.js +7 -0
- package/dist/simulator/src/fleet/FleetSimulator.d.ts +60 -0
- package/dist/simulator/src/fleet/FleetSimulator.d.ts.map +1 -0
- package/dist/simulator/src/fleet/FleetSimulator.js +114 -0
- package/dist/simulator/src/index.d.ts +28 -0
- package/dist/simulator/src/index.d.ts.map +1 -0
- package/dist/simulator/src/index.js +29 -0
- package/dist/simulator/src/node.d.ts +26 -0
- package/dist/simulator/src/node.d.ts.map +1 -0
- package/dist/simulator/src/node.js +25 -0
- package/dist/simulator/src/react-native/SimulatorControls.d.ts +35 -0
- package/dist/simulator/src/react-native/SimulatorControls.d.ts.map +1 -0
- package/dist/simulator/src/react-native/SimulatorControls.js +254 -0
- package/dist/simulator/src/react-native/useSimulator.d.ts +59 -0
- package/dist/simulator/src/react-native/useSimulator.d.ts.map +1 -0
- package/dist/simulator/src/react-native/useSimulator.js +100 -0
- package/dist/simulator/src/replay/GpxParser.d.ts +40 -0
- package/dist/simulator/src/replay/GpxParser.d.ts.map +1 -0
- package/dist/simulator/src/replay/GpxParser.js +95 -0
- package/dist/simulator/src/replay/SessionRecorder.d.ts +37 -0
- package/dist/simulator/src/replay/SessionRecorder.d.ts.map +1 -0
- package/dist/simulator/src/replay/SessionRecorder.js +120 -0
- package/dist/simulator/src/scenarios/ScenarioInjector.d.ts +55 -0
- package/dist/simulator/src/scenarios/ScenarioInjector.d.ts.map +1 -0
- package/dist/simulator/src/scenarios/ScenarioInjector.js +150 -0
- package/package.json +23 -23
- package/src/__tests__/integration/simulator-pipeline.test.ts +233 -0
- package/src/core/NoiseEngine.ts +137 -0
- package/src/core/RouteWalker.ts +227 -0
- package/src/core/SimulatorEngine.ts +364 -0
- package/src/core/__tests__/NoiseEngine.test.ts +131 -0
- package/src/core/__tests__/RouteWalker.test.ts +139 -0
- package/src/core/__tests__/SimulatorEngine.test.ts +317 -0
- package/src/core/types.ts +191 -0
- package/src/fleet/FleetSimulator.ts +162 -0
- package/src/index.ts +54 -0
- package/src/node.ts +48 -0
- package/src/react-native/SimulatorControls.tsx +473 -0
- package/src/react-native/useSimulator.ts +155 -0
- package/src/replay/GpxParser.ts +122 -0
- package/src/replay/SessionRecorder.ts +138 -0
- package/src/replay/__tests__/GpxParser.test.ts +136 -0
- package/src/scenarios/ScenarioInjector.ts +170 -0
- package/src/scenarios/__tests__/ScenarioInjector.test.ts +171 -0
- package/dist/index-i9TY71RY.d.mts +0 -494
- package/dist/index-i9TY71RY.d.ts +0 -494
- package/dist/index.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -937
- package/dist/index.mjs +0 -929
- package/dist/node.d.mts +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -937
- package/dist/node.mjs +0 -929
- package/dist/react-native.d.mts +0 -82
- package/dist/react-native.d.ts +0 -82
- package/dist/react-native.js +0 -1370
- package/dist/react-native.mjs +0 -1360
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParallelRoadResolver
|
|
3
|
+
*
|
|
4
|
+
* First-class feature: resolve GPS ambiguity on parallel roads (e.g. dual carriageways,
|
|
5
|
+
* motorway + service road) and prevent U-turn snap.
|
|
6
|
+
*
|
|
7
|
+
* Extracts and extends the bearing penalty + inertia logic from RouteSnapper
|
|
8
|
+
* into a standalone, composable API that can be used independently.
|
|
9
|
+
*
|
|
10
|
+
* FEATURE: PARALLEL_ROAD_RESOLUTION + U_TURN_PROTECTION
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const resolver = new ParallelRoadResolver({ bearingPenalty: 80, inertiaPenalty: 40 });
|
|
15
|
+
* const best = resolver.resolve(candidates, vehicleBearing, prevSegmentIndex);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { getBearingBetweenPoints, normalizeAngle } from '../geo/bearing';
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
bearingPenalty: 80,
|
|
21
|
+
bearingMismatchThresholdDeg: 60,
|
|
22
|
+
inertiaPenalty: 40,
|
|
23
|
+
inertiaSegmentDist: 3,
|
|
24
|
+
};
|
|
25
|
+
export class ParallelRoadResolver {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.options = { ...DEFAULTS, ...options };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Score and sort snap candidates — lowest score wins.
|
|
31
|
+
*
|
|
32
|
+
* @param candidates Raw snap candidates (distance-sorted or unsorted)
|
|
33
|
+
* @param vehicleBearing Current vehicle heading in degrees (null disables bearing check)
|
|
34
|
+
* @param prevSegmentIdx Previously snapped segment index (null disables inertia)
|
|
35
|
+
* @returns Candidates sorted by score ascending, with debug metadata attached
|
|
36
|
+
*/
|
|
37
|
+
resolve(candidates, vehicleBearing = null, prevSegmentIdx = null) {
|
|
38
|
+
return candidates
|
|
39
|
+
.map(c => this.score(c, vehicleBearing, prevSegmentIdx))
|
|
40
|
+
.sort((a, b) => a.score - b.score);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve and return only the best candidate.
|
|
44
|
+
*/
|
|
45
|
+
resolveBest(candidates, vehicleBearing = null, prevSegmentIdx = null) {
|
|
46
|
+
if (candidates.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
const resolved = this.resolve(candidates, vehicleBearing, prevSegmentIdx);
|
|
49
|
+
return resolved[0] ?? null;
|
|
50
|
+
}
|
|
51
|
+
score(c, vehicleBearing, prevSegmentIdx) {
|
|
52
|
+
let score = c.distanceMetres;
|
|
53
|
+
let bearingDeltaDeg = null;
|
|
54
|
+
let inertiaPenaltyApplied = false;
|
|
55
|
+
// ── U-turn / parallel road bearing penalty ───────────────────────────────
|
|
56
|
+
if (vehicleBearing !== null) {
|
|
57
|
+
const segBearing = getBearingBetweenPoints(c.segA, c.segB);
|
|
58
|
+
const delta = Math.abs(normalizeAngle(vehicleBearing - segBearing));
|
|
59
|
+
bearingDeltaDeg = delta;
|
|
60
|
+
if (delta > this.options.bearingMismatchThresholdDeg) {
|
|
61
|
+
score += this.options.bearingPenalty;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ── Inertia penalty (anti-oscillation) ───────────────────────────────────
|
|
65
|
+
if (prevSegmentIdx !== null &&
|
|
66
|
+
Math.abs(c.segmentIndex - prevSegmentIdx) >= this.options.inertiaSegmentDist) {
|
|
67
|
+
score += this.options.inertiaPenalty;
|
|
68
|
+
inertiaPenaltyApplied = true;
|
|
69
|
+
}
|
|
70
|
+
return { ...c, score, bearingDeltaDeg, inertiaPenaltyApplied };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Quick utility: detect if the vehicle is likely performing a U-turn
|
|
74
|
+
* based on the difference between current bearing and route segment bearing.
|
|
75
|
+
*
|
|
76
|
+
* @returns true if the bearing difference exceeds the configured threshold
|
|
77
|
+
*/
|
|
78
|
+
isLikelyUTurn(vehicleBearing, segA, segB) {
|
|
79
|
+
const segBearing = getBearingBetweenPoints(segA, segB);
|
|
80
|
+
const delta = Math.abs(normalizeAngle(vehicleBearing - segBearing));
|
|
81
|
+
return delta > this.options.bearingMismatchThresholdDeg;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module 2: RouteSnapper
|
|
3
|
+
* Projects GPS coordinates onto the route with anti-oscillation and U-turn protection.
|
|
4
|
+
* Merges BasicSnapper + AdvancedSnapper from nav-sdk into a single module.
|
|
5
|
+
*/
|
|
6
|
+
import { type Coordinate } from '../geo/haversine';
|
|
7
|
+
export interface SnapResult {
|
|
8
|
+
idx: number;
|
|
9
|
+
dist: number;
|
|
10
|
+
proj: Coordinate;
|
|
11
|
+
}
|
|
12
|
+
export interface SnapperOptions {
|
|
13
|
+
/** Enable bearing-based U-turn anti-snap penalty */
|
|
14
|
+
useBearingPenalty?: boolean;
|
|
15
|
+
/** Enable inertia penalty to prevent oscillation between segments */
|
|
16
|
+
useInertiaPenalty?: boolean;
|
|
17
|
+
/** Max search distance hard cutoff (metres) */
|
|
18
|
+
maxSnapDistance?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class RouteSnapper {
|
|
21
|
+
private readonly options;
|
|
22
|
+
constructor(options?: SnapperOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Find the nearest projection of a GPS point onto the route.
|
|
25
|
+
*
|
|
26
|
+
* @param gps Current GPS coordinate [lng, lat]
|
|
27
|
+
* @param route Route geometry
|
|
28
|
+
* @param startIdx Search window start
|
|
29
|
+
* @param windowSize Number of segments to scan
|
|
30
|
+
* @param isCircuit Whether the route is a closed circuit
|
|
31
|
+
* @param maxProgressIdx Highest index ever reached (for circuit wrap detection)
|
|
32
|
+
* @param fallbackIdx Fallback index if no match found
|
|
33
|
+
* @param vehicleBearing Current vehicle bearing (null disables bearing penalty)
|
|
34
|
+
* @param prevSegment Previous snapped segment index (null disables inertia)
|
|
35
|
+
*/
|
|
36
|
+
snap(gps: Coordinate, route: Coordinate[], startIdx: number, windowSize: number, isCircuit: boolean, maxProgressIdx: number, fallbackIdx: number, vehicleBearing?: number | null, prevSegment?: number | null): SnapResult;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=RouteSnapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteSnapper.d.ts","sourceRoot":"","sources":["../../../../../core/src/snapper/RouteSnapper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,kBAAkB,CAAC;AAWtE,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAQD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;gBAEvC,OAAO,CAAC,EAAE,cAAc;IAIpC;;;;;;;;;;;;OAYG;IACH,IAAI,CACF,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,OAAO,EAClB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,MAAM,GAAG,IAAW,EACpC,WAAW,GAAE,MAAM,GAAG,IAAW,GAChC,UAAU;CAuDd"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module 2: RouteSnapper
|
|
3
|
+
* Projects GPS coordinates onto the route with anti-oscillation and U-turn protection.
|
|
4
|
+
* Merges BasicSnapper + AdvancedSnapper from nav-sdk into a single module.
|
|
5
|
+
*/
|
|
6
|
+
import { haversineDistance } from '../geo/haversine';
|
|
7
|
+
import { projectPointOnSegment } from '../geo/projection';
|
|
8
|
+
import { getBearingBetweenPoints, normalizeAngle } from '../geo/bearing';
|
|
9
|
+
import { MAX_SNAP_DISTANCE, BEARING_MISMATCH_THRESHOLD, BEARING_MISMATCH_PENALTY, INERTIA_PENALTY, INERTIA_SEGMENT_DIST } from '../constants';
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
useBearingPenalty: true,
|
|
12
|
+
useInertiaPenalty: true,
|
|
13
|
+
maxSnapDistance: MAX_SNAP_DISTANCE,
|
|
14
|
+
};
|
|
15
|
+
export class RouteSnapper {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Find the nearest projection of a GPS point onto the route.
|
|
21
|
+
*
|
|
22
|
+
* @param gps Current GPS coordinate [lng, lat]
|
|
23
|
+
* @param route Route geometry
|
|
24
|
+
* @param startIdx Search window start
|
|
25
|
+
* @param windowSize Number of segments to scan
|
|
26
|
+
* @param isCircuit Whether the route is a closed circuit
|
|
27
|
+
* @param maxProgressIdx Highest index ever reached (for circuit wrap detection)
|
|
28
|
+
* @param fallbackIdx Fallback index if no match found
|
|
29
|
+
* @param vehicleBearing Current vehicle bearing (null disables bearing penalty)
|
|
30
|
+
* @param prevSegment Previous snapped segment index (null disables inertia)
|
|
31
|
+
*/
|
|
32
|
+
snap(gps, route, startIdx, windowSize, isCircuit, maxProgressIdx, fallbackIdx, vehicleBearing = null, prevSegment = null) {
|
|
33
|
+
const routeLength = route.length;
|
|
34
|
+
let bestIdx = fallbackIdx;
|
|
35
|
+
let bestScore = Infinity;
|
|
36
|
+
let bestDist = Infinity;
|
|
37
|
+
let bestProj = route[fallbackIdx] ?? [0, 0];
|
|
38
|
+
for (let step = 0; step <= windowSize; step++) {
|
|
39
|
+
let i = startIdx + step;
|
|
40
|
+
if (i > routeLength - 2) {
|
|
41
|
+
if (isCircuit && maxProgressIdx > routeLength * 0.7) {
|
|
42
|
+
i = i % (routeLength - 1);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (i < 0 || i >= routeLength - 1)
|
|
49
|
+
continue;
|
|
50
|
+
const segA = route[i];
|
|
51
|
+
const segB = route[i + 1];
|
|
52
|
+
const p = projectPointOnSegment(gps, segA, segB);
|
|
53
|
+
const d = haversineDistance(gps[1], gps[0], p[1], p[0]);
|
|
54
|
+
// Hard cutoff
|
|
55
|
+
if (d > this.options.maxSnapDistance * 2)
|
|
56
|
+
continue;
|
|
57
|
+
let score = d;
|
|
58
|
+
// ── Bearing penalty (U-turn anti-snap) ──────────────────────────────
|
|
59
|
+
if (this.options.useBearingPenalty && vehicleBearing !== null) {
|
|
60
|
+
const segBearing = getBearingBetweenPoints(segA, segB);
|
|
61
|
+
const bearingDiff = Math.abs(normalizeAngle(vehicleBearing - segBearing));
|
|
62
|
+
if (bearingDiff > BEARING_MISMATCH_THRESHOLD) {
|
|
63
|
+
score += BEARING_MISMATCH_PENALTY;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── Inertia penalty ─────────────────────────────────────────────────
|
|
67
|
+
if (this.options.useInertiaPenalty && prevSegment !== null &&
|
|
68
|
+
Math.abs(i - prevSegment) >= INERTIA_SEGMENT_DIST) {
|
|
69
|
+
score += INERTIA_PENALTY;
|
|
70
|
+
}
|
|
71
|
+
if (score < bestScore) {
|
|
72
|
+
bestScore = score;
|
|
73
|
+
bestDist = d;
|
|
74
|
+
bestIdx = i;
|
|
75
|
+
bestProj = p;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { idx: bestIdx, dist: bestDist, proj: bestProj };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceTriggerEngine
|
|
3
|
+
*
|
|
4
|
+
* Converts NavCore instruction events into timed voice cues.
|
|
5
|
+
* Handles deduplication, look-ahead windows, and priority levels.
|
|
6
|
+
* Consumer calls TTS — this engine only computes WHEN to speak.
|
|
7
|
+
*
|
|
8
|
+
* FEATURE: VOICE_TRIGGERS
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const voice = new VoiceTriggerEngine({ earlyTriggerMeters: 150 });
|
|
13
|
+
* voice.setInstructions(instructions);
|
|
14
|
+
*
|
|
15
|
+
* engine.on('instruction', (instr) => {
|
|
16
|
+
* const cue = voice.onInstruction(instr, state);
|
|
17
|
+
* if (cue) speakText(cue.text);
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Or poll each GPS tick:
|
|
21
|
+
* const cue = voice.update(state);
|
|
22
|
+
* if (cue) speakText(cue.text);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import type { NavInstruction } from '../progress/InstructionResolver';
|
|
26
|
+
import type { NavCoreState } from '../NavCore';
|
|
27
|
+
export type VoicePriority = 'low' | 'normal' | 'high' | 'urgent';
|
|
28
|
+
export interface VoiceCue {
|
|
29
|
+
/** The instruction that triggered this cue */
|
|
30
|
+
instruction: NavInstruction;
|
|
31
|
+
/** Text to pass to TTS */
|
|
32
|
+
text: string;
|
|
33
|
+
/** Distance to the instruction point in metres */
|
|
34
|
+
distanceMetres: number;
|
|
35
|
+
/** Priority hint for TTS queue management */
|
|
36
|
+
priority: VoicePriority;
|
|
37
|
+
/** Monotonic trigger ID — use this to deduplicate */
|
|
38
|
+
triggerId: string;
|
|
39
|
+
}
|
|
40
|
+
export interface VoiceTriggerOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Trigger voice this many metres before the instruction point.
|
|
43
|
+
* Default: 120m (comfortable at 50 km/h).
|
|
44
|
+
*/
|
|
45
|
+
earlyTriggerMeters?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Stop trying to trigger if vehicle is already past this many metres.
|
|
48
|
+
* Default: 20m.
|
|
49
|
+
*/
|
|
50
|
+
lateTriggerMeters?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Re-announce if user is still approaching after this distance.
|
|
53
|
+
* Set to 0 to disable re-announcement. Default: 300m.
|
|
54
|
+
*/
|
|
55
|
+
reannounceMeters?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare class VoiceTriggerEngine {
|
|
58
|
+
private instructions;
|
|
59
|
+
private triggeredIds;
|
|
60
|
+
private reannouncedIds;
|
|
61
|
+
private readonly options;
|
|
62
|
+
constructor(options?: VoiceTriggerOptions);
|
|
63
|
+
/** Load the instruction set (call after engine.setRoute) */
|
|
64
|
+
setInstructions(instructions: NavInstruction[]): void;
|
|
65
|
+
/**
|
|
66
|
+
* Poll on every GPS state update.
|
|
67
|
+
* Returns a `VoiceCue` when an instruction should be spoken, or null.
|
|
68
|
+
*/
|
|
69
|
+
update(state: NavCoreState): VoiceCue | null;
|
|
70
|
+
/**
|
|
71
|
+
* Called directly from engine's 'instruction' event.
|
|
72
|
+
* Always fires immediately regardless of distance window.
|
|
73
|
+
*/
|
|
74
|
+
onInstruction(instr: NavInstruction, state: NavCoreState): VoiceCue;
|
|
75
|
+
/** Reset triggered state (call on setRoute / new journey) */
|
|
76
|
+
reset(): void;
|
|
77
|
+
private instrId;
|
|
78
|
+
private makeCue;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=VoiceTriggerEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceTriggerEngine.d.ts","sourceRoot":"","sources":["../../../../../core/src/voice/VoiceTriggerEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,QAAQ;IACvB,8CAA8C;IAC9C,WAAW,EAAE,cAAc,CAAC;IAC5B,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,QAAQ,EAAE,aAAa,CAAC;IACxB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAQD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;gBAE5C,OAAO,CAAC,EAAE,mBAAmB;IAIzC,4DAA4D;IAC5D,eAAe,CAAC,YAAY,EAAE,cAAc,EAAE,GAAG,IAAI;IAMrD;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,QAAQ,GAAG,IAAI;IA6C5C;;;OAGG;IACH,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,QAAQ;IAKnE,6DAA6D;IAC7D,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,OAAO;CAShB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceTriggerEngine
|
|
3
|
+
*
|
|
4
|
+
* Converts NavCore instruction events into timed voice cues.
|
|
5
|
+
* Handles deduplication, look-ahead windows, and priority levels.
|
|
6
|
+
* Consumer calls TTS — this engine only computes WHEN to speak.
|
|
7
|
+
*
|
|
8
|
+
* FEATURE: VOICE_TRIGGERS
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const voice = new VoiceTriggerEngine({ earlyTriggerMeters: 150 });
|
|
13
|
+
* voice.setInstructions(instructions);
|
|
14
|
+
*
|
|
15
|
+
* engine.on('instruction', (instr) => {
|
|
16
|
+
* const cue = voice.onInstruction(instr, state);
|
|
17
|
+
* if (cue) speakText(cue.text);
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Or poll each GPS tick:
|
|
21
|
+
* const cue = voice.update(state);
|
|
22
|
+
* if (cue) speakText(cue.text);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULTS = {
|
|
26
|
+
earlyTriggerMeters: 120,
|
|
27
|
+
lateTriggerMeters: 20,
|
|
28
|
+
reannounceMeters: 300,
|
|
29
|
+
};
|
|
30
|
+
export class VoiceTriggerEngine {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.instructions = [];
|
|
33
|
+
this.triggeredIds = new Set();
|
|
34
|
+
this.reannouncedIds = new Set();
|
|
35
|
+
this.options = { ...DEFAULTS, ...options };
|
|
36
|
+
}
|
|
37
|
+
/** Load the instruction set (call after engine.setRoute) */
|
|
38
|
+
setInstructions(instructions) {
|
|
39
|
+
this.instructions = [...instructions];
|
|
40
|
+
this.triggeredIds.clear();
|
|
41
|
+
this.reannouncedIds.clear();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Poll on every GPS state update.
|
|
45
|
+
* Returns a `VoiceCue` when an instruction should be spoken, or null.
|
|
46
|
+
*/
|
|
47
|
+
update(state) {
|
|
48
|
+
const { distanceToNextInstruction, nextInstructionIndex } = state;
|
|
49
|
+
if (nextInstructionIndex === null ||
|
|
50
|
+
distanceToNextInstruction === null ||
|
|
51
|
+
nextInstructionIndex >= this.instructions.length) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const instr = this.instructions[nextInstructionIndex];
|
|
55
|
+
const id = this.instrId(nextInstructionIndex);
|
|
56
|
+
const dist = distanceToNextInstruction;
|
|
57
|
+
// Already fully triggered — don't repeat
|
|
58
|
+
if (this.triggeredIds.has(id)) {
|
|
59
|
+
// Check re-announce window
|
|
60
|
+
if (this.options.reannounceMeters > 0 &&
|
|
61
|
+
!this.reannouncedIds.has(id) &&
|
|
62
|
+
dist <= this.options.reannounceMeters &&
|
|
63
|
+
dist > this.options.earlyTriggerMeters) {
|
|
64
|
+
this.reannouncedIds.add(id);
|
|
65
|
+
return this.makeCue(instr, dist, 'low');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Too far — not yet
|
|
70
|
+
if (dist > this.options.earlyTriggerMeters)
|
|
71
|
+
return null;
|
|
72
|
+
// Already passed — too late
|
|
73
|
+
if (dist < -this.options.lateTriggerMeters) {
|
|
74
|
+
this.triggeredIds.add(id); // mark so we skip
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// In the trigger window — fire!
|
|
78
|
+
this.triggeredIds.add(id);
|
|
79
|
+
const priority = dist < 30 ? 'urgent' : dist < 60 ? 'high' : 'normal';
|
|
80
|
+
return this.makeCue(instr, dist, priority);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Called directly from engine's 'instruction' event.
|
|
84
|
+
* Always fires immediately regardless of distance window.
|
|
85
|
+
*/
|
|
86
|
+
onInstruction(instr, state) {
|
|
87
|
+
const dist = state.distanceToNextInstruction ?? 0;
|
|
88
|
+
return this.makeCue(instr, dist, 'high');
|
|
89
|
+
}
|
|
90
|
+
/** Reset triggered state (call on setRoute / new journey) */
|
|
91
|
+
reset() {
|
|
92
|
+
this.triggeredIds.clear();
|
|
93
|
+
this.reannouncedIds.clear();
|
|
94
|
+
}
|
|
95
|
+
instrId(index) {
|
|
96
|
+
const instr = this.instructions[index];
|
|
97
|
+
return `${index}_${instr?.geometryIndex ?? 0}`;
|
|
98
|
+
}
|
|
99
|
+
makeCue(instr, dist, priority) {
|
|
100
|
+
return {
|
|
101
|
+
instruction: instr,
|
|
102
|
+
text: instr.text ?? instr.instruction ?? '',
|
|
103
|
+
distanceMetres: Math.max(0, dist),
|
|
104
|
+
priority,
|
|
105
|
+
triggerId: `${instr.geometryIndex}_${dist.toFixed(0)}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoiseEngine — GPS noise simulation.
|
|
3
|
+
*
|
|
4
|
+
* Formalises the ±0.00011° noise from the original useGpsSimulator
|
|
5
|
+
* into a testable, seeded, swappable noise model.
|
|
6
|
+
*
|
|
7
|
+
* Box-Muller transform for Gaussian sampling — ZERO external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Degrees-per-metre constants:
|
|
10
|
+
* 1° latitude ≈ 110540 m
|
|
11
|
+
* 1° longitude ≈ 111320 m (at equator; good enough for noise simulation)
|
|
12
|
+
*/
|
|
13
|
+
import type { Coordinate } from '@ingissa/navcore-core';
|
|
14
|
+
import type { NoiseProfile } from './types';
|
|
15
|
+
export declare class NoiseEngine {
|
|
16
|
+
private profile;
|
|
17
|
+
private rng;
|
|
18
|
+
private _tickIndex;
|
|
19
|
+
constructor(profile: NoiseProfile);
|
|
20
|
+
/** No noise — returns the exact coordinate */
|
|
21
|
+
static readonly NONE: NoiseProfile;
|
|
22
|
+
/** Good urban GPS — ~3m standard deviation */
|
|
23
|
+
static readonly CLEAN: NoiseProfile;
|
|
24
|
+
/** Standard Go4Exams value — ~12m standard deviation (matches original ±0.00011°) */
|
|
25
|
+
static readonly STANDARD: NoiseProfile;
|
|
26
|
+
/** Dense city canyon — ~25m standard deviation */
|
|
27
|
+
static readonly URBAN: NoiseProfile;
|
|
28
|
+
/** Bad conditions — ~50m standard deviation */
|
|
29
|
+
static readonly DEGRADED: NoiseProfile;
|
|
30
|
+
/**
|
|
31
|
+
* Apply noise to a coordinate.
|
|
32
|
+
* Returns a new coordinate — never mutates the input.
|
|
33
|
+
*/
|
|
34
|
+
apply(coord: Coordinate): Coordinate;
|
|
35
|
+
/** Hot-swap the noise profile mid-simulation */
|
|
36
|
+
setProfile(profile: NoiseProfile): void;
|
|
37
|
+
get currentProfile(): NoiseProfile;
|
|
38
|
+
/**
|
|
39
|
+
* Box-Muller transform: produces Gaussian N(0, stdDev²) offset.
|
|
40
|
+
* Returns [lngOffset, latOffset] in degrees.
|
|
41
|
+
*/
|
|
42
|
+
private _gaussianOffset;
|
|
43
|
+
/** Returns a float in [0, 1) — uses seeded RNG if available, else Math.random */
|
|
44
|
+
private _rand;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=NoiseEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NoiseEngine.d.ts","sourceRoot":"","sources":["../../../../src/core/NoiseEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyB5C,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,EAAE,YAAY;IAOjC,8CAA8C;IAC9C,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAgC;IAElE,8CAA8C;IAC9C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAoC;IAEvE,qFAAqF;IACrF,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAqC;IAE3E,kDAAkD;IAClD,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAkC;IAErE,+CAA+C;IAC/C,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAqC;IAI3E;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IA6BpC,gDAAgD;IAChD,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAMvC,IAAI,cAAc,IAAI,YAAY,CAEjC;IAID;;;OAGG;IACH,OAAO,CAAC,eAAe;IAcvB,iFAAiF;IACjF,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoiseEngine — GPS noise simulation.
|
|
3
|
+
*
|
|
4
|
+
* Formalises the ±0.00011° noise from the original useGpsSimulator
|
|
5
|
+
* into a testable, seeded, swappable noise model.
|
|
6
|
+
*
|
|
7
|
+
* Box-Muller transform for Gaussian sampling — ZERO external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Degrees-per-metre constants:
|
|
10
|
+
* 1° latitude ≈ 110540 m
|
|
11
|
+
* 1° longitude ≈ 111320 m (at equator; good enough for noise simulation)
|
|
12
|
+
*/
|
|
13
|
+
// Conversion factors
|
|
14
|
+
const LAT_DEG_PER_M = 1 / 110540;
|
|
15
|
+
const LNG_DEG_PER_M = 1 / 111320;
|
|
16
|
+
/**
|
|
17
|
+
* Simple seeded LCG (Linear Congruential Generator).
|
|
18
|
+
* Same seed guarantees identical noise sequences — critical for CI regression.
|
|
19
|
+
*/
|
|
20
|
+
class SeededRandom {
|
|
21
|
+
constructor(seed) {
|
|
22
|
+
this.state = seed >>> 0; // ensure unsigned 32-bit
|
|
23
|
+
}
|
|
24
|
+
/** Returns a float in [0, 1) */
|
|
25
|
+
next() {
|
|
26
|
+
// LCG parameters from Numerical Recipes
|
|
27
|
+
this.state = (Math.imul(1664525, this.state) + 1013904223) >>> 0;
|
|
28
|
+
return this.state / 0x100000000;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class NoiseEngine {
|
|
32
|
+
constructor(profile) {
|
|
33
|
+
this._tickIndex = 0;
|
|
34
|
+
this.profile = profile;
|
|
35
|
+
this.rng = profile.seed !== undefined ? new SeededRandom(profile.seed) : null;
|
|
36
|
+
}
|
|
37
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Apply noise to a coordinate.
|
|
40
|
+
* Returns a new coordinate — never mutates the input.
|
|
41
|
+
*/
|
|
42
|
+
apply(coord) {
|
|
43
|
+
const idx = this._tickIndex++;
|
|
44
|
+
switch (this.profile.type) {
|
|
45
|
+
case 'NONE':
|
|
46
|
+
return coord;
|
|
47
|
+
case 'GAUSSIAN':
|
|
48
|
+
case 'URBAN': {
|
|
49
|
+
const [dLng, dLat] = this._gaussianOffset(this.profile.stdDevM);
|
|
50
|
+
return [coord[0] + dLng, coord[1] + dLat];
|
|
51
|
+
}
|
|
52
|
+
case 'TUNNEL':
|
|
53
|
+
// Signal loss: return a fixed position (last known would be handled by engine)
|
|
54
|
+
// Here we just return the exact coord (engine handles blackout via scenario)
|
|
55
|
+
return coord;
|
|
56
|
+
case 'CUSTOM': {
|
|
57
|
+
if (!this.profile.customFn)
|
|
58
|
+
return coord;
|
|
59
|
+
const [lngOff, latOff] = this.profile.customFn(idx);
|
|
60
|
+
return [coord[0] + lngOff, coord[1] + latOff];
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
return coord;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Hot-swap the noise profile mid-simulation */
|
|
67
|
+
setProfile(profile) {
|
|
68
|
+
this.profile = profile;
|
|
69
|
+
this.rng = profile.seed !== undefined ? new SeededRandom(profile.seed) : null;
|
|
70
|
+
this._tickIndex = 0;
|
|
71
|
+
}
|
|
72
|
+
get currentProfile() {
|
|
73
|
+
return this.profile;
|
|
74
|
+
}
|
|
75
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Box-Muller transform: produces Gaussian N(0, stdDev²) offset.
|
|
78
|
+
* Returns [lngOffset, latOffset] in degrees.
|
|
79
|
+
*/
|
|
80
|
+
_gaussianOffset(stdDevM) {
|
|
81
|
+
if (stdDevM === 0)
|
|
82
|
+
return [0, 0];
|
|
83
|
+
const u1 = this._rand();
|
|
84
|
+
const u2 = this._rand();
|
|
85
|
+
// Box-Muller
|
|
86
|
+
const mag = stdDevM * Math.sqrt(-2 * Math.log(Math.max(u1, 1e-10)));
|
|
87
|
+
const z0 = mag * Math.cos(2 * Math.PI * u2);
|
|
88
|
+
const z1 = mag * Math.sin(2 * Math.PI * u2);
|
|
89
|
+
return [z0 * LNG_DEG_PER_M, z1 * LAT_DEG_PER_M];
|
|
90
|
+
}
|
|
91
|
+
/** Returns a float in [0, 1) — uses seeded RNG if available, else Math.random */
|
|
92
|
+
_rand() {
|
|
93
|
+
return this.rng ? this.rng.next() : Math.random();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ── Static presets ─────────────────────────────────────────────────────────
|
|
97
|
+
/** No noise — returns the exact coordinate */
|
|
98
|
+
NoiseEngine.NONE = { type: 'NONE', stdDevM: 0 };
|
|
99
|
+
/** Good urban GPS — ~3m standard deviation */
|
|
100
|
+
NoiseEngine.CLEAN = { type: 'GAUSSIAN', stdDevM: 3 };
|
|
101
|
+
/** Standard Go4Exams value — ~12m standard deviation (matches original ±0.00011°) */
|
|
102
|
+
NoiseEngine.STANDARD = { type: 'GAUSSIAN', stdDevM: 12 };
|
|
103
|
+
/** Dense city canyon — ~25m standard deviation */
|
|
104
|
+
NoiseEngine.URBAN = { type: 'URBAN', stdDevM: 25 };
|
|
105
|
+
/** Bad conditions — ~50m standard deviation */
|
|
106
|
+
NoiseEngine.DEGRADED = { type: 'GAUSSIAN', stdDevM: 50 };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RouteWalker — Manages traversal of a route geometry array.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the inline useGpsSimulator hook so the traversal logic
|
|
5
|
+
* is testable independently of React, timers, or noise.
|
|
6
|
+
*
|
|
7
|
+
* ZERO dependencies beyond @navcore/core types.
|
|
8
|
+
*/
|
|
9
|
+
import type { Coordinate } from '@ingissa/navcore-core';
|
|
10
|
+
export interface WalkerPosition {
|
|
11
|
+
/** Current floating-point index (e.g. 3.75 = between points 3 and 4) */
|
|
12
|
+
index: number;
|
|
13
|
+
/** Interpolated coordinate at the current index */
|
|
14
|
+
coord: Coordinate;
|
|
15
|
+
/** Forward-looking bearing at the current segment */
|
|
16
|
+
bearing: number;
|
|
17
|
+
/** Total metres travelled from route start */
|
|
18
|
+
distanceTravelled: number;
|
|
19
|
+
/** True if the walker has reached or passed the last point */
|
|
20
|
+
isComplete: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class RouteWalker {
|
|
23
|
+
private readonly geometry;
|
|
24
|
+
private _index;
|
|
25
|
+
private _distanceTravelled;
|
|
26
|
+
private readonly _cumulativeDistances;
|
|
27
|
+
private _isComplete;
|
|
28
|
+
constructor(geometry: Coordinate[], startIndex?: number);
|
|
29
|
+
/**
|
|
30
|
+
* Advance the walker by `steps` geometry points.
|
|
31
|
+
* Fractional steps are supported for sub-second precision.
|
|
32
|
+
*/
|
|
33
|
+
advance(steps: number): WalkerPosition;
|
|
34
|
+
/**
|
|
35
|
+
* Advance the walker by a physical distance in metres.
|
|
36
|
+
* This is the preferred way for smooth constant-speed simulation.
|
|
37
|
+
*/
|
|
38
|
+
advanceByDistance(meters: number): WalkerPosition;
|
|
39
|
+
private _finalizeAdvance;
|
|
40
|
+
/**
|
|
41
|
+
* Seek directly to a geometry index (integer or fractional).
|
|
42
|
+
* Recalculates bearing from the new position.
|
|
43
|
+
*/
|
|
44
|
+
seek(index: number): WalkerPosition;
|
|
45
|
+
/**
|
|
46
|
+
* Get an interpolated coordinate between geometry[index] and geometry[index+1].
|
|
47
|
+
* @param index - integer index
|
|
48
|
+
* @param fraction - 0..1 interpolation factor
|
|
49
|
+
*/
|
|
50
|
+
interpolate(index: number, fraction: number): Coordinate;
|
|
51
|
+
/**
|
|
52
|
+
* Compute the forward-looking bearing at a given integer index.
|
|
53
|
+
* At the last point, returns the bearing of the final segment.
|
|
54
|
+
*/
|
|
55
|
+
bearingAt(index: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Total haversine distance from the route start to the given index.
|
|
58
|
+
* Uses the pre-computed cumulative distances (O(1)).
|
|
59
|
+
*/
|
|
60
|
+
distanceAtIndex(index: number): number;
|
|
61
|
+
/**
|
|
62
|
+
* Alias for distanceAtIndex.
|
|
63
|
+
*/
|
|
64
|
+
distanceToIndex(index: number): number;
|
|
65
|
+
/**
|
|
66
|
+
* Convert a distance in metres back to a fractional index.
|
|
67
|
+
* Uses binary search for O(log N) performance.
|
|
68
|
+
*/
|
|
69
|
+
private _distanceToIndex;
|
|
70
|
+
get totalDistanceM(): number;
|
|
71
|
+
get totalPoints(): number;
|
|
72
|
+
get isComplete(): boolean;
|
|
73
|
+
get currentIndex(): number;
|
|
74
|
+
private _currentPosition;
|
|
75
|
+
/** Interpolate cumulative distance for fractional index */
|
|
76
|
+
private _interpolatedDistance;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=RouteWalker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteWalker.d.ts","sourceRoot":"","sources":["../../../../src/core/RouteWalker.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,EAAE,UAAU,CAAC;IAClB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAW;IAChD,OAAO,CAAC,WAAW,CAAU;gBAEjB,QAAQ,EAAE,UAAU,EAAE,EAAE,UAAU,SAAI;IAuBlD;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAMtC;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAejD,OAAO,CAAC,gBAAgB;IAUxB;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAOnC;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU;IAQxD;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAOhC;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAKtC;;OAEG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAItC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgCxB,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAID,OAAO,CAAC,gBAAgB;IAexB,2DAA2D;IAC3D,OAAO,CAAC,qBAAqB;CAQ9B"}
|