gps-plus-slam-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EULA.md ADDED
@@ -0,0 +1,39 @@
1
+ # End User License Agreement (EULA)
2
+
3
+ **gps-plus-slam-js** — Copyright (c) 2026 cs-util-com. All rights reserved.
4
+
5
+ ## 1. Grant of License
6
+
7
+ Subject to the terms of this Agreement, cs-util-com grants you a limited, non-exclusive, non-transferable, revocable license to use the software ("Software") solely for the purpose of developing applications that integrate with it via its published API.
8
+
9
+ ## 2. Restrictions
10
+
11
+ You shall NOT:
12
+
13
+ - Reverse engineer, decompile, disassemble, or otherwise attempt to derive the source code of the Software, except to the extent permitted by applicable law (e.g., EU Directive 2009/24/EC for interoperability purposes).
14
+ - Modify, adapt, translate, or create derivative works based on the Software.
15
+ - Redistribute, sublicense, rent, lease, or lend the Software to any third party, except as part of an application that uses the Software as an unmodified dependency.
16
+ - Remove or alter any proprietary notices, labels, or marks on the Software.
17
+
18
+ ## 3. License Key
19
+
20
+ The Software requires a valid license key to operate. A community license key is provided in the open-source companion repositories. The community key has a rolling expiration date of 12 months. Releases of the open-source packages include a renewed key to replace the old one when needed.
21
+ Active users who regularly update their dependencies will automatically receive new valid license keys this way.
22
+
23
+ For licenses with an extended validity date, please contact support@csutil.com.
24
+
25
+ ## 4. Intellectual Property
26
+
27
+ The Software and all copies thereof are proprietary to cs-util-com and title thereto remains in cs-util-com. The Software is copyrighted and is protected by copyright laws and international treaty provisions.
28
+
29
+ ## 5. Disclaimer of Warranty
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL CS-UTIL-COM BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
32
+
33
+ ## 6. Termination
34
+
35
+ This license is effective until terminated. It will terminate automatically if you fail to comply with any term of this Agreement. Upon termination, you must destroy all copies of the Software in your possession.
36
+
37
+ ## 7. Governing Law
38
+
39
+ This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which cs-util-com is established, without regard to conflict of law principles.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 cs-util-com. All rights reserved.
2
+
3
+ This software is proprietary and confidential. No part of this software
4
+ may be reproduced, distributed, or transmitted in any form or by any
5
+ means without the prior written permission of the copyright holder.
6
+
7
+ See EULA.md for the full End User License Agreement.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # gps-plus-slam-js
2
+
3
+ TypeScript library for real-time GPS + AR odometry alignment.
4
+
5
+ > **Proprietary — see [EULA.md](EULA.md).** Distributed as a pre-built package. Use requires a valid license key (a community key is bundled with the open-source companion packages — see [License Key](#license-key) below).
6
+
7
+ `gps-plus-slam-js` fuses AR pose tracking (odometry) with GPS readings to compute a transformation matrix that maps device-local coordinates to geo-referenced positions. It is designed for location-based AR applications where accurate outdoor positioning matters.
8
+
9
+ - **Sub-meter positioning** — Combines high-frequency AR odometry with noisy GPS to produce smoother, more accurate trajectories than raw GPS alone.
10
+ - **Fully offline** — All computation runs on-device with no network requests; works in airplane mode or areas without connectivity.
11
+ - **Framework-agnostic** — Pure TypeScript with a Redux-based state store; integrates with any AR runtime (WebXR, AR Foundation, etc.).
12
+ - **Incremental alignment** — The alignment matrix updates live as new observations arrive; no batch post-processing required.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install gps-plus-slam-js
18
+ ```
19
+
20
+ Requires Node.js ≥ 20.
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import {
26
+ createGpsSlamStore,
27
+ setZeroPos,
28
+ recordGpsEvent,
29
+ getAlignmentMatrix,
30
+ getZeroReference,
31
+ fusedGpsFromOdom,
32
+ } from 'gps-plus-slam-js';
33
+
34
+ // 1. Create the state store
35
+ const store = createGpsSlamStore();
36
+
37
+ // 2. Set the GPS origin (zero reference)
38
+ store.dispatch(setZeroPos({ lat: 48.2082, lon: 16.3738 }));
39
+
40
+ // 3. Feed odometry + GPS observations
41
+ store.dispatch(
42
+ recordGpsEvent({
43
+ odomPosition: [x, y, z], // AR-local position
44
+ odomRotation: [qx, qy, qz, qw], // AR-local rotation
45
+ gpsPoint: {
46
+ id: crypto.randomUUID(),
47
+ zeroRef: { lat: 48.2082, lon: 16.3738 },
48
+ latitude: 48.2083,
49
+ longitude: 16.3739,
50
+ altitude: 170,
51
+ latLongAccuracy: 5,
52
+ coordinates: [dx, dy, dz], // meters relative to zeroRef
53
+ weight: 1,
54
+ timestamp: Date.now(),
55
+ },
56
+ })
57
+ );
58
+
59
+ // 4. Read the computed alignment
60
+ const state = store.getState();
61
+ const matrix = getAlignmentMatrix(state); // 4×4 column-major Matrix4 | null
62
+ const zeroRef = getZeroReference(state); // { lat, lon } | null
63
+
64
+ // 5. Convert any AR position to GPS coordinates
65
+ if (matrix && zeroRef) {
66
+ const gps = fusedGpsFromOdom(matrix, [x, y, z], zeroRef);
67
+ console.log(gps.lat, gps.lon, gps.altitude);
68
+ }
69
+ ```
70
+
71
+ ## API Overview
72
+
73
+ ### Store
74
+
75
+ | Export | Description |
76
+ | ------------------------------ | --------------------------------------- |
77
+ | `createGpsSlamStore(options?)` | Create a Redux store for GPS+SLAM state |
78
+ | `LIB_VERSION` | Library version string |
79
+
80
+ ### Actions (dispatch to the store)
81
+
82
+ | Export | Description |
83
+ | --------------------------------------------------------- | ------------------------------------ |
84
+ | `setZeroPos({ lat, lon })` | Set the GPS origin |
85
+ | `recordGpsEvent(payload)` | Record an odometry + GPS observation |
86
+ | `odometryTrackingRestarted(payload)` | Handle AR tracking reset |
87
+ | `arLoopClosureDetected(payload)` | Apply loop closure correction |
88
+ | `add2dImage(payload)` | Record a camera capture |
89
+ | `markReferencePoint(payload)` | Mark a ground-truth reference point |
90
+ | `addMarker / addLine / addArea` | Add map elements |
91
+ | `addToHeatMaps / addHeatMapArea` | Add heat map data |
92
+ | `recordPhoneHeight` | Record floor detection |
93
+ | `addOrUpdateArPlane` | Record AR plane |
94
+ | `startEventArea / addCornerToEventArea / finishEventArea` | Event area workflow |
95
+
96
+ ### Selectors (read from state)
97
+
98
+ | Export | Description |
99
+ | ----------------------------- | ------------------------------- |
100
+ | `getAlignmentMatrix(state)` | 4×4 alignment matrix or `null` |
101
+ | `getAlignmentRotation(state)` | Alignment quaternion or `null` |
102
+ | `getOdometryPositions(state)` | Recorded odometry positions |
103
+ | `getOdometryRotations(state)` | Recorded odometry rotations |
104
+ | `getGpsPositions(state)` | Recorded GPS positions |
105
+ | `getReferencePoints(state)` | User-defined reference points |
106
+ | `getZeroReference(state)` | GPS origin or `null` |
107
+ | `getGpsAccuracyStats(state)` | `{ median, mean }` GPS accuracy |
108
+
109
+ ### Math Utilities
110
+
111
+ | Export | Description |
112
+ | -------------------------------------------- | ----------------------------------------- |
113
+ | `fusedGpsFromOdom(matrix, odomPos, zeroRef)` | AR position → GPS coordinates |
114
+ | `calcRelativeCoordsInMeters(origin, coord)` | GPS → local meters offset |
115
+ | `calcGpsCoords(origin, relative)` | Local meters → GPS coordinates |
116
+ | `distanceInMeters(a, b)` | Haversine distance between two GPS points |
117
+ | `calcGeoHash / geoHashToLatLong` | Geohash encoding/decoding |
118
+ | `calcQuadKey / quadKeyToLatLong` | Bing Maps quad-key encoding/decoding |
119
+ | `toEarthCenteredCoordinates(coord)` | GPS → ECEF |
120
+
121
+ ### Serializable Types
122
+
123
+ `Vector3`, `Quaternion`, `Matrix4`, `Vector4`, `LatLong`, `LatLongAlt`, and conversion helpers (`fromVector3`, `toVector3`, `fromQuaternion`, `toQuaternion`, `fromMatrix4`, `toMatrix4`, etc.).
124
+
125
+ ### License Key
126
+
127
+ | Export | Description |
128
+ | ----------------------------------- | -------------------------------------- |
129
+ | `validateLicenseKey(key, options?)` | Validate an Ed25519-signed license key |
130
+
131
+ A community license key is included in the open-source companion packages and renewed with each release. See [EULA.md](EULA.md) §3.
132
+
133
+ ## License
134
+
135
+ **Proprietary** — Copyright (c) 2026 cs-util-com. All rights reserved.
136
+
137
+ This library is distributed as a pre-built package. See [EULA.md](EULA.md) for the full End User License Agreement.
138
+
139
+ ## Companion Packages
140
+
141
+ The open-source companion packages provide a full AR+GPS application framework built on top of this library:
142
+
143
+ - **gps-plus-slam-app-framework** — Reusable AR session management, GPS sensors, visualization, and storage utilities (Apache 2.0)
144
+ - **gps-plus-slam-recorder** — Reference AR recording application (Apache 2.0)
145
+
146
+ Source: [github.com/cs-util-com/location-based-webxr](https://github.com/cs-util-com/location-based-webxr)
@@ -0,0 +1,564 @@
1
+
2
+ import * as _$_reduxjs_toolkit0 from "@reduxjs/toolkit";
3
+ import { mat4, quat, vec3 } from "gl-matrix";
4
+ import * as _$redux from "redux";
5
+ import * as _$redux_thunk0 from "redux-thunk";
6
+
7
+ //#region ../src/state/serializableTypes.d.ts
8
+ /** Serializable equivalent of vec3 (Float32Array) */
9
+ type Vector3 = readonly [number, number, number];
10
+ /** Serializable equivalent of quat (Float32Array) */
11
+ type Quaternion = readonly [number, number, number, number];
12
+ /**
13
+ * Generic 4-component tuple for non-rotation data (e.g., weighted GPS
14
+ * positions `[lat, lon, alt, weight]`). Structurally identical to
15
+ * `Quaternion` but semantically distinct.
16
+ */
17
+ type Vector4 = readonly [number, number, number, number];
18
+ /**
19
+ * Serializable equivalent of mat4 (Float32Array) - column-major 4x4
20
+ * gl-matrix uses column-major order internally.
21
+ */
22
+ type Matrix4 = readonly [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number];
23
+ /** Convert a Vector3 to a gl-matrix vec3 for math operations */
24
+ declare const fromVector3: (t: Vector3) => vec3;
25
+ /** Convert a Quaternion to a gl-matrix quat for math operations */
26
+ declare const fromQuaternion: (t: Quaternion) => quat;
27
+ /** Convert a Matrix4 to a gl-matrix mat4 for math operations */
28
+ declare const fromMatrix4: (t: Matrix4) => mat4;
29
+ /** Convert a gl-matrix vec3 to a Vector3 for Redux storage */
30
+ declare const toVector3: (v: vec3) => Vector3;
31
+ /** Convert a gl-matrix quat to a Quaternion for Redux storage */
32
+ declare const toQuaternion: (q: quat) => Quaternion;
33
+ /** Convert a gl-matrix mat4 to a Matrix4 for Redux storage */
34
+ declare const toMatrix4: (m: mat4) => Matrix4;
35
+ /** Clone a Vector3 (creates a new array reference) */
36
+ declare const cloneVector3: (t: Vector3) => Vector3;
37
+ /** Clone a Quaternion (creates a new array reference) */
38
+ declare const cloneQuaternion: (t: Quaternion) => Quaternion;
39
+ /** Clone a Matrix4 (creates a new array reference) */
40
+ declare const cloneMatrix4: (t: Matrix4) => Matrix4;
41
+ /** Identity quaternion as a tuple [0, 0, 0, 1] */
42
+ declare const IDENTITY_QUATERNION: Quaternion;
43
+ /** Zero vector as a tuple [0, 0, 0] */
44
+ declare const ZERO_VECTOR3: Vector3;
45
+ /** Identity matrix as a tuple */
46
+ declare const IDENTITY_MATRIX4: Matrix4;
47
+ /** Normalize a quaternion tuple (returns a new tuple) */
48
+ declare const normalizeQuaternion: (t: Quaternion) => Quaternion;
49
+ /** Check if a Matrix4 is the identity matrix (exact comparison of all 16 elements) */
50
+ declare const isIdentityMatrix4: (m: Matrix4) => boolean;
51
+ /** Compute the magnitude (length) of a Quaternion tuple */
52
+ declare const quaternionMagnitude: (q: Quaternion) => number;
53
+ /** Check if a Quaternion is exactly the identity [0, 0, 0, 1] */
54
+ declare const isIdentityQuaternion: (q: Quaternion) => boolean;
55
+ /** Check if a Quaternion is approximately the identity within a per-component tolerance */
56
+ declare const isNearIdentityQuaternion: (q: Quaternion, epsilon: number) => boolean;
57
+ /**
58
+ * Check if two quaternions represent the same rotation within a tolerance.
59
+ * Handles the q ≈ -q double cover: both q and -q encode the same rotation in SO(3).
60
+ */
61
+ declare const quaternionsEquivalent: (q1: Quaternion, q2: Quaternion, tolerance?: number) => boolean;
62
+ /** Multiply two Quaternion tuples, returning a new Quaternion tuple */
63
+ declare const multiplyQuaternions: (a: Quaternion, b: Quaternion) => Quaternion;
64
+ /** Invert a Quaternion tuple, returning a new Quaternion tuple */
65
+ declare const invertQuaternion: (q: Quaternion) => Quaternion;
66
+ /**
67
+ * Convert a position from WebXR local-floor frame to NUE (North-Up-East).
68
+ *
69
+ * WebXR: X=East, Y=Up, Z=South (toward viewer).
70
+ * NUE: X=North, Y=Up, Z=East.
71
+ *
72
+ * Mapping: NUE[North] = -WebXR[Z], NUE[Up] = WebXR[Y], NUE[East] = WebXR[X].
73
+ */
74
+ declare const webxrToNUE: (v: Vector3) => Vector3;
75
+ /**
76
+ * Convert a position from NUE (North-Up-East) to WebXR local-floor frame.
77
+ * Inverse of webxrToNUE.
78
+ *
79
+ * NUE: X=North, Y=Up, Z=East.
80
+ * WebXR: X=East, Y=Up, Z=South.
81
+ *
82
+ * Mapping: WebXR[X=East] = NUE[East], WebXR[Y=Up] = NUE[Up], WebXR[Z=South] = -NUE[North].
83
+ */
84
+ declare const nueToWebXR: (v: Vector3) => Vector3;
85
+ /**
86
+ * Convert a quaternion from WebXR local-floor frame to NUE (North-Up-East).
87
+ *
88
+ * Equivalent to the similarity transform R·q·R⁻¹ where R is the 90° CW
89
+ * Y-rotation basis-change quaternion (0, sin(π/4), 0, cos(π/4)).
90
+ * The vector part [x, y, z] transforms the same way as position: [-z, y, x].
91
+ * The scalar part w is invariant under basis change.
92
+ */
93
+ declare const webxrQuaternionToNUE: (q: Quaternion) => Quaternion;
94
+ /**
95
+ * Convert a quaternion from NUE (North-Up-East) to WebXR local-floor frame.
96
+ * Inverse of webxrQuaternionToNUE.
97
+ *
98
+ * The vector part [x, y, z] transforms the same way as position: [z, y, -x].
99
+ * The scalar part w is invariant under basis change.
100
+ */
101
+ declare const nueQuaternionToWebXR: (q: Quaternion) => Quaternion;
102
+ /**
103
+ * Convert a quaternion from ENU (East-North-Up) to NUE (North-Up-East).
104
+ *
105
+ * ENU is the W3C DeviceOrientation earth frame: X=East, Y=North, Z=Up.
106
+ * NUE is our internal state frame: X=North, Y=Up, Z=East.
107
+ *
108
+ * Mapping: NUE_x(North) = ENU_y, NUE_y(Up) = ENU_z, NUE_z(East) = ENU_x.
109
+ * The vector part [x, y, z] transforms as [qy, qz, qx].
110
+ * The scalar part w is invariant under basis change.
111
+ */
112
+ declare const enuQuaternionToNUE: (q: Quaternion) => Quaternion;
113
+ /**
114
+ * Convert a quaternion from NUE (North-Up-East) to ENU (East-North-Up).
115
+ * Inverse of enuQuaternionToNUE.
116
+ *
117
+ * Mapping: ENU_x(East) = NUE_z, ENU_y(North) = NUE_x, ENU_z(Up) = NUE_y.
118
+ * The vector part [x, y, z] transforms as [qz, qx, qy].
119
+ * The scalar part w is invariant under basis change.
120
+ */
121
+ declare const nueQuaternionToENU: (q: Quaternion) => Quaternion;
122
+ /**
123
+ * Raw device-orientation sensor reading.
124
+ * Represents the Euler angles from the DeviceOrientationEvent API.
125
+ */
126
+ interface RawDeviceOrientation {
127
+ /** Compass heading in degrees (0–360). */
128
+ alpha: number;
129
+ /** Pitch (front-back tilt) in degrees (-180 to 180). */
130
+ beta: number;
131
+ /** Roll (left-right tilt) in degrees (-90 to 90). */
132
+ gamma: number;
133
+ /** Whether alpha is relative to magnetic north. */
134
+ absolute: boolean;
135
+ }
136
+ /**
137
+ * Convert Euler angles from W3C DeviceOrientationEvent to a Quaternion.
138
+ * Rotation order: ZXY intrinsic (per W3C DeviceOrientation spec §3.1).
139
+ * R = Rz(α) · Rx(β) · Ry(γ) → q = qZ · qX · qY
140
+ *
141
+ * @param alpha - Compass heading in degrees
142
+ * @param beta - Pitch (front-back tilt) in degrees
143
+ * @param gamma - Roll (left-right tilt) in degrees
144
+ * @returns Quaternion as [x, y, z, w]
145
+ */
146
+ declare const eulerToQuaternion: (alpha: number, beta: number, gamma: number) => Quaternion;
147
+ //#endregion
148
+ //#region ../src/state/models/gpsEvents.d.ts
149
+ /**
150
+ * Plain object representing GPS events state.
151
+ * Fully serializable for Redux - no class, no [immerable] symbol needed.
152
+ */
153
+ interface GpsEventsState {
154
+ odometryPositions: Vector3[];
155
+ odometryRotations: Quaternion[];
156
+ gpsPositions: GpsPoint[];
157
+ gpsPositionsVec4: Vector4[];
158
+ alignmentMatrix: Matrix4;
159
+ alignmentRotation: Quaternion;
160
+ alignmentTranslation: Vector3;
161
+ alignmentRotationInDegree: Vector3;
162
+ odometryPosOffset: Vector3;
163
+ odometryRotOffset: Quaternion;
164
+ latestLoopClosureFixPointPos: Vector3 | null;
165
+ latestLoopClosureFixPointRot: Quaternion | null;
166
+ gpsAccuracyMedian: number | null;
167
+ gpsAccuracyMean: number | null;
168
+ currentGpsPosGeoHash: string | null;
169
+ }
170
+ //#endregion
171
+ //#region ../src/state/types.d.ts
172
+ /**
173
+ * GpsEventsState is now a plain serializable interface.
174
+ * All Redux state is now 100% serializable - no class instances, no middleware exceptions needed.
175
+ */
176
+ type GpsEventsType = GpsEventsState;
177
+ interface LatLong {
178
+ lat: number;
179
+ lon: number;
180
+ }
181
+ /**
182
+ * LatLong extended with an optional altitude (metres above WGS-84 ellipsoid).
183
+ * Use for any geo-coordinate that may carry elevation, e.g. reference-point
184
+ * GPS positions and parsed action payloads.
185
+ */
186
+ interface LatLongAlt extends LatLong {
187
+ altitude?: number;
188
+ }
189
+ /**
190
+ * Raw GPS observation as captured from the sensor — no derived fields.
191
+ * Used in action payloads (persisted to disk) to store only raw sensor values.
192
+ * The reducer converts RawGpsPoint → GpsPoint by computing derived fields.
193
+ */
194
+ interface RawGpsPoint {
195
+ id: string;
196
+ latitude: number;
197
+ longitude: number;
198
+ altitude?: number;
199
+ latLongAccuracy?: number;
200
+ altitudeAccuracy?: number;
201
+ /** GPS heading (direction of travel) in degrees, or undefined if unavailable */
202
+ heading?: number;
203
+ /** GPS speed in m/s, or undefined if unavailable */
204
+ speed?: number;
205
+ /**
206
+ * Whether the device orientation alpha angle is relative to magnetic north.
207
+ * `true` = absolute (compass heading meaningful for drift correction).
208
+ * `false` = arbitrary reference (heading unreliable).
209
+ * `undefined` = sensor not available or flag not captured.
210
+ */
211
+ compassAbsolute?: boolean;
212
+ /** Epoch milliseconds (Date.now() / new Date().getTime()) */
213
+ timestamp: number;
214
+ }
215
+ /**
216
+ * State-side GPS point — extends RawGpsPoint with all derived fields.
217
+ * Computed by the reducer from RawGpsPoint + state.zero + rawDeviceOrientation.
218
+ */
219
+ interface GpsPoint extends RawGpsPoint {
220
+ zeroRef: LatLong;
221
+ coordinates: Vector3;
222
+ readonly weight: number;
223
+ deviceRotation?: Quaternion;
224
+ }
225
+ interface GpsMarker {
226
+ id: string;
227
+ position: LatLong;
228
+ label?: string;
229
+ }
230
+ interface GpsLine {
231
+ id: string;
232
+ points: LatLong[];
233
+ properties?: Record<string, unknown>;
234
+ }
235
+ interface AreaPolygon {
236
+ id: string;
237
+ outline: LatLong[];
238
+ properties?: Record<string, unknown>;
239
+ }
240
+ interface HeatMapTile {
241
+ id: string;
242
+ centroid: LatLong;
243
+ intensity: number;
244
+ samples: number;
245
+ }
246
+ interface HeatArea {
247
+ id: string;
248
+ category: string;
249
+ geoHash: string;
250
+ average: number;
251
+ samples: number;
252
+ }
253
+ interface ArImageCapture {
254
+ imageFile: string;
255
+ position: Vector3;
256
+ rotation: Quaternion;
257
+ screenRotation: number;
258
+ /** Epoch milliseconds (Date.now() / new Date().getTime()) */
259
+ capturedAt?: number;
260
+ }
261
+ /**
262
+ * A user-defined reference point for ground truth validation.
263
+ * Reference points are physical landmarks that can be revisited across sessions
264
+ * to verify alignment accuracy.
265
+ */
266
+ interface ReferencePoint {
267
+ /** Unique identifier for this reference point (e.g., "benchCorner", "pointA") */
268
+ id: string;
269
+ /** AR pose when the reference point was marked */
270
+ position: Vector3;
271
+ rotation: Quaternion;
272
+ /** GPS data at the moment of marking */
273
+ gpsPoint: GpsPoint;
274
+ /** Epoch milliseconds (Date.now() / new Date().getTime()) */
275
+ timestamp: number;
276
+ }
277
+ /**
278
+ * Payload for the markReferencePoint action.
279
+ */
280
+ interface MarkReferencePointPayload {
281
+ /** Unique identifier for this reference point */
282
+ id: string;
283
+ /** AR pose position when marking */
284
+ position: Vector3;
285
+ /** AR pose rotation when marking */
286
+ rotation: Quaternion;
287
+ /** Raw GPS point data at the moment of marking */
288
+ rawGpsPoint: RawGpsPoint;
289
+ /** Optional timestamp (defaults to Date.now() if not provided) */
290
+ timestamp?: number;
291
+ }
292
+ interface OdometryPath {
293
+ points: ArImageCapture[];
294
+ }
295
+ interface FloorDetection {
296
+ id: string;
297
+ height: number;
298
+ /** Epoch milliseconds (Date.now() / new Date().getTime()) */
299
+ timestamp: number;
300
+ confidence?: number;
301
+ }
302
+ type PlaneAlignment = 'HorizontalUp' | 'HorizontalDown' | 'Vertical';
303
+ interface DetectedArPlane {
304
+ id: string;
305
+ planeId: string;
306
+ alignment: PlaneAlignment;
307
+ polygon: LatLong[];
308
+ parentPlaneId: string | null;
309
+ arSpacePosition: Vector3;
310
+ arSpaceRotation: Quaternion;
311
+ }
312
+ interface EventArea {
313
+ id: string;
314
+ label: string;
315
+ polygon: LatLong[];
316
+ isInArSpace: boolean;
317
+ arPoints?: Vector3[];
318
+ }
319
+ interface GpsElementsState {
320
+ gpsMarkers: GpsMarker[];
321
+ gpsLines: GpsLine[];
322
+ areas: AreaPolygon[];
323
+ heatMap: Record<string, Record<string, HeatMapTile>>;
324
+ heatAreas: Record<string, Record<string, HeatArea>>;
325
+ }
326
+ interface ArElementsState {
327
+ arPlanes: Record<string, DetectedArPlane>;
328
+ floorDetections: FloorDetection[];
329
+ eventAreas: EventArea[];
330
+ currentEventAreaId: string | null;
331
+ currentEventAreaPoints: Vector3[];
332
+ }
333
+ interface GpsModel {
334
+ zero: LatLong;
335
+ gpsEvents: GpsEventsType;
336
+ odometryPath: OdometryPath;
337
+ /** User-defined reference points for ground truth validation */
338
+ referencePoints: ReferencePoint[];
339
+ }
340
+ interface GpsSlamState {
341
+ gpsData: GpsModel | null;
342
+ gpsElements: GpsElementsState;
343
+ arElements: ArElementsState;
344
+ }
345
+ interface RecordGpsEventPayload {
346
+ odomPosition: Vector3;
347
+ odomRotation: Quaternion;
348
+ rawGpsPoint: RawGpsPoint;
349
+ rawDeviceOrientation?: RawDeviceOrientation;
350
+ }
351
+ interface OdometryTrackingRestartedPayload {
352
+ lastValidOdomPos: Vector3;
353
+ lastValidOdomRot: Quaternion;
354
+ /** @deprecated Use lastSensorOrientation. Legacy field kept for backward compat with old recordings. */
355
+ lastSensorRot?: Quaternion;
356
+ newOdomRot: Quaternion;
357
+ /** @deprecated Use newSensorOrientation. Legacy field kept for backward compat with old recordings. */
358
+ newSensorRot?: Quaternion;
359
+ /** Raw Euler angles captured when tracking was last active. */
360
+ lastSensorOrientation?: RawDeviceOrientation;
361
+ /** Raw Euler angles at the moment tracking resumed. */
362
+ newSensorOrientation?: RawDeviceOrientation;
363
+ /**
364
+ * New odometry position at the moment tracking resumed.
365
+ * Captured for diagnostic analysis of tracking restarts.
366
+ */
367
+ newOdomPos?: Vector3;
368
+ /**
369
+ * The XRReferenceSpaceEvent.transform from the reset event, serialized
370
+ * as position + orientation. Describes the post-reset native origin in
371
+ * the pre-reset coordinate system. `null` when the runtime cannot
372
+ * determine the delta (e.g. map was discarded). `undefined` when the
373
+ * reset event did not provide a transform (older browsers or no event).
374
+ */
375
+ resetTransform?: {
376
+ position: Vector3;
377
+ orientation: Quaternion;
378
+ } | null;
379
+ }
380
+ interface ArLoopClosureDetectedPayload {
381
+ lastPos: Vector3;
382
+ newPos: Vector3;
383
+ lastRot: Quaternion;
384
+ newRot: Quaternion;
385
+ }
386
+ type Add2dImagePayload = ArImageCapture;
387
+ //#endregion
388
+ //#region ../src/state/store.d.ts
389
+ interface CreateGpsSlamStoreOptions {
390
+ preloadedState?: Partial<GpsSlamState>;
391
+ /**
392
+ * Enable DevTools sanitizers for human-readable numeric array display.
393
+ * Default: true.
394
+ *
395
+ * Note: Redux Toolkit's devTools option is automatically disabled in production
396
+ * builds (when process.env.NODE_ENV === 'production'), so these sanitizers have
397
+ * no runtime effect in production regardless of this setting.
398
+ */
399
+ enableDevToolsSanitizers?: boolean;
400
+ /**
401
+ * When false, disables the SerializableStateInvariantMiddleware and
402
+ * ImmutableStateInvariantMiddleware. Set to false for high-throughput replay
403
+ * scenarios (e.g. investigation tests) where these checks add significant
404
+ * overhead.
405
+ * Default: true.
406
+ */
407
+ enableDevChecks?: boolean;
408
+ /**
409
+ * License key for the core library. **Required at runtime.** The key is
410
+ * validated using Ed25519 signature verification and an expiry check;
411
+ * `createGpsSlamStore` throws if the key is missing, invalid, or expired.
412
+ *
413
+ * Most callers should not invoke `createGpsSlamStore` directly — use
414
+ * `createRecorderStore()` from `gps-plus-slam-app-framework`, which
415
+ * supplies the bundled community license key automatically. The community
416
+ * key is renewed with each release; keeping dependencies up to date is
417
+ * sufficient for open-source / community use.
418
+ *
419
+ * Optional in TypeScript only because callers normally use the framework
420
+ * wrapper. At runtime the field is required and `undefined` throws.
421
+ *
422
+ * @see EULA.md §3 — License Key
423
+ */
424
+ licenseKey?: string;
425
+ }
426
+ /**
427
+ * Recursively sanitize an object for DevTools display, converting numeric arrays
428
+ * to readable tuple representations like "[1.23, 4.56, 7.89]".
429
+ *
430
+ * Exposed as a public export so consumers wiring their own `configureStore`
431
+ * (e.g. the AppFramework) can pass it as `actionSanitizer` / `stateSanitizer`
432
+ * to keep Redux DevTools output readable.
433
+ */
434
+ declare const sanitizeForDevTools: <T>(value: T, depth?: number) => T;
435
+ declare const createGpsSlamStore: (options?: CreateGpsSlamStoreOptions) => _$_reduxjs_toolkit0.EnhancedStore<{
436
+ gpsData: GpsModel | null;
437
+ gpsElements: GpsElementsState;
438
+ arElements: ArElementsState;
439
+ }, _$redux.UnknownAction, _$_reduxjs_toolkit0.Tuple<[_$redux.StoreEnhancer<{
440
+ dispatch: _$redux_thunk0.ThunkDispatch<{
441
+ gpsData: GpsModel | null;
442
+ gpsElements: GpsElementsState;
443
+ arElements: ArElementsState;
444
+ }, undefined, _$redux.UnknownAction>;
445
+ }>, _$redux.StoreEnhancer]>>;
446
+ type GpsSlamStore = ReturnType<typeof createGpsSlamStore>;
447
+ type GpsSlamDispatch = GpsSlamStore['dispatch'];
448
+ type RootState = ReturnType<GpsSlamStore['getState']>;
449
+ //#endregion
450
+ //#region ../src/state/gpsDataSlice.d.ts
451
+ declare const setZeroPos: _$_reduxjs_toolkit0.ActionCreatorWithPreparedPayload<[payload: LatLong], LatLong, "gpsData/setZeroPos", never, never>;
452
+ declare const recordGpsEvent: _$_reduxjs_toolkit0.ActionCreatorWithPayload<RecordGpsEventPayload, "gpsData/recordGpsEvent">;
453
+ declare const odometryTrackingRestarted: _$_reduxjs_toolkit0.ActionCreatorWithPayload<OdometryTrackingRestartedPayload, "gpsData/odometryTrackingRestarted">;
454
+ declare const arLoopClosureDetected: _$_reduxjs_toolkit0.ActionCreatorWithPayload<ArLoopClosureDetectedPayload, "gpsData/arLoopClosureDetected">;
455
+ declare const add2dImage: _$_reduxjs_toolkit0.ActionCreatorWithPayload<ArImageCapture, "gpsData/add2dImage">;
456
+ declare const markReferencePoint: _$_reduxjs_toolkit0.ActionCreatorWithPayload<MarkReferencePointPayload, "gpsData/markReferencePoint">;
457
+ declare const gpsDataReducer: (state: GpsModel | null | undefined, action: _$redux.UnknownAction) => GpsModel | null;
458
+ //#endregion
459
+ //#region ../src/state/gpsElementsSlice.d.ts
460
+ declare const addMarker: _$_reduxjs_toolkit0.ActionCreatorWithPayload<GpsMarker, "gpsElements/addMarker">;
461
+ declare const addLine: _$_reduxjs_toolkit0.ActionCreatorWithPayload<GpsLine, "gpsElements/addLine">;
462
+ declare const addArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<AreaPolygon, "gpsElements/addArea">;
463
+ declare const addToHeatMaps: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
464
+ category: string;
465
+ tiles: Record<string, HeatMapTile>;
466
+ }, "gpsElements/addToHeatMaps">;
467
+ declare const addHeatMapArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<HeatArea, "gpsElements/addHeatMapArea">;
468
+ declare const resetGpsElements: _$_reduxjs_toolkit0.ActionCreatorWithoutPayload<"gpsElements/resetGpsElements">;
469
+ declare const gpsElementsReducer: (state: GpsElementsState | undefined, action: _$redux.UnknownAction) => GpsElementsState;
470
+ //#endregion
471
+ //#region ../src/state/arElementsSlice.d.ts
472
+ declare const recordPhoneHeight: _$_reduxjs_toolkit0.ActionCreatorWithPayload<FloorDetection, "arElements/recordPhoneHeight">;
473
+ declare const addOrUpdateArPlane: _$_reduxjs_toolkit0.ActionCreatorWithPayload<DetectedArPlane, "arElements/addOrUpdateArPlane">;
474
+ declare const startEventArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
475
+ id: string;
476
+ }, "arElements/startEventArea">;
477
+ declare const addCornerToEventArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
478
+ position: Vector3;
479
+ }, "arElements/addCornerToEventArea">;
480
+ declare const correctArDriftForEventArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
481
+ drift: Vector3;
482
+ }, "arElements/correctArDriftForEventArea">;
483
+ declare const finishEventArea: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
484
+ label: string;
485
+ }, "arElements/finishEventArea">;
486
+ declare const anchorEventAreaInWorldSpace: _$_reduxjs_toolkit0.ActionCreatorWithPayload<{
487
+ areaId: string;
488
+ alignmentMatrix: Matrix4;
489
+ zeroRef: LatLong;
490
+ }, "arElements/anchorEventAreaInWorldSpace">;
491
+ declare const resetArElements: _$_reduxjs_toolkit0.ActionCreatorWithoutPayload<"arElements/resetArElements">;
492
+ declare const arElementsReducer: (state: ArElementsState | undefined, action: _$redux.UnknownAction) => ArElementsState;
493
+ //#endregion
494
+ //#region ../src/math/gpsMath.d.ts
495
+ declare const calcQuadKey: (coord: LatLong, levelOfDetail: number) => string;
496
+ declare const quadKeyToLatLong: (quadKey: string) => LatLong;
497
+ declare const calcGeoHash: (coord: LatLong, precision: number) => string;
498
+ declare const geoHashToLatLong: (geoHash: string) => LatLong;
499
+ declare const distanceInMeters: (a: LatLong, b: LatLong) => number;
500
+ declare const distanceInMetersRelative: (origin: LatLong, coord: LatLong) => number;
501
+ declare const calcRelativeCoordsInMeters: (origin: LatLong, coord: LatLong, altitude?: number, originAltitude?: number) => vec3;
502
+ declare const calcGpsCoords: (origin: LatLongAlt, relative: vec3) => LatLongAlt;
503
+ declare const toEarthCenteredCoordinates: (coord: LatLong, altitude?: number) => vec3;
504
+ declare const getGoogleMapsLink: (coord: LatLong) => string;
505
+ declare const getGoogleMapsDirectionsLink: (destination: LatLong, origin: LatLong) => string;
506
+ declare const getOpenStreetMapLink: (coord: LatLong, zoomLevel?: number) => string;
507
+ /**
508
+ * Transform a single AR odometry position to GPS coordinates using the alignment matrix.
509
+ *
510
+ * This is the canonical single-point alignment→GPS pipeline: applies the 4×4
511
+ * alignment matrix to an AR-local odometry position, then converts the resulting
512
+ * NUE offset to GPS coordinates relative to the zero reference.
513
+ *
514
+ * @param alignmentMatrix - 4×4 column-major alignment matrix from the solver
515
+ * @param odomPosition - Odometry position in NUE convention [North, Up, East].
516
+ * Must match the convention used by the alignment solver (NUE state data).
517
+ * If you have a raw WebXR position, convert with webxrToNUE() first.
518
+ * @param zeroRef - GPS origin for NUE→GPS conversion
519
+ * @returns GPS coordinates as { lat, lon, altitude? }
520
+ */
521
+ declare const fusedGpsFromOdom: (alignmentMatrix: Matrix4, odomPosition: Vector3, zeroRef: LatLongAlt) => LatLongAlt;
522
+ //#endregion
523
+ //#region ../src/state/selectors.d.ts
524
+ /** Returns the 4×4 alignment matrix, or null if not yet computed. */
525
+ declare const getAlignmentMatrix: (state: GpsSlamState) => Matrix4 | null;
526
+ /** Returns the alignment rotation quaternion, or null if not yet computed. */
527
+ declare const getAlignmentRotation: (state: GpsSlamState) => Quaternion | null;
528
+ /** Returns the recorded odometry positions (AR-local space). */
529
+ declare const getOdometryPositions: (state: GpsSlamState) => readonly Vector3[];
530
+ /** Returns the recorded odometry rotations (AR-local space). */
531
+ declare const getOdometryRotations: (state: GpsSlamState) => readonly Quaternion[];
532
+ /** Returns the recorded GPS positions with metadata. */
533
+ declare const getGpsPositions: (state: GpsSlamState) => readonly GpsPoint[];
534
+ /** Returns the user-defined reference points. */
535
+ declare const getReferencePoints: (state: GpsSlamState) => readonly ReferencePoint[];
536
+ /** Returns the GPS zero reference (origin for coordinate conversion), or null. */
537
+ declare const getZeroReference: (state: GpsSlamState) => LatLong | null;
538
+ /** Returns median and mean GPS accuracy statistics, or null values if unavailable. */
539
+ declare const getGpsAccuracyStats: (state: GpsSlamState) => {
540
+ median: number | null;
541
+ mean: number | null;
542
+ };
543
+ //#endregion
544
+ //#region ../src/licensing/license-key.d.ts
545
+ interface LicenseValidationResult {
546
+ type: 'community';
547
+ expiresAt: Date;
548
+ }
549
+ /**
550
+ * Validate a license key string using Ed25519 signature verification.
551
+ *
552
+ * The key format is `base64url(jsonPayload).base64url(ed25519Signature)`.
553
+ * The JSON payload must contain `{ type: "community", exp: <unix_seconds> }`.
554
+ *
555
+ * @param key — License key string
556
+ * @returns Validation result with key type and expiry date
557
+ * @throws Error if the key is malformed, has an invalid signature, or is expired
558
+ */
559
+ declare function validateLicenseKey(key: string): LicenseValidationResult;
560
+ //#endregion
561
+ //#region ../src/index.d.ts
562
+ declare const LIB_VERSION = "1.0.0";
563
+ //#endregion
564
+ export { type Add2dImagePayload, type ArElementsState, type ArImageCapture, type ArLoopClosureDetectedPayload, type AreaPolygon, type CreateGpsSlamStoreOptions, type DetectedArPlane, type EventArea, type FloorDetection, type GpsElementsState, type GpsLine, type GpsMarker, type GpsModel, type GpsPoint, type GpsSlamDispatch, type GpsSlamState, type GpsSlamStore, type HeatArea, type HeatMapTile, IDENTITY_MATRIX4, IDENTITY_QUATERNION, LIB_VERSION, type LatLong, type LatLongAlt, type LicenseValidationResult, type MarkReferencePointPayload, type Matrix4, type OdometryPath, type OdometryTrackingRestartedPayload, type Quaternion, type RawDeviceOrientation, type RawGpsPoint, type RecordGpsEventPayload, type ReferencePoint, type RootState, type Vector3, type Vector4, ZERO_VECTOR3, add2dImage, addArea, addCornerToEventArea, addHeatMapArea, addLine, addMarker, addOrUpdateArPlane, addToHeatMaps, anchorEventAreaInWorldSpace, arElementsReducer, arLoopClosureDetected, calcGeoHash, calcGpsCoords, calcQuadKey, calcRelativeCoordsInMeters, cloneMatrix4, cloneQuaternion, cloneVector3, correctArDriftForEventArea, createGpsSlamStore, distanceInMeters, distanceInMetersRelative, enuQuaternionToNUE, eulerToQuaternion, finishEventArea, fromMatrix4, fromQuaternion, fromVector3, fusedGpsFromOdom, geoHashToLatLong, getAlignmentMatrix, getAlignmentRotation, getGoogleMapsDirectionsLink, getGoogleMapsLink, getGpsAccuracyStats, getGpsPositions, getOdometryPositions, getOdometryRotations, getOpenStreetMapLink, getReferencePoints, getZeroReference, gpsDataReducer, gpsElementsReducer, invertQuaternion, isIdentityMatrix4, isIdentityQuaternion, isNearIdentityQuaternion, markReferencePoint, multiplyQuaternions, normalizeQuaternion, nueQuaternionToENU, nueQuaternionToWebXR, nueToWebXR, odometryTrackingRestarted, quadKeyToLatLong, quaternionMagnitude, quaternionsEquivalent, recordGpsEvent, recordPhoneHeight, resetArElements, resetGpsElements, sanitizeForDevTools, setZeroPos, startEventArea, toEarthCenteredCoordinates, toMatrix4, toQuaternion, toVector3, validateLicenseKey, webxrQuaternionToNUE, webxrToNUE };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! gps-plus-slam-js | (c) 2026 cs-util-com | UNLICENSED — see EULA.md */
2
+ import{configureStore as e,createSlice as t}from"@reduxjs/toolkit";import{ed25519 as n}from"@noble/curves/ed25519.js";import{hexToBytes as r}from"@noble/curves/utils.js";import{mat4 as i,quat as a,vec3 as o,vec4 as s}from"gl-matrix";import{castDraft as c}from"immer";let l=!1;function u(){l=!0}function d(){if(!l)throw Error(`gps-plus-slam-js: license not activated. Construct your store via createRecorderStore() from gps-plus-slam-app-framework, or pass a valid licenseKey to createGpsSlamStore(). See EULA §3.`)}function f(e){return((...t)=>(d(),e(...t)))}function p(e){let t=((...t)=>(d(),e(...t)));return Object.assign(t,e),t.toString=()=>e.type,t}function m(e){return(t,n)=>(d(),e(t,n))}function h(e){let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=`=`.repeat((4-t.length%4)%4),r=atob(t+n);return Uint8Array.from(r,e=>e.charCodeAt(0))}function g(e){let t=new Date,i=e.indexOf(`.`);if(i<=0||i>=e.length-1)throw Error(`Invalid license key format: expected "payload.signature" with base64url-encoded parts.`);let a,o;try{a=h(e.substring(0,i)),o=h(e.substring(i+1))}catch{throw Error(`Invalid license key format: base64url decoding failed.`)}let s=r(`dfe5c62120b7a0ce962b17907a15b27cf1b056e6619bf6ce55e6e094b322a470`);if(!n.verify(o,a,s))throw Error(`Invalid license key: signature verification failed.`);let c=JSON.parse(new TextDecoder().decode(a));if(typeof c!=`object`||!c)throw Error(`Invalid license key: payload must be a JSON object.`);let{type:l,exp:d}=c;if(l!==`community`)throw Error(`Invalid license key: "type" must be "community".`);if(typeof d!=`number`||!Number.isFinite(d))throw Error(`Invalid license key: "exp" must be a finite number (Unix timestamp in seconds).`);if(d<Math.floor(t.getTime()/1e3)){let e=new Date(d*1e3);throw Error(`License key expired on ${e.toISOString()}. Update to the latest version for a renewed community key.`)}return u(),{type:l,expiresAt:new Date(d*1e3)}}const _=180/Math.PI,v=e=>{let t=e[0],n=e[1],r=e[2],i=e[3],a=2*(i*t+n*r),o=1-2*(t*t+n*n),s=Math.atan2(a,o)*_,c=2*(i*n-r*t),l=Math.abs(c)>=1?Math.sign(c)*90:Math.asin(c)*_,u=2*(i*r+t*n),d=1-2*(n*n+r*r);return{pitch:l,yaw:Math.atan2(u,d)*_,roll:s}},y=1/298.257223563,b=y*(2-y),x=40075016.6856/360,S=39940652.7422/360,C=Math.PI/180,ee=180/Math.PI,w=`0123456789bcdefghjkmnpqrstuvwxyz`,T=[16,8,4,2,1],E=(e,t,n)=>Math.min(Math.max(e,t),n),te=e=>E(e,-180,180),D=e=>e*C,ne=e=>e*ee,re=e=>{if(!Number.isFinite(e))throw RangeError(`Coordinate values must be finite numbers`);return String(e)},ie=e=>({lat:re(e.lat),lon:re(e.lon)}),ae=(e,t=0)=>{let n=D(e.lat),r=D(e.lon),i=Math.sin(n),a=Math.cos(n),s=Math.sin(r),c=Math.cos(r),l=6378137/Math.sqrt(1-b*i*i),u=(l+t)*a*c,d=(l+t)*a*s,f=(l*(1-b)+t)*i;return o.fromValues(u,d,f)},oe=f((e,t)=>{if(t<1||t>23)throw RangeError(`levelOfDetail must be between 1 and 23 (inclusive)`);let n=E(e.lat,-85.05112878,85.05112878),r=te(e.lon),i=Math.sin(D(n)),a=256*2**t,o=(r+180)/360*a,s=(.5-Math.log((1+i)/(1-i))/(4*Math.PI))*a,c=2**t-1,l=E(Math.floor(o/256),0,c),u=E(Math.floor(s/256),0,c),d=``;for(let e=t;e>0;--e){let t=0,n=1<<e-1;(l&n)!==0&&(t+=1),(u&n)!==0&&(t+=2),d+=t.toString()}return d}),se=f(e=>{if(!/^[0-3]+$/.test(e))throw Error(`QuadKey must consist of digits 0-3`);let t=e.length;if(t===0)throw Error(`QuadKey must not be empty`);let n=0,r=0;for(let i=0;i<t;i+=1){let a=Number.parseInt(e[i],10),o=1<<t-i-1;a&1&&(n+=o),a&2&&(r+=o)}let i=256*2**t,a=(n+.5)*256,o=(r+.5)*256,s=a/i,c=o/i,l=s*360-180;return{lat:ne(Math.atan(Math.sinh(Math.PI*(1-2*c)))),lon:l}}),ce=f((e,t)=>{if(t<=0)throw RangeError(`precision must be greater than 0`);let n=-90,r=90,i=-180,a=180,o=``,s=0,c=0,l=!0;for(;o.length<t;){if(l){let t=(i+a)/2;e.lon>=t?(c|=T[s],i=t):a=t}else{let t=(n+r)/2;e.lat>=t?(c|=T[s],n=t):r=t}l=!l,s<4?s+=1:(o+=w[c],s=0,c=0)}return o}),le=f(e=>{if(e.length===0)throw Error(`geoHash must not be empty`);let t=-90,n=90,r=-180,i=180,a=!0;for(let o of e){let e=w.indexOf(o);if(e===-1)throw Error(`Invalid geohash character: ${o}`);for(let o of T){if(a){let t=(r+i)/2;(e&o)===0?i=t:r=t}else{let r=(t+n)/2;(e&o)===0?n=r:t=r}a=!a}}return{lat:(t+n)/2,lon:(r+i)/2}}),ue=f((e,t)=>{let n=D(e.lat),r=D(t.lat),i=r-n,a=D(t.lon-e.lon),o=Math.sin(i/2),s=Math.sin(a/2),c=o*o+Math.cos(n)*Math.cos(r)*s*s,l=Math.min(1,Math.max(0,c));return 6371008.8*(2*Math.atan2(Math.sqrt(l),Math.sqrt(1-l)))}),de=f((e,t)=>{let n=O(e,t);return Math.hypot(n[0],n[2])}),O=f((e,t,n=0,r=0)=>{let i=(t.lon-e.lon)*x*Math.cos(D(e.lat)),a=(t.lat-e.lat)*S,s=n-r;return o.fromValues(a,s,i)}),k=f((e,t)=>{let n=t[0],r=t[2];return{lat:n/S+e.lat,lon:r/(x*Math.cos(D(e.lat)))+e.lon,altitude:e.altitude==null?void 0:e.altitude+t[1]}}),fe=f((e,t=0)=>ae(e,t)),A=e=>{let{lat:t,lon:n}=ie(e);return`${t}%2C${n}`},pe=f(e=>`https://www.google.com/maps/search/?api=1&query=${A(e)}`),me=f((e,t)=>`https://www.google.com/maps/dir/?api=1&origin=${A(t)}&destination=${A(e)}`),he=f((e,t=19)=>{let{lat:n,lon:r}=ie(e);return`https://www.openstreetmap.org/query?lat=${n}&lon=${r}#map=${t}/${n}/${r}`}),ge=f((e,t,n)=>{let r=i.fromValues(...e),a=o.fromValues(t[0],t[1],t[2]),s=o.create();return o.transformMat4(s,a,r),k(n,s)}),_e=()=>({points:[]}),j=()=>({gpsMarkers:[],gpsLines:[],areas:[],heatMap:{},heatAreas:{}}),M=()=>({arPlanes:{},floorDetections:[],eventAreas:[],currentEventAreaId:null,currentEventAreaPoints:[]}),N=e=>o.fromValues(e[0],e[1],e[2]),P=e=>a.fromValues(e[0],e[1],e[2],e[3]),ve=e=>i.fromValues(...e),F=e=>[e[0],e[1],e[2]],I=e=>[e[0],e[1],e[2],e[3]],L=e=>[e[0],e[1],e[2],e[3],e[4],e[5],e[6],e[7],e[8],e[9],e[10],e[11],e[12],e[13],e[14],e[15]],R=e=>[e[0],e[1],e[2]],ye=e=>[e[0],e[1],e[2],e[3]],be=e=>[...e],z=Object.freeze([0,0,0,1]),B=Object.freeze([0,0,0]),V=Object.freeze([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),H=e=>{let t=Se(e);if(t===0)return z;let n=1/t;return[e[0]*n,e[1]*n,e[2]*n,e[3]*n]},xe=e=>{for(let t=0;t<16;t++)if(e[t]!==V[t])return!1;return!0},Se=e=>Math.sqrt(e[0]*e[0]+e[1]*e[1]+e[2]*e[2]+e[3]*e[3]),Ce=e=>e[0]===0&&e[1]===0&&e[2]===0&&e[3]===1,we=(e,t)=>Math.abs(e[0])<t&&Math.abs(e[1])<t&&Math.abs(e[2])<t&&Math.abs(e[3]-1)<t,Te=(e,t,n=1e-10)=>{let r=Math.abs(e[0]-t[0])<n&&Math.abs(e[1]-t[1])<n&&Math.abs(e[2]-t[2])<n&&Math.abs(e[3]-t[3])<n,i=Math.abs(e[0]+t[0])<n&&Math.abs(e[1]+t[1])<n&&Math.abs(e[2]+t[2])<n&&Math.abs(e[3]+t[3])<n;return r||i},Ee=(e,t)=>{let n=a.create();return a.multiply(n,e,t),[n[0],n[1],n[2],n[3]]},De=e=>{let t=a.create();return a.invert(t,e),[t[0],t[1],t[2],t[3]]},U=e=>[-e[2],e[1],e[0]],Oe=e=>[e[2],e[1],-e[0]],W=e=>[-e[2],e[1],e[0],e[3]],ke=e=>[e[2],e[1],-e[0],e[3]],G=e=>[e[1],e[2],e[0],e[3]],Ae=e=>[e[2],e[0],e[1],e[3]],K=(e,t,n)=>{if(!Number.isFinite(e)||!Number.isFinite(t)||!Number.isFinite(n))throw Error(`eulerToQuaternion: non-finite input (alpha=${e}, beta=${t}, gamma=${n})`);let r=e*Math.PI/180,i=t*Math.PI/180,o=n*Math.PI/180,s=a.create(),c=a.create(),l=a.create();a.setAxisAngle(s,[0,0,1],r),a.setAxisAngle(c,[1,0,0],i),a.setAxisAngle(l,[0,1,0],o);let u=a.create();return a.multiply(u,s,c),a.multiply(u,u,l),[u[0],u[1],u[2],u[3]]},je=t({name:`arElements`,initialState:M(),reducers:{recordPhoneHeight:(e,t)=>{e.floorDetections.push(t.payload)},addOrUpdateArPlane:(e,t)=>{let n=t.payload;if(n.parentPlaneId){delete e.arPlanes[n.planeId];return}e.arPlanes[n.planeId]=c(n)},startEventArea:(e,t)=>{e.currentEventAreaId=t.payload.id,e.currentEventAreaPoints=[]},addCornerToEventArea:(e,t)=>{e.currentEventAreaId&&e.currentEventAreaPoints.push(c(R(t.payload.position)))},correctArDriftForEventArea:(e,t)=>{let n=t.payload.drift;for(let t of e.currentEventAreaPoints)t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]},finishEventArea:(e,t)=>{e.currentEventAreaId&&(e.eventAreas.push({id:e.currentEventAreaId,label:t.payload.label,polygon:[],isInArSpace:!0,arPoints:e.currentEventAreaPoints}),e.currentEventAreaId=null,e.currentEventAreaPoints=[])},anchorEventAreaInWorldSpace:(e,t)=>{let{areaId:n,alignmentMatrix:r,zeroRef:i}=t.payload,a=e.eventAreas.findIndex(e=>e.id===n);if(a===-1)return;let s=e.eventAreas[a];if(!s.isInArSpace||!s.arPoints)throw Error(`The area ${s.id} is already anchored in world space`);let c=ve(r),l=s.arPoints.map(e=>{let t=N(e);return k(i,o.transformMat4(o.create(),t,c))});e.eventAreas[a]={...s,isInArSpace:!1,polygon:l,arPoints:void 0}},resetArElements:()=>M()}}),{recordPhoneHeight:Me,addOrUpdateArPlane:Ne,startEventArea:Pe,addCornerToEventArea:Fe,correctArDriftForEventArea:Ie,finishEventArea:Le,anchorEventAreaInWorldSpace:Re,resetArElements:ze}=je.actions,Be=p(Me),Ve=p(Ne),He=p(Pe),Ue=p(Fe),We=p(Ie),Ge=p(Le),Ke=p(Re),qe=p(ze),Je=m(je.reducer);function Ye(e,t,n,r){let{inCentroid:i,refCentroid:a,refVec:s,tmp:c}=r;o.set(i,0,0,0),o.set(a,0,0,0);let l=0;for(let n=0;n<e.length;n++){let r=t[n][3];o.scaleAndAdd(i,i,e[n],r),q(s,t[n]),o.scaleAndAdd(a,a,s,r),l+=r}if(l===0)throw Error(`Weights must not sum to zero when solving Kabsch`);let u=1/l;if(o.scale(i,i,u),o.scale(a,a,u),!n)return{inCentroid:i,refCentroid:a,scaleRatio:1};let d=0,f=0;for(let n=0;n<e.length;n++)o.subtract(c,e[n],i),d+=o.length(c),q(s,t[n]),o.subtract(c,s,a),f+=o.length(c);return{inCentroid:i,refCentroid:a,scaleRatio:d===0?1:f/d}}function Xe(e,t,n,r,a,s,c){let{negCentroid:l,scaleVec:u}=c;return i.fromRotationTranslation(e,r,n),s&&(o.set(u,a,a,a),i.scale(e,e,u)),o.negate(l,t),i.translate(e,e,l),e}function Ze(e,t,n={}){let r=e.length;if(r!==t.length)throw Error(`Length of the point lists was not equal: ${r} vs ${t.length}`);if(r===0)return i.create();let s=n.solveRotation??!0,c=n.solveScale??!1,l=n.ignoreYAxisForRotation??!1,u=n.rotationIterations??9,{inCentroid:d,refCentroid:f,scaleRatio:p}=Ye(e,t,c,{inCentroid:o.create(),refCentroid:o.create(),refVec:o.create(),tmp:o.create()}),m=a.create();s?new Qe(l).solve(e,t,d,f,m,u):a.identity(m);let h={negCentroid:o.create(),scaleVec:o.create()};return Xe(i.create(),d,f,m,p,c,h)}var Qe=class e{static twoPi=Math.PI*2;static basisX=o.fromValues(1,0,0);static basisY=o.fromValues(0,1,0);static basisZ=o.fromValues(0,0,1);constructor(e){this.ignoreYAxis=e}solve(t,n,r,i,o,s){let c=this.transposeMultSubtract(t,n,r,i);if(this.ignoreYAxis){let t=c[0][0],n=c[0][2],r=c[2][0],i=c[2][2],s=Math.atan2(r-n,t+i);a.setAxisAngle(o,e.basisY,s)}else this.extractRotation(c,o,s)}transposeMultSubtract(e,t,n,r){let i=[o.create(),o.create(),o.create()],a=o.clone(n),s=o.clone(r);this.ignoreYAxis&&(a[1]=0,s[1]=0);let c=o.create(),l=o.create(),u=o.create();for(let n=0;n<e.length;n++){o.copy(c,e[n]),q(u,t[n]),this.ignoreYAxis&&(c[1]=0,u[1]=0);let r=t[n][3];o.subtract(c,c,a),o.scale(c,c,r),o.subtract(l,u,s),i[0][0]+=c[0]*l[0],i[1][0]+=c[1]*l[0],i[2][0]+=c[2]*l[0],i[0][1]+=c[0]*l[1],i[1][1]+=c[1]*l[1],i[2][1]+=c[2]*l[1],i[0][2]+=c[0]*l[2],i[1][2]+=c[1]*l[2],i[2][2]+=c[2]*l[2]}return i}extractRotation(t,n,r){let i=[o.create(),o.create(),o.create()],s=o.create(),c=o.create(),l=o.create();for(let u=0;u<r;u++){this.fillMatrixFromQuaternion(n,i),o.cross(s,i[0],t[0]),o.cross(l,i[1],t[1]),o.add(s,s,l),o.cross(l,i[2],t[2]),o.add(s,s,l);let r=o.dot(i[0],t[0])+o.dot(i[1],t[1])+o.dot(i[2],t[2])+1e-9;r=Math.abs(r),o.scale(s,s,1/r);let u=o.length(s);if(u<1e-9)break;o.scale(c,s,1/u);let d=u%e.twoPi,f=a.create();a.setAxisAngle(f,c,d),a.multiply(n,f,n),a.normalize(n,n)}}fillMatrixFromQuaternion(t,n){o.transformQuat(n[0],e.basisX,t),o.transformQuat(n[1],e.basisY,t),o.transformQuat(n[2],e.basisZ,t)}};const q=(e,t)=>(e[0]=t[0],e[1]=t[1],e[2]=t[2],e);function $e(e,t,n,r,i,a,o=Math.random){let s=Array.from(e);if(n>s.length)throw RangeError(`minSampleSize must be smaller than the number of elements: minSampleSize=${n}, elements=${s.length}`);return nt(s,et(s,t,n,r,i,a,o),i,a)}function et(e,t,n,r,i,a,o){let s=null,c=Array(n),l=new Uint8Array(e.length);for(let u=0;u<r;u+=1){it(e,n,o,c,l);let{inliers:r,outliers:u}=tt(e,l,i(c),a);if(s===null||n+r.length>=t){let e=[...c,...r],t=i(e),n=rt(t,`createModel must assign totalModelError for comparison during RANSAC`);(s===null||n<s.error)&&(t.inliers=e,t.outliers=u,s={model:t,error:n})}}return s?.model??null}function tt(e,t,n,r){let i=[],a=[];for(let o=0;o<e.length;o+=1){if(t?.[o])continue;let s=e[o];(r(n,s)?i:a).push(s)}return{inliers:i,outliers:a}}function nt(e,t,n,r){let i=t??n(e);rt(i,`RANSAC failed to produce a model with totalModelError`);let{inliers:a,outliers:o}=tt(e,null,i,r);return a.length>0&&(i=n(a),rt(i,`createModel must assign totalModelError for final RANSAC model`)),i.inliers=a,i.outliers=o,i}function rt(e,t){let{totalModelError:n}=e;if(n==null)throw Error(t);return n}function it(e,t,n,r,i){i.fill(0);let a=0;for(;a<t;){let t=Math.floor(n()*e.length);i[t]||(i[t]=1,r[a]=e[t],a+=1)}}const at={useOnlyRecentData:!1,recentSeconds:180,includeTimeWeight:!0,weightByTimeFactor:1321,ignoreYAxisForRotation:!0,useRansac:!1,ransacInlierRatio:.95,ransacSampleRatio:.3,ransacMaxIterations:230,ransacErrorTolerance:1,geohashPrecision:8,geohashWindowSize:10,gpsAccuracyExponent:.5},ot=e=>e.map(e=>R(e)),st=e=>e.map(e=>H(e)),ct=e=>e.map(Q),lt=e=>e.map(e=>[e[0],e[1],e[2],e[3]]),J=e=>Array.isArray(e)?e:F(e),Y=e=>Array.isArray(e)?e:I(e),ut=(e,t,n,r)=>({odometryPosOffset:R(J(e)),odometryRotOffset:H(Y(t)),latestLoopClosureFixPointPos:n?R(J(n)):null,latestLoopClosureFixPointRot:r?H(Y(r)):null}),dt=(e,t,n,r)=>({odometryPositions:[],odometryRotations:[],gpsPositions:[],gpsPositionsVec4:[],alignmentMatrix:[...V],alignmentRotation:[...z],alignmentTranslation:[...B],alignmentRotationInDegree:[...B],...ut(e,t,n,r),gpsAccuracyMedian:null,gpsAccuracyMean:null,currentGpsPosGeoHash:null}),ft=()=>dt(B,z,null,null),pt=(e,t,n,r)=>{let i=e.map(e=>J(e)),a=t.map(e=>H(Y(e))),o=n.map(Q),s=r.gpsAccuracyExponent;for(let e of o)e.weight=ht(e.latLongAccuracy,s);let c=o.map(Ot),l=r.includeTimeWeight?wt(c,o,r.weightByTimeFactor):c,u=r.useOnlyRecentData?Ct(o,r.recentSeconds):0;return{allPositionTuples:i,allRotationTuples:a,allGpsPoints:o,allVec4Tuples:l,solverPositionsTyped:i.slice(u).map(N),solverGpsPoints:o.slice(u),solverWeightedVec4Tuples:l.slice(u)}},mt=e=>{let{odometryPositions:t,odometryRotations:n,gpsPoints:r,odometryPosOffset:i=B,odometryRotOffset:s=z,latestLoopClosureFixPointPos:c=null,latestLoopClosureFixPointRot:l=null,alignmentConfig:u,random:d}=e;if(t.length!==n.length||t.length!==r.length)throw Error(`GpsEvents requires odometry positions, rotations, and gps points to have identical lengths`);let f={...at,...u??{}};if(t.length===0)return dt(i,s,c,l);let{allPositionTuples:p,allRotationTuples:m,allGpsPoints:h,allVec4Tuples:g,solverPositionsTyped:_,solverGpsPoints:v,solverWeightedVec4Tuples:y}=pt(t,n,r,f),b=yt(_,y,f,d??Math.random),x=a.create(),S=o.create(),C=o.create();_t(b.matrix,x,S,C);let{mean:ee,median:w}=Et(h),T=Tt(r,f.geohashPrecision,f.geohashWindowSize);return{odometryPositions:ot(p),odometryRotations:st(m),gpsPositions:ct(h),gpsPositionsVec4:lt(g),alignmentMatrix:L(b.matrix),alignmentRotation:I(x),alignmentTranslation:F(S),alignmentRotationInDegree:F(C),...ut(i,s,c,l),gpsAccuracyMedian:w,gpsAccuracyMean:ee,currentGpsPosGeoHash:T}},ht=(e,t)=>e!=null&&e>0?1/Math.max(e,1)**+t:1,gt=(e,t,n,r,i,s)=>{let c=e.odometryPositions,l=e.odometryRotations,u=e.gpsPositions,d=e.gpsPositionsVec4;c.push(J(t)),l.push(H(Y(n)));let f=Q(r);u.push(f);let p={...at,...i??{}};f.weight=ht(f.latLongAccuracy,p.gpsAccuracyExponent);let m=Ot(f);if(p.includeTimeWeight){d.push(m);let e=wt(d,u,p.weightByTimeFactor);for(let t=0;t<e.length;t++)d[t]=e[t]}else d.push(m);let h=0;p.useOnlyRecentData&&(h=Ct(u,p.recentSeconds));let g=yt(c.slice(h).map(N),d.slice(h),p,s??Math.random);e.alignmentMatrix=L(g.matrix);let _=a.create(),v=o.create(),y=o.create();_t(g.matrix,_,v,y),e.alignmentRotation=I(_),e.alignmentTranslation=F(v),e.alignmentRotationInDegree=F(y);let{mean:b,median:x}=Et(u);e.gpsAccuracyMean=b,e.gpsAccuracyMedian=x,e.currentGpsPosGeoHash=Tt(u,p.geohashPrecision,p.geohashWindowSize)},_t=(e,t,n,r)=>{i.getRotation(t,e),a.normalize(t,t),i.getTranslation(n,e),Dt(t,r)},X=o.create(),vt=o.create(),yt=(e,t,n,r)=>{if(e.length===0)return{matrix:i.create(),meanError:0};let a=e.length,o=[];for(let n=0;n<a;n+=1){let r=t[n];o.push({odom:e[n],ref:s.fromValues(r[0],r[1],r[2],r[3])})}if(n.useRansac&&o.length>=3){let e=o.length,t=Math.max(3,Math.min(e,Math.ceil(e*n.ransacSampleRatio))),i=$e(o,Math.max(t,Math.min(e,Math.ceil(e*n.ransacInlierRatio))),t,n.ransacMaxIterations,e=>bt(e,n.ignoreYAxisForRotation),(e,t)=>xt(e,t)<e.meanAlignmentError+n.ransacErrorTolerance,r);return{matrix:i.alignmentMatrix,meanError:i.meanAlignmentError}}let c=bt(o,n.ignoreYAxisForRotation);return{matrix:c.alignmentMatrix,meanError:c.meanAlignmentError}},bt=(e,t)=>{let n=Array.from(e),r=n.length,i=Array(r),a=Array(r);for(let e=0;e<r;e+=1)i[e]=n[e].odom,a[e]=n[e].ref;let s=Ze(i,a,{ignoreYAxisForRotation:t}),c=0;for(let e=0;e<r;e+=1)o.transformMat4(X,i[e],s),c+=St(X,n[e].ref);return{alignmentMatrix:s,meanAlignmentError:r===0?0:c/r,totalModelError:c}},xt=(e,t)=>(o.transformMat4(X,t.odom,e.alignmentMatrix),St(X,t.ref)),St=(e,t)=>(o.set(vt,t[0],t[1],t[2]),o.squaredDistance(e,vt)),Ct=(e,t)=>{if(e.length===0||t<=0)return 0;let n=e[e.length-1].timestamp-t*1e3;for(let t=0;t<e.length;t+=1)if(e[t].timestamp>=n)return t;return 0},wt=(e,t,n)=>{if(e.length<2)return e.map(e=>[e[0],e[1],e[2],e[3]]);let r=t[t.length-1].timestamp,i=t[0].timestamp,a=Math.max(1,r-i);return e.map((e,i)=>{let o=n*((r-t[i].timestamp)/a)+1,s=t[i].weight,c=1/(1/(s>0&&Number.isFinite(s)?s:1)+o);return[e[0],e[1],e[2],c]})},Z=new Map,Tt=(e,t,n)=>{if(e.length===0)return null;let r=Math.max(0,e.length-n);Z.clear();for(let n=r;n<e.length;n+=1){let r=e[n],i=ce(kt(r),t),a=Z.get(i);a?(a.count+=1,a.index=n):Z.set(i,{count:1,index:n})}let i=null,a=-1,o=-1;for(let[e,{count:t,index:n}]of Z)(t>a||t===a&&n>o)&&(i=e,a=t,o=n);return i},Et=e=>{let t=e.map(e=>e.latLongAccuracy).filter(e=>typeof e==`number`&&Number.isFinite(e));if(t.length===0)return{mean:null,median:null};let n=t.reduce((e,t)=>e+t,0)/t.length,r=t.slice().sort((e,t)=>e-t),i=Math.floor(r.length/2);return{mean:n,median:r.length%2==0?(r[i-1]+r[i])/2:r[i]}},Dt=(e,t)=>{let n=e[0],r=e[1],i=e[2],a=e[3],s=2*(a*n+r*i),c=1-2*(n*n+r*r),l=Math.atan2(s,c),u=2*(a*r-i*n),d;d=Math.abs(u)>=1?Math.PI/2*Math.sign(u):Math.asin(u);let f=2*(a*i+n*r),p=1-2*(r*r+i*i),m=Math.atan2(f,p),h=180/Math.PI,g=t??o.create();return g[0]=l*h,g[1]=d*h,g[2]=m*h,g},Ot=e=>[e.coordinates[0],e.coordinates[1],e.coordinates[2],e.weight>0&&Number.isFinite(e.weight)?e.weight:1],kt=e=>({lat:e.latitude,lon:e.longitude}),Q=e=>({...e,zeroRef:{lat:e.zeroRef.lat,lon:e.zeroRef.lon},coordinates:[e.coordinates[0],e.coordinates[1],e.coordinates[2]],timestamp:e.timestamp,deviceRotation:e.deviceRotation?H(Y(e.deviceRotation)):void 0}),At=({lat:e,lon:t})=>{if(!Number.isFinite(e))throw Error(`Invalid latitude value: ${e}`);if(!Number.isFinite(t))throw Error(`Invalid longitude value: ${t}`);if(e<-90||e>90)throw Error(`Invalid latitude range: ${e}`);if(t<-180||t>180)throw Error(`Invalid longitude range: ${t}`)},jt=o.fromValues(1,1,1),Mt=(e,t)=>[e[0]+t[0],e[1]+t[1],e[2]+t[2]],Nt=(e,t,n)=>{let r=O(t,{lat:e.latitude,lon:e.longitude},e.altitude??0,0),i=n?.alpha!=null&&n?.beta!=null&&n?.gamma!=null?G(K(n.alpha,n.beta,n.gamma)):void 0;return{...e,zeroRef:t,coordinates:F(r),weight:1,deviceRotation:i}},Pt=(e,t,n)=>{if(e)return K(e.alpha,e.beta,e.gamma);if(t)return t;throw Error(`Sensor rotation missing for ${n}: neither raw orientation nor legacy quaternion provided`)},Ft=e=>{let t=a.invert(a.create(),e);if(!t)throw Error(`Encountered non-invertible quaternion`);return t},It=e=>{let t=Ft(a.normalize(a.create(),P(e.lastSensorRot))),n=a.normalize(a.create(),P(e.lastValidOdomRot)),r=a.multiply(a.create(),t,n),i=a.normalize(a.create(),P(e.newSensorRot)),o=a.multiply(a.create(),i,r),s=a.normalize(a.create(),P(e.newOdomRot)),c=a.multiply(a.create(),o,s);return a.normalize(c,c),I(c)},Lt=(e,t)=>{let n=i.create();return i.fromRotationTranslationScale(n,a.normalize(a.create(),P(t)),N(e),jt),n},Rt=(e,t,n)=>{let r=o.create(),s=o.create(),c=a.create(),l=a.create();i.getTranslation(r,e),i.getTranslation(s,t),i.getRotation(c,e),i.getRotation(l,t);let u=o.create();o.lerp(u,r,s,n);let d=a.create();return a.slerp(d,c,l,n),i.fromRotationTranslation(i.create(),d,u)},zt=(e,t)=>{if(e.length===0)return[];let n=e[e.length-1],r=i.invert(i.create(),n);if(!r)throw Error(`End pose matrix is not invertible`);let a=i.multiply(i.create(),t,r),o=i.create(),s=e.length-1;return e.map((e,t)=>{let n=Rt(o,a,s<=0?1:t/s);return i.multiply(i.create(),n,e)})},Bt=(e,t,n)=>{for(let r=e.length-1;r>=0;--r)if(n(e[r],t))return r;return-1},Vt=(e,t)=>e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2],Ht=(e,t)=>e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3],Ut=t({name:`gpsData`,initialState:null,reducers:{setZeroPos:{reducer:(e,t)=>e===null?{zero:t.payload,gpsEvents:ft(),odometryPath:_e(),referencePoints:[]}:e,prepare:e=>(At(e),{payload:e})},recordGpsEvent:(e,t)=>{if(!e)return e;let{odomPosition:n,odomRotation:r,rawGpsPoint:i,rawDeviceOrientation:a}=t.payload,o=Nt(i,e.zero,a);gt(e.gpsEvents,U(n),W(r),o)},odometryTrackingRestarted:(e,t)=>{if(!e)return e;let n=Mt(e.gpsEvents.odometryPosOffset,U(t.payload.lastValidOdomPos)),r=Pt(t.payload.lastSensorOrientation,t.payload.lastSensorRot,`lastSensor`),i=Pt(t.payload.newSensorOrientation,t.payload.newSensorRot,`newSensor`);e.gpsEvents=c(mt({odometryPositions:[],odometryRotations:[],gpsPoints:[],odometryPosOffset:n,odometryRotOffset:It({...t.payload,lastValidOdomRot:W(t.payload.lastValidOdomRot),newOdomRot:W(t.payload.newOdomRot),lastSensorRot:G(r),newSensorRot:G(i)}),latestLoopClosureFixPointPos:null,latestLoopClosureFixPointRot:null}))},arLoopClosureDetected:(e,t)=>{if(!e)return e;let n=e.gpsEvents;if(n.odometryPositions.length===0)return e;let r=n.latestLoopClosureFixPointPos?Bt(n.odometryPositions,n.latestLoopClosureFixPointPos,Vt):0,s=n.latestLoopClosureFixPointRot?Bt(n.odometryRotations,n.latestLoopClosureFixPointRot,Ht):0;if(r<0||s<0)throw Error(`Loop closure fix point could not be located in history`);if(r!==s)throw Error(`Loop closure fix point indices for position and rotation diverged`);let l=n.odometryPositions.slice(0,r).map(e=>R(e)),u=n.odometryRotations.slice(0,r).map(e=>H(e)),d=n.odometryPositions.slice(r).map(e=>R(e)),f=n.odometryRotations.slice(r).map(e=>H(e)),p=d.map((e,t)=>Lt(e,f[t])),m=Lt(U(t.payload.lastPos),H(W(t.payload.lastRot)));p.push(m);let h=zt(p,Lt(U(t.payload.newPos),H(W(t.payload.newRot)))),g=[],_=[],v=o.create();for(let e=0;e<h.length-1;e+=1){let t=h[e],n=o.create();i.getTranslation(n,t);let r=a.create();if(i.getRotation(r,t),a.normalize(r,r),i.getScaling(v,t),Math.hypot(v[0]-1,v[1]-1,v[2]-1)>.05)throw Error(`Unexpected scale drift encountered during loop closure correction`);g.push(F(n)),_.push(I(r))}e.gpsEvents=c(mt({odometryPositions:l.concat(g),odometryRotations:u.concat(_),gpsPoints:n.gpsPositions,odometryPosOffset:n.odometryPosOffset,odometryRotOffset:n.odometryRotOffset,latestLoopClosureFixPointPos:null,latestLoopClosureFixPointRot:null}))},add2dImage:(e,t)=>{if(!e)return e;e.odometryPath.points.push(c({imageFile:t.payload.imageFile,screenRotation:t.payload.screenRotation,position:U(t.payload.position),rotation:H(W(t.payload.rotation)),capturedAt:t.payload.capturedAt}))},markReferencePoint:(e,t)=>{if(!e)return e;let{id:n,position:r,rotation:i,rawGpsPoint:a,timestamp:o}=t.payload,s=Nt(a,e.zero);e.referencePoints.push(c({id:n,position:U(r),rotation:H(W(i)),gpsPoint:Q(s),timestamp:o??Date.now()}))}}}),{setZeroPos:Wt,recordGpsEvent:Gt,odometryTrackingRestarted:Kt,arLoopClosureDetected:qt,add2dImage:Jt,markReferencePoint:Yt}=Ut.actions,Xt=p(Wt),Zt=p(Gt),Qt=p(Kt),$t=p(qt),en=p(Jt),tn=p(Yt),nn=m(Ut.reducer),rn=t({name:`gpsElements`,initialState:j(),reducers:{addMarker:(e,t)=>{e.gpsMarkers.push(t.payload)},addLine:(e,t)=>{e.gpsLines.push(t.payload)},addArea:(e,t)=>{e.areas.push(t.payload)},addToHeatMaps:(e,t)=>{let{category:n,tiles:r}=t.payload,i=e.heatMap[n]??{};e.heatMap[n]={...i,...r}},addHeatMapArea:(e,t)=>{let n=t.payload,r=n.category;if(e.heatAreas[r]||(e.heatAreas[r]={}),e.heatAreas[r][n.geoHash])throw Error(`HeatArea collision detected for GeoHash: ${n.geoHash}`);e.heatAreas[r][n.geoHash]=n},resetGpsElements:()=>j()}}),{addMarker:an,addLine:on,addArea:sn,addToHeatMaps:cn,addHeatMapArea:ln,resetGpsElements:un}=rn.actions,dn=p(an),fn=p(on),pn=p(sn),mn=p(cn),hn=p(ln),gn=p(un),_n=m(rn.reducer),vn=()=>({gpsData:null,gpsElements:j(),arElements:M()}),yn=e=>{let t=Array.from({length:Math.min(e.length,4)},(t,n)=>e[n].toFixed(2));e.length>4&&t.push(`…`);let n=`[${t.join(`, `)}]`;if(e.length===4){let{pitch:t,yaw:r,roll:i}=v(e);return`${n} (pitch=${t.toFixed(0)}°, yaw=${r.toFixed(0)}°, roll=${i.toFixed(0)}°)`}return n},bn=e=>(e.length===3||e.length===4||e.length===16)&&e.every(e=>typeof e==`number`),$=(e,t=0)=>{if(t>10||e==null)return e;if(Array.isArray(e))return bn(e)?yn(e):e.map(e=>$(e,t+1));if(typeof e==`object`){let n={};for(let[r,i]of Object.entries(e))n[r]=$(i,t+1);return n}return e},xn=(t={})=>{let{preloadedState:n,enableDevToolsSanitizers:r=!0,enableDevChecks:i=!0,licenseKey:a}=t;if(a==null)throw Error(`License key required. Pass options.licenseKey or use createRecorderStore() from gps-plus-slam-app-framework. See EULA §3.`);g(a);let o=vn(),s=n?{gpsData:n.gpsData??o.gpsData,gpsElements:n.gpsElements??o.gpsElements,arElements:n.arElements??o.arElements}:void 0;return e({reducer:{gpsData:nn,gpsElements:_n,arElements:Je},preloadedState:s,middleware:e=>e({serializableCheck:i,immutableCheck:i}),devTools:r?{actionSanitizer:$,stateSanitizer:$}:!0})},Sn=[],Cn=[],wn=[],Tn=[],En={median:null,mean:null},Dn=f(function(e){let t=e.gpsData?.gpsEvents;return!t||t.gpsPositions.length===0?null:t.alignmentMatrix}),On=f(function(e){let t=e.gpsData?.gpsEvents;return!t||t.gpsPositions.length===0?null:t.alignmentRotation}),kn=f(function(e){return e.gpsData?.gpsEvents?.odometryPositions??Sn}),An=f(function(e){return e.gpsData?.gpsEvents?.odometryRotations??Cn}),jn=f(function(e){return e.gpsData?.gpsEvents?.gpsPositions??wn}),Mn=f(function(e){return e.gpsData?.referencePoints??Tn}),Nn=f(function(e){return e.gpsData?.zero??null}),Pn=f(function(e){let t=e.gpsData?.gpsEvents?.gpsAccuracyMedian??null,n=e.gpsData?.gpsEvents?.gpsAccuracyMean??null;return t===null&&n===null?En:{median:t,mean:n}}),Fn=`1.0.0`;export{V as IDENTITY_MATRIX4,z as IDENTITY_QUATERNION,Fn as LIB_VERSION,B as ZERO_VECTOR3,en as add2dImage,pn as addArea,Ue as addCornerToEventArea,hn as addHeatMapArea,fn as addLine,dn as addMarker,Ve as addOrUpdateArPlane,mn as addToHeatMaps,Ke as anchorEventAreaInWorldSpace,Je as arElementsReducer,$t as arLoopClosureDetected,ce as calcGeoHash,k as calcGpsCoords,oe as calcQuadKey,O as calcRelativeCoordsInMeters,be as cloneMatrix4,ye as cloneQuaternion,R as cloneVector3,We as correctArDriftForEventArea,xn as createGpsSlamStore,ue as distanceInMeters,de as distanceInMetersRelative,G as enuQuaternionToNUE,K as eulerToQuaternion,Ge as finishEventArea,ve as fromMatrix4,P as fromQuaternion,N as fromVector3,ge as fusedGpsFromOdom,le as geoHashToLatLong,Dn as getAlignmentMatrix,On as getAlignmentRotation,me as getGoogleMapsDirectionsLink,pe as getGoogleMapsLink,Pn as getGpsAccuracyStats,jn as getGpsPositions,kn as getOdometryPositions,An as getOdometryRotations,he as getOpenStreetMapLink,Mn as getReferencePoints,Nn as getZeroReference,nn as gpsDataReducer,_n as gpsElementsReducer,De as invertQuaternion,xe as isIdentityMatrix4,Ce as isIdentityQuaternion,we as isNearIdentityQuaternion,tn as markReferencePoint,Ee as multiplyQuaternions,H as normalizeQuaternion,Ae as nueQuaternionToENU,ke as nueQuaternionToWebXR,Oe as nueToWebXR,Qt as odometryTrackingRestarted,se as quadKeyToLatLong,Se as quaternionMagnitude,Te as quaternionsEquivalent,Zt as recordGpsEvent,Be as recordPhoneHeight,qe as resetArElements,gn as resetGpsElements,$ as sanitizeForDevTools,Xt as setZeroPos,He as startEventArea,fe as toEarthCenteredCoordinates,L as toMatrix4,I as toQuaternion,F as toVector3,g as validateLicenseKey,W as webxrQuaternionToNUE,U as webxrToNUE};
package/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "gps-plus-slam-js",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript library for real-time GPS + AR odometry alignment.",
5
+ "author": "cs-util-com",
6
+ "keywords": [
7
+ "gps",
8
+ "slam",
9
+ "ar",
10
+ "xr",
11
+ "webxr",
12
+ "odometry",
13
+ "alignment",
14
+ "geospatial",
15
+ "vio",
16
+ "visual-inertial-odometry",
17
+ "sensor-fusion"
18
+ ],
19
+ "type": "module",
20
+ "homepage": "https://github.com/cs-util-com/location-based-webxr#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/cs-util-com/location-based-webxr.git"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "EULA.md"
28
+ ],
29
+ "exports": {
30
+ ".": "./dist/index.js"
31
+ },
32
+ "types": "./dist/index.d.ts",
33
+ "engines": {
34
+ "node": ">=20.19.0"
35
+ },
36
+ "dependencies": {
37
+ "@noble/curves": "^2.2.0",
38
+ "@reduxjs/toolkit": "^2.11.2",
39
+ "gl-matrix": "3.4.4",
40
+ "immer": "^11.1.4",
41
+ "redux": "^5.0.1",
42
+ "redux-thunk": "^3.1.0"
43
+ },
44
+ "license": "UNLICENSED",
45
+ "devDependencies": {
46
+ "@eslint/js": "^10.0.1",
47
+ "@playwright/test": "^1.59.1",
48
+ "@stryker-mutator/core": "^9.6.1",
49
+ "@stryker-mutator/typescript-checker": "^9.6.1",
50
+ "@stryker-mutator/vitest-runner": "^9.6.1",
51
+ "@types/node": "^25.6.0",
52
+ "@vitest/coverage-v8": "^4.1.5",
53
+ "@vitest/eslint-plugin": "^1.6.16",
54
+ "dependency-cruiser": "^17.3.10",
55
+ "dpdm": "^4.0.1",
56
+ "eslint": "^10.2.1",
57
+ "eslint-config-prettier": "^10.1.8",
58
+ "fast-check": "^4.7.0",
59
+ "globals": "^17.5.0",
60
+ "jscpd": "^4.0.9",
61
+ "knip": "^6.6.0",
62
+ "prettier": "^3.8.3",
63
+ "serve": "^14.2.6",
64
+ "tailwindcss": "^4.2.4",
65
+ "tsdown": "^0.21.9",
66
+ "typescript": "^6.0.3",
67
+ "typescript-eslint": "^8.59.0",
68
+ "vitest": "^4.1.5"
69
+ },
70
+ "scripts": {
71
+ "generate-keys": "node scripts/generate-keys.mjs",
72
+ "test": "pnpm run test:core && pnpm run test:e2e:index && pnpm run test:guardrail",
73
+ "test:core": "pnpm run format && pnpm run lint && pnpm run check:all && pnpm run typecheck && pnpm run typecheck:tests && pnpm run test:unit",
74
+ "test:guardrail": "pnpm --filter gps-plus-slam-investigation run test:guardrail",
75
+ "format": "prettier --log-level warn --write --ignore-unknown --no-error-on-unmatched-pattern \"src\" \"pages\" \"config\" \"docs\" index.html README.md SPEC.md AGENTS.md package.json",
76
+ "format:check": "prettier --check --ignore-unknown .",
77
+ "build": "tsdown --config config/tsdown.config.ts",
78
+ "verify:pack": "pnpm pack --dry-run",
79
+ "verify:no-keys": "node scripts/verify-no-keys-in-tarball.mjs",
80
+ "verify:banner": "node scripts/verify-copyright-banner.mjs",
81
+ "verify:community-key-lifetime": "node scripts/verify-community-key-lifetime.mjs",
82
+ "test:unit": "vitest run --coverage --config config/vitest.config.ts",
83
+ "bench": "vitest bench --run --config config/vitest.bench.config.ts",
84
+ "bench:watch": "vitest bench --config config/vitest.bench.config.ts",
85
+ "typecheck:tests": "tsc -p tsconfig.vitest.json --noEmit",
86
+ "lint": "eslint . --config config/eslint.config.mjs",
87
+ "typecheck": "tsc -p tsconfig.json --noEmit",
88
+ "test:watch": "vitest --config config/vitest.config.ts",
89
+ "mutation": "stryker run mutation-testing/stryker.conf.json; node mutation-testing/mutation-report-to-md.js",
90
+ "check:dup": "jscpd --config config/.jscpd.json src",
91
+ "check:cycles": "dpdm -T --exit-code circular:1 --no-warning --no-tree ./src/index.ts",
92
+ "check:boundaries": "depcruise -c config/.dependency-cruiser.cjs src",
93
+ "check:deadcode": "knip --config config/knip.json",
94
+ "check:all": "pnpm run check:dup && pnpm run check:cycles && pnpm run check:boundaries && pnpm run check:deadcode",
95
+ "validate:all": "pnpm run test && pnpm run mutation",
96
+ "serve:static": "serve -l 4173 .",
97
+ "test:e2e": "pnpm exec playwright install && playwright test --config playwright-ui-tests/playwright.config.js",
98
+ "test:e2e:index": "pnpm exec playwright install chromium && playwright test --config playwright-ui-tests/playwright.config.js --project=chromium playwright-ui-tests/index.spec.js",
99
+ "test:e2e:artifacts": "pnpm exec playwright install && PLAYWRIGHT_CAPTURE=1 playwright test --config playwright-ui-tests/playwright.config.js",
100
+ "test:e2e:headed": "pnpm exec playwright install && playwright test --config playwright-ui-tests/playwright.config.js --headed",
101
+ "test:e2e:ui": "pnpm exec playwright install && playwright test --config playwright-ui-tests/playwright.config.js --ui",
102
+ "setup": "bash ./scripts/setup.sh"
103
+ }
104
+ }