@ingissa/navcore-simulator 1.0.4 → 1.0.8

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 (173) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +10 -18
  3. package/dist/core/src/NavCore.d.ts +142 -0
  4. package/dist/core/src/NavCore.d.ts.map +1 -0
  5. package/dist/core/src/NavCore.js +390 -0
  6. package/dist/core/src/bearing/BearingEngine.d.ts +24 -0
  7. package/dist/core/src/bearing/BearingEngine.d.ts.map +1 -0
  8. package/dist/core/src/bearing/BearingEngine.js +50 -0
  9. package/dist/core/src/constants.d.ts +34 -0
  10. package/dist/core/src/constants.d.ts.map +1 -0
  11. package/dist/core/src/constants.js +41 -0
  12. package/dist/core/src/corridor/DeviationDetector.d.ts +33 -0
  13. package/dist/core/src/corridor/DeviationDetector.d.ts.map +1 -0
  14. package/dist/core/src/corridor/DeviationDetector.js +83 -0
  15. package/dist/core/src/corridor/DynamicCorridor.d.ts +13 -0
  16. package/dist/core/src/corridor/DynamicCorridor.d.ts.map +1 -0
  17. package/dist/core/src/corridor/DynamicCorridor.js +26 -0
  18. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +35 -0
  19. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +1 -0
  20. package/dist/core/src/dead-reckoning/DeadReckoning.js +49 -0
  21. package/dist/core/src/directions/DirectionsProvider.d.ts +70 -0
  22. package/dist/core/src/directions/DirectionsProvider.d.ts.map +1 -0
  23. package/dist/core/src/directions/DirectionsProvider.js +7 -0
  24. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +26 -0
  25. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +1 -0
  26. package/dist/core/src/directions/OSRMDirectionsProvider.js +90 -0
  27. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +30 -0
  28. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +1 -0
  29. package/dist/core/src/directions/OpenRouteServiceProvider.js +111 -0
  30. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +33 -0
  31. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +1 -0
  32. package/dist/core/src/directions/ValhallaDirectionsProvider.js +155 -0
  33. package/dist/core/src/eta/ETAEngine.d.ts +61 -0
  34. package/dist/core/src/eta/ETAEngine.d.ts.map +1 -0
  35. package/dist/core/src/eta/ETAEngine.js +74 -0
  36. package/dist/core/src/features.d.ts +76 -0
  37. package/dist/core/src/features.d.ts.map +1 -0
  38. package/dist/core/src/features.js +76 -0
  39. package/dist/core/src/geo/bearing.d.ts +11 -0
  40. package/dist/core/src/geo/bearing.d.ts.map +1 -0
  41. package/dist/core/src/geo/bearing.js +25 -0
  42. package/dist/core/src/geo/cumulative.d.ts +9 -0
  43. package/dist/core/src/geo/cumulative.d.ts.map +1 -0
  44. package/dist/core/src/geo/cumulative.js +20 -0
  45. package/dist/core/src/geo/haversine.d.ts +13 -0
  46. package/dist/core/src/geo/haversine.d.ts.map +1 -0
  47. package/dist/core/src/geo/haversine.js +39 -0
  48. package/dist/core/src/geo/projection.d.ts +7 -0
  49. package/dist/core/src/geo/projection.d.ts.map +1 -0
  50. package/dist/core/src/geo/projection.js +17 -0
  51. package/dist/core/src/geofencing/GeofencingEngine.d.ts +93 -0
  52. package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +1 -0
  53. package/dist/core/src/geofencing/GeofencingEngine.js +154 -0
  54. package/dist/core/src/index.d.ts +66 -0
  55. package/dist/core/src/index.d.ts.map +1 -0
  56. package/dist/core/src/index.js +62 -0
  57. package/dist/core/src/kalman/KalmanFilter2D.d.ts +46 -0
  58. package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +1 -0
  59. package/dist/core/src/kalman/KalmanFilter2D.js +77 -0
  60. package/dist/core/src/license/LicenseManager.d.ts +40 -0
  61. package/dist/core/src/license/LicenseManager.d.ts.map +1 -0
  62. package/dist/core/src/license/LicenseManager.js +119 -0
  63. package/dist/core/src/license/verifyP256Signature.d.ts +2 -0
  64. package/dist/core/src/license/verifyP256Signature.d.ts.map +1 -0
  65. package/dist/core/src/license/verifyP256Signature.js +26 -0
  66. package/dist/core/src/off-route/NavigationFSM.d.ts +42 -0
  67. package/dist/core/src/off-route/NavigationFSM.d.ts.map +1 -0
  68. package/dist/core/src/off-route/NavigationFSM.js +56 -0
  69. package/dist/core/src/off-route/OffRouteFSM.d.ts +70 -0
  70. package/dist/core/src/off-route/OffRouteFSM.d.ts.map +1 -0
  71. package/dist/core/src/off-route/OffRouteFSM.js +151 -0
  72. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +24 -0
  73. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +1 -0
  74. package/dist/core/src/predictor/RouteOffsetPredictor.js +55 -0
  75. package/dist/core/src/progress/InstructionEditor.d.ts +77 -0
  76. package/dist/core/src/progress/InstructionEditor.d.ts.map +1 -0
  77. package/dist/core/src/progress/InstructionEditor.js +151 -0
  78. package/dist/core/src/progress/InstructionResolver.d.ts +25 -0
  79. package/dist/core/src/progress/InstructionResolver.d.ts.map +1 -0
  80. package/dist/core/src/progress/InstructionResolver.js +78 -0
  81. package/dist/core/src/progress/ProgressTracker.d.ts +12 -0
  82. package/dist/core/src/progress/ProgressTracker.d.ts.map +1 -0
  83. package/dist/core/src/progress/ProgressTracker.js +22 -0
  84. package/dist/core/src/renderer/MapRendererAdapter.d.ts +72 -0
  85. package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +1 -0
  86. package/dist/core/src/renderer/MapRendererAdapter.js +19 -0
  87. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +83 -0
  88. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +1 -0
  89. package/dist/core/src/route-builder/CustomRouteBuilder.js +160 -0
  90. package/dist/core/src/route-plan/RoutePlan.d.ts +38 -0
  91. package/dist/core/src/route-plan/RoutePlan.d.ts.map +1 -0
  92. package/dist/core/src/route-plan/RoutePlan.js +102 -0
  93. package/dist/core/src/session/NavigationSession.d.ts +81 -0
  94. package/dist/core/src/session/NavigationSession.d.ts.map +1 -0
  95. package/dist/core/src/session/NavigationSession.js +115 -0
  96. package/dist/core/src/snapper/ParallelRoadResolver.d.ts +86 -0
  97. package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +1 -0
  98. package/dist/core/src/snapper/ParallelRoadResolver.js +83 -0
  99. package/dist/core/src/snapper/RouteSnapper.d.ts +38 -0
  100. package/dist/core/src/snapper/RouteSnapper.d.ts.map +1 -0
  101. package/dist/core/src/snapper/RouteSnapper.js +80 -0
  102. package/dist/core/src/voice/VoiceTriggerEngine.d.ts +80 -0
  103. package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +1 -0
  104. package/dist/core/src/voice/VoiceTriggerEngine.js +108 -0
  105. package/dist/simulator/src/core/NoiseEngine.d.ts +46 -0
  106. package/dist/simulator/src/core/NoiseEngine.d.ts.map +1 -0
  107. package/dist/simulator/src/core/NoiseEngine.js +106 -0
  108. package/dist/simulator/src/core/RouteWalker.d.ts +78 -0
  109. package/dist/simulator/src/core/RouteWalker.d.ts.map +1 -0
  110. package/dist/simulator/src/core/RouteWalker.js +180 -0
  111. package/dist/simulator/src/core/SimulatorEngine.d.ts +53 -0
  112. package/dist/simulator/src/core/SimulatorEngine.d.ts.map +1 -0
  113. package/dist/simulator/src/core/SimulatorEngine.js +298 -0
  114. package/dist/simulator/src/core/types.d.ts +157 -0
  115. package/dist/simulator/src/core/types.d.ts.map +1 -0
  116. package/dist/simulator/src/core/types.js +7 -0
  117. package/dist/simulator/src/fleet/FleetSimulator.d.ts +60 -0
  118. package/dist/simulator/src/fleet/FleetSimulator.d.ts.map +1 -0
  119. package/dist/simulator/src/fleet/FleetSimulator.js +114 -0
  120. package/dist/simulator/src/index.d.ts +28 -0
  121. package/dist/simulator/src/index.d.ts.map +1 -0
  122. package/dist/simulator/src/index.js +29 -0
  123. package/dist/simulator/src/node.d.ts +26 -0
  124. package/dist/simulator/src/node.d.ts.map +1 -0
  125. package/dist/simulator/src/node.js +25 -0
  126. package/dist/simulator/src/react-native/SimulatorControls.d.ts +35 -0
  127. package/dist/simulator/src/react-native/SimulatorControls.d.ts.map +1 -0
  128. package/dist/simulator/src/react-native/SimulatorControls.js +254 -0
  129. package/dist/simulator/src/react-native/useSimulator.d.ts +59 -0
  130. package/dist/simulator/src/react-native/useSimulator.d.ts.map +1 -0
  131. package/dist/simulator/src/react-native/useSimulator.js +100 -0
  132. package/dist/simulator/src/replay/GpxParser.d.ts +40 -0
  133. package/dist/simulator/src/replay/GpxParser.d.ts.map +1 -0
  134. package/dist/simulator/src/replay/GpxParser.js +95 -0
  135. package/dist/simulator/src/replay/SessionRecorder.d.ts +37 -0
  136. package/dist/simulator/src/replay/SessionRecorder.d.ts.map +1 -0
  137. package/dist/simulator/src/replay/SessionRecorder.js +120 -0
  138. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts +55 -0
  139. package/dist/simulator/src/scenarios/ScenarioInjector.d.ts.map +1 -0
  140. package/dist/simulator/src/scenarios/ScenarioInjector.js +150 -0
  141. package/package.json +23 -23
  142. package/src/__tests__/integration/simulator-pipeline.test.ts +233 -0
  143. package/src/core/NoiseEngine.ts +137 -0
  144. package/src/core/RouteWalker.ts +227 -0
  145. package/src/core/SimulatorEngine.ts +364 -0
  146. package/src/core/__tests__/NoiseEngine.test.ts +131 -0
  147. package/src/core/__tests__/RouteWalker.test.ts +139 -0
  148. package/src/core/__tests__/SimulatorEngine.test.ts +317 -0
  149. package/src/core/types.ts +191 -0
  150. package/src/fleet/FleetSimulator.ts +162 -0
  151. package/src/index.ts +54 -0
  152. package/src/node.ts +48 -0
  153. package/src/react-native/SimulatorControls.tsx +473 -0
  154. package/src/react-native/useSimulator.ts +155 -0
  155. package/src/replay/GpxParser.ts +122 -0
  156. package/src/replay/SessionRecorder.ts +138 -0
  157. package/src/replay/__tests__/GpxParser.test.ts +136 -0
  158. package/src/scenarios/ScenarioInjector.ts +170 -0
  159. package/src/scenarios/__tests__/ScenarioInjector.test.ts +171 -0
  160. package/dist/index-i9TY71RY.d.mts +0 -494
  161. package/dist/index-i9TY71RY.d.ts +0 -494
  162. package/dist/index.d.mts +0 -1
  163. package/dist/index.d.ts +0 -1
  164. package/dist/index.js +0 -937
  165. package/dist/index.mjs +0 -929
  166. package/dist/node.d.mts +0 -1
  167. package/dist/node.d.ts +0 -1
  168. package/dist/node.js +0 -937
  169. package/dist/node.mjs +0 -929
  170. package/dist/react-native.d.mts +0 -82
  171. package/dist/react-native.d.ts +0 -82
  172. package/dist/react-native.js +0 -1370
  173. package/dist/react-native.mjs +0 -1360
@@ -0,0 +1,59 @@
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
+ import type { SimPosition, SimulatorConfig, SimSession, Scenario, SimLocationObject } from '../core/types';
19
+ import type { Coordinate } from '@ingissa/navcore-core';
20
+ export interface UseSimulatorOptions {
21
+ /** Route geometry to simulate along */
22
+ routeGeometry: Coordinate[] | null;
23
+ /** Whether the simulation should be active */
24
+ active: boolean;
25
+ /** Engine configuration (all fields optional) */
26
+ config?: Partial<Omit<SimulatorConfig, 'routeGeometry' | 'onPositionUpdate' | 'onRouteComplete'>>;
27
+ /** Pre-registered scenarios */
28
+ scenarios?: Scenario[];
29
+ /** Called when a scenario fires */
30
+ onScenarioFired?: (scenario: Scenario) => void;
31
+ /** Called when the route simulation finishes */
32
+ onRouteComplete?: () => void;
33
+ }
34
+ export interface SimulatorControls {
35
+ pause: () => void;
36
+ resume: () => void;
37
+ seek: (geometryIndex: number) => void;
38
+ setSpeed: (multiplier: 1 | 2 | 4 | 8) => void;
39
+ }
40
+ export interface SessionControls {
41
+ start: (sessionId?: string) => void;
42
+ stop: () => SimSession | null;
43
+ }
44
+ export interface UseSimulatorResult {
45
+ /** Current simulated position — null when inactive */
46
+ position: SimPosition | null;
47
+ /** Conforms to Expo LocationObject (null when inactive) */
48
+ location: SimLocationObject | null;
49
+ /** Alias for location conforming to Expo LocationObject */
50
+ rawLocation: SimLocationObject | null;
51
+ /** Playback controls */
52
+ controls: SimulatorControls;
53
+ /** Recording controls */
54
+ session: SessionControls;
55
+ /** Whether a recording is in progress */
56
+ isRecording: boolean;
57
+ }
58
+ export declare function useSimulator(options: UseSimulatorOptions): UseSimulatorResult;
59
+ //# sourceMappingURL=useSimulator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSimulator.d.ts","sourceRoot":"","sources":["../../../../src/react-native/useSimulator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC3G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,uCAAuC;IACvC,aAAa,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IACnC,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,kBAAkB,GAAG,iBAAiB,CAAC,CAAC,CAAC;IAClG,+BAA+B;IAC/B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,mCAAmC;IACnC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC/C,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,IAAI,EAAE,MAAM,UAAU,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,2DAA2D;IAC3D,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,wBAAwB;IACxB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,yBAAyB;IACzB,OAAO,EAAE,eAAe,CAAC;IACzB,yCAAyC;IACzC,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAwF7E"}
@@ -0,0 +1,100 @@
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
+ import { useEffect, useRef, useState, useCallback } from 'react';
19
+ import { SimulatorEngine } from '../core/SimulatorEngine';
20
+ import { NoiseEngine } from '../core/NoiseEngine';
21
+ export function useSimulator(options) {
22
+ const { routeGeometry, active, config = {}, scenarios = [], onScenarioFired } = options;
23
+ const [position, setPosition] = useState(null);
24
+ const [isRecording, setIsRecording] = useState(false);
25
+ const engineRef = useRef(null);
26
+ // Keep a stable ref to the latest onScenarioFired callback
27
+ const onScenarioFiredRef = useRef(onScenarioFired);
28
+ onScenarioFiredRef.current = onScenarioFired;
29
+ // Create / recreate the engine whenever the route geometry changes
30
+ useEffect(() => {
31
+ if (!routeGeometry || routeGeometry.length < 2) {
32
+ setPosition(null);
33
+ return;
34
+ }
35
+ const engine = new SimulatorEngine({
36
+ routeGeometry,
37
+ speedKmh: config.speedKmh ?? 50,
38
+ speedMultiplier: config.speedMultiplier ?? 1,
39
+ noiseProfile: config.noiseProfile ?? NoiseEngine.STANDARD,
40
+ loop: config.loop ?? false,
41
+ startIndex: config.startIndex ?? 0,
42
+ onPositionUpdate: (pos) => setPosition(pos),
43
+ onRouteComplete: () => {
44
+ if (options.onRouteComplete) {
45
+ options.onRouteComplete();
46
+ }
47
+ },
48
+ onScenarioStart: (s) => {
49
+ try {
50
+ onScenarioFiredRef.current?.(s);
51
+ }
52
+ catch (_) { /* noop */ }
53
+ },
54
+ });
55
+ // Register any pre-declared scenarios
56
+ scenarios.forEach((s) => engine.addScenario(s));
57
+ engineRef.current = engine;
58
+ return () => {
59
+ engine.destroy();
60
+ engineRef.current = null;
61
+ setPosition(null);
62
+ };
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, [routeGeometry]); // intentionally only re-runs when route changes
65
+ // Start/stop based on `active` prop
66
+ useEffect(() => {
67
+ const engine = engineRef.current;
68
+ if (!engine)
69
+ return;
70
+ if (active) {
71
+ engine.start();
72
+ }
73
+ else {
74
+ engine.stop();
75
+ setPosition(null);
76
+ }
77
+ }, [active]);
78
+ // ── Controls ────────────────────────────────────────────────────────────────
79
+ const pause = useCallback(() => engineRef.current?.pause(), []);
80
+ const resume = useCallback(() => engineRef.current?.resume(), []);
81
+ const seek = useCallback((index) => engineRef.current?.seek(index), []);
82
+ const setSpeed = useCallback((m) => engineRef.current?.setSpeed(m), []);
83
+ const startSession = useCallback((sessionId) => {
84
+ engineRef.current?.startRecording(sessionId);
85
+ setIsRecording(true);
86
+ }, []);
87
+ const stopSession = useCallback(() => {
88
+ setIsRecording(false);
89
+ return engineRef.current?.stopRecording() ?? null;
90
+ }, []);
91
+ const location = position?.location ?? (position ? { coords: position.coords, timestamp: position.timestamp } : null);
92
+ return {
93
+ position,
94
+ location,
95
+ rawLocation: location,
96
+ controls: { pause, resume, seek, setSpeed },
97
+ session: { start: startSession, stop: stopSession },
98
+ isRecording,
99
+ };
100
+ }
@@ -0,0 +1,40 @@
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
+ import type { Coordinate } from '@ingissa/navcore-core';
12
+ export interface GpxParseResult {
13
+ coordinates: Coordinate[];
14
+ /** Milliseconds since epoch for each coordinate (0 if no time element) */
15
+ timestamps: number[];
16
+ metadata: {
17
+ name?: string;
18
+ /** Total number of coordinate points */
19
+ totalPoints: number;
20
+ /** Duration in ms (first to last timestamp, 0 if no timestamps) */
21
+ durationMs: number;
22
+ /** Total route distance in metres */
23
+ distanceM: number;
24
+ /** Average speed in km/h (0 if no timestamps) */
25
+ avgSpeedKmh: number;
26
+ };
27
+ }
28
+ export declare class GpxParser {
29
+ /**
30
+ * Parse a GPX XML string into coordinates and metadata.
31
+ * Supports both <trkpt> and <wpt> elements.
32
+ */
33
+ static parse(gpxString: string): GpxParseResult;
34
+ /**
35
+ * Parse a base64-encoded GPX string.
36
+ * Works in React Native (uses atob) and Node.js (uses Buffer.from).
37
+ */
38
+ static fromBase64(base64: string): GpxParseResult;
39
+ }
40
+ //# sourceMappingURL=GpxParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GpxParser.d.ts","sourceRoot":"","sources":["../../../../src/replay/GpxParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,wCAAwC;QACxC,WAAW,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,EAAE,MAAM,CAAC;QACnB,qCAAqC;QACrC,SAAS,EAAE,MAAM,CAAC;QAClB,iDAAiD;QACjD,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,qBAAa,SAAS;IACpB;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc;IAuE/C;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;CAUlD"}
@@ -0,0 +1,95 @@
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
+ import { haversineDistance } from '@ingissa/navcore-core';
12
+ export class GpxParser {
13
+ /**
14
+ * Parse a GPX XML string into coordinates and metadata.
15
+ * Supports both <trkpt> and <wpt> elements.
16
+ */
17
+ static parse(gpxString) {
18
+ const coordinates = [];
19
+ const timestamps = [];
20
+ // Match both <trkpt> and <wpt> tags
21
+ const pointRegex = /<(?:trkpt|wpt)\s+([^>]+)>([\s\S]*?)<\/(?:trkpt|wpt)>/g;
22
+ const latLonRegex = /lat="([^"]+)"/;
23
+ const lonRegex = /lon="([^"]+)"/;
24
+ const timeRegex = /<time>([^<]+)<\/time>/;
25
+ let match;
26
+ while ((match = pointRegex.exec(gpxString)) !== null) {
27
+ const attrs = match[1] ?? '';
28
+ const body = match[2] ?? '';
29
+ const latMatch = latLonRegex.exec(attrs);
30
+ const lonMatch = lonRegex.exec(attrs);
31
+ if (!latMatch || !lonMatch)
32
+ continue;
33
+ const lat = parseFloat(latMatch[1] ?? '0');
34
+ const lon = parseFloat(lonMatch[1] ?? '0');
35
+ if (isNaN(lat) || isNaN(lon))
36
+ continue;
37
+ coordinates.push([lon, lat]);
38
+ const timeMatch = timeRegex.exec(body);
39
+ if (timeMatch) {
40
+ const ms = Date.parse(timeMatch[1] ?? '');
41
+ timestamps.push(isNaN(ms) ? 0 : ms);
42
+ }
43
+ else {
44
+ timestamps.push(0);
45
+ }
46
+ }
47
+ // Extract track/route name
48
+ const nameMatch = /<name>([^<]+)<\/name>/.exec(gpxString);
49
+ const name = nameMatch ? nameMatch[1]?.trim() : undefined;
50
+ // Compute distance
51
+ let distanceM = 0;
52
+ for (let i = 1; i < coordinates.length; i++) {
53
+ const a = coordinates[i - 1];
54
+ const b = coordinates[i];
55
+ distanceM += haversineDistance(a[1], a[0], b[1], b[0]);
56
+ }
57
+ // Duration and speed from timestamps
58
+ const validTs = timestamps.filter((t) => t > 0);
59
+ let durationMs = 0;
60
+ let avgSpeedKmh = 0;
61
+ if (validTs.length >= 2) {
62
+ durationMs = (validTs[validTs.length - 1] ?? 0) - (validTs[0] ?? 0);
63
+ if (durationMs > 0) {
64
+ const speedMs = distanceM / (durationMs / 1000);
65
+ avgSpeedKmh = speedMs * 3.6;
66
+ }
67
+ }
68
+ return {
69
+ coordinates,
70
+ timestamps,
71
+ metadata: {
72
+ name,
73
+ totalPoints: coordinates.length,
74
+ durationMs,
75
+ distanceM,
76
+ avgSpeedKmh,
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * Parse a base64-encoded GPX string.
82
+ * Works in React Native (uses atob) and Node.js (uses Buffer.from).
83
+ */
84
+ static fromBase64(base64) {
85
+ let decoded;
86
+ if (typeof atob !== 'undefined') {
87
+ decoded = atob(base64);
88
+ }
89
+ else {
90
+ // Node.js
91
+ decoded = Buffer.from(base64, 'base64').toString('utf-8');
92
+ }
93
+ return GpxParser.parse(decoded);
94
+ }
95
+ }
@@ -0,0 +1,37 @@
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
+ import type { SimSession } from '../core/types';
12
+ export type ExportFormat = 'json' | 'gpx' | 'csv';
13
+ export declare class SessionRecorder {
14
+ private _sessionId;
15
+ private _startedAt;
16
+ private _session;
17
+ /**
18
+ * Start a new recording.
19
+ * If the engine already has a session via startRecording(), you can pass
20
+ * the existing sessionId to keep IDs consistent.
21
+ */
22
+ start(sessionId?: string): void;
23
+ /**
24
+ * Stop recording and return the captured session.
25
+ * If called with an existing SimSession (from SimulatorEngine.stopRecording()),
26
+ * it stores that session for subsequent export() calls.
27
+ */
28
+ stop(existing?: SimSession): SimSession;
29
+ /**
30
+ * Export the most recent session in the specified format.
31
+ * Must call stop() first.
32
+ */
33
+ export(format: ExportFormat): string;
34
+ private static _toGpx;
35
+ private static _toCsv;
36
+ }
37
+ //# sourceMappingURL=SessionRecorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRecorder.d.ts","sourceRoot":"","sources":["../../../../src/replay/SessionRecorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,eAAe,CAAC;AAW7D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAElD,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAA2B;IAE3C;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAM/B;;;;OAIG;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,UAAU;IA0BvC;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM;IAkBpC,OAAO,CAAC,MAAM,CAAC,MAAM;IAqCrB,OAAO,CAAC,MAAM,CAAC,MAAM;CAOtB"}
@@ -0,0 +1,120 @@
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
+ /** Generate a UUID v4 without external dependencies */
12
+ function uuidV4() {
13
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
14
+ const r = (Math.random() * 16) | 0;
15
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
16
+ return v.toString(16);
17
+ });
18
+ }
19
+ export class SessionRecorder {
20
+ constructor() {
21
+ this._sessionId = null;
22
+ this._startedAt = null;
23
+ this._session = null;
24
+ }
25
+ /**
26
+ * Start a new recording.
27
+ * If the engine already has a session via startRecording(), you can pass
28
+ * the existing sessionId to keep IDs consistent.
29
+ */
30
+ start(sessionId) {
31
+ this._sessionId = sessionId ?? uuidV4();
32
+ this._startedAt = Date.now();
33
+ this._session = null;
34
+ }
35
+ /**
36
+ * Stop recording and return the captured session.
37
+ * If called with an existing SimSession (from SimulatorEngine.stopRecording()),
38
+ * it stores that session for subsequent export() calls.
39
+ */
40
+ stop(existing) {
41
+ if (existing) {
42
+ this._session = existing;
43
+ return existing;
44
+ }
45
+ // Minimal empty session if stop() is called without an engine session
46
+ const session = {
47
+ id: this._sessionId ?? uuidV4(),
48
+ startedAt: new Date(this._startedAt ?? Date.now()).toISOString(),
49
+ endedAt: new Date().toISOString(),
50
+ route: [],
51
+ positions: [],
52
+ scenarios: [],
53
+ stats: {
54
+ durationMs: Date.now() - (this._startedAt ?? Date.now()),
55
+ distanceTravelledM: 0,
56
+ avgSpeedKmh: 0,
57
+ noiseSamplesAdded: 0,
58
+ scenariosFired: 0,
59
+ },
60
+ };
61
+ this._session = session;
62
+ return session;
63
+ }
64
+ /**
65
+ * Export the most recent session in the specified format.
66
+ * Must call stop() first.
67
+ */
68
+ export(format) {
69
+ const session = this._session;
70
+ if (!session)
71
+ throw new Error('SessionRecorder: call stop() before export()');
72
+ switch (format) {
73
+ case 'json':
74
+ return JSON.stringify(session, null, 2);
75
+ case 'gpx':
76
+ return SessionRecorder._toGpx(session);
77
+ case 'csv':
78
+ return SessionRecorder._toCsv(session);
79
+ default:
80
+ throw new Error(`SessionRecorder: unknown format "${format}"`);
81
+ }
82
+ }
83
+ // ── Static export helpers ───────────────────────────────────────────────────
84
+ static _toGpx(session) {
85
+ const trkpts = session.positions
86
+ .map((p) => {
87
+ const time = new Date(p.timestamp).toISOString();
88
+ return (` <trkpt lat="${p.coord[1]}" lon="${p.coord[0]}">\n` +
89
+ ` <time>${time}</time>\n` +
90
+ ` <extensions>\n` +
91
+ ` <speed>${p.speed.toFixed(4)}</speed>\n` +
92
+ ` <bearing>${p.bearing.toFixed(4)}</bearing>\n` +
93
+ ` <accuracy>${p.accuracy.toFixed(2)}</accuracy>\n` +
94
+ ` </extensions>\n` +
95
+ ` </trkpt>`);
96
+ })
97
+ .join('\n');
98
+ return (`<?xml version="1.0" encoding="UTF-8"?>\n` +
99
+ `<gpx version="1.1" creator="@ingissa/navcore-simulator"\n` +
100
+ ` xmlns="http://www.topografix.com/GPX/1/1"\n` +
101
+ ` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n` +
102
+ ` xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">\n` +
103
+ ` <metadata>\n` +
104
+ ` <name>${session.id}</name>\n` +
105
+ ` <time>${session.startedAt}</time>\n` +
106
+ ` </metadata>\n` +
107
+ ` <trk>\n` +
108
+ ` <name>${session.id}</name>\n` +
109
+ ` <trkseg>\n` +
110
+ trkpts + '\n' +
111
+ ` </trkseg>\n` +
112
+ ` </trk>\n` +
113
+ `</gpx>`);
114
+ }
115
+ static _toCsv(session) {
116
+ const header = 'timestamp,lng,lat,bearing,speed,accuracy';
117
+ const rows = session.positions.map((p) => `${p.timestamp},${p.coord[0]},${p.coord[1]},${p.bearing.toFixed(4)},${p.speed.toFixed(4)},${p.accuracy.toFixed(2)}`);
118
+ return [header, ...rows].join('\n');
119
+ }
120
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ScenarioInjector — Registers and fires Scenario objects at geometry indices.
3
+ *
4
+ * Called internally by SimulatorEngine on each tick.
5
+ * Also provides static factory methods for the built-in scenario types.
6
+ */
7
+ import type { Scenario } from '../core/types';
8
+ export type ScenarioFireCallback = (scenario: Scenario) => void;
9
+ export type ScenarioEndCallback = (scenario: Scenario) => void;
10
+ export declare class ScenarioInjector {
11
+ /** Map from triggerIndex → Scenario */
12
+ private readonly scenarios;
13
+ /** Tracks which scenarios have already fired (by name) to avoid double-fire */
14
+ private readonly fired;
15
+ /** Currently active scenario, if any */
16
+ private _activeScenario;
17
+ private _activeEndTimer;
18
+ private onStart;
19
+ private onEnd;
20
+ constructor(onStart?: ScenarioFireCallback, onEnd?: ScenarioEndCallback);
21
+ register(scenario: Scenario): void;
22
+ remove(name: string): void;
23
+ clear(): void;
24
+ /**
25
+ * Check and fire any scenario whose triggerIndex matches (or was passed) by
26
+ * currentIndex. Called by SimulatorEngine on every tick.
27
+ *
28
+ * Returns the name of the active scenario (if any), for metadata injection.
29
+ */
30
+ tick(currentIndex: number): string | undefined;
31
+ get activeScenario(): Scenario | null;
32
+ /** Clean up any pending timers */
33
+ destroy(): void;
34
+ /**
35
+ * GPS blackout: no position updates for `durationMs`.
36
+ * Engine handles signal loss; this scenario signals the blackout period.
37
+ */
38
+ static gpsBlackout(atIndex: number, durationMs: number): Scenario;
39
+ /**
40
+ * Wrong-side road: offset the simulated position by `offsetM` metres
41
+ * perpendicular to the route to stress-test the parallel road resolver.
42
+ */
43
+ static wrongSideRoad(atIndex: number, offsetM: number): Scenario;
44
+ /** U-turn at the given index — stress-tests U-turn protection. */
45
+ static uTurn(atIndex: number): Scenario;
46
+ /** Speed spike: briefly report spikeKmh instead of the configured speed. */
47
+ static speedSpike(atIndex: number, spikeKmh: number, durationMs?: number): Scenario;
48
+ /**
49
+ * Accuracy drop: GPS accuracy degrades to newAccuracyM for durationMs.
50
+ */
51
+ static accuracyDrop(atIndex: number, newAccuracyM: number, durationMs: number): Scenario;
52
+ private _fire;
53
+ private _clearActiveTimer;
54
+ }
55
+ //# sourceMappingURL=ScenarioInjector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScenarioInjector.d.ts","sourceRoot":"","sources":["../../../../src/scenarios/ScenarioInjector.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;AAChE,MAAM,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;AAE/D,qBAAa,gBAAgB;IAC3B,uCAAuC;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;IACzD,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,wCAAwC;IACxC,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,eAAe,CAA8C;IAErE,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,KAAK,CAAsB;gBAEvB,OAAO,CAAC,EAAE,oBAAoB,EAAE,KAAK,CAAC,EAAE,mBAAmB;IAOvE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAIlC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAS1B,KAAK,IAAI,IAAI;IASb;;;;;OAKG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAa9C,IAAI,cAAc,IAAI,QAAQ,GAAG,IAAI,CAEpC;IAED,kCAAkC;IAClC,OAAO,IAAI,IAAI;IAMf;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ;IAUjE;;;OAGG;IACH,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ;IAShE,kEAAkE;IAClE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ;IASvC,4EAA4E;IAC5E,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,SAAO,GAAG,QAAQ;IAUjF;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ;IAYxF,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,iBAAiB;CAM1B"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * ScenarioInjector — Registers and fires Scenario objects at geometry indices.
3
+ *
4
+ * Called internally by SimulatorEngine on each tick.
5
+ * Also provides static factory methods for the built-in scenario types.
6
+ */
7
+ export class ScenarioInjector {
8
+ constructor(onStart, onEnd) {
9
+ /** Map from triggerIndex → Scenario */
10
+ this.scenarios = new Map();
11
+ /** Tracks which scenarios have already fired (by name) to avoid double-fire */
12
+ this.fired = new Set();
13
+ /** Currently active scenario, if any */
14
+ this._activeScenario = null;
15
+ this._activeEndTimer = null;
16
+ this.onStart = onStart ?? (() => undefined);
17
+ this.onEnd = onEnd ?? (() => undefined);
18
+ }
19
+ // ── Registration ────────────────────────────────────────────────────────────
20
+ register(scenario) {
21
+ this.scenarios.set(scenario.triggerIndex, scenario);
22
+ }
23
+ remove(name) {
24
+ for (const [idx, s] of this.scenarios) {
25
+ if (s.name === name) {
26
+ this.scenarios.delete(idx);
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ clear() {
32
+ this.scenarios.clear();
33
+ this.fired.clear();
34
+ this._clearActiveTimer();
35
+ this._activeScenario = null;
36
+ }
37
+ // ── Tick ────────────────────────────────────────────────────────────────────
38
+ /**
39
+ * Check and fire any scenario whose triggerIndex matches (or was passed) by
40
+ * currentIndex. Called by SimulatorEngine on every tick.
41
+ *
42
+ * Returns the name of the active scenario (if any), for metadata injection.
43
+ */
44
+ tick(currentIndex) {
45
+ const floorIdx = Math.floor(currentIndex);
46
+ for (const [triggerIdx, scenario] of this.scenarios) {
47
+ if (floorIdx >= triggerIdx && !this.fired.has(scenario.name)) {
48
+ this.fired.add(scenario.name);
49
+ this._fire(scenario);
50
+ }
51
+ }
52
+ return this._activeScenario?.name;
53
+ }
54
+ get activeScenario() {
55
+ return this._activeScenario;
56
+ }
57
+ /** Clean up any pending timers */
58
+ destroy() {
59
+ this._clearActiveTimer();
60
+ }
61
+ // ── Static factories ────────────────────────────────────────────────────────
62
+ /**
63
+ * GPS blackout: no position updates for `durationMs`.
64
+ * Engine handles signal loss; this scenario signals the blackout period.
65
+ */
66
+ static gpsBlackout(atIndex, durationMs) {
67
+ return {
68
+ name: `gps-blackout-@${atIndex}`,
69
+ triggerIndex: atIndex,
70
+ type: 'GPS_BLACKOUT',
71
+ durationMs,
72
+ params: { durationMs },
73
+ };
74
+ }
75
+ /**
76
+ * Wrong-side road: offset the simulated position by `offsetM` metres
77
+ * perpendicular to the route to stress-test the parallel road resolver.
78
+ */
79
+ static wrongSideRoad(atIndex, offsetM) {
80
+ return {
81
+ name: `wrong-side-@${atIndex}`,
82
+ triggerIndex: atIndex,
83
+ type: 'WRONG_SIDE_ROAD',
84
+ params: { offsetM },
85
+ };
86
+ }
87
+ /** U-turn at the given index — stress-tests U-turn protection. */
88
+ static uTurn(atIndex) {
89
+ return {
90
+ name: `uturn-@${atIndex}`,
91
+ triggerIndex: atIndex,
92
+ type: 'UTURN',
93
+ params: {},
94
+ };
95
+ }
96
+ /** Speed spike: briefly report spikeKmh instead of the configured speed. */
97
+ static speedSpike(atIndex, spikeKmh, durationMs = 2000) {
98
+ return {
99
+ name: `speed-spike-@${atIndex}`,
100
+ triggerIndex: atIndex,
101
+ type: 'SPEED_SPIKE',
102
+ durationMs,
103
+ params: { spikeKmh },
104
+ };
105
+ }
106
+ /**
107
+ * Accuracy drop: GPS accuracy degrades to newAccuracyM for durationMs.
108
+ */
109
+ static accuracyDrop(atIndex, newAccuracyM, durationMs) {
110
+ return {
111
+ name: `accuracy-drop-@${atIndex}`,
112
+ triggerIndex: atIndex,
113
+ type: 'ACCURACY_DROP',
114
+ durationMs,
115
+ params: { newAccuracyM },
116
+ };
117
+ }
118
+ // ── Private ─────────────────────────────────────────────────────────────────
119
+ _fire(scenario) {
120
+ this._activeScenario = scenario;
121
+ try {
122
+ this.onStart(scenario);
123
+ }
124
+ catch (_) { /* never throw from callback */ }
125
+ if (scenario.durationMs != null && scenario.durationMs > 0) {
126
+ this._clearActiveTimer();
127
+ this._activeEndTimer = setTimeout(() => {
128
+ this._activeScenario = null;
129
+ try {
130
+ this.onEnd(scenario);
131
+ }
132
+ catch (_) { /* never throw */ }
133
+ }, scenario.durationMs);
134
+ }
135
+ else {
136
+ // Instantaneous scenario — end immediately
137
+ this._activeScenario = null;
138
+ try {
139
+ this.onEnd(scenario);
140
+ }
141
+ catch (_) { /* never throw */ }
142
+ }
143
+ }
144
+ _clearActiveTimer() {
145
+ if (this._activeEndTimer !== null) {
146
+ clearTimeout(this._activeEndTimer);
147
+ this._activeEndTimer = null;
148
+ }
149
+ }
150
+ }