@ingissa/navcore-core 1.0.0
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/dist/NavCore-Bjl5fTea.d.mts +377 -0
- package/dist/NavCore-Bjl5fTea.d.ts +377 -0
- package/dist/core.d.mts +488 -0
- package/dist/core.d.ts +488 -0
- package/dist/core.js +8 -0
- package/dist/core.mjs +8 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/index.mjs +8 -0
- package/dist/pro.d.mts +519 -0
- package/dist/pro.d.ts +519 -0
- package/dist/pro.js +1 -0
- package/dist/pro.mjs +1 -0
- package/package.json +72 -0
package/dist/pro.d.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { C as Coordinate, i as NavInstruction, g as NavCoreState } from './NavCore-Bjl5fTea.js';
|
|
2
|
+
export { d as LifecycleContext, e as LifecycleEvent, j as NavLifecycleState, k as NavigationFSM, O as OffRouteContext, l as OffRouteEvent, m as OffRouteFSM, n as OffRouteState, R as RejoinTarget, T as TempRoute, r as resolveNextInstruction } from './NavCore-Bjl5fTea.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ParallelRoadResolver
|
|
6
|
+
*
|
|
7
|
+
* First-class feature: resolve GPS ambiguity on parallel roads (e.g. dual carriageways,
|
|
8
|
+
* motorway + service road) and prevent U-turn snap.
|
|
9
|
+
*
|
|
10
|
+
* Extracts and extends the bearing penalty + inertia logic from RouteSnapper
|
|
11
|
+
* into a standalone, composable API that can be used independently.
|
|
12
|
+
*
|
|
13
|
+
* FEATURE: PARALLEL_ROAD_RESOLUTION + U_TURN_PROTECTION
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const resolver = new ParallelRoadResolver({ bearingPenalty: 80, inertiaPenalty: 40 });
|
|
18
|
+
* const best = resolver.resolve(candidates, vehicleBearing, prevSegmentIndex);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface SnapCandidate {
|
|
23
|
+
/** Route segment index */
|
|
24
|
+
segmentIndex: number;
|
|
25
|
+
/** Projected point on segment */
|
|
26
|
+
projectedCoord: Coordinate;
|
|
27
|
+
/** Raw distance to route in metres */
|
|
28
|
+
distanceMetres: number;
|
|
29
|
+
/** Start of segment */
|
|
30
|
+
segA: Coordinate;
|
|
31
|
+
/** End of segment */
|
|
32
|
+
segB: Coordinate;
|
|
33
|
+
}
|
|
34
|
+
interface ResolvedCandidate extends SnapCandidate {
|
|
35
|
+
/** Final score (lower is better) */
|
|
36
|
+
score: number;
|
|
37
|
+
/** Bearing delta that triggered U-turn protection (null if not triggered) */
|
|
38
|
+
bearingDeltaDeg: number | null;
|
|
39
|
+
/** Whether inertia penalty was applied */
|
|
40
|
+
inertiaPenaltyApplied: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface ParallelRoadResolverOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Penalty added to score when vehicle bearing mismatches segment bearing
|
|
45
|
+
* beyond `bearingMismatchThresholdDeg`. Default: 80m equivalent.
|
|
46
|
+
*/
|
|
47
|
+
bearingPenalty?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Bearing mismatch threshold in degrees that activates the penalty.
|
|
50
|
+
* Default: 60°. Higher = more tolerant of U-turns.
|
|
51
|
+
*/
|
|
52
|
+
bearingMismatchThresholdDeg?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Penalty added when the candidate segment is far from the previously
|
|
55
|
+
* snapped segment, preventing oscillation. Default: 40m equivalent.
|
|
56
|
+
*/
|
|
57
|
+
inertiaPenalty?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Minimum segment jump (in index units) to trigger inertia penalty.
|
|
60
|
+
* Default: 3.
|
|
61
|
+
*/
|
|
62
|
+
inertiaSegmentDist?: number;
|
|
63
|
+
}
|
|
64
|
+
declare class ParallelRoadResolver {
|
|
65
|
+
private readonly options;
|
|
66
|
+
constructor(options?: ParallelRoadResolverOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Score and sort snap candidates — lowest score wins.
|
|
69
|
+
*
|
|
70
|
+
* @param candidates Raw snap candidates (distance-sorted or unsorted)
|
|
71
|
+
* @param vehicleBearing Current vehicle heading in degrees (null disables bearing check)
|
|
72
|
+
* @param prevSegmentIdx Previously snapped segment index (null disables inertia)
|
|
73
|
+
* @returns Candidates sorted by score ascending, with debug metadata attached
|
|
74
|
+
*/
|
|
75
|
+
resolve(candidates: SnapCandidate[], vehicleBearing?: number | null, prevSegmentIdx?: number | null): ResolvedCandidate[];
|
|
76
|
+
/**
|
|
77
|
+
* Resolve and return only the best candidate.
|
|
78
|
+
*/
|
|
79
|
+
resolveBest(candidates: SnapCandidate[], vehicleBearing?: number | null, prevSegmentIdx?: number | null): ResolvedCandidate | null;
|
|
80
|
+
private score;
|
|
81
|
+
/**
|
|
82
|
+
* Quick utility: detect if the vehicle is likely performing a U-turn
|
|
83
|
+
* based on the difference between current bearing and route segment bearing.
|
|
84
|
+
*
|
|
85
|
+
* @returns true if the bearing difference exceeds the configured threshold
|
|
86
|
+
*/
|
|
87
|
+
isLikelyUTurn(vehicleBearing: number, segA: Coordinate, segB: Coordinate): boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Module 3: DeadReckoning
|
|
92
|
+
* Predicts vehicle position during GPS outages or stale fixes.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
interface DRAnchor {
|
|
96
|
+
coord: Coordinate;
|
|
97
|
+
idx: number;
|
|
98
|
+
ts: number;
|
|
99
|
+
}
|
|
100
|
+
interface DRPrediction {
|
|
101
|
+
predictedCoord: Coordinate;
|
|
102
|
+
isActive: boolean;
|
|
103
|
+
ageMs: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Given a snapped anchor on the route, extrapolate forward using speed and bearing.
|
|
107
|
+
*/
|
|
108
|
+
declare function computeDeadReckoningOnRoute(anchor: DRAnchor | null, speed: number, bearing: number, now: number, maxAgeMs?: number): DRPrediction;
|
|
109
|
+
/**
|
|
110
|
+
* Stateful dead reckoning engine.
|
|
111
|
+
*/
|
|
112
|
+
declare class DeadReckoningEngine {
|
|
113
|
+
private anchor;
|
|
114
|
+
private readonly maxAgeMs;
|
|
115
|
+
constructor(maxAgeMs?: number);
|
|
116
|
+
/** Set the anchor point (called when GPS is available and snapping succeeds) */
|
|
117
|
+
setAnchor(coord: Coordinate, idx: number, timestamp: number): void;
|
|
118
|
+
/** Predict position given current speed and bearing */
|
|
119
|
+
predict(speed: number, bearing: number, now: number): DRPrediction;
|
|
120
|
+
/** Clear the anchor (GPS restored) */
|
|
121
|
+
clearAnchor(): void;
|
|
122
|
+
getAnchor(): DRAnchor | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Module 4b: DeviationDetector
|
|
127
|
+
* Compound deviation detection using distance, bearing, and movement signals.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
interface DeviationSignals {
|
|
131
|
+
/** Distance from GPS to nearest route segment (metres) */
|
|
132
|
+
distanceToRoute: number;
|
|
133
|
+
/** Whether the distance exceeds the dynamic corridor */
|
|
134
|
+
outsideCorridor: boolean;
|
|
135
|
+
/** Whether the vehicle bearing diverges from the route bearing */
|
|
136
|
+
bearingMismatch: boolean;
|
|
137
|
+
/** Whether the vehicle is moving away from the next segment */
|
|
138
|
+
movingAway: boolean;
|
|
139
|
+
/** Compound confirmed: outsideCorridor AND (bearingMismatch OR movingAway) */
|
|
140
|
+
confirmed: boolean;
|
|
141
|
+
}
|
|
142
|
+
interface ClosestSegment {
|
|
143
|
+
segmentIndex: number;
|
|
144
|
+
distance: number;
|
|
145
|
+
projection: Coordinate;
|
|
146
|
+
}
|
|
147
|
+
declare class DeviationDetector {
|
|
148
|
+
/**
|
|
149
|
+
* Evaluate deviation signals for the current GPS position.
|
|
150
|
+
*/
|
|
151
|
+
evaluate(gps: Coordinate, gpsBearing: number | null, speedMps: number, closest: ClosestSegment, route: Coordinate[], corridor: number, prevDistToNextSegment: number | null): DeviationSignals;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Module 4a: DynamicCorridor
|
|
156
|
+
* Speed-adaptive corridor width calculation.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* Compute speed-adaptive corridor width in metres.
|
|
160
|
+
*
|
|
161
|
+
* Corridor widens with speed: BASE + speed * FACTOR, capped at MAX.
|
|
162
|
+
* GPS accuracy optionally widens the corridor further.
|
|
163
|
+
*/
|
|
164
|
+
declare function calculateDynamicCorridor(speedMps: number, gpsAccuracy?: number | null, baseMeters?: number, maxMeters?: number, speedFactor?: number, accuracyFactor?: number): number;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Module 5: BearingEngine
|
|
168
|
+
* Speed-adaptive IIR bearing smoothing.
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate speed-adaptive smoothing factor.
|
|
173
|
+
* Higher speed → less smoothing (alpha closer to 1).
|
|
174
|
+
* Lower speed → more smoothing (alpha closer to base value).
|
|
175
|
+
*/
|
|
176
|
+
declare function getSpeedAdaptiveAlpha(speedMps: number, baseAlpha?: number): number;
|
|
177
|
+
/**
|
|
178
|
+
* Compute bearing from the route geometry at the current segment.
|
|
179
|
+
*/
|
|
180
|
+
declare function calculateRobustBearing(route: Coordinate[], currentIdx: number, lookAhead?: number): number;
|
|
181
|
+
/**
|
|
182
|
+
* Blend two bearings using the shortest angular path.
|
|
183
|
+
*/
|
|
184
|
+
declare function blendBearing(current: number, target: number, alpha: number): number;
|
|
185
|
+
/**
|
|
186
|
+
* Full bearing smoothing pipeline: route bearing → IIR blend → output.
|
|
187
|
+
*/
|
|
188
|
+
declare function applyBearingSmoothing(currentBearing: number, route: Coordinate[], routeIdx: number, speedMps: number, gpsBearing: number | null, baseAlpha?: number): number;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Module 6: ProgressTracker
|
|
192
|
+
* Monotonic route progress tracking with anti-jitter.
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate a progress update: ensures monotonic forward progress with jitter tolerance.
|
|
197
|
+
*
|
|
198
|
+
* @returns The new progress index (may be the same as current if update is rejected)
|
|
199
|
+
*/
|
|
200
|
+
declare function validateProgressUpdate(currentIdx: number, candidateIdx: number, route: Coordinate[], distToRoute: number, maxSnapDistance: number): number;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* InstructionEditor — Fluent builder for NavInstruction arrays.
|
|
204
|
+
*
|
|
205
|
+
* Create, edit, and annotate navigation instructions for custom routes.
|
|
206
|
+
* Supports standard turn instructions and exam-specific annotations.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const editor = new InstructionEditor();
|
|
211
|
+
* editor
|
|
212
|
+
* .addTurn([2.347, 48.859], 'Turn left onto Rue de Rivoli', 42)
|
|
213
|
+
* .addExamPoint([2.350, 48.860], 'Check mirrors before changing lane', 50)
|
|
214
|
+
* .addCheckpoint([2.355, 48.861], 65);
|
|
215
|
+
*
|
|
216
|
+
* editor.attachToCumulative(routeGeometry);
|
|
217
|
+
* engine.setRoute(routeGeometry, editor.toArray());
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
type InstructionType = 'turn' | 'exam_point' | 'checkpoint' | 'depart' | 'arrive' | 'custom';
|
|
222
|
+
interface RichInstruction extends NavInstruction {
|
|
223
|
+
/** Instruction category */
|
|
224
|
+
type: InstructionType;
|
|
225
|
+
/** Unique id for referencing in edit operations */
|
|
226
|
+
id: string;
|
|
227
|
+
/** Display text (same as NavInstruction.text) */
|
|
228
|
+
text: string;
|
|
229
|
+
/** Whether to suppress voice read-out */
|
|
230
|
+
silent?: boolean;
|
|
231
|
+
/** Exam-specific severity level */
|
|
232
|
+
severity?: 'info' | 'warning' | 'critical';
|
|
233
|
+
/** Custom metadata for integrators */
|
|
234
|
+
meta?: Record<string, unknown>;
|
|
235
|
+
}
|
|
236
|
+
declare class InstructionEditor {
|
|
237
|
+
private instructions;
|
|
238
|
+
/** Add a turn instruction at the given route geometry index */
|
|
239
|
+
addTurn(location: Coordinate, text: string, geometryIndex: number, opts?: {
|
|
240
|
+
silent?: boolean;
|
|
241
|
+
meta?: Record<string, unknown>;
|
|
242
|
+
}): this;
|
|
243
|
+
/** Add an exam-specific point (e.g. "Check mirrors", "Yield here") */
|
|
244
|
+
addExamPoint(location: Coordinate, text: string, geometryIndex: number, opts?: {
|
|
245
|
+
severity?: RichInstruction['severity'];
|
|
246
|
+
silent?: boolean;
|
|
247
|
+
meta?: Record<string, unknown>;
|
|
248
|
+
}): this;
|
|
249
|
+
/** Add a silent progress checkpoint (does not trigger voice) */
|
|
250
|
+
addCheckpoint(location: Coordinate, geometryIndex: number, meta?: Record<string, unknown>): this;
|
|
251
|
+
/** Add a departure instruction (auto-inserted at index 0) */
|
|
252
|
+
addDepart(location: Coordinate, text?: string): this;
|
|
253
|
+
/** Add an arrival instruction */
|
|
254
|
+
addArrive(location: Coordinate, text?: string): this;
|
|
255
|
+
/** Update any field of an instruction by id */
|
|
256
|
+
update(id: string, patch: Partial<Omit<RichInstruction, 'id'>>): this;
|
|
257
|
+
/** Remove an instruction by id */
|
|
258
|
+
remove(id: string): this;
|
|
259
|
+
/** Move an instruction to a new position in the list */
|
|
260
|
+
reorder(id: string, newIndex: number): this;
|
|
261
|
+
/** Sort instructions by geometryIndex ascending */
|
|
262
|
+
sort(): this;
|
|
263
|
+
/** Remove all instructions */
|
|
264
|
+
clear(): this;
|
|
265
|
+
/**
|
|
266
|
+
* Compute and attach `cumulativeOffset` and `bearingBefore`/`bearingAfter`
|
|
267
|
+
* from the route geometry. Call this after loading the route but before
|
|
268
|
+
* passing instructions to the engine.
|
|
269
|
+
*/
|
|
270
|
+
attachToCumulative(route: Coordinate[]): this;
|
|
271
|
+
/** Export as plain NavInstruction[] for use with engine.setRoute() */
|
|
272
|
+
toArray(): NavInstruction[];
|
|
273
|
+
/** Export rich instructions with all metadata */
|
|
274
|
+
toRichArray(): ReadonlyArray<RichInstruction>;
|
|
275
|
+
get size(): number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Module 8: RouteOffsetPredictor
|
|
280
|
+
* Linear distance-based position predictor for smooth rendering between GPS fixes.
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Advance the predictor's offset by speed * dt.
|
|
285
|
+
*/
|
|
286
|
+
declare function advancePredictor(currentOffset: number, speedMps: number, dtSec: number): number;
|
|
287
|
+
/**
|
|
288
|
+
* Correct the predictor when a new GPS fix arrives.
|
|
289
|
+
* Blend towards the actual measured offset using betaCorrection.
|
|
290
|
+
*/
|
|
291
|
+
declare function applyGpsCorrection(predictedOffset: number, measuredOffset: number, betaCorrection?: number): number;
|
|
292
|
+
/**
|
|
293
|
+
* Given a linear offset (metres from route start), find the [lng, lat] on the route.
|
|
294
|
+
*
|
|
295
|
+
* @param offset Linear distance from route start in metres
|
|
296
|
+
* @param cumulativeDistances Forward cumulative distance array
|
|
297
|
+
* @param route Route coordinates
|
|
298
|
+
* @returns Interpolated coordinate on the route
|
|
299
|
+
*/
|
|
300
|
+
declare function pointAtOffset(offset: number, cumulativeDistances: Float64Array, route: Coordinate[]): Coordinate;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* VoiceTriggerEngine
|
|
304
|
+
*
|
|
305
|
+
* Converts NavCore instruction events into timed voice cues.
|
|
306
|
+
* Handles deduplication, look-ahead windows, and priority levels.
|
|
307
|
+
* Consumer calls TTS — this engine only computes WHEN to speak.
|
|
308
|
+
*
|
|
309
|
+
* FEATURE: VOICE_TRIGGERS
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* const voice = new VoiceTriggerEngine({ earlyTriggerMeters: 150 });
|
|
314
|
+
* voice.setInstructions(instructions);
|
|
315
|
+
*
|
|
316
|
+
* engine.on('instruction', (instr) => {
|
|
317
|
+
* const cue = voice.onInstruction(instr, state);
|
|
318
|
+
* if (cue) speakText(cue.text);
|
|
319
|
+
* });
|
|
320
|
+
*
|
|
321
|
+
* // Or poll each GPS tick:
|
|
322
|
+
* const cue = voice.update(state);
|
|
323
|
+
* if (cue) speakText(cue.text);
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
type VoicePriority = 'low' | 'normal' | 'high' | 'urgent';
|
|
328
|
+
interface VoiceCue {
|
|
329
|
+
/** The instruction that triggered this cue */
|
|
330
|
+
instruction: NavInstruction;
|
|
331
|
+
/** Text to pass to TTS */
|
|
332
|
+
text: string;
|
|
333
|
+
/** Distance to the instruction point in metres */
|
|
334
|
+
distanceMetres: number;
|
|
335
|
+
/** Priority hint for TTS queue management */
|
|
336
|
+
priority: VoicePriority;
|
|
337
|
+
/** Monotonic trigger ID — use this to deduplicate */
|
|
338
|
+
triggerId: string;
|
|
339
|
+
}
|
|
340
|
+
interface VoiceTriggerOptions {
|
|
341
|
+
/**
|
|
342
|
+
* Trigger voice this many metres before the instruction point.
|
|
343
|
+
* Default: 120m (comfortable at 50 km/h).
|
|
344
|
+
*/
|
|
345
|
+
earlyTriggerMeters?: number;
|
|
346
|
+
/**
|
|
347
|
+
* Stop trying to trigger if vehicle is already past this many metres.
|
|
348
|
+
* Default: 20m.
|
|
349
|
+
*/
|
|
350
|
+
lateTriggerMeters?: number;
|
|
351
|
+
/**
|
|
352
|
+
* Re-announce if user is still approaching after this distance.
|
|
353
|
+
* Set to 0 to disable re-announcement. Default: 300m.
|
|
354
|
+
*/
|
|
355
|
+
reannounceMeters?: number;
|
|
356
|
+
}
|
|
357
|
+
declare class VoiceTriggerEngine {
|
|
358
|
+
private instructions;
|
|
359
|
+
private triggeredIds;
|
|
360
|
+
private reannouncedIds;
|
|
361
|
+
private readonly options;
|
|
362
|
+
constructor(options?: VoiceTriggerOptions);
|
|
363
|
+
/** Load the instruction set (call after engine.setRoute) */
|
|
364
|
+
setInstructions(instructions: NavInstruction[]): void;
|
|
365
|
+
/**
|
|
366
|
+
* Poll on every GPS state update.
|
|
367
|
+
* Returns a `VoiceCue` when an instruction should be spoken, or null.
|
|
368
|
+
*/
|
|
369
|
+
update(state: NavCoreState): VoiceCue | null;
|
|
370
|
+
/**
|
|
371
|
+
* Called directly from engine's 'instruction' event.
|
|
372
|
+
* Always fires immediately regardless of distance window.
|
|
373
|
+
*/
|
|
374
|
+
onInstruction(instr: NavInstruction, state: NavCoreState): VoiceCue;
|
|
375
|
+
/** Reset triggered state (call on setRoute / new journey) */
|
|
376
|
+
reset(): void;
|
|
377
|
+
private instrId;
|
|
378
|
+
private makeCue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* ETAEngine — Local ETA computation. Zero API calls.
|
|
383
|
+
*
|
|
384
|
+
* Computes Estimated Time of Arrival from current NavCore state using
|
|
385
|
+
* a rolling speed average. No server required — entirely offline.
|
|
386
|
+
*
|
|
387
|
+
* FEATURE: ETA
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* const eta = new ETAEngine({ speedWindowSize: 8 });
|
|
392
|
+
* engine.on('update', (state) => {
|
|
393
|
+
* const result = eta.update(state);
|
|
394
|
+
* console.log(`ETA: ${result.etaSeconds}s, ${result.distanceRemainingM.toFixed(0)}m`);
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
interface ETAResult {
|
|
400
|
+
/** ETA in seconds from now */
|
|
401
|
+
etaSeconds: number;
|
|
402
|
+
/** ETA as absolute timestamp (Date.now() + etaSeconds * 1000) */
|
|
403
|
+
etaMs: number;
|
|
404
|
+
/** Remaining distance in metres */
|
|
405
|
+
distanceRemainingM: number;
|
|
406
|
+
/** Rolling average speed in m/s */
|
|
407
|
+
averageSpeedMs: number;
|
|
408
|
+
/** Whether the estimate is reliable (enough speed samples + non-zero speed) */
|
|
409
|
+
isReliable: boolean;
|
|
410
|
+
}
|
|
411
|
+
interface ETAOptions {
|
|
412
|
+
/**
|
|
413
|
+
* Number of speed samples in the rolling average window.
|
|
414
|
+
* More samples = smoother but slower to react. Default: 6.
|
|
415
|
+
*/
|
|
416
|
+
speedWindowSize?: number;
|
|
417
|
+
/**
|
|
418
|
+
* Minimum speed (m/s) below which we use the last reliable speed
|
|
419
|
+
* rather than 0 (avoids Infinity ETA while stopped). Default: 0.5 m/s (~2 km/h).
|
|
420
|
+
*/
|
|
421
|
+
minimumSpeedMs?: number;
|
|
422
|
+
/**
|
|
423
|
+
* Speed to assume if no reliable samples yet. Default: 8.3 m/s (30 km/h).
|
|
424
|
+
*/
|
|
425
|
+
fallbackSpeedMs?: number;
|
|
426
|
+
}
|
|
427
|
+
declare class ETAEngine {
|
|
428
|
+
private readonly options;
|
|
429
|
+
private speedSamples;
|
|
430
|
+
private lastReliableSpeed;
|
|
431
|
+
constructor(options?: ETAOptions);
|
|
432
|
+
/**
|
|
433
|
+
* Feed a NavCore state snapshot and receive an ETA estimate.
|
|
434
|
+
* Call this on every GPS update tick.
|
|
435
|
+
*/
|
|
436
|
+
update(state: NavCoreState): ETAResult;
|
|
437
|
+
/** Reset the speed window (call on new route or significant GPS gap) */
|
|
438
|
+
reset(): void;
|
|
439
|
+
private rollingAverage;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* GeofencingEngine
|
|
444
|
+
*
|
|
445
|
+
* Circular and polygon geofences — zero server dependency.
|
|
446
|
+
* Uses haversine distance for circles and ray-casting for polygons.
|
|
447
|
+
*
|
|
448
|
+
* FEATURE: GEOFENCING
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* const geo = new GeofencingEngine();
|
|
453
|
+
* geo.addCircle('school-zone', [2.347, 48.859], 50);
|
|
454
|
+
* geo.addPolygon('exam-area', [[2.34, 48.85], [2.36, 48.85], [2.36, 48.87], [2.34, 48.87]]);
|
|
455
|
+
*
|
|
456
|
+
* engine.on('update', ({ snappedCoord }) => {
|
|
457
|
+
* const events = geo.update(snappedCoord);
|
|
458
|
+
* for (const evt of events) {
|
|
459
|
+
* console.log(`${evt.type}: ${evt.id}`);
|
|
460
|
+
* }
|
|
461
|
+
* });
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
type GeofenceType = 'circle' | 'polygon';
|
|
466
|
+
type GeofenceEventType = 'enter' | 'exit' | 'dwell';
|
|
467
|
+
interface GeofenceEvent {
|
|
468
|
+
/** Geofence ID that triggered the event */
|
|
469
|
+
id: string;
|
|
470
|
+
type: GeofenceEventType;
|
|
471
|
+
/** Coordinate that triggered the event */
|
|
472
|
+
coord: Coordinate;
|
|
473
|
+
/** Milliseconds spent inside (for dwell events) */
|
|
474
|
+
dwellMs?: number;
|
|
475
|
+
/** Custom metadata passed at creation */
|
|
476
|
+
meta?: unknown;
|
|
477
|
+
}
|
|
478
|
+
interface GeofencingOptions {
|
|
479
|
+
/**
|
|
480
|
+
* Milliseconds a vehicle must remain inside a geofence before a 'dwell' event fires.
|
|
481
|
+
* Default: 10000ms (10 seconds).
|
|
482
|
+
*/
|
|
483
|
+
dwellThresholdMs?: number;
|
|
484
|
+
}
|
|
485
|
+
declare class GeofencingEngine {
|
|
486
|
+
private fences;
|
|
487
|
+
private states;
|
|
488
|
+
private readonly options;
|
|
489
|
+
constructor(options?: GeofencingOptions);
|
|
490
|
+
/** Add a circular geofence. Returns the ID. */
|
|
491
|
+
addCircle(id: string, center: Coordinate, radiusM: number, meta?: unknown): string;
|
|
492
|
+
/**
|
|
493
|
+
* Add a polygon geofence. Coords are [lng, lat] pairs.
|
|
494
|
+
* The polygon is auto-closed (first == last not required).
|
|
495
|
+
*/
|
|
496
|
+
addPolygon(id: string, coords: Coordinate[], meta?: unknown): string;
|
|
497
|
+
/** Remove a geofence by ID */
|
|
498
|
+
remove(id: string): void;
|
|
499
|
+
/** Remove all geofences */
|
|
500
|
+
clear(): void;
|
|
501
|
+
/** List all registered geofence IDs */
|
|
502
|
+
listIds(): string[];
|
|
503
|
+
/**
|
|
504
|
+
* Feed the current coordinate and receive a (possibly empty) list of events.
|
|
505
|
+
* Call this on every NavCore state update.
|
|
506
|
+
*/
|
|
507
|
+
update(coord: Coordinate): GeofenceEvent[];
|
|
508
|
+
/** Check if a coordinate is currently inside a named geofence */
|
|
509
|
+
isInside(id: string, coord: Coordinate): boolean;
|
|
510
|
+
private contains;
|
|
511
|
+
/**
|
|
512
|
+
* Ray-casting point-in-polygon algorithm.
|
|
513
|
+
* Works in [lng, lat] space (sufficient for small areas; use projected
|
|
514
|
+
* coords for very large polygons spanning many degrees).
|
|
515
|
+
*/
|
|
516
|
+
private pointInPolygon;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export { DeadReckoningEngine, DeviationDetector, ETAEngine, type ETAOptions, type ETAResult, type GeofenceEvent, type GeofenceEventType, type GeofenceType, GeofencingEngine, type GeofencingOptions, InstructionEditor, type InstructionType, NavInstruction, ParallelRoadResolver, type ParallelRoadResolverOptions, type ResolvedCandidate, type RichInstruction, type SnapCandidate, type VoiceCue, type VoicePriority, VoiceTriggerEngine, type VoiceTriggerOptions, advancePredictor, applyBearingSmoothing, applyGpsCorrection, blendBearing, calculateDynamicCorridor, calculateRobustBearing, computeDeadReckoningOnRoute, getSpeedAdaptiveAlpha, pointAtOffset, validateProgressUpdate };
|
package/dist/pro.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
'use strict';function h(o,e,t,n){let i=(t-o)*(Math.PI/180),r=(n-e)*(Math.PI/180),s=Math.sin(i/2)*Math.sin(i/2)+Math.cos(o*(Math.PI/180))*Math.cos(t*(Math.PI/180))*Math.sin(r/2)*Math.sin(r/2);return 6371e3*(2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s)))}function P(o,e,t,n){let i=n/6371e3,r=t*(Math.PI/180),s=o*(Math.PI/180),c=e*(Math.PI/180),a=Math.asin(Math.sin(s)*Math.cos(i)+Math.cos(s)*Math.sin(i)*Math.cos(r));return [((c+Math.atan2(Math.sin(r)*Math.sin(i)*Math.cos(s),Math.cos(i)-Math.sin(s)*Math.sin(a)))*180/Math.PI+540)%360-180,a*180/Math.PI]}function w(o,e,t,n,i=5e3){if(!o||e<.5)return {predictedCoord:o?.coord??[0,0],isActive:false,ageMs:0};let r=n-o.ts;if(r>i||r<0)return {predictedCoord:o.coord,isActive:false,ageMs:r};let s=e*(r/1e3);return {predictedCoord:P(o.coord[1],o.coord[0],t,s),isActive:true,ageMs:r}}var M=class{constructor(e=5e3){this.anchor=null;this.maxAgeMs=e;}setAnchor(e,t,n){this.anchor={coord:e,idx:t,ts:n};}predict(e,t,n){return w(this.anchor,e,t,n,this.maxAgeMs)}clearAnchor(){this.anchor=null;}getAnchor(){return this.anchor}};function p(o){let e=o%360;return e>180?e-360:e<-180?e+360:e}function l(o,e){let t=o[1]*(Math.PI/180),n=o[0]*(Math.PI/180),i=e[1]*(Math.PI/180),r=e[0]*(Math.PI/180),s=Math.sin(r-n)*Math.cos(i),c=Math.cos(t)*Math.sin(i)-Math.sin(t)*Math.cos(i)*Math.cos(r-n);return (Math.atan2(s,c)*180/Math.PI+360)%360}function G(o,e,t){let n=(i,r,s)=>i+(r-i)*s;return [n(o[0],e[0],t),n(o[1],e[1],t)]}var T=class{evaluate(e,t,n,i,r,s,c){let a=i.distance>s,u=false;if(t!==null&&i.segmentIndex<r.length-1){let m=l(r[i.segmentIndex],r[i.segmentIndex+1]);u=Math.abs(p(t-m))>90;}let d=false;if(c!==null&&n>1.5){let m=r[Math.min(i.segmentIndex+2,r.length-1)];d=h(e[1],e[0],m[1],m[0])>c+5;}let R=a&&(u||d);return {distanceToRoute:i.distance,outsideCorridor:a,bearingMismatch:u,movingAway:d,confirmed:R}}};function B(o,e=null,t=25,n=60,i=1.2,r=.5){let s=t+o*i;return e!==null&&e>0&&(s+=e*r),Math.min(s,n)}function U(o,e=.3){return o<1.5?e*.5:o>20?Math.min(.9,e*2):e}function L(o,e,t=3){let n=Math.min(e+t,o.length-1);return e>=o.length-1||e===n?e>0?l(o[e-1],o[e]):0:l(o[e],o[n])}function E(o,e,t){let n=p(e-o);return (o+t*n+360)%360}function J(o,e,t,n,i,r=.3){let s=U(n,r),c=L(e,t);if(i!==null&&n>1.5){let a=E(o,i,s*.3);return E(a,c,s)}return E(o,c,s)}function q(o,e,t,n,i){return e<o||e>o+30||n>i?o:e}var z=20,W=60,$=3;function X(o,e,t,n,i,r,s,c=0){if(!e||e.length===0||t>=e.length)return {nextIdx:t,instruction:null,wasTriggered:false};let a=t;for(;a<e.length;){let g=e[a],b=o>g.geometryIndex+30,I=g.cumulativeOffset!==void 0&&s>g.cumulativeOffset+15;if(!b&&!I)break;a++;}if(a>=e.length)return {nextIdx:a,instruction:null,wasTriggered:false};let u=e[a],d=u.cumulativeOffset!==void 0?Math.max(0,u.cumulativeOffset-s):h(i[1],i[0],u.location[1],u.location[0]),R=Math.min(W,Math.max(z,c*$)),m=o<=u.geometryIndex+2,V=d<=R&&m,D=false;if(V){let g=true,b=u.bearingAfter??(u.geometryIndex<n.length-1?l(n[u.geometryIndex],n[u.geometryIndex+1]):null),I=u.bearingBefore??(u.geometryIndex>0?l(n[u.geometryIndex-1],n[u.geometryIndex]):null);I!==null&&b!==null&&r!==null&&Math.abs(p(b-I))>30&&Math.abs(p(r-b))>55&&(g=false),g&&(D=true);}return {nextIdx:a,instruction:u,wasTriggered:D}}var Y=0;function x(){return `instr_${Date.now()}_${++Y}`}var _=class{constructor(){this.instructions=[];}addTurn(e,t,n,i){return this.instructions.push({id:x(),type:"turn",location:e,text:t,instruction:t,geometryIndex:n,silent:i?.silent,meta:i?.meta}),this}addExamPoint(e,t,n,i){return this.instructions.push({id:x(),type:"exam_point",location:e,text:t,instruction:t,geometryIndex:n,severity:i?.severity??"info",silent:i?.silent,meta:i?.meta}),this}addCheckpoint(e,t,n){return this.instructions.push({id:x(),type:"checkpoint",location:e,text:"",instruction:"",geometryIndex:t,silent:true,meta:n}),this}addDepart(e,t="Start navigation"){let n={id:x(),type:"depart",location:e,text:t,instruction:t,geometryIndex:0};return this.instructions.unshift(n),this}addArrive(e,t="You have arrived"){return this.instructions.push({id:x(),type:"arrive",location:e,text:t,instruction:t,geometryIndex:Number.MAX_SAFE_INTEGER}),this}update(e,t){let n=this.instructions.findIndex(i=>i.id===e);if(n===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);return this.instructions[n]={...this.instructions[n],...t},this}remove(e){let t=this.instructions.findIndex(n=>n.id===e);if(t===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);return this.instructions.splice(t,1),this}reorder(e,t){let n=this.instructions.findIndex(r=>r.id===e);if(n===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);let[i]=this.instructions.splice(n,1);return this.instructions.splice(Math.max(0,t),0,i),this}sort(){return this.instructions.sort((e,t)=>e.geometryIndex-t.geometryIndex),this}clear(){return this.instructions=[],this}attachToCumulative(e){if(e.length<2)return this;let t=[0];for(let n=1;n<e.length;n++){let i=e[n-1],r=e[n];t.push(t[n-1]+h(i[1],i[0],r[1],r[0]));}for(let n of this.instructions){let i=Math.min(n.geometryIndex,e.length-1);n.cumulativeOffset=t[i]??0,i>0&&i<e.length-1&&(n.bearingBefore=l(e[i-1],e[i]),n.bearingAfter=l(e[i],e[i+1]));}return this}toArray(){return this.instructions.map(({id:e,type:t,severity:n,meta:i,...r})=>r)}toRichArray(){return [...this.instructions]}get size(){return this.instructions.length}};function K(o,e,t){return o+e*t}function Q(o,e,t=.15){let n=e-o;return o+t*n}function Z(o,e,t){if(t.length===0)return [0,0];if(o<=0)return t[0];let n=e[e.length-1]??0;if(o>=n)return t[t.length-1];let i=0,r=e.length-1;for(;i<r-1;){let d=i+r>>1;e[d]<=o?i=d:r=d;}let s=e[i],a=e[r]-s;if(a===0)return t[i];let u=(o-s)/a;return G(t[i],t[r],u)}var O=class{constructor(){this._state="IDLE";this._context={resumeIndex:null};}get state(){return this._state}get context(){return {...this._context}}dispatch(e){switch(this._state){case "IDLE":e.type==="ROUTE_LOADED"&&(this._state="OVERVIEW"),e.type==="USER_START_NAVIGATION"&&(this._state="APPROACHING_ROUTE");break;case "OVERVIEW":e.type==="USER_START_NAVIGATION"&&(this._state="APPROACHING_ROUTE");break;case "APPROACHING_ROUTE":e.type==="ROUTE_JOINED"&&(this._state="ON_ROUTE",this._context.resumeIndex=e.data.index),e.type==="NAVIGATION_FINISHED"&&(this._state="IDLE");break;case "ON_ROUTE":e.type==="NAVIGATION_FINISHED"&&(this._state="IDLE"),e.type==="ROUTE_JOINED"&&(this._context.resumeIndex=e.data.index);break}return (e.type==="RESET"||e.type==="CANCEL_NAVIGATION")&&(this._state="IDLE",this._context={resumeIndex:null}),{state:this._state,context:{...this._context}}}reset(){this._state="IDLE",this._context={resumeIndex:null};}};var f={deviationSegmentIdx:null,deviationStartMs:null,tempRoute:null,rejoinTarget:null,lastRerouteMs:null,rejoinedAtMs:null,rejoinDwellFrames:0,failureReason:null},k=2e3,ee=3,C=class{constructor(){this._state="ON_ROUTE";this._context={...f};}get state(){return this._state}get context(){return {...this._context}}dispatch(e,t){let n=t??Date.now();switch(this._state){case "ON_ROUTE":e.type==="DEVIATION_SUSPECTED"&&(this._state="DEVIATING",this._context.deviationSegmentIdx=e.deviationSegmentIdx,this._context.deviationStartMs=n);break;case "DEVIATING":e.type==="DEVIATION_CONFIRMED"&&(this._context.deviationStartMs!==null?n-this._context.deviationStartMs:0)>=k&&(this._state="OFF_ROUTE",this._context.deviationSegmentIdx=e.deviationSegmentIdx),(e.type==="RESUME"||e.type==="RESET")&&(this._state="ON_ROUTE",e.type==="RESET"?this._context={...f}:(this._context.deviationSegmentIdx=null,this._context.deviationStartMs=null));break;case "OFF_ROUTE":e.type==="REROUTE_STARTED"&&(this._state="REROUTING",this._context.lastRerouteMs=n),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "REROUTING":e.type==="REROUTE_SUCCESS"&&(this._state="REJOINING",this._context.tempRoute=e.tempRoute,this._context.rejoinTarget=e.rejoinTarget),e.type==="REROUTE_FAILED"&&(this._state="FAILED",this._context.failureReason=e.reason);break;case "REJOINING":e.type==="REJOIN_DETECTED"&&this._context.rejoinDwellFrames++,(e.type==="REJOIN_CONFIRMED"||this._context.rejoinDwellFrames>=ee)&&(this._state="REJOINED",this._context.rejoinedAtMs=n,this._context.rejoinDwellFrames=0),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "REJOINED":e.type==="RESUME"&&(this._state="ON_ROUTE",this._context.tempRoute=null,this._context.rejoinTarget=null,this._context.rejoinedAtMs=null,this._context.deviationSegmentIdx=null,this._context.deviationStartMs=null),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "FAILED":e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break}return {state:this._state,context:{...this._context}}}reset(){this._state="ON_ROUTE",this._context={...f};}isDeviationConfirmed(e=Date.now()){return this._context.deviationStartMs?e-this._context.deviationStartMs>=k:false}};var te={bearingPenalty:80,bearingMismatchThresholdDeg:60,inertiaPenalty:40,inertiaSegmentDist:3},S=class{constructor(e){this.options={...te,...e};}resolve(e,t=null,n=null){return e.map(i=>this.score(i,t,n)).sort((i,r)=>i.score-r.score)}resolveBest(e,t=null,n=null){return e.length===0?null:this.resolve(e,t,n)[0]??null}score(e,t,n){let i=e.distanceMetres,r=null,s=false;if(t!==null){let c=l(e.segA,e.segB),a=Math.abs(p(t-c));r=a,a>this.options.bearingMismatchThresholdDeg&&(i+=this.options.bearingPenalty);}return n!==null&&Math.abs(e.segmentIndex-n)>=this.options.inertiaSegmentDist&&(i+=this.options.inertiaPenalty,s=true),{...e,score:i,bearingDeltaDeg:r,inertiaPenaltyApplied:s}}isLikelyUTurn(e,t,n){let i=l(t,n);return Math.abs(p(e-i))>this.options.bearingMismatchThresholdDeg}};var ne={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},A=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...ne,...e};}setInstructions(e){this.instructions=[...e],this.triggeredIds.clear(),this.reannouncedIds.clear();}update(e){let{distanceToNextInstruction:t,nextInstructionIndex:n}=e;if(n===null||t===null||n>=this.instructions.length)return null;let i=this.instructions[n],r=this.instrId(n),s=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&s<=this.options.reannounceMeters&&s>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,s,"low")):null;if(s>this.options.earlyTriggerMeters)return null;if(s<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=s<30?"urgent":s<60?"high":"normal";return this.makeCue(i,s,c)}onInstruction(e,t){let n=t.distanceToNextInstruction??0;return this.makeCue(e,n,"high")}reset(){this.triggeredIds.clear(),this.reannouncedIds.clear();}instrId(e){let t=this.instructions[e];return `${e}_${t?.geometryIndex??0}`}makeCue(e,t,n){return {instruction:e,text:e.text??e.instruction??"",distanceMetres:Math.max(0,t),priority:n,triggerId:`${e.geometryIndex}_${t.toFixed(0)}`}}};var ie={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},v=class{constructor(e){this.speedSamples=[];this.options={...ie,...e},this.lastReliableSpeed=this.options.fallbackSpeedMs;}update(e){let t=e.distanceToDestination??0,n=e.currentSpeed??0;this.speedSamples.push(n),this.speedSamples.length>this.options.speedWindowSize&&this.speedSamples.shift();let i=this.rollingAverage();i>=this.options.minimumSpeedMs&&(this.lastReliableSpeed=i);let r=i>=this.options.minimumSpeedMs?i:this.lastReliableSpeed,s=this.speedSamples.length>=Math.ceil(this.options.speedWindowSize/2)&&r>0&&t>0,c=r>0?t/r:0;return {etaSeconds:Math.round(c),etaMs:Date.now()+c*1e3,distanceRemainingM:t,averageSpeedMs:i,isReliable:s}}reset(){this.speedSamples=[],this.lastReliableSpeed=this.options.fallbackSpeedMs;}rollingAverage(){return this.speedSamples.length===0?0:this.speedSamples.reduce((t,n)=>t+n,0)/this.speedSamples.length}};var re={dwellThresholdMs:1e4},N=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...re,...e};}addCircle(e,t,n,i){return this.fences.set(e,{kind:"circle",id:e,center:t,radiusM:n,meta:i}),this.states.set(e,{inside:false,enteredAtMs:null}),e}addPolygon(e,t,n){if(t.length<3)throw new Error("[Geofencing] Polygon requires at least 3 coordinates");return this.fences.set(e,{kind:"polygon",id:e,coords:t,meta:n}),this.states.set(e,{inside:false,enteredAtMs:null}),e}remove(e){this.fences.delete(e),this.states.delete(e);}clear(){this.fences.clear(),this.states.clear();}listIds(){return Array.from(this.fences.keys())}update(e){if(!e)return [];let t=[],n=Date.now();for(let[i,r]of this.fences){let s=this.states.get(i),c=this.contains(r,e);if(!s.inside&&c)s.inside=true,s.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(s.inside&&!c){s.inside=false;let a=s.enteredAtMs?n-s.enteredAtMs:0;s.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(s.inside&&s.enteredAtMs!==null){let a=n-s.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),s.enteredAtMs=n);}}return t}isInside(e,t){let n=this.fences.get(e);return n?this.contains(n,t):false}contains(e,t){return e.kind==="circle"?h(t[1],t[0],e.center[1],e.center[0])<=e.radiusM:this.pointInPolygon(t,e.coords)}pointInPolygon(e,t){let[n,i]=e,r=false,s=t.length;for(let c=0,a=s-1;c<s;a=c++){let[u,d]=t[c],[R,m]=t[a];d>i!=m>i&&n<(R-u)*(i-d)/(m-d)+u&&(r=!r);}return r}};exports.DeadReckoningEngine=M;exports.DeviationDetector=T;exports.ETAEngine=v;exports.GeofencingEngine=N;exports.InstructionEditor=_;exports.NavigationFSM=O;exports.OffRouteFSM=C;exports.ParallelRoadResolver=S;exports.VoiceTriggerEngine=A;exports.advancePredictor=K;exports.applyBearingSmoothing=J;exports.applyGpsCorrection=Q;exports.blendBearing=E;exports.calculateDynamicCorridor=B;exports.calculateRobustBearing=L;exports.computeDeadReckoningOnRoute=w;exports.getSpeedAdaptiveAlpha=U;exports.pointAtOffset=Z;exports.resolveNextInstruction=X;exports.validateProgressUpdate=q;
|
package/dist/pro.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function h(o,e,t,n){let i=(t-o)*(Math.PI/180),r=(n-e)*(Math.PI/180),s=Math.sin(i/2)*Math.sin(i/2)+Math.cos(o*(Math.PI/180))*Math.cos(t*(Math.PI/180))*Math.sin(r/2)*Math.sin(r/2);return 6371e3*(2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s)))}function P(o,e,t,n){let i=n/6371e3,r=t*(Math.PI/180),s=o*(Math.PI/180),c=e*(Math.PI/180),a=Math.asin(Math.sin(s)*Math.cos(i)+Math.cos(s)*Math.sin(i)*Math.cos(r));return [((c+Math.atan2(Math.sin(r)*Math.sin(i)*Math.cos(s),Math.cos(i)-Math.sin(s)*Math.sin(a)))*180/Math.PI+540)%360-180,a*180/Math.PI]}function w(o,e,t,n,i=5e3){if(!o||e<.5)return {predictedCoord:o?.coord??[0,0],isActive:false,ageMs:0};let r=n-o.ts;if(r>i||r<0)return {predictedCoord:o.coord,isActive:false,ageMs:r};let s=e*(r/1e3);return {predictedCoord:P(o.coord[1],o.coord[0],t,s),isActive:true,ageMs:r}}var M=class{constructor(e=5e3){this.anchor=null;this.maxAgeMs=e;}setAnchor(e,t,n){this.anchor={coord:e,idx:t,ts:n};}predict(e,t,n){return w(this.anchor,e,t,n,this.maxAgeMs)}clearAnchor(){this.anchor=null;}getAnchor(){return this.anchor}};function p(o){let e=o%360;return e>180?e-360:e<-180?e+360:e}function l(o,e){let t=o[1]*(Math.PI/180),n=o[0]*(Math.PI/180),i=e[1]*(Math.PI/180),r=e[0]*(Math.PI/180),s=Math.sin(r-n)*Math.cos(i),c=Math.cos(t)*Math.sin(i)-Math.sin(t)*Math.cos(i)*Math.cos(r-n);return (Math.atan2(s,c)*180/Math.PI+360)%360}function G(o,e,t){let n=(i,r,s)=>i+(r-i)*s;return [n(o[0],e[0],t),n(o[1],e[1],t)]}var T=class{evaluate(e,t,n,i,r,s,c){let a=i.distance>s,u=false;if(t!==null&&i.segmentIndex<r.length-1){let m=l(r[i.segmentIndex],r[i.segmentIndex+1]);u=Math.abs(p(t-m))>90;}let d=false;if(c!==null&&n>1.5){let m=r[Math.min(i.segmentIndex+2,r.length-1)];d=h(e[1],e[0],m[1],m[0])>c+5;}let R=a&&(u||d);return {distanceToRoute:i.distance,outsideCorridor:a,bearingMismatch:u,movingAway:d,confirmed:R}}};function B(o,e=null,t=25,n=60,i=1.2,r=.5){let s=t+o*i;return e!==null&&e>0&&(s+=e*r),Math.min(s,n)}function U(o,e=.3){return o<1.5?e*.5:o>20?Math.min(.9,e*2):e}function L(o,e,t=3){let n=Math.min(e+t,o.length-1);return e>=o.length-1||e===n?e>0?l(o[e-1],o[e]):0:l(o[e],o[n])}function E(o,e,t){let n=p(e-o);return (o+t*n+360)%360}function J(o,e,t,n,i,r=.3){let s=U(n,r),c=L(e,t);if(i!==null&&n>1.5){let a=E(o,i,s*.3);return E(a,c,s)}return E(o,c,s)}function q(o,e,t,n,i){return e<o||e>o+30||n>i?o:e}var z=20,W=60,$=3;function X(o,e,t,n,i,r,s,c=0){if(!e||e.length===0||t>=e.length)return {nextIdx:t,instruction:null,wasTriggered:false};let a=t;for(;a<e.length;){let g=e[a],b=o>g.geometryIndex+30,I=g.cumulativeOffset!==void 0&&s>g.cumulativeOffset+15;if(!b&&!I)break;a++;}if(a>=e.length)return {nextIdx:a,instruction:null,wasTriggered:false};let u=e[a],d=u.cumulativeOffset!==void 0?Math.max(0,u.cumulativeOffset-s):h(i[1],i[0],u.location[1],u.location[0]),R=Math.min(W,Math.max(z,c*$)),m=o<=u.geometryIndex+2,V=d<=R&&m,D=false;if(V){let g=true,b=u.bearingAfter??(u.geometryIndex<n.length-1?l(n[u.geometryIndex],n[u.geometryIndex+1]):null),I=u.bearingBefore??(u.geometryIndex>0?l(n[u.geometryIndex-1],n[u.geometryIndex]):null);I!==null&&b!==null&&r!==null&&Math.abs(p(b-I))>30&&Math.abs(p(r-b))>55&&(g=false),g&&(D=true);}return {nextIdx:a,instruction:u,wasTriggered:D}}var Y=0;function x(){return `instr_${Date.now()}_${++Y}`}var _=class{constructor(){this.instructions=[];}addTurn(e,t,n,i){return this.instructions.push({id:x(),type:"turn",location:e,text:t,instruction:t,geometryIndex:n,silent:i?.silent,meta:i?.meta}),this}addExamPoint(e,t,n,i){return this.instructions.push({id:x(),type:"exam_point",location:e,text:t,instruction:t,geometryIndex:n,severity:i?.severity??"info",silent:i?.silent,meta:i?.meta}),this}addCheckpoint(e,t,n){return this.instructions.push({id:x(),type:"checkpoint",location:e,text:"",instruction:"",geometryIndex:t,silent:true,meta:n}),this}addDepart(e,t="Start navigation"){let n={id:x(),type:"depart",location:e,text:t,instruction:t,geometryIndex:0};return this.instructions.unshift(n),this}addArrive(e,t="You have arrived"){return this.instructions.push({id:x(),type:"arrive",location:e,text:t,instruction:t,geometryIndex:Number.MAX_SAFE_INTEGER}),this}update(e,t){let n=this.instructions.findIndex(i=>i.id===e);if(n===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);return this.instructions[n]={...this.instructions[n],...t},this}remove(e){let t=this.instructions.findIndex(n=>n.id===e);if(t===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);return this.instructions.splice(t,1),this}reorder(e,t){let n=this.instructions.findIndex(r=>r.id===e);if(n===-1)throw new Error(`[InstructionEditor] Instruction ${e} not found`);let[i]=this.instructions.splice(n,1);return this.instructions.splice(Math.max(0,t),0,i),this}sort(){return this.instructions.sort((e,t)=>e.geometryIndex-t.geometryIndex),this}clear(){return this.instructions=[],this}attachToCumulative(e){if(e.length<2)return this;let t=[0];for(let n=1;n<e.length;n++){let i=e[n-1],r=e[n];t.push(t[n-1]+h(i[1],i[0],r[1],r[0]));}for(let n of this.instructions){let i=Math.min(n.geometryIndex,e.length-1);n.cumulativeOffset=t[i]??0,i>0&&i<e.length-1&&(n.bearingBefore=l(e[i-1],e[i]),n.bearingAfter=l(e[i],e[i+1]));}return this}toArray(){return this.instructions.map(({id:e,type:t,severity:n,meta:i,...r})=>r)}toRichArray(){return [...this.instructions]}get size(){return this.instructions.length}};function K(o,e,t){return o+e*t}function Q(o,e,t=.15){let n=e-o;return o+t*n}function Z(o,e,t){if(t.length===0)return [0,0];if(o<=0)return t[0];let n=e[e.length-1]??0;if(o>=n)return t[t.length-1];let i=0,r=e.length-1;for(;i<r-1;){let d=i+r>>1;e[d]<=o?i=d:r=d;}let s=e[i],a=e[r]-s;if(a===0)return t[i];let u=(o-s)/a;return G(t[i],t[r],u)}var O=class{constructor(){this._state="IDLE";this._context={resumeIndex:null};}get state(){return this._state}get context(){return {...this._context}}dispatch(e){switch(this._state){case "IDLE":e.type==="ROUTE_LOADED"&&(this._state="OVERVIEW"),e.type==="USER_START_NAVIGATION"&&(this._state="APPROACHING_ROUTE");break;case "OVERVIEW":e.type==="USER_START_NAVIGATION"&&(this._state="APPROACHING_ROUTE");break;case "APPROACHING_ROUTE":e.type==="ROUTE_JOINED"&&(this._state="ON_ROUTE",this._context.resumeIndex=e.data.index),e.type==="NAVIGATION_FINISHED"&&(this._state="IDLE");break;case "ON_ROUTE":e.type==="NAVIGATION_FINISHED"&&(this._state="IDLE"),e.type==="ROUTE_JOINED"&&(this._context.resumeIndex=e.data.index);break}return (e.type==="RESET"||e.type==="CANCEL_NAVIGATION")&&(this._state="IDLE",this._context={resumeIndex:null}),{state:this._state,context:{...this._context}}}reset(){this._state="IDLE",this._context={resumeIndex:null};}};var f={deviationSegmentIdx:null,deviationStartMs:null,tempRoute:null,rejoinTarget:null,lastRerouteMs:null,rejoinedAtMs:null,rejoinDwellFrames:0,failureReason:null},k=2e3,ee=3,C=class{constructor(){this._state="ON_ROUTE";this._context={...f};}get state(){return this._state}get context(){return {...this._context}}dispatch(e,t){let n=t??Date.now();switch(this._state){case "ON_ROUTE":e.type==="DEVIATION_SUSPECTED"&&(this._state="DEVIATING",this._context.deviationSegmentIdx=e.deviationSegmentIdx,this._context.deviationStartMs=n);break;case "DEVIATING":e.type==="DEVIATION_CONFIRMED"&&(this._context.deviationStartMs!==null?n-this._context.deviationStartMs:0)>=k&&(this._state="OFF_ROUTE",this._context.deviationSegmentIdx=e.deviationSegmentIdx),(e.type==="RESUME"||e.type==="RESET")&&(this._state="ON_ROUTE",e.type==="RESET"?this._context={...f}:(this._context.deviationSegmentIdx=null,this._context.deviationStartMs=null));break;case "OFF_ROUTE":e.type==="REROUTE_STARTED"&&(this._state="REROUTING",this._context.lastRerouteMs=n),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "REROUTING":e.type==="REROUTE_SUCCESS"&&(this._state="REJOINING",this._context.tempRoute=e.tempRoute,this._context.rejoinTarget=e.rejoinTarget),e.type==="REROUTE_FAILED"&&(this._state="FAILED",this._context.failureReason=e.reason);break;case "REJOINING":e.type==="REJOIN_DETECTED"&&this._context.rejoinDwellFrames++,(e.type==="REJOIN_CONFIRMED"||this._context.rejoinDwellFrames>=ee)&&(this._state="REJOINED",this._context.rejoinedAtMs=n,this._context.rejoinDwellFrames=0),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "REJOINED":e.type==="RESUME"&&(this._state="ON_ROUTE",this._context.tempRoute=null,this._context.rejoinTarget=null,this._context.rejoinedAtMs=null,this._context.deviationSegmentIdx=null,this._context.deviationStartMs=null),e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break;case "FAILED":e.type==="RESET"&&(this._state="ON_ROUTE",this._context={...f});break}return {state:this._state,context:{...this._context}}}reset(){this._state="ON_ROUTE",this._context={...f};}isDeviationConfirmed(e=Date.now()){return this._context.deviationStartMs?e-this._context.deviationStartMs>=k:false}};var te={bearingPenalty:80,bearingMismatchThresholdDeg:60,inertiaPenalty:40,inertiaSegmentDist:3},S=class{constructor(e){this.options={...te,...e};}resolve(e,t=null,n=null){return e.map(i=>this.score(i,t,n)).sort((i,r)=>i.score-r.score)}resolveBest(e,t=null,n=null){return e.length===0?null:this.resolve(e,t,n)[0]??null}score(e,t,n){let i=e.distanceMetres,r=null,s=false;if(t!==null){let c=l(e.segA,e.segB),a=Math.abs(p(t-c));r=a,a>this.options.bearingMismatchThresholdDeg&&(i+=this.options.bearingPenalty);}return n!==null&&Math.abs(e.segmentIndex-n)>=this.options.inertiaSegmentDist&&(i+=this.options.inertiaPenalty,s=true),{...e,score:i,bearingDeltaDeg:r,inertiaPenaltyApplied:s}}isLikelyUTurn(e,t,n){let i=l(t,n);return Math.abs(p(e-i))>this.options.bearingMismatchThresholdDeg}};var ne={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},A=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...ne,...e};}setInstructions(e){this.instructions=[...e],this.triggeredIds.clear(),this.reannouncedIds.clear();}update(e){let{distanceToNextInstruction:t,nextInstructionIndex:n}=e;if(n===null||t===null||n>=this.instructions.length)return null;let i=this.instructions[n],r=this.instrId(n),s=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&s<=this.options.reannounceMeters&&s>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,s,"low")):null;if(s>this.options.earlyTriggerMeters)return null;if(s<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=s<30?"urgent":s<60?"high":"normal";return this.makeCue(i,s,c)}onInstruction(e,t){let n=t.distanceToNextInstruction??0;return this.makeCue(e,n,"high")}reset(){this.triggeredIds.clear(),this.reannouncedIds.clear();}instrId(e){let t=this.instructions[e];return `${e}_${t?.geometryIndex??0}`}makeCue(e,t,n){return {instruction:e,text:e.text??e.instruction??"",distanceMetres:Math.max(0,t),priority:n,triggerId:`${e.geometryIndex}_${t.toFixed(0)}`}}};var ie={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},v=class{constructor(e){this.speedSamples=[];this.options={...ie,...e},this.lastReliableSpeed=this.options.fallbackSpeedMs;}update(e){let t=e.distanceToDestination??0,n=e.currentSpeed??0;this.speedSamples.push(n),this.speedSamples.length>this.options.speedWindowSize&&this.speedSamples.shift();let i=this.rollingAverage();i>=this.options.minimumSpeedMs&&(this.lastReliableSpeed=i);let r=i>=this.options.minimumSpeedMs?i:this.lastReliableSpeed,s=this.speedSamples.length>=Math.ceil(this.options.speedWindowSize/2)&&r>0&&t>0,c=r>0?t/r:0;return {etaSeconds:Math.round(c),etaMs:Date.now()+c*1e3,distanceRemainingM:t,averageSpeedMs:i,isReliable:s}}reset(){this.speedSamples=[],this.lastReliableSpeed=this.options.fallbackSpeedMs;}rollingAverage(){return this.speedSamples.length===0?0:this.speedSamples.reduce((t,n)=>t+n,0)/this.speedSamples.length}};var re={dwellThresholdMs:1e4},N=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...re,...e};}addCircle(e,t,n,i){return this.fences.set(e,{kind:"circle",id:e,center:t,radiusM:n,meta:i}),this.states.set(e,{inside:false,enteredAtMs:null}),e}addPolygon(e,t,n){if(t.length<3)throw new Error("[Geofencing] Polygon requires at least 3 coordinates");return this.fences.set(e,{kind:"polygon",id:e,coords:t,meta:n}),this.states.set(e,{inside:false,enteredAtMs:null}),e}remove(e){this.fences.delete(e),this.states.delete(e);}clear(){this.fences.clear(),this.states.clear();}listIds(){return Array.from(this.fences.keys())}update(e){if(!e)return [];let t=[],n=Date.now();for(let[i,r]of this.fences){let s=this.states.get(i),c=this.contains(r,e);if(!s.inside&&c)s.inside=true,s.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(s.inside&&!c){s.inside=false;let a=s.enteredAtMs?n-s.enteredAtMs:0;s.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(s.inside&&s.enteredAtMs!==null){let a=n-s.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),s.enteredAtMs=n);}}return t}isInside(e,t){let n=this.fences.get(e);return n?this.contains(n,t):false}contains(e,t){return e.kind==="circle"?h(t[1],t[0],e.center[1],e.center[0])<=e.radiusM:this.pointInPolygon(t,e.coords)}pointInPolygon(e,t){let[n,i]=e,r=false,s=t.length;for(let c=0,a=s-1;c<s;a=c++){let[u,d]=t[c],[R,m]=t[a];d>i!=m>i&&n<(R-u)*(i-d)/(m-d)+u&&(r=!r);}return r}};export{M as DeadReckoningEngine,T as DeviationDetector,v as ETAEngine,N as GeofencingEngine,_ as InstructionEditor,O as NavigationFSM,C as OffRouteFSM,S as ParallelRoadResolver,A as VoiceTriggerEngine,K as advancePredictor,J as applyBearingSmoothing,Q as applyGpsCorrection,E as blendBearing,B as calculateDynamicCorridor,L as calculateRobustBearing,w as computeDeadReckoningOnRoute,U as getSpeedAdaptiveAlpha,Z as pointAtOffset,X as resolveNextInstruction,q as validateProgressUpdate};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingissa/navcore-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "NavCore navigation engine — zero runtime dependencies. Framework-agnostic. Works in browsers, React Native, Node.js, and CI/CD.",
|
|
5
|
+
"author": "Ingissa <contact@navcore.dev>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://navcore.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/ingissa/navcore-sdk"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ingissa/navcore-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"navigation",
|
|
17
|
+
"gps",
|
|
18
|
+
"routing",
|
|
19
|
+
"kalman",
|
|
20
|
+
"dead-reckoning",
|
|
21
|
+
"mapbox",
|
|
22
|
+
"maplibre",
|
|
23
|
+
"react-native",
|
|
24
|
+
"geofencing",
|
|
25
|
+
"eta",
|
|
26
|
+
"off-route",
|
|
27
|
+
"turn-by-turn"
|
|
28
|
+
],
|
|
29
|
+
"main": "dist/index.cjs",
|
|
30
|
+
"module": "dist/index.mjs",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.mjs",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
},
|
|
38
|
+
"./core": {
|
|
39
|
+
"types": "./dist/core.d.ts",
|
|
40
|
+
"import": "./dist/core.mjs",
|
|
41
|
+
"require": "./dist/core.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./pro": {
|
|
44
|
+
"types": "./dist/pro.d.ts",
|
|
45
|
+
"import": "./dist/pro.mjs",
|
|
46
|
+
"require": "./dist/pro.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist"
|
|
51
|
+
],
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=16"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
62
|
+
"prepublishOnly": "npm run build",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"lint": "tsc --noEmit"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"tsup": "^8.5.1",
|
|
69
|
+
"typescript": "^5.4.0",
|
|
70
|
+
"vitest": "^3.1.0"
|
|
71
|
+
}
|
|
72
|
+
}
|