@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,155 +0,0 @@
1
- /**
2
- * useSimulator — React Native hook wrapping SimulatorEngine.
3
- *
4
- * Drop-in replacement for useGpsSimulator.
5
- * All engine logic lives in SimulatorEngine — this hook is purely a thin
6
- * React adapter with no business logic of its own.
7
- *
8
- * BEFORE (current Go4Exams code):
9
- * const simGps = useGpsSimulator(route, isNavigating, speedMultiplier)
10
- *
11
- * AFTER:
12
- * const { position, controls, session, isRecording } = useSimulator({
13
- * routeGeometry: route,
14
- * active: isNavigating,
15
- * config: { speedKmh: 50, noiseProfile: NoiseEngine.STANDARD },
16
- * })
17
- */
18
-
19
- import { useEffect, useRef, useState, useCallback } from 'react';
20
- import { SimulatorEngine } from '../core/SimulatorEngine';
21
- import { NoiseEngine } from '../core/NoiseEngine';
22
- import type { SimPosition, SimulatorConfig, SimSession, Scenario, SimLocationObject } from '../core/types';
23
- import type { Coordinate } from '@ingissa/navcore-core';
24
-
25
- export interface UseSimulatorOptions {
26
- /** Route geometry to simulate along */
27
- routeGeometry: Coordinate[] | null;
28
- /** Whether the simulation should be active */
29
- active: boolean;
30
- /** Engine configuration (all fields optional) */
31
- config?: Partial<Omit<SimulatorConfig, 'routeGeometry' | 'onPositionUpdate' | 'onRouteComplete'>>;
32
- /** Pre-registered scenarios */
33
- scenarios?: Scenario[];
34
- /** Called when a scenario fires */
35
- onScenarioFired?: (scenario: Scenario) => void;
36
- /** Called when the route simulation finishes */
37
- onRouteComplete?: () => void;
38
- }
39
-
40
- export interface SimulatorControls {
41
- pause: () => void;
42
- resume: () => void;
43
- seek: (geometryIndex: number) => void;
44
- setSpeed: (multiplier: 1 | 2 | 4 | 8) => void;
45
- }
46
-
47
- export interface SessionControls {
48
- start: (sessionId?: string) => void;
49
- stop: () => SimSession | null;
50
- }
51
-
52
- export interface UseSimulatorResult {
53
- /** Current simulated position — null when inactive */
54
- position: SimPosition | null;
55
- /** Conforms to Expo LocationObject (null when inactive) */
56
- location: SimLocationObject | null;
57
- /** Alias for location conforming to Expo LocationObject */
58
- rawLocation: SimLocationObject | null;
59
- /** Playback controls */
60
- controls: SimulatorControls;
61
- /** Recording controls */
62
- session: SessionControls;
63
- /** Whether a recording is in progress */
64
- isRecording: boolean;
65
- }
66
-
67
- export function useSimulator(options: UseSimulatorOptions): UseSimulatorResult {
68
- const { routeGeometry, active, config = {}, scenarios = [], onScenarioFired } = options;
69
-
70
- const [position, setPosition] = useState<SimPosition | null>(null);
71
- const [isRecording, setIsRecording] = useState(false);
72
- const engineRef = useRef<SimulatorEngine | null>(null);
73
- // Keep a stable ref to the latest onScenarioFired callback
74
- const onScenarioFiredRef = useRef(onScenarioFired);
75
- onScenarioFiredRef.current = onScenarioFired;
76
-
77
- // Create / recreate the engine whenever the route geometry changes
78
- useEffect(() => {
79
- if (!routeGeometry || routeGeometry.length < 2) {
80
- setPosition(null);
81
- return;
82
- }
83
-
84
- const engine = new SimulatorEngine({
85
- routeGeometry,
86
- speedKmh: config.speedKmh ?? 50,
87
- speedMultiplier: config.speedMultiplier ?? 1,
88
- noiseProfile: config.noiseProfile ?? NoiseEngine.STANDARD,
89
- loop: config.loop ?? false,
90
- startIndex: config.startIndex ?? 0,
91
- onPositionUpdate: (pos) => setPosition(pos),
92
- onRouteComplete: () => {
93
- if (options.onRouteComplete) {
94
- options.onRouteComplete();
95
- }
96
- },
97
- onScenarioStart: (s) => {
98
- try { onScenarioFiredRef.current?.(s); } catch (_) { /* noop */ }
99
- },
100
- });
101
-
102
- // Register any pre-declared scenarios
103
- scenarios.forEach((s) => engine.addScenario(s));
104
-
105
- engineRef.current = engine;
106
-
107
- return () => {
108
- engine.destroy();
109
- engineRef.current = null;
110
- setPosition(null);
111
- };
112
- // eslint-disable-next-line react-hooks/exhaustive-deps
113
- }, [routeGeometry]); // intentionally only re-runs when route changes
114
-
115
- // Start/stop based on `active` prop
116
- useEffect(() => {
117
- const engine = engineRef.current;
118
- if (!engine) return;
119
-
120
- if (active) {
121
- engine.start();
122
- } else {
123
- engine.stop();
124
- setPosition(null);
125
- }
126
- }, [active]);
127
-
128
- // ── Controls ────────────────────────────────────────────────────────────────
129
-
130
- const pause = useCallback(() => engineRef.current?.pause(), []);
131
- const resume = useCallback(() => engineRef.current?.resume(), []);
132
- const seek = useCallback((index: number) => engineRef.current?.seek(index), []);
133
- const setSpeed = useCallback((m: 1 | 2 | 4 | 8) => engineRef.current?.setSpeed(m), []);
134
-
135
- const startSession = useCallback((sessionId?: string) => {
136
- engineRef.current?.startRecording(sessionId);
137
- setIsRecording(true);
138
- }, []);
139
-
140
- const stopSession = useCallback((): SimSession | null => {
141
- setIsRecording(false);
142
- return engineRef.current?.stopRecording() ?? null;
143
- }, []);
144
-
145
- const location = position?.location ?? (position ? { coords: position.coords, timestamp: position.timestamp } : null);
146
-
147
- return {
148
- position,
149
- location,
150
- rawLocation: location,
151
- controls: { pause, resume, seek, setSpeed },
152
- session: { start: startSession, stop: stopSession },
153
- isRecording,
154
- };
155
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * GpxParser — Parse GPX 1.1 files into NavCore coordinate arrays.
3
- *
4
- * ZERO dependencies. Pure string/regex parsing.
5
- * No DOMParser, no xml2js, no cheerio.
6
- *
7
- * Supports:
8
- * <trkpt> (track points — most common)
9
- * <wpt> (waypoints)
10
- */
11
-
12
- import type { Coordinate } from '@ingissa/navcore-core';
13
- import { haversineDistance } from '@ingissa/navcore-core';
14
-
15
- export interface GpxParseResult {
16
- coordinates: Coordinate[];
17
- /** Milliseconds since epoch for each coordinate (0 if no time element) */
18
- timestamps: number[];
19
- metadata: {
20
- name?: string;
21
- /** Total number of coordinate points */
22
- totalPoints: number;
23
- /** Duration in ms (first to last timestamp, 0 if no timestamps) */
24
- durationMs: number;
25
- /** Total route distance in metres */
26
- distanceM: number;
27
- /** Average speed in km/h (0 if no timestamps) */
28
- avgSpeedKmh: number;
29
- };
30
- }
31
-
32
- export class GpxParser {
33
- /**
34
- * Parse a GPX XML string into coordinates and metadata.
35
- * Supports both <trkpt> and <wpt> elements.
36
- */
37
- static parse(gpxString: string): GpxParseResult {
38
- const coordinates: Coordinate[] = [];
39
- const timestamps: number[] = [];
40
-
41
- // Match both <trkpt> and <wpt> tags
42
- const pointRegex = /<(?:trkpt|wpt)\s+([^>]+)>([\s\S]*?)<\/(?:trkpt|wpt)>/g;
43
- const latLonRegex = /lat="([^"]+)"/;
44
- const lonRegex = /lon="([^"]+)"/;
45
- const timeRegex = /<time>([^<]+)<\/time>/;
46
-
47
- let match: RegExpExecArray | null;
48
- while ((match = pointRegex.exec(gpxString)) !== null) {
49
- const attrs = match[1] ?? '';
50
- const body = match[2] ?? '';
51
-
52
- const latMatch = latLonRegex.exec(attrs);
53
- const lonMatch = lonRegex.exec(attrs);
54
- if (!latMatch || !lonMatch) continue;
55
-
56
- const lat = parseFloat(latMatch[1] ?? '0');
57
- const lon = parseFloat(lonMatch[1] ?? '0');
58
- if (isNaN(lat) || isNaN(lon)) continue;
59
-
60
- coordinates.push([lon, lat]);
61
-
62
- const timeMatch = timeRegex.exec(body);
63
- if (timeMatch) {
64
- const ms = Date.parse(timeMatch[1] ?? '');
65
- timestamps.push(isNaN(ms) ? 0 : ms);
66
- } else {
67
- timestamps.push(0);
68
- }
69
- }
70
-
71
- // Extract track/route name
72
- const nameMatch = /<name>([^<]+)<\/name>/.exec(gpxString);
73
- const name = nameMatch ? nameMatch[1]?.trim() : undefined;
74
-
75
- // Compute distance
76
- let distanceM = 0;
77
- for (let i = 1; i < coordinates.length; i++) {
78
- const a = coordinates[i - 1]!;
79
- const b = coordinates[i]!;
80
- distanceM += haversineDistance(a[1], a[0], b[1], b[0]);
81
- }
82
-
83
- // Duration and speed from timestamps
84
- const validTs = timestamps.filter((t) => t > 0);
85
- let durationMs = 0;
86
- let avgSpeedKmh = 0;
87
- if (validTs.length >= 2) {
88
- durationMs = (validTs[validTs.length - 1] ?? 0) - (validTs[0] ?? 0);
89
- if (durationMs > 0) {
90
- const speedMs = distanceM / (durationMs / 1000);
91
- avgSpeedKmh = speedMs * 3.6;
92
- }
93
- }
94
-
95
- return {
96
- coordinates,
97
- timestamps,
98
- metadata: {
99
- name,
100
- totalPoints: coordinates.length,
101
- durationMs,
102
- distanceM,
103
- avgSpeedKmh,
104
- },
105
- };
106
- }
107
-
108
- /**
109
- * Parse a base64-encoded GPX string.
110
- * Works in React Native (uses atob) and Node.js (uses Buffer.from).
111
- */
112
- static fromBase64(base64: string): GpxParseResult {
113
- let decoded: string;
114
- if (typeof atob !== 'undefined') {
115
- decoded = atob(base64);
116
- } else {
117
- // Node.js
118
- decoded = Buffer.from(base64, 'base64').toString('utf-8');
119
- }
120
- return GpxParser.parse(decoded);
121
- }
122
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * SessionRecorder — Record and export simulation sessions.
3
- *
4
- * Supports JSON, GPX 1.1, and CSV export formats.
5
- *
6
- * The GPX export is the critical format: a session recorded in the simulator
7
- * can be exported as GPX, then replayed via GpxParser on a future run.
8
- * This creates a full regression test loop:
9
- * real GPS trace → gpx → GpxParser → SimulatorEngine → CI test
10
- */
11
-
12
- import type { SimSession, SimPosition } from '../core/types';
13
-
14
- /** Generate a UUID v4 without external dependencies */
15
- function uuidV4(): string {
16
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
17
- const r = (Math.random() * 16) | 0;
18
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
19
- return v.toString(16);
20
- });
21
- }
22
-
23
- export type ExportFormat = 'json' | 'gpx' | 'csv';
24
-
25
- export class SessionRecorder {
26
- private _sessionId: string | null = null;
27
- private _startedAt: number | null = null;
28
- private _session: SimSession | null = null;
29
-
30
- /**
31
- * Start a new recording.
32
- * If the engine already has a session via startRecording(), you can pass
33
- * the existing sessionId to keep IDs consistent.
34
- */
35
- start(sessionId?: string): void {
36
- this._sessionId = sessionId ?? uuidV4();
37
- this._startedAt = Date.now();
38
- this._session = null;
39
- }
40
-
41
- /**
42
- * Stop recording and return the captured session.
43
- * If called with an existing SimSession (from SimulatorEngine.stopRecording()),
44
- * it stores that session for subsequent export() calls.
45
- */
46
- stop(existing?: SimSession): SimSession {
47
- if (existing) {
48
- this._session = existing;
49
- return existing;
50
- }
51
-
52
- // Minimal empty session if stop() is called without an engine session
53
- const session: SimSession = {
54
- id: this._sessionId ?? uuidV4(),
55
- startedAt: new Date(this._startedAt ?? Date.now()).toISOString(),
56
- endedAt: new Date().toISOString(),
57
- route: [],
58
- positions: [],
59
- scenarios: [],
60
- stats: {
61
- durationMs: Date.now() - (this._startedAt ?? Date.now()),
62
- distanceTravelledM: 0,
63
- avgSpeedKmh: 0,
64
- noiseSamplesAdded: 0,
65
- scenariosFired: 0,
66
- },
67
- };
68
- this._session = session;
69
- return session;
70
- }
71
-
72
- /**
73
- * Export the most recent session in the specified format.
74
- * Must call stop() first.
75
- */
76
- export(format: ExportFormat): string {
77
- const session = this._session;
78
- if (!session) throw new Error('SessionRecorder: call stop() before export()');
79
-
80
- switch (format) {
81
- case 'json':
82
- return JSON.stringify(session, null, 2);
83
- case 'gpx':
84
- return SessionRecorder._toGpx(session);
85
- case 'csv':
86
- return SessionRecorder._toCsv(session);
87
- default:
88
- throw new Error(`SessionRecorder: unknown format "${format}"`);
89
- }
90
- }
91
-
92
- // ── Static export helpers ───────────────────────────────────────────────────
93
-
94
- private static _toGpx(session: SimSession): string {
95
- const trkpts = session.positions
96
- .map((p: SimPosition) => {
97
- const time = new Date(p.timestamp).toISOString();
98
- return (
99
- ` <trkpt lat="${p.coord[1]}" lon="${p.coord[0]}">\n` +
100
- ` <time>${time}</time>\n` +
101
- ` <extensions>\n` +
102
- ` <speed>${p.speed.toFixed(4)}</speed>\n` +
103
- ` <bearing>${p.bearing.toFixed(4)}</bearing>\n` +
104
- ` <accuracy>${p.accuracy.toFixed(2)}</accuracy>\n` +
105
- ` </extensions>\n` +
106
- ` </trkpt>`
107
- );
108
- })
109
- .join('\n');
110
-
111
- return (
112
- `<?xml version="1.0" encoding="UTF-8"?>\n` +
113
- `<gpx version="1.1" creator="@ingissa/navcore-simulator"\n` +
114
- ` xmlns="http://www.topografix.com/GPX/1/1"\n` +
115
- ` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n` +
116
- ` xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">\n` +
117
- ` <metadata>\n` +
118
- ` <name>${session.id}</name>\n` +
119
- ` <time>${session.startedAt}</time>\n` +
120
- ` </metadata>\n` +
121
- ` <trk>\n` +
122
- ` <name>${session.id}</name>\n` +
123
- ` <trkseg>\n` +
124
- trkpts + '\n' +
125
- ` </trkseg>\n` +
126
- ` </trk>\n` +
127
- `</gpx>`
128
- );
129
- }
130
-
131
- private static _toCsv(session: SimSession): string {
132
- const header = 'timestamp,lng,lat,bearing,speed,accuracy';
133
- const rows = session.positions.map((p: SimPosition) =>
134
- `${p.timestamp},${p.coord[0]},${p.coord[1]},${p.bearing.toFixed(4)},${p.speed.toFixed(4)},${p.accuracy.toFixed(2)}`
135
- );
136
- return [header, ...rows].join('\n');
137
- }
138
- }
@@ -1,136 +0,0 @@
1
- /**
2
- * GpxParser unit tests
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { GpxParser } from '@ingissa/navcore-simulator';
6
-
7
- const MINIMAL_GPX = `<?xml version="1.0"?>
8
- <gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
9
- <trk>
10
- <name>Test Route</name>
11
- <trkseg>
12
- <trkpt lat="30.4278" lon="-9.5981">
13
- <time>2024-01-01T10:00:00Z</time>
14
- </trkpt>
15
- <trkpt lat="30.4300" lon="-9.5960">
16
- <time>2024-01-01T10:00:30Z</time>
17
- </trkpt>
18
- <trkpt lat="30.4320" lon="-9.5940">
19
- <time>2024-01-01T10:01:00Z</time>
20
- </trkpt>
21
- </trkseg>
22
- </trk>
23
- </gpx>`;
24
-
25
- const WAYPOINT_GPX = `<?xml version="1.0"?>
26
- <gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">
27
- <wpt lat="51.5074" lon="-0.1278"></wpt>
28
- <wpt lat="51.5080" lon="-0.1270"></wpt>
29
- </gpx>`;
30
-
31
- const NO_TIMESTAMPS_GPX = `<?xml version="1.0"?>
32
- <gpx version="1.1">
33
- <trk><trkseg>
34
- <trkpt lat="0.0" lon="0.0"></trkpt>
35
- <trkpt lat="0.001" lon="0.001"></trkpt>
36
- </trkseg></trk>
37
- </gpx>`;
38
-
39
- describe('GpxParser', () => {
40
- describe('parse — basic trkpt', () => {
41
- it('parses 3 track points', () => {
42
- const result = GpxParser.parse(MINIMAL_GPX);
43
- expect(result.coordinates).toHaveLength(3);
44
- });
45
-
46
- it('coordinates are [lng, lat] tuples', () => {
47
- const result = GpxParser.parse(MINIMAL_GPX);
48
- // First point: lat=30.4278, lon=-9.5981 → [lon, lat]
49
- expect(result.coordinates[0]![0]).toBeCloseTo(-9.5981, 4);
50
- expect(result.coordinates[0]![1]).toBeCloseTo(30.4278, 4);
51
- });
52
-
53
- it('parses timestamps correctly', () => {
54
- const result = GpxParser.parse(MINIMAL_GPX);
55
- expect(result.timestamps).toHaveLength(3);
56
- expect(result.timestamps[0]).toBeGreaterThan(0);
57
- });
58
-
59
- it('computes duration from first to last timestamp', () => {
60
- const result = GpxParser.parse(MINIMAL_GPX);
61
- // 10:00:00 to 10:01:00 = 60000ms
62
- expect(result.metadata.durationMs).toBe(60000);
63
- });
64
-
65
- it('computes positive distance', () => {
66
- const result = GpxParser.parse(MINIMAL_GPX);
67
- expect(result.metadata.distanceM).toBeGreaterThan(0);
68
- });
69
-
70
- it('extracts track name', () => {
71
- const result = GpxParser.parse(MINIMAL_GPX);
72
- expect(result.metadata.name).toBe('Test Route');
73
- });
74
-
75
- it('totalPoints matches coordinate count', () => {
76
- const result = GpxParser.parse(MINIMAL_GPX);
77
- expect(result.metadata.totalPoints).toBe(3);
78
- });
79
-
80
- it('computes positive avgSpeedKmh', () => {
81
- const result = GpxParser.parse(MINIMAL_GPX);
82
- expect(result.metadata.avgSpeedKmh).toBeGreaterThan(0);
83
- });
84
- });
85
-
86
- describe('parse — waypoints', () => {
87
- it('parses <wpt> elements', () => {
88
- const result = GpxParser.parse(WAYPOINT_GPX);
89
- expect(result.coordinates).toHaveLength(2);
90
- expect(result.coordinates[0]![0]).toBeCloseTo(-0.1278, 4);
91
- expect(result.coordinates[0]![1]).toBeCloseTo(51.5074, 4);
92
- });
93
- });
94
-
95
- describe('parse — no timestamps', () => {
96
- it('returns zero durationMs and zero avgSpeedKmh', () => {
97
- const result = GpxParser.parse(NO_TIMESTAMPS_GPX);
98
- expect(result.metadata.durationMs).toBe(0);
99
- expect(result.metadata.avgSpeedKmh).toBe(0);
100
- });
101
-
102
- it('still computes distance', () => {
103
- const result = GpxParser.parse(NO_TIMESTAMPS_GPX);
104
- expect(result.metadata.distanceM).toBeGreaterThan(0);
105
- });
106
- });
107
-
108
- describe('parse — empty GPX', () => {
109
- it('returns empty arrays for empty GPX', () => {
110
- const result = GpxParser.parse('<gpx></gpx>');
111
- expect(result.coordinates).toHaveLength(0);
112
- expect(result.timestamps).toHaveLength(0);
113
- expect(result.metadata.totalPoints).toBe(0);
114
- });
115
- });
116
-
117
- describe('fromBase64', () => {
118
- it('round-trips a GPX string via base64', () => {
119
- const b64 = Buffer.from(MINIMAL_GPX).toString('base64');
120
- const result = GpxParser.fromBase64(b64);
121
- expect(result.coordinates).toHaveLength(3);
122
- expect(result.coordinates[0]![0]).toBeCloseTo(-9.5981, 4);
123
- });
124
- });
125
-
126
- describe('parse — malformed attributes', () => {
127
- it('skips points with invalid lat/lon', () => {
128
- const gpx = `<gpx><trk><trkseg>
129
- <trkpt lat="not-a-number" lon="-9.5981"></trkpt>
130
- <trkpt lat="30.0" lon="-9.0"></trkpt>
131
- </trkseg></trk></gpx>`;
132
- const result = GpxParser.parse(gpx);
133
- expect(result.coordinates).toHaveLength(1);
134
- });
135
- });
136
- });