@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
package/LICENSE
ADDED
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
import { SimulatorEngine, NoiseEngine } from '@ingissa/navcore-simulator'
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
**Peer dependencies:** `@ingissa/navcore-core` (already in monorepo). React Native hooks additionally require `react` and `react-native`,
|
|
39
|
+
**Peer dependencies:** `@ingissa/navcore-core` (already in monorepo). React Native hooks additionally require `react` and `react-native`, which are optional peer deps (tree-shaken in Node.js CI).
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -255,17 +255,11 @@ const report = fleet.report();
|
|
|
255
255
|
|
|
256
256
|
### useSimulator
|
|
257
257
|
|
|
258
|
-
Drop-in replacement for `useGpsSimulator`:
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
ScenarioInjector,
|
|
264
|
-
useSimulator,
|
|
265
|
-
} from '@ingissa/navcore-simulator/react-native';
|
|
266
|
-
|
|
267
|
-
// BEFORE
|
|
268
|
-
const simGps = useGpsSimulator(route, isNavigating, speedMultiplier);
|
|
258
|
+
Drop-in replacement for `useGpsSimulator`:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// BEFORE
|
|
262
|
+
const simGps = useGpsSimulator(route, isNavigating, speedMultiplier);
|
|
269
263
|
|
|
270
264
|
// AFTER
|
|
271
265
|
const { position, controls, session, isRecording } = useSimulator({
|
|
@@ -295,12 +289,10 @@ const { position, controls, session, isRecording } = useSimulator({
|
|
|
295
289
|
|
|
296
290
|
### SimulatorControls
|
|
297
291
|
|
|
298
|
-
Self-contained control panel. Drop it into any screen:
|
|
299
|
-
|
|
300
|
-
```tsx
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<SimulatorControls
|
|
292
|
+
Self-contained control panel. Drop it into any screen:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<SimulatorControls
|
|
304
296
|
simulator={controls}
|
|
305
297
|
session={session}
|
|
306
298
|
style="minimal" // 'minimal' | 'full' | 'floating'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavCore — Main Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Fuses all extracted modules into a single engine class.
|
|
5
|
+
* Framework-agnostic; license verification does not require platform crypto APIs.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const engine = new NavCore({ licenseKey: '...' });
|
|
9
|
+
* engine.setRoute(routeCoords, instructions);
|
|
10
|
+
* engine.update(gpsUpdate);
|
|
11
|
+
* const state = engine.getState();
|
|
12
|
+
*/
|
|
13
|
+
import type { Coordinate } from './geo/haversine';
|
|
14
|
+
import { type NavInstruction } from './progress/InstructionResolver';
|
|
15
|
+
import { NavigationFSM } from './off-route/NavigationFSM';
|
|
16
|
+
import { OffRouteFSM } from './off-route/OffRouteFSM';
|
|
17
|
+
import { LicenseManager } from './license/LicenseManager';
|
|
18
|
+
import type { RoutePlan } from './route-plan/RoutePlan';
|
|
19
|
+
export interface NavGpsUpdate {
|
|
20
|
+
coord: Coordinate;
|
|
21
|
+
accuracy: number | null;
|
|
22
|
+
bearing: number | null;
|
|
23
|
+
speed: number;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
export interface NavCoreOptions {
|
|
27
|
+
licenseKey?: string;
|
|
28
|
+
isDev?: boolean;
|
|
29
|
+
isCircuit?: boolean;
|
|
30
|
+
kalmanQ?: number;
|
|
31
|
+
kalmanRBase?: number;
|
|
32
|
+
baseCorridorMeters?: number;
|
|
33
|
+
maxCorridorMeters?: number;
|
|
34
|
+
arrivalThresholdMeters?: number;
|
|
35
|
+
bearingSmoothingAlpha?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface NavCoreState {
|
|
38
|
+
snappedCoord: Coordinate | null;
|
|
39
|
+
ghostCoord: Coordinate | null;
|
|
40
|
+
rawGpsCoord: Coordinate | null;
|
|
41
|
+
bearing: number;
|
|
42
|
+
rawGpsBearing: number | null;
|
|
43
|
+
routeIndex: number;
|
|
44
|
+
anchorRouteIndex: number | null;
|
|
45
|
+
distanceToRoute: number;
|
|
46
|
+
corridor: number;
|
|
47
|
+
confidence: number;
|
|
48
|
+
isOffRoute: boolean;
|
|
49
|
+
navFsmState: string;
|
|
50
|
+
progressIndex: number;
|
|
51
|
+
hasArrived: boolean;
|
|
52
|
+
rejoinCandidate: {
|
|
53
|
+
coord: Coordinate;
|
|
54
|
+
index: number;
|
|
55
|
+
} | null;
|
|
56
|
+
confidenceCone: {
|
|
57
|
+
center: Coordinate;
|
|
58
|
+
bearing: number;
|
|
59
|
+
angle: number;
|
|
60
|
+
length: number;
|
|
61
|
+
} | null;
|
|
62
|
+
alertState: 'NONE' | 'DEVIATING' | 'RECALCULATING' | 'REJOINING' | 'REJOINED';
|
|
63
|
+
detourMode: 'NONE' | 'FREE' | 'COMMITTED';
|
|
64
|
+
detourCommitProgress: number;
|
|
65
|
+
distanceToDestination: number | null;
|
|
66
|
+
distanceToNextInstruction: number | null;
|
|
67
|
+
nextInstrIdx: number | null;
|
|
68
|
+
nextInstructionIndex: number | null;
|
|
69
|
+
currentSpeed: number;
|
|
70
|
+
isGpsStale: boolean;
|
|
71
|
+
gpsAgeMs: number;
|
|
72
|
+
licenseStatus: string;
|
|
73
|
+
offRouteState: string;
|
|
74
|
+
lifecycleState: string;
|
|
75
|
+
stability?: {
|
|
76
|
+
oscillation: number;
|
|
77
|
+
drift: number;
|
|
78
|
+
jump: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
type EventHandler = (...args: unknown[]) => void;
|
|
82
|
+
export declare class NavCore {
|
|
83
|
+
private kalman;
|
|
84
|
+
private snapper;
|
|
85
|
+
private deadReckoning;
|
|
86
|
+
private deviationDetector;
|
|
87
|
+
private lifecycleFSM;
|
|
88
|
+
private offRouteFSM;
|
|
89
|
+
private license;
|
|
90
|
+
private route;
|
|
91
|
+
private instructions;
|
|
92
|
+
private forwardCum;
|
|
93
|
+
private reverseCum;
|
|
94
|
+
private snappedCoord;
|
|
95
|
+
private ghostCoord;
|
|
96
|
+
private rawGpsCoord;
|
|
97
|
+
private bearing;
|
|
98
|
+
private rawGpsBearing;
|
|
99
|
+
private routeIndex;
|
|
100
|
+
private maxProgressIdx;
|
|
101
|
+
private prevSegment;
|
|
102
|
+
private distanceToRoute;
|
|
103
|
+
private corridor;
|
|
104
|
+
private prevDistToNext;
|
|
105
|
+
private currentSpeed;
|
|
106
|
+
private lastGpsTimestamp;
|
|
107
|
+
private instrIdx;
|
|
108
|
+
private linearOffset;
|
|
109
|
+
private hasArrived;
|
|
110
|
+
private readonly options;
|
|
111
|
+
private listeners;
|
|
112
|
+
constructor(options?: NavCoreOptions);
|
|
113
|
+
/**
|
|
114
|
+
* Returns true if the feature is unlocked (dev mode OR valid license).
|
|
115
|
+
* Emits a one-time console warning when a Pro feature is blocked.
|
|
116
|
+
*/
|
|
117
|
+
private has;
|
|
118
|
+
/** Load route geometry and instructions */
|
|
119
|
+
setRoute(route: ReadonlyArray<Coordinate>, instructions?: ReadonlyArray<NavInstruction>): void;
|
|
120
|
+
/** Load a provider-normalized route plan without exposing mutable route state. */
|
|
121
|
+
loadRoute(plan: RoutePlan): void;
|
|
122
|
+
/** Start navigation */
|
|
123
|
+
startNavigation(): void;
|
|
124
|
+
/** Process a GPS update — the main engine tick */
|
|
125
|
+
update(gps: NavGpsUpdate): NavCoreState;
|
|
126
|
+
/** Time-advance tick for dead reckoning (call at RAF rate) */
|
|
127
|
+
tick(nowMs: number): NavCoreState;
|
|
128
|
+
/** Get the current engine state snapshot */
|
|
129
|
+
getState(): NavCoreState;
|
|
130
|
+
/** Destroy the engine */
|
|
131
|
+
destroy(): void;
|
|
132
|
+
on(event: string, handler: EventHandler): void;
|
|
133
|
+
off(event: string, handler: EventHandler): void;
|
|
134
|
+
private emit;
|
|
135
|
+
getLicenseManager(): LicenseManager;
|
|
136
|
+
getRoute(): ReadonlyArray<Coordinate>;
|
|
137
|
+
getInstructions(): ReadonlyArray<NavInstruction>;
|
|
138
|
+
getOffRouteFSM(): OffRouteFSM;
|
|
139
|
+
getLifecycleFSM(): NavigationFSM;
|
|
140
|
+
}
|
|
141
|
+
export {};
|
|
142
|
+
//# sourceMappingURL=NavCore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NavCore.d.ts","sourceRoot":"","sources":["../../../../core/src/NavCore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavCore — Main Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Fuses all extracted modules into a single engine class.
|
|
5
|
+
* Framework-agnostic; license verification does not require platform crypto APIs.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const engine = new NavCore({ licenseKey: '...' });
|
|
9
|
+
* engine.setRoute(routeCoords, instructions);
|
|
10
|
+
* engine.update(gpsUpdate);
|
|
11
|
+
* const state = engine.getState();
|
|
12
|
+
*/
|
|
13
|
+
import { haversineDistance } from './geo/haversine';
|
|
14
|
+
import { buildForwardCumulativeDistances, buildReverseCumulativeDistances } from './geo/cumulative';
|
|
15
|
+
import { KalmanFilter2D } from './kalman/KalmanFilter2D';
|
|
16
|
+
import { RouteSnapper } from './snapper/RouteSnapper';
|
|
17
|
+
import { DeadReckoningEngine } from './dead-reckoning/DeadReckoning';
|
|
18
|
+
import { calculateDynamicCorridor } from './corridor/DynamicCorridor';
|
|
19
|
+
import { DeviationDetector, findClosestSegment } from './corridor/DeviationDetector';
|
|
20
|
+
import { applyBearingSmoothing } from './bearing/BearingEngine';
|
|
21
|
+
import { validateProgressUpdate } from './progress/ProgressTracker';
|
|
22
|
+
import { resolveNextInstruction } from './progress/InstructionResolver';
|
|
23
|
+
import { advancePredictor, applyGpsCorrection } from './predictor/RouteOffsetPredictor';
|
|
24
|
+
import { NavigationFSM } from './off-route/NavigationFSM';
|
|
25
|
+
import { OffRouteFSM } from './off-route/OffRouteFSM';
|
|
26
|
+
import { LicenseManager } from './license/LicenseManager';
|
|
27
|
+
import * as C from './constants';
|
|
28
|
+
import { FEATURES } from './features';
|
|
29
|
+
// ── NavCore class ─────────────────────────────────────────────
|
|
30
|
+
export class NavCore {
|
|
31
|
+
constructor(options) {
|
|
32
|
+
// ── Route data ───────────────────────────────────────────────
|
|
33
|
+
this.route = [];
|
|
34
|
+
this.instructions = [];
|
|
35
|
+
this.forwardCum = new Float64Array(0);
|
|
36
|
+
this.reverseCum = new Float64Array(0);
|
|
37
|
+
// ── State ────────────────────────────────────────────────────
|
|
38
|
+
this.snappedCoord = null;
|
|
39
|
+
this.ghostCoord = null;
|
|
40
|
+
this.rawGpsCoord = null;
|
|
41
|
+
this.bearing = 0;
|
|
42
|
+
this.rawGpsBearing = null;
|
|
43
|
+
this.routeIndex = 0;
|
|
44
|
+
this.maxProgressIdx = 0;
|
|
45
|
+
this.prevSegment = null;
|
|
46
|
+
this.distanceToRoute = 0;
|
|
47
|
+
this.corridor = C.BASE_CORRIDOR_METERS;
|
|
48
|
+
this.prevDistToNext = null;
|
|
49
|
+
this.currentSpeed = 0;
|
|
50
|
+
this.lastGpsTimestamp = 0;
|
|
51
|
+
this.instrIdx = 0;
|
|
52
|
+
this.linearOffset = 0;
|
|
53
|
+
this.hasArrived = false;
|
|
54
|
+
// ── Events ───────────────────────────────────────────────────
|
|
55
|
+
this.listeners = new Map();
|
|
56
|
+
this.options = {
|
|
57
|
+
licenseKey: options?.licenseKey ?? '',
|
|
58
|
+
isDev: options?.isDev ?? false,
|
|
59
|
+
isCircuit: options?.isCircuit ?? false,
|
|
60
|
+
kalmanQ: options?.kalmanQ ?? 0.00001,
|
|
61
|
+
kalmanRBase: options?.kalmanRBase ?? 0.00005,
|
|
62
|
+
baseCorridorMeters: options?.baseCorridorMeters ?? C.BASE_CORRIDOR_METERS,
|
|
63
|
+
maxCorridorMeters: options?.maxCorridorMeters ?? C.MAX_CORRIDOR_METERS,
|
|
64
|
+
arrivalThresholdMeters: options?.arrivalThresholdMeters ?? C.ARRIVAL_THRESHOLD_METERS,
|
|
65
|
+
bearingSmoothingAlpha: options?.bearingSmoothingAlpha ?? C.BEARING_SMOOTHING_ALPHA,
|
|
66
|
+
};
|
|
67
|
+
this.kalman = new KalmanFilter2D(this.options.kalmanQ, this.options.kalmanRBase);
|
|
68
|
+
this.snapper = new RouteSnapper({ useBearingPenalty: true, useInertiaPenalty: true });
|
|
69
|
+
this.deadReckoning = new DeadReckoningEngine();
|
|
70
|
+
this.deviationDetector = new DeviationDetector();
|
|
71
|
+
this.lifecycleFSM = new NavigationFSM();
|
|
72
|
+
this.offRouteFSM = new OffRouteFSM();
|
|
73
|
+
this.license = new LicenseManager(this.options.licenseKey, this.options.isDev);
|
|
74
|
+
}
|
|
75
|
+
// ── License gate helper ──────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Returns true if the feature is unlocked (dev mode OR valid license).
|
|
78
|
+
* Emits a one-time console warning when a Pro feature is blocked.
|
|
79
|
+
*/
|
|
80
|
+
has(feature) {
|
|
81
|
+
const ok = this.license.hasFeature(feature);
|
|
82
|
+
if (!ok)
|
|
83
|
+
this.license.warnOnce(feature);
|
|
84
|
+
return ok;
|
|
85
|
+
}
|
|
86
|
+
// ── Public API ───────────────────────────────────────────────
|
|
87
|
+
/** Load route geometry and instructions */
|
|
88
|
+
setRoute(route, instructions) {
|
|
89
|
+
// Keep engine state isolated from consumer-owned provider responses.
|
|
90
|
+
this.route = route.map((coord) => [coord[0], coord[1]]);
|
|
91
|
+
this.instructions = (instructions ?? []).map((instruction) => ({
|
|
92
|
+
...instruction,
|
|
93
|
+
location: [instruction.location[0], instruction.location[1]],
|
|
94
|
+
}));
|
|
95
|
+
this.forwardCum = buildForwardCumulativeDistances(this.route);
|
|
96
|
+
this.reverseCum = buildReverseCumulativeDistances(this.route);
|
|
97
|
+
this.routeIndex = 0;
|
|
98
|
+
this.maxProgressIdx = 0;
|
|
99
|
+
this.prevSegment = null;
|
|
100
|
+
this.instrIdx = 0;
|
|
101
|
+
this.linearOffset = 0;
|
|
102
|
+
this.hasArrived = false;
|
|
103
|
+
this.snappedCoord = null;
|
|
104
|
+
this.prevDistToNext = null;
|
|
105
|
+
this.lifecycleFSM.dispatch({ type: 'ROUTE_LOADED' });
|
|
106
|
+
// Auto-fill missing cumulative offsets for instructions
|
|
107
|
+
for (const instr of this.instructions) {
|
|
108
|
+
if (instr.cumulativeOffset === undefined && instr.geometryIndex < this.forwardCum.length) {
|
|
109
|
+
instr.cumulativeOffset = this.forwardCum[instr.geometryIndex];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.emit('routeLoaded', { routeLength: this.route.length });
|
|
113
|
+
}
|
|
114
|
+
/** Load a provider-normalized route plan without exposing mutable route state. */
|
|
115
|
+
loadRoute(plan) {
|
|
116
|
+
this.setRoute(plan.geometry, plan.instructions);
|
|
117
|
+
}
|
|
118
|
+
/** Start navigation */
|
|
119
|
+
startNavigation() {
|
|
120
|
+
this.lifecycleFSM.dispatch({ type: 'USER_START_NAVIGATION' });
|
|
121
|
+
this.emit('navigationStarted');
|
|
122
|
+
}
|
|
123
|
+
/** Process a GPS update — the main engine tick */
|
|
124
|
+
update(gps) {
|
|
125
|
+
this.rawGpsCoord = gps.coord;
|
|
126
|
+
this.rawGpsBearing = gps.bearing;
|
|
127
|
+
this.currentSpeed = gps.speed;
|
|
128
|
+
this.lastGpsTimestamp = gps.timestamp;
|
|
129
|
+
if (this.route.length < 2)
|
|
130
|
+
return this.getState();
|
|
131
|
+
// 1. Kalman filter
|
|
132
|
+
const [filteredLng, filteredLat] = this.kalman.update(gps.coord[0], gps.coord[1], gps.accuracy ?? 10, gps.timestamp);
|
|
133
|
+
const filteredCoord = [filteredLng, filteredLat];
|
|
134
|
+
// 2. Snap to route
|
|
135
|
+
const startIdx = Math.max(0, this.routeIndex - 2);
|
|
136
|
+
const snap = this.snapper.snap(filteredCoord, this.route, startIdx, C.SNAP_WINDOW_SIZE, this.options.isCircuit, this.maxProgressIdx, this.routeIndex, gps.bearing, this.prevSegment);
|
|
137
|
+
this.ghostCoord = snap.proj;
|
|
138
|
+
// Look ahead to upcoming maneuver for dynamic corridor widening
|
|
139
|
+
let distToUpcomingManeuver = null;
|
|
140
|
+
if (this.instructions.length > 0 && this.instrIdx < this.instructions.length) {
|
|
141
|
+
const upcoming = this.instructions[this.instrIdx];
|
|
142
|
+
distToUpcomingManeuver = upcoming.cumulativeOffset !== undefined
|
|
143
|
+
? Math.max(0, upcoming.cumulativeOffset - this.linearOffset)
|
|
144
|
+
: haversineDistance(filteredCoord[1], filteredCoord[0], upcoming.location[1], upcoming.location[0]);
|
|
145
|
+
}
|
|
146
|
+
// 3. Dynamic corridor
|
|
147
|
+
this.corridor = calculateDynamicCorridor(gps.speed, gps.accuracy, this.options.baseCorridorMeters, this.options.maxCorridorMeters, C.SPEED_CORRIDOR_FACTOR, C.GPS_ACCURACY_FACTOR, distToUpcomingManeuver);
|
|
148
|
+
// 4. Progress validation
|
|
149
|
+
const validatedIdx = validateProgressUpdate(this.routeIndex, snap.idx, this.route, snap.dist, C.MAX_SNAP_DISTANCE);
|
|
150
|
+
this.prevSegment = this.routeIndex;
|
|
151
|
+
this.routeIndex = validatedIdx;
|
|
152
|
+
this.maxProgressIdx = Math.max(this.maxProgressIdx, validatedIdx);
|
|
153
|
+
this.snappedCoord = snap.proj;
|
|
154
|
+
this.distanceToRoute = snap.dist;
|
|
155
|
+
// 5. Linear offset update
|
|
156
|
+
if (this.forwardCum.length > validatedIdx) {
|
|
157
|
+
const measuredOffset = this.forwardCum[validatedIdx] ?? 0;
|
|
158
|
+
this.linearOffset = applyGpsCorrection(this.linearOffset, measuredOffset, C.BETA_CORRECTION);
|
|
159
|
+
}
|
|
160
|
+
// 6. Bearing smoothing (PRO: ADVANCED_BEARING)
|
|
161
|
+
if (this.has(FEATURES.ADVANCED_BEARING)) {
|
|
162
|
+
this.bearing = applyBearingSmoothing(this.bearing, this.route, this.routeIndex, gps.speed, gps.bearing, this.options.bearingSmoothingAlpha);
|
|
163
|
+
}
|
|
164
|
+
else if (gps.bearing !== null) {
|
|
165
|
+
this.bearing = gps.bearing;
|
|
166
|
+
}
|
|
167
|
+
// 7. Deviation detection & Route Rejoin Handling
|
|
168
|
+
const isProDeviation = this.has(FEATURES.COMPOUND_DEVIATION);
|
|
169
|
+
if (this.offRouteFSM.state === 'ON_ROUTE' || this.offRouteFSM.state === 'DEVIATING') {
|
|
170
|
+
if (isProDeviation) {
|
|
171
|
+
const closest = findClosestSegment(filteredCoord, this.route, startIdx, C.SNAP_WINDOW_SIZE);
|
|
172
|
+
const signals = this.deviationDetector.evaluate(filteredCoord, gps.bearing, gps.speed, closest, this.route, this.corridor, this.prevDistToNext);
|
|
173
|
+
// Update moving-away tracker
|
|
174
|
+
if (closest.segmentIndex < this.route.length - 1) {
|
|
175
|
+
const nextEnd = this.route[Math.min(closest.segmentIndex + 2, this.route.length - 1)];
|
|
176
|
+
this.prevDistToNext = haversineDistance(filteredCoord[1], filteredCoord[0], nextEnd[1], nextEnd[0]);
|
|
177
|
+
}
|
|
178
|
+
if (signals.confirmed) {
|
|
179
|
+
if (this.offRouteFSM.state === 'ON_ROUTE') {
|
|
180
|
+
this.offRouteFSM.dispatch({
|
|
181
|
+
type: 'DEVIATION_SUSPECTED',
|
|
182
|
+
deviationSegmentIdx: closest.segmentIndex
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (this.offRouteFSM.isDeviationConfirmed()) {
|
|
186
|
+
this.offRouteFSM.dispatch({
|
|
187
|
+
type: 'DEVIATION_CONFIRMED',
|
|
188
|
+
deviationSegmentIdx: closest.segmentIndex
|
|
189
|
+
});
|
|
190
|
+
this.emit('deviation', {
|
|
191
|
+
anchorIndex: closest.segmentIndex,
|
|
192
|
+
ghostCoord: snap.proj,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (this.offRouteFSM.state === 'DEVIATING') {
|
|
197
|
+
this.offRouteFSM.dispatch({ type: 'RESUME' });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Core fallback: simple distance-based off-route check
|
|
202
|
+
if (snap.dist > this.corridor && this.offRouteFSM.state === 'ON_ROUTE') {
|
|
203
|
+
this.offRouteFSM.dispatch({ type: 'DEVIATION_SUSPECTED', deviationSegmentIdx: snap.idx });
|
|
204
|
+
this.emit('deviation', { anchorIndex: snap.idx, ghostCoord: snap.proj });
|
|
205
|
+
}
|
|
206
|
+
else if (snap.dist <= this.corridor && this.offRouteFSM.state === 'DEVIATING') {
|
|
207
|
+
this.offRouteFSM.dispatch({ type: 'RESUME' });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Vehicle is in OFF_ROUTE, REJOINING, or REJOINED state.
|
|
213
|
+
// If the vehicle re-enters the valid corridor, resume ON_ROUTE and emit 'rejoined'.
|
|
214
|
+
if (snap.dist <= this.corridor) {
|
|
215
|
+
const wasOffRoute = this.offRouteFSM.state === 'OFF_ROUTE' ||
|
|
216
|
+
this.offRouteFSM.state === 'REJOINING' ||
|
|
217
|
+
this.offRouteFSM.state === 'REJOINED';
|
|
218
|
+
this.offRouteFSM.dispatch({ type: 'RESUME' });
|
|
219
|
+
if (wasOffRoute) {
|
|
220
|
+
this.emit('rejoined', { resumeIndex: snap.idx });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 8. Lifecycle join detection
|
|
225
|
+
if (this.lifecycleFSM.state === 'APPROACHING_ROUTE' && snap.dist < this.corridor) {
|
|
226
|
+
this.lifecycleFSM.dispatch({ type: 'ROUTE_JOINED', data: { index: snap.idx } });
|
|
227
|
+
}
|
|
228
|
+
// 9. Instruction resolution
|
|
229
|
+
if (this.instructions.length > 0) {
|
|
230
|
+
const instrResult = resolveNextInstruction(this.routeIndex, this.instructions, this.instrIdx, this.route, filteredCoord, gps.bearing, this.linearOffset, gps.speed, this.options.isCircuit);
|
|
231
|
+
if (instrResult.nextIdx !== this.instrIdx) {
|
|
232
|
+
this.instrIdx = instrResult.nextIdx;
|
|
233
|
+
}
|
|
234
|
+
if (instrResult.wasTriggered && instrResult.instruction) {
|
|
235
|
+
this.emit('instruction', instrResult.instruction);
|
|
236
|
+
this.instrIdx = instrResult.nextIdx + 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// 10. Arrival detection
|
|
240
|
+
if (!this.hasArrived && this.route.length > 0) {
|
|
241
|
+
const dest = this.route[this.route.length - 1];
|
|
242
|
+
const distToDest = haversineDistance(filteredCoord[1], filteredCoord[0], dest[1], dest[0]);
|
|
243
|
+
// Prevent immediate arrival on circuits by ensuring we've traversed at least 80% of the waypoints
|
|
244
|
+
const hasTraveledFarEnough = this.options.isCircuit ?
|
|
245
|
+
(this.routeIndex >= this.route.length * 0.8) : true;
|
|
246
|
+
if (distToDest < this.options.arrivalThresholdMeters && hasTraveledFarEnough) {
|
|
247
|
+
this.hasArrived = true;
|
|
248
|
+
this.lifecycleFSM.dispatch({ type: 'NAVIGATION_FINISHED' });
|
|
249
|
+
this.emit('arrival');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// 11. Dead reckoning anchor (PRO: DEAD_RECKONING)
|
|
253
|
+
if (this.has(FEATURES.DEAD_RECKONING)) {
|
|
254
|
+
this.deadReckoning.setAnchor(snap.proj, snap.idx, gps.timestamp);
|
|
255
|
+
}
|
|
256
|
+
const state = this.getState();
|
|
257
|
+
this.emit('update', state);
|
|
258
|
+
return state;
|
|
259
|
+
}
|
|
260
|
+
/** Time-advance tick for dead reckoning (call at RAF rate) */
|
|
261
|
+
tick(nowMs) {
|
|
262
|
+
if (this.has(FEATURES.DEAD_RECKONING)) {
|
|
263
|
+
const dr = this.deadReckoning.predict(this.currentSpeed, this.bearing, nowMs);
|
|
264
|
+
if (dr.isActive) {
|
|
265
|
+
this.snappedCoord = dr.predictedCoord;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Advance linear offset predictor
|
|
269
|
+
if (this.lastGpsTimestamp > 0) {
|
|
270
|
+
const dt = (nowMs - this.lastGpsTimestamp) / 1000;
|
|
271
|
+
if (dt > 0 && dt < C.GPS_AGE_CAP_SECONDS) {
|
|
272
|
+
this.linearOffset = advancePredictor(this.linearOffset, this.currentSpeed, dt);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const state = this.getState();
|
|
276
|
+
this.emit('update', state);
|
|
277
|
+
return state;
|
|
278
|
+
}
|
|
279
|
+
/** Get the current engine state snapshot */
|
|
280
|
+
getState() {
|
|
281
|
+
const distToDest = this.route.length > 0 && this.reverseCum.length > this.routeIndex
|
|
282
|
+
? (this.reverseCum[this.routeIndex] ?? null)
|
|
283
|
+
: null;
|
|
284
|
+
const distToNextInstr = this.instrIdx < this.instructions.length
|
|
285
|
+
? (() => {
|
|
286
|
+
const instr = this.instructions[this.instrIdx];
|
|
287
|
+
return instr.cumulativeOffset !== undefined
|
|
288
|
+
? Math.max(0, instr.cumulativeOffset - this.linearOffset)
|
|
289
|
+
: null;
|
|
290
|
+
})()
|
|
291
|
+
: null;
|
|
292
|
+
const gpsAgeMs = this.lastGpsTimestamp > 0 ? Date.now() - this.lastGpsTimestamp : 0;
|
|
293
|
+
const confidence = Math.max(0, Math.min(1, 1 - (this.distanceToRoute / Math.max(1, this.corridor))));
|
|
294
|
+
const alertState = this.offRouteFSM.state === 'DEVIATING' ? 'DEVIATING' :
|
|
295
|
+
this.offRouteFSM.state === 'OFF_ROUTE' || this.offRouteFSM.state === 'REROUTING' ? 'RECALCULATING' :
|
|
296
|
+
this.offRouteFSM.state === 'REJOINING' ? 'REJOINING' :
|
|
297
|
+
this.offRouteFSM.state === 'REJOINED' ? 'REJOINED' : 'NONE';
|
|
298
|
+
const isOff = this.offRouteFSM.state !== 'ON_ROUTE' && this.offRouteFSM.state !== 'DEVIATING';
|
|
299
|
+
const detourMode = this.offRouteFSM.state === 'OFF_ROUTE' || this.offRouteFSM.state === 'REROUTING' ? 'FREE' : 'NONE';
|
|
300
|
+
const detourCommitProgress = this.offRouteFSM.state === 'OFF_ROUTE' ? 1.0 : 0.0;
|
|
301
|
+
const rejoinCandidate = this.offRouteFSM.context.rejoinTarget
|
|
302
|
+
? {
|
|
303
|
+
coord: this.offRouteFSM.context.rejoinTarget.coordinates,
|
|
304
|
+
index: this.offRouteFSM.context.rejoinTarget.index,
|
|
305
|
+
}
|
|
306
|
+
: null;
|
|
307
|
+
const confidenceCone = this.snappedCoord
|
|
308
|
+
? {
|
|
309
|
+
center: this.snappedCoord,
|
|
310
|
+
bearing: this.bearing,
|
|
311
|
+
angle: 30,
|
|
312
|
+
length: Math.max(10, Math.min(50, this.currentSpeed * 3)),
|
|
313
|
+
}
|
|
314
|
+
: null;
|
|
315
|
+
const nextIdx = this.instrIdx < this.instructions.length ? this.instrIdx : null;
|
|
316
|
+
return {
|
|
317
|
+
snappedCoord: this.snappedCoord,
|
|
318
|
+
ghostCoord: this.ghostCoord ?? this.snappedCoord,
|
|
319
|
+
rawGpsCoord: this.rawGpsCoord,
|
|
320
|
+
bearing: this.bearing,
|
|
321
|
+
rawGpsBearing: this.rawGpsBearing,
|
|
322
|
+
routeIndex: this.routeIndex,
|
|
323
|
+
anchorRouteIndex: this.prevSegment ?? this.routeIndex,
|
|
324
|
+
distanceToRoute: this.distanceToRoute,
|
|
325
|
+
corridor: this.corridor,
|
|
326
|
+
confidence,
|
|
327
|
+
isOffRoute: isOff,
|
|
328
|
+
navFsmState: this.lifecycleFSM.state,
|
|
329
|
+
progressIndex: this.routeIndex,
|
|
330
|
+
hasArrived: this.hasArrived,
|
|
331
|
+
rejoinCandidate,
|
|
332
|
+
confidenceCone,
|
|
333
|
+
alertState,
|
|
334
|
+
detourMode,
|
|
335
|
+
detourCommitProgress,
|
|
336
|
+
distanceToDestination: distToDest,
|
|
337
|
+
distanceToNextInstruction: distToNextInstr,
|
|
338
|
+
nextInstrIdx: nextIdx,
|
|
339
|
+
nextInstructionIndex: nextIdx,
|
|
340
|
+
currentSpeed: this.currentSpeed,
|
|
341
|
+
isGpsStale: gpsAgeMs > C.GPS_STALE_MS,
|
|
342
|
+
gpsAgeMs,
|
|
343
|
+
licenseStatus: this.license.getStatus(),
|
|
344
|
+
offRouteState: this.offRouteFSM.state,
|
|
345
|
+
lifecycleState: this.lifecycleFSM.state,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/** Destroy the engine */
|
|
349
|
+
destroy() {
|
|
350
|
+
this.listeners.clear();
|
|
351
|
+
this.route = [];
|
|
352
|
+
this.instructions = [];
|
|
353
|
+
this.snappedCoord = null;
|
|
354
|
+
this.ghostCoord = null;
|
|
355
|
+
this.rawGpsCoord = null;
|
|
356
|
+
this.kalman.reset();
|
|
357
|
+
this.deadReckoning.clearAnchor();
|
|
358
|
+
this.lifecycleFSM.reset();
|
|
359
|
+
this.offRouteFSM.reset();
|
|
360
|
+
}
|
|
361
|
+
// ── Event system ─────────────────────────────────────────────
|
|
362
|
+
on(event, handler) {
|
|
363
|
+
if (!this.listeners.has(event)) {
|
|
364
|
+
this.listeners.set(event, []);
|
|
365
|
+
}
|
|
366
|
+
this.listeners.get(event).push(handler);
|
|
367
|
+
}
|
|
368
|
+
off(event, handler) {
|
|
369
|
+
const handlers = this.listeners.get(event);
|
|
370
|
+
if (handlers) {
|
|
371
|
+
const idx = handlers.indexOf(handler);
|
|
372
|
+
if (idx >= 0)
|
|
373
|
+
handlers.splice(idx, 1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
emit(event, ...args) {
|
|
377
|
+
const handlers = this.listeners.get(event);
|
|
378
|
+
if (handlers) {
|
|
379
|
+
for (const handler of handlers) {
|
|
380
|
+
handler(...args);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ── Getters ──────────────────────────────────────────────────
|
|
385
|
+
getLicenseManager() { return this.license; }
|
|
386
|
+
getRoute() { return this.route; }
|
|
387
|
+
getInstructions() { return this.instructions; }
|
|
388
|
+
getOffRouteFSM() { return this.offRouteFSM; }
|
|
389
|
+
getLifecycleFSM() { return this.lifecycleFSM; }
|
|
390
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module 5: BearingEngine
|
|
3
|
+
* Speed-adaptive IIR bearing smoothing.
|
|
4
|
+
*/
|
|
5
|
+
import { type Coordinate } from '../geo/haversine';
|
|
6
|
+
/**
|
|
7
|
+
* Calculate speed-adaptive smoothing factor.
|
|
8
|
+
* Higher speed → less smoothing (alpha closer to 1).
|
|
9
|
+
* Lower speed → more smoothing (alpha closer to base value).
|
|
10
|
+
*/
|
|
11
|
+
export declare function getSpeedAdaptiveAlpha(speedMps: number, baseAlpha?: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Compute bearing from the route geometry at the current segment.
|
|
14
|
+
*/
|
|
15
|
+
export declare function calculateRobustBearing(route: Coordinate[], currentIdx: number, lookAhead?: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Blend two bearings using the shortest angular path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function blendBearing(current: number, target: number, alpha: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Full bearing smoothing pipeline: route bearing → IIR blend → output.
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyBearingSmoothing(currentBearing: number, route: Coordinate[], routeIdx: number, speedMps: number, gpsBearing: number | null, baseAlpha?: number): number;
|
|
24
|
+
//# sourceMappingURL=BearingEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BearingEngine.d.ts","sourceRoot":"","sources":["../../../../../core/src/bearing/BearingEngine.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOnD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAgC,GAC1C,MAAM,CAIR;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EAAE,EACnB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,MAAU,GACpB,MAAM,CAQR;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,GAAE,MAAgC,GAC1C,MAAM,CAWR"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module 5: BearingEngine
|
|
3
|
+
* Speed-adaptive IIR bearing smoothing.
|
|
4
|
+
*/
|
|
5
|
+
import { getBearingBetweenPoints, normalizeAngle } from '../geo/bearing';
|
|
6
|
+
import { BEARING_SMOOTHING_ALPHA, BEARING_MIN_SPEED_MPS, } from '../constants';
|
|
7
|
+
/**
|
|
8
|
+
* Calculate speed-adaptive smoothing factor.
|
|
9
|
+
* Higher speed → less smoothing (alpha closer to 1).
|
|
10
|
+
* Lower speed → more smoothing (alpha closer to base value).
|
|
11
|
+
*/
|
|
12
|
+
export function getSpeedAdaptiveAlpha(speedMps, baseAlpha = BEARING_SMOOTHING_ALPHA) {
|
|
13
|
+
if (speedMps < BEARING_MIN_SPEED_MPS)
|
|
14
|
+
return baseAlpha * 0.5;
|
|
15
|
+
if (speedMps > 20)
|
|
16
|
+
return Math.min(0.9, baseAlpha * 2);
|
|
17
|
+
return baseAlpha;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute bearing from the route geometry at the current segment.
|
|
21
|
+
*/
|
|
22
|
+
export function calculateRobustBearing(route, currentIdx, lookAhead = 3) {
|
|
23
|
+
const endIdx = Math.min(currentIdx + lookAhead, route.length - 1);
|
|
24
|
+
if (currentIdx >= route.length - 1 || currentIdx === endIdx) {
|
|
25
|
+
return currentIdx > 0
|
|
26
|
+
? getBearingBetweenPoints(route[currentIdx - 1], route[currentIdx])
|
|
27
|
+
: 0;
|
|
28
|
+
}
|
|
29
|
+
return getBearingBetweenPoints(route[currentIdx], route[endIdx]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Blend two bearings using the shortest angular path.
|
|
33
|
+
*/
|
|
34
|
+
export function blendBearing(current, target, alpha) {
|
|
35
|
+
const diff = normalizeAngle(target - current);
|
|
36
|
+
return (current + alpha * diff + 360) % 360;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Full bearing smoothing pipeline: route bearing → IIR blend → output.
|
|
40
|
+
*/
|
|
41
|
+
export function applyBearingSmoothing(currentBearing, route, routeIdx, speedMps, gpsBearing, baseAlpha = BEARING_SMOOTHING_ALPHA) {
|
|
42
|
+
const alpha = getSpeedAdaptiveAlpha(speedMps, baseAlpha);
|
|
43
|
+
const routeBearing = calculateRobustBearing(route, routeIdx);
|
|
44
|
+
// Use GPS bearing if available and speed is sufficient, blend with route bearing
|
|
45
|
+
if (gpsBearing !== null && speedMps > BEARING_MIN_SPEED_MPS) {
|
|
46
|
+
const gpsBlended = blendBearing(currentBearing, gpsBearing, alpha * 0.3);
|
|
47
|
+
return blendBearing(gpsBlended, routeBearing, alpha);
|
|
48
|
+
}
|
|
49
|
+
return blendBearing(currentBearing, routeBearing, alpha);
|
|
50
|
+
}
|