@ingissa/navcore-simulator 1.0.8 → 1.0.10

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.
Files changed (170) hide show
  1. package/LICENSE +3 -3
  2. package/README.md +161 -356
  3. package/dist/index.d.mts +87 -0
  4. package/dist/{simulator/src/react-native/useSimulator.d.ts → index.d.ts} +37 -9
  5. package/dist/index.js +1393 -0
  6. package/dist/index.mjs +1383 -0
  7. package/dist/node.d.mts +527 -0
  8. package/dist/node.d.ts +527 -0
  9. package/dist/node.js +957 -0
  10. package/dist/node.mjs +949 -0
  11. package/dist/react-native.d.mts +4 -0
  12. package/dist/react-native.d.ts +4 -0
  13. package/dist/react-native.js +1393 -0
  14. package/dist/react-native.mjs +1383 -0
  15. package/package.json +61 -58
  16. package/dist/core/src/NavCore.d.ts +0 -142
  17. package/dist/core/src/NavCore.d.ts.map +0 -1
  18. package/dist/core/src/NavCore.js +0 -390
  19. package/dist/core/src/bearing/BearingEngine.d.ts +0 -24
  20. package/dist/core/src/bearing/BearingEngine.d.ts.map +0 -1
  21. package/dist/core/src/bearing/BearingEngine.js +0 -50
  22. package/dist/core/src/constants.d.ts +0 -34
  23. package/dist/core/src/constants.d.ts.map +0 -1
  24. package/dist/core/src/constants.js +0 -41
  25. package/dist/core/src/corridor/DeviationDetector.d.ts +0 -33
  26. package/dist/core/src/corridor/DeviationDetector.d.ts.map +0 -1
  27. package/dist/core/src/corridor/DeviationDetector.js +0 -83
  28. package/dist/core/src/corridor/DynamicCorridor.d.ts +0 -13
  29. package/dist/core/src/corridor/DynamicCorridor.d.ts.map +0 -1
  30. package/dist/core/src/corridor/DynamicCorridor.js +0 -26
  31. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +0 -35
  32. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +0 -1
  33. package/dist/core/src/dead-reckoning/DeadReckoning.js +0 -49
  34. package/dist/core/src/directions/DirectionsProvider.d.ts +0 -70
  35. package/dist/core/src/directions/DirectionsProvider.d.ts.map +0 -1
  36. package/dist/core/src/directions/DirectionsProvider.js +0 -7
  37. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +0 -26
  38. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +0 -1
  39. package/dist/core/src/directions/OSRMDirectionsProvider.js +0 -90
  40. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +0 -30
  41. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +0 -1
  42. package/dist/core/src/directions/OpenRouteServiceProvider.js +0 -111
  43. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +0 -33
  44. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +0 -1
  45. package/dist/core/src/directions/ValhallaDirectionsProvider.js +0 -155
  46. package/dist/core/src/eta/ETAEngine.d.ts +0 -61
  47. package/dist/core/src/eta/ETAEngine.d.ts.map +0 -1
  48. package/dist/core/src/eta/ETAEngine.js +0 -74
  49. package/dist/core/src/features.d.ts +0 -76
  50. package/dist/core/src/features.d.ts.map +0 -1
  51. package/dist/core/src/features.js +0 -76
  52. package/dist/core/src/geo/bearing.d.ts +0 -11
  53. package/dist/core/src/geo/bearing.d.ts.map +0 -1
  54. package/dist/core/src/geo/bearing.js +0 -25
  55. package/dist/core/src/geo/cumulative.d.ts +0 -9
  56. package/dist/core/src/geo/cumulative.d.ts.map +0 -1
  57. package/dist/core/src/geo/cumulative.js +0 -20
  58. package/dist/core/src/geo/haversine.d.ts +0 -13
  59. package/dist/core/src/geo/haversine.d.ts.map +0 -1
  60. package/dist/core/src/geo/haversine.js +0 -39
  61. package/dist/core/src/geo/projection.d.ts +0 -7
  62. package/dist/core/src/geo/projection.d.ts.map +0 -1
  63. package/dist/core/src/geo/projection.js +0 -17
  64. package/dist/core/src/geofencing/GeofencingEngine.d.ts +0 -93
  65. package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +0 -1
  66. package/dist/core/src/geofencing/GeofencingEngine.js +0 -154
  67. package/dist/core/src/index.d.ts +0 -66
  68. package/dist/core/src/index.d.ts.map +0 -1
  69. package/dist/core/src/index.js +0 -62
  70. package/dist/core/src/kalman/KalmanFilter2D.d.ts +0 -46
  71. package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +0 -1
  72. package/dist/core/src/kalman/KalmanFilter2D.js +0 -77
  73. package/dist/core/src/license/LicenseManager.d.ts +0 -40
  74. package/dist/core/src/license/LicenseManager.d.ts.map +0 -1
  75. package/dist/core/src/license/LicenseManager.js +0 -119
  76. package/dist/core/src/license/verifyP256Signature.d.ts +0 -2
  77. package/dist/core/src/license/verifyP256Signature.d.ts.map +0 -1
  78. package/dist/core/src/license/verifyP256Signature.js +0 -26
  79. package/dist/core/src/off-route/NavigationFSM.d.ts +0 -42
  80. package/dist/core/src/off-route/NavigationFSM.d.ts.map +0 -1
  81. package/dist/core/src/off-route/NavigationFSM.js +0 -56
  82. package/dist/core/src/off-route/OffRouteFSM.d.ts +0 -70
  83. package/dist/core/src/off-route/OffRouteFSM.d.ts.map +0 -1
  84. package/dist/core/src/off-route/OffRouteFSM.js +0 -151
  85. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +0 -24
  86. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +0 -1
  87. package/dist/core/src/predictor/RouteOffsetPredictor.js +0 -55
  88. package/dist/core/src/progress/InstructionEditor.d.ts +0 -77
  89. package/dist/core/src/progress/InstructionEditor.d.ts.map +0 -1
  90. package/dist/core/src/progress/InstructionEditor.js +0 -151
  91. package/dist/core/src/progress/InstructionResolver.d.ts +0 -25
  92. package/dist/core/src/progress/InstructionResolver.d.ts.map +0 -1
  93. package/dist/core/src/progress/InstructionResolver.js +0 -78
  94. package/dist/core/src/progress/ProgressTracker.d.ts +0 -12
  95. package/dist/core/src/progress/ProgressTracker.d.ts.map +0 -1
  96. package/dist/core/src/progress/ProgressTracker.js +0 -22
  97. package/dist/core/src/renderer/MapRendererAdapter.d.ts +0 -72
  98. package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +0 -1
  99. package/dist/core/src/renderer/MapRendererAdapter.js +0 -19
  100. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +0 -83
  101. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +0 -1
  102. package/dist/core/src/route-builder/CustomRouteBuilder.js +0 -160
  103. package/dist/core/src/route-plan/RoutePlan.d.ts +0 -38
  104. package/dist/core/src/route-plan/RoutePlan.d.ts.map +0 -1
  105. package/dist/core/src/route-plan/RoutePlan.js +0 -102
  106. package/dist/core/src/session/NavigationSession.d.ts +0 -81
  107. package/dist/core/src/session/NavigationSession.d.ts.map +0 -1
  108. package/dist/core/src/session/NavigationSession.js +0 -115
  109. package/dist/core/src/snapper/ParallelRoadResolver.d.ts +0 -86
  110. package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +0 -1
  111. package/dist/core/src/snapper/ParallelRoadResolver.js +0 -83
  112. package/dist/core/src/snapper/RouteSnapper.d.ts +0 -38
  113. package/dist/core/src/snapper/RouteSnapper.d.ts.map +0 -1
  114. package/dist/core/src/snapper/RouteSnapper.js +0 -80
  115. package/dist/core/src/voice/VoiceTriggerEngine.d.ts +0 -80
  116. package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +0 -1
  117. package/dist/core/src/voice/VoiceTriggerEngine.js +0 -108
  118. package/dist/simulator/src/core/NoiseEngine.d.ts +0 -46
  119. package/dist/simulator/src/core/NoiseEngine.d.ts.map +0 -1
  120. package/dist/simulator/src/core/NoiseEngine.js +0 -106
  121. package/dist/simulator/src/core/RouteWalker.d.ts +0 -78
  122. package/dist/simulator/src/core/RouteWalker.d.ts.map +0 -1
  123. package/dist/simulator/src/core/RouteWalker.js +0 -180
  124. package/dist/simulator/src/core/SimulatorEngine.d.ts +0 -53
  125. package/dist/simulator/src/core/SimulatorEngine.d.ts.map +0 -1
  126. package/dist/simulator/src/core/SimulatorEngine.js +0 -298
  127. package/dist/simulator/src/core/types.d.ts +0 -157
  128. package/dist/simulator/src/core/types.d.ts.map +0 -1
  129. package/dist/simulator/src/core/types.js +0 -7
  130. package/dist/simulator/src/fleet/FleetSimulator.d.ts +0 -60
  131. package/dist/simulator/src/fleet/FleetSimulator.d.ts.map +0 -1
  132. package/dist/simulator/src/fleet/FleetSimulator.js +0 -114
  133. package/dist/simulator/src/index.d.ts +0 -28
  134. package/dist/simulator/src/index.d.ts.map +0 -1
  135. package/dist/simulator/src/index.js +0 -29
  136. package/dist/simulator/src/node.d.ts +0 -26
  137. package/dist/simulator/src/node.d.ts.map +0 -1
  138. package/dist/simulator/src/node.js +0 -25
  139. package/dist/simulator/src/react-native/SimulatorControls.d.ts +0 -35
  140. package/dist/simulator/src/react-native/SimulatorControls.d.ts.map +0 -1
  141. package/dist/simulator/src/react-native/SimulatorControls.js +0 -254
  142. package/dist/simulator/src/react-native/useSimulator.d.ts.map +0 -1
  143. package/dist/simulator/src/react-native/useSimulator.js +0 -100
  144. package/dist/simulator/src/replay/GpxParser.d.ts +0 -40
  145. package/dist/simulator/src/replay/GpxParser.d.ts.map +0 -1
  146. package/dist/simulator/src/replay/GpxParser.js +0 -95
  147. package/dist/simulator/src/replay/SessionRecorder.d.ts +0 -37
  148. package/dist/simulator/src/replay/SessionRecorder.d.ts.map +0 -1
  149. package/dist/simulator/src/replay/SessionRecorder.js +0 -120
  150. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts +0 -55
  151. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts.map +0 -1
  152. package/dist/simulator/src/scenarios/ScenarioInjector.js +0 -150
  153. package/src/__tests__/integration/simulator-pipeline.test.ts +0 -233
  154. package/src/core/NoiseEngine.ts +0 -137
  155. package/src/core/RouteWalker.ts +0 -227
  156. package/src/core/SimulatorEngine.ts +0 -364
  157. package/src/core/__tests__/NoiseEngine.test.ts +0 -131
  158. package/src/core/__tests__/RouteWalker.test.ts +0 -139
  159. package/src/core/__tests__/SimulatorEngine.test.ts +0 -317
  160. package/src/core/types.ts +0 -191
  161. package/src/fleet/FleetSimulator.ts +0 -162
  162. package/src/index.ts +0 -54
  163. package/src/node.ts +0 -48
  164. package/src/react-native/SimulatorControls.tsx +0 -473
  165. package/src/react-native/useSimulator.ts +0 -155
  166. package/src/replay/GpxParser.ts +0 -122
  167. package/src/replay/SessionRecorder.ts +0 -138
  168. package/src/replay/__tests__/GpxParser.test.ts +0 -136
  169. package/src/scenarios/ScenarioInjector.ts +0 -170
  170. package/src/scenarios/__tests__/ScenarioInjector.test.ts +0 -171
@@ -1,364 +0,0 @@
1
- /**
2
- * SimulatorEngine — The headless GPS simulation core.
3
- *
4
- * Works in Node.js for CI tests. NO React, NO Expo APIs.
5
- * All timers are cleaned up on destroy(). No memory leaks.
6
- *
7
- * State machine: idle → running ⇄ paused → complete
8
- */
9
-
10
- import { NoiseEngine } from './NoiseEngine';
11
- import { RouteWalker } from './RouteWalker';
12
- import { ScenarioInjector } from '../scenarios/ScenarioInjector';
13
- import type { SimPosition, SimulatorConfig, SimSession, Scenario } from './types';
14
-
15
- /** Generate a UUID v4 without external dependencies */
16
- function uuidV4(): string {
17
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
18
- const r = (Math.random() * 16) | 0;
19
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
20
- return v.toString(16);
21
- });
22
- }
23
-
24
- export type EngineState = 'idle' | 'running' | 'paused' | 'complete';
25
-
26
- export class SimulatorEngine {
27
- // ── Config ──────────────────────────────────────────────────────────────────
28
- private readonly config: Required<
29
- Omit<SimulatorConfig, 'onPositionUpdate' | 'onRouteComplete' | 'onScenarioStart' | 'onScenarioEnd'>
30
- > & Pick<SimulatorConfig, 'onPositionUpdate' | 'onRouteComplete' | 'onScenarioStart' | 'onScenarioEnd'>;
31
-
32
- // ── Internal components ──────────────────────────────────────────────────────
33
- private walker: RouteWalker;
34
- private noise: NoiseEngine;
35
- private injector: ScenarioInjector;
36
-
37
- // ── State ────────────────────────────────────────────────────────────────────
38
- private _state: EngineState = 'idle';
39
- private _position: SimPosition | null = null;
40
- private _speedMultiplier: 1 | 2 | 4 | 8;
41
- private _timer: ReturnType<typeof setInterval> | null = null;
42
- private _lastTick: number = 0;
43
-
44
- private static readonly TICK_MS = 50; // 20 FPS for smooth movement
45
-
46
- // ── Recording ────────────────────────────────────────────────────────────────
47
- private _recording = false;
48
- private _recordedPositions: SimPosition[] = [];
49
- private _recordedScenarios: Array<{ scenario: Scenario; firedAt: number }> = [];
50
- private _sessionId: string | null = null;
51
- private _sessionStart: number | null = null;
52
- private _noiseSamplesAdded = 0;
53
-
54
- constructor(config: SimulatorConfig) {
55
- const geometry = config.routeGeometry;
56
- if (!geometry || geometry.length < 2) {
57
- throw new Error('SimulatorEngine: routeGeometry must have at least 2 points');
58
- }
59
-
60
- this._speedMultiplier = config.speedMultiplier ?? 1;
61
-
62
- this.config = {
63
- routeGeometry: geometry,
64
- speedKmh: config.speedKmh ?? 50,
65
- speedMultiplier: this._speedMultiplier,
66
- noiseProfile: config.noiseProfile ?? NoiseEngine.STANDARD,
67
- loop: config.loop ?? false,
68
- startIndex: config.startIndex ?? 0,
69
- autoStart: config.autoStart ?? false,
70
- onPositionUpdate: config.onPositionUpdate,
71
- onRouteComplete: config.onRouteComplete,
72
- onScenarioStart: config.onScenarioStart,
73
- onScenarioEnd: config.onScenarioEnd,
74
- };
75
-
76
- this.walker = new RouteWalker(geometry, this.config.startIndex);
77
- this.noise = new NoiseEngine(this.config.noiseProfile);
78
-
79
- this.injector = new ScenarioInjector(
80
- (s) => {
81
- if (this._recording) {
82
- this._recordedScenarios.push({ scenario: s, firedAt: Date.now() });
83
- }
84
- try { this.config.onScenarioStart?.(s); } catch (_) { /* never throw */ }
85
- },
86
- (s) => {
87
- try { this.config.onScenarioEnd?.(s); } catch (_) { /* never throw */ }
88
- },
89
- );
90
-
91
- if (this.config.autoStart) {
92
- this.start();
93
- }
94
- }
95
-
96
- // ── Playback controls ────────────────────────────────────────────────────────
97
-
98
- start(): void {
99
- if (this._state === 'running') return;
100
- if (this._state === 'complete') return;
101
- this._state = 'running';
102
- this._startTimer();
103
- }
104
-
105
- stop(): void {
106
- this._stopTimer();
107
- this._state = 'idle';
108
- this._position = null;
109
- }
110
-
111
- pause(): void {
112
- if (this._state !== 'running') return;
113
- this._stopTimer();
114
- this._state = 'paused';
115
- }
116
-
117
- resume(): void {
118
- if (this._state !== 'paused') return;
119
- this._state = 'running';
120
- this._startTimer();
121
- }
122
-
123
- /**
124
- * Jump to any geometry index.
125
- * Bearing is recalculated from the new segment — seek() is always bearing-safe.
126
- */
127
- seek(geometryIndex: number): void {
128
- const wasRunning = this._state === 'running';
129
- if (wasRunning) this._stopTimer();
130
-
131
- const pos = this.walker.seek(geometryIndex);
132
- this._buildAndEmitPosition(pos.coord, pos.bearing, pos.distanceTravelled, false);
133
-
134
- // Reset the completion flag so simulation can continue from the new index
135
- if (this._state === 'complete' && !pos.isComplete) {
136
- this._state = 'idle';
137
- }
138
-
139
- if (wasRunning && !pos.isComplete) {
140
- this._state = 'running';
141
- this._startTimer();
142
- }
143
- }
144
-
145
- setSpeed(multiplier: 1 | 2 | 4 | 8): void {
146
- this._speedMultiplier = multiplier;
147
- if (this._state === 'running') {
148
- this._stopTimer();
149
- this._startTimer();
150
- }
151
- }
152
-
153
- // ── Scenario management ──────────────────────────────────────────────────────
154
-
155
- addScenario(scenario: Scenario): void {
156
- this.injector.register(scenario);
157
- }
158
-
159
- removeScenario(name: string): void {
160
- this.injector.remove(name);
161
- }
162
-
163
- clearScenarios(): void {
164
- this.injector.clear();
165
- }
166
-
167
- // ── State accessors ──────────────────────────────────────────────────────────
168
-
169
- get state(): EngineState {
170
- return this._state;
171
- }
172
-
173
- get position(): SimPosition | null {
174
- return this._position;
175
- }
176
-
177
- get progress(): number {
178
- if (this.walker.totalPoints <= 1) return 0;
179
- return Math.min(1, this.walker.currentIndex / (this.walker.totalPoints - 1));
180
- }
181
-
182
- // ── Session recording ────────────────────────────────────────────────────────
183
-
184
- startRecording(sessionId?: string): void {
185
- this._recording = true;
186
- this._recordedPositions = [];
187
- this._recordedScenarios = [];
188
- this._sessionId = sessionId ?? uuidV4();
189
- this._sessionStart = Date.now();
190
- this._noiseSamplesAdded = 0;
191
- }
192
-
193
- stopRecording(): SimSession {
194
- this._recording = false;
195
- const endedAt = Date.now();
196
- const startedAt = this._sessionStart ?? endedAt;
197
-
198
- const positions = this._recordedPositions;
199
- const durationMs = endedAt - startedAt;
200
-
201
- let distanceTravelledM = 0;
202
- if (positions.length > 0) {
203
- distanceTravelledM = positions[positions.length - 1]?.metadata?.distanceTravelled ?? 0;
204
- }
205
-
206
- const avgSpeedKmh =
207
- durationMs > 0
208
- ? ((distanceTravelledM / (durationMs / 1000)) * 3.6)
209
- : 0;
210
-
211
- return {
212
- id: this._sessionId ?? uuidV4(),
213
- startedAt: new Date(startedAt).toISOString(),
214
- endedAt: new Date(endedAt).toISOString(),
215
- route: this.config.routeGeometry,
216
- positions,
217
- scenarios: this._recordedScenarios,
218
- stats: {
219
- durationMs,
220
- distanceTravelledM,
221
- avgSpeedKmh,
222
- noiseSamplesAdded: this._noiseSamplesAdded,
223
- scenariosFired: this._recordedScenarios.length,
224
- },
225
- };
226
- }
227
-
228
- // ── Destroy ──────────────────────────────────────────────────────────────────
229
-
230
- destroy(): void {
231
- this._stopTimer();
232
- this.injector.destroy();
233
- this._state = 'idle';
234
- this._position = null;
235
- this._recording = false;
236
- this._recordedPositions = [];
237
- }
238
-
239
- // ── Private ──────────────────────────────────────────────────────────────────
240
-
241
- private _startTimer(): void {
242
- this._lastTick = Date.now();
243
- const intervalMs = Math.round(1000 / this._speedMultiplier);
244
- this._timer = setInterval(() => this._tick(), intervalMs);
245
- }
246
-
247
- private _stopTimer(): void {
248
- if (this._timer !== null) {
249
- clearInterval(this._timer);
250
- this._timer = null;
251
- }
252
- }
253
-
254
- private _tick(): void {
255
- if (this._state !== 'running') return;
256
-
257
- const now = Date.now();
258
- this._lastTick = now;
259
-
260
- const speedMs = (this.config.speedKmh / 3.6);
261
- const walkerPos = this.walker.advance(1);
262
-
263
- // Fire any scenarios at this index
264
- const activeScenarioName = this.injector.tick(walkerPos.index);
265
-
266
- // Apply noise (skip if GPS_BLACKOUT scenario is active)
267
- const activeScenario = this.injector.activeScenario;
268
- let coord = walkerPos.coord;
269
- let isNoisy = false;
270
-
271
- if (activeScenario?.type !== 'GPS_BLACKOUT') {
272
- coord = this.noise.apply(walkerPos.coord);
273
- isNoisy = this.noise.currentProfile.stdDevM > 0;
274
- if (isNoisy) this._noiseSamplesAdded++;
275
- }
276
-
277
- // Compute speed — apply speed spike if active
278
- //let speedMs = (this.config.speedKmh / 3.6);
279
- if (activeScenario?.type === 'SPEED_SPIKE') {
280
- const spikeKmh = (activeScenario.params?.['spikeKmh'] as number | undefined) ?? this.config.speedKmh;
281
- //speedMs = spikeKmh / 3.6;
282
- }
283
-
284
- // Compute accuracy — degrade if ACCURACY_DROP scenario is active
285
- let accuracy = 8 + Math.random() * 6;
286
- if (activeScenario?.type === 'ACCURACY_DROP') {
287
- accuracy = (activeScenario.params?.['newAccuracyM'] as number | undefined) ?? accuracy;
288
- }
289
-
290
- this._buildAndEmitPosition(coord, walkerPos.bearing, walkerPos.distanceTravelled, isNoisy, {
291
- speedMs,
292
- accuracy,
293
- activeScenarioName,
294
- segmentIndex: Math.floor(walkerPos.index),
295
- });
296
-
297
- if (walkerPos.isComplete) {
298
- this._stopTimer();
299
-
300
- if (this.config.loop) {
301
- // Restart from the beginning
302
- this.walker = new RouteWalker(this.config.routeGeometry, 0);
303
- this._startTimer();
304
- } else {
305
- this._state = 'complete';
306
- }
307
-
308
- try { this.config.onRouteComplete?.(); } catch (_) { /* never throw */ }
309
- }
310
- }
311
-
312
- private _buildAndEmitPosition(
313
- coord: [number, number],
314
- bearing: number,
315
- distanceTravelled: number,
316
- isNoisy: boolean,
317
- opts?: {
318
- speedMs?: number;
319
- accuracy?: number;
320
- activeScenarioName?: string;
321
- segmentIndex?: number;
322
- },
323
- ): void {
324
- const speedMs = opts?.speedMs ?? (this.config.speedKmh / 3.6);
325
- const accuracy = opts?.accuracy ?? 8;
326
- const ts = Date.now();
327
- const locationCoords = {
328
- latitude: coord[1],
329
- longitude: coord[0],
330
- altitude: 0,
331
- accuracy,
332
- altitudeAccuracy: null,
333
- heading: bearing,
334
- speed: speedMs,
335
- };
336
-
337
- const pos: SimPosition = {
338
- coord: coord as [number, number],
339
- bearing,
340
- speed: speedMs,
341
- accuracy,
342
- timestamp: ts,
343
- coords: locationCoords,
344
- location: {
345
- coords: locationCoords,
346
- timestamp: ts,
347
- },
348
- metadata: {
349
- segmentIndex: opts?.segmentIndex ?? Math.floor(this.walker.currentIndex),
350
- distanceTravelled,
351
- isNoisy,
352
- scenarioActive: opts?.activeScenarioName,
353
- },
354
- };
355
-
356
- this._position = pos;
357
-
358
- if (this._recording) {
359
- this._recordedPositions.push(pos);
360
- }
361
-
362
- try { this.config.onPositionUpdate?.(pos); } catch (_) { /* never throw */ }
363
- }
364
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * NoiseEngine unit tests
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { NoiseEngine } from '@ingissa/navcore-simulator';
6
- import type { Coordinate } from '@ingissa/navcore-core';
7
-
8
- const ORIGIN: Coordinate = [0.0, 0.0];
9
- const RUNS = 500; // statistical test runs
10
-
11
- /** Sample standard deviation of an array */
12
- function stdDev(values: number[]): number {
13
- const mean = values.reduce((a, b) => a + b, 0) / values.length;
14
- const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
15
- return Math.sqrt(variance);
16
- }
17
-
18
- describe('NoiseEngine', () => {
19
- describe('NONE profile', () => {
20
- it('returns the exact same coordinate', () => {
21
- const engine = new NoiseEngine(NoiseEngine.NONE);
22
- const result = engine.apply(ORIGIN);
23
- expect(result[0]).toBe(ORIGIN[0]);
24
- expect(result[1]).toBe(ORIGIN[1]);
25
- });
26
-
27
- it('never mutates the input coordinate', () => {
28
- const engine = new NoiseEngine(NoiseEngine.NONE);
29
- const coord: Coordinate = [1.234, 5.678];
30
- engine.apply(coord);
31
- expect(coord[0]).toBe(1.234);
32
- expect(coord[1]).toBe(5.678);
33
- });
34
- });
35
-
36
- describe('STANDARD profile (12m stddev)', () => {
37
- it('applies noise that shifts the coordinate', () => {
38
- const engine = new NoiseEngine(NoiseEngine.STANDARD);
39
- const result = engine.apply(ORIGIN);
40
- // There's a tiny probability of hitting exactly 0 — this test is still valid
41
- const changed = result[0] !== ORIGIN[0] || result[1] !== ORIGIN[1];
42
- expect(changed).toBe(true);
43
- });
44
-
45
- it('stddev of longitude offsets is within expected range (~12m ± 50%)', () => {
46
- const engine = new NoiseEngine(NoiseEngine.STANDARD);
47
- // 1° longitude ≈ 111320m
48
- const lngOffsets: number[] = [];
49
- for (let i = 0; i < RUNS; i++) {
50
- const r = engine.apply(ORIGIN);
51
- lngOffsets.push((r[0] - ORIGIN[0]) * 111320);
52
- }
53
- const sd = stdDev(lngOffsets);
54
- // Should be roughly 12m (allow 50% tolerance for statistical variation)
55
- expect(sd).toBeGreaterThan(6);
56
- expect(sd).toBeLessThan(24);
57
- });
58
- });
59
-
60
- describe('Seed determinism', () => {
61
- it('same seed produces identical sequence', () => {
62
- const profile = { ...NoiseEngine.STANDARD, seed: 42 };
63
- const engineA = new NoiseEngine(profile);
64
- const engineB = new NoiseEngine(profile);
65
-
66
- for (let i = 0; i < 20; i++) {
67
- const a = engineA.apply(ORIGIN);
68
- const b = engineB.apply(ORIGIN);
69
- expect(a[0]).toBe(b[0]);
70
- expect(a[1]).toBe(b[1]);
71
- }
72
- });
73
-
74
- it('different seeds produce different sequences', () => {
75
- const engineA = new NoiseEngine({ ...NoiseEngine.STANDARD, seed: 1 });
76
- const engineB = new NoiseEngine({ ...NoiseEngine.STANDARD, seed: 2 });
77
-
78
- const resultsA = Array.from({ length: 10 }, () => engineA.apply(ORIGIN));
79
- const resultsB = Array.from({ length: 10 }, () => engineB.apply(ORIGIN));
80
-
81
- // At least one result should differ
82
- const anyDiff = resultsA.some((a, i) => a[0] !== resultsB[i]![0]);
83
- expect(anyDiff).toBe(true);
84
- });
85
- });
86
-
87
- describe('CUSTOM profile', () => {
88
- it('delegates to customFn', () => {
89
- let callCount = 0;
90
- const engine = new NoiseEngine({
91
- type: 'CUSTOM',
92
- stdDevM: 0,
93
- customFn: (idx) => {
94
- callCount++;
95
- return [idx * 0.001, idx * 0.0005];
96
- },
97
- });
98
-
99
- const r0 = engine.apply(ORIGIN);
100
- const r1 = engine.apply(ORIGIN);
101
-
102
- expect(callCount).toBe(2);
103
- expect(r0[0]).toBeCloseTo(0 * 0.001, 6);
104
- expect(r1[0]).toBeCloseTo(1 * 0.001, 6);
105
- });
106
-
107
- it('returns original coord if customFn is missing', () => {
108
- const engine = new NoiseEngine({ type: 'CUSTOM', stdDevM: 0 });
109
- const result = engine.apply(ORIGIN);
110
- expect(result[0]).toBe(ORIGIN[0]);
111
- });
112
- });
113
-
114
- describe('setProfile', () => {
115
- it('hot-swaps from STANDARD to NONE mid-simulation', () => {
116
- const engine = new NoiseEngine(NoiseEngine.STANDARD);
117
- engine.setProfile(NoiseEngine.NONE);
118
- const result = engine.apply([1.0, 2.0]);
119
- expect(result[0]).toBe(1.0);
120
- expect(result[1]).toBe(2.0);
121
- });
122
- });
123
-
124
- describe('Static presets', () => {
125
- it('CLEAN has stdDevM: 3', () => expect(NoiseEngine.CLEAN.stdDevM).toBe(3));
126
- it('STANDARD has stdDevM: 12', () => expect(NoiseEngine.STANDARD.stdDevM).toBe(12));
127
- it('URBAN has stdDevM: 25', () => expect(NoiseEngine.URBAN.stdDevM).toBe(25));
128
- it('DEGRADED has stdDevM: 50', () => expect(NoiseEngine.DEGRADED.stdDevM).toBe(50));
129
- it('NONE has stdDevM: 0', () => expect(NoiseEngine.NONE.stdDevM).toBe(0));
130
- });
131
- });
@@ -1,139 +0,0 @@
1
- /**
2
- * RouteWalker unit tests
3
- */
4
- import { describe, it, expect, beforeEach } from 'vitest';
5
- import { RouteWalker } from '@ingissa/navcore-simulator';
6
- import type { Coordinate } from '@ingissa/navcore-core';
7
-
8
- // Simple 5-point straight line going east
9
- // Each point is ~111m apart (1 second at 400 km/h, but fine for geometry tests)
10
- const STRAIGHT: Coordinate[] = [
11
- [0.000, 0.000],
12
- [0.001, 0.000],
13
- [0.002, 0.000],
14
- [0.003, 0.000],
15
- [0.004, 0.000],
16
- ];
17
-
18
- describe('RouteWalker', () => {
19
- let walker: RouteWalker;
20
-
21
- beforeEach(() => {
22
- walker = new RouteWalker(STRAIGHT);
23
- });
24
-
25
- it('throws with fewer than 2 points', () => {
26
- expect(() => new RouteWalker([[0, 0]])).toThrow();
27
- expect(() => new RouteWalker([])).toThrow();
28
- });
29
-
30
- it('initialises at index 0 and is not complete', () => {
31
- expect(walker.currentIndex).toBe(0);
32
- expect(walker.isComplete).toBe(false);
33
- });
34
-
35
- it('advance(1) moves to index 1', () => {
36
- const pos = walker.advance(1);
37
- expect(pos.index).toBe(1);
38
- expect(walker.currentIndex).toBe(1);
39
- expect(pos.isComplete).toBe(false);
40
- });
41
-
42
- it('advance(N) reaches the last point and marks complete', () => {
43
- const pos = walker.advance(STRAIGHT.length - 1);
44
- expect(pos.isComplete).toBe(true);
45
- expect(walker.isComplete).toBe(true);
46
- });
47
-
48
- it('advance beyond end clamps to last point', () => {
49
- walker.advance(100);
50
- expect(walker.currentIndex).toBe(STRAIGHT.length - 1);
51
- expect(walker.isComplete).toBe(true);
52
- });
53
-
54
- it('advance does nothing after completion', () => {
55
- walker.advance(100);
56
- const pos = walker.advance(1);
57
- expect(pos.index).toBe(STRAIGHT.length - 1);
58
- });
59
-
60
- it('interpolate returns midpoint correctly', () => {
61
- const mid = walker.interpolate(0, 0.5);
62
- expect(mid[0]).toBeCloseTo(0.0005, 5);
63
- expect(mid[1]).toBeCloseTo(0.0, 5);
64
- });
65
-
66
- it('interpolate clamps fraction to [0, 1]', () => {
67
- const start = walker.interpolate(0, -0.5);
68
- const startExpected = STRAIGHT[0]!;
69
- expect(start[0]).toBeCloseTo(startExpected[0], 5);
70
-
71
- const end = walker.interpolate(0, 1.5);
72
- const endExpected = STRAIGHT[1]!;
73
- expect(end[0]).toBeCloseTo(endExpected[0], 5);
74
- });
75
-
76
- it('bearingAt returns ~90° for eastward segment', () => {
77
- const bearing = walker.bearingAt(0);
78
- // Should be roughly east (90°)
79
- expect(bearing).toBeGreaterThan(85);
80
- expect(bearing).toBeLessThan(95);
81
- });
82
-
83
- it('bearingAt on last index returns bearing of last segment', () => {
84
- const bearing = walker.bearingAt(STRAIGHT.length - 1);
85
- expect(bearing).toBeGreaterThan(85);
86
- expect(bearing).toBeLessThan(95);
87
- });
88
-
89
- it('distanceToIndex(0) is 0', () => {
90
- expect(walker.distanceToIndex(0)).toBe(0);
91
- });
92
-
93
- it('distanceToIndex increases monotonically', () => {
94
- for (let i = 1; i < STRAIGHT.length; i++) {
95
- expect(walker.distanceToIndex(i)).toBeGreaterThan(walker.distanceToIndex(i - 1));
96
- }
97
- });
98
-
99
- it('totalDistanceM is positive and equals distanceToIndex(last)', () => {
100
- const total = walker.totalDistanceM;
101
- expect(total).toBeGreaterThan(0);
102
- expect(total).toBeCloseTo(walker.distanceToIndex(STRAIGHT.length - 1), 1);
103
- });
104
-
105
- it('totalPoints equals geometry length', () => {
106
- expect(walker.totalPoints).toBe(STRAIGHT.length);
107
- });
108
-
109
- it('seek jumps to the correct index', () => {
110
- const pos = walker.seek(3);
111
- expect(pos.index).toBe(3);
112
- expect(walker.currentIndex).toBe(3);
113
- expect(pos.isComplete).toBe(false);
114
- });
115
-
116
- it('seek recalculates bearing at new position', () => {
117
- const pos = walker.seek(2);
118
- // Eastward segment → bearing ~90°
119
- expect(pos.bearing).toBeGreaterThan(85);
120
- expect(pos.bearing).toBeLessThan(95);
121
- });
122
-
123
- it('seek to last index marks complete', () => {
124
- const pos = walker.seek(STRAIGHT.length - 1);
125
- expect(pos.isComplete).toBe(true);
126
- });
127
-
128
- it('startIndex constructor param positions correctly', () => {
129
- const w = new RouteWalker(STRAIGHT, 2);
130
- expect(w.currentIndex).toBe(2);
131
- expect(w.distanceToIndex(2)).toBeGreaterThan(0);
132
- });
133
-
134
- it('advance with fractional steps interpolates coord', () => {
135
- const pos = walker.advance(0.5);
136
- // Should be halfway between point 0 and 1
137
- expect(pos.coord[0]).toBeCloseTo(0.0005, 4);
138
- });
139
- });