@ingissa/navcore-react-native 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 (123) hide show
  1. package/LICENSE +3 -0
  2. package/dist/core/src/NavCore.d.ts +142 -0
  3. package/dist/core/src/NavCore.d.ts.map +1 -0
  4. package/dist/core/src/NavCore.js +390 -0
  5. package/dist/core/src/bearing/BearingEngine.d.ts +24 -0
  6. package/dist/core/src/bearing/BearingEngine.d.ts.map +1 -0
  7. package/dist/core/src/bearing/BearingEngine.js +50 -0
  8. package/dist/core/src/constants.d.ts +34 -0
  9. package/dist/core/src/constants.d.ts.map +1 -0
  10. package/dist/core/src/constants.js +41 -0
  11. package/dist/core/src/corridor/DeviationDetector.d.ts +33 -0
  12. package/dist/core/src/corridor/DeviationDetector.d.ts.map +1 -0
  13. package/dist/core/src/corridor/DeviationDetector.js +83 -0
  14. package/dist/core/src/corridor/DynamicCorridor.d.ts +13 -0
  15. package/dist/core/src/corridor/DynamicCorridor.d.ts.map +1 -0
  16. package/dist/core/src/corridor/DynamicCorridor.js +26 -0
  17. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts +35 -0
  18. package/dist/core/src/dead-reckoning/DeadReckoning.d.ts.map +1 -0
  19. package/dist/core/src/dead-reckoning/DeadReckoning.js +49 -0
  20. package/dist/core/src/directions/DirectionsProvider.d.ts +70 -0
  21. package/dist/core/src/directions/DirectionsProvider.d.ts.map +1 -0
  22. package/dist/core/src/directions/DirectionsProvider.js +7 -0
  23. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts +26 -0
  24. package/dist/core/src/directions/OSRMDirectionsProvider.d.ts.map +1 -0
  25. package/dist/core/src/directions/OSRMDirectionsProvider.js +90 -0
  26. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts +30 -0
  27. package/dist/core/src/directions/OpenRouteServiceProvider.d.ts.map +1 -0
  28. package/dist/core/src/directions/OpenRouteServiceProvider.js +111 -0
  29. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts +33 -0
  30. package/dist/core/src/directions/ValhallaDirectionsProvider.d.ts.map +1 -0
  31. package/dist/core/src/directions/ValhallaDirectionsProvider.js +155 -0
  32. package/dist/core/src/eta/ETAEngine.d.ts +61 -0
  33. package/dist/core/src/eta/ETAEngine.d.ts.map +1 -0
  34. package/dist/core/src/eta/ETAEngine.js +74 -0
  35. package/dist/core/src/features.d.ts +76 -0
  36. package/dist/core/src/features.d.ts.map +1 -0
  37. package/dist/core/src/features.js +76 -0
  38. package/dist/core/src/geo/bearing.d.ts +11 -0
  39. package/dist/core/src/geo/bearing.d.ts.map +1 -0
  40. package/dist/core/src/geo/bearing.js +25 -0
  41. package/dist/core/src/geo/cumulative.d.ts +9 -0
  42. package/dist/core/src/geo/cumulative.d.ts.map +1 -0
  43. package/dist/core/src/geo/cumulative.js +20 -0
  44. package/dist/core/src/geo/haversine.d.ts +13 -0
  45. package/dist/core/src/geo/haversine.d.ts.map +1 -0
  46. package/dist/core/src/geo/haversine.js +39 -0
  47. package/dist/core/src/geo/projection.d.ts +7 -0
  48. package/dist/core/src/geo/projection.d.ts.map +1 -0
  49. package/dist/core/src/geo/projection.js +17 -0
  50. package/dist/core/src/geofencing/GeofencingEngine.d.ts +93 -0
  51. package/dist/core/src/geofencing/GeofencingEngine.d.ts.map +1 -0
  52. package/dist/core/src/geofencing/GeofencingEngine.js +154 -0
  53. package/dist/core/src/index.d.ts +66 -0
  54. package/dist/core/src/index.d.ts.map +1 -0
  55. package/dist/core/src/index.js +62 -0
  56. package/dist/core/src/kalman/KalmanFilter2D.d.ts +46 -0
  57. package/dist/core/src/kalman/KalmanFilter2D.d.ts.map +1 -0
  58. package/dist/core/src/kalman/KalmanFilter2D.js +77 -0
  59. package/dist/core/src/license/LicenseManager.d.ts +40 -0
  60. package/dist/core/src/license/LicenseManager.d.ts.map +1 -0
  61. package/dist/core/src/license/LicenseManager.js +119 -0
  62. package/dist/core/src/license/verifyP256Signature.d.ts +2 -0
  63. package/dist/core/src/license/verifyP256Signature.d.ts.map +1 -0
  64. package/dist/core/src/license/verifyP256Signature.js +26 -0
  65. package/dist/core/src/off-route/NavigationFSM.d.ts +42 -0
  66. package/dist/core/src/off-route/NavigationFSM.d.ts.map +1 -0
  67. package/dist/core/src/off-route/NavigationFSM.js +56 -0
  68. package/dist/core/src/off-route/OffRouteFSM.d.ts +70 -0
  69. package/dist/core/src/off-route/OffRouteFSM.d.ts.map +1 -0
  70. package/dist/core/src/off-route/OffRouteFSM.js +151 -0
  71. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts +24 -0
  72. package/dist/core/src/predictor/RouteOffsetPredictor.d.ts.map +1 -0
  73. package/dist/core/src/predictor/RouteOffsetPredictor.js +55 -0
  74. package/dist/core/src/progress/InstructionEditor.d.ts +77 -0
  75. package/dist/core/src/progress/InstructionEditor.d.ts.map +1 -0
  76. package/dist/core/src/progress/InstructionEditor.js +151 -0
  77. package/dist/core/src/progress/InstructionResolver.d.ts +25 -0
  78. package/dist/core/src/progress/InstructionResolver.d.ts.map +1 -0
  79. package/dist/core/src/progress/InstructionResolver.js +78 -0
  80. package/dist/core/src/progress/ProgressTracker.d.ts +12 -0
  81. package/dist/core/src/progress/ProgressTracker.d.ts.map +1 -0
  82. package/dist/core/src/progress/ProgressTracker.js +22 -0
  83. package/dist/core/src/renderer/MapRendererAdapter.d.ts +72 -0
  84. package/dist/core/src/renderer/MapRendererAdapter.d.ts.map +1 -0
  85. package/dist/core/src/renderer/MapRendererAdapter.js +19 -0
  86. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts +83 -0
  87. package/dist/core/src/route-builder/CustomRouteBuilder.d.ts.map +1 -0
  88. package/dist/core/src/route-builder/CustomRouteBuilder.js +160 -0
  89. package/dist/core/src/route-plan/RoutePlan.d.ts +38 -0
  90. package/dist/core/src/route-plan/RoutePlan.d.ts.map +1 -0
  91. package/dist/core/src/route-plan/RoutePlan.js +102 -0
  92. package/dist/core/src/session/NavigationSession.d.ts +81 -0
  93. package/dist/core/src/session/NavigationSession.d.ts.map +1 -0
  94. package/dist/core/src/session/NavigationSession.js +115 -0
  95. package/dist/core/src/snapper/ParallelRoadResolver.d.ts +86 -0
  96. package/dist/core/src/snapper/ParallelRoadResolver.d.ts.map +1 -0
  97. package/dist/core/src/snapper/ParallelRoadResolver.js +83 -0
  98. package/dist/core/src/snapper/RouteSnapper.d.ts +38 -0
  99. package/dist/core/src/snapper/RouteSnapper.d.ts.map +1 -0
  100. package/dist/core/src/snapper/RouteSnapper.js +80 -0
  101. package/dist/core/src/voice/VoiceTriggerEngine.d.ts +80 -0
  102. package/dist/core/src/voice/VoiceTriggerEngine.d.ts.map +1 -0
  103. package/dist/core/src/voice/VoiceTriggerEngine.js +108 -0
  104. package/dist/react-native/src/NavCoreProvider.d.ts +33 -0
  105. package/dist/react-native/src/NavCoreProvider.d.ts.map +1 -0
  106. package/dist/react-native/src/NavCoreProvider.js +41 -0
  107. package/dist/react-native/src/index.d.ts +10 -0
  108. package/dist/react-native/src/index.d.ts.map +1 -0
  109. package/dist/react-native/src/index.js +8 -0
  110. package/dist/{index.d.mts → react-native/src/useNavCore.d.ts} +12 -39
  111. package/dist/react-native/src/useNavCore.d.ts.map +1 -0
  112. package/dist/react-native/src/useNavCore.js +119 -0
  113. package/dist/react-native/src/useNavSelectors.d.ts +42 -0
  114. package/dist/react-native/src/useNavSelectors.d.ts.map +1 -0
  115. package/dist/react-native/src/useNavSelectors.js +103 -0
  116. package/package.json +20 -12
  117. package/src/NavCoreProvider.tsx +59 -0
  118. package/src/index.ts +18 -0
  119. package/src/useNavCore.ts +208 -0
  120. package/src/useNavSelectors.ts +143 -0
  121. package/dist/index.d.ts +0 -87
  122. package/dist/index.js +0 -161
  123. package/dist/index.mjs +0 -157
@@ -0,0 +1,38 @@
1
+ /**
2
+ * RoutePlan — the canonical, navigation-ready form of a provider route.
3
+ *
4
+ * Directions providers return vendor-specific steps and geometry. NavCore needs
5
+ * instructions positioned on that geometry. This module owns that conversion so
6
+ * applications do not need provider-specific glue code.
7
+ */
8
+ import type { DirectionsResult } from '../directions/DirectionsProvider';
9
+ import { type Coordinate } from '../geo/haversine';
10
+ import type { NavInstruction } from '../progress/InstructionResolver';
11
+ export interface RoutePlan {
12
+ /** Validated, immutable route geometry in [lng, lat] order. */
13
+ geometry: ReadonlyArray<Coordinate>;
14
+ /** Immutable, route-aligned maneuver instructions. */
15
+ instructions: ReadonlyArray<NavInstruction>;
16
+ /** Distance measured along the canonical geometry, in metres. */
17
+ distanceM: number;
18
+ /** Duration supplied by the directions provider, in seconds. */
19
+ durationS: number;
20
+ /** Original provider-reported distance, when supplied. */
21
+ providerDistanceM: number;
22
+ /** Optional identifier useful for diagnostics and analytics. */
23
+ provider?: string;
24
+ }
25
+ export interface CreateRoutePlanOptions {
26
+ /** Identifier for the provider that produced the route. */
27
+ provider?: string;
28
+ }
29
+ /**
30
+ * Converts a generic directions response into NavCore's canonical route model.
31
+ *
32
+ * Each maneuver is projected onto the complete route instead of trusting a
33
+ * provider-specific step index. The resulting `cumulativeOffset` is precise to
34
+ * the point within a segment, which keeps progress and voice timing correct on
35
+ * sparse route geometry.
36
+ */
37
+ export declare function createRoutePlan(result: DirectionsResult, options?: CreateRoutePlanOptions): RoutePlan;
38
+ //# sourceMappingURL=RoutePlan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoutePlan.d.ts","sourceRoot":"","sources":["../../../../../core/src/route-plan/RoutePlan.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACpC,sDAAsD;IACtD,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAQD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,sBAA2B,GACnC,SAAS,CA4BX"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * RoutePlan — the canonical, navigation-ready form of a provider route.
3
+ *
4
+ * Directions providers return vendor-specific steps and geometry. NavCore needs
5
+ * instructions positioned on that geometry. This module owns that conversion so
6
+ * applications do not need provider-specific glue code.
7
+ */
8
+ import { buildForwardCumulativeDistances } from '../geo/cumulative';
9
+ import { haversineDistance } from '../geo/haversine';
10
+ import { projectPointOnSegment } from '../geo/projection';
11
+ /**
12
+ * Converts a generic directions response into NavCore's canonical route model.
13
+ *
14
+ * Each maneuver is projected onto the complete route instead of trusting a
15
+ * provider-specific step index. The resulting `cumulativeOffset` is precise to
16
+ * the point within a segment, which keeps progress and voice timing correct on
17
+ * sparse route geometry.
18
+ */
19
+ export function createRoutePlan(result, options = {}) {
20
+ const geometry = copyAndValidateGeometry(result.geometry);
21
+ const cumulative = buildForwardCumulativeDistances(geometry);
22
+ const instructions = result.legs
23
+ .flatMap((leg) => leg.steps ?? [])
24
+ .map((step) => {
25
+ const position = locateOnRoute(step.maneuver.location, geometry, cumulative);
26
+ const text = step.maneuver.instruction ?? step.name;
27
+ return freezeInstruction({
28
+ location: position.coordinate,
29
+ geometryIndex: position.geometryIndex,
30
+ cumulativeOffset: position.cumulativeOffset,
31
+ bearingBefore: step.maneuver.bearingBefore,
32
+ bearingAfter: step.maneuver.bearingAfter,
33
+ ...(text ? { text, instruction: text } : {}),
34
+ });
35
+ })
36
+ .sort((a, b) => a.cumulativeOffset - b.cumulativeOffset);
37
+ return Object.freeze({
38
+ geometry: Object.freeze(geometry),
39
+ instructions: Object.freeze(instructions),
40
+ distanceM: cumulative[cumulative.length - 1] ?? 0,
41
+ durationS: requireFiniteNonNegative(result.duration, 'duration'),
42
+ providerDistanceM: requireFiniteNonNegative(result.distance, 'distance'),
43
+ ...(options.provider ? { provider: options.provider } : {}),
44
+ });
45
+ }
46
+ function copyAndValidateGeometry(input) {
47
+ if (input.length < 2) {
48
+ throw new Error('[RoutePlan] A route requires at least two coordinates');
49
+ }
50
+ return input.map((coord, index) => {
51
+ if (!isValidCoordinate(coord)) {
52
+ throw new Error(`[RoutePlan] Invalid coordinate at geometry index ${index}`);
53
+ }
54
+ return Object.freeze([coord[0], coord[1]]);
55
+ });
56
+ }
57
+ function locateOnRoute(target, geometry, cumulative) {
58
+ if (!isValidCoordinate(target)) {
59
+ throw new Error('[RoutePlan] Invalid maneuver coordinate');
60
+ }
61
+ let best = null;
62
+ let bestDistance = Infinity;
63
+ for (let index = 0; index < geometry.length - 1; index++) {
64
+ const start = geometry[index];
65
+ const end = geometry[index + 1];
66
+ const projected = projectPointOnSegment(target, start, end);
67
+ const distance = haversineDistance(target[1], target[0], projected[1], projected[0]);
68
+ if (distance >= bestDistance)
69
+ continue;
70
+ const segmentLength = haversineDistance(start[1], start[0], end[1], end[0]);
71
+ const fraction = segmentFraction(projected, start, end);
72
+ bestDistance = distance;
73
+ best = {
74
+ geometryIndex: index,
75
+ cumulativeOffset: (cumulative[index] ?? 0) + segmentLength * fraction,
76
+ coordinate: Object.freeze([projected[0], projected[1]]),
77
+ };
78
+ }
79
+ // Geometry has already been checked for at least two points.
80
+ return best;
81
+ }
82
+ function segmentFraction(point, start, end) {
83
+ const dx = end[0] - start[0];
84
+ const dy = end[1] - start[1];
85
+ const lengthSquared = dx * dx + dy * dy;
86
+ if (lengthSquared === 0)
87
+ return 0;
88
+ return Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / lengthSquared));
89
+ }
90
+ function freezeInstruction(instruction) {
91
+ return Object.freeze(instruction);
92
+ }
93
+ function isValidCoordinate(coord) {
94
+ return Number.isFinite(coord[0]) && Number.isFinite(coord[1]) &&
95
+ coord[0] >= -180 && coord[0] <= 180 && coord[1] >= -90 && coord[1] <= 90;
96
+ }
97
+ function requireFiniteNonNegative(value, name) {
98
+ if (!Number.isFinite(value) || value < 0) {
99
+ throw new Error(`[RoutePlan] Invalid ${name}`);
100
+ }
101
+ return value;
102
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * NavigationSession — the ergonomic application-facing NavCore API.
3
+ *
4
+ * It owns the route-provider → RoutePlan → NavCore handoff and offers typed
5
+ * subscriptions. `coordinates` are ordered routing waypoints; use `loadRoute`
6
+ * for a route that has already been normalized into a RoutePlan.
7
+ */
8
+ import type { DirectionsOptions, DirectionsProvider } from '../directions/DirectionsProvider';
9
+ import { NavCore, type NavCoreOptions, type NavCoreState, type NavGpsUpdate } from '../NavCore';
10
+ import type { NavInstruction } from '../progress/InstructionResolver';
11
+ import { type RoutePlan } from '../route-plan/RoutePlan';
12
+ import type { Coordinate } from '../geo/haversine';
13
+ export interface OriginDestinationNavigationRequest {
14
+ origin: Coordinate;
15
+ destination: Coordinate;
16
+ /** Optional stops, traversed in the supplied order. */
17
+ waypoints?: ReadonlyArray<Coordinate>;
18
+ coordinates?: never;
19
+ directions?: DirectionsOptions;
20
+ }
21
+ export interface CoordinatesNavigationRequest {
22
+ /** Ordered routing waypoints. At least two are required. */
23
+ coordinates: ReadonlyArray<Coordinate>;
24
+ origin?: never;
25
+ destination?: never;
26
+ waypoints?: never;
27
+ directions?: DirectionsOptions;
28
+ }
29
+ export type NavigationRequest = OriginDestinationNavigationRequest | CoordinatesNavigationRequest;
30
+ export interface NavigationSessionOptions {
31
+ provider: DirectionsProvider;
32
+ /** Optional provider label retained on created RoutePlans. */
33
+ providerName?: string;
34
+ /** Reuse an existing engine; the caller retains responsibility for it. */
35
+ engine?: NavCore;
36
+ /** Options used only when the session creates its own engine. */
37
+ engineOptions?: NavCoreOptions;
38
+ }
39
+ export interface NavigationSessionEventMap {
40
+ state: NavCoreState;
41
+ routeLoaded: RoutePlan;
42
+ navigationStarted: undefined;
43
+ instruction: NavInstruction;
44
+ deviation: {
45
+ anchorIndex: number;
46
+ ghostCoord: Coordinate;
47
+ };
48
+ rejoined: {
49
+ resumeIndex: number;
50
+ };
51
+ arrival: undefined;
52
+ }
53
+ type EventName = keyof NavigationSessionEventMap;
54
+ type EventHandler<K extends EventName> = (payload: NavigationSessionEventMap[K]) => void;
55
+ export declare class NavigationSession {
56
+ readonly engine: NavCore;
57
+ private readonly provider;
58
+ private readonly providerName?;
59
+ private readonly ownsEngine;
60
+ private readonly listeners;
61
+ private readonly engineListeners;
62
+ private destroyed;
63
+ private currentPlan;
64
+ constructor(options: NavigationSessionOptions);
65
+ /** Fetch, normalize, load, and start a route from either supported input form. */
66
+ navigate(request: NavigationRequest): Promise<RoutePlan>;
67
+ /** Load a route that was previously normalized, optionally starting navigation. */
68
+ loadRoute(plan: RoutePlan, start?: boolean): void;
69
+ update(gps: NavGpsUpdate): NavCoreState;
70
+ tick(nowMs: number): NavCoreState;
71
+ getState(): NavCoreState;
72
+ getRoutePlan(): RoutePlan | null;
73
+ /** Subscribe to a typed event. Returns an unsubscribe function. */
74
+ subscribe<K extends EventName>(event: K, handler: EventHandler<K>): () => void;
75
+ destroy(): void;
76
+ private emit;
77
+ private assertActive;
78
+ }
79
+ export declare function createNavigationSession(options: NavigationSessionOptions): NavigationSession;
80
+ export {};
81
+ //# sourceMappingURL=NavigationSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavigationSession.d.ts","sourceRoot":"","sources":["../../../../../core/src/session/NavigationSession.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * NavigationSession — the ergonomic application-facing NavCore API.
3
+ *
4
+ * It owns the route-provider → RoutePlan → NavCore handoff and offers typed
5
+ * subscriptions. `coordinates` are ordered routing waypoints; use `loadRoute`
6
+ * for a route that has already been normalized into a RoutePlan.
7
+ */
8
+ import { NavCore } from '../NavCore';
9
+ import { createRoutePlan } from '../route-plan/RoutePlan';
10
+ export class NavigationSession {
11
+ constructor(options) {
12
+ this.listeners = new Map();
13
+ this.destroyed = false;
14
+ this.currentPlan = null;
15
+ this.provider = options.provider;
16
+ this.providerName = options.providerName;
17
+ this.ownsEngine = !options.engine;
18
+ this.engine = options.engine ?? new NavCore(options.engineOptions);
19
+ this.engineListeners = [
20
+ { event: 'update', handler: (state) => this.emit('state', state) },
21
+ { event: 'instruction', handler: (instruction) => this.emit('instruction', instruction) },
22
+ { event: 'deviation', handler: (event) => this.emit('deviation', event) },
23
+ { event: 'rejoined', handler: (event) => this.emit('rejoined', event) },
24
+ { event: 'arrival', handler: () => this.emit('arrival', undefined) },
25
+ ];
26
+ for (const { event, handler } of this.engineListeners) {
27
+ this.engine.on(event, handler);
28
+ }
29
+ }
30
+ /** Fetch, normalize, load, and start a route from either supported input form. */
31
+ async navigate(request) {
32
+ this.assertActive();
33
+ const result = await this.provider.getRoute(resolveWaypoints(request), request.directions);
34
+ const plan = createRoutePlan(result, { provider: this.providerName });
35
+ this.loadRoute(plan, true);
36
+ return plan;
37
+ }
38
+ /** Load a route that was previously normalized, optionally starting navigation. */
39
+ loadRoute(plan, start = false) {
40
+ this.assertActive();
41
+ this.currentPlan = plan;
42
+ this.engine.loadRoute(plan);
43
+ this.emit('routeLoaded', plan);
44
+ if (start) {
45
+ this.engine.startNavigation();
46
+ this.emit('navigationStarted', undefined);
47
+ }
48
+ }
49
+ update(gps) {
50
+ this.assertActive();
51
+ return this.engine.update(gps);
52
+ }
53
+ tick(nowMs) {
54
+ this.assertActive();
55
+ return this.engine.tick(nowMs);
56
+ }
57
+ getState() {
58
+ return this.engine.getState();
59
+ }
60
+ getRoutePlan() {
61
+ return this.currentPlan;
62
+ }
63
+ /** Subscribe to a typed event. Returns an unsubscribe function. */
64
+ subscribe(event, handler) {
65
+ this.assertActive();
66
+ const handlers = this.listeners.get(event) ?? new Set();
67
+ const untypedHandler = handler;
68
+ handlers.add(untypedHandler);
69
+ this.listeners.set(event, handlers);
70
+ return () => {
71
+ handlers.delete(untypedHandler);
72
+ if (handlers.size === 0)
73
+ this.listeners.delete(event);
74
+ };
75
+ }
76
+ destroy() {
77
+ if (this.destroyed)
78
+ return;
79
+ this.destroyed = true;
80
+ for (const { event, handler } of this.engineListeners) {
81
+ this.engine.off(event, handler);
82
+ }
83
+ this.listeners.clear();
84
+ this.currentPlan = null;
85
+ if (this.ownsEngine)
86
+ this.engine.destroy();
87
+ }
88
+ emit(event, payload) {
89
+ for (const handler of this.listeners.get(event) ?? []) {
90
+ handler(payload);
91
+ }
92
+ }
93
+ assertActive() {
94
+ if (this.destroyed)
95
+ throw new Error('[NavigationSession] Session has been destroyed');
96
+ }
97
+ }
98
+ export function createNavigationSession(options) {
99
+ return new NavigationSession(options);
100
+ }
101
+ function resolveWaypoints(request) {
102
+ const coordinates = request.coordinates !== undefined
103
+ ? request.coordinates
104
+ : [request.origin, ...(request.waypoints ?? []), request.destination];
105
+ if (coordinates.length < 2) {
106
+ throw new Error('[NavigationSession] At least two routing coordinates are required');
107
+ }
108
+ return coordinates.map((coordinate, index) => {
109
+ if (!Number.isFinite(coordinate[0]) || !Number.isFinite(coordinate[1]) ||
110
+ coordinate[0] < -180 || coordinate[0] > 180 || coordinate[1] < -90 || coordinate[1] > 90) {
111
+ throw new Error(`[NavigationSession] Invalid routing coordinate at index ${index}`);
112
+ }
113
+ return [coordinate[0], coordinate[1]];
114
+ });
115
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ParallelRoadResolver
3
+ *
4
+ * First-class feature: resolve GPS ambiguity on parallel roads (e.g. dual carriageways,
5
+ * motorway + service road) and prevent U-turn snap.
6
+ *
7
+ * Extracts and extends the bearing penalty + inertia logic from RouteSnapper
8
+ * into a standalone, composable API that can be used independently.
9
+ *
10
+ * FEATURE: PARALLEL_ROAD_RESOLUTION + U_TURN_PROTECTION
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const resolver = new ParallelRoadResolver({ bearingPenalty: 80, inertiaPenalty: 40 });
15
+ * const best = resolver.resolve(candidates, vehicleBearing, prevSegmentIndex);
16
+ * ```
17
+ */
18
+ import type { Coordinate } from '../geo/haversine';
19
+ export interface SnapCandidate {
20
+ /** Route segment index */
21
+ segmentIndex: number;
22
+ /** Projected point on segment */
23
+ projectedCoord: Coordinate;
24
+ /** Raw distance to route in metres */
25
+ distanceMetres: number;
26
+ /** Start of segment */
27
+ segA: Coordinate;
28
+ /** End of segment */
29
+ segB: Coordinate;
30
+ }
31
+ export interface ResolvedCandidate extends SnapCandidate {
32
+ /** Final score (lower is better) */
33
+ score: number;
34
+ /** Bearing delta that triggered U-turn protection (null if not triggered) */
35
+ bearingDeltaDeg: number | null;
36
+ /** Whether inertia penalty was applied */
37
+ inertiaPenaltyApplied: boolean;
38
+ }
39
+ export interface ParallelRoadResolverOptions {
40
+ /**
41
+ * Penalty added to score when vehicle bearing mismatches segment bearing
42
+ * beyond `bearingMismatchThresholdDeg`. Default: 80m equivalent.
43
+ */
44
+ bearingPenalty?: number;
45
+ /**
46
+ * Bearing mismatch threshold in degrees that activates the penalty.
47
+ * Default: 60°. Higher = more tolerant of U-turns.
48
+ */
49
+ bearingMismatchThresholdDeg?: number;
50
+ /**
51
+ * Penalty added when the candidate segment is far from the previously
52
+ * snapped segment, preventing oscillation. Default: 40m equivalent.
53
+ */
54
+ inertiaPenalty?: number;
55
+ /**
56
+ * Minimum segment jump (in index units) to trigger inertia penalty.
57
+ * Default: 3.
58
+ */
59
+ inertiaSegmentDist?: number;
60
+ }
61
+ export declare class ParallelRoadResolver {
62
+ private readonly options;
63
+ constructor(options?: ParallelRoadResolverOptions);
64
+ /**
65
+ * Score and sort snap candidates — lowest score wins.
66
+ *
67
+ * @param candidates Raw snap candidates (distance-sorted or unsorted)
68
+ * @param vehicleBearing Current vehicle heading in degrees (null disables bearing check)
69
+ * @param prevSegmentIdx Previously snapped segment index (null disables inertia)
70
+ * @returns Candidates sorted by score ascending, with debug metadata attached
71
+ */
72
+ resolve(candidates: SnapCandidate[], vehicleBearing?: number | null, prevSegmentIdx?: number | null): ResolvedCandidate[];
73
+ /**
74
+ * Resolve and return only the best candidate.
75
+ */
76
+ resolveBest(candidates: SnapCandidate[], vehicleBearing?: number | null, prevSegmentIdx?: number | null): ResolvedCandidate | null;
77
+ private score;
78
+ /**
79
+ * Quick utility: detect if the vehicle is likely performing a U-turn
80
+ * based on the difference between current bearing and route segment bearing.
81
+ *
82
+ * @returns true if the bearing difference exceeds the configured threshold
83
+ */
84
+ isLikelyUTurn(vehicleBearing: number, segA: Coordinate, segB: Coordinate): boolean;
85
+ }
86
+ //# sourceMappingURL=ParallelRoadResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ParallelRoadResolver.d.ts","sourceRoot":"","sources":["../../../../../core/src/snapper/ParallelRoadResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,cAAc,EAAE,UAAU,CAAC;IAC3B,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,qBAAqB;IACrB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,0CAA0C;IAC1C,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AASD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;gBAEpD,OAAO,CAAC,EAAE,2BAA2B;IAIjD;;;;;;;OAOG;IACH,OAAO,CACL,UAAU,EAAE,aAAa,EAAE,EAC3B,cAAc,GAAE,MAAM,GAAG,IAAW,EACpC,cAAc,GAAE,MAAM,GAAG,IAAW,GACnC,iBAAiB,EAAE;IAMtB;;OAEG;IACH,WAAW,CACT,UAAU,EAAE,aAAa,EAAE,EAC3B,cAAc,GAAE,MAAM,GAAG,IAAW,EACpC,cAAc,GAAE,MAAM,GAAG,IAAW,GACnC,iBAAiB,GAAG,IAAI;IAM3B,OAAO,CAAC,KAAK;IA+Bb;;;;;OAKG;IACH,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO;CAKnF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ParallelRoadResolver
3
+ *
4
+ * First-class feature: resolve GPS ambiguity on parallel roads (e.g. dual carriageways,
5
+ * motorway + service road) and prevent U-turn snap.
6
+ *
7
+ * Extracts and extends the bearing penalty + inertia logic from RouteSnapper
8
+ * into a standalone, composable API that can be used independently.
9
+ *
10
+ * FEATURE: PARALLEL_ROAD_RESOLUTION + U_TURN_PROTECTION
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const resolver = new ParallelRoadResolver({ bearingPenalty: 80, inertiaPenalty: 40 });
15
+ * const best = resolver.resolve(candidates, vehicleBearing, prevSegmentIndex);
16
+ * ```
17
+ */
18
+ import { getBearingBetweenPoints, normalizeAngle } from '../geo/bearing';
19
+ const DEFAULTS = {
20
+ bearingPenalty: 80,
21
+ bearingMismatchThresholdDeg: 60,
22
+ inertiaPenalty: 40,
23
+ inertiaSegmentDist: 3,
24
+ };
25
+ export class ParallelRoadResolver {
26
+ constructor(options) {
27
+ this.options = { ...DEFAULTS, ...options };
28
+ }
29
+ /**
30
+ * Score and sort snap candidates — lowest score wins.
31
+ *
32
+ * @param candidates Raw snap candidates (distance-sorted or unsorted)
33
+ * @param vehicleBearing Current vehicle heading in degrees (null disables bearing check)
34
+ * @param prevSegmentIdx Previously snapped segment index (null disables inertia)
35
+ * @returns Candidates sorted by score ascending, with debug metadata attached
36
+ */
37
+ resolve(candidates, vehicleBearing = null, prevSegmentIdx = null) {
38
+ return candidates
39
+ .map(c => this.score(c, vehicleBearing, prevSegmentIdx))
40
+ .sort((a, b) => a.score - b.score);
41
+ }
42
+ /**
43
+ * Resolve and return only the best candidate.
44
+ */
45
+ resolveBest(candidates, vehicleBearing = null, prevSegmentIdx = null) {
46
+ if (candidates.length === 0)
47
+ return null;
48
+ const resolved = this.resolve(candidates, vehicleBearing, prevSegmentIdx);
49
+ return resolved[0] ?? null;
50
+ }
51
+ score(c, vehicleBearing, prevSegmentIdx) {
52
+ let score = c.distanceMetres;
53
+ let bearingDeltaDeg = null;
54
+ let inertiaPenaltyApplied = false;
55
+ // ── U-turn / parallel road bearing penalty ───────────────────────────────
56
+ if (vehicleBearing !== null) {
57
+ const segBearing = getBearingBetweenPoints(c.segA, c.segB);
58
+ const delta = Math.abs(normalizeAngle(vehicleBearing - segBearing));
59
+ bearingDeltaDeg = delta;
60
+ if (delta > this.options.bearingMismatchThresholdDeg) {
61
+ score += this.options.bearingPenalty;
62
+ }
63
+ }
64
+ // ── Inertia penalty (anti-oscillation) ───────────────────────────────────
65
+ if (prevSegmentIdx !== null &&
66
+ Math.abs(c.segmentIndex - prevSegmentIdx) >= this.options.inertiaSegmentDist) {
67
+ score += this.options.inertiaPenalty;
68
+ inertiaPenaltyApplied = true;
69
+ }
70
+ return { ...c, score, bearingDeltaDeg, inertiaPenaltyApplied };
71
+ }
72
+ /**
73
+ * Quick utility: detect if the vehicle is likely performing a U-turn
74
+ * based on the difference between current bearing and route segment bearing.
75
+ *
76
+ * @returns true if the bearing difference exceeds the configured threshold
77
+ */
78
+ isLikelyUTurn(vehicleBearing, segA, segB) {
79
+ const segBearing = getBearingBetweenPoints(segA, segB);
80
+ const delta = Math.abs(normalizeAngle(vehicleBearing - segBearing));
81
+ return delta > this.options.bearingMismatchThresholdDeg;
82
+ }
83
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Module 2: RouteSnapper
3
+ * Projects GPS coordinates onto the route with anti-oscillation and U-turn protection.
4
+ * Merges BasicSnapper + AdvancedSnapper from nav-sdk into a single module.
5
+ */
6
+ import { type Coordinate } from '../geo/haversine';
7
+ export interface SnapResult {
8
+ idx: number;
9
+ dist: number;
10
+ proj: Coordinate;
11
+ }
12
+ export interface SnapperOptions {
13
+ /** Enable bearing-based U-turn anti-snap penalty */
14
+ useBearingPenalty?: boolean;
15
+ /** Enable inertia penalty to prevent oscillation between segments */
16
+ useInertiaPenalty?: boolean;
17
+ /** Max search distance hard cutoff (metres) */
18
+ maxSnapDistance?: number;
19
+ }
20
+ export declare class RouteSnapper {
21
+ private readonly options;
22
+ constructor(options?: SnapperOptions);
23
+ /**
24
+ * Find the nearest projection of a GPS point onto the route.
25
+ *
26
+ * @param gps Current GPS coordinate [lng, lat]
27
+ * @param route Route geometry
28
+ * @param startIdx Search window start
29
+ * @param windowSize Number of segments to scan
30
+ * @param isCircuit Whether the route is a closed circuit
31
+ * @param maxProgressIdx Highest index ever reached (for circuit wrap detection)
32
+ * @param fallbackIdx Fallback index if no match found
33
+ * @param vehicleBearing Current vehicle bearing (null disables bearing penalty)
34
+ * @param prevSegment Previous snapped segment index (null disables inertia)
35
+ */
36
+ snap(gps: Coordinate, route: Coordinate[], startIdx: number, windowSize: number, isCircuit: boolean, maxProgressIdx: number, fallbackIdx: number, vehicleBearing?: number | null, prevSegment?: number | null): SnapResult;
37
+ }
38
+ //# sourceMappingURL=RouteSnapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteSnapper.d.ts","sourceRoot":"","sources":["../../../../../core/src/snapper/RouteSnapper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,kBAAkB,CAAC;AAWtE,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAQD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;gBAEvC,OAAO,CAAC,EAAE,cAAc;IAIpC;;;;;;;;;;;;OAYG;IACH,IAAI,CACF,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,OAAO,EAClB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,MAAM,GAAG,IAAW,EACpC,WAAW,GAAE,MAAM,GAAG,IAAW,GAChC,UAAU;CAuDd"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Module 2: RouteSnapper
3
+ * Projects GPS coordinates onto the route with anti-oscillation and U-turn protection.
4
+ * Merges BasicSnapper + AdvancedSnapper from nav-sdk into a single module.
5
+ */
6
+ import { haversineDistance } from '../geo/haversine';
7
+ import { projectPointOnSegment } from '../geo/projection';
8
+ import { getBearingBetweenPoints, normalizeAngle } from '../geo/bearing';
9
+ import { MAX_SNAP_DISTANCE, BEARING_MISMATCH_THRESHOLD, BEARING_MISMATCH_PENALTY, INERTIA_PENALTY, INERTIA_SEGMENT_DIST } from '../constants';
10
+ const DEFAULT_OPTIONS = {
11
+ useBearingPenalty: true,
12
+ useInertiaPenalty: true,
13
+ maxSnapDistance: MAX_SNAP_DISTANCE,
14
+ };
15
+ export class RouteSnapper {
16
+ constructor(options) {
17
+ this.options = { ...DEFAULT_OPTIONS, ...options };
18
+ }
19
+ /**
20
+ * Find the nearest projection of a GPS point onto the route.
21
+ *
22
+ * @param gps Current GPS coordinate [lng, lat]
23
+ * @param route Route geometry
24
+ * @param startIdx Search window start
25
+ * @param windowSize Number of segments to scan
26
+ * @param isCircuit Whether the route is a closed circuit
27
+ * @param maxProgressIdx Highest index ever reached (for circuit wrap detection)
28
+ * @param fallbackIdx Fallback index if no match found
29
+ * @param vehicleBearing Current vehicle bearing (null disables bearing penalty)
30
+ * @param prevSegment Previous snapped segment index (null disables inertia)
31
+ */
32
+ snap(gps, route, startIdx, windowSize, isCircuit, maxProgressIdx, fallbackIdx, vehicleBearing = null, prevSegment = null) {
33
+ const routeLength = route.length;
34
+ let bestIdx = fallbackIdx;
35
+ let bestScore = Infinity;
36
+ let bestDist = Infinity;
37
+ let bestProj = route[fallbackIdx] ?? [0, 0];
38
+ for (let step = 0; step <= windowSize; step++) {
39
+ let i = startIdx + step;
40
+ if (i > routeLength - 2) {
41
+ if (isCircuit && maxProgressIdx > routeLength * 0.7) {
42
+ i = i % (routeLength - 1);
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ }
48
+ if (i < 0 || i >= routeLength - 1)
49
+ continue;
50
+ const segA = route[i];
51
+ const segB = route[i + 1];
52
+ const p = projectPointOnSegment(gps, segA, segB);
53
+ const d = haversineDistance(gps[1], gps[0], p[1], p[0]);
54
+ // Hard cutoff
55
+ if (d > this.options.maxSnapDistance * 2)
56
+ continue;
57
+ let score = d;
58
+ // ── Bearing penalty (U-turn anti-snap) ──────────────────────────────
59
+ if (this.options.useBearingPenalty && vehicleBearing !== null) {
60
+ const segBearing = getBearingBetweenPoints(segA, segB);
61
+ const bearingDiff = Math.abs(normalizeAngle(vehicleBearing - segBearing));
62
+ if (bearingDiff > BEARING_MISMATCH_THRESHOLD) {
63
+ score += BEARING_MISMATCH_PENALTY;
64
+ }
65
+ }
66
+ // ── Inertia penalty ─────────────────────────────────────────────────
67
+ if (this.options.useInertiaPenalty && prevSegment !== null &&
68
+ Math.abs(i - prevSegment) >= INERTIA_SEGMENT_DIST) {
69
+ score += INERTIA_PENALTY;
70
+ }
71
+ if (score < bestScore) {
72
+ bestScore = score;
73
+ bestDist = d;
74
+ bestIdx = i;
75
+ bestProj = p;
76
+ }
77
+ }
78
+ return { idx: bestIdx, dist: bestDist, proj: bestProj };
79
+ }
80
+ }