gps-plus-slam-app-framework 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/LICENSE +201 -0
- package/README.md +222 -0
- package/dist/alignment-lerper-D9BeDwZX.d.ts +29 -0
- package/dist/app-selectors-DyMzxCEC.d.ts +140 -0
- package/dist/ar/camera-blit-capture.d.ts +2 -0
- package/dist/ar/camera-blit-capture.js +253 -0
- package/dist/ar/capture-failure-tracker.d.ts +2 -0
- package/dist/ar/capture-failure-tracker.js +45 -0
- package/dist/ar/chromium-camera-access-workaround.d.ts +2 -0
- package/dist/ar/chromium-camera-access-workaround.js +32 -0
- package/dist/ar/depth-sampler.d.ts +3 -0
- package/dist/ar/depth-sampler.js +147 -0
- package/dist/ar/image-capture.d.ts +2 -0
- package/dist/ar/image-capture.js +152 -0
- package/dist/ar/index.d.ts +12 -0
- package/dist/ar/index.js +12 -0
- package/dist/ar/replay-scene.d.ts +2 -0
- package/dist/ar/replay-scene.js +325 -0
- package/dist/ar/scene-node-names.d.ts +2 -0
- package/dist/ar/scene-node-names.js +16 -0
- package/dist/ar/tracking-state.d.ts +2 -0
- package/dist/ar/tracking-state.js +164 -0
- package/dist/ar/webxr-session.d.ts +4 -0
- package/dist/ar/webxr-session.js +890 -0
- package/dist/ar/xr-camera-texture.d.ts +2 -0
- package/dist/ar/xr-camera-texture.js +39 -0
- package/dist/ar/xr-error-handler.d.ts +2 -0
- package/dist/ar/xr-error-handler.js +37 -0
- package/dist/ar-types-B-ORgk6Z.d.ts +82 -0
- package/dist/camera-blit-capture-DHuyrrxr.d.ts +108 -0
- package/dist/camera-follower-BMqPddxx.d.ts +29 -0
- package/dist/capture-failure-tracker-2Q6oI2uQ.d.ts +52 -0
- package/dist/chromium-camera-access-workaround-CU5zSKNr.d.ts +61 -0
- package/dist/community-license-key-ChZLCJyO.d.ts +21 -0
- package/dist/concurrency-p6gB30fT.d.ts +24 -0
- package/dist/css3d-renderer-manager-rgLMVAvd.d.ts +23 -0
- package/dist/depth-sampler-BNCtnEoP.d.ts +108 -0
- package/dist/failure-tracker-1sQ_Cgyw.d.ts +49 -0
- package/dist/file-system-BsQgYCU3.d.ts +128 -0
- package/dist/file-system-utils-5cWO8Xyv.d.ts +24 -0
- package/dist/format-file-size-onGq0k1Q.d.ts +24 -0
- package/dist/fused-path-DtqCSqJh.d.ts +25 -0
- package/dist/geo-types-Xx-LVcnw.d.ts +29 -0
- package/dist/gps-Ddzr2KlX.d.ts +54 -0
- package/dist/gps-compass-cubes-C-6JEnpR.d.ts +22 -0
- package/dist/gps-error-handler-VPuNOuf8.d.ts +40 -0
- package/dist/gps-event-markers-DpEy2qRd.d.ts +67 -0
- package/dist/h3-ref-point-CFhayNSZ.d.ts +59 -0
- package/dist/image-capture-2pKjYUS6.d.ts +130 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +61 -0
- package/dist/leaflet-map-overlay-CvfQXTye.d.ts +114 -0
- package/dist/lerp-utils-87NWjUOD.d.ts +20 -0
- package/dist/licensing/community-license-key.d.ts +2 -0
- package/dist/licensing/community-license-key.js +21 -0
- package/dist/licensing/index.d.ts +2 -0
- package/dist/licensing/index.js +2 -0
- package/dist/list-formatter-DKQQVLcl.d.ts +14 -0
- package/dist/logger-B81iwxx0.d.ts +74 -0
- package/dist/map-overlay-DuiZuCTx.d.ts +153 -0
- package/dist/null-storage-backend-CsWvQwN8.d.ts +11 -0
- package/dist/opfs-storage-ChOzF_Wp.d.ts +181 -0
- package/dist/opfs-storage-backend-CVcmfJHN.d.ts +11 -0
- package/dist/permission-checker-jO2ziCWC.d.ts +146 -0
- package/dist/persistence-middleware-DrptY02i.d.ts +31 -0
- package/dist/recorder-slice-fcwAXrsh.d.ts +35 -0
- package/dist/recording-coordinator-DzflP07_.d.ts +81 -0
- package/dist/recording-options-BYUFX8iU.d.ts +166 -0
- package/dist/recording-replayer-oOzPITrd.d.ts +32 -0
- package/dist/ref-point-importer-B_wn5ABk.d.ts +62 -0
- package/dist/ref-point-loader-Pt1smgHS.d.ts +117 -0
- package/dist/ref-points/h3-ref-point.d.ts +2 -0
- package/dist/ref-points/h3-ref-point.js +74 -0
- package/dist/ref-points/index.d.ts +2 -0
- package/dist/ref-points/index.js +2 -0
- package/dist/ref-points-slice-tC-Wd6XJ.d.ts +48 -0
- package/dist/reference-points-CFWy8_wj.d.ts +68 -0
- package/dist/replay-engine-DuX316ae.d.ts +103 -0
- package/dist/replay-scene-DtUm6bsr.d.ts +109 -0
- package/dist/routing-slice-BlHTM8eh.d.ts +23 -0
- package/dist/scene-node-names-DPsaE2z3.d.ts +14 -0
- package/dist/sensors/gps-error-handler.d.ts +2 -0
- package/dist/sensors/gps-error-handler.js +53 -0
- package/dist/sensors/gps.d.ts +2 -0
- package/dist/sensors/gps.js +93 -0
- package/dist/sensors/index.d.ts +4 -0
- package/dist/sensors/index.js +4 -0
- package/dist/sensors/permission-checker.d.ts +2 -0
- package/dist/sensors/permission-checker.js +397 -0
- package/dist/state/app-selectors.d.ts +2 -0
- package/dist/state/app-selectors.js +35 -0
- package/dist/state/index.d.ts +17 -0
- package/dist/state/index.js +13 -0
- package/dist/state/persistence-middleware.d.ts +2 -0
- package/dist/state/persistence-middleware.js +71 -0
- package/dist/state/recorder-slice.d.ts +2 -0
- package/dist/state/recorder-slice.js +47 -0
- package/dist/state/recording-coordinator.d.ts +2 -0
- package/dist/state/recording-coordinator.js +150 -0
- package/dist/state/recording-options.d.ts +2 -0
- package/dist/state/recording-options.js +192 -0
- package/dist/state/recording-replayer.d.ts +2 -0
- package/dist/state/recording-replayer.js +39 -0
- package/dist/state/ref-points-slice.d.ts +2 -0
- package/dist/state/ref-points-slice.js +44 -0
- package/dist/state/replay-engine.d.ts +2 -0
- package/dist/state/replay-engine.js +230 -0
- package/dist/state/routing-slice.d.ts +2 -0
- package/dist/state/routing-slice.js +13 -0
- package/dist/state/store-subscribers.d.ts +3 -0
- package/dist/state/store-subscribers.js +97 -0
- package/dist/state/store.d.ts +9 -0
- package/dist/state/store.js +71 -0
- package/dist/state/subscribe-to-selector.d.ts +2 -0
- package/dist/state/subscribe-to-selector.js +26 -0
- package/dist/storage/file-system-utils.d.ts +2 -0
- package/dist/storage/file-system-utils.js +31 -0
- package/dist/storage/file-system.d.ts +2 -0
- package/dist/storage/file-system.js +229 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +10 -0
- package/dist/storage/null-storage-backend.d.ts +2 -0
- package/dist/storage/null-storage-backend.js +8 -0
- package/dist/storage/opfs-storage-backend.d.ts +2 -0
- package/dist/storage/opfs-storage-backend.js +15 -0
- package/dist/storage/opfs-storage.d.ts +2 -0
- package/dist/storage/opfs-storage.js +242 -0
- package/dist/storage/ref-point-importer.d.ts +2 -0
- package/dist/storage/ref-point-importer.js +163 -0
- package/dist/storage/ref-point-loader.d.ts +2 -0
- package/dist/storage/ref-point-loader.js +248 -0
- package/dist/storage/ref-point-recovery.d.ts +28 -0
- package/dist/storage/ref-point-recovery.js +159 -0
- package/dist/storage/storage-backend.d.ts +2 -0
- package/dist/storage/storage-backend.js +1 -0
- package/dist/storage/zip-export.d.ts +2 -0
- package/dist/storage/zip-export.js +214 -0
- package/dist/storage/zip-reader.d.ts +2 -0
- package/dist/storage/zip-reader.js +190 -0
- package/dist/storage-backend-BcEItnFr.d.ts +20 -0
- package/dist/store-C7PKOj8S.d.ts +96 -0
- package/dist/store-subscribers-CuoIzB5T.d.ts +77 -0
- package/dist/subscribe-to-selector-UTZ0KkAc.d.ts +26 -0
- package/dist/test-utils/browser-mocks.d.ts +294 -0
- package/dist/test-utils/browser-mocks.js +568 -0
- package/dist/test-utils/zip-round-trip-helpers.d.ts +60 -0
- package/dist/test-utils/zip-round-trip-helpers.js +207 -0
- package/dist/three-dispose-CCBHjE_a.d.ts +30 -0
- package/dist/tracking-state-B9QfuZCg.d.ts +101 -0
- package/dist/types/ar-types.d.ts +2 -0
- package/dist/types/ar-types.js +1 -0
- package/dist/types/geo-types.d.ts +2 -0
- package/dist/types/geo-types.js +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/concurrency.d.ts +2 -0
- package/dist/utils/concurrency.js +40 -0
- package/dist/utils/failure-tracker.d.ts +2 -0
- package/dist/utils/failure-tracker.js +48 -0
- package/dist/utils/format-file-size.d.ts +2 -0
- package/dist/utils/format-file-size.js +41 -0
- package/dist/utils/fused-path.d.ts +2 -0
- package/dist/utils/fused-path.js +44 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/list-formatter.d.ts +2 -0
- package/dist/utils/list-formatter.js +17 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +226 -0
- package/dist/vis-colors-Qi2vlRGt.d.ts +63 -0
- package/dist/visualization/alignment-lerper.d.ts +2 -0
- package/dist/visualization/alignment-lerper.js +68 -0
- package/dist/visualization/camera-follower.d.ts +2 -0
- package/dist/visualization/camera-follower.js +51 -0
- package/dist/visualization/css3d-renderer-manager.d.ts +2 -0
- package/dist/visualization/css3d-renderer-manager.js +41 -0
- package/dist/visualization/gps-compass-cubes.d.ts +2 -0
- package/dist/visualization/gps-compass-cubes.js +129 -0
- package/dist/visualization/gps-event-markers.d.ts +2 -0
- package/dist/visualization/gps-event-markers.js +159 -0
- package/dist/visualization/index.d.ts +12 -0
- package/dist/visualization/index.js +12 -0
- package/dist/visualization/leaflet-map-overlay.d.ts +2 -0
- package/dist/visualization/leaflet-map-overlay.js +313 -0
- package/dist/visualization/lerp-utils.d.ts +2 -0
- package/dist/visualization/lerp-utils.js +22 -0
- package/dist/visualization/map-overlay.d.ts +2 -0
- package/dist/visualization/map-overlay.js +241 -0
- package/dist/visualization/reference-points.d.ts +2 -0
- package/dist/visualization/reference-points.js +160 -0
- package/dist/visualization/three-dispose.d.ts +2 -0
- package/dist/visualization/three-dispose.js +55 -0
- package/dist/visualization/vis-colors.d.ts +2 -0
- package/dist/visualization/vis-colors.js +74 -0
- package/dist/webxr-session-COlSrXdL.d.ts +316 -0
- package/dist/xr-camera-texture-BbukNE79.d.ts +53 -0
- package/dist/xr-error-handler-DTHfmvrl.d.ts +24 -0
- package/dist/zip-export-Rtpi75JJ.d.ts +87 -0
- package/dist/zip-reader-D7pkpX8K.d.ts +97 -0
- package/package.json +104 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { h3RefsMatch, isH3Index } from "../ref-points/h3-ref-point.js";
|
|
3
|
+
import { BlobReader, TextWriter, ZipReader } from "@zip.js/zip.js";
|
|
4
|
+
//#region ../src/storage/ref-point-importer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reference Point Importer Module
|
|
7
|
+
*
|
|
8
|
+
* Extracts reference points from ZIP files in a folder, enabling reuse of
|
|
9
|
+
* ref points from previous recording sessions.
|
|
10
|
+
*
|
|
11
|
+
* This module:
|
|
12
|
+
* 1. Enumerates all *.zip files in a folder
|
|
13
|
+
* 2. Opens each ZIP and looks for refPoints/*.json files
|
|
14
|
+
* 3. Parses and validates each ref point definition
|
|
15
|
+
* 4. Merges and deduplicates ref points by ID
|
|
16
|
+
*
|
|
17
|
+
* Uses @zip.js/zip.js for ZIP reading (same library as zip-export.ts).
|
|
18
|
+
*/
|
|
19
|
+
const log = createLogger("RefPointImporter");
|
|
20
|
+
/**
|
|
21
|
+
* Check if a file name is a ZIP file (case-insensitive).
|
|
22
|
+
*/
|
|
23
|
+
function isZipFileName(name) {
|
|
24
|
+
return name.toLowerCase().endsWith(".zip");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a ZIP entry path is a ref point JSON file.
|
|
28
|
+
* Expected path: refPoints/{id}.json
|
|
29
|
+
*/
|
|
30
|
+
function isRefPointEntry(entryPath) {
|
|
31
|
+
return entryPath.startsWith("refPoints/") && entryPath.endsWith(".json") && entryPath !== "refPoints/";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Type guard to validate parsed JSON matches RefPointDefinition shape.
|
|
35
|
+
* Validates at least one observation exists with required nested properties.
|
|
36
|
+
*/
|
|
37
|
+
function isValidRefPointDefinition(value) {
|
|
38
|
+
if (typeof value !== "object" || value === null) return false;
|
|
39
|
+
const obj = value;
|
|
40
|
+
if (typeof obj.id !== "string" || typeof obj.name !== "string" || typeof obj.createdAt !== "number" || !Array.isArray(obj.observations)) return false;
|
|
41
|
+
if (obj.observations.length === 0) return true;
|
|
42
|
+
const firstObs = obj.observations[0];
|
|
43
|
+
if (typeof firstObs !== "object" || firstObs === null) return false;
|
|
44
|
+
const gpsPoint = firstObs.gpsPoint;
|
|
45
|
+
if (typeof gpsPoint !== "object" || gpsPoint === null || typeof gpsPoint.latitude !== "number" || typeof gpsPoint.longitude !== "number") return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert a RefPointDefinition to an ImportedRefPoint.
|
|
50
|
+
* Extracts GPS coordinates from the first observation.
|
|
51
|
+
*/
|
|
52
|
+
function toImportedRefPoint(def, sourceZipName) {
|
|
53
|
+
if (def.observations.length === 0) return null;
|
|
54
|
+
const firstObs = def.observations[0];
|
|
55
|
+
return {
|
|
56
|
+
id: def.id,
|
|
57
|
+
name: def.name,
|
|
58
|
+
lat: firstObs.gpsPoint.latitude,
|
|
59
|
+
lon: firstObs.gpsPoint.longitude,
|
|
60
|
+
alt: firstObs.gpsPoint.altitude,
|
|
61
|
+
sourceZipName
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract reference points from a single ZIP file.
|
|
66
|
+
*
|
|
67
|
+
* @param zipBlob - The ZIP file as a Blob
|
|
68
|
+
* @param zipFileName - Name of the ZIP file (for error messages)
|
|
69
|
+
* @returns Array of imported ref points and any errors
|
|
70
|
+
*/
|
|
71
|
+
async function extractRefPointsFromZip(zipBlob, zipFileName) {
|
|
72
|
+
const refPoints = [];
|
|
73
|
+
const errors = [];
|
|
74
|
+
const zipReader = new ZipReader(new BlobReader(zipBlob));
|
|
75
|
+
try {
|
|
76
|
+
const entries = await zipReader.getEntries();
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (entry.directory) continue;
|
|
79
|
+
if (!isRefPointEntry(entry.filename)) continue;
|
|
80
|
+
try {
|
|
81
|
+
const textWriter = new TextWriter();
|
|
82
|
+
const jsonText = await entry.getData(textWriter);
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(jsonText);
|
|
86
|
+
} catch (parseErr) {
|
|
87
|
+
errors.push(`${zipFileName}/${entry.filename}: Invalid JSON - ${parseErr.message}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!isValidRefPointDefinition(parsed)) {
|
|
91
|
+
errors.push(`${zipFileName}/${entry.filename}: Invalid ref point schema`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const imported = toImportedRefPoint(parsed, zipFileName);
|
|
95
|
+
if (imported) refPoints.push(imported);
|
|
96
|
+
} catch (entryErr) {
|
|
97
|
+
errors.push(`${zipFileName}/${entry.filename}: ${entryErr.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
await zipReader.close();
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
refPoints,
|
|
105
|
+
errors
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Import reference points from all ZIP files in a folder.
|
|
110
|
+
*
|
|
111
|
+
* Enumerates all *.zip files in the folder, extracts refPoints/*.json from each,
|
|
112
|
+
* and merges them into a deduplicated list.
|
|
113
|
+
*
|
|
114
|
+
* Deduplication: If the same ref point ID appears in multiple ZIPs, the first
|
|
115
|
+
* occurrence is kept (based on iteration order).
|
|
116
|
+
*
|
|
117
|
+
* @param folderHandle - Read-only directory handle from showDirectoryPicker
|
|
118
|
+
* @returns Result containing merged ref points, scan count, and errors
|
|
119
|
+
*/
|
|
120
|
+
async function importRefPointsFromFolder(folderHandle) {
|
|
121
|
+
const allRefPoints = [];
|
|
122
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
123
|
+
const allErrors = [];
|
|
124
|
+
let zipFilesScanned = 0;
|
|
125
|
+
log.info(`Scanning folder: ${folderHandle.name}`);
|
|
126
|
+
try {
|
|
127
|
+
for await (const entry of folderHandle.values()) {
|
|
128
|
+
if (entry.kind !== "file" || !isZipFileName(entry.name)) continue;
|
|
129
|
+
log.debug(`Processing ZIP: ${entry.name}`);
|
|
130
|
+
try {
|
|
131
|
+
const { refPoints, errors } = await extractRefPointsFromZip(await (await folderHandle.getFileHandle(entry.name)).getFile(), entry.name);
|
|
132
|
+
zipFilesScanned++;
|
|
133
|
+
allErrors.push(...errors);
|
|
134
|
+
for (const refPoint of refPoints) if (!(isH3Index(refPoint.id) ? allRefPoints.some((existing) => isH3Index(existing.id) && h3RefsMatch(existing.id, refPoint.id)) : seenIds.has(refPoint.id))) {
|
|
135
|
+
seenIds.add(refPoint.id);
|
|
136
|
+
allRefPoints.push(refPoint);
|
|
137
|
+
} else log.debug(`Skipping duplicate ref point: ${refPoint.id} from ${entry.name}`);
|
|
138
|
+
} catch (zipErr) {
|
|
139
|
+
const errorMsg = `Failed to process ${entry.name}: ${zipErr.message}`;
|
|
140
|
+
log.warn(errorMsg);
|
|
141
|
+
allErrors.push(errorMsg);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
log.info(`Imported ${allRefPoints.length} ref points from ${zipFilesScanned} ZIP files`);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
refPoints: allRefPoints,
|
|
148
|
+
zipFilesScanned,
|
|
149
|
+
errors: allErrors
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const errorMsg = `Failed to scan folder: ${err.message}`;
|
|
153
|
+
log.error(errorMsg);
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
refPoints: allRefPoints,
|
|
157
|
+
zipFilesScanned,
|
|
158
|
+
errors: [...allErrors, errorMsg]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { importRefPointsFromFolder };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as flattenRefPointsToMarks, c as loadRefPoint, i as averageGpsPerRefPoint, l as saveRefPointObservation, n as RefPointMark, o as listRefPointIds, r as RefPointObservation, s as loadAllRefPoints, t as RefPointDefinition, u as writeRefPointDefinition } from "../ref-point-loader-Pt1smgHS.js";
|
|
2
|
+
export { RefPointDefinition, RefPointMark, RefPointObservation, averageGpsPerRefPoint, flattenRefPointsToMarks, listRefPointIds, loadAllRefPoints, loadRefPoint, saveRefPointObservation, writeRefPointDefinition };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
//#region ../src/storage/ref-point-loader.ts
|
|
3
|
+
const log = createLogger("RefPointLoader");
|
|
4
|
+
/**
|
|
5
|
+
* Check if arPose has required position and rotation arrays.
|
|
6
|
+
*/
|
|
7
|
+
function hasValidArPose(o) {
|
|
8
|
+
if (typeof o.arPose !== "object" || o.arPose === null) return false;
|
|
9
|
+
const arPose = o.arPose;
|
|
10
|
+
return Array.isArray(arPose.position) && Array.isArray(arPose.rotation);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if gpsPoint has required lat/lon numbers.
|
|
14
|
+
*/
|
|
15
|
+
function hasValidGpsPoint(o) {
|
|
16
|
+
if (typeof o.gpsPoint !== "object" || o.gpsPoint === null) return false;
|
|
17
|
+
const gpsPoint = o.gpsPoint;
|
|
18
|
+
return typeof gpsPoint.latitude === "number" && typeof gpsPoint.longitude === "number";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to validate a single observation has required nested properties.
|
|
22
|
+
* Prevents runtime errors from accessing arPose.position or gpsPoint.latitude on malformed data.
|
|
23
|
+
*/
|
|
24
|
+
function isValidObservation(obs) {
|
|
25
|
+
if (typeof obs !== "object" || obs === null) return false;
|
|
26
|
+
const o = obs;
|
|
27
|
+
return hasValidArPose(o) && hasValidGpsPoint(o);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to validate parsed JSON matches RefPointDefinition shape.
|
|
31
|
+
* Prevents runtime errors from malformed or corrupted JSON files.
|
|
32
|
+
* Also validates each observation to ensure nested properties exist.
|
|
33
|
+
*/
|
|
34
|
+
function isRefPointDefinition(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null) return false;
|
|
36
|
+
const obj = value;
|
|
37
|
+
if (typeof obj.id !== "string" || typeof obj.name !== "string" || typeof obj.createdAt !== "number" || !Array.isArray(obj.observations)) return false;
|
|
38
|
+
return obj.observations.every(isValidObservation);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a single reference point file and validate its schema.
|
|
42
|
+
* Returns null if parsing fails or schema is invalid.
|
|
43
|
+
*/
|
|
44
|
+
async function parseRefPointFile(fileHandle, fileName) {
|
|
45
|
+
try {
|
|
46
|
+
const text = await (await fileHandle.getFile()).text();
|
|
47
|
+
const parsed = JSON.parse(text);
|
|
48
|
+
if (isRefPointDefinition(parsed)) return parsed;
|
|
49
|
+
log.warn(`Invalid schema in ${fileName}`);
|
|
50
|
+
return null;
|
|
51
|
+
} catch (parseErr) {
|
|
52
|
+
log.error(`Failed to parse ${fileName}:`, parseErr);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load all reference point definitions from the scenario's refPoints/ directory.
|
|
58
|
+
*
|
|
59
|
+
* @param scenarioHandle - File system handle for the scenario folder
|
|
60
|
+
* @returns Array of all reference point definitions
|
|
61
|
+
*/
|
|
62
|
+
async function loadAllRefPoints(scenarioHandle) {
|
|
63
|
+
try {
|
|
64
|
+
const refPointsHandle = await scenarioHandle.getDirectoryHandle("refPoints");
|
|
65
|
+
const refPoints = [];
|
|
66
|
+
for await (const [name, handle] of refPointsHandle.entries()) {
|
|
67
|
+
if (handle.kind !== "file" || !name.endsWith(".json")) continue;
|
|
68
|
+
const parsed = await parseRefPointFile(handle, name);
|
|
69
|
+
if (parsed) refPoints.push(parsed);
|
|
70
|
+
}
|
|
71
|
+
return refPoints;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
log.debug("No refPoints directory found (yet):", err);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load a specific reference point by ID.
|
|
79
|
+
*
|
|
80
|
+
* @param scenarioHandle - File system handle for the scenario folder
|
|
81
|
+
* @param pointId - Reference point ID
|
|
82
|
+
* @returns Reference point definition or null if not found
|
|
83
|
+
*/
|
|
84
|
+
async function loadRefPoint(scenarioHandle, pointId) {
|
|
85
|
+
try {
|
|
86
|
+
const text = await (await (await (await scenarioHandle.getDirectoryHandle("refPoints")).getFileHandle(`${pointId}.json`)).getFile()).text();
|
|
87
|
+
const parsed = JSON.parse(text);
|
|
88
|
+
if (!isRefPointDefinition(parsed)) {
|
|
89
|
+
log.warn(`Invalid schema for ${pointId}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
log.debug(`Reference point ${pointId} not found:`, err);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Save or update a reference point observation.
|
|
100
|
+
* Creates the refPoints/ directory if it doesn't exist.
|
|
101
|
+
* If the reference point exists, appends the new observation.
|
|
102
|
+
* If it doesn't exist, creates a new reference point file.
|
|
103
|
+
*
|
|
104
|
+
* @param scenarioHandle - File system handle for the scenario folder
|
|
105
|
+
* @param pointId - Reference point ID
|
|
106
|
+
* @param pointName - Human-readable name for the reference point
|
|
107
|
+
* @param observation - New observation to add
|
|
108
|
+
*/
|
|
109
|
+
async function saveRefPointObservation(scenarioHandle, pointId, pointName, observation) {
|
|
110
|
+
try {
|
|
111
|
+
const refPointsHandle = await scenarioHandle.getDirectoryHandle("refPoints", { create: true });
|
|
112
|
+
const existing = await loadRefPoint(scenarioHandle, pointId);
|
|
113
|
+
const definition = existing ? {
|
|
114
|
+
...existing,
|
|
115
|
+
observations: [...existing.observations, observation]
|
|
116
|
+
} : {
|
|
117
|
+
id: pointId,
|
|
118
|
+
name: pointName,
|
|
119
|
+
createdAt: observation.timestamp,
|
|
120
|
+
observations: [observation]
|
|
121
|
+
};
|
|
122
|
+
const writable = await (await refPointsHandle.getFileHandle(`${pointId}.json`, { create: true })).createWritable();
|
|
123
|
+
let writeError = null;
|
|
124
|
+
try {
|
|
125
|
+
await writable.write(JSON.stringify(definition, null, 2));
|
|
126
|
+
await writable.close();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
writeError = error;
|
|
129
|
+
} finally {
|
|
130
|
+
if (writeError !== null) try {
|
|
131
|
+
await writable.abort();
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
if (writeError !== null) {
|
|
135
|
+
if (writeError instanceof Error) throw writeError;
|
|
136
|
+
throw new Error("OPFS write failed");
|
|
137
|
+
}
|
|
138
|
+
log.info(`Saved observation for ${pointId} (${definition.observations.length} total observations)`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log.error(`Failed to save reference point ${pointId}:`, err);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Write a complete RefPointDefinition to OPFS.
|
|
146
|
+
* Creates the refPoints/ directory if it doesn't exist.
|
|
147
|
+
* Used by the recovery flow to restore definitions extracted from ZIPs.
|
|
148
|
+
*
|
|
149
|
+
* @param scenarioHandle - File system handle for the scenario folder
|
|
150
|
+
* @param definition - Complete ref point definition to write
|
|
151
|
+
*/
|
|
152
|
+
async function writeRefPointDefinition(scenarioHandle, definition) {
|
|
153
|
+
const writable = await (await (await scenarioHandle.getDirectoryHandle("refPoints", { create: true })).getFileHandle(`${definition.id}.json`, { create: true })).createWritable();
|
|
154
|
+
let writeError = null;
|
|
155
|
+
try {
|
|
156
|
+
await writable.write(JSON.stringify(definition, null, 2));
|
|
157
|
+
await writable.close();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
writeError = error;
|
|
160
|
+
} finally {
|
|
161
|
+
if (writeError !== null) try {
|
|
162
|
+
await writable.abort();
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
if (writeError !== null) {
|
|
166
|
+
if (writeError instanceof Error) throw writeError;
|
|
167
|
+
throw new Error("OPFS write failed");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get list of all reference point IDs in the scenario.
|
|
172
|
+
* Useful for autocomplete/suggestions.
|
|
173
|
+
*
|
|
174
|
+
* @param scenarioHandle - File system handle for the scenario folder
|
|
175
|
+
* @returns Array of reference point IDs
|
|
176
|
+
*/
|
|
177
|
+
async function listRefPointIds(scenarioHandle) {
|
|
178
|
+
try {
|
|
179
|
+
const refPointsHandle = await scenarioHandle.getDirectoryHandle("refPoints");
|
|
180
|
+
const ids = [];
|
|
181
|
+
for await (const [name, handle] of refPointsHandle.entries()) if (handle.kind === "file" && name.endsWith(".json")) ids.push(name.slice(0, -5));
|
|
182
|
+
return ids.sort();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
log.debug("No refPoints directory found (yet):", err);
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Flatten an array of RefPointDefinition objects into individual RefPointMark
|
|
190
|
+
* observations suitable for visualization.
|
|
191
|
+
*
|
|
192
|
+
* @param refPointDefs - Array of reference point definitions with observations
|
|
193
|
+
* @returns Flat array of individual marks (one per observation)
|
|
194
|
+
*/
|
|
195
|
+
function flattenRefPointsToMarks(refPointDefs) {
|
|
196
|
+
return refPointDefs.flatMap((def) => def.observations.map((obs) => {
|
|
197
|
+
const src = obs.fusedGpsPoint ?? obs.gpsPoint;
|
|
198
|
+
const gpsPosition = {
|
|
199
|
+
lat: src.latitude,
|
|
200
|
+
lon: src.longitude,
|
|
201
|
+
altitude: src.altitude
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
id: def.id,
|
|
205
|
+
odomPosition: obs.arPose.position,
|
|
206
|
+
odomRotation: obs.arPose.rotation,
|
|
207
|
+
gpsPosition,
|
|
208
|
+
timestamp: obs.timestamp
|
|
209
|
+
};
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Compute one averaged GPS position per reference point ID.
|
|
214
|
+
* For each observation, prefers `fusedGpsPoint` (sub-meter accuracy) when
|
|
215
|
+
* available, falling back to raw `gpsPoint`. Returns the centroid (mean
|
|
216
|
+
* lat/lon) across all observations.
|
|
217
|
+
*/
|
|
218
|
+
function averageGpsPerRefPoint(refPointDefs) {
|
|
219
|
+
return refPointDefs.filter((def) => def.observations.length > 0).map((def) => {
|
|
220
|
+
const coords = def.observations.map((obs) => {
|
|
221
|
+
if (obs.fusedGpsPoint) return {
|
|
222
|
+
lat: obs.fusedGpsPoint.latitude,
|
|
223
|
+
lon: obs.fusedGpsPoint.longitude,
|
|
224
|
+
alt: obs.fusedGpsPoint.altitude
|
|
225
|
+
};
|
|
226
|
+
if (typeof obs.gpsPoint.latitude === "number" && typeof obs.gpsPoint.longitude === "number") return {
|
|
227
|
+
lat: obs.gpsPoint.latitude,
|
|
228
|
+
lon: obs.gpsPoint.longitude,
|
|
229
|
+
alt: obs.gpsPoint.altitude
|
|
230
|
+
};
|
|
231
|
+
return null;
|
|
232
|
+
}).filter((c) => c !== null);
|
|
233
|
+
if (coords.length === 0) return null;
|
|
234
|
+
const avgLat = coords.reduce((s, c) => s + c.lat, 0) / coords.length;
|
|
235
|
+
const avgLon = coords.reduce((s, c) => s + c.lon, 0) / coords.length;
|
|
236
|
+
const alts = coords.filter((c) => c.alt != null);
|
|
237
|
+
const avgAlt = alts.length > 0 ? alts.reduce((s, c) => s + c.alt, 0) / alts.length : void 0;
|
|
238
|
+
return {
|
|
239
|
+
id: def.id,
|
|
240
|
+
name: def.name,
|
|
241
|
+
lat: avgLat,
|
|
242
|
+
lon: avgLon,
|
|
243
|
+
alt: avgAlt
|
|
244
|
+
};
|
|
245
|
+
}).filter((r) => r !== null);
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { averageGpsPerRefPoint, flattenRefPointsToMarks, listRefPointIds, loadAllRefPoints, loadRefPoint, saveRefPointObservation, writeRefPointDefinition };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as RefPointDefinition } from "../ref-point-loader-Pt1smgHS.js";
|
|
2
|
+
|
|
3
|
+
//#region ../src/storage/ref-point-recovery.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Result of recovering ref point definitions from ZIP files.
|
|
6
|
+
*/
|
|
7
|
+
interface RefPointRecoveryResult {
|
|
8
|
+
/** Merged, deduplicated RefPointDefinition objects from all ZIPs */
|
|
9
|
+
readonly definitions: RefPointDefinition[];
|
|
10
|
+
/** Number of ZIP files successfully scanned */
|
|
11
|
+
readonly zipFilesScanned: number;
|
|
12
|
+
/** Error messages from failed ZIPs or malformed ref points */
|
|
13
|
+
readonly errors: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract full RefPointDefinition objects from all ZIPs in a folder,
|
|
17
|
+
* merge observations by ref point ID, and return the merged definitions.
|
|
18
|
+
*
|
|
19
|
+
* Unlike importRefPointsFromFolder() which returns simplified ImportedRefPoint[],
|
|
20
|
+
* this preserves full observation data (AR poses, GPS, timestamps) needed for
|
|
21
|
+
* 3D display and OPFS recovery.
|
|
22
|
+
*
|
|
23
|
+
* @param folderHandle - Read-only directory handle from showDirectoryPicker
|
|
24
|
+
* @returns Result containing merged definitions, scan count, and errors
|
|
25
|
+
*/
|
|
26
|
+
declare function recoverRefPointDefinitionsFromZips(folderHandle: FileSystemDirectoryHandle): Promise<RefPointRecoveryResult>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { RefPointRecoveryResult, recoverRefPointDefinitionsFromZips };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { BlobReader, TextWriter, ZipReader } from "@zip.js/zip.js";
|
|
3
|
+
//#region ../src/storage/ref-point-recovery.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reference Point Recovery Module
|
|
6
|
+
*
|
|
7
|
+
* Extracts full RefPointDefinition objects from ZIP files in a folder and
|
|
8
|
+
* merges observations by ref point ID. Unlike ref-point-importer.ts (which
|
|
9
|
+
* returns simplified ImportedRefPoint with only lat/lon), this module
|
|
10
|
+
* preserves complete observation data (AR poses, GPS, timestamps) needed
|
|
11
|
+
* for 3D display and OPFS restoration after browser data loss.
|
|
12
|
+
*
|
|
13
|
+
* Used by the recovery flow: when OPFS is empty after browser data clear,
|
|
14
|
+
* this module reconstructs the full scenario-level ref point state from
|
|
15
|
+
* session ZIPs so the user can continue recording with prior ref points visible.
|
|
16
|
+
*
|
|
17
|
+
* Uses @zip.js/zip.js for ZIP reading (same library as zip-export.ts
|
|
18
|
+
* and ref-point-importer.ts).
|
|
19
|
+
*/
|
|
20
|
+
const log = createLogger("RefPointRecovery");
|
|
21
|
+
function isZipFileName(name) {
|
|
22
|
+
return name.toLowerCase().endsWith(".zip");
|
|
23
|
+
}
|
|
24
|
+
function isRefPointEntry(entryPath) {
|
|
25
|
+
return entryPath.startsWith("refPoints/") && entryPath.endsWith(".json") && entryPath !== "refPoints/";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate parsed JSON matches RefPointDefinition shape.
|
|
29
|
+
* Looser than ref-point-loader's validator: accepts empty observations
|
|
30
|
+
* (schema-valid, preserves identity) and doesn't require arPose/gpsPoint
|
|
31
|
+
* validation on every observation (the importer's validator already checks
|
|
32
|
+
* first obs structure).
|
|
33
|
+
*/
|
|
34
|
+
function isValidRefPointDefinition(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null) return false;
|
|
36
|
+
const obj = value;
|
|
37
|
+
if (typeof obj.id !== "string" || typeof obj.name !== "string" || typeof obj.createdAt !== "number" || !Array.isArray(obj.observations)) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract full RefPointDefinition objects from a single ZIP file.
|
|
42
|
+
*/
|
|
43
|
+
async function extractDefinitionsFromZip(zipBlob, zipFileName) {
|
|
44
|
+
const definitions = [];
|
|
45
|
+
const errors = [];
|
|
46
|
+
const zipReader = new ZipReader(new BlobReader(zipBlob));
|
|
47
|
+
try {
|
|
48
|
+
const entries = await zipReader.getEntries();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.directory) continue;
|
|
51
|
+
if (!isRefPointEntry(entry.filename)) continue;
|
|
52
|
+
try {
|
|
53
|
+
const textWriter = new TextWriter();
|
|
54
|
+
const jsonText = await entry.getData(textWriter);
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(jsonText);
|
|
58
|
+
} catch (parseErr) {
|
|
59
|
+
errors.push(`${zipFileName}/${entry.filename}: Invalid JSON - ${parseErr.message}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isValidRefPointDefinition(parsed)) {
|
|
63
|
+
errors.push(`${zipFileName}/${entry.filename}: Invalid ref point schema`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
definitions.push(parsed);
|
|
67
|
+
} catch (entryErr) {
|
|
68
|
+
errors.push(`${zipFileName}/${entry.filename}: ${entryErr.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
await zipReader.close();
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
definitions,
|
|
76
|
+
errors
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Merge observations from multiple RefPointDefinitions with the same ID.
|
|
81
|
+
* Deduplicates observations by sessionId + timestamp.
|
|
82
|
+
* Uses earliest createdAt and first-encountered name.
|
|
83
|
+
*/
|
|
84
|
+
function mergeDefinitions(allDefs) {
|
|
85
|
+
const byId = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const def of allDefs) {
|
|
87
|
+
let entry = byId.get(def.id);
|
|
88
|
+
if (!entry) {
|
|
89
|
+
entry = {
|
|
90
|
+
def: {
|
|
91
|
+
id: def.id,
|
|
92
|
+
name: def.name,
|
|
93
|
+
createdAt: def.createdAt,
|
|
94
|
+
observations: []
|
|
95
|
+
},
|
|
96
|
+
seen: /* @__PURE__ */ new Set()
|
|
97
|
+
};
|
|
98
|
+
byId.set(def.id, entry);
|
|
99
|
+
} else if (def.createdAt < entry.def.createdAt) entry.def.createdAt = def.createdAt;
|
|
100
|
+
for (const obs of def.observations) {
|
|
101
|
+
const key = `${obs.sessionId}:${obs.timestamp}`;
|
|
102
|
+
if (!entry.seen.has(key)) {
|
|
103
|
+
entry.seen.add(key);
|
|
104
|
+
entry.def.observations.push(obs);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Array.from(byId.values()).map((e) => e.def).sort((a, b) => a.createdAt - b.createdAt);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Extract full RefPointDefinition objects from all ZIPs in a folder,
|
|
112
|
+
* merge observations by ref point ID, and return the merged definitions.
|
|
113
|
+
*
|
|
114
|
+
* Unlike importRefPointsFromFolder() which returns simplified ImportedRefPoint[],
|
|
115
|
+
* this preserves full observation data (AR poses, GPS, timestamps) needed for
|
|
116
|
+
* 3D display and OPFS recovery.
|
|
117
|
+
*
|
|
118
|
+
* @param folderHandle - Read-only directory handle from showDirectoryPicker
|
|
119
|
+
* @returns Result containing merged definitions, scan count, and errors
|
|
120
|
+
*/
|
|
121
|
+
async function recoverRefPointDefinitionsFromZips(folderHandle) {
|
|
122
|
+
const allDefinitions = [];
|
|
123
|
+
const allErrors = [];
|
|
124
|
+
let zipFilesScanned = 0;
|
|
125
|
+
log.info(`Recovery scan: ${folderHandle.name}`);
|
|
126
|
+
try {
|
|
127
|
+
for await (const entry of folderHandle.values()) {
|
|
128
|
+
if (entry.kind !== "file" || !isZipFileName(entry.name)) continue;
|
|
129
|
+
log.debug(`Processing ZIP: ${entry.name}`);
|
|
130
|
+
try {
|
|
131
|
+
const { definitions, errors } = await extractDefinitionsFromZip(await entry.getFile(), entry.name);
|
|
132
|
+
zipFilesScanned++;
|
|
133
|
+
allErrors.push(...errors);
|
|
134
|
+
allDefinitions.push(...definitions);
|
|
135
|
+
} catch (zipErr) {
|
|
136
|
+
const errorMsg = `Failed to process ${entry.name}: ${zipErr.message}`;
|
|
137
|
+
log.warn(errorMsg);
|
|
138
|
+
allErrors.push(errorMsg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const merged = mergeDefinitions(allDefinitions);
|
|
142
|
+
log.info(`Recovered ${merged.length} ref points from ${zipFilesScanned} ZIP files`);
|
|
143
|
+
return {
|
|
144
|
+
definitions: merged,
|
|
145
|
+
zipFilesScanned,
|
|
146
|
+
errors: allErrors
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const errorMsg = `Failed to scan folder: ${err.message}`;
|
|
150
|
+
log.error(errorMsg);
|
|
151
|
+
return {
|
|
152
|
+
definitions: mergeDefinitions(allDefinitions),
|
|
153
|
+
zipFilesScanned,
|
|
154
|
+
errors: [...allErrors, errorMsg]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { recoverRefPointDefinitionsFromZips };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|