@ingissa/navcore-simulator 1.0.9 → 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.
- package/LICENSE +3 -3
- package/README.md +160 -357
- package/package.json +61 -61
package/LICENSE
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copyright (c) 2026 Abdelnasser Aitabi. All rights reserved.
|
|
2
|
-
|
|
3
|
-
No license is granted except by separate written agreement.
|
|
1
|
+
Copyright (c) 2026 Abdelnasser Aitabi. All rights reserved.
|
|
2
|
+
|
|
3
|
+
No license is granted except by separate written agreement.
|
package/README.md
CHANGED
|
@@ -1,66 +1,92 @@
|
|
|
1
|
-
# @ingissa/navcore-simulator
|
|
1
|
+
# @ingissa/navcore-simulator
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[Official Website](https://navcore-web.vercel.app/)
|
|
4
4
|
|
|
5
5
|
> Headless GPS simulation engine for the NavCore ecosystem.
|
|
6
|
-
> Works in **Node.js** (CI), **React Native**, and
|
|
6
|
+
> Works in **Node.js** (CI/CD), **React Native**, and browser runtimes.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@ingissa/navcore-simulator)
|
|
9
|
+

|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
7
11
|
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
## Contents
|
|
11
15
|
|
|
16
|
+
- [Overview](#overview)
|
|
12
17
|
- [Installation](#installation)
|
|
13
18
|
- [Quick Start](#quick-start)
|
|
14
19
|
- [Core Classes](#core-classes)
|
|
15
|
-
- [SimulatorEngine](#simulatorengine)
|
|
16
|
-
- [RouteWalker](#routewalker)
|
|
17
|
-
- [NoiseEngine](#noiseengine)
|
|
18
|
-
- [ScenarioInjector](#scenarioinjector)
|
|
19
|
-
- [GpxParser](#gpxparser)
|
|
20
|
-
- [SessionRecorder](#sessionrecorder)
|
|
21
|
-
- [FleetSimulator](#fleetsimulator
|
|
22
|
-
- [React Native](#react-native)
|
|
23
|
-
- [useSimulator](#usesimulator)
|
|
24
|
-
- [SimulatorControls](#simulatorcontrols)
|
|
20
|
+
- [1. SimulatorEngine](#1-simulatorengine)
|
|
21
|
+
- [2. RouteWalker](#2-routewalker)
|
|
22
|
+
- [3. NoiseEngine](#3-noiseengine)
|
|
23
|
+
- [4. ScenarioInjector](#4-scenarioinjector)
|
|
24
|
+
- [5. GpxParser](#5-gpxparser)
|
|
25
|
+
- [6. SessionRecorder](#6-sessionrecorder)
|
|
26
|
+
- [7. FleetSimulator](#7-fleetsimulator)
|
|
27
|
+
- [React Native Integration](#react-native-integration)
|
|
25
28
|
- [Noise Profiles](#noise-profiles)
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
- [CI Regression Testing](#ci-regression-testing)
|
|
30
|
+
- [Licensing](#licensing)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
`@ingissa/navcore-simulator` provides realistic GPS signal simulation for development, UI validation, and automated regression testing. It simulates vehicle movement along route geometry with configurable speeds, bearing interpolation, realistic Gaussian noise, and dynamic scenario faults (such as GPS blackouts, sensor jitter, and temporary deviations).
|
|
30
37
|
|
|
31
38
|
---
|
|
32
39
|
|
|
33
40
|
## Installation
|
|
34
41
|
|
|
35
42
|
```bash
|
|
36
|
-
|
|
37
|
-
# Import directly from the source tree:
|
|
38
|
-
import { SimulatorEngine, NoiseEngine } from '@ingissa/navcore-simulator'
|
|
43
|
+
npm install @ingissa/navcore-simulator @ingissa/navcore-core
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
### Peer Dependencies
|
|
47
|
+
- `@ingissa/navcore-core`: `^1.0.9` (required)
|
|
48
|
+
- `react`: `>=18` (optional, needed only for React Native UI hooks)
|
|
49
|
+
- `react-native`: `>=0.72` (optional, needed only for React Native UI hooks)
|
|
50
|
+
|
|
51
|
+
### Subpath Exports
|
|
52
|
+
- `@ingissa/navcore-simulator`: Universal export containing engine, utilities, and UI controls.
|
|
53
|
+
- `@ingissa/navcore-simulator/node`: Headless export for Node.js scripts, Vitest/Jest suites, and CI pipelines (zero React/DOM dependencies).
|
|
54
|
+
- `@ingissa/navcore-simulator/react-native`: Mobile export with `useSimulator` and `SimulatorControls`.
|
|
42
55
|
|
|
43
56
|
---
|
|
44
57
|
|
|
45
58
|
## Quick Start
|
|
46
59
|
|
|
60
|
+
### Node.js / CI Headless Simulation
|
|
61
|
+
|
|
47
62
|
```typescript
|
|
48
|
-
import { SimulatorEngine, NoiseEngine, ScenarioInjector } from '@ingissa/navcore-simulator';
|
|
63
|
+
import { SimulatorEngine, NoiseEngine, ScenarioInjector } from '@ingissa/navcore-simulator/node';
|
|
64
|
+
import type { Coordinate } from '@ingissa/navcore-core';
|
|
65
|
+
|
|
66
|
+
const routeGeometry: Coordinate[] = [
|
|
67
|
+
[2.3522, 48.8566],
|
|
68
|
+
[2.3400, 48.8650],
|
|
69
|
+
[2.3009, 48.8741],
|
|
70
|
+
];
|
|
49
71
|
|
|
50
72
|
const engine = new SimulatorEngine({
|
|
51
|
-
routeGeometry
|
|
73
|
+
routeGeometry,
|
|
52
74
|
speedKmh: 50,
|
|
53
|
-
speedMultiplier:
|
|
75
|
+
speedMultiplier: 2, // 2× real-time playback
|
|
54
76
|
noiseProfile: NoiseEngine.STANDARD,
|
|
55
77
|
loop: false,
|
|
56
78
|
onPositionUpdate: (pos) => {
|
|
57
|
-
console.log(pos.coord, pos.bearing
|
|
79
|
+
console.log(`GPS: [${pos.coord[0].toFixed(5)}, ${pos.coord[1].toFixed(5)}] | Heading: ${Math.round(pos.bearing)}° | Speed: ${pos.speed} m/s`);
|
|
80
|
+
},
|
|
81
|
+
onRouteComplete: () => {
|
|
82
|
+
console.log('Simulation journey finished.');
|
|
58
83
|
},
|
|
59
|
-
onRouteComplete: () => console.log('Route complete'),
|
|
60
84
|
});
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
engine.
|
|
86
|
+
// Inject a 5-second GPS blackout at geometry segment 10
|
|
87
|
+
engine.addScenario(ScenarioInjector.gpsBlackout(10, 5000));
|
|
88
|
+
|
|
89
|
+
// Start playback
|
|
64
90
|
engine.start();
|
|
65
91
|
```
|
|
66
92
|
|
|
@@ -68,361 +94,138 @@ engine.start();
|
|
|
68
94
|
|
|
69
95
|
## Core Classes
|
|
70
96
|
|
|
71
|
-
### SimulatorEngine
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
engine.stop()
|
|
81
|
-
engine.pause()
|
|
82
|
-
engine.resume()
|
|
83
|
-
engine.seek(geometryIndex: number) // jump to any route index, recalculates bearing
|
|
84
|
-
engine.setSpeed(1 | 2 | 4 | 8)
|
|
85
|
-
|
|
86
|
-
// Scenarios
|
|
87
|
-
engine.addScenario(scenario: Scenario)
|
|
88
|
-
engine.removeScenario(name: string)
|
|
89
|
-
engine.clearScenarios()
|
|
90
|
-
|
|
91
|
-
// State
|
|
92
|
-
engine.state // 'idle' | 'running' | 'paused' | 'complete'
|
|
93
|
-
engine.position // SimPosition | null
|
|
94
|
-
engine.progress // 0–1
|
|
95
|
-
|
|
96
|
-
// Recording
|
|
97
|
-
engine.startRecording(sessionId?: string)
|
|
98
|
-
engine.stopRecording(): SimSession
|
|
99
|
-
|
|
100
|
-
// Cleanup
|
|
101
|
-
engine.destroy() // clears all timers — no leaks
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
**Timer internals:** `setInterval` at `1000ms / speedMultiplier`. Always cleaned up on `destroy()` and on natural completion.
|
|
105
|
-
|
|
106
|
-
**Callback safety:** All callbacks (`onPositionUpdate`, `onRouteComplete`, `onScenarioStart`, `onScenarioEnd`) are wrapped in `try/catch` — a throwing callback never crashes the engine.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
### RouteWalker
|
|
111
|
-
|
|
112
|
-
Manages geometry traversal. Extracted from the engine so it's independently testable.
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
const walker = new RouteWalker(geometry: Coordinate[], startIndex?: number);
|
|
116
|
-
|
|
117
|
-
walker.advance(steps: number): WalkerPosition // fractional steps supported
|
|
118
|
-
walker.seek(index: number): WalkerPosition
|
|
119
|
-
walker.interpolate(index: number, fraction: number): Coordinate
|
|
120
|
-
walker.bearingAt(index: number): number // forward-looking bearing
|
|
121
|
-
|
|
122
|
-
walker.totalDistanceM // total route length in metres
|
|
123
|
-
walker.totalPoints // geometry.length
|
|
124
|
-
walker.isComplete // true when at last point
|
|
125
|
-
walker.currentIndex // current floating-point index
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### NoiseEngine
|
|
131
|
-
|
|
132
|
-
GPS noise simulation using the Box-Muller transform. Zero external dependencies.
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
const engine = new NoiseEngine(profile: NoiseProfile);
|
|
136
|
-
|
|
137
|
-
engine.apply(coord: Coordinate): Coordinate // returns noisy coord (never mutates input)
|
|
138
|
-
engine.setProfile(profile: NoiseProfile) // hot-swap mid-simulation
|
|
139
|
-
|
|
140
|
-
// Static presets
|
|
141
|
-
NoiseEngine.NONE // stdDevM: 0 — exact coordinates
|
|
142
|
-
NoiseEngine.CLEAN // stdDevM: 3 — good urban GPS
|
|
143
|
-
NoiseEngine.STANDARD // stdDevM: 12 — matches the original Go4Exams ±0.00011°
|
|
144
|
-
NoiseEngine.URBAN // stdDevM: 25 — city canyon
|
|
145
|
-
NoiseEngine.DEGRADED // stdDevM: 50 — bad conditions
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Seeded mode:** Add `seed: number` to any profile for deterministic output — same seed + same route = identical noise trace (critical for regression tests).
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
const deterministicProfile = { ...NoiseEngine.STANDARD, seed: 42 };
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
### ScenarioInjector
|
|
157
|
-
|
|
158
|
-
Registers scenarios to fire at geometry indices. Called internally by `SimulatorEngine` on every tick.
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
const injector = new ScenarioInjector(onStart?, onEnd?);
|
|
162
|
-
|
|
163
|
-
injector.register(scenario: Scenario)
|
|
164
|
-
injector.remove(name: string)
|
|
165
|
-
injector.clear()
|
|
166
|
-
injector.tick(currentIndex: number): string | undefined // returns active scenario name
|
|
167
|
-
|
|
168
|
-
// Static factories
|
|
169
|
-
ScenarioInjector.gpsBlackout(atIndex, durationMs)
|
|
170
|
-
ScenarioInjector.wrongSideRoad(atIndex, offsetM)
|
|
171
|
-
ScenarioInjector.uTurn(atIndex)
|
|
172
|
-
ScenarioInjector.speedSpike(atIndex, spikeKmh, durationMs?)
|
|
173
|
-
ScenarioInjector.accuracyDrop(atIndex, newAccuracyM, durationMs)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**No double-fire:** Each scenario fires exactly once per simulation run, even if `tick()` is called with the same index multiple times.
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
### GpxParser
|
|
181
|
-
|
|
182
|
-
Zero-dependency GPX 1.1 parser. Pure string/regex — no DOMParser, no xml2js.
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
// Parse GPX string
|
|
186
|
-
const result = GpxParser.parse(gpxString: string): GpxParseResult
|
|
187
|
-
|
|
188
|
-
// Load from file (Node.js only)
|
|
189
|
-
const result = GpxParser.fromFile('/path/to/route.gpx')
|
|
190
|
-
|
|
191
|
-
// Load from base64 (React Native file picker)
|
|
192
|
-
const result = GpxParser.fromBase64(base64String)
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
interface GpxParseResult {
|
|
197
|
-
coordinates: Coordinate[]; // [lng, lat] — NavCore convention
|
|
198
|
-
timestamps: number[]; // ms since epoch per point
|
|
199
|
-
metadata: {
|
|
200
|
-
name?: string;
|
|
201
|
-
totalPoints: number;
|
|
202
|
-
durationMs: number;
|
|
203
|
-
distanceM: number;
|
|
204
|
-
avgSpeedKmh: number;
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Supports both `<trkpt>` (track points) and `<wpt>` (waypoints).
|
|
97
|
+
### 1. `SimulatorEngine`
|
|
98
|
+
The central stateful playback controller:
|
|
99
|
+
- `start()`: Begin or resume playback.
|
|
100
|
+
- `pause()`: Suspend playback at the current position.
|
|
101
|
+
- `stop()`: Halt simulation and reset to starting coordinate.
|
|
102
|
+
- `seek(geometryIndex: number)`: Jump directly to any waypoint along the route geometry.
|
|
103
|
+
- `setSpeed(multiplier: 1 | 2 | 4 | 8)`: Adjust playback speed.
|
|
104
|
+
- `addScenario(scenario)`: Schedule a fault or anomaly.
|
|
105
|
+
- `startRecording()` / `stopRecording()`: Capture recorded positions as a `SimSession`.
|
|
210
106
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
### SessionRecorder
|
|
214
|
-
|
|
215
|
-
Records and exports simulation sessions.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
const recorder = new SessionRecorder();
|
|
219
|
-
recorder.start(sessionId?: string)
|
|
220
|
-
recorder.stop(existing?: SimSession): SimSession
|
|
221
|
-
|
|
222
|
-
// Export formats
|
|
223
|
-
recorder.export('json') // full SimSession as JSON
|
|
224
|
-
recorder.export('gpx') // standard GPX 1.1 — re-importable by GpxParser
|
|
225
|
-
recorder.export('csv') // timestamp,lng,lat,bearing,speed,accuracy
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**The GPX export is critical:** a recorded session → GPX → `GpxParser` → `SimulatorEngine` creates a full regression test loop.
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
### FleetSimulator (Phase 2)
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
const fleet = FleetSimulator.spawn(5, {
|
|
236
|
-
routeGeometry: myRoute,
|
|
237
|
-
speedKmh: 50,
|
|
238
|
-
noiseProfile: NoiseEngine.STANDARD,
|
|
239
|
-
profiles: [
|
|
240
|
-
{ label: 'urban', noiseProfile: NoiseEngine.URBAN },
|
|
241
|
-
{ label: 'clean', noiseProfile: NoiseEngine.CLEAN },
|
|
242
|
-
],
|
|
243
|
-
});
|
|
107
|
+
### 2. `RouteWalker`
|
|
108
|
+
Deterministic geometric interpolation engine that moves a virtual point along a polyline at a constant physical speed (metres per second), computing exact distance and forward bearing.
|
|
244
109
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const report = fleet.report();
|
|
248
|
-
// report.comparison.avgScoreByProfile
|
|
249
|
-
// report.comparison.deviationsBySegment (heatmap data)
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
> **@experimental** — fully typed but not tested in Phase 1.
|
|
253
|
-
|
|
254
|
-
---
|
|
110
|
+
### 3. `NoiseEngine`
|
|
111
|
+
Applies Gaussian noise perturbations to latitude, longitude, and bearing based on configurable standard deviations.
|
|
255
112
|
|
|
256
|
-
|
|
113
|
+
### 4. `ScenarioInjector`
|
|
114
|
+
Factory for simulated road anomalies:
|
|
115
|
+
- `ScenarioInjector.gpsBlackout(index, durationMs)`: Emulates tunnels or signal loss by omitting position updates.
|
|
116
|
+
- `ScenarioInjector.accuracyDegradation(index, durationMs, accuracyM)`: Emulates urban canyons with high accuracy error bounds.
|
|
117
|
+
- `ScenarioInjector.speedSpike(index, durationMs, speedMultiplier)`: Simulates vehicle acceleration bursts.
|
|
118
|
+
- `ScenarioInjector.offRouteDrift(index, driftMeters, durationMs)`: Moves vehicle laterally outside the route corridor to trigger off-route FSMs.
|
|
257
119
|
|
|
258
|
-
###
|
|
120
|
+
### 5. `GpxParser`
|
|
121
|
+
Parses `.gpx` XML tracks into arrays of `[longitude, latitude]` coordinates with elevation and timestamps.
|
|
259
122
|
|
|
260
|
-
|
|
123
|
+
### 6. `SessionRecorder`
|
|
124
|
+
Records simulation output into JSON sessions for regression testing and replay.
|
|
261
125
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const simGps = useGpsSimulator(route, isNavigating, speedMultiplier);
|
|
265
|
-
|
|
266
|
-
// AFTER
|
|
267
|
-
const { position, controls, session, isRecording } = useSimulator({
|
|
268
|
-
routeGeometry: route,
|
|
269
|
-
active: isNavigating,
|
|
270
|
-
config: {
|
|
271
|
-
speedKmh: 50,
|
|
272
|
-
speedMultiplier: 1,
|
|
273
|
-
noiseProfile: NoiseEngine.STANDARD,
|
|
274
|
-
loop: false,
|
|
275
|
-
},
|
|
276
|
-
scenarios: [
|
|
277
|
-
ScenarioInjector.gpsBlackout(42, 5000),
|
|
278
|
-
ScenarioInjector.wrongSideRoad(78, 18),
|
|
279
|
-
],
|
|
280
|
-
onScenarioFired: (s) => console.log('Scenario:', s.name),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// position has the exact same shape as the old SimPosition
|
|
284
|
-
// controls: { pause, resume, seek, setSpeed }
|
|
285
|
-
// session: { start, stop } — for recording
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
**The engine is recreated only when `routeGeometry` changes** (identity comparison). Destroyed automatically on unmount.
|
|
126
|
+
### 7. `FleetSimulator`
|
|
127
|
+
Simulates multiple concurrent vehicles along independent routes for backend ingestion testing.
|
|
289
128
|
|
|
290
129
|
---
|
|
291
130
|
|
|
292
|
-
|
|
131
|
+
## React Native Integration
|
|
293
132
|
|
|
294
|
-
|
|
133
|
+
In React Native apps, render the built-in control overlay:
|
|
295
134
|
|
|
296
135
|
```tsx
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
isRecording={
|
|
306
|
-
|
|
307
|
-
|
|
136
|
+
import React from 'react';
|
|
137
|
+
import { View } from 'react-native';
|
|
138
|
+
import { useSimulator, SimulatorControls } from '@ingissa/navcore-simulator/react-native';
|
|
139
|
+
import { useNavCoreContext } from '@ingissa/navcore-react-native';
|
|
140
|
+
|
|
141
|
+
export function SimulationScreen({ routeGeometry }) {
|
|
142
|
+
const { engine: navCore } = useNavCoreContext();
|
|
143
|
+
|
|
144
|
+
const { simulator, session, isRecording, currentSpeed } = useSimulator({
|
|
145
|
+
routeGeometry,
|
|
146
|
+
speedKmh: 60,
|
|
147
|
+
onPositionUpdate: (pos) => {
|
|
148
|
+
navCore.update({
|
|
149
|
+
coord: pos.coord,
|
|
150
|
+
accuracy: pos.accuracy,
|
|
151
|
+
bearing: pos.bearing,
|
|
152
|
+
speed: pos.speed,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<View style={{ flex: 1 }}>
|
|
160
|
+
<SimulatorControls
|
|
161
|
+
simulator={simulator}
|
|
162
|
+
session={session}
|
|
163
|
+
currentSpeed={currentSpeed}
|
|
164
|
+
isRecording={isRecording}
|
|
165
|
+
/>
|
|
166
|
+
</View>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
308
169
|
```
|
|
309
170
|
|
|
310
|
-
| Style | Description |
|
|
311
|
-
|---|---|
|
|
312
|
-
| `minimal` | Horizontal bar: speed selector (1×/2×/4×/8×) + pause/resume |
|
|
313
|
-
| `full` | Card: speed, pause/resume, scenario log, session recorder |
|
|
314
|
-
| `floating` | Collapsible bottom sheet — does not obscure the map |
|
|
315
|
-
|
|
316
|
-
Uses Klaroways colour tokens (`#081525`, `#00BF76`, etc.) to match `NavCoreTestScreen`.
|
|
317
|
-
|
|
318
171
|
---
|
|
319
172
|
|
|
320
173
|
## Noise Profiles
|
|
321
174
|
|
|
322
|
-
| Profile |
|
|
323
|
-
|---|---|---|
|
|
324
|
-
| `NONE` | 0 | Exact coordinates — CI tests |
|
|
325
|
-
| `CLEAN` | 3 m | Good open-sky GPS |
|
|
326
|
-
| `STANDARD` | 12 m | Matches original Go4Exams ±0.00011° |
|
|
327
|
-
| `URBAN` | 25 m | Dense city canyon |
|
|
328
|
-
| `DEGRADED` | 50 m | Tunnel exit / bad conditions |
|
|
329
|
-
| `CUSTOM` | any | Provide `customFn(index) → [lngOff, latOff]` |
|
|
330
|
-
|
|
331
|
-
---
|
|
332
|
-
|
|
333
|
-
## Scenario Types
|
|
334
|
-
|
|
335
|
-
| Type | Factory | Effect |
|
|
175
|
+
| Profile | Standard Deviation (Metres) | Description |
|
|
336
176
|
|---|---|---|
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `ACCURACY_DROP` | `accuracyDrop(idx, m, ms)` | GPS accuracy degrades to `m` metres for `ms` |
|
|
342
|
-
| `CUSTOM` | — | Provide `params` manually |
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## GPX Round-Trip Loop
|
|
347
|
-
|
|
348
|
-
```
|
|
349
|
-
real GPS trace
|
|
350
|
-
↓
|
|
351
|
-
.gpx file
|
|
352
|
-
↓
|
|
353
|
-
GpxParser.parse()
|
|
354
|
-
↓
|
|
355
|
-
SimulatorEngine (replay with noise + scenarios)
|
|
356
|
-
↓
|
|
357
|
-
SessionRecorder.export('gpx')
|
|
358
|
-
↓
|
|
359
|
-
GpxParser.parse() ←── CI assertion: reparsed.coordinates.length === session.positions.length
|
|
360
|
-
↓
|
|
361
|
-
Next regression run
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
This loop is the foundation of the CI strategy. Every format change to `SessionRecorder._toGpx()` that breaks `GpxParser.parse()` will fail the integration test immediately.
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## CI / Regression Testing
|
|
369
|
-
|
|
370
|
-
```bash
|
|
371
|
-
cd packages/navcore-sdk
|
|
372
|
-
npx vitest run --reporter=verbose
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
Test files:
|
|
376
|
-
- `packages/simulator/src/core/__tests__/RouteWalker.test.ts`
|
|
377
|
-
- `packages/simulator/src/core/__tests__/NoiseEngine.test.ts`
|
|
378
|
-
- `packages/simulator/src/core/__tests__/SimulatorEngine.test.ts`
|
|
379
|
-
- `packages/simulator/src/scenarios/__tests__/ScenarioInjector.test.ts`
|
|
380
|
-
- `packages/simulator/src/replay/__tests__/GpxParser.test.ts`
|
|
381
|
-
- `packages/simulator/src/__tests__/integration/simulator-pipeline.test.ts`
|
|
382
|
-
|
|
383
|
-
All tests use `vi.useFakeTimers()` — no real waiting, no flakiness.
|
|
177
|
+
| `NONE` | 0 m | Zero jitter; exact mathematical coordinates on the polyline |
|
|
178
|
+
| `LOW` | 4 m | High-quality open-sky dual-frequency GNSS |
|
|
179
|
+
| `STANDARD` | 12 m | Typical smartphone GPS accuracy in suburban conditions |
|
|
180
|
+
| `URBAN_CANYON` | 35 m | Severe multipath reflection and tall building interference |
|
|
384
181
|
|
|
385
182
|
---
|
|
386
183
|
|
|
387
|
-
##
|
|
388
|
-
|
|
389
|
-
The migration is a **search-and-replace**, not a rewrite.
|
|
390
|
-
|
|
391
|
-
### 1. Remove the inline hook and local type
|
|
392
|
-
|
|
393
|
-
Delete `useGpsSimulator` function (lines 114–174 in `NavCoreTestScreen`) and the local `SimPosition` interface.
|
|
394
|
-
|
|
395
|
-
### 2. Add import
|
|
184
|
+
## CI Regression Testing
|
|
396
185
|
|
|
397
186
|
```typescript
|
|
398
|
-
import {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
187
|
+
import { describe, it, expect } from 'vitest';
|
|
188
|
+
import { NavCore } from '@ingissa/navcore-core';
|
|
189
|
+
import { SimulatorEngine, NoiseEngine } from '@ingissa/navcore-simulator/node';
|
|
190
|
+
|
|
191
|
+
describe('Navigation Regression', () => {
|
|
192
|
+
it('navigates route without spurious off-route alerts under normal noise', () => {
|
|
193
|
+
const route = [[2.3522, 48.8566], [2.3400, 48.8650], [2.3009, 48.8741]];
|
|
194
|
+
const nav = new NavCore({ isDev: true });
|
|
195
|
+
nav.setRoute(route);
|
|
196
|
+
nav.startNavigation();
|
|
197
|
+
|
|
198
|
+
let offRouteFired = false;
|
|
199
|
+
nav.on('deviation', () => { offRouteFired = true; });
|
|
200
|
+
|
|
201
|
+
const sim = new SimulatorEngine({
|
|
202
|
+
routeGeometry: route,
|
|
203
|
+
noiseProfile: NoiseEngine.LOW,
|
|
204
|
+
speedMultiplier: 8,
|
|
205
|
+
onPositionUpdate: (pos) => {
|
|
206
|
+
nav.update({
|
|
207
|
+
coord: pos.coord,
|
|
208
|
+
accuracy: pos.accuracy,
|
|
209
|
+
bearing: pos.bearing,
|
|
210
|
+
speed: pos.speed,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
sim.start();
|
|
217
|
+
// Verify engine state upon completion
|
|
218
|
+
expect(offRouteFired).toBe(false);
|
|
219
|
+
});
|
|
417
220
|
});
|
|
418
221
|
```
|
|
419
222
|
|
|
420
|
-
|
|
223
|
+
---
|
|
421
224
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
225
|
+
## Licensing
|
|
226
|
+
|
|
227
|
+
- [Simulator Guide](../../docs/simulator.md)
|
|
228
|
+
- [API Reference](../../docs/api-reference/navcore-simulator.md)
|
|
229
|
+
- [Official Website](https://navcore-web.vercel.app/)
|
|
427
230
|
|
|
428
|
-
|
|
231
|
+
Proprietary software. Copyright (c) 2026 Abdelnasser Aitabi. All rights reserved. No license is granted except by separate written agreement.
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ingissa/navcore-simulator",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Headless GPS simulator for NavCore. Works in Node.js, React Native, and CI.",
|
|
5
|
-
"author": "Ingissa <8abnav@gmail.com>",
|
|
6
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
-
"homepage": "https://navcore-web.vercel.app/",
|
|
8
|
-
"main": "dist/index.js",
|
|
9
|
-
"module": "dist/index.mjs",
|
|
10
|
-
"types": "dist/index.d.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"import": "./dist/index.mjs",
|
|
15
|
-
"require": "./dist/index.js"
|
|
16
|
-
},
|
|
17
|
-
"./node": {
|
|
18
|
-
"types": "./dist/node.d.ts",
|
|
19
|
-
"import": "./dist/node.mjs",
|
|
20
|
-
"require": "./dist/node.js"
|
|
21
|
-
},
|
|
22
|
-
"./react-native": {
|
|
23
|
-
"types": "./dist/react-native.d.ts",
|
|
24
|
-
"import": "./dist/react-native.mjs",
|
|
25
|
-
"require": "./dist/react-native.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"dist",
|
|
30
|
-
"LICENSE",
|
|
31
|
-
"README.md"
|
|
32
|
-
],
|
|
33
|
-
"sideEffects": false,
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public"
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=16"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsup",
|
|
42
|
-
"lint": "tsc --noEmit",
|
|
43
|
-
"prepublishOnly": "npm run build --prefix ../.. && node ../../scripts/check-release.mjs"
|
|
44
|
-
},
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@ingissa/navcore-core": "^1.0.
|
|
47
|
-
"react": ">=18",
|
|
48
|
-
"react-native": ">=0.72"
|
|
49
|
-
},
|
|
50
|
-
"peerDependenciesMeta": {
|
|
51
|
-
"react": {
|
|
52
|
-
"optional": true
|
|
53
|
-
},
|
|
54
|
-
"react-native": {
|
|
55
|
-
"optional": true
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@types/node": "^25.8.0"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingissa/navcore-simulator",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "Headless GPS simulator for NavCore. Works in Node.js, React Native, and CI.",
|
|
5
|
+
"author": "Ingissa <8abnav@gmail.com>",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"homepage": "https://navcore-web.vercel.app/",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.mjs",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./node": {
|
|
18
|
+
"types": "./dist/node.d.ts",
|
|
19
|
+
"import": "./dist/node.mjs",
|
|
20
|
+
"require": "./dist/node.js"
|
|
21
|
+
},
|
|
22
|
+
"./react-native": {
|
|
23
|
+
"types": "./dist/react-native.d.ts",
|
|
24
|
+
"import": "./dist/react-native.mjs",
|
|
25
|
+
"require": "./dist/react-native.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"lint": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "npm run build --prefix ../.. && node ../../scripts/check-release.mjs"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@ingissa/navcore-core": "^1.0.10",
|
|
47
|
+
"react": ">=18",
|
|
48
|
+
"react-native": ">=0.72"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"react-native": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.8.0"
|
|
60
|
+
}
|
|
61
|
+
}
|