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,229 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { setFileSystemState } from "../sensors/permission-checker.js";
|
|
3
|
+
import { createSession, getScenarioHandle, getScenariosRootHandle, initOpfsStorage, listScenarios, resetOpfsStorage, resetSessionHandles, writeAction as writeAction$1, writeFrame as writeFrame$1, writeSessionMetadata as writeSessionMetadata$1 } from "./opfs-storage.js";
|
|
4
|
+
//#region ../src/storage/file-system.ts
|
|
5
|
+
const log = createLogger("Storage");
|
|
6
|
+
let storageInitialized = false;
|
|
7
|
+
/**
|
|
8
|
+
* Reset module state - exported for testing only
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function resetStorageState() {
|
|
12
|
+
storageInitialized = false;
|
|
13
|
+
resetOpfsStorage();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reset session-level state for a new recording.
|
|
17
|
+
*
|
|
18
|
+
* Clears scenario/session names and OPFS session handles, but preserves
|
|
19
|
+
* `storageInitialized` and the OPFS root so `initStorage()` doesn't need
|
|
20
|
+
* to be called again.
|
|
21
|
+
*
|
|
22
|
+
* Used during soft reset for new recordings (Issue 4, 2026-02-06 user feedback).
|
|
23
|
+
*/
|
|
24
|
+
function resetForNewSession() {
|
|
25
|
+
resetSessionHandles();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify that we have actual write access to a directory.
|
|
29
|
+
*
|
|
30
|
+
* This probes the directory by creating a test file, writing content,
|
|
31
|
+
* and cleaning up. This catches cases where showDirectoryPicker grants
|
|
32
|
+
* a handle but the underlying file system is read-only.
|
|
33
|
+
*
|
|
34
|
+
* User Feedback Issue #1: Even with mode: 'readwrite', Android can
|
|
35
|
+
* return a handle where getFileHandle({create: true}) succeeds but
|
|
36
|
+
* createWritable() fails with NoModificationAllowedError.
|
|
37
|
+
*
|
|
38
|
+
* @param dirHandle - Directory handle to verify
|
|
39
|
+
* @returns Success status and optional error message
|
|
40
|
+
*/
|
|
41
|
+
async function verifyWriteAccess(dirHandle) {
|
|
42
|
+
const testFilename = `.write-test-${Date.now()}`;
|
|
43
|
+
try {
|
|
44
|
+
const writable = await (await dirHandle.getFileHandle(testFilename, { create: true })).createWritable();
|
|
45
|
+
await writable.write("write-test");
|
|
46
|
+
await writable.close();
|
|
47
|
+
try {
|
|
48
|
+
await dirHandle.removeEntry(testFilename);
|
|
49
|
+
} catch (cleanupErr) {
|
|
50
|
+
log.warn("Write test cleanup failed (non-critical):", cleanupErr);
|
|
51
|
+
}
|
|
52
|
+
log.info("Write access verified for:", dirHandle.name);
|
|
53
|
+
return { success: true };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
56
|
+
log.error("Write verification failed:", error);
|
|
57
|
+
if (error.name === "NoModificationAllowedError") return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: "Folder is read-only. Please select a different folder with write access."
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: `Cannot write to folder: ${error.message}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initialize storage using OPFS.
|
|
69
|
+
*
|
|
70
|
+
* This replaces the old showDirectoryPicker approach which failed on Android Chrome.
|
|
71
|
+
* OPFS is automatically available and doesn't require user folder selection.
|
|
72
|
+
*
|
|
73
|
+
* @returns List of existing scenario folder names
|
|
74
|
+
* @throws Error if OPFS is not supported
|
|
75
|
+
*/
|
|
76
|
+
async function initStorage() {
|
|
77
|
+
try {
|
|
78
|
+
await initOpfsStorage();
|
|
79
|
+
storageInitialized = true;
|
|
80
|
+
setFileSystemState({
|
|
81
|
+
folderSelected: true,
|
|
82
|
+
writeVerified: true
|
|
83
|
+
});
|
|
84
|
+
const scenarios = await listScenarios();
|
|
85
|
+
log.info("OPFS storage initialized, found scenarios:", scenarios);
|
|
86
|
+
return scenarios;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
log.error("OPFS initialization failed:", error);
|
|
90
|
+
setFileSystemState({
|
|
91
|
+
folderSelected: false,
|
|
92
|
+
writeVerified: false,
|
|
93
|
+
writeError: error.message
|
|
94
|
+
});
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create or open a scenario folder, then create a new session folder.
|
|
100
|
+
*
|
|
101
|
+
* Now uses OPFS backend instead of showDirectoryPicker.
|
|
102
|
+
*
|
|
103
|
+
* @param scenarioName - Name of the scenario
|
|
104
|
+
*/
|
|
105
|
+
async function startSession(scenarioName) {
|
|
106
|
+
if (!storageInitialized) throw new Error("Storage not initialized. Call initStorage first.");
|
|
107
|
+
const result = await createSession(scenarioName, /* @__PURE__ */ new Date());
|
|
108
|
+
result.scenarioName;
|
|
109
|
+
result.sessionName;
|
|
110
|
+
log.info("Session started:", `${result.scenarioName}/${result.sessionName}`);
|
|
111
|
+
return {
|
|
112
|
+
scenarioPath: result.scenarioName,
|
|
113
|
+
sessionPath: result.sessionName
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Write a Redux action to OPFS as a JSON file.
|
|
118
|
+
*/
|
|
119
|
+
async function writeAction(action, index) {
|
|
120
|
+
await writeAction$1(action, index);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Write a captured frame image to OPFS.
|
|
124
|
+
*/
|
|
125
|
+
async function writeFrame(blob, index) {
|
|
126
|
+
await writeFrame$1(blob, index);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Write session metadata (session.json) to OPFS.
|
|
130
|
+
* Contains timing, counts, and user agent for the recording session.
|
|
131
|
+
*/
|
|
132
|
+
async function writeSessionMetadata(metadata) {
|
|
133
|
+
await writeSessionMetadata$1(metadata);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Type guard for reference point actions
|
|
137
|
+
* Exported for testing
|
|
138
|
+
*/
|
|
139
|
+
function isRefPointAction(action) {
|
|
140
|
+
if (typeof action !== "object" || action === null) return false;
|
|
141
|
+
const obj = action;
|
|
142
|
+
const payload = obj.payload;
|
|
143
|
+
return obj.type === "recorder/markRefPoint" && typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse action files from a session's actions directory
|
|
147
|
+
*/
|
|
148
|
+
async function parseSessionRefPoints(actionsDir, sessionName) {
|
|
149
|
+
const refPoints = [];
|
|
150
|
+
for await (const actionEntry of actionsDir.values()) {
|
|
151
|
+
if (actionEntry.kind !== "file" || !actionEntry.name.endsWith(".json")) continue;
|
|
152
|
+
const text = await (await (await actionsDir.getFileHandle(actionEntry.name)).getFile()).text();
|
|
153
|
+
try {
|
|
154
|
+
const action = JSON.parse(text);
|
|
155
|
+
if (isRefPointAction(action)) refPoints.push({
|
|
156
|
+
id: action.payload.id,
|
|
157
|
+
sessionName,
|
|
158
|
+
gpsPosition: action.payload.gpsPosition,
|
|
159
|
+
arPose: action.payload.arPose
|
|
160
|
+
});
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
return refPoints;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Load existing reference points from a scenario (across all sessions).
|
|
167
|
+
* Now uses OPFS storage backend.
|
|
168
|
+
*/
|
|
169
|
+
async function loadScenarioRefPoints(scenarioName) {
|
|
170
|
+
const scenariosRoot = getScenariosRootHandle();
|
|
171
|
+
if (!scenariosRoot) return [];
|
|
172
|
+
const refPoints = [];
|
|
173
|
+
try {
|
|
174
|
+
const scenarioHandle = await scenariosRoot.getDirectoryHandle(scenarioName);
|
|
175
|
+
for await (const sessionEntry of scenarioHandle.values()) {
|
|
176
|
+
if (sessionEntry.kind !== "directory") continue;
|
|
177
|
+
const sessionHandle = await scenarioHandle.getDirectoryHandle(sessionEntry.name);
|
|
178
|
+
let actionsDir;
|
|
179
|
+
try {
|
|
180
|
+
actionsDir = await sessionHandle.getDirectoryHandle("actions");
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const sessionRefPoints = await parseSessionRefPoints(actionsDir, sessionEntry.name);
|
|
185
|
+
refPoints.push(...sessionRefPoints);
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
return refPoints;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the current scenario handle (for loading prior sessions).
|
|
192
|
+
* Now delegates to OPFS storage backend.
|
|
193
|
+
*/
|
|
194
|
+
function getCurrentScenarioHandle() {
|
|
195
|
+
return getScenarioHandle();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Set the current scenario handle (when user selects a scenario).
|
|
199
|
+
* With OPFS, we just store the name - the actual handle is retrieved when needed.
|
|
200
|
+
*/
|
|
201
|
+
async function setCurrentScenario(scenarioName) {
|
|
202
|
+
const scenariosRoot = getScenariosRootHandle();
|
|
203
|
+
if (!scenariosRoot) return null;
|
|
204
|
+
try {
|
|
205
|
+
return await scenariosRoot.getDirectoryHandle(scenarioName);
|
|
206
|
+
} catch {
|
|
207
|
+
log.warn("Scenario not found:", scenarioName);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Ensure a scenario directory exists in OPFS, creating it if necessary.
|
|
213
|
+
* Used during OPFS recovery when the scenario directory was lost but ZIP
|
|
214
|
+
* data is available for restoration.
|
|
215
|
+
*
|
|
216
|
+
* Unlike setCurrentScenario() which only reads, this creates on demand.
|
|
217
|
+
*/
|
|
218
|
+
async function ensureScenarioDirectory(scenarioName) {
|
|
219
|
+
const scenariosRoot = getScenariosRootHandle();
|
|
220
|
+
if (!scenariosRoot) return null;
|
|
221
|
+
try {
|
|
222
|
+
return await scenariosRoot.getDirectoryHandle(scenarioName, { create: true });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log.error("Failed to create scenario directory:", err);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { ensureScenarioDirectory, getCurrentScenarioHandle, initStorage, isRefPointAction, loadScenarioRefPoints, resetForNewSession, resetStorageState, setCurrentScenario, startSession, verifyWriteAccess, writeAction, writeFrame, writeSessionMetadata };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { a as getScenarioHandle, c as initOpfsStorage, d as resetOpfsStorage, f as resetSessionHandles, h as writeSessionMetadata$1, i as createSession, n as SessionMetadata, o as getScenariosRootHandle, r as checkStorageQuota, s as getSessionHandle, t as CreateSessionResult, u as listSessions } from "../opfs-storage-ChOzF_Wp.js";
|
|
2
|
+
import { t as StorageBackend } from "../storage-backend-BcEItnFr.js";
|
|
3
|
+
import { n as RefPointImportResult, r as importRefPointsFromFolder, t as ImportedRefPoint } from "../ref-point-importer-B_wn5ABk.js";
|
|
4
|
+
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 } from "../ref-point-loader-Pt1smgHS.js";
|
|
5
|
+
import { a as ZipActionEntry, c as loadSessionMetadata, i as RecordedAction, l as loadSessionMetadataFromBlob, n as GpsPathCoord, o as loadActionsFromZip, r as MAX_ACTION_FILE_SIZE, s as loadGpsPathFromBlob, t as Entry, u as readZipEntries } from "../zip-reader-D7pkpX8K.js";
|
|
6
|
+
import { t as NullStorageBackend } from "../null-storage-backend-CsWvQwN8.js";
|
|
7
|
+
import { t as OpfsStorageBackend } from "../opfs-storage-backend-CVcmfJHN.js";
|
|
8
|
+
import { a as initStorage, c as resetForNewSession, d as startSession, f as verifyWriteAccess, h as writeSessionMetadata, i as getCurrentScenarioHandle, l as resetStorageState, m as writeFrame, n as WriteAccessResult, o as isRefPointAction, p as writeAction, s as loadScenarioRefPoints, u as setCurrentScenario } from "../file-system-BsQgYCU3.js";
|
|
9
|
+
import { n as formatFrameFilename, r as formatTimestamp, t as formatActionFilename } from "../file-system-utils-5cWO8Xyv.js";
|
|
10
|
+
import { a as syncToExternalZip, i as exportSessionAsZip, n as downloadZip, r as exportAndDownloadSession, t as ZipExportResult } from "../zip-export-Rtpi75JJ.js";
|
|
11
|
+
export { type CreateSessionResult, type Entry, type GpsPathCoord, type ImportedRefPoint, MAX_ACTION_FILE_SIZE, NullStorageBackend, OpfsStorageBackend, type RecordedAction, type RefPointDefinition, type RefPointImportResult, type RefPointMark, type RefPointObservation, type SessionMetadata, type StorageBackend, type WriteAccessResult, type ZipActionEntry, type ZipExportResult, averageGpsPerRefPoint, checkStorageQuota, createSession, downloadZip, exportAndDownloadSession, exportSessionAsZip, flattenRefPointsToMarks, formatActionFilename, formatFrameFilename, formatTimestamp, getCurrentScenarioHandle, getScenarioHandle, getScenariosRootHandle, getSessionHandle, importRefPointsFromFolder, initOpfsStorage, initStorage, isRefPointAction, listRefPointIds, listSessions, loadActionsFromZip, loadAllRefPoints, loadGpsPathFromBlob, loadRefPoint, loadScenarioRefPoints, loadSessionMetadataFromBlob, loadSessionMetadata as loadSessionMetadataFromZip, readZipEntries, resetForNewSession, resetOpfsStorage, resetSessionHandles, resetStorageState, saveRefPointObservation, setCurrentScenario, startSession as startStorageSession, syncToExternalZip, verifyWriteAccess, writeAction, writeFrame, writeSessionMetadata as writeSessionMeta, writeSessionMetadata$1 as writeSessionMetadata };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { formatActionFilename, formatFrameFilename, formatTimestamp } from "./file-system-utils.js";
|
|
2
|
+
import { checkStorageQuota, createSession, getScenarioHandle, getScenariosRootHandle, getSessionHandle, initOpfsStorage, listSessions, resetOpfsStorage, resetSessionHandles, writeSessionMetadata as writeSessionMetadata$1 } from "./opfs-storage.js";
|
|
3
|
+
import { getCurrentScenarioHandle, initStorage, isRefPointAction, loadScenarioRefPoints, resetForNewSession, resetStorageState, setCurrentScenario, startSession, verifyWriteAccess, writeAction, writeFrame, writeSessionMetadata } from "./file-system.js";
|
|
4
|
+
import { OpfsStorageBackend } from "./opfs-storage-backend.js";
|
|
5
|
+
import { MAX_ACTION_FILE_SIZE, loadActionsFromZip, loadGpsPathFromBlob, loadSessionMetadata, loadSessionMetadataFromBlob, readZipEntries } from "./zip-reader.js";
|
|
6
|
+
import { NullStorageBackend } from "./null-storage-backend.js";
|
|
7
|
+
import { importRefPointsFromFolder } from "./ref-point-importer.js";
|
|
8
|
+
import { averageGpsPerRefPoint, flattenRefPointsToMarks, listRefPointIds, loadAllRefPoints, loadRefPoint, saveRefPointObservation } from "./ref-point-loader.js";
|
|
9
|
+
import { downloadZip, exportAndDownloadSession, exportSessionAsZip, syncToExternalZip } from "./zip-export.js";
|
|
10
|
+
export { MAX_ACTION_FILE_SIZE, NullStorageBackend, OpfsStorageBackend, averageGpsPerRefPoint, checkStorageQuota, createSession, downloadZip, exportAndDownloadSession, exportSessionAsZip, flattenRefPointsToMarks, formatActionFilename, formatFrameFilename, formatTimestamp, getCurrentScenarioHandle, getScenarioHandle, getScenariosRootHandle, getSessionHandle, importRefPointsFromFolder, initOpfsStorage, initStorage, isRefPointAction, listRefPointIds, listSessions, loadActionsFromZip, loadAllRefPoints, loadGpsPathFromBlob, loadRefPoint, loadScenarioRefPoints, loadSessionMetadataFromBlob, loadSessionMetadata as loadSessionMetadataFromZip, readZipEntries, resetForNewSession, resetOpfsStorage, resetSessionHandles, resetStorageState, saveRefPointObservation, setCurrentScenario, startSession as startStorageSession, syncToExternalZip, verifyWriteAccess, writeAction, writeFrame, writeSessionMetadata as writeSessionMeta, writeSessionMetadata$1 as writeSessionMetadata };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { writeAction, writeFrame, writeSessionMetadata } from "./file-system.js";
|
|
2
|
+
//#region ../src/storage/opfs-storage-backend.ts
|
|
3
|
+
var OpfsStorageBackend = class {
|
|
4
|
+
async writeAction(action, index) {
|
|
5
|
+
await writeAction(action, index);
|
|
6
|
+
}
|
|
7
|
+
async writeFrame(blob, index) {
|
|
8
|
+
await writeFrame(blob, index);
|
|
9
|
+
}
|
|
10
|
+
async writeSessionMetadata(metadata) {
|
|
11
|
+
await writeSessionMetadata(metadata);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { OpfsStorageBackend };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as getScenarioHandle, c as initOpfsStorage, d as resetOpfsStorage, f as resetSessionHandles, h as writeSessionMetadata, i as createSession, l as listScenarios, m as writeFrame, n as SessionMetadata, o as getScenariosRootHandle, p as writeAction, r as checkStorageQuota, s as getSessionHandle, t as CreateSessionResult, u as listSessions } from "../opfs-storage-ChOzF_Wp.js";
|
|
2
|
+
export { CreateSessionResult, SessionMetadata, checkStorageQuota, createSession, getScenarioHandle, getScenariosRootHandle, getSessionHandle, initOpfsStorage, listScenarios, listSessions, resetOpfsStorage, resetSessionHandles, writeAction, writeFrame, writeSessionMetadata };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger.js";
|
|
2
|
+
import { formatActionFilename, formatFrameFilename, formatTimestamp } from "./file-system-utils.js";
|
|
3
|
+
//#region ../src/storage/opfs-storage.ts
|
|
4
|
+
/**
|
|
5
|
+
* OPFS Storage Module
|
|
6
|
+
*
|
|
7
|
+
* Uses the Origin Private File System (OPFS) API to persist recording data.
|
|
8
|
+
* OPFS works consistently on Desktop Chrome, Android Chrome, and iOS Safari,
|
|
9
|
+
* unlike showDirectoryPicker which has write restrictions on Android.
|
|
10
|
+
*
|
|
11
|
+
* Directory structure:
|
|
12
|
+
* /gps-recorder/
|
|
13
|
+
* └── scenarios/
|
|
14
|
+
* ├── {scenario-name}/
|
|
15
|
+
* │ ├── recording-YYYY-MM-DD_HH-MM-SSutc/
|
|
16
|
+
* │ │ ├── session.json
|
|
17
|
+
* │ │ ├── actions/
|
|
18
|
+
* │ │ │ ├── 000001.json
|
|
19
|
+
* │ │ │ └── ...
|
|
20
|
+
* │ │ └── frames/
|
|
21
|
+
* │ │ ├── frame-000001.jpg
|
|
22
|
+
* │ │ └── ...
|
|
23
|
+
* │ └── ...
|
|
24
|
+
* └── ...
|
|
25
|
+
*/
|
|
26
|
+
const log = createLogger("OPFS");
|
|
27
|
+
let opfsRoot = null;
|
|
28
|
+
let gpsRecorderDir = null;
|
|
29
|
+
let scenariosDir = null;
|
|
30
|
+
let currentScenarioHandle = null;
|
|
31
|
+
let currentSessionHandle = null;
|
|
32
|
+
let actionsHandle = null;
|
|
33
|
+
let framesHandle = null;
|
|
34
|
+
/**
|
|
35
|
+
* Reset module state - exported for testing only.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function resetOpfsStorage() {
|
|
39
|
+
opfsRoot = null;
|
|
40
|
+
gpsRecorderDir = null;
|
|
41
|
+
scenariosDir = null;
|
|
42
|
+
currentScenarioHandle = null;
|
|
43
|
+
currentSessionHandle = null;
|
|
44
|
+
actionsHandle = null;
|
|
45
|
+
framesHandle = null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reset session-level handles only.
|
|
49
|
+
*
|
|
50
|
+
* Preserves opfsRoot/gpsRecorderDir/scenariosDir (OPFS stays initialized)
|
|
51
|
+
* but clears current scenario/session/actions/frames handles so a new
|
|
52
|
+
* session can be started fresh.
|
|
53
|
+
*
|
|
54
|
+
* Used during soft reset for new recordings (Issue 4, 2026-02-06 user feedback).
|
|
55
|
+
*/
|
|
56
|
+
function resetSessionHandles() {
|
|
57
|
+
currentScenarioHandle = null;
|
|
58
|
+
currentSessionHandle = null;
|
|
59
|
+
actionsHandle = null;
|
|
60
|
+
framesHandle = null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Initialize OPFS storage by creating the required directory structure.
|
|
64
|
+
*
|
|
65
|
+
* Creates:
|
|
66
|
+
* - /gps-recorder/
|
|
67
|
+
* - /gps-recorder/scenarios/
|
|
68
|
+
*
|
|
69
|
+
* This is idempotent - calling multiple times is safe.
|
|
70
|
+
*
|
|
71
|
+
* @throws Error if OPFS is not supported
|
|
72
|
+
*/
|
|
73
|
+
async function initOpfsStorage() {
|
|
74
|
+
if (typeof navigator === "undefined" || !navigator.storage || typeof navigator.storage.getDirectory !== "function") throw new Error("OPFS (Origin Private File System) is not supported in this browser. Please use Chrome 86+, Safari 15.2+, or Firefox 111+.");
|
|
75
|
+
opfsRoot = await navigator.storage.getDirectory();
|
|
76
|
+
log.info("OPFS root obtained");
|
|
77
|
+
gpsRecorderDir = await opfsRoot.getDirectoryHandle("gps-recorder", { create: true });
|
|
78
|
+
scenariosDir = await gpsRecorderDir.getDirectoryHandle("scenarios", { create: true });
|
|
79
|
+
log.info("OPFS storage initialized");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a new recording session within a scenario.
|
|
83
|
+
*
|
|
84
|
+
* Creates the directory structure:
|
|
85
|
+
* - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/
|
|
86
|
+
* - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/actions/
|
|
87
|
+
* - /gps-recorder/scenarios/{scenarioName}/recording-{timestamp}/frames/
|
|
88
|
+
*
|
|
89
|
+
* @param scenarioName - Name of the scenario (creates if doesn't exist)
|
|
90
|
+
* @param timestamp - Session start time (used for folder naming)
|
|
91
|
+
* @returns Session information
|
|
92
|
+
* @throws Error if storage not initialized
|
|
93
|
+
*/
|
|
94
|
+
async function createSession(scenarioName, timestamp) {
|
|
95
|
+
if (!scenariosDir) throw new Error("OPFS storage not initialized. Call initOpfsStorage first.");
|
|
96
|
+
currentScenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName, { create: true });
|
|
97
|
+
const sessionName = `recording-${formatTimestamp(timestamp)}`;
|
|
98
|
+
currentSessionHandle = await currentScenarioHandle.getDirectoryHandle(sessionName, { create: true });
|
|
99
|
+
actionsHandle = await currentSessionHandle.getDirectoryHandle("actions", { create: true });
|
|
100
|
+
framesHandle = await currentSessionHandle.getDirectoryHandle("frames", { create: true });
|
|
101
|
+
log.info("Session created:", `${scenarioName}/${sessionName}`);
|
|
102
|
+
return {
|
|
103
|
+
scenarioName,
|
|
104
|
+
sessionName
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get the current session directory handle.
|
|
109
|
+
* Useful for test assertions.
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
function getSessionHandle() {
|
|
113
|
+
return currentSessionHandle;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get the current scenario directory handle.
|
|
117
|
+
*/
|
|
118
|
+
function getScenarioHandle() {
|
|
119
|
+
return currentScenarioHandle;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the scenarios root directory handle.
|
|
123
|
+
* Used for loading reference points across scenarios.
|
|
124
|
+
*/
|
|
125
|
+
function getScenariosRootHandle() {
|
|
126
|
+
return scenariosDir;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Safely write data to a file handle with proper cleanup.
|
|
130
|
+
*
|
|
131
|
+
* Uses try/finally to ensure the writable stream is always cleaned up,
|
|
132
|
+
* calling abort() on errors to prevent resource leaks and file locks.
|
|
133
|
+
*
|
|
134
|
+
* @param fileHandle - The file handle to write to
|
|
135
|
+
* @param data - The data to write (string or Blob)
|
|
136
|
+
*/
|
|
137
|
+
async function safeWriteToFile(fileHandle, data) {
|
|
138
|
+
const writable = await fileHandle.createWritable();
|
|
139
|
+
let writeError = null;
|
|
140
|
+
try {
|
|
141
|
+
await writable.write(data);
|
|
142
|
+
await writable.close();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
writeError = error;
|
|
145
|
+
} finally {
|
|
146
|
+
if (writeError !== null) try {
|
|
147
|
+
await writable.abort();
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
if (writeError !== null) {
|
|
151
|
+
if (writeError instanceof Error) throw writeError;
|
|
152
|
+
throw new Error(typeof writeError === "string" ? writeError : "File write failed");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Write a Redux action to the current session's actions directory.
|
|
157
|
+
*
|
|
158
|
+
* @param action - The action object to write
|
|
159
|
+
* @param index - Sequential action index (1-based)
|
|
160
|
+
* @throws Error if no active session
|
|
161
|
+
*/
|
|
162
|
+
async function writeAction(action, index) {
|
|
163
|
+
if (!actionsHandle) throw new Error("No active session. Call createSession first.");
|
|
164
|
+
const filename = formatActionFilename(index);
|
|
165
|
+
await safeWriteToFile(await actionsHandle.getFileHandle(filename, { create: true }), JSON.stringify(action, null, 2));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Write a frame blob to the current session's frames directory.
|
|
169
|
+
*
|
|
170
|
+
* @param blob - Image blob to write
|
|
171
|
+
* @param index - Sequential frame index (1-based)
|
|
172
|
+
* @throws Error if no active session
|
|
173
|
+
*/
|
|
174
|
+
async function writeFrame(blob, index) {
|
|
175
|
+
if (!framesHandle) throw new Error("No active session. Call createSession first.");
|
|
176
|
+
const filename = formatFrameFilename(index);
|
|
177
|
+
await safeWriteToFile(await framesHandle.getFileHandle(filename, { create: true }), blob);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Write session metadata to session.json in the current session directory.
|
|
181
|
+
*
|
|
182
|
+
* @param metadata - Session metadata object
|
|
183
|
+
* @throws Error if no active session
|
|
184
|
+
*/
|
|
185
|
+
async function writeSessionMetadata(metadata) {
|
|
186
|
+
if (!currentSessionHandle) throw new Error("No active session. Call createSession first.");
|
|
187
|
+
await safeWriteToFile(await currentSessionHandle.getFileHandle("session.json", { create: true }), JSON.stringify(metadata, null, 2));
|
|
188
|
+
log.info("Session metadata written");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* List all scenario names in OPFS storage.
|
|
192
|
+
*
|
|
193
|
+
* @returns Array of scenario folder names
|
|
194
|
+
*/
|
|
195
|
+
async function listScenarios() {
|
|
196
|
+
if (!scenariosDir) return [];
|
|
197
|
+
const scenarios = [];
|
|
198
|
+
for await (const entry of scenariosDir.values()) if (entry.kind === "directory") scenarios.push(entry.name);
|
|
199
|
+
return scenarios;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* List all session names within a scenario.
|
|
203
|
+
*
|
|
204
|
+
* @param scenarioName - Name of the scenario
|
|
205
|
+
* @returns Array of session folder names
|
|
206
|
+
*/
|
|
207
|
+
async function listSessions(scenarioName) {
|
|
208
|
+
if (!scenariosDir) return [];
|
|
209
|
+
try {
|
|
210
|
+
const scenarioHandle = await scenariosDir.getDirectoryHandle(scenarioName);
|
|
211
|
+
const sessions = [];
|
|
212
|
+
for await (const entry of scenarioHandle.values()) if (entry.kind === "directory") sessions.push(entry.name);
|
|
213
|
+
return sessions;
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check storage quota.
|
|
220
|
+
*
|
|
221
|
+
* Note: Prefer calling `initOpfsStorage()` before this function to ensure
|
|
222
|
+
* the Storage API is available. If the Storage Manager API is not supported,
|
|
223
|
+
* returns a safe default of `{ available: 0, used: 0 }`.
|
|
224
|
+
*
|
|
225
|
+
* @returns Available and used storage in bytes
|
|
226
|
+
*/
|
|
227
|
+
async function checkStorageQuota() {
|
|
228
|
+
if (typeof navigator === "undefined" || !navigator.storage || typeof navigator.storage.estimate !== "function") {
|
|
229
|
+
log.warn("Storage Manager API not available, returning zero quota");
|
|
230
|
+
return {
|
|
231
|
+
available: 0,
|
|
232
|
+
used: 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const estimate = await navigator.storage.estimate();
|
|
236
|
+
return {
|
|
237
|
+
available: estimate.quota ?? 0,
|
|
238
|
+
used: estimate.usage ?? 0
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { checkStorageQuota, createSession, getScenarioHandle, getScenariosRootHandle, getSessionHandle, initOpfsStorage, listScenarios, listSessions, resetOpfsStorage, resetSessionHandles, writeAction, writeFrame, writeSessionMetadata };
|