@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
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* SimulatorControls — Self-contained React Native control panel for SimulatorEngine.
|
|
4
|
-
*
|
|
5
|
-
* Three style variants:
|
|
6
|
-
* 'minimal' — compact horizontal bar: speed selector + pause/resume
|
|
7
|
-
* 'full' — full card with speed, seek, scenario log, recorder, stats
|
|
8
|
-
* 'floating' — collapsible bottom sheet (Animated, no external libs)
|
|
9
|
-
*
|
|
10
|
-
* Colour tokens match the Klaroways design system used in NavCoreTestScreen.
|
|
11
|
-
*/
|
|
12
|
-
import { useState, useRef, useCallback } from 'react';
|
|
13
|
-
import { View, Text, TouchableOpacity, StyleSheet, ScrollView, Animated, Platform, } from 'react-native';
|
|
14
|
-
// ─── Colour Tokens ────────────────────────────────────────────────────────────
|
|
15
|
-
const KW = {
|
|
16
|
-
navyDeep: '#081525',
|
|
17
|
-
navyCard: '#152240',
|
|
18
|
-
navyBorder: '#1E3255',
|
|
19
|
-
emerald: '#00BF76',
|
|
20
|
-
emeraldDark: '#04342C',
|
|
21
|
-
emeraldMid: '#009A5F',
|
|
22
|
-
amber: '#F5A623',
|
|
23
|
-
amberDark: '#412402',
|
|
24
|
-
slate: '#6B7FA3',
|
|
25
|
-
slateLight: '#8FA3C8',
|
|
26
|
-
light: '#EBEFF8',
|
|
27
|
-
fault: '#FF4848',
|
|
28
|
-
faultDark: '#3D0000',
|
|
29
|
-
white: '#FFFFFF',
|
|
30
|
-
mono: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
|
31
|
-
};
|
|
32
|
-
// ─── Speed Selector ───────────────────────────────────────────────────────────
|
|
33
|
-
function SpeedSelector({ current, onChange, }) {
|
|
34
|
-
return (_jsxs(View, { style: spd.row, children: [_jsx(Text, { style: spd.label, children: "SIM SPEED" }), [1, 2, 4, 8].map((v) => (_jsx(TouchableOpacity, { style: [spd.btn, current === v && spd.btnActive], onPress: () => onChange(v), activeOpacity: 0.7, children: _jsxs(Text, { style: [spd.btnText, current === v && { color: KW.emeraldDark }], children: [v, "\u00D7"] }) }, v)))] }));
|
|
35
|
-
}
|
|
36
|
-
const spd = StyleSheet.create({
|
|
37
|
-
row: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
38
|
-
label: { fontSize: 9, letterSpacing: 1.2, color: KW.slate, marginRight: 4 },
|
|
39
|
-
btn: {
|
|
40
|
-
paddingHorizontal: 10,
|
|
41
|
-
paddingVertical: 5,
|
|
42
|
-
borderRadius: 6,
|
|
43
|
-
borderWidth: 1,
|
|
44
|
-
borderColor: KW.navyBorder,
|
|
45
|
-
},
|
|
46
|
-
btnActive: { backgroundColor: KW.emerald, borderColor: KW.emerald },
|
|
47
|
-
btnText: { fontSize: 11, fontWeight: '700', color: KW.slate },
|
|
48
|
-
});
|
|
49
|
-
// ─── Pause / Resume ───────────────────────────────────────────────────────────
|
|
50
|
-
function PauseResumeButton({ state, onPause, onResume, }) {
|
|
51
|
-
if (state !== 'running' && state !== 'paused')
|
|
52
|
-
return null;
|
|
53
|
-
return (_jsx(TouchableOpacity, { style: [pr.btn, state === 'paused' && pr.btnResume], onPress: state === 'running' ? onPause : onResume, activeOpacity: 0.75, children: _jsx(Text, { style: pr.text, children: state === 'running' ? '⏸' : '▶' }) }));
|
|
54
|
-
}
|
|
55
|
-
const pr = StyleSheet.create({
|
|
56
|
-
btn: {
|
|
57
|
-
paddingHorizontal: 14,
|
|
58
|
-
paddingVertical: 7,
|
|
59
|
-
borderRadius: 8,
|
|
60
|
-
backgroundColor: KW.navyDeep,
|
|
61
|
-
borderWidth: 1,
|
|
62
|
-
borderColor: KW.navyBorder,
|
|
63
|
-
alignItems: 'center',
|
|
64
|
-
},
|
|
65
|
-
btnResume: { borderColor: KW.emerald },
|
|
66
|
-
text: { fontSize: 16 },
|
|
67
|
-
});
|
|
68
|
-
// ─── Scenario Log ─────────────────────────────────────────────────────────────
|
|
69
|
-
function ScenarioLog({ entries }) {
|
|
70
|
-
return (_jsxs(View, { style: log.container, children: [_jsx(Text, { style: log.title, children: "// scenario log" }), _jsx(ScrollView, { style: log.scroll, showsVerticalScrollIndicator: false, children: entries.length === 0 ? (_jsx(Text, { style: log.empty, children: "No scenarios fired yet\u2026" })) : (entries.map((e, i) => {
|
|
71
|
-
const d = new Date(e.ts);
|
|
72
|
-
const time = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
73
|
-
return (_jsxs(Text, { style: log.entry, children: [_jsxs(Text, { style: log.time, children: [time, " "] }), _jsxs(Text, { style: { color: KW.amber }, children: ["[", e.type, "]"] }), ' ', e.name] }, i));
|
|
74
|
-
})) })] }));
|
|
75
|
-
}
|
|
76
|
-
const log = StyleSheet.create({
|
|
77
|
-
container: {
|
|
78
|
-
backgroundColor: KW.navyDeep,
|
|
79
|
-
borderRadius: 8,
|
|
80
|
-
padding: 8,
|
|
81
|
-
borderWidth: 1,
|
|
82
|
-
borderColor: KW.navyBorder,
|
|
83
|
-
maxHeight: 100,
|
|
84
|
-
marginTop: 8,
|
|
85
|
-
},
|
|
86
|
-
title: {
|
|
87
|
-
fontSize: 9,
|
|
88
|
-
letterSpacing: 1,
|
|
89
|
-
color: KW.slate,
|
|
90
|
-
fontFamily: KW.mono,
|
|
91
|
-
marginBottom: 4,
|
|
92
|
-
},
|
|
93
|
-
scroll: { flex: 1 },
|
|
94
|
-
empty: { fontSize: 10, color: KW.slate, fontFamily: KW.mono },
|
|
95
|
-
entry: { fontSize: 10, color: KW.slateLight, fontFamily: KW.mono, marginBottom: 2 },
|
|
96
|
-
time: { color: KW.slate },
|
|
97
|
-
});
|
|
98
|
-
// ─── Session Recorder Controls ────────────────────────────────────────────────
|
|
99
|
-
function SessionRecorderControls({ isRecording, session, onExport, lastSession, }) {
|
|
100
|
-
if (!session)
|
|
101
|
-
return null;
|
|
102
|
-
return (_jsxs(View, { style: rec.row, children: [_jsx(TouchableOpacity, { style: [rec.btn, isRecording && rec.btnActive], onPress: () => {
|
|
103
|
-
if (isRecording) {
|
|
104
|
-
// session stop returns SimSession | null from hook
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
session.start();
|
|
108
|
-
}
|
|
109
|
-
}, activeOpacity: 0.75, children: _jsx(Text, { style: rec.btnText, children: isRecording ? '⏹ STOP REC' : '⏺ RECORD' }) }), lastSession && onExport && (_jsx(TouchableOpacity, { style: rec.exportBtn, onPress: () => onExport(lastSession), activeOpacity: 0.75, children: _jsx(Text, { style: rec.exportText, children: "EXPORT JSON" }) }))] }));
|
|
110
|
-
}
|
|
111
|
-
const rec = StyleSheet.create({
|
|
112
|
-
row: { flexDirection: 'row', gap: 8, marginTop: 8 },
|
|
113
|
-
btn: {
|
|
114
|
-
flex: 1,
|
|
115
|
-
paddingVertical: 8,
|
|
116
|
-
borderRadius: 8,
|
|
117
|
-
backgroundColor: KW.navyDeep,
|
|
118
|
-
borderWidth: 1,
|
|
119
|
-
borderColor: KW.navyBorder,
|
|
120
|
-
alignItems: 'center',
|
|
121
|
-
},
|
|
122
|
-
btnActive: { borderColor: KW.fault, backgroundColor: KW.faultDark },
|
|
123
|
-
btnText: { fontSize: 11, fontWeight: '700', color: KW.slateLight, letterSpacing: 0.8 },
|
|
124
|
-
exportBtn: {
|
|
125
|
-
paddingHorizontal: 12,
|
|
126
|
-
paddingVertical: 8,
|
|
127
|
-
borderRadius: 8,
|
|
128
|
-
backgroundColor: KW.navyDeep,
|
|
129
|
-
borderWidth: 1,
|
|
130
|
-
borderColor: KW.emeraldMid,
|
|
131
|
-
alignItems: 'center',
|
|
132
|
-
},
|
|
133
|
-
exportText: { fontSize: 10, fontWeight: '700', color: KW.emerald, letterSpacing: 0.5 },
|
|
134
|
-
});
|
|
135
|
-
// ─── Main Component ───────────────────────────────────────────────────────────
|
|
136
|
-
export function SimulatorControls({ simulator, session, style = 'minimal', showScenarioLog = false, showSessionRecorder = false, scenarioLog = [], currentSpeed = 1, engineState = 'idle', isRecording = false, onExportSession, containerStyle, }) {
|
|
137
|
-
const [speed, setSpeedState] = useState(currentSpeed);
|
|
138
|
-
const [lastSession, setLastSession] = useState(null);
|
|
139
|
-
// Floating sheet animation
|
|
140
|
-
const sheetAnim = useRef(new Animated.Value(0)).current;
|
|
141
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
142
|
-
const handleSpeedChange = useCallback((v) => {
|
|
143
|
-
setSpeedState(v);
|
|
144
|
-
simulator.setSpeed(v);
|
|
145
|
-
}, [simulator]);
|
|
146
|
-
const handleStopRecording = useCallback(() => {
|
|
147
|
-
const s = session?.stop();
|
|
148
|
-
if (s)
|
|
149
|
-
setLastSession(s);
|
|
150
|
-
}, [session]);
|
|
151
|
-
const toggleSheet = useCallback(() => {
|
|
152
|
-
const toValue = isExpanded ? 0 : 1;
|
|
153
|
-
setIsExpanded(!isExpanded);
|
|
154
|
-
Animated.spring(sheetAnim, {
|
|
155
|
-
toValue,
|
|
156
|
-
useNativeDriver: false,
|
|
157
|
-
bounciness: 4,
|
|
158
|
-
}).start();
|
|
159
|
-
}, [isExpanded, sheetAnim]);
|
|
160
|
-
// ── Minimal ────────────────────────────────────────────────────────────────
|
|
161
|
-
if (style === 'minimal') {
|
|
162
|
-
return (_jsxs(View, { style: [s.minimal, containerStyle], children: [_jsx(SpeedSelector, { current: speed, onChange: handleSpeedChange }), _jsx(PauseResumeButton, { state: engineState, onPause: simulator.pause, onResume: simulator.resume })] }));
|
|
163
|
-
}
|
|
164
|
-
// ── Full ───────────────────────────────────────────────────────────────────
|
|
165
|
-
if (style === 'full') {
|
|
166
|
-
return (_jsxs(View, { style: [s.full, containerStyle], children: [_jsxs(View, { style: s.fullHeader, children: [_jsx(Text, { style: s.fullTitle, children: "SIMULATOR" }), _jsx(View, { style: [s.stateBadge, engineState === 'running' && s.stateBadgeActive], children: _jsx(Text, { style: s.stateText, children: engineState.toUpperCase() }) })] }), _jsx(SpeedSelector, { current: speed, onChange: handleSpeedChange }), _jsx(View, { style: s.pauseRow, children: _jsx(PauseResumeButton, { state: engineState, onPause: simulator.pause, onResume: simulator.resume }) }), showScenarioLog && _jsx(ScenarioLog, { entries: scenarioLog }), showSessionRecorder && (_jsx(SessionRecorderControls, { isRecording: isRecording, session: session, onExport: onExportSession, lastSession: lastSession }))] }));
|
|
167
|
-
}
|
|
168
|
-
// ── Floating ───────────────────────────────────────────────────────────────
|
|
169
|
-
const sheetHeight = sheetAnim.interpolate({
|
|
170
|
-
inputRange: [0, 1],
|
|
171
|
-
outputRange: [44, showScenarioLog ? 260 : 160],
|
|
172
|
-
});
|
|
173
|
-
return (_jsxs(Animated.View, { style: [s.floating, { height: sheetHeight }, containerStyle], children: [_jsxs(TouchableOpacity, { style: s.floatHandle, onPress: toggleSheet, activeOpacity: 0.8, children: [_jsx(View, { style: s.handleBar }), _jsxs(Text, { style: s.floatTitle, children: ["SIMULATOR ", isExpanded ? '▼' : '▲'] })] }), isExpanded && (_jsxs(View, { style: s.floatContent, children: [_jsx(SpeedSelector, { current: speed, onChange: handleSpeedChange }), _jsx(View, { style: s.pauseRow, children: _jsx(PauseResumeButton, { state: engineState, onPause: simulator.pause, onResume: simulator.resume }) }), showScenarioLog && _jsx(ScenarioLog, { entries: scenarioLog })] }))] }));
|
|
174
|
-
}
|
|
175
|
-
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
176
|
-
const s = StyleSheet.create({
|
|
177
|
-
// Minimal
|
|
178
|
-
minimal: {
|
|
179
|
-
flexDirection: 'row',
|
|
180
|
-
alignItems: 'center',
|
|
181
|
-
gap: 12,
|
|
182
|
-
paddingHorizontal: 4,
|
|
183
|
-
paddingVertical: 6,
|
|
184
|
-
},
|
|
185
|
-
// Full
|
|
186
|
-
full: {
|
|
187
|
-
backgroundColor: KW.navyCard,
|
|
188
|
-
borderRadius: 12,
|
|
189
|
-
padding: 14,
|
|
190
|
-
borderWidth: 1,
|
|
191
|
-
borderColor: KW.navyBorder,
|
|
192
|
-
},
|
|
193
|
-
fullHeader: {
|
|
194
|
-
flexDirection: 'row',
|
|
195
|
-
justifyContent: 'space-between',
|
|
196
|
-
alignItems: 'center',
|
|
197
|
-
marginBottom: 10,
|
|
198
|
-
},
|
|
199
|
-
fullTitle: {
|
|
200
|
-
fontSize: 11,
|
|
201
|
-
fontWeight: '800',
|
|
202
|
-
color: KW.slateLight,
|
|
203
|
-
letterSpacing: 1.5,
|
|
204
|
-
},
|
|
205
|
-
stateBadge: {
|
|
206
|
-
paddingHorizontal: 8,
|
|
207
|
-
paddingVertical: 3,
|
|
208
|
-
borderRadius: 4,
|
|
209
|
-
backgroundColor: KW.navyDeep,
|
|
210
|
-
borderWidth: 1,
|
|
211
|
-
borderColor: KW.navyBorder,
|
|
212
|
-
},
|
|
213
|
-
stateBadgeActive: { borderColor: KW.emerald },
|
|
214
|
-
stateText: { fontSize: 9, fontWeight: '700', color: KW.slate, letterSpacing: 1 },
|
|
215
|
-
pauseRow: { marginTop: 8 },
|
|
216
|
-
// Floating
|
|
217
|
-
floating: {
|
|
218
|
-
position: 'absolute',
|
|
219
|
-
bottom: 0,
|
|
220
|
-
left: 0,
|
|
221
|
-
right: 0,
|
|
222
|
-
backgroundColor: KW.navyCard,
|
|
223
|
-
borderTopLeftRadius: 16,
|
|
224
|
-
borderTopRightRadius: 16,
|
|
225
|
-
borderTopWidth: 1,
|
|
226
|
-
borderColor: KW.navyBorder,
|
|
227
|
-
overflow: 'hidden',
|
|
228
|
-
zIndex: 100,
|
|
229
|
-
},
|
|
230
|
-
floatHandle: {
|
|
231
|
-
height: 44,
|
|
232
|
-
alignItems: 'center',
|
|
233
|
-
justifyContent: 'center',
|
|
234
|
-
flexDirection: 'row',
|
|
235
|
-
gap: 10,
|
|
236
|
-
},
|
|
237
|
-
handleBar: {
|
|
238
|
-
width: 36,
|
|
239
|
-
height: 4,
|
|
240
|
-
backgroundColor: KW.navyBorder,
|
|
241
|
-
borderRadius: 2,
|
|
242
|
-
},
|
|
243
|
-
floatTitle: {
|
|
244
|
-
fontSize: 10,
|
|
245
|
-
fontWeight: '700',
|
|
246
|
-
color: KW.slate,
|
|
247
|
-
letterSpacing: 1.2,
|
|
248
|
-
},
|
|
249
|
-
floatContent: {
|
|
250
|
-
paddingHorizontal: 16,
|
|
251
|
-
paddingBottom: 16,
|
|
252
|
-
gap: 8,
|
|
253
|
-
},
|
|
254
|
-
});
|
|
@@ -1,55 +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
|
-
import type { SimPosition, SimulatorConfig, SimSession, Scenario } from '../core/types';
|
|
19
|
-
import type { Coordinate } from '../core/types';
|
|
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
|
-
/** Playback controls */
|
|
48
|
-
controls: SimulatorControls;
|
|
49
|
-
/** Recording controls */
|
|
50
|
-
session: SessionControls;
|
|
51
|
-
/** Whether a recording is in progress */
|
|
52
|
-
isRecording: boolean;
|
|
53
|
-
}
|
|
54
|
-
export declare function useSimulator(options: UseSimulatorOptions): UseSimulatorResult;
|
|
55
|
-
//# sourceMappingURL=useSimulator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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,MAAM,eAAe,CAAC;AACxF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,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,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,CAoF7E"}
|
|
@@ -1,97 +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
|
-
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
|
-
return {
|
|
92
|
-
position,
|
|
93
|
-
controls: { pause, resume, seek, setSpeed },
|
|
94
|
-
session: { start: startSession, stop: stopSession },
|
|
95
|
-
isRecording,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
@@ -1,40 +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
|
-
import type { Coordinate } from '../core/types';
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GpxParser.d.ts","sourceRoot":"","sources":["../../src/replay/GpxParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,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"}
|
package/dist/replay/GpxParser.js
DELETED
|
@@ -1,95 +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
|
-
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
|
-
}
|
|
@@ -1,37 +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
|
-
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|