@ingissa/navcore-simulator 1.0.3 → 1.0.4
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/README.md +18 -10
- package/dist/index-i9TY71RY.d.mts +494 -0
- package/dist/index-i9TY71RY.d.ts +494 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -28
- package/dist/index.js +937 -29
- package/dist/index.mjs +929 -0
- package/dist/node.d.mts +1 -0
- package/dist/node.d.ts +1 -26
- package/dist/node.js +937 -25
- package/dist/node.mjs +929 -0
- package/dist/react-native.d.mts +82 -0
- package/dist/react-native.d.ts +82 -0
- package/dist/react-native.js +1370 -0
- package/dist/react-native.mjs +1360 -0
- package/package.json +15 -5
- package/dist/core/NoiseEngine.d.ts +0 -46
- package/dist/core/NoiseEngine.d.ts.map +0 -1
- package/dist/core/NoiseEngine.js +0 -106
- package/dist/core/RouteWalker.d.ts +0 -75
- package/dist/core/RouteWalker.d.ts.map +0 -1
- package/dist/core/RouteWalker.js +0 -177
- package/dist/core/SimulatorEngine.d.ts +0 -53
- package/dist/core/SimulatorEngine.d.ts.map +0 -1
- package/dist/core/SimulatorEngine.js +0 -283
- package/dist/core/types.d.ts +0 -129
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -7
- package/dist/fleet/FleetSimulator.d.ts +0 -60
- package/dist/fleet/FleetSimulator.d.ts.map +0 -1
- package/dist/fleet/FleetSimulator.js +0 -114
- package/dist/index.d.ts.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/react-native/SimulatorControls.d.ts +0 -35
- package/dist/react-native/SimulatorControls.d.ts.map +0 -1
- package/dist/react-native/SimulatorControls.js +0 -254
- package/dist/react-native/useSimulator.d.ts +0 -55
- package/dist/react-native/useSimulator.d.ts.map +0 -1
- package/dist/react-native/useSimulator.js +0 -97
- package/dist/replay/GpxParser.d.ts +0 -40
- package/dist/replay/GpxParser.d.ts.map +0 -1
- package/dist/replay/GpxParser.js +0 -95
- package/dist/replay/SessionRecorder.d.ts +0 -37
- package/dist/replay/SessionRecorder.d.ts.map +0 -1
- package/dist/replay/SessionRecorder.js +0 -120
- package/dist/scenarios/ScenarioInjector.d.ts +0 -55
- package/dist/scenarios/ScenarioInjector.d.ts.map +0 -1
- package/dist/scenarios/ScenarioInjector.js +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingissa/navcore-simulator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Headless GPS simulator for NavCore. Works in Node.js, React Native, and CI.",
|
|
5
5
|
"author": "Ingissa <8abnav@gmail.com>",
|
|
6
6
|
"license": "Commercial",
|
|
@@ -17,9 +17,19 @@
|
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
20
21
|
"import": "./dist/index.mjs",
|
|
21
|
-
"require": "./dist/index.js"
|
|
22
|
-
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./node": {
|
|
25
|
+
"types": "./dist/node.d.ts",
|
|
26
|
+
"import": "./dist/node.mjs",
|
|
27
|
+
"require": "./dist/node.js"
|
|
28
|
+
},
|
|
29
|
+
"./react-native": {
|
|
30
|
+
"types": "./dist/react-native.d.ts",
|
|
31
|
+
"import": "./dist/react-native.mjs",
|
|
32
|
+
"require": "./dist/react-native.js"
|
|
23
33
|
}
|
|
24
34
|
},
|
|
25
35
|
"files": ["dist"],
|
|
@@ -29,12 +39,12 @@
|
|
|
29
39
|
},
|
|
30
40
|
"engines": { "node": ">=18" },
|
|
31
41
|
"scripts": {
|
|
32
|
-
"build": "
|
|
42
|
+
"build": "tsup",
|
|
33
43
|
"lint": "tsc --noEmit",
|
|
34
44
|
"prepublishOnly": "npm run build"
|
|
35
45
|
},
|
|
36
46
|
"peerDependencies": {
|
|
37
|
-
"@ingissa/navcore-core": "^1.0.
|
|
47
|
+
"@ingissa/navcore-core": "^1.0.4",
|
|
38
48
|
"react": ">=18",
|
|
39
49
|
"react-native": ">=0.72"
|
|
40
50
|
},
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NoiseEngine — GPS noise simulation.
|
|
3
|
-
*
|
|
4
|
-
* Formalises the ±0.00011° noise from the original useGpsSimulator
|
|
5
|
-
* into a testable, seeded, swappable noise model.
|
|
6
|
-
*
|
|
7
|
-
* Box-Muller transform for Gaussian sampling — ZERO external dependencies.
|
|
8
|
-
*
|
|
9
|
-
* Degrees-per-metre constants:
|
|
10
|
-
* 1° latitude ≈ 110540 m
|
|
11
|
-
* 1° longitude ≈ 111320 m (at equator; good enough for noise simulation)
|
|
12
|
-
*/
|
|
13
|
-
import type { Coordinate } from './types';
|
|
14
|
-
import type { NoiseProfile } from './types';
|
|
15
|
-
export declare class NoiseEngine {
|
|
16
|
-
private profile;
|
|
17
|
-
private rng;
|
|
18
|
-
private _tickIndex;
|
|
19
|
-
constructor(profile: NoiseProfile);
|
|
20
|
-
/** No noise — returns the exact coordinate */
|
|
21
|
-
static readonly NONE: NoiseProfile;
|
|
22
|
-
/** Good urban GPS — ~3m standard deviation */
|
|
23
|
-
static readonly CLEAN: NoiseProfile;
|
|
24
|
-
/** Standard Go4Exams value — ~12m standard deviation (matches original ±0.00011°) */
|
|
25
|
-
static readonly STANDARD: NoiseProfile;
|
|
26
|
-
/** Dense city canyon — ~25m standard deviation */
|
|
27
|
-
static readonly URBAN: NoiseProfile;
|
|
28
|
-
/** Bad conditions — ~50m standard deviation */
|
|
29
|
-
static readonly DEGRADED: NoiseProfile;
|
|
30
|
-
/**
|
|
31
|
-
* Apply noise to a coordinate.
|
|
32
|
-
* Returns a new coordinate — never mutates the input.
|
|
33
|
-
*/
|
|
34
|
-
apply(coord: Coordinate): Coordinate;
|
|
35
|
-
/** Hot-swap the noise profile mid-simulation */
|
|
36
|
-
setProfile(profile: NoiseProfile): void;
|
|
37
|
-
get currentProfile(): NoiseProfile;
|
|
38
|
-
/**
|
|
39
|
-
* Box-Muller transform: produces Gaussian N(0, stdDev²) offset.
|
|
40
|
-
* Returns [lngOffset, latOffset] in degrees.
|
|
41
|
-
*/
|
|
42
|
-
private _gaussianOffset;
|
|
43
|
-
/** Returns a float in [0, 1) — uses seeded RNG if available, else Math.random */
|
|
44
|
-
private _rand;
|
|
45
|
-
}
|
|
46
|
-
//# sourceMappingURL=NoiseEngine.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"NoiseEngine.d.ts","sourceRoot":"","sources":["../../src/core/NoiseEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyB5C,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,EAAE,YAAY;IAOjC,8CAA8C;IAC9C,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAgC;IAElE,8CAA8C;IAC9C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAoC;IAEvE,qFAAqF;IACrF,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAqC;IAE3E,kDAAkD;IAClD,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAkC;IAErE,+CAA+C;IAC/C,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAqC;IAI3E;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;IA6BpC,gDAAgD;IAChD,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAMvC,IAAI,cAAc,IAAI,YAAY,CAEjC;IAID;;;OAGG;IACH,OAAO,CAAC,eAAe;IAcvB,iFAAiF;IACjF,OAAO,CAAC,KAAK;CAGd"}
|
package/dist/core/NoiseEngine.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NoiseEngine — GPS noise simulation.
|
|
3
|
-
*
|
|
4
|
-
* Formalises the ±0.00011° noise from the original useGpsSimulator
|
|
5
|
-
* into a testable, seeded, swappable noise model.
|
|
6
|
-
*
|
|
7
|
-
* Box-Muller transform for Gaussian sampling — ZERO external dependencies.
|
|
8
|
-
*
|
|
9
|
-
* Degrees-per-metre constants:
|
|
10
|
-
* 1° latitude ≈ 110540 m
|
|
11
|
-
* 1° longitude ≈ 111320 m (at equator; good enough for noise simulation)
|
|
12
|
-
*/
|
|
13
|
-
// Conversion factors
|
|
14
|
-
const LAT_DEG_PER_M = 1 / 110540;
|
|
15
|
-
const LNG_DEG_PER_M = 1 / 111320;
|
|
16
|
-
/**
|
|
17
|
-
* Simple seeded LCG (Linear Congruential Generator).
|
|
18
|
-
* Same seed guarantees identical noise sequences — critical for CI regression.
|
|
19
|
-
*/
|
|
20
|
-
class SeededRandom {
|
|
21
|
-
constructor(seed) {
|
|
22
|
-
this.state = seed >>> 0; // ensure unsigned 32-bit
|
|
23
|
-
}
|
|
24
|
-
/** Returns a float in [0, 1) */
|
|
25
|
-
next() {
|
|
26
|
-
// LCG parameters from Numerical Recipes
|
|
27
|
-
this.state = (Math.imul(1664525, this.state) + 1013904223) >>> 0;
|
|
28
|
-
return this.state / 0x100000000;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export class NoiseEngine {
|
|
32
|
-
constructor(profile) {
|
|
33
|
-
this._tickIndex = 0;
|
|
34
|
-
this.profile = profile;
|
|
35
|
-
this.rng = profile.seed !== undefined ? new SeededRandom(profile.seed) : null;
|
|
36
|
-
}
|
|
37
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
38
|
-
/**
|
|
39
|
-
* Apply noise to a coordinate.
|
|
40
|
-
* Returns a new coordinate — never mutates the input.
|
|
41
|
-
*/
|
|
42
|
-
apply(coord) {
|
|
43
|
-
const idx = this._tickIndex++;
|
|
44
|
-
switch (this.profile.type) {
|
|
45
|
-
case 'NONE':
|
|
46
|
-
return coord;
|
|
47
|
-
case 'GAUSSIAN':
|
|
48
|
-
case 'URBAN': {
|
|
49
|
-
const [dLng, dLat] = this._gaussianOffset(this.profile.stdDevM);
|
|
50
|
-
return [coord[0] + dLng, coord[1] + dLat];
|
|
51
|
-
}
|
|
52
|
-
case 'TUNNEL':
|
|
53
|
-
// Signal loss: return a fixed position (last known would be handled by engine)
|
|
54
|
-
// Here we just return the exact coord (engine handles blackout via scenario)
|
|
55
|
-
return coord;
|
|
56
|
-
case 'CUSTOM': {
|
|
57
|
-
if (!this.profile.customFn)
|
|
58
|
-
return coord;
|
|
59
|
-
const [lngOff, latOff] = this.profile.customFn(idx);
|
|
60
|
-
return [coord[0] + lngOff, coord[1] + latOff];
|
|
61
|
-
}
|
|
62
|
-
default:
|
|
63
|
-
return coord;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Hot-swap the noise profile mid-simulation */
|
|
67
|
-
setProfile(profile) {
|
|
68
|
-
this.profile = profile;
|
|
69
|
-
this.rng = profile.seed !== undefined ? new SeededRandom(profile.seed) : null;
|
|
70
|
-
this._tickIndex = 0;
|
|
71
|
-
}
|
|
72
|
-
get currentProfile() {
|
|
73
|
-
return this.profile;
|
|
74
|
-
}
|
|
75
|
-
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
76
|
-
/**
|
|
77
|
-
* Box-Muller transform: produces Gaussian N(0, stdDev²) offset.
|
|
78
|
-
* Returns [lngOffset, latOffset] in degrees.
|
|
79
|
-
*/
|
|
80
|
-
_gaussianOffset(stdDevM) {
|
|
81
|
-
if (stdDevM === 0)
|
|
82
|
-
return [0, 0];
|
|
83
|
-
const u1 = this._rand();
|
|
84
|
-
const u2 = this._rand();
|
|
85
|
-
// Box-Muller
|
|
86
|
-
const mag = stdDevM * Math.sqrt(-2 * Math.log(Math.max(u1, 1e-10)));
|
|
87
|
-
const z0 = mag * Math.cos(2 * Math.PI * u2);
|
|
88
|
-
const z1 = mag * Math.sin(2 * Math.PI * u2);
|
|
89
|
-
return [z0 * LNG_DEG_PER_M, z1 * LAT_DEG_PER_M];
|
|
90
|
-
}
|
|
91
|
-
/** Returns a float in [0, 1) — uses seeded RNG if available, else Math.random */
|
|
92
|
-
_rand() {
|
|
93
|
-
return this.rng ? this.rng.next() : Math.random();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// ── Static presets ─────────────────────────────────────────────────────────
|
|
97
|
-
/** No noise — returns the exact coordinate */
|
|
98
|
-
NoiseEngine.NONE = { type: 'NONE', stdDevM: 0 };
|
|
99
|
-
/** Good urban GPS — ~3m standard deviation */
|
|
100
|
-
NoiseEngine.CLEAN = { type: 'GAUSSIAN', stdDevM: 3 };
|
|
101
|
-
/** Standard Go4Exams value — ~12m standard deviation (matches original ±0.00011°) */
|
|
102
|
-
NoiseEngine.STANDARD = { type: 'GAUSSIAN', stdDevM: 12 };
|
|
103
|
-
/** Dense city canyon — ~25m standard deviation */
|
|
104
|
-
NoiseEngine.URBAN = { type: 'URBAN', stdDevM: 25 };
|
|
105
|
-
/** Bad conditions — ~50m standard deviation */
|
|
106
|
-
NoiseEngine.DEGRADED = { type: 'GAUSSIAN', stdDevM: 50 };
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RouteWalker — Manages traversal of a route geometry array.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from the inline useGpsSimulator hook so the traversal logic
|
|
5
|
-
* is testable independently of React, timers, or noise.
|
|
6
|
-
*
|
|
7
|
-
* ZERO dependencies beyond @navcore/core types.
|
|
8
|
-
*/
|
|
9
|
-
import type { Coordinate } from './types';
|
|
10
|
-
export interface WalkerPosition {
|
|
11
|
-
/** Current floating-point index (e.g. 3.75 = between points 3 and 4) */
|
|
12
|
-
index: number;
|
|
13
|
-
/** Interpolated coordinate at the current index */
|
|
14
|
-
coord: Coordinate;
|
|
15
|
-
/** Forward-looking bearing at the current segment */
|
|
16
|
-
bearing: number;
|
|
17
|
-
/** Total metres travelled from route start */
|
|
18
|
-
distanceTravelled: number;
|
|
19
|
-
/** True if the walker has reached or passed the last point */
|
|
20
|
-
isComplete: boolean;
|
|
21
|
-
}
|
|
22
|
-
export declare class RouteWalker {
|
|
23
|
-
private readonly geometry;
|
|
24
|
-
private _index;
|
|
25
|
-
private _distanceTravelled;
|
|
26
|
-
private readonly _cumulativeDistances;
|
|
27
|
-
private _isComplete;
|
|
28
|
-
constructor(geometry: Coordinate[], startIndex?: number);
|
|
29
|
-
/**
|
|
30
|
-
* Advance the walker by `steps` geometry points.
|
|
31
|
-
* Fractional steps are supported for sub-second precision.
|
|
32
|
-
*/
|
|
33
|
-
advance(steps: number): WalkerPosition;
|
|
34
|
-
/**
|
|
35
|
-
* Advance the walker by a physical distance in metres.
|
|
36
|
-
* This is the preferred way for smooth constant-speed simulation.
|
|
37
|
-
*/
|
|
38
|
-
advanceByDistance(meters: number): WalkerPosition;
|
|
39
|
-
private _finalizeAdvance;
|
|
40
|
-
/**
|
|
41
|
-
* Seek directly to a geometry index (integer or fractional).
|
|
42
|
-
* Recalculates bearing from the new position.
|
|
43
|
-
*/
|
|
44
|
-
seek(index: number): WalkerPosition;
|
|
45
|
-
/**
|
|
46
|
-
* Get an interpolated coordinate between geometry[index] and geometry[index+1].
|
|
47
|
-
* @param index - integer index
|
|
48
|
-
* @param fraction - 0..1 interpolation factor
|
|
49
|
-
*/
|
|
50
|
-
interpolate(index: number, fraction: number): Coordinate;
|
|
51
|
-
/**
|
|
52
|
-
* Compute the forward-looking bearing at a given integer index.
|
|
53
|
-
* At the last point, returns the bearing of the final segment.
|
|
54
|
-
*/
|
|
55
|
-
bearingAt(index: number): number;
|
|
56
|
-
/**
|
|
57
|
-
* Total haversine distance from the route start to the given index.
|
|
58
|
-
* Uses the pre-computed cumulative distances (O(1)).
|
|
59
|
-
*/
|
|
60
|
-
distanceAtIndex(index: number): number;
|
|
61
|
-
distanceToIndex(index: number): number;
|
|
62
|
-
/**
|
|
63
|
-
* Convert a distance in metres back to a fractional index.
|
|
64
|
-
* Uses binary search for O(log N) performance.
|
|
65
|
-
*/
|
|
66
|
-
private _distanceToIndex;
|
|
67
|
-
get totalDistanceM(): number;
|
|
68
|
-
get totalPoints(): number;
|
|
69
|
-
get isComplete(): boolean;
|
|
70
|
-
get currentIndex(): number;
|
|
71
|
-
private _currentPosition;
|
|
72
|
-
/** Interpolate cumulative distance for fractional index */
|
|
73
|
-
private _interpolatedDistance;
|
|
74
|
-
}
|
|
75
|
-
//# sourceMappingURL=RouteWalker.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RouteWalker.d.ts","sourceRoot":"","sources":["../../src/core/RouteWalker.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,EAAE,UAAU,CAAC;IAClB,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAW;IAChD,OAAO,CAAC,WAAW,CAAU;gBAEjB,QAAQ,EAAE,UAAU,EAAE,EAAE,UAAU,SAAI;IAuBlD;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAMtC;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAejD,OAAO,CAAC,gBAAgB;IAUxB;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAOnC;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU;IAQxD;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAOhC;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAKtC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAItC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgCxB,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAID,OAAO,CAAC,gBAAgB;IAexB,2DAA2D;IAC3D,OAAO,CAAC,qBAAqB;CAQ9B"}
|
package/dist/core/RouteWalker.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RouteWalker — Manages traversal of a route geometry array.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from the inline useGpsSimulator hook so the traversal logic
|
|
5
|
-
* is testable independently of React, timers, or noise.
|
|
6
|
-
*
|
|
7
|
-
* ZERO dependencies beyond @navcore/core types.
|
|
8
|
-
*/
|
|
9
|
-
import { getBearingBetweenPoints, haversineDistance, } from '@ingissa/navcore-core';
|
|
10
|
-
export class RouteWalker {
|
|
11
|
-
constructor(geometry, startIndex = 0) {
|
|
12
|
-
if (geometry.length < 2) {
|
|
13
|
-
throw new Error('RouteWalker requires at least 2 geometry points');
|
|
14
|
-
}
|
|
15
|
-
this.geometry = geometry;
|
|
16
|
-
this._index = Math.max(0, Math.min(startIndex, geometry.length - 1));
|
|
17
|
-
this._isComplete = false;
|
|
18
|
-
// Pre-compute cumulative distances for O(1) distanceToIndex()
|
|
19
|
-
this._cumulativeDistances = new Array(geometry.length).fill(0);
|
|
20
|
-
for (let i = 1; i < geometry.length; i++) {
|
|
21
|
-
const a = geometry[i - 1];
|
|
22
|
-
const b = geometry[i];
|
|
23
|
-
this._cumulativeDistances[i] =
|
|
24
|
-
(this._cumulativeDistances[i - 1] ?? 0) +
|
|
25
|
-
haversineDistance(a[1], a[0], b[1], b[0]);
|
|
26
|
-
}
|
|
27
|
-
this._distanceTravelled = this._cumulativeDistances[this._index] ?? 0;
|
|
28
|
-
}
|
|
29
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
30
|
-
/**
|
|
31
|
-
* Advance the walker by `steps` geometry points.
|
|
32
|
-
* Fractional steps are supported for sub-second precision.
|
|
33
|
-
*/
|
|
34
|
-
advance(steps) {
|
|
35
|
-
if (this._isComplete)
|
|
36
|
-
return this._currentPosition();
|
|
37
|
-
this._index += steps;
|
|
38
|
-
return this._finalizeAdvance();
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Advance the walker by a physical distance in metres.
|
|
42
|
-
* This is the preferred way for smooth constant-speed simulation.
|
|
43
|
-
*/
|
|
44
|
-
advanceByDistance(meters) {
|
|
45
|
-
if (this._isComplete)
|
|
46
|
-
return this._currentPosition();
|
|
47
|
-
this._distanceTravelled += meters;
|
|
48
|
-
if (this._distanceTravelled >= this.totalDistanceM) {
|
|
49
|
-
this._distanceTravelled = this.totalDistanceM;
|
|
50
|
-
this._index = this.geometry.length - 1;
|
|
51
|
-
this._isComplete = true;
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
this._index = this._distanceToIndex(this._distanceTravelled);
|
|
55
|
-
}
|
|
56
|
-
return this._currentPosition();
|
|
57
|
-
}
|
|
58
|
-
_finalizeAdvance() {
|
|
59
|
-
if (this._index >= this.geometry.length - 1) {
|
|
60
|
-
this._index = this.geometry.length - 1;
|
|
61
|
-
this._isComplete = true;
|
|
62
|
-
}
|
|
63
|
-
this._distanceTravelled = this._interpolatedDistance(this._index);
|
|
64
|
-
return this._currentPosition();
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Seek directly to a geometry index (integer or fractional).
|
|
68
|
-
* Recalculates bearing from the new position.
|
|
69
|
-
*/
|
|
70
|
-
seek(index) {
|
|
71
|
-
this._index = Math.max(0, Math.min(index, this.geometry.length - 1));
|
|
72
|
-
this._isComplete = this._index >= this.geometry.length - 1;
|
|
73
|
-
this._distanceTravelled = this._interpolatedDistance(this._index);
|
|
74
|
-
return this._currentPosition();
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Get an interpolated coordinate between geometry[index] and geometry[index+1].
|
|
78
|
-
* @param index - integer index
|
|
79
|
-
* @param fraction - 0..1 interpolation factor
|
|
80
|
-
*/
|
|
81
|
-
interpolate(index, fraction) {
|
|
82
|
-
const clampedIdx = Math.max(0, Math.min(index, this.geometry.length - 2));
|
|
83
|
-
const a = this.geometry[clampedIdx];
|
|
84
|
-
const b = this.geometry[clampedIdx + 1];
|
|
85
|
-
const f = Math.max(0, Math.min(1, fraction));
|
|
86
|
-
return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Compute the forward-looking bearing at a given integer index.
|
|
90
|
-
* At the last point, returns the bearing of the final segment.
|
|
91
|
-
*/
|
|
92
|
-
bearingAt(index) {
|
|
93
|
-
const clampedIdx = Math.max(0, Math.min(Math.floor(index), this.geometry.length - 2));
|
|
94
|
-
const a = this.geometry[clampedIdx];
|
|
95
|
-
const b = this.geometry[clampedIdx + 1];
|
|
96
|
-
return getBearingBetweenPoints(a, b);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Total haversine distance from the route start to the given index.
|
|
100
|
-
* Uses the pre-computed cumulative distances (O(1)).
|
|
101
|
-
*/
|
|
102
|
-
distanceAtIndex(index) {
|
|
103
|
-
const clampedIdx = Math.max(0, Math.min(Math.floor(index), this.geometry.length - 1));
|
|
104
|
-
return this._cumulativeDistances[clampedIdx] ?? 0;
|
|
105
|
-
}
|
|
106
|
-
distanceToIndex(index) {
|
|
107
|
-
return this.distanceAtIndex(index);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Convert a distance in metres back to a fractional index.
|
|
111
|
-
* Uses binary search for O(log N) performance.
|
|
112
|
-
*/
|
|
113
|
-
_distanceToIndex(targetDistance) {
|
|
114
|
-
if (targetDistance <= 0)
|
|
115
|
-
return 0;
|
|
116
|
-
if (targetDistance >= this.totalDistanceM)
|
|
117
|
-
return this.geometry.length - 1;
|
|
118
|
-
// Binary search for the segment [i, i+1] containing targetDistance
|
|
119
|
-
let low = 0;
|
|
120
|
-
let high = this._cumulativeDistances.length - 1;
|
|
121
|
-
let i = 0;
|
|
122
|
-
while (low <= high) {
|
|
123
|
-
const mid = Math.floor((low + high) / 2);
|
|
124
|
-
if (this._cumulativeDistances[mid] <= targetDistance) {
|
|
125
|
-
i = mid;
|
|
126
|
-
low = mid + 1;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
high = mid - 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Now targetDistance is between i and i+1
|
|
133
|
-
const dStart = this._cumulativeDistances[i];
|
|
134
|
-
const dEnd = this._cumulativeDistances[i + 1];
|
|
135
|
-
const segmentDist = dEnd - dStart;
|
|
136
|
-
if (segmentDist <= 0)
|
|
137
|
-
return i;
|
|
138
|
-
const fraction = (targetDistance - dStart) / segmentDist;
|
|
139
|
-
return i + fraction;
|
|
140
|
-
}
|
|
141
|
-
// ── Readonly state ──────────────────────────────────────────────────────────
|
|
142
|
-
get totalDistanceM() {
|
|
143
|
-
return this._cumulativeDistances[this.geometry.length - 1] ?? 0;
|
|
144
|
-
}
|
|
145
|
-
get totalPoints() {
|
|
146
|
-
return this.geometry.length;
|
|
147
|
-
}
|
|
148
|
-
get isComplete() {
|
|
149
|
-
return this._isComplete;
|
|
150
|
-
}
|
|
151
|
-
get currentIndex() {
|
|
152
|
-
return this._index;
|
|
153
|
-
}
|
|
154
|
-
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
155
|
-
_currentPosition() {
|
|
156
|
-
const floorIdx = Math.min(Math.floor(this._index), this.geometry.length - 2);
|
|
157
|
-
const fraction = this._index - floorIdx;
|
|
158
|
-
const coord = this.interpolate(floorIdx, fraction);
|
|
159
|
-
const bearing = this.bearingAt(floorIdx);
|
|
160
|
-
return {
|
|
161
|
-
index: this._index,
|
|
162
|
-
coord,
|
|
163
|
-
bearing,
|
|
164
|
-
distanceTravelled: this._distanceTravelled,
|
|
165
|
-
isComplete: this._isComplete,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
/** Interpolate cumulative distance for fractional index */
|
|
169
|
-
_interpolatedDistance(index) {
|
|
170
|
-
const floor = Math.min(Math.floor(index), this.geometry.length - 1);
|
|
171
|
-
const ceil = Math.min(floor + 1, this.geometry.length - 1);
|
|
172
|
-
const fraction = index - floor;
|
|
173
|
-
const dFloor = this._cumulativeDistances[floor] ?? 0;
|
|
174
|
-
const dCeil = this._cumulativeDistances[ceil] ?? 0;
|
|
175
|
-
return dFloor + (dCeil - dFloor) * fraction;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -1,53 +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
|
-
import type { SimPosition, SimulatorConfig, SimSession, Scenario } from './types';
|
|
10
|
-
export type EngineState = 'idle' | 'running' | 'paused' | 'complete';
|
|
11
|
-
export declare class SimulatorEngine {
|
|
12
|
-
private readonly config;
|
|
13
|
-
private walker;
|
|
14
|
-
private noise;
|
|
15
|
-
private injector;
|
|
16
|
-
private _state;
|
|
17
|
-
private _position;
|
|
18
|
-
private _speedMultiplier;
|
|
19
|
-
private _timer;
|
|
20
|
-
private _lastTick;
|
|
21
|
-
private static readonly TICK_MS;
|
|
22
|
-
private _recording;
|
|
23
|
-
private _recordedPositions;
|
|
24
|
-
private _recordedScenarios;
|
|
25
|
-
private _sessionId;
|
|
26
|
-
private _sessionStart;
|
|
27
|
-
private _noiseSamplesAdded;
|
|
28
|
-
constructor(config: SimulatorConfig);
|
|
29
|
-
start(): void;
|
|
30
|
-
stop(): void;
|
|
31
|
-
pause(): void;
|
|
32
|
-
resume(): void;
|
|
33
|
-
/**
|
|
34
|
-
* Jump to any geometry index.
|
|
35
|
-
* Bearing is recalculated from the new segment — seek() is always bearing-safe.
|
|
36
|
-
*/
|
|
37
|
-
seek(geometryIndex: number): void;
|
|
38
|
-
setSpeed(multiplier: 1 | 2 | 4 | 8): void;
|
|
39
|
-
addScenario(scenario: Scenario): void;
|
|
40
|
-
removeScenario(name: string): void;
|
|
41
|
-
clearScenarios(): void;
|
|
42
|
-
get state(): EngineState;
|
|
43
|
-
get position(): SimPosition | null;
|
|
44
|
-
get progress(): number;
|
|
45
|
-
startRecording(sessionId?: string): void;
|
|
46
|
-
stopRecording(): SimSession;
|
|
47
|
-
destroy(): void;
|
|
48
|
-
private _startTimer;
|
|
49
|
-
private _stopTimer;
|
|
50
|
-
private _tick;
|
|
51
|
-
private _buildAndEmitPosition;
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=SimulatorEngine.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SimulatorEngine.d.ts","sourceRoot":"","sources":["../../src/core/SimulatorEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAWlF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;AAErE,qBAAa,eAAe;IAE1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAEiF;IAGxG,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,QAAQ,CAAmB;IAGnC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,MAAM,CAA+C;IAC7D,OAAO,CAAC,SAAS,CAAa;IAE9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAM;IAGrC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,kBAAkB,CAAsD;IAChF,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,kBAAkB,CAAK;gBAEnB,MAAM,EAAE,eAAe;IA4CnC,KAAK,IAAI,IAAI;IAOb,IAAI,IAAI,IAAI;IAMZ,KAAK,IAAI,IAAI;IAMb,MAAM,IAAI,IAAI;IAMd;;;OAGG;IACH,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;IAkBjC,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI;IAOzC,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAIrC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIlC,cAAc,IAAI,IAAI;IAMtB,IAAI,KAAK,IAAI,WAAW,CAEvB;IAED,IAAI,QAAQ,IAAI,WAAW,GAAG,IAAI,CAEjC;IAED,IAAI,QAAQ,IAAI,MAAM,CAGrB;IAID,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IASxC,aAAa,IAAI,UAAU;IAqC3B,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,KAAK;IA+Db,OAAO,CAAC,qBAAqB;CAqC9B"}
|