@ingissa/navcore-core 1.0.1 → 1.0.2
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 +279 -279
- package/dist/core.js +1 -2
- package/dist/core.mjs +1 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +72 -72
package/README.md
CHANGED
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
# @ingissa/navcore-core
|
|
2
|
-
|
|
3
|
-
> **The navigation layer you own.** A framework-agnostic, zero-dependency navigation engine that runs everywhere — browsers, React Native, Node.js, and CI/CD.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@ingissa/navcore-core)
|
|
6
|
-
[](
|
|
7
|
-
[](https://www.typescriptlang.org/)
|
|
8
|
-
[](https://www.npmjs.com/package/@ingissa/navcore-core)
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Why NavCore?
|
|
13
|
-
|
|
14
|
-
| Feature | NavCore | Mapbox Nav SDK | Google Maps |
|
|
15
|
-
|---|---|---|---|
|
|
16
|
-
| **Framework** | Any (pure TS) | React Native only | Platform-locked |
|
|
17
|
-
| **Runtime deps** | 0 | 50+ | N/A |
|
|
18
|
-
| **Map renderer** | BYO (adapter) | Mapbox GL only | Google Maps only |
|
|
19
|
-
| **Directions** | BYO (adapter) | Mapbox API only | Google API only |
|
|
20
|
-
| **Off-route detection** | Built-in FSM | Basic | Basic |
|
|
21
|
-
| **U-turn protection** | ✅ Bearing penalty | ❌ | ❌ |
|
|
22
|
-
| **Parallel road resolver** | ✅ Built-in | ❌ | ❌ |
|
|
23
|
-
| **Dead reckoning** | ✅ Built-in | ❌ | ❌ |
|
|
24
|
-
| **Geofencing** | ✅ Built-in | ❌ | Paid API |
|
|
25
|
-
| **ETA** | ✅ Offline | API only | API only |
|
|
26
|
-
| **Voice triggers** | ✅ Built-in | SDK only | ❌ |
|
|
27
|
-
| **Headless testing** | ✅ Full pipeline | ❌ | ❌ |
|
|
28
|
-
| **Custom routes** | ✅ GPX/GeoJSON/Builder | ❌ | ❌ |
|
|
29
|
-
| **License** | Core MIT / Pro commercial | Commercial | Commercial |
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Tier Model
|
|
34
|
-
|
|
35
|
-
NavCore uses a two-tier model controlled by a signed license key:
|
|
36
|
-
|
|
37
|
-
| Tier | Import | License | Includes |
|
|
38
|
-
|---|---|---|---|
|
|
39
|
-
| **Core** | `@ingissa/navcore-core/core` | MIT — free forever | Geo utils, Kalman filter, route snapping, directions adapters, route builder |
|
|
40
|
-
| **Pro** | `@ingissa/navcore-core/pro` | Commercial key required | Dead reckoning, advanced bearing, deviation detection, FSMs, voice, ETA, geofencing |
|
|
41
|
-
| **Engine** | `@ingissa/navcore-core` | Gated internally at runtime | Full `NavCore` orchestrator (license key unlocks Pro features) |
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Installation
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npm install @ingissa/navcore-core
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Quick Start
|
|
54
|
-
|
|
55
|
-
### Core tier (no license needed)
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import { NavCore } from '@ingissa/navcore-core';
|
|
59
|
-
import { OSRMDirectionsProvider } from '@ingissa/navcore-core/core';
|
|
60
|
-
|
|
61
|
-
// 1. Get a route from your self-hosted OSRM (free)
|
|
62
|
-
const provider = new OSRMDirectionsProvider({ baseUrl: 'http://router.project-osrm.org' });
|
|
63
|
-
const route = await provider.getRoute([[2.3522, 48.8566], [2.3400, 48.8700]]);
|
|
64
|
-
|
|
65
|
-
// 2. Create engine (Core features work without a key)
|
|
66
|
-
const engine = new NavCore();
|
|
67
|
-
engine.setRoute(route.geometry);
|
|
68
|
-
engine.startNavigation();
|
|
69
|
-
|
|
70
|
-
// 3. Feed GPS updates
|
|
71
|
-
engine.on('arrival', () => console.log('Arrived!'));
|
|
72
|
-
|
|
73
|
-
navigator.geolocation.watchPosition(({ coords }) => {
|
|
74
|
-
const state = engine.update({
|
|
75
|
-
coord: [coords.longitude, coords.latitude],
|
|
76
|
-
accuracy: coords.accuracy,
|
|
77
|
-
bearing: coords.heading,
|
|
78
|
-
speed: coords.speed ?? 0,
|
|
79
|
-
timestamp: Date.now(),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
console.log('Distance remaining:', state.distanceToDestination, 'm');
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Pro tier (with license key)
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
import { NavCore } from '@ingissa/navcore-core';
|
|
90
|
-
|
|
91
|
-
// Pro features unlock automatically with a valid key
|
|
92
|
-
const engine = new NavCore({ licenseKey: 'your-signed-license-key' });
|
|
93
|
-
engine.setRoute(routeCoords, instructions);
|
|
94
|
-
engine.startNavigation();
|
|
95
|
-
|
|
96
|
-
engine.on('instruction', (instr) => speak(instr.text)); // voice cues (PRO)
|
|
97
|
-
engine.on('deviation', ({ anchorIndex }) => recalculate(anchorIndex));
|
|
98
|
-
engine.on('arrival', () => console.log('Arrived!'));
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Using subpath imports directly
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
// Free MIT tier
|
|
105
|
-
import { KalmanFilter2D } from '@ingissa/navcore-core/core';
|
|
106
|
-
import { OSRMDirectionsProvider } from '@ingissa/navcore-core/core';
|
|
107
|
-
import { CustomRouteBuilder } from '@ingissa/navcore-core/core';
|
|
108
|
-
|
|
109
|
-
// Commercial Pro tier
|
|
110
|
-
import { DeadReckoningEngine } from '@ingissa/navcore-core/pro';
|
|
111
|
-
import { VoiceTriggerEngine } from '@ingissa/navcore-core/pro';
|
|
112
|
-
import { ETAEngine } from '@ingissa/navcore-core/pro';
|
|
113
|
-
import { GeofencingEngine } from '@ingissa/navcore-core/pro';
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Directions Providers (built-in, no extra install)
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import {
|
|
122
|
-
OSRMDirectionsProvider, // Self-hosted OSRM (recommended, free)
|
|
123
|
-
ValhallaDirectionsProvider, // Self-hosted Valhalla (free)
|
|
124
|
-
OpenRouteServiceProvider, // openrouteservice.org (free tier)
|
|
125
|
-
} from '@ingissa/navcore-core/core';
|
|
126
|
-
|
|
127
|
-
// OSRM
|
|
128
|
-
const osrm = new OSRMDirectionsProvider({ baseUrl: 'http://router.project-osrm.org' });
|
|
129
|
-
const route = await osrm.getRoute([[lng1, lat1], [lng2, lat2]]);
|
|
130
|
-
|
|
131
|
-
// Valhalla
|
|
132
|
-
const valhalla = new ValhallaDirectionsProvider({ baseUrl: 'https://valhalla1.openstreetmap.de' });
|
|
133
|
-
const route = await valhalla.getRoute([[lng1, lat1], [lng2, lat2]], { costing: 'auto' });
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## Renderer Adapters (separate packages)
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
npm install @ingissa/navcore-maplibre # Web + MapLibre GL
|
|
142
|
-
npm install @ingissa/navcore-leaflet # Web + Leaflet.js
|
|
143
|
-
npm install @ingissa/navcore-google-maps # Google Maps JS API
|
|
144
|
-
npm install @ingissa/navcore-mapbox # React Native + @rnmapbox/maps
|
|
145
|
-
npm install @ingissa/navcore-react-native # React Native bindings
|
|
146
|
-
npm install @ingissa/navcore-headless # Node.js / CI testing
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
> ⚠️ Renderer adapter packages require a commercial license. [Get yours at
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Feature Reference
|
|
154
|
-
|
|
155
|
-
### CORE — free, MIT
|
|
156
|
-
|
|
157
|
-
| Feature | API |
|
|
158
|
-
|---|---|
|
|
159
|
-
| GPS smoothing | `KalmanFilter2D` |
|
|
160
|
-
| Route snapping | `RouteSnapper` |
|
|
161
|
-
| Haversine & bearing | `haversineDistance`, `getBearingBetweenPoints` |
|
|
162
|
-
| Cumulative distances | `buildForwardCumulativeDistances` |
|
|
163
|
-
| Route builder | `CustomRouteBuilder` |
|
|
164
|
-
| Directions — OSRM | `OSRMDirectionsProvider` |
|
|
165
|
-
| Directions — Valhalla | `ValhallaDirectionsProvider` |
|
|
166
|
-
| Directions — ORS | `OpenRouteServiceProvider` |
|
|
167
|
-
| Renderer interface | `MapRendererAdapter` |
|
|
168
|
-
| License verification | `LicenseManager` |
|
|
169
|
-
|
|
170
|
-
### PRO — commercial license required
|
|
171
|
-
|
|
172
|
-
| Feature | API | FEATURES key |
|
|
173
|
-
|---|---|---|
|
|
174
|
-
| Advanced snapping (bearing + inertia) | `RouteSnapper` options | `ADVANCED_SNAPPING` |
|
|
175
|
-
| Parallel road resolver | `ParallelRoadResolver` | `PARALLEL_ROAD_RESOLUTION` |
|
|
176
|
-
| Dead reckoning | `DeadReckoningEngine` | `DEAD_RECKONING` |
|
|
177
|
-
| Compound deviation detection | `DeviationDetector` | `COMPOUND_DEVIATION` |
|
|
178
|
-
| Advanced bearing engine | `BearingEngine` | `ADVANCED_BEARING` |
|
|
179
|
-
| Progress anti-regression | `ProgressTracker` | `PROGRESS_TRACKING` |
|
|
180
|
-
| Route offset predictor | `RouteOffsetPredictor` | `ROUTE_OFFSET` |
|
|
181
|
-
| Navigation lifecycle FSM | `NavigationFSM` | — |
|
|
182
|
-
| Off-route FSM | `OffRouteFSM` | — |
|
|
183
|
-
| Instruction editor | `InstructionEditor` | — |
|
|
184
|
-
| Voice trigger engine | `VoiceTriggerEngine` | `VOICE_TRIGGERS` |
|
|
185
|
-
| ETA engine (offline) | `ETAEngine` | `ETA` |
|
|
186
|
-
| Geofencing engine | `GeofencingEngine` | `GEOFENCING` |
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## License Gate Behaviour
|
|
191
|
-
|
|
192
|
-
| Condition | Result |
|
|
193
|
-
|---|---|
|
|
194
|
-
| No `licenseKey` | Core features work; Pro features blocked with `console.warn` |
|
|
195
|
-
| Valid Pro key | All features unlocked; `🔑 [NavCore] License validation complete. Plan: pro` logged |
|
|
196
|
-
| `isDev: true` | All features bypassed (development mode) |
|
|
197
|
-
| Expired / tampered key | Pro features blocked; status: `EXPIRED` / `INVALID` |
|
|
198
|
-
|
|
199
|
-
License keys are verified **offline** using ECDSA P-256 — no network calls, no tracking.
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## NavCore Engine API
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
const engine = new NavCore(options?: NavCoreOptions);
|
|
207
|
-
|
|
208
|
-
// Setup
|
|
209
|
-
engine.setRoute(coords: Coordinate[], instructions?: NavInstruction[]);
|
|
210
|
-
engine.startNavigation();
|
|
211
|
-
|
|
212
|
-
// GPS update (returns state snapshot)
|
|
213
|
-
const state: NavCoreState = engine.update(gpsUpdate: NavGpsUpdate);
|
|
214
|
-
|
|
215
|
-
// Render tick (call at ~60fps between GPS updates)
|
|
216
|
-
engine.tick(nowMs: number);
|
|
217
|
-
|
|
218
|
-
// Events
|
|
219
|
-
engine.on('instruction', (instr: NavInstruction) => void);
|
|
220
|
-
engine.on('deviation', ({ anchorIndex, ghostCoord }) => void);
|
|
221
|
-
engine.on('arrival', () => void);
|
|
222
|
-
engine.on('routeLoaded', ({ routeLength }) => void);
|
|
223
|
-
engine.off(event, handler);
|
|
224
|
-
|
|
225
|
-
// State & introspection
|
|
226
|
-
engine.getState(): NavCoreState;
|
|
227
|
-
engine.getLicenseManager(): LicenseManager;
|
|
228
|
-
engine.getRoute(): ReadonlyArray<Coordinate>;
|
|
229
|
-
engine.destroy();
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### NavCoreState
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
interface NavCoreState {
|
|
236
|
-
snappedCoord: Coordinate | null; // GPS-snapped position on route
|
|
237
|
-
rawGpsCoord: Coordinate | null; // Raw GPS position
|
|
238
|
-
bearing: number; // Smoothed heading (degrees)
|
|
239
|
-
routeIndex: number; // Current segment index
|
|
240
|
-
distanceToRoute: number; // Distance from route (metres)
|
|
241
|
-
corridor: number; // Dynamic off-route threshold (metres)
|
|
242
|
-
isOffRoute: boolean; // True when deviation confirmed
|
|
243
|
-
hasArrived: boolean;
|
|
244
|
-
distanceToDestination: number | null; // Remaining distance (metres)
|
|
245
|
-
distanceToNextInstruction: number | null; // Distance to next turn
|
|
246
|
-
nextInstructionIndex: number | null;
|
|
247
|
-
currentSpeed: number; // m/s
|
|
248
|
-
isGpsStale: boolean;
|
|
249
|
-
gpsAgeMs: number;
|
|
250
|
-
licenseStatus: string; // 'VALID' | 'INVALID' | 'EXPIRED' | 'MISSING'
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Documentation
|
|
257
|
-
|
|
258
|
-
| Guide | Link |
|
|
259
|
-
|---|---|
|
|
260
|
-
| Architecture | [docs/architecture.md](./docs/architecture.md) |
|
|
261
|
-
| Getting Started | [docs/getting-started.md](./docs/getting-started.md) |
|
|
262
|
-
| Directions Providers | [docs/directions-providers.md](./docs/directions-providers.md) |
|
|
263
|
-
| Renderer Adapters | [docs/renderer-adapters.md](./docs/renderer-adapters.md) |
|
|
264
|
-
| API Reference | [docs/api-reference.md](./docs/api-reference.md) |
|
|
265
|
-
| Custom Routes | [docs/custom-routes.md](./docs/custom-routes.md) |
|
|
266
|
-
| Instructions & Voice | [docs/instructions.md](./docs/instructions.md) |
|
|
267
|
-
| Pro Features | [docs/p3-features.md](./docs/p3-features.md) |
|
|
268
|
-
| Testing Guide | [docs/testing.md](./docs/testing.md) |
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## License
|
|
273
|
-
|
|
274
|
-
- **Core engine** (`@ingissa/navcore-core`): [MIT](./LICENSE) for open-source projects
|
|
275
|
-
- **Pro features & adapters**: Commercial license required — [
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
© Ingissa · [
|
|
1
|
+
# @ingissa/navcore-core
|
|
2
|
+
|
|
3
|
+
> **The navigation layer you own.** A framework-agnostic, zero-dependency navigation engine that runs everywhere — browsers, React Native, Node.js, and CI/CD.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ingissa/navcore-core)
|
|
6
|
+
[]()
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://www.npmjs.com/package/@ingissa/navcore-core)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why NavCore?
|
|
13
|
+
|
|
14
|
+
| Feature | NavCore | Mapbox Nav SDK | Google Maps |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| **Framework** | Any (pure TS) | React Native only | Platform-locked |
|
|
17
|
+
| **Runtime deps** | 0 | 50+ | N/A |
|
|
18
|
+
| **Map renderer** | BYO (adapter) | Mapbox GL only | Google Maps only |
|
|
19
|
+
| **Directions** | BYO (adapter) | Mapbox API only | Google API only |
|
|
20
|
+
| **Off-route detection** | Built-in FSM | Basic | Basic |
|
|
21
|
+
| **U-turn protection** | ✅ Bearing penalty | ❌ | ❌ |
|
|
22
|
+
| **Parallel road resolver** | ✅ Built-in | ❌ | ❌ |
|
|
23
|
+
| **Dead reckoning** | ✅ Built-in | ❌ | ❌ |
|
|
24
|
+
| **Geofencing** | ✅ Built-in | ❌ | Paid API |
|
|
25
|
+
| **ETA** | ✅ Offline | API only | API only |
|
|
26
|
+
| **Voice triggers** | ✅ Built-in | SDK only | ❌ |
|
|
27
|
+
| **Headless testing** | ✅ Full pipeline | ❌ | ❌ |
|
|
28
|
+
| **Custom routes** | ✅ GPX/GeoJSON/Builder | ❌ | ❌ |
|
|
29
|
+
| **License** | Core MIT / Pro commercial | Commercial | Commercial |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Tier Model
|
|
34
|
+
|
|
35
|
+
NavCore uses a two-tier model controlled by a signed license key:
|
|
36
|
+
|
|
37
|
+
| Tier | Import | License | Includes |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| **Core** | `@ingissa/navcore-core/core` | MIT — free forever | Geo utils, Kalman filter, route snapping, directions adapters, route builder |
|
|
40
|
+
| **Pro** | `@ingissa/navcore-core/pro` | Commercial key required | Dead reckoning, advanced bearing, deviation detection, FSMs, voice, ETA, geofencing |
|
|
41
|
+
| **Engine** | `@ingissa/navcore-core` | Gated internally at runtime | Full `NavCore` orchestrator (license key unlocks Pro features) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @ingissa/navcore-core
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Core tier (no license needed)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { NavCore } from '@ingissa/navcore-core';
|
|
59
|
+
import { OSRMDirectionsProvider } from '@ingissa/navcore-core/core';
|
|
60
|
+
|
|
61
|
+
// 1. Get a route from your self-hosted OSRM (free)
|
|
62
|
+
const provider = new OSRMDirectionsProvider({ baseUrl: 'http://router.project-osrm.org' });
|
|
63
|
+
const route = await provider.getRoute([[2.3522, 48.8566], [2.3400, 48.8700]]);
|
|
64
|
+
|
|
65
|
+
// 2. Create engine (Core features work without a key)
|
|
66
|
+
const engine = new NavCore();
|
|
67
|
+
engine.setRoute(route.geometry);
|
|
68
|
+
engine.startNavigation();
|
|
69
|
+
|
|
70
|
+
// 3. Feed GPS updates
|
|
71
|
+
engine.on('arrival', () => console.log('Arrived!'));
|
|
72
|
+
|
|
73
|
+
navigator.geolocation.watchPosition(({ coords }) => {
|
|
74
|
+
const state = engine.update({
|
|
75
|
+
coord: [coords.longitude, coords.latitude],
|
|
76
|
+
accuracy: coords.accuracy,
|
|
77
|
+
bearing: coords.heading,
|
|
78
|
+
speed: coords.speed ?? 0,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log('Distance remaining:', state.distanceToDestination, 'm');
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Pro tier (with license key)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { NavCore } from '@ingissa/navcore-core';
|
|
90
|
+
|
|
91
|
+
// Pro features unlock automatically with a valid key
|
|
92
|
+
const engine = new NavCore({ licenseKey: 'your-signed-license-key' });
|
|
93
|
+
engine.setRoute(routeCoords, instructions);
|
|
94
|
+
engine.startNavigation();
|
|
95
|
+
|
|
96
|
+
engine.on('instruction', (instr) => speak(instr.text)); // voice cues (PRO)
|
|
97
|
+
engine.on('deviation', ({ anchorIndex }) => recalculate(anchorIndex));
|
|
98
|
+
engine.on('arrival', () => console.log('Arrived!'));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Using subpath imports directly
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Free MIT tier
|
|
105
|
+
import { KalmanFilter2D } from '@ingissa/navcore-core/core';
|
|
106
|
+
import { OSRMDirectionsProvider } from '@ingissa/navcore-core/core';
|
|
107
|
+
import { CustomRouteBuilder } from '@ingissa/navcore-core/core';
|
|
108
|
+
|
|
109
|
+
// Commercial Pro tier
|
|
110
|
+
import { DeadReckoningEngine } from '@ingissa/navcore-core/pro';
|
|
111
|
+
import { VoiceTriggerEngine } from '@ingissa/navcore-core/pro';
|
|
112
|
+
import { ETAEngine } from '@ingissa/navcore-core/pro';
|
|
113
|
+
import { GeofencingEngine } from '@ingissa/navcore-core/pro';
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Directions Providers (built-in, no extra install)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import {
|
|
122
|
+
OSRMDirectionsProvider, // Self-hosted OSRM (recommended, free)
|
|
123
|
+
ValhallaDirectionsProvider, // Self-hosted Valhalla (free)
|
|
124
|
+
OpenRouteServiceProvider, // openrouteservice.org (free tier)
|
|
125
|
+
} from '@ingissa/navcore-core/core';
|
|
126
|
+
|
|
127
|
+
// OSRM
|
|
128
|
+
const osrm = new OSRMDirectionsProvider({ baseUrl: 'http://router.project-osrm.org' });
|
|
129
|
+
const route = await osrm.getRoute([[lng1, lat1], [lng2, lat2]]);
|
|
130
|
+
|
|
131
|
+
// Valhalla
|
|
132
|
+
const valhalla = new ValhallaDirectionsProvider({ baseUrl: 'https://valhalla1.openstreetmap.de' });
|
|
133
|
+
const route = await valhalla.getRoute([[lng1, lat1], [lng2, lat2]], { costing: 'auto' });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Renderer Adapters (separate packages)
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm install @ingissa/navcore-maplibre # Web + MapLibre GL
|
|
142
|
+
npm install @ingissa/navcore-leaflet # Web + Leaflet.js
|
|
143
|
+
npm install @ingissa/navcore-google-maps # Google Maps JS API
|
|
144
|
+
npm install @ingissa/navcore-mapbox # React Native + @rnmapbox/maps
|
|
145
|
+
npm install @ingissa/navcore-react-native # React Native bindings
|
|
146
|
+
npm install @ingissa/navcore-headless # Node.js / CI testing
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> ⚠️ Renderer adapter packages require a commercial license. [Get yours at Not yet available]()
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Feature Reference
|
|
154
|
+
|
|
155
|
+
### CORE — free, MIT
|
|
156
|
+
|
|
157
|
+
| Feature | API |
|
|
158
|
+
|---|---|
|
|
159
|
+
| GPS smoothing | `KalmanFilter2D` |
|
|
160
|
+
| Route snapping | `RouteSnapper` |
|
|
161
|
+
| Haversine & bearing | `haversineDistance`, `getBearingBetweenPoints` |
|
|
162
|
+
| Cumulative distances | `buildForwardCumulativeDistances` |
|
|
163
|
+
| Route builder | `CustomRouteBuilder` |
|
|
164
|
+
| Directions — OSRM | `OSRMDirectionsProvider` |
|
|
165
|
+
| Directions — Valhalla | `ValhallaDirectionsProvider` |
|
|
166
|
+
| Directions — ORS | `OpenRouteServiceProvider` |
|
|
167
|
+
| Renderer interface | `MapRendererAdapter` |
|
|
168
|
+
| License verification | `LicenseManager` |
|
|
169
|
+
|
|
170
|
+
### PRO — commercial license required
|
|
171
|
+
|
|
172
|
+
| Feature | API | FEATURES key |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| Advanced snapping (bearing + inertia) | `RouteSnapper` options | `ADVANCED_SNAPPING` |
|
|
175
|
+
| Parallel road resolver | `ParallelRoadResolver` | `PARALLEL_ROAD_RESOLUTION` |
|
|
176
|
+
| Dead reckoning | `DeadReckoningEngine` | `DEAD_RECKONING` |
|
|
177
|
+
| Compound deviation detection | `DeviationDetector` | `COMPOUND_DEVIATION` |
|
|
178
|
+
| Advanced bearing engine | `BearingEngine` | `ADVANCED_BEARING` |
|
|
179
|
+
| Progress anti-regression | `ProgressTracker` | `PROGRESS_TRACKING` |
|
|
180
|
+
| Route offset predictor | `RouteOffsetPredictor` | `ROUTE_OFFSET` |
|
|
181
|
+
| Navigation lifecycle FSM | `NavigationFSM` | — |
|
|
182
|
+
| Off-route FSM | `OffRouteFSM` | — |
|
|
183
|
+
| Instruction editor | `InstructionEditor` | — |
|
|
184
|
+
| Voice trigger engine | `VoiceTriggerEngine` | `VOICE_TRIGGERS` |
|
|
185
|
+
| ETA engine (offline) | `ETAEngine` | `ETA` |
|
|
186
|
+
| Geofencing engine | `GeofencingEngine` | `GEOFENCING` |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## License Gate Behaviour
|
|
191
|
+
|
|
192
|
+
| Condition | Result |
|
|
193
|
+
|---|---|
|
|
194
|
+
| No `licenseKey` | Core features work; Pro features blocked with `console.warn` |
|
|
195
|
+
| Valid Pro key | All features unlocked; `🔑 [NavCore] License validation complete. Plan: pro` logged |
|
|
196
|
+
| `isDev: true` | All features bypassed (development mode) |
|
|
197
|
+
| Expired / tampered key | Pro features blocked; status: `EXPIRED` / `INVALID` |
|
|
198
|
+
|
|
199
|
+
License keys are verified **offline** using ECDSA P-256 — no network calls, no tracking.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## NavCore Engine API
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const engine = new NavCore(options?: NavCoreOptions);
|
|
207
|
+
|
|
208
|
+
// Setup
|
|
209
|
+
engine.setRoute(coords: Coordinate[], instructions?: NavInstruction[]);
|
|
210
|
+
engine.startNavigation();
|
|
211
|
+
|
|
212
|
+
// GPS update (returns state snapshot)
|
|
213
|
+
const state: NavCoreState = engine.update(gpsUpdate: NavGpsUpdate);
|
|
214
|
+
|
|
215
|
+
// Render tick (call at ~60fps between GPS updates)
|
|
216
|
+
engine.tick(nowMs: number);
|
|
217
|
+
|
|
218
|
+
// Events
|
|
219
|
+
engine.on('instruction', (instr: NavInstruction) => void);
|
|
220
|
+
engine.on('deviation', ({ anchorIndex, ghostCoord }) => void);
|
|
221
|
+
engine.on('arrival', () => void);
|
|
222
|
+
engine.on('routeLoaded', ({ routeLength }) => void);
|
|
223
|
+
engine.off(event, handler);
|
|
224
|
+
|
|
225
|
+
// State & introspection
|
|
226
|
+
engine.getState(): NavCoreState;
|
|
227
|
+
engine.getLicenseManager(): LicenseManager;
|
|
228
|
+
engine.getRoute(): ReadonlyArray<Coordinate>;
|
|
229
|
+
engine.destroy();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### NavCoreState
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface NavCoreState {
|
|
236
|
+
snappedCoord: Coordinate | null; // GPS-snapped position on route
|
|
237
|
+
rawGpsCoord: Coordinate | null; // Raw GPS position
|
|
238
|
+
bearing: number; // Smoothed heading (degrees)
|
|
239
|
+
routeIndex: number; // Current segment index
|
|
240
|
+
distanceToRoute: number; // Distance from route (metres)
|
|
241
|
+
corridor: number; // Dynamic off-route threshold (metres)
|
|
242
|
+
isOffRoute: boolean; // True when deviation confirmed
|
|
243
|
+
hasArrived: boolean;
|
|
244
|
+
distanceToDestination: number | null; // Remaining distance (metres)
|
|
245
|
+
distanceToNextInstruction: number | null; // Distance to next turn
|
|
246
|
+
nextInstructionIndex: number | null;
|
|
247
|
+
currentSpeed: number; // m/s
|
|
248
|
+
isGpsStale: boolean;
|
|
249
|
+
gpsAgeMs: number;
|
|
250
|
+
licenseStatus: string; // 'VALID' | 'INVALID' | 'EXPIRED' | 'MISSING'
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Documentation
|
|
257
|
+
|
|
258
|
+
| Guide | Link |
|
|
259
|
+
|---|---|
|
|
260
|
+
| Architecture | [docs/architecture.md](./docs/architecture.md) |
|
|
261
|
+
| Getting Started | [docs/getting-started.md](./docs/getting-started.md) |
|
|
262
|
+
| Directions Providers | [docs/directions-providers.md](./docs/directions-providers.md) |
|
|
263
|
+
| Renderer Adapters | [docs/renderer-adapters.md](./docs/renderer-adapters.md) |
|
|
264
|
+
| API Reference | [docs/api-reference.md](./docs/api-reference.md) |
|
|
265
|
+
| Custom Routes | [docs/custom-routes.md](./docs/custom-routes.md) |
|
|
266
|
+
| Instructions & Voice | [docs/instructions.md](./docs/instructions.md) |
|
|
267
|
+
| Pro Features | [docs/p3-features.md](./docs/p3-features.md) |
|
|
268
|
+
| Testing Guide | [docs/testing.md](./docs/testing.md) |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
- **Core engine** (`@ingissa/navcore-core`): [MIT](./LICENSE) for open-source projects
|
|
275
|
+
- **Pro features & adapters**: Commercial license required — [Not yet available]()
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
© Ingissa · [8abnav@gmail.com](mailto:8abnav@gmail.com)
|
package/dist/core.js
CHANGED
|
@@ -4,5 +4,4 @@ ${this.waypoints.map(e=>` <wpt lat="${e.coord[1]}" lon="${e.coord[0]}">`+(e.o
|
|
|
4
4
|
<name>${e.options.name}</name>`:"")+`
|
|
5
5
|
</wpt>`).join(`
|
|
6
6
|
`)}
|
|
7
|
-
</gpx>`}fromGeoJSON(t){if(this.waypoints=[],t.type==="FeatureCollection"&&t.features){for(let e of t.features)if(e.geometry.type==="Point"){let n=e.geometry.coordinates;this.addWaypoint(n);}else if(e.geometry.type==="LineString"){let n=e.geometry.coordinates;for(let r of n)this.addWaypoint(r);}}}toGeoJSON(){let t=this.waypoints.map((n,r)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:r,name:n.options?.name??null,silent:n.options?.silent??false}})),e={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...t,e]}}clear(){this.waypoints=[];}};var O={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var g=class g{constructor(t,e){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=e??false,t&&this.validate(t);}async validate(t){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[e,n]=t.split(".");if(!e||!n)return this.status="INVALID",this.status;let r=atob(e);if(this.payload=JSON.parse(r),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(e,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let t of this.payload.f)this.enabledFeatures.add(t);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(t){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(t)}warnOnce(t){this.warnedFeatures.has(t)||(console.warn(`[NavCore] Feature '${t}' requires a Pro license
|
|
8
|
-
exports.Constants=W;exports.CustomRouteBuilder=v;exports.FEATURES=O;exports.KalmanFilter2D=S;exports.LicenseManager=I;exports.OSRMDirectionsProvider=D;exports.OpenRouteServiceProvider=M;exports.RouteSnapper=E;exports.ValhallaDirectionsProvider=_;exports.buildForwardCumulativeDistances=z;exports.buildReverseCumulativeDistances=Y;exports.destinationPoint=X;exports.geoGetBearing=j;exports.getBearingBetweenPoints=x;exports.haversineDistance=f;exports.interpolatePoint=q;exports.kalmanUpdate2D=L;exports.normalizeAngle=P;exports.projectPointOnSegment=A;
|
|
7
|
+
</gpx>`}fromGeoJSON(t){if(this.waypoints=[],t.type==="FeatureCollection"&&t.features){for(let e of t.features)if(e.geometry.type==="Point"){let n=e.geometry.coordinates;this.addWaypoint(n);}else if(e.geometry.type==="LineString"){let n=e.geometry.coordinates;for(let r of n)this.addWaypoint(r);}}}toGeoJSON(){let t=this.waypoints.map((n,r)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:r,name:n.options?.name??null,silent:n.options?.silent??false}})),e={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...t,e]}}clear(){this.waypoints=[];}};var O={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var g=class g{constructor(t,e){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=e??false,t&&this.validate(t);}async validate(t){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[e,n]=t.split(".");if(!e||!n)return this.status="INVALID",this.status;let r=atob(e);if(this.payload=JSON.parse(r),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(e,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let t of this.payload.f)this.enabledFeatures.add(t);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(t){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(t)}warnOnce(t){this.warnedFeatures.has(t)||(console.warn(`[NavCore] Feature '${t}' requires a Pro license`),this.warnedFeatures.add(t));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(O.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(t,e){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let r=new TextEncoder().encode(t),s=Uint8Array.from(atob(e),a=>a.charCodeAt(0)),l=Uint8Array.from(atob(g.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),p=await crypto.subtle.importKey("spki",l,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},p,s,r)}catch{return false}}};g.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var I=g;exports.Constants=W;exports.CustomRouteBuilder=v;exports.FEATURES=O;exports.KalmanFilter2D=S;exports.LicenseManager=I;exports.OSRMDirectionsProvider=D;exports.OpenRouteServiceProvider=M;exports.RouteSnapper=E;exports.ValhallaDirectionsProvider=_;exports.buildForwardCumulativeDistances=z;exports.buildReverseCumulativeDistances=Y;exports.destinationPoint=X;exports.geoGetBearing=j;exports.getBearingBetweenPoints=x;exports.haversineDistance=f;exports.interpolatePoint=q;exports.kalmanUpdate2D=L;exports.normalizeAngle=P;exports.projectPointOnSegment=A;
|
package/dist/core.mjs
CHANGED
|
@@ -4,5 +4,4 @@ ${this.waypoints.map(e=>` <wpt lat="${e.coord[1]}" lon="${e.coord[0]}">`+(e.o
|
|
|
4
4
|
<name>${e.options.name}</name>`:"")+`
|
|
5
5
|
</wpt>`).join(`
|
|
6
6
|
`)}
|
|
7
|
-
</gpx>`}fromGeoJSON(t){if(this.waypoints=[],t.type==="FeatureCollection"&&t.features){for(let e of t.features)if(e.geometry.type==="Point"){let n=e.geometry.coordinates;this.addWaypoint(n);}else if(e.geometry.type==="LineString"){let n=e.geometry.coordinates;for(let r of n)this.addWaypoint(r);}}}toGeoJSON(){let t=this.waypoints.map((n,r)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:r,name:n.options?.name??null,silent:n.options?.silent??false}})),e={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...t,e]}}clear(){this.waypoints=[];}};var O={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var g=class g{constructor(t,e){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=e??false,t&&this.validate(t);}async validate(t){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[e,n]=t.split(".");if(!e||!n)return this.status="INVALID",this.status;let r=atob(e);if(this.payload=JSON.parse(r),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(e,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let t of this.payload.f)this.enabledFeatures.add(t);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(t){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(t)}warnOnce(t){this.warnedFeatures.has(t)||(console.warn(`[NavCore] Feature '${t}' requires a Pro license
|
|
8
|
-
export{W as Constants,v as CustomRouteBuilder,O as FEATURES,S as KalmanFilter2D,I as LicenseManager,D as OSRMDirectionsProvider,M as OpenRouteServiceProvider,E as RouteSnapper,_ as ValhallaDirectionsProvider,z as buildForwardCumulativeDistances,Y as buildReverseCumulativeDistances,X as destinationPoint,j as geoGetBearing,x as getBearingBetweenPoints,f as haversineDistance,q as interpolatePoint,L as kalmanUpdate2D,P as normalizeAngle,A as projectPointOnSegment};
|
|
7
|
+
</gpx>`}fromGeoJSON(t){if(this.waypoints=[],t.type==="FeatureCollection"&&t.features){for(let e of t.features)if(e.geometry.type==="Point"){let n=e.geometry.coordinates;this.addWaypoint(n);}else if(e.geometry.type==="LineString"){let n=e.geometry.coordinates;for(let r of n)this.addWaypoint(r);}}}toGeoJSON(){let t=this.waypoints.map((n,r)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:r,name:n.options?.name??null,silent:n.options?.silent??false}})),e={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...t,e]}}clear(){this.waypoints=[];}};var O={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var g=class g{constructor(t,e){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=e??false,t&&this.validate(t);}async validate(t){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[e,n]=t.split(".");if(!e||!n)return this.status="INVALID",this.status;let r=atob(e);if(this.payload=JSON.parse(r),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(e,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let t of this.payload.f)this.enabledFeatures.add(t);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(t){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(t)}warnOnce(t){this.warnedFeatures.has(t)||(console.warn(`[NavCore] Feature '${t}' requires a Pro license`),this.warnedFeatures.add(t));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(O.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(t,e){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let r=new TextEncoder().encode(t),s=Uint8Array.from(atob(e),a=>a.charCodeAt(0)),l=Uint8Array.from(atob(g.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),p=await crypto.subtle.importKey("spki",l,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},p,s,r)}catch{return false}}};g.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var I=g;export{W as Constants,v as CustomRouteBuilder,O as FEATURES,S as KalmanFilter2D,I as LicenseManager,D as OSRMDirectionsProvider,M as OpenRouteServiceProvider,E as RouteSnapper,_ as ValhallaDirectionsProvider,z as buildForwardCumulativeDistances,Y as buildReverseCumulativeDistances,X as destinationPoint,j as geoGetBearing,x as getBearingBetweenPoints,f as haversineDistance,q as interpolatePoint,L as kalmanUpdate2D,P as normalizeAngle,A as projectPointOnSegment};
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,5 @@ ${this.waypoints.map(t=>` <wpt lat="${t.coord[1]}" lon="${t.coord[0]}">`+(t.o
|
|
|
4
4
|
<name>${t.options.name}</name>`:"")+`
|
|
5
5
|
</wpt>`).join(`
|
|
6
6
|
`)}
|
|
7
|
-
</gpx>`}fromGeoJSON(e){if(this.waypoints=[],e.type==="FeatureCollection"&&e.features){for(let t of e.features)if(t.geometry.type==="Point"){let n=t.geometry.coordinates;this.addWaypoint(n);}else if(t.geometry.type==="LineString"){let n=t.geometry.coordinates;for(let i of n)this.addWaypoint(i);}}}toGeoJSON(){let e=this.waypoints.map((n,i)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:i,name:n.options?.name??null,silent:n.options?.silent??false}})),t={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...e,t]}}clear(){this.waypoints=[];}};var ae=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),profile:e.profile??"driving",maxWaypointsPerChunk:e.maxWaypointsPerChunk??100};}async getRoute(e,t){if(e.length<2)throw new Error("[OSRM] At least 2 waypoints required");let n=t?.profile??this.config.profile;return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}async fetchRoute(e,t,n){let i=e.map(d=>`${d[0]},${d[1]}`).join(";"),r=n?.steps!==false?"true":"false",o=`${this.config.baseUrl}/route/v1/${t}/${i}?overview=full&geometries=geojson&steps=${r}&annotations=false`,c=await fetch(o);if(!c.ok)throw new Error(`[OSRM] HTTP ${c.status}: ${c.statusText}`);let a=await c.json();if(a.code!=="Ok"||!a.routes?.length)throw new Error(`[OSRM] No route found: ${a.code}`);let u=a.routes[0];return {geometry:u.geometry.coordinates,distance:u.distance,duration:u.duration,legs:(u.legs??[]).map(d=>({distance:d.distance,duration:d.duration,steps:(d.steps??[]).map(l=>({distance:l.distance,duration:l.duration,geometry:l.geometry?.coordinates??[],maneuver:{type:l.maneuver?.type??"unknown",modifier:l.maneuver?.modifier,location:l.maneuver?.location??[0,0],bearingBefore:l.maneuver?.bearing_before??0,bearingAfter:l.maneuver?.bearing_after??0,instruction:l.name??void 0},name:l.name}))})),raw:a}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var ce=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),costing:e.costing??"auto",apiKey:e.apiKey??"",maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[Valhalla] At least 2 waypoints required");let n=this.profileToCosting(t?.profile);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileToCosting(e){switch(e){case "cycling":return "bicycle";case "walking":return "pedestrian";default:return this.config.costing}}async fetchRoute(e,t,n){let i=e.map(p=>({lon:p[0],lat:p[1],type:"break"})),r=JSON.stringify({locations:i,costing:t,directions_options:{units:"kilometers"}}),o={"Content-Type":"application/json"};this.config.apiKey&&(o.api_key=this.config.apiKey);let c=await fetch(`${this.config.baseUrl}/route`,{method:"POST",headers:o,body:r});if(!c.ok){let p=await c.text();throw new Error(`[Valhalla] HTTP ${c.status}: ${p}`)}let a=await c.json(),u=this.decodePolyline(a.trip?.legs?.[0]?.shape??""),d=a.trip?.summary??{},l=(a.trip?.legs??[]).map(p=>({distance:(p.summary?.length??0)*1e3,duration:p.summary?.time??0,steps:(p.maneuvers??[]).map(g=>({distance:(g.length??0)*1e3,duration:g.time??0,geometry:[],maneuver:{type:g.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:g.begin_heading??0,bearingAfter:g.end_heading??0,instruction:g.instruction},name:g.street_names?.[0]}))}));return {geometry:u,distance:(d.length??0)*1e3,duration:d.time??0,legs:l,raw:a}}decodePolyline(e,t=6){let n=Math.pow(10,t),i=[],r=0,o=0,c=0;for(;r<e.length;){let a=0,u=0,d;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let l=(u&1)!==0?~(u>>1):u>>1;o+=l,a=0,u=0;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let p=(u&1)!==0?~(u>>1):u>>1;c+=p,i.push([c/n,o/n]);}return i}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var Ze="https://api.openrouteservice.org/v2/directions";var le=class{constructor(e){this.config={apiKey:e.apiKey,profile:e.profile??"driving-car",baseUrl:e.baseUrl??Ze,maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[ORS] At least 2 waypoints required");let n=this.profileFromOptions(t);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileFromOptions(e){switch(e?.profile){case "cycling":return "cycling-regular";case "walking":return "foot-walking";default:return this.config.profile}}async fetchRoute(e,t,n){let i=JSON.stringify({coordinates:e,instructions:n?.steps!==false,language:n?.language??"en"}),r=await fetch(`${this.config.baseUrl}/${t}/geojson`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this.config.apiKey},body:i});if(!r.ok){let l=await r.text();throw new Error(`[ORS] HTTP ${r.status}: ${l}`)}let o=await r.json(),c=o.features?.[0];if(!c)throw new Error("[ORS] No route returned");let a=c.properties?.summary??{},d=(c.properties?.segments??[]).map(l=>({distance:l.distance??0,duration:l.duration??0,steps:(l.steps??[]).map(p=>({distance:p.distance??0,duration:p.duration??0,geometry:[],maneuver:{type:p.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:0,bearingAfter:0,instruction:p.instruction},name:p.name}))}));return {geometry:c.geometry?.coordinates??[],distance:a.distance??0,duration:a.duration??0,legs:d,raw:o}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var et={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},ue=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...et,...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),o=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&o<=this.options.reannounceMeters&&o>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,o,"low")):null;if(o>this.options.earlyTriggerMeters)return null;if(o<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=o<30?"urgent":o<60?"high":"normal";return this.makeCue(i,o,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 tt={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},de=class{constructor(e){this.speedSamples=[];this.options={...tt,...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,o=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:o}}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 nt={dwellThresholdMs:1e4},pe=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...nt,...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 o=this.states.get(i),c=this.contains(r,e);if(!o.inside&&c)o.inside=true,o.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(o.inside&&!c){o.inside=false;let a=o.enteredAtMs?n-o.enteredAtMs:0;o.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(o.inside&&o.enteredAtMs!==null){let a=n-o.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),o.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"?m(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,o=t.length;for(let c=0,a=o-1;c<o;a=c++){let[u,d]=t[c],[l,p]=t[a];d>i!=p>i&&n<(l-u)*(i-d)/(p-d)+u&&(r=!r);}return r}};var E={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var B=class B{constructor(e,t){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=t??false,e&&this.validate(e);}async validate(e){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[t,n]=e.split(".");if(!t||!n)return this.status="INVALID",this.status;let i=atob(t);if(this.payload=JSON.parse(i),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(t,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let e of this.payload.f)this.enabledFeatures.add(e);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(e){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(e)}warnOnce(e){this.warnedFeatures.has(e)||(console.warn(`[NavCore] Feature '${e}' requires a Pro license \u2192 upgrade at https://navcore.dev/pricing`),this.warnedFeatures.add(e));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(E.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(e,t){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let i=new TextEncoder().encode(e),r=Uint8Array.from(atob(t),a=>a.charCodeAt(0)),o=Uint8Array.from(atob(B.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),c=await crypto.subtle.importKey("spki",o,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},c,r,i)}catch{return false}}};B.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var G=B;var he=class{constructor(e){this.route=[];this.instructions=[];this.forwardCum=new Float64Array(0);this.reverseCum=new Float64Array(0);this.snappedCoord=null;this.rawGpsCoord=null;this.bearing=0;this.rawGpsBearing=null;this.routeIndex=0;this.maxProgressIdx=0;this.prevSegment=null;this.distanceToRoute=0;this.corridor=25;this.prevDistToNext=null;this.currentSpeed=0;this.lastGpsTimestamp=0;this.instrIdx=0;this.linearOffset=0;this.hasArrived=false;this.listeners=new Map;this.options={licenseKey:e?.licenseKey??"",isDev:e?.isDev??false,isCircuit:e?.isCircuit??false,kalmanQ:e?.kalmanQ??1e-5,kalmanRBase:e?.kalmanRBase??5e-5,baseCorridorMeters:e?.baseCorridorMeters??25,maxCorridorMeters:e?.maxCorridorMeters??60,arrivalThresholdMeters:e?.arrivalThresholdMeters??30,bearingSmoothingAlpha:e?.bearingSmoothingAlpha??.3},this.kalman=new M(this.options.kalmanQ,this.options.kalmanRBase),this.snapper=new T({useBearingPenalty:true,useInertiaPenalty:true}),this.deadReckoning=new D,this.deviationDetector=new N,this.lifecycleFSM=new w,this.offRouteFSM=new F,this.license=new G(this.options.licenseKey,this.options.isDev);}has(e){let t=this.license.hasFeature(e);return t||this.license.warnOnce(e),t}setRoute(e,t){this.route=e,this.instructions=t??[],this.forwardCum=K(e),this.reverseCum=j(e),this.routeIndex=0,this.maxProgressIdx=0,this.prevSegment=null,this.instrIdx=0,this.linearOffset=0,this.hasArrived=false,this.snappedCoord=null,this.prevDistToNext=null,this.lifecycleFSM.dispatch({type:"ROUTE_LOADED"}),this.emit("routeLoaded",{routeLength:e.length});}startNavigation(){this.lifecycleFSM.dispatch({type:"USER_START_NAVIGATION"}),this.emit("navigationStarted");}update(e){if(this.rawGpsCoord=e.coord,this.rawGpsBearing=e.bearing,this.currentSpeed=e.speed,this.lastGpsTimestamp=e.timestamp,this.route.length<2)return this.getState();let[t,n]=this.kalman.update(e.coord[0],e.coord[1],e.accuracy??10,e.timestamp),i=[t,n],r=Math.max(0,this.routeIndex-2),o=this.snapper.snap(i,this.route,r,25,this.options.isCircuit,this.maxProgressIdx,this.routeIndex,e.bearing,this.prevSegment);this.corridor=Z(e.speed,e.accuracy,this.options.baseCorridorMeters,this.options.maxCorridorMeters);let c=te(this.routeIndex,o.idx,this.route,o.dist,80);if(this.prevSegment=this.routeIndex,this.routeIndex=c,this.maxProgressIdx=Math.max(this.maxProgressIdx,c),this.snappedCoord=o.proj,this.distanceToRoute=o.dist,this.forwardCum.length>c){let a=this.forwardCum[c]??0;this.linearOffset=oe(this.linearOffset,a,.15);}if(this.has(E.ADVANCED_BEARING)?this.bearing=ee(this.bearing,this.route,this.routeIndex,e.speed,e.bearing,this.options.bearingSmoothingAlpha):e.bearing!==null&&(this.bearing=e.bearing),this.has(E.COMPOUND_DEVIATION)&&(this.offRouteFSM.state==="ON_ROUTE"||this.offRouteFSM.state==="DEVIATING")){let a=Ae(i,this.route,r,25),u=this.deviationDetector.evaluate(i,e.bearing,e.speed,a,this.route,this.corridor,this.prevDistToNext);if(a.segmentIndex<this.route.length-1){let d=this.route[Math.min(a.segmentIndex+2,this.route.length-1)];this.prevDistToNext=m(i[1],i[0],d[1],d[0]);}u.confirmed?this.offRouteFSM.state==="ON_ROUTE"?this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:a.segmentIndex}):this.offRouteFSM.isDeviationConfirmed()&&(this.offRouteFSM.dispatch({type:"DEVIATION_CONFIRMED",deviationSegmentIdx:a.segmentIndex}),this.emit("deviation",{anchorIndex:a.segmentIndex,ghostCoord:o.proj})):this.offRouteFSM.state==="DEVIATING"&&this.offRouteFSM.dispatch({type:"RESUME"});}else this.has(E.COMPOUND_DEVIATION)||(o.dist>this.corridor&&this.offRouteFSM.state==="ON_ROUTE"?(this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:o.idx}),this.emit("deviation",{anchorIndex:o.idx,ghostCoord:o.proj})):o.dist<=this.corridor&&this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.dispatch({type:"RESUME"}));if(this.lifecycleFSM.state==="APPROACHING_ROUTE"&&o.dist<this.corridor&&this.lifecycleFSM.dispatch({type:"ROUTE_JOINED",data:{index:o.idx}}),this.instructions.length>0){let a=ne(this.routeIndex,this.instructions,this.instrIdx,this.route,i,e.bearing,this.linearOffset,e.speed);a.nextIdx!==this.instrIdx&&(this.instrIdx=a.nextIdx),a.wasTriggered&&a.instruction&&(this.emit("instruction",a.instruction),this.instrIdx=a.nextIdx+1);}if(!this.hasArrived&&this.route.length>0){let a=this.route[this.route.length-1],u=m(i[1],i[0],a[1],a[0]),d=this.options.isCircuit?this.routeIndex>=this.route.length*.8:true;u<this.options.arrivalThresholdMeters&&d&&(this.hasArrived=true,this.lifecycleFSM.dispatch({type:"NAVIGATION_FINISHED"}),this.emit("arrival"));}return this.has(E.DEAD_RECKONING)&&this.deadReckoning.setAnchor(o.proj,o.idx,e.timestamp),this.getState()}tick(e){if(this.has(E.DEAD_RECKONING)){let t=this.deadReckoning.predict(this.currentSpeed,this.bearing,e);t.isActive&&(this.snappedCoord=t.predictedCoord);}if(this.lastGpsTimestamp>0){let t=(e-this.lastGpsTimestamp)/1e3;t>0&&t<10&&(this.linearOffset=re(this.linearOffset,this.currentSpeed,t));}return this.getState()}getState(){let e=this.route.length>0&&this.reverseCum.length>this.routeIndex?this.reverseCum[this.routeIndex]??null:null,t=this.instrIdx<this.instructions.length?(()=>{let i=this.instructions[this.instrIdx];return i.cumulativeOffset!==void 0?Math.max(0,i.cumulativeOffset-this.linearOffset):null})():null,n=this.lastGpsTimestamp>0?Date.now()-this.lastGpsTimestamp:0;return {snappedCoord:this.snappedCoord,rawGpsCoord:this.rawGpsCoord,bearing:this.bearing,rawGpsBearing:this.rawGpsBearing,routeIndex:this.routeIndex,distanceToRoute:this.distanceToRoute,corridor:this.corridor,isOffRoute:this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.state!=="DEVIATING",progressIndex:this.routeIndex,hasArrived:this.hasArrived,offRouteState:this.offRouteFSM.state,lifecycleState:this.lifecycleFSM.state,distanceToDestination:e,distanceToNextInstruction:t,nextInstructionIndex:this.instrIdx<this.instructions.length?this.instrIdx:null,currentSpeed:this.currentSpeed,isGpsStale:n>3e3,gpsAgeMs:n,licenseStatus:this.license.getStatus()}}destroy(){this.listeners.clear(),this.route=[],this.instructions=[],this.kalman.reset(),this.deadReckoning.clearAnchor(),this.lifecycleFSM.reset(),this.offRouteFSM.reset();}on(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t);}off(e,t){let n=this.listeners.get(e);if(n){let i=n.indexOf(t);i>=0&&n.splice(i,1);}}emit(e,...t){let n=this.listeners.get(e);if(n)for(let i of n)i(...t);}getLicenseManager(){return this.license}getRoute(){return this.route}getInstructions(){return this.instructions}getOffRouteFSM(){return this.offRouteFSM}getLifecycleFSM(){return this.lifecycleFSM}};
|
|
7
|
+
</gpx>`}fromGeoJSON(e){if(this.waypoints=[],e.type==="FeatureCollection"&&e.features){for(let t of e.features)if(t.geometry.type==="Point"){let n=t.geometry.coordinates;this.addWaypoint(n);}else if(t.geometry.type==="LineString"){let n=t.geometry.coordinates;for(let i of n)this.addWaypoint(i);}}}toGeoJSON(){let e=this.waypoints.map((n,i)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:i,name:n.options?.name??null,silent:n.options?.silent??false}})),t={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...e,t]}}clear(){this.waypoints=[];}};var ae=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),profile:e.profile??"driving",maxWaypointsPerChunk:e.maxWaypointsPerChunk??100};}async getRoute(e,t){if(e.length<2)throw new Error("[OSRM] At least 2 waypoints required");let n=t?.profile??this.config.profile;return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}async fetchRoute(e,t,n){let i=e.map(d=>`${d[0]},${d[1]}`).join(";"),r=n?.steps!==false?"true":"false",o=`${this.config.baseUrl}/route/v1/${t}/${i}?overview=full&geometries=geojson&steps=${r}&annotations=false`,c=await fetch(o);if(!c.ok)throw new Error(`[OSRM] HTTP ${c.status}: ${c.statusText}`);let a=await c.json();if(a.code!=="Ok"||!a.routes?.length)throw new Error(`[OSRM] No route found: ${a.code}`);let u=a.routes[0];return {geometry:u.geometry.coordinates,distance:u.distance,duration:u.duration,legs:(u.legs??[]).map(d=>({distance:d.distance,duration:d.duration,steps:(d.steps??[]).map(l=>({distance:l.distance,duration:l.duration,geometry:l.geometry?.coordinates??[],maneuver:{type:l.maneuver?.type??"unknown",modifier:l.maneuver?.modifier,location:l.maneuver?.location??[0,0],bearingBefore:l.maneuver?.bearing_before??0,bearingAfter:l.maneuver?.bearing_after??0,instruction:l.name??void 0},name:l.name}))})),raw:a}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var ce=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),costing:e.costing??"auto",apiKey:e.apiKey??"",maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[Valhalla] At least 2 waypoints required");let n=this.profileToCosting(t?.profile);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileToCosting(e){switch(e){case "cycling":return "bicycle";case "walking":return "pedestrian";default:return this.config.costing}}async fetchRoute(e,t,n){let i=e.map(p=>({lon:p[0],lat:p[1],type:"break"})),r=JSON.stringify({locations:i,costing:t,directions_options:{units:"kilometers"}}),o={"Content-Type":"application/json"};this.config.apiKey&&(o.api_key=this.config.apiKey);let c=await fetch(`${this.config.baseUrl}/route`,{method:"POST",headers:o,body:r});if(!c.ok){let p=await c.text();throw new Error(`[Valhalla] HTTP ${c.status}: ${p}`)}let a=await c.json(),u=this.decodePolyline(a.trip?.legs?.[0]?.shape??""),d=a.trip?.summary??{},l=(a.trip?.legs??[]).map(p=>({distance:(p.summary?.length??0)*1e3,duration:p.summary?.time??0,steps:(p.maneuvers??[]).map(g=>({distance:(g.length??0)*1e3,duration:g.time??0,geometry:[],maneuver:{type:g.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:g.begin_heading??0,bearingAfter:g.end_heading??0,instruction:g.instruction},name:g.street_names?.[0]}))}));return {geometry:u,distance:(d.length??0)*1e3,duration:d.time??0,legs:l,raw:a}}decodePolyline(e,t=6){let n=Math.pow(10,t),i=[],r=0,o=0,c=0;for(;r<e.length;){let a=0,u=0,d;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let l=(u&1)!==0?~(u>>1):u>>1;o+=l,a=0,u=0;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let p=(u&1)!==0?~(u>>1):u>>1;c+=p,i.push([c/n,o/n]);}return i}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var Ze="https://api.openrouteservice.org/v2/directions";var le=class{constructor(e){this.config={apiKey:e.apiKey,profile:e.profile??"driving-car",baseUrl:e.baseUrl??Ze,maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[ORS] At least 2 waypoints required");let n=this.profileFromOptions(t);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileFromOptions(e){switch(e?.profile){case "cycling":return "cycling-regular";case "walking":return "foot-walking";default:return this.config.profile}}async fetchRoute(e,t,n){let i=JSON.stringify({coordinates:e,instructions:n?.steps!==false,language:n?.language??"en"}),r=await fetch(`${this.config.baseUrl}/${t}/geojson`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this.config.apiKey},body:i});if(!r.ok){let l=await r.text();throw new Error(`[ORS] HTTP ${r.status}: ${l}`)}let o=await r.json(),c=o.features?.[0];if(!c)throw new Error("[ORS] No route returned");let a=c.properties?.summary??{},d=(c.properties?.segments??[]).map(l=>({distance:l.distance??0,duration:l.duration??0,steps:(l.steps??[]).map(p=>({distance:p.distance??0,duration:p.duration??0,geometry:[],maneuver:{type:p.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:0,bearingAfter:0,instruction:p.instruction},name:p.name}))}));return {geometry:c.geometry?.coordinates??[],distance:a.distance??0,duration:a.duration??0,legs:d,raw:o}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var et={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},ue=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...et,...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),o=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&o<=this.options.reannounceMeters&&o>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,o,"low")):null;if(o>this.options.earlyTriggerMeters)return null;if(o<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=o<30?"urgent":o<60?"high":"normal";return this.makeCue(i,o,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 tt={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},de=class{constructor(e){this.speedSamples=[];this.options={...tt,...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,o=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:o}}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 nt={dwellThresholdMs:1e4},pe=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...nt,...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 o=this.states.get(i),c=this.contains(r,e);if(!o.inside&&c)o.inside=true,o.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(o.inside&&!c){o.inside=false;let a=o.enteredAtMs?n-o.enteredAtMs:0;o.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(o.inside&&o.enteredAtMs!==null){let a=n-o.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),o.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"?m(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,o=t.length;for(let c=0,a=o-1;c<o;a=c++){let[u,d]=t[c],[l,p]=t[a];d>i!=p>i&&n<(l-u)*(i-d)/(p-d)+u&&(r=!r);}return r}};var E={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var B=class B{constructor(e,t){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=t??false,e&&this.validate(e);}async validate(e){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[t,n]=e.split(".");if(!t||!n)return this.status="INVALID",this.status;let i=atob(t);if(this.payload=JSON.parse(i),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(t,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let e of this.payload.f)this.enabledFeatures.add(e);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(e){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(e)}warnOnce(e){this.warnedFeatures.has(e)||(console.warn(`[NavCore] Feature '${e}' requires a Pro license`),this.warnedFeatures.add(e));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(E.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(e,t){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let i=new TextEncoder().encode(e),r=Uint8Array.from(atob(t),a=>a.charCodeAt(0)),o=Uint8Array.from(atob(B.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),c=await crypto.subtle.importKey("spki",o,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},c,r,i)}catch{return false}}};B.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var G=B;var he=class{constructor(e){this.route=[];this.instructions=[];this.forwardCum=new Float64Array(0);this.reverseCum=new Float64Array(0);this.snappedCoord=null;this.rawGpsCoord=null;this.bearing=0;this.rawGpsBearing=null;this.routeIndex=0;this.maxProgressIdx=0;this.prevSegment=null;this.distanceToRoute=0;this.corridor=25;this.prevDistToNext=null;this.currentSpeed=0;this.lastGpsTimestamp=0;this.instrIdx=0;this.linearOffset=0;this.hasArrived=false;this.listeners=new Map;this.options={licenseKey:e?.licenseKey??"",isDev:e?.isDev??false,isCircuit:e?.isCircuit??false,kalmanQ:e?.kalmanQ??1e-5,kalmanRBase:e?.kalmanRBase??5e-5,baseCorridorMeters:e?.baseCorridorMeters??25,maxCorridorMeters:e?.maxCorridorMeters??60,arrivalThresholdMeters:e?.arrivalThresholdMeters??30,bearingSmoothingAlpha:e?.bearingSmoothingAlpha??.3},this.kalman=new M(this.options.kalmanQ,this.options.kalmanRBase),this.snapper=new T({useBearingPenalty:true,useInertiaPenalty:true}),this.deadReckoning=new D,this.deviationDetector=new N,this.lifecycleFSM=new w,this.offRouteFSM=new F,this.license=new G(this.options.licenseKey,this.options.isDev);}has(e){let t=this.license.hasFeature(e);return t||this.license.warnOnce(e),t}setRoute(e,t){this.route=e,this.instructions=t??[],this.forwardCum=K(e),this.reverseCum=j(e),this.routeIndex=0,this.maxProgressIdx=0,this.prevSegment=null,this.instrIdx=0,this.linearOffset=0,this.hasArrived=false,this.snappedCoord=null,this.prevDistToNext=null,this.lifecycleFSM.dispatch({type:"ROUTE_LOADED"}),this.emit("routeLoaded",{routeLength:e.length});}startNavigation(){this.lifecycleFSM.dispatch({type:"USER_START_NAVIGATION"}),this.emit("navigationStarted");}update(e){if(this.rawGpsCoord=e.coord,this.rawGpsBearing=e.bearing,this.currentSpeed=e.speed,this.lastGpsTimestamp=e.timestamp,this.route.length<2)return this.getState();let[t,n]=this.kalman.update(e.coord[0],e.coord[1],e.accuracy??10,e.timestamp),i=[t,n],r=Math.max(0,this.routeIndex-2),o=this.snapper.snap(i,this.route,r,25,this.options.isCircuit,this.maxProgressIdx,this.routeIndex,e.bearing,this.prevSegment);this.corridor=Z(e.speed,e.accuracy,this.options.baseCorridorMeters,this.options.maxCorridorMeters);let c=te(this.routeIndex,o.idx,this.route,o.dist,80);if(this.prevSegment=this.routeIndex,this.routeIndex=c,this.maxProgressIdx=Math.max(this.maxProgressIdx,c),this.snappedCoord=o.proj,this.distanceToRoute=o.dist,this.forwardCum.length>c){let a=this.forwardCum[c]??0;this.linearOffset=oe(this.linearOffset,a,.15);}if(this.has(E.ADVANCED_BEARING)?this.bearing=ee(this.bearing,this.route,this.routeIndex,e.speed,e.bearing,this.options.bearingSmoothingAlpha):e.bearing!==null&&(this.bearing=e.bearing),this.has(E.COMPOUND_DEVIATION)&&(this.offRouteFSM.state==="ON_ROUTE"||this.offRouteFSM.state==="DEVIATING")){let a=Ae(i,this.route,r,25),u=this.deviationDetector.evaluate(i,e.bearing,e.speed,a,this.route,this.corridor,this.prevDistToNext);if(a.segmentIndex<this.route.length-1){let d=this.route[Math.min(a.segmentIndex+2,this.route.length-1)];this.prevDistToNext=m(i[1],i[0],d[1],d[0]);}u.confirmed?this.offRouteFSM.state==="ON_ROUTE"?this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:a.segmentIndex}):this.offRouteFSM.isDeviationConfirmed()&&(this.offRouteFSM.dispatch({type:"DEVIATION_CONFIRMED",deviationSegmentIdx:a.segmentIndex}),this.emit("deviation",{anchorIndex:a.segmentIndex,ghostCoord:o.proj})):this.offRouteFSM.state==="DEVIATING"&&this.offRouteFSM.dispatch({type:"RESUME"});}else this.has(E.COMPOUND_DEVIATION)||(o.dist>this.corridor&&this.offRouteFSM.state==="ON_ROUTE"?(this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:o.idx}),this.emit("deviation",{anchorIndex:o.idx,ghostCoord:o.proj})):o.dist<=this.corridor&&this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.dispatch({type:"RESUME"}));if(this.lifecycleFSM.state==="APPROACHING_ROUTE"&&o.dist<this.corridor&&this.lifecycleFSM.dispatch({type:"ROUTE_JOINED",data:{index:o.idx}}),this.instructions.length>0){let a=ne(this.routeIndex,this.instructions,this.instrIdx,this.route,i,e.bearing,this.linearOffset,e.speed);a.nextIdx!==this.instrIdx&&(this.instrIdx=a.nextIdx),a.wasTriggered&&a.instruction&&(this.emit("instruction",a.instruction),this.instrIdx=a.nextIdx+1);}if(!this.hasArrived&&this.route.length>0){let a=this.route[this.route.length-1],u=m(i[1],i[0],a[1],a[0]),d=this.options.isCircuit?this.routeIndex>=this.route.length*.8:true;u<this.options.arrivalThresholdMeters&&d&&(this.hasArrived=true,this.lifecycleFSM.dispatch({type:"NAVIGATION_FINISHED"}),this.emit("arrival"));}return this.has(E.DEAD_RECKONING)&&this.deadReckoning.setAnchor(o.proj,o.idx,e.timestamp),this.getState()}tick(e){if(this.has(E.DEAD_RECKONING)){let t=this.deadReckoning.predict(this.currentSpeed,this.bearing,e);t.isActive&&(this.snappedCoord=t.predictedCoord);}if(this.lastGpsTimestamp>0){let t=(e-this.lastGpsTimestamp)/1e3;t>0&&t<10&&(this.linearOffset=re(this.linearOffset,this.currentSpeed,t));}return this.getState()}getState(){let e=this.route.length>0&&this.reverseCum.length>this.routeIndex?this.reverseCum[this.routeIndex]??null:null,t=this.instrIdx<this.instructions.length?(()=>{let i=this.instructions[this.instrIdx];return i.cumulativeOffset!==void 0?Math.max(0,i.cumulativeOffset-this.linearOffset):null})():null,n=this.lastGpsTimestamp>0?Date.now()-this.lastGpsTimestamp:0;return {snappedCoord:this.snappedCoord,rawGpsCoord:this.rawGpsCoord,bearing:this.bearing,rawGpsBearing:this.rawGpsBearing,routeIndex:this.routeIndex,distanceToRoute:this.distanceToRoute,corridor:this.corridor,isOffRoute:this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.state!=="DEVIATING",progressIndex:this.routeIndex,hasArrived:this.hasArrived,offRouteState:this.offRouteFSM.state,lifecycleState:this.lifecycleFSM.state,distanceToDestination:e,distanceToNextInstruction:t,nextInstructionIndex:this.instrIdx<this.instructions.length?this.instrIdx:null,currentSpeed:this.currentSpeed,isGpsStale:n>3e3,gpsAgeMs:n,licenseStatus:this.license.getStatus()}}destroy(){this.listeners.clear(),this.route=[],this.instructions=[],this.kalman.reset(),this.deadReckoning.clearAnchor(),this.lifecycleFSM.reset(),this.offRouteFSM.reset();}on(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t);}off(e,t){let n=this.listeners.get(e);if(n){let i=n.indexOf(t);i>=0&&n.splice(i,1);}}emit(e,...t){let n=this.listeners.get(e);if(n)for(let i of n)i(...t);}getLicenseManager(){return this.license}getRoute(){return this.route}getInstructions(){return this.instructions}getOffRouteFSM(){return this.offRouteFSM}getLifecycleFSM(){return this.lifecycleFSM}};
|
|
8
8
|
exports.Constants=ve;exports.CustomRouteBuilder=se;exports.DeadReckoningEngine=D;exports.DeviationDetector=N;exports.ETAEngine=de;exports.FEATURES=E;exports.GeofencingEngine=pe;exports.InstructionEditor=ie;exports.KalmanFilter2D=M;exports.LicenseManager=G;exports.NavCore=he;exports.NavigationFSM=w;exports.OSRMDirectionsProvider=ae;exports.OffRouteFSM=F;exports.OpenRouteServiceProvider=le;exports.ParallelRoadResolver=Q;exports.RouteSnapper=T;exports.ValhallaDirectionsProvider=ce;exports.VoiceTriggerEngine=ue;exports.advancePredictor=re;exports.applyBearingSmoothing=ee;exports.applyGpsCorrection=oe;exports.blendBearing=U;exports.buildForwardCumulativeDistances=K;exports.buildReverseCumulativeDistances=j;exports.calculateDynamicCorridor=Z;exports.calculateRobustBearing=Me;exports.computeDeadReckoningOnRoute=Oe;exports.destinationPoint=V;exports.geoGetBearing=we;exports.getBearingBetweenPoints=f;exports.getSpeedAdaptiveAlpha=_e;exports.haversineDistance=m;exports.interpolatePoint=H;exports.kalmanUpdate2D=me;exports.normalizeAngle=y;exports.pointAtOffset=ze;exports.projectPointOnSegment=W;exports.resolveNextInstruction=ne;exports.validateProgressUpdate=te;
|
package/dist/index.mjs
CHANGED
|
@@ -4,5 +4,5 @@ ${this.waypoints.map(t=>` <wpt lat="${t.coord[1]}" lon="${t.coord[0]}">`+(t.o
|
|
|
4
4
|
<name>${t.options.name}</name>`:"")+`
|
|
5
5
|
</wpt>`).join(`
|
|
6
6
|
`)}
|
|
7
|
-
</gpx>`}fromGeoJSON(e){if(this.waypoints=[],e.type==="FeatureCollection"&&e.features){for(let t of e.features)if(t.geometry.type==="Point"){let n=t.geometry.coordinates;this.addWaypoint(n);}else if(t.geometry.type==="LineString"){let n=t.geometry.coordinates;for(let i of n)this.addWaypoint(i);}}}toGeoJSON(){let e=this.waypoints.map((n,i)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:i,name:n.options?.name??null,silent:n.options?.silent??false}})),t={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...e,t]}}clear(){this.waypoints=[];}};var ae=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),profile:e.profile??"driving",maxWaypointsPerChunk:e.maxWaypointsPerChunk??100};}async getRoute(e,t){if(e.length<2)throw new Error("[OSRM] At least 2 waypoints required");let n=t?.profile??this.config.profile;return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}async fetchRoute(e,t,n){let i=e.map(d=>`${d[0]},${d[1]}`).join(";"),r=n?.steps!==false?"true":"false",o=`${this.config.baseUrl}/route/v1/${t}/${i}?overview=full&geometries=geojson&steps=${r}&annotations=false`,c=await fetch(o);if(!c.ok)throw new Error(`[OSRM] HTTP ${c.status}: ${c.statusText}`);let a=await c.json();if(a.code!=="Ok"||!a.routes?.length)throw new Error(`[OSRM] No route found: ${a.code}`);let u=a.routes[0];return {geometry:u.geometry.coordinates,distance:u.distance,duration:u.duration,legs:(u.legs??[]).map(d=>({distance:d.distance,duration:d.duration,steps:(d.steps??[]).map(l=>({distance:l.distance,duration:l.duration,geometry:l.geometry?.coordinates??[],maneuver:{type:l.maneuver?.type??"unknown",modifier:l.maneuver?.modifier,location:l.maneuver?.location??[0,0],bearingBefore:l.maneuver?.bearing_before??0,bearingAfter:l.maneuver?.bearing_after??0,instruction:l.name??void 0},name:l.name}))})),raw:a}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var ce=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),costing:e.costing??"auto",apiKey:e.apiKey??"",maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[Valhalla] At least 2 waypoints required");let n=this.profileToCosting(t?.profile);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileToCosting(e){switch(e){case "cycling":return "bicycle";case "walking":return "pedestrian";default:return this.config.costing}}async fetchRoute(e,t,n){let i=e.map(p=>({lon:p[0],lat:p[1],type:"break"})),r=JSON.stringify({locations:i,costing:t,directions_options:{units:"kilometers"}}),o={"Content-Type":"application/json"};this.config.apiKey&&(o.api_key=this.config.apiKey);let c=await fetch(`${this.config.baseUrl}/route`,{method:"POST",headers:o,body:r});if(!c.ok){let p=await c.text();throw new Error(`[Valhalla] HTTP ${c.status}: ${p}`)}let a=await c.json(),u=this.decodePolyline(a.trip?.legs?.[0]?.shape??""),d=a.trip?.summary??{},l=(a.trip?.legs??[]).map(p=>({distance:(p.summary?.length??0)*1e3,duration:p.summary?.time??0,steps:(p.maneuvers??[]).map(g=>({distance:(g.length??0)*1e3,duration:g.time??0,geometry:[],maneuver:{type:g.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:g.begin_heading??0,bearingAfter:g.end_heading??0,instruction:g.instruction},name:g.street_names?.[0]}))}));return {geometry:u,distance:(d.length??0)*1e3,duration:d.time??0,legs:l,raw:a}}decodePolyline(e,t=6){let n=Math.pow(10,t),i=[],r=0,o=0,c=0;for(;r<e.length;){let a=0,u=0,d;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let l=(u&1)!==0?~(u>>1):u>>1;o+=l,a=0,u=0;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let p=(u&1)!==0?~(u>>1):u>>1;c+=p,i.push([c/n,o/n]);}return i}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var Ze="https://api.openrouteservice.org/v2/directions";var le=class{constructor(e){this.config={apiKey:e.apiKey,profile:e.profile??"driving-car",baseUrl:e.baseUrl??Ze,maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[ORS] At least 2 waypoints required");let n=this.profileFromOptions(t);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileFromOptions(e){switch(e?.profile){case "cycling":return "cycling-regular";case "walking":return "foot-walking";default:return this.config.profile}}async fetchRoute(e,t,n){let i=JSON.stringify({coordinates:e,instructions:n?.steps!==false,language:n?.language??"en"}),r=await fetch(`${this.config.baseUrl}/${t}/geojson`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this.config.apiKey},body:i});if(!r.ok){let l=await r.text();throw new Error(`[ORS] HTTP ${r.status}: ${l}`)}let o=await r.json(),c=o.features?.[0];if(!c)throw new Error("[ORS] No route returned");let a=c.properties?.summary??{},d=(c.properties?.segments??[]).map(l=>({distance:l.distance??0,duration:l.duration??0,steps:(l.steps??[]).map(p=>({distance:p.distance??0,duration:p.duration??0,geometry:[],maneuver:{type:p.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:0,bearingAfter:0,instruction:p.instruction},name:p.name}))}));return {geometry:c.geometry?.coordinates??[],distance:a.distance??0,duration:a.duration??0,legs:d,raw:o}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var et={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},ue=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...et,...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),o=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&o<=this.options.reannounceMeters&&o>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,o,"low")):null;if(o>this.options.earlyTriggerMeters)return null;if(o<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=o<30?"urgent":o<60?"high":"normal";return this.makeCue(i,o,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 tt={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},de=class{constructor(e){this.speedSamples=[];this.options={...tt,...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,o=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:o}}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 nt={dwellThresholdMs:1e4},pe=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...nt,...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 o=this.states.get(i),c=this.contains(r,e);if(!o.inside&&c)o.inside=true,o.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(o.inside&&!c){o.inside=false;let a=o.enteredAtMs?n-o.enteredAtMs:0;o.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(o.inside&&o.enteredAtMs!==null){let a=n-o.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),o.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"?m(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,o=t.length;for(let c=0,a=o-1;c<o;a=c++){let[u,d]=t[c],[l,p]=t[a];d>i!=p>i&&n<(l-u)*(i-d)/(p-d)+u&&(r=!r);}return r}};var E={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var B=class B{constructor(e,t){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=t??false,e&&this.validate(e);}async validate(e){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[t,n]=e.split(".");if(!t||!n)return this.status="INVALID",this.status;let i=atob(t);if(this.payload=JSON.parse(i),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(t,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let e of this.payload.f)this.enabledFeatures.add(e);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(e){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(e)}warnOnce(e){this.warnedFeatures.has(e)||(console.warn(`[NavCore] Feature '${e}' requires a Pro license \u2192 upgrade at https://navcore.dev/pricing`),this.warnedFeatures.add(e));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(E.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(e,t){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let i=new TextEncoder().encode(e),r=Uint8Array.from(atob(t),a=>a.charCodeAt(0)),o=Uint8Array.from(atob(B.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),c=await crypto.subtle.importKey("spki",o,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},c,r,i)}catch{return false}}};B.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var G=B;var he=class{constructor(e){this.route=[];this.instructions=[];this.forwardCum=new Float64Array(0);this.reverseCum=new Float64Array(0);this.snappedCoord=null;this.rawGpsCoord=null;this.bearing=0;this.rawGpsBearing=null;this.routeIndex=0;this.maxProgressIdx=0;this.prevSegment=null;this.distanceToRoute=0;this.corridor=25;this.prevDistToNext=null;this.currentSpeed=0;this.lastGpsTimestamp=0;this.instrIdx=0;this.linearOffset=0;this.hasArrived=false;this.listeners=new Map;this.options={licenseKey:e?.licenseKey??"",isDev:e?.isDev??false,isCircuit:e?.isCircuit??false,kalmanQ:e?.kalmanQ??1e-5,kalmanRBase:e?.kalmanRBase??5e-5,baseCorridorMeters:e?.baseCorridorMeters??25,maxCorridorMeters:e?.maxCorridorMeters??60,arrivalThresholdMeters:e?.arrivalThresholdMeters??30,bearingSmoothingAlpha:e?.bearingSmoothingAlpha??.3},this.kalman=new M(this.options.kalmanQ,this.options.kalmanRBase),this.snapper=new T({useBearingPenalty:true,useInertiaPenalty:true}),this.deadReckoning=new D,this.deviationDetector=new N,this.lifecycleFSM=new w,this.offRouteFSM=new F,this.license=new G(this.options.licenseKey,this.options.isDev);}has(e){let t=this.license.hasFeature(e);return t||this.license.warnOnce(e),t}setRoute(e,t){this.route=e,this.instructions=t??[],this.forwardCum=K(e),this.reverseCum=j(e),this.routeIndex=0,this.maxProgressIdx=0,this.prevSegment=null,this.instrIdx=0,this.linearOffset=0,this.hasArrived=false,this.snappedCoord=null,this.prevDistToNext=null,this.lifecycleFSM.dispatch({type:"ROUTE_LOADED"}),this.emit("routeLoaded",{routeLength:e.length});}startNavigation(){this.lifecycleFSM.dispatch({type:"USER_START_NAVIGATION"}),this.emit("navigationStarted");}update(e){if(this.rawGpsCoord=e.coord,this.rawGpsBearing=e.bearing,this.currentSpeed=e.speed,this.lastGpsTimestamp=e.timestamp,this.route.length<2)return this.getState();let[t,n]=this.kalman.update(e.coord[0],e.coord[1],e.accuracy??10,e.timestamp),i=[t,n],r=Math.max(0,this.routeIndex-2),o=this.snapper.snap(i,this.route,r,25,this.options.isCircuit,this.maxProgressIdx,this.routeIndex,e.bearing,this.prevSegment);this.corridor=Z(e.speed,e.accuracy,this.options.baseCorridorMeters,this.options.maxCorridorMeters);let c=te(this.routeIndex,o.idx,this.route,o.dist,80);if(this.prevSegment=this.routeIndex,this.routeIndex=c,this.maxProgressIdx=Math.max(this.maxProgressIdx,c),this.snappedCoord=o.proj,this.distanceToRoute=o.dist,this.forwardCum.length>c){let a=this.forwardCum[c]??0;this.linearOffset=oe(this.linearOffset,a,.15);}if(this.has(E.ADVANCED_BEARING)?this.bearing=ee(this.bearing,this.route,this.routeIndex,e.speed,e.bearing,this.options.bearingSmoothingAlpha):e.bearing!==null&&(this.bearing=e.bearing),this.has(E.COMPOUND_DEVIATION)&&(this.offRouteFSM.state==="ON_ROUTE"||this.offRouteFSM.state==="DEVIATING")){let a=Ae(i,this.route,r,25),u=this.deviationDetector.evaluate(i,e.bearing,e.speed,a,this.route,this.corridor,this.prevDistToNext);if(a.segmentIndex<this.route.length-1){let d=this.route[Math.min(a.segmentIndex+2,this.route.length-1)];this.prevDistToNext=m(i[1],i[0],d[1],d[0]);}u.confirmed?this.offRouteFSM.state==="ON_ROUTE"?this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:a.segmentIndex}):this.offRouteFSM.isDeviationConfirmed()&&(this.offRouteFSM.dispatch({type:"DEVIATION_CONFIRMED",deviationSegmentIdx:a.segmentIndex}),this.emit("deviation",{anchorIndex:a.segmentIndex,ghostCoord:o.proj})):this.offRouteFSM.state==="DEVIATING"&&this.offRouteFSM.dispatch({type:"RESUME"});}else this.has(E.COMPOUND_DEVIATION)||(o.dist>this.corridor&&this.offRouteFSM.state==="ON_ROUTE"?(this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:o.idx}),this.emit("deviation",{anchorIndex:o.idx,ghostCoord:o.proj})):o.dist<=this.corridor&&this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.dispatch({type:"RESUME"}));if(this.lifecycleFSM.state==="APPROACHING_ROUTE"&&o.dist<this.corridor&&this.lifecycleFSM.dispatch({type:"ROUTE_JOINED",data:{index:o.idx}}),this.instructions.length>0){let a=ne(this.routeIndex,this.instructions,this.instrIdx,this.route,i,e.bearing,this.linearOffset,e.speed);a.nextIdx!==this.instrIdx&&(this.instrIdx=a.nextIdx),a.wasTriggered&&a.instruction&&(this.emit("instruction",a.instruction),this.instrIdx=a.nextIdx+1);}if(!this.hasArrived&&this.route.length>0){let a=this.route[this.route.length-1],u=m(i[1],i[0],a[1],a[0]),d=this.options.isCircuit?this.routeIndex>=this.route.length*.8:true;u<this.options.arrivalThresholdMeters&&d&&(this.hasArrived=true,this.lifecycleFSM.dispatch({type:"NAVIGATION_FINISHED"}),this.emit("arrival"));}return this.has(E.DEAD_RECKONING)&&this.deadReckoning.setAnchor(o.proj,o.idx,e.timestamp),this.getState()}tick(e){if(this.has(E.DEAD_RECKONING)){let t=this.deadReckoning.predict(this.currentSpeed,this.bearing,e);t.isActive&&(this.snappedCoord=t.predictedCoord);}if(this.lastGpsTimestamp>0){let t=(e-this.lastGpsTimestamp)/1e3;t>0&&t<10&&(this.linearOffset=re(this.linearOffset,this.currentSpeed,t));}return this.getState()}getState(){let e=this.route.length>0&&this.reverseCum.length>this.routeIndex?this.reverseCum[this.routeIndex]??null:null,t=this.instrIdx<this.instructions.length?(()=>{let i=this.instructions[this.instrIdx];return i.cumulativeOffset!==void 0?Math.max(0,i.cumulativeOffset-this.linearOffset):null})():null,n=this.lastGpsTimestamp>0?Date.now()-this.lastGpsTimestamp:0;return {snappedCoord:this.snappedCoord,rawGpsCoord:this.rawGpsCoord,bearing:this.bearing,rawGpsBearing:this.rawGpsBearing,routeIndex:this.routeIndex,distanceToRoute:this.distanceToRoute,corridor:this.corridor,isOffRoute:this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.state!=="DEVIATING",progressIndex:this.routeIndex,hasArrived:this.hasArrived,offRouteState:this.offRouteFSM.state,lifecycleState:this.lifecycleFSM.state,distanceToDestination:e,distanceToNextInstruction:t,nextInstructionIndex:this.instrIdx<this.instructions.length?this.instrIdx:null,currentSpeed:this.currentSpeed,isGpsStale:n>3e3,gpsAgeMs:n,licenseStatus:this.license.getStatus()}}destroy(){this.listeners.clear(),this.route=[],this.instructions=[],this.kalman.reset(),this.deadReckoning.clearAnchor(),this.lifecycleFSM.reset(),this.offRouteFSM.reset();}on(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t);}off(e,t){let n=this.listeners.get(e);if(n){let i=n.indexOf(t);i>=0&&n.splice(i,1);}}emit(e,...t){let n=this.listeners.get(e);if(n)for(let i of n)i(...t);}getLicenseManager(){return this.license}getRoute(){return this.route}getInstructions(){return this.instructions}getOffRouteFSM(){return this.offRouteFSM}getLifecycleFSM(){return this.lifecycleFSM}};
|
|
7
|
+
</gpx>`}fromGeoJSON(e){if(this.waypoints=[],e.type==="FeatureCollection"&&e.features){for(let t of e.features)if(t.geometry.type==="Point"){let n=t.geometry.coordinates;this.addWaypoint(n);}else if(t.geometry.type==="LineString"){let n=t.geometry.coordinates;for(let i of n)this.addWaypoint(i);}}}toGeoJSON(){let e=this.waypoints.map((n,i)=>({type:"Feature",geometry:{type:"Point",coordinates:n.coord},properties:{id:n.id,order:i,name:n.options?.name??null,silent:n.options?.silent??false}})),t={type:"Feature",geometry:{type:"LineString",coordinates:this.waypoints.map(n=>n.coord)},properties:{type:"route"}};return {type:"FeatureCollection",features:[...e,t]}}clear(){this.waypoints=[];}};var ae=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),profile:e.profile??"driving",maxWaypointsPerChunk:e.maxWaypointsPerChunk??100};}async getRoute(e,t){if(e.length<2)throw new Error("[OSRM] At least 2 waypoints required");let n=t?.profile??this.config.profile;return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}async fetchRoute(e,t,n){let i=e.map(d=>`${d[0]},${d[1]}`).join(";"),r=n?.steps!==false?"true":"false",o=`${this.config.baseUrl}/route/v1/${t}/${i}?overview=full&geometries=geojson&steps=${r}&annotations=false`,c=await fetch(o);if(!c.ok)throw new Error(`[OSRM] HTTP ${c.status}: ${c.statusText}`);let a=await c.json();if(a.code!=="Ok"||!a.routes?.length)throw new Error(`[OSRM] No route found: ${a.code}`);let u=a.routes[0];return {geometry:u.geometry.coordinates,distance:u.distance,duration:u.duration,legs:(u.legs??[]).map(d=>({distance:d.distance,duration:d.duration,steps:(d.steps??[]).map(l=>({distance:l.distance,duration:l.duration,geometry:l.geometry?.coordinates??[],maneuver:{type:l.maneuver?.type??"unknown",modifier:l.maneuver?.modifier,location:l.maneuver?.location??[0,0],bearingBefore:l.maneuver?.bearing_before??0,bearingAfter:l.maneuver?.bearing_after??0,instruction:l.name??void 0},name:l.name}))})),raw:a}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var ce=class{constructor(e){this.config={baseUrl:e.baseUrl.replace(/\/$/,""),costing:e.costing??"auto",apiKey:e.apiKey??"",maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[Valhalla] At least 2 waypoints required");let n=this.profileToCosting(t?.profile);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileToCosting(e){switch(e){case "cycling":return "bicycle";case "walking":return "pedestrian";default:return this.config.costing}}async fetchRoute(e,t,n){let i=e.map(p=>({lon:p[0],lat:p[1],type:"break"})),r=JSON.stringify({locations:i,costing:t,directions_options:{units:"kilometers"}}),o={"Content-Type":"application/json"};this.config.apiKey&&(o.api_key=this.config.apiKey);let c=await fetch(`${this.config.baseUrl}/route`,{method:"POST",headers:o,body:r});if(!c.ok){let p=await c.text();throw new Error(`[Valhalla] HTTP ${c.status}: ${p}`)}let a=await c.json(),u=this.decodePolyline(a.trip?.legs?.[0]?.shape??""),d=a.trip?.summary??{},l=(a.trip?.legs??[]).map(p=>({distance:(p.summary?.length??0)*1e3,duration:p.summary?.time??0,steps:(p.maneuvers??[]).map(g=>({distance:(g.length??0)*1e3,duration:g.time??0,geometry:[],maneuver:{type:g.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:g.begin_heading??0,bearingAfter:g.end_heading??0,instruction:g.instruction},name:g.street_names?.[0]}))}));return {geometry:u,distance:(d.length??0)*1e3,duration:d.time??0,legs:l,raw:a}}decodePolyline(e,t=6){let n=Math.pow(10,t),i=[],r=0,o=0,c=0;for(;r<e.length;){let a=0,u=0,d;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let l=(u&1)!==0?~(u>>1):u>>1;o+=l,a=0,u=0;do d=e.charCodeAt(r++)-63,u|=(d&31)<<a,a+=5;while(d>=32);let p=(u&1)!==0?~(u>>1):u>>1;c+=p,i.push([c/n,o/n]);}return i}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var Ze="https://api.openrouteservice.org/v2/directions";var le=class{constructor(e){this.config={apiKey:e.apiKey,profile:e.profile??"driving-car",baseUrl:e.baseUrl??Ze,maxWaypointsPerChunk:e.maxWaypointsPerChunk??50};}async getRoute(e,t){if(e.length<2)throw new Error("[ORS] At least 2 waypoints required");let n=this.profileFromOptions(t);return e.length<=this.config.maxWaypointsPerChunk?this.fetchRoute(e,n,t):this.fetchChunked(e,n,t)}profileFromOptions(e){switch(e?.profile){case "cycling":return "cycling-regular";case "walking":return "foot-walking";default:return this.config.profile}}async fetchRoute(e,t,n){let i=JSON.stringify({coordinates:e,instructions:n?.steps!==false,language:n?.language??"en"}),r=await fetch(`${this.config.baseUrl}/${t}/geojson`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this.config.apiKey},body:i});if(!r.ok){let l=await r.text();throw new Error(`[ORS] HTTP ${r.status}: ${l}`)}let o=await r.json(),c=o.features?.[0];if(!c)throw new Error("[ORS] No route returned");let a=c.properties?.summary??{},d=(c.properties?.segments??[]).map(l=>({distance:l.distance??0,duration:l.duration??0,steps:(l.steps??[]).map(p=>({distance:p.distance??0,duration:p.duration??0,geometry:[],maneuver:{type:p.type?.toString()??"unknown",modifier:void 0,location:e[0],bearingBefore:0,bearingAfter:0,instruction:p.instruction},name:p.name}))}));return {geometry:c.geometry?.coordinates??[],distance:a.distance??0,duration:a.duration??0,legs:d,raw:o}}async fetchChunked(e,t,n){let i=this.config.maxWaypointsPerChunk,r=[];for(let l=0;l<e.length;l+=i-1){let p=e.slice(l,l+i);if(p.length<2||(r.push(p),l+i>=e.length))break}let o=await Promise.all(r.map(l=>this.fetchRoute(l,t,n))),c=[],a=0,u=0,d=[];for(let l=0;l<o.length;l++){let p=o[l];c.push(...l===0?p.geometry:p.geometry.slice(1)),a+=p.distance,u+=p.duration,d.push(...p.legs);}return {geometry:c,distance:a,duration:u,legs:d,raw:o.map(l=>l.raw)}}};var et={earlyTriggerMeters:120,lateTriggerMeters:20,reannounceMeters:300},ue=class{constructor(e){this.instructions=[];this.triggeredIds=new Set;this.reannouncedIds=new Set;this.options={...et,...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),o=t;if(this.triggeredIds.has(r))return this.options.reannounceMeters>0&&!this.reannouncedIds.has(r)&&o<=this.options.reannounceMeters&&o>this.options.earlyTriggerMeters?(this.reannouncedIds.add(r),this.makeCue(i,o,"low")):null;if(o>this.options.earlyTriggerMeters)return null;if(o<-this.options.lateTriggerMeters)return this.triggeredIds.add(r),null;this.triggeredIds.add(r);let c=o<30?"urgent":o<60?"high":"normal";return this.makeCue(i,o,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 tt={speedWindowSize:6,minimumSpeedMs:.5,fallbackSpeedMs:8.33},de=class{constructor(e){this.speedSamples=[];this.options={...tt,...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,o=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:o}}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 nt={dwellThresholdMs:1e4},pe=class{constructor(e){this.fences=new Map;this.states=new Map;this.options={...nt,...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 o=this.states.get(i),c=this.contains(r,e);if(!o.inside&&c)o.inside=true,o.enteredAtMs=n,t.push({id:i,type:"enter",coord:e,meta:r.meta});else if(o.inside&&!c){o.inside=false;let a=o.enteredAtMs?n-o.enteredAtMs:0;o.enteredAtMs=null,t.push({id:i,type:"exit",coord:e,dwellMs:a,meta:r.meta});}else if(o.inside&&o.enteredAtMs!==null){let a=n-o.enteredAtMs;a>=this.options.dwellThresholdMs&&(t.push({id:i,type:"dwell",coord:e,dwellMs:a,meta:r.meta}),o.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"?m(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,o=t.length;for(let c=0,a=o-1;c<o;a=c++){let[u,d]=t[c],[l,p]=t[a];d>i!=p>i&&n<(l-u)*(i-d)/(p-d)+u&&(r=!r);}return r}};var E={GPS_SMOOTHING:"gps_smoothing",BASIC_SNAPPING:"basic_snapping",ADVANCED_SNAPPING:"advanced_snapping",DEAD_RECKONING:"dead_reckoning",ROUTE_OFFSET:"route_offset",PROGRESS_TRACKING:"progress_tracking",ADVANCED_BEARING:"advanced_bearing",COMPOUND_DEVIATION:"compound_deviation",PARALLEL_ROAD_RESOLUTION:"parallel_road_resolution",U_TURN_PROTECTION:"u_turn_protection",VOICE_TRIGGERS:"voice_triggers",ETA:"eta",GEOFENCING:"geofencing",NAV_AI:"nav_ai"};var B=class B{constructor(e,t){this.status="MISSING";this.payload=null;this.enabledFeatures=new Set;this.warnedFeatures=new Set;this.isDev=false;this.isDev=t??false,e&&this.validate(e);}async validate(e){try{if(this.status==="VALID"&&this.payload?.bid==="dev.bypass")return "VALID";let[t,n]=e.split(".");if(!t||!n)return this.status="INVALID",this.status;let i=atob(t);if(this.payload=JSON.parse(i),!this.payload||this.payload.exp<Date.now())return this.status="EXPIRED",this.status;if(!await this.verifySignature(t,n))return this.status="INVALID",this.status;this.status="VALID",this.extractFeatures(),console.log(`\u{1F511} [NavCore] License validation complete. Plan: ${this.isProEnabled()?"pro":"core"}`);}catch{this.status="INVALID";}return this.status}extractFeatures(){if(this.enabledFeatures.clear(),!!this.payload){if(Array.isArray(this.payload.f))for(let e of this.payload.f)this.enabledFeatures.add(e);this.payload.t==="pro"&&this.enabledFeatures.add("*");}}hasFeature(e){return this.isDev?true:this.status!=="VALID"?false:this.enabledFeatures.has("*")?true:this.enabledFeatures.has(e)}warnOnce(e){this.warnedFeatures.has(e)||(console.warn(`[NavCore] Feature '${e}' requires a Pro license`),this.warnedFeatures.add(e));}isProEnabled(){return this.hasFeature("*")}isAIEnabled(){return this.hasFeature(E.NAV_AI)}getStatus(){return this.status}getPayload(){return this.payload}getEnabledFeatures(){return Array.from(this.enabledFeatures)}async verifySignature(e,t){try{if(typeof crypto>"u"||!crypto.subtle)return !1;let i=new TextEncoder().encode(e),r=Uint8Array.from(atob(t),a=>a.charCodeAt(0)),o=Uint8Array.from(atob(B.PUBLIC_KEY_SPKI),a=>a.charCodeAt(0)),c=await crypto.subtle.importKey("spki",o,{name:"ECDSA",namedCurve:"P-256"},!0,["verify"]);return await crypto.subtle.verify({name:"ECDSA",hash:{name:"SHA-256"}},c,r,i)}catch{return false}}};B.PUBLIC_KEY_SPKI="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk6onJmJkWtRFusiSS69dcdONX4xL2Snm4AZO+dYjSynfJiL4x7trEy80MImLftknBWT/qI0z7ox4Q7VNL3te6A==";var G=B;var he=class{constructor(e){this.route=[];this.instructions=[];this.forwardCum=new Float64Array(0);this.reverseCum=new Float64Array(0);this.snappedCoord=null;this.rawGpsCoord=null;this.bearing=0;this.rawGpsBearing=null;this.routeIndex=0;this.maxProgressIdx=0;this.prevSegment=null;this.distanceToRoute=0;this.corridor=25;this.prevDistToNext=null;this.currentSpeed=0;this.lastGpsTimestamp=0;this.instrIdx=0;this.linearOffset=0;this.hasArrived=false;this.listeners=new Map;this.options={licenseKey:e?.licenseKey??"",isDev:e?.isDev??false,isCircuit:e?.isCircuit??false,kalmanQ:e?.kalmanQ??1e-5,kalmanRBase:e?.kalmanRBase??5e-5,baseCorridorMeters:e?.baseCorridorMeters??25,maxCorridorMeters:e?.maxCorridorMeters??60,arrivalThresholdMeters:e?.arrivalThresholdMeters??30,bearingSmoothingAlpha:e?.bearingSmoothingAlpha??.3},this.kalman=new M(this.options.kalmanQ,this.options.kalmanRBase),this.snapper=new T({useBearingPenalty:true,useInertiaPenalty:true}),this.deadReckoning=new D,this.deviationDetector=new N,this.lifecycleFSM=new w,this.offRouteFSM=new F,this.license=new G(this.options.licenseKey,this.options.isDev);}has(e){let t=this.license.hasFeature(e);return t||this.license.warnOnce(e),t}setRoute(e,t){this.route=e,this.instructions=t??[],this.forwardCum=K(e),this.reverseCum=j(e),this.routeIndex=0,this.maxProgressIdx=0,this.prevSegment=null,this.instrIdx=0,this.linearOffset=0,this.hasArrived=false,this.snappedCoord=null,this.prevDistToNext=null,this.lifecycleFSM.dispatch({type:"ROUTE_LOADED"}),this.emit("routeLoaded",{routeLength:e.length});}startNavigation(){this.lifecycleFSM.dispatch({type:"USER_START_NAVIGATION"}),this.emit("navigationStarted");}update(e){if(this.rawGpsCoord=e.coord,this.rawGpsBearing=e.bearing,this.currentSpeed=e.speed,this.lastGpsTimestamp=e.timestamp,this.route.length<2)return this.getState();let[t,n]=this.kalman.update(e.coord[0],e.coord[1],e.accuracy??10,e.timestamp),i=[t,n],r=Math.max(0,this.routeIndex-2),o=this.snapper.snap(i,this.route,r,25,this.options.isCircuit,this.maxProgressIdx,this.routeIndex,e.bearing,this.prevSegment);this.corridor=Z(e.speed,e.accuracy,this.options.baseCorridorMeters,this.options.maxCorridorMeters);let c=te(this.routeIndex,o.idx,this.route,o.dist,80);if(this.prevSegment=this.routeIndex,this.routeIndex=c,this.maxProgressIdx=Math.max(this.maxProgressIdx,c),this.snappedCoord=o.proj,this.distanceToRoute=o.dist,this.forwardCum.length>c){let a=this.forwardCum[c]??0;this.linearOffset=oe(this.linearOffset,a,.15);}if(this.has(E.ADVANCED_BEARING)?this.bearing=ee(this.bearing,this.route,this.routeIndex,e.speed,e.bearing,this.options.bearingSmoothingAlpha):e.bearing!==null&&(this.bearing=e.bearing),this.has(E.COMPOUND_DEVIATION)&&(this.offRouteFSM.state==="ON_ROUTE"||this.offRouteFSM.state==="DEVIATING")){let a=Ae(i,this.route,r,25),u=this.deviationDetector.evaluate(i,e.bearing,e.speed,a,this.route,this.corridor,this.prevDistToNext);if(a.segmentIndex<this.route.length-1){let d=this.route[Math.min(a.segmentIndex+2,this.route.length-1)];this.prevDistToNext=m(i[1],i[0],d[1],d[0]);}u.confirmed?this.offRouteFSM.state==="ON_ROUTE"?this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:a.segmentIndex}):this.offRouteFSM.isDeviationConfirmed()&&(this.offRouteFSM.dispatch({type:"DEVIATION_CONFIRMED",deviationSegmentIdx:a.segmentIndex}),this.emit("deviation",{anchorIndex:a.segmentIndex,ghostCoord:o.proj})):this.offRouteFSM.state==="DEVIATING"&&this.offRouteFSM.dispatch({type:"RESUME"});}else this.has(E.COMPOUND_DEVIATION)||(o.dist>this.corridor&&this.offRouteFSM.state==="ON_ROUTE"?(this.offRouteFSM.dispatch({type:"DEVIATION_SUSPECTED",deviationSegmentIdx:o.idx}),this.emit("deviation",{anchorIndex:o.idx,ghostCoord:o.proj})):o.dist<=this.corridor&&this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.dispatch({type:"RESUME"}));if(this.lifecycleFSM.state==="APPROACHING_ROUTE"&&o.dist<this.corridor&&this.lifecycleFSM.dispatch({type:"ROUTE_JOINED",data:{index:o.idx}}),this.instructions.length>0){let a=ne(this.routeIndex,this.instructions,this.instrIdx,this.route,i,e.bearing,this.linearOffset,e.speed);a.nextIdx!==this.instrIdx&&(this.instrIdx=a.nextIdx),a.wasTriggered&&a.instruction&&(this.emit("instruction",a.instruction),this.instrIdx=a.nextIdx+1);}if(!this.hasArrived&&this.route.length>0){let a=this.route[this.route.length-1],u=m(i[1],i[0],a[1],a[0]),d=this.options.isCircuit?this.routeIndex>=this.route.length*.8:true;u<this.options.arrivalThresholdMeters&&d&&(this.hasArrived=true,this.lifecycleFSM.dispatch({type:"NAVIGATION_FINISHED"}),this.emit("arrival"));}return this.has(E.DEAD_RECKONING)&&this.deadReckoning.setAnchor(o.proj,o.idx,e.timestamp),this.getState()}tick(e){if(this.has(E.DEAD_RECKONING)){let t=this.deadReckoning.predict(this.currentSpeed,this.bearing,e);t.isActive&&(this.snappedCoord=t.predictedCoord);}if(this.lastGpsTimestamp>0){let t=(e-this.lastGpsTimestamp)/1e3;t>0&&t<10&&(this.linearOffset=re(this.linearOffset,this.currentSpeed,t));}return this.getState()}getState(){let e=this.route.length>0&&this.reverseCum.length>this.routeIndex?this.reverseCum[this.routeIndex]??null:null,t=this.instrIdx<this.instructions.length?(()=>{let i=this.instructions[this.instrIdx];return i.cumulativeOffset!==void 0?Math.max(0,i.cumulativeOffset-this.linearOffset):null})():null,n=this.lastGpsTimestamp>0?Date.now()-this.lastGpsTimestamp:0;return {snappedCoord:this.snappedCoord,rawGpsCoord:this.rawGpsCoord,bearing:this.bearing,rawGpsBearing:this.rawGpsBearing,routeIndex:this.routeIndex,distanceToRoute:this.distanceToRoute,corridor:this.corridor,isOffRoute:this.offRouteFSM.state!=="ON_ROUTE"&&this.offRouteFSM.state!=="DEVIATING",progressIndex:this.routeIndex,hasArrived:this.hasArrived,offRouteState:this.offRouteFSM.state,lifecycleState:this.lifecycleFSM.state,distanceToDestination:e,distanceToNextInstruction:t,nextInstructionIndex:this.instrIdx<this.instructions.length?this.instrIdx:null,currentSpeed:this.currentSpeed,isGpsStale:n>3e3,gpsAgeMs:n,licenseStatus:this.license.getStatus()}}destroy(){this.listeners.clear(),this.route=[],this.instructions=[],this.kalman.reset(),this.deadReckoning.clearAnchor(),this.lifecycleFSM.reset(),this.offRouteFSM.reset();}on(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t);}off(e,t){let n=this.listeners.get(e);if(n){let i=n.indexOf(t);i>=0&&n.splice(i,1);}}emit(e,...t){let n=this.listeners.get(e);if(n)for(let i of n)i(...t);}getLicenseManager(){return this.license}getRoute(){return this.route}getInstructions(){return this.instructions}getOffRouteFSM(){return this.offRouteFSM}getLifecycleFSM(){return this.lifecycleFSM}};
|
|
8
8
|
export{ve as Constants,se as CustomRouteBuilder,D as DeadReckoningEngine,N as DeviationDetector,de as ETAEngine,E as FEATURES,pe as GeofencingEngine,ie as InstructionEditor,M as KalmanFilter2D,G as LicenseManager,he as NavCore,w as NavigationFSM,ae as OSRMDirectionsProvider,F as OffRouteFSM,le as OpenRouteServiceProvider,Q as ParallelRoadResolver,T as RouteSnapper,ce as ValhallaDirectionsProvider,ue as VoiceTriggerEngine,re as advancePredictor,ee as applyBearingSmoothing,oe as applyGpsCorrection,U as blendBearing,K as buildForwardCumulativeDistances,j as buildReverseCumulativeDistances,Z as calculateDynamicCorridor,Me as calculateRobustBearing,Oe as computeDeadReckoningOnRoute,V as destinationPoint,we as geoGetBearing,f as getBearingBetweenPoints,_e as getSpeedAdaptiveAlpha,m as haversineDistance,H as interpolatePoint,me as kalmanUpdate2D,y as normalizeAngle,ze as pointAtOffset,W as projectPointOnSegment,ne as resolveNextInstruction,te as validateProgressUpdate};
|
package/package.json
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ingissa/navcore-core",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "NavCore navigation engine — zero runtime dependencies. Framework-agnostic. Works in browsers, React Native, Node.js, and CI/CD.",
|
|
5
|
-
"author": "Ingissa <
|
|
6
|
-
"license": "
|
|
7
|
-
"homepage": "https://navcore.dev",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/ingissa/navcore-sdk.git"
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ingissa/navcore-core",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "NavCore navigation engine — zero runtime dependencies. Framework-agnostic. Works in browsers, React Native, Node.js, and CI/CD.",
|
|
5
|
+
"author": "Ingissa <8abnav@gmail.com>",
|
|
6
|
+
"license": "Commercial",
|
|
7
|
+
"homepage": "https://navcore.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ingissa/navcore-sdk.git"
|
|
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
|
+
}
|