@ingissa/navcore-simulator 1.0.7 → 1.0.9

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 (169) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.mts +87 -0
  3. package/dist/{simulator/src/react-native/useSimulator.d.ts → index.d.ts} +37 -9
  4. package/dist/index.js +1393 -0
  5. package/dist/index.mjs +1383 -0
  6. package/dist/node.d.mts +527 -0
  7. package/dist/node.d.ts +527 -0
  8. package/dist/node.js +957 -0
  9. package/dist/node.mjs +949 -0
  10. package/dist/react-native.d.mts +4 -0
  11. package/dist/react-native.d.ts +4 -0
  12. package/dist/react-native.js +1393 -0
  13. package/dist/react-native.mjs +1383 -0
  14. package/package.json +19 -16
  15. package/dist/core/src/NavCore.d.ts +0 -142
  16. package/dist/core/src/NavCore.d.ts.map +0 -1
  17. package/dist/core/src/NavCore.js +0 -390
  18. package/dist/core/src/bearing/BearingEngine.d.ts +0 -24
  19. package/dist/core/src/bearing/BearingEngine.d.ts.map +0 -1
  20. package/dist/core/src/bearing/BearingEngine.js +0 -50
  21. package/dist/core/src/constants.d.ts +0 -34
  22. package/dist/core/src/constants.d.ts.map +0 -1
  23. package/dist/core/src/constants.js +0 -41
  24. package/dist/core/src/corridor/DeviationDetector.d.ts +0 -33
  25. package/dist/core/src/corridor/DeviationDetector.d.ts.map +0 -1
  26. package/dist/core/src/corridor/DeviationDetector.js +0 -83
  27. package/dist/core/src/corridor/DynamicCorridor.d.ts +0 -13
  28. package/dist/core/src/corridor/DynamicCorridor.d.ts.map +0 -1
  29. package/dist/core/src/corridor/DynamicCorridor.js +0 -26
  30. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +0 -35
  31. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +0 -1
  32. package/dist/core/src/dead-reckoning/DeadReckoning.js +0 -49
  33. package/dist/core/src/directions/DirectionsProvider.d.ts +0 -70
  34. package/dist/core/src/directions/DirectionsProvider.d.ts.map +0 -1
  35. package/dist/core/src/directions/DirectionsProvider.js +0 -7
  36. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +0 -26
  37. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +0 -1
  38. package/dist/core/src/directions/OSRMDirectionsProvider.js +0 -90
  39. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +0 -30
  40. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +0 -1
  41. package/dist/core/src/directions/OpenRouteServiceProvider.js +0 -111
  42. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +0 -33
  43. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +0 -1
  44. package/dist/core/src/directions/ValhallaDirectionsProvider.js +0 -155
  45. package/dist/core/src/eta/ETAEngine.d.ts +0 -61
  46. package/dist/core/src/eta/ETAEngine.d.ts.map +0 -1
  47. package/dist/core/src/eta/ETAEngine.js +0 -74
  48. package/dist/core/src/features.d.ts +0 -76
  49. package/dist/core/src/features.d.ts.map +0 -1
  50. package/dist/core/src/features.js +0 -76
  51. package/dist/core/src/geo/bearing.d.ts +0 -11
  52. package/dist/core/src/geo/bearing.d.ts.map +0 -1
  53. package/dist/core/src/geo/bearing.js +0 -25
  54. package/dist/core/src/geo/cumulative.d.ts +0 -9
  55. package/dist/core/src/geo/cumulative.d.ts.map +0 -1
  56. package/dist/core/src/geo/cumulative.js +0 -20
  57. package/dist/core/src/geo/haversine.d.ts +0 -13
  58. package/dist/core/src/geo/haversine.d.ts.map +0 -1
  59. package/dist/core/src/geo/haversine.js +0 -39
  60. package/dist/core/src/geo/projection.d.ts +0 -7
  61. package/dist/core/src/geo/projection.d.ts.map +0 -1
  62. package/dist/core/src/geo/projection.js +0 -17
  63. package/dist/core/src/geofencing/GeofencingEngine.d.ts +0 -93
  64. package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +0 -1
  65. package/dist/core/src/geofencing/GeofencingEngine.js +0 -154
  66. package/dist/core/src/index.d.ts +0 -66
  67. package/dist/core/src/index.d.ts.map +0 -1
  68. package/dist/core/src/index.js +0 -62
  69. package/dist/core/src/kalman/KalmanFilter2D.d.ts +0 -46
  70. package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +0 -1
  71. package/dist/core/src/kalman/KalmanFilter2D.js +0 -77
  72. package/dist/core/src/license/LicenseManager.d.ts +0 -40
  73. package/dist/core/src/license/LicenseManager.d.ts.map +0 -1
  74. package/dist/core/src/license/LicenseManager.js +0 -119
  75. package/dist/core/src/license/verifyP256Signature.d.ts +0 -2
  76. package/dist/core/src/license/verifyP256Signature.d.ts.map +0 -1
  77. package/dist/core/src/license/verifyP256Signature.js +0 -26
  78. package/dist/core/src/off-route/NavigationFSM.d.ts +0 -42
  79. package/dist/core/src/off-route/NavigationFSM.d.ts.map +0 -1
  80. package/dist/core/src/off-route/NavigationFSM.js +0 -56
  81. package/dist/core/src/off-route/OffRouteFSM.d.ts +0 -70
  82. package/dist/core/src/off-route/OffRouteFSM.d.ts.map +0 -1
  83. package/dist/core/src/off-route/OffRouteFSM.js +0 -151
  84. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +0 -24
  85. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +0 -1
  86. package/dist/core/src/predictor/RouteOffsetPredictor.js +0 -55
  87. package/dist/core/src/progress/InstructionEditor.d.ts +0 -77
  88. package/dist/core/src/progress/InstructionEditor.d.ts.map +0 -1
  89. package/dist/core/src/progress/InstructionEditor.js +0 -151
  90. package/dist/core/src/progress/InstructionResolver.d.ts +0 -25
  91. package/dist/core/src/progress/InstructionResolver.d.ts.map +0 -1
  92. package/dist/core/src/progress/InstructionResolver.js +0 -78
  93. package/dist/core/src/progress/ProgressTracker.d.ts +0 -12
  94. package/dist/core/src/progress/ProgressTracker.d.ts.map +0 -1
  95. package/dist/core/src/progress/ProgressTracker.js +0 -22
  96. package/dist/core/src/renderer/MapRendererAdapter.d.ts +0 -72
  97. package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +0 -1
  98. package/dist/core/src/renderer/MapRendererAdapter.js +0 -19
  99. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +0 -83
  100. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +0 -1
  101. package/dist/core/src/route-builder/CustomRouteBuilder.js +0 -160
  102. package/dist/core/src/route-plan/RoutePlan.d.ts +0 -38
  103. package/dist/core/src/route-plan/RoutePlan.d.ts.map +0 -1
  104. package/dist/core/src/route-plan/RoutePlan.js +0 -102
  105. package/dist/core/src/session/NavigationSession.d.ts +0 -81
  106. package/dist/core/src/session/NavigationSession.d.ts.map +0 -1
  107. package/dist/core/src/session/NavigationSession.js +0 -115
  108. package/dist/core/src/snapper/ParallelRoadResolver.d.ts +0 -86
  109. package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +0 -1
  110. package/dist/core/src/snapper/ParallelRoadResolver.js +0 -83
  111. package/dist/core/src/snapper/RouteSnapper.d.ts +0 -38
  112. package/dist/core/src/snapper/RouteSnapper.d.ts.map +0 -1
  113. package/dist/core/src/snapper/RouteSnapper.js +0 -80
  114. package/dist/core/src/voice/VoiceTriggerEngine.d.ts +0 -80
  115. package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +0 -1
  116. package/dist/core/src/voice/VoiceTriggerEngine.js +0 -108
  117. package/dist/simulator/src/core/NoiseEngine.d.ts +0 -46
  118. package/dist/simulator/src/core/NoiseEngine.d.ts.map +0 -1
  119. package/dist/simulator/src/core/NoiseEngine.js +0 -106
  120. package/dist/simulator/src/core/RouteWalker.d.ts +0 -78
  121. package/dist/simulator/src/core/RouteWalker.d.ts.map +0 -1
  122. package/dist/simulator/src/core/RouteWalker.js +0 -180
  123. package/dist/simulator/src/core/SimulatorEngine.d.ts +0 -53
  124. package/dist/simulator/src/core/SimulatorEngine.d.ts.map +0 -1
  125. package/dist/simulator/src/core/SimulatorEngine.js +0 -298
  126. package/dist/simulator/src/core/types.d.ts +0 -157
  127. package/dist/simulator/src/core/types.d.ts.map +0 -1
  128. package/dist/simulator/src/core/types.js +0 -7
  129. package/dist/simulator/src/fleet/FleetSimulator.d.ts +0 -60
  130. package/dist/simulator/src/fleet/FleetSimulator.d.ts.map +0 -1
  131. package/dist/simulator/src/fleet/FleetSimulator.js +0 -114
  132. package/dist/simulator/src/index.d.ts +0 -28
  133. package/dist/simulator/src/index.d.ts.map +0 -1
  134. package/dist/simulator/src/index.js +0 -29
  135. package/dist/simulator/src/node.d.ts +0 -26
  136. package/dist/simulator/src/node.d.ts.map +0 -1
  137. package/dist/simulator/src/node.js +0 -25
  138. package/dist/simulator/src/react-native/SimulatorControls.d.ts +0 -35
  139. package/dist/simulator/src/react-native/SimulatorControls.d.ts.map +0 -1
  140. package/dist/simulator/src/react-native/SimulatorControls.js +0 -254
  141. package/dist/simulator/src/react-native/useSimulator.d.ts.map +0 -1
  142. package/dist/simulator/src/react-native/useSimulator.js +0 -100
  143. package/dist/simulator/src/replay/GpxParser.d.ts +0 -40
  144. package/dist/simulator/src/replay/GpxParser.d.ts.map +0 -1
  145. package/dist/simulator/src/replay/GpxParser.js +0 -95
  146. package/dist/simulator/src/replay/SessionRecorder.d.ts +0 -37
  147. package/dist/simulator/src/replay/SessionRecorder.d.ts.map +0 -1
  148. package/dist/simulator/src/replay/SessionRecorder.js +0 -120
  149. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts +0 -55
  150. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts.map +0 -1
  151. package/dist/simulator/src/scenarios/ScenarioInjector.js +0 -150
  152. package/src/__tests__/integration/simulator-pipeline.test.ts +0 -233
  153. package/src/core/NoiseEngine.ts +0 -137
  154. package/src/core/RouteWalker.ts +0 -227
  155. package/src/core/SimulatorEngine.ts +0 -364
  156. package/src/core/__tests__/NoiseEngine.test.ts +0 -131
  157. package/src/core/__tests__/RouteWalker.test.ts +0 -139
  158. package/src/core/__tests__/SimulatorEngine.test.ts +0 -317
  159. package/src/core/types.ts +0 -191
  160. package/src/fleet/FleetSimulator.ts +0 -162
  161. package/src/index.ts +0 -54
  162. package/src/node.ts +0 -48
  163. package/src/react-native/SimulatorControls.tsx +0 -473
  164. package/src/react-native/useSimulator.ts +0 -155
  165. package/src/replay/GpxParser.ts +0 -122
  166. package/src/replay/SessionRecorder.ts +0 -138
  167. package/src/replay/__tests__/GpxParser.test.ts +0 -136
  168. package/src/scenarios/ScenarioInjector.ts +0 -170
  169. 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
- });