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 +39 -0
- package/LICENSE +7 -0
- package/README.md +146 -0
- package/dist/index.d.ts +564 -0
- package/dist/index.js +2 -0
- package/package.json +104 -0
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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|