Skip to content

Commit b3539f0

Browse files
committed
fix(raster): keep local rasters in saved projects and under layer order
Three desktop-facing raster problems from issue #1463. Rasters vanish from a reopened project. A local raster reaches the control as a browser `File`, so nothing on the layer pointed at the file it came from and restoring a project dropped it. The desktop host now records the absolute path it read the bytes from (`metadata.localFilePath`) and re-reads the file on project open through a host-registered reader; the raster panel's own browse button routes through the native file dialog so a panel-opened raster carries a path too. Unchanged in the browser, which has no path: those rasters are still dropped with a notice. Rasters obscure the vectors above them. The deck.gl engine draws through an overlay that, on desktop, is a separate canvas stacked over the map, so layer order could not reach it. Rasters now default to the `cog-tiler-wasm` engine, which decodes tiles in WebAssembly and feeds a native MapLibre raster source/layer, so ordering is the map's own. The store sync is engine-aware and records the real style layer id. Trade-off: the WASM tiler renders from its own built-in colormaps, so "Classify into discrete classes" and custom color ramps -- which work by patching the deck.gl render pipeline -- do not apply while it is active. The panel's Rendering engine selector still offers the GPU engine. A data: URI image fails to load on desktop only. The Tauri CSP allowed `data:` in `img-src` but not `connect-src`, and MapLibre fetches an image source's URL rather than assigning it to an `<img>` -- so a KML/KMZ ground overlay (or a deck.gl icon atlas) died with an opaque "Load failed". The web CSP already carried `data:` for this reason; the desktop one now matches.
1 parent 691d633 commit b3539f0

8 files changed

Lines changed: 442 additions & 22 deletions

File tree

apps/geolibre-desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
}
2222
],
2323
"security": {
24-
"csp": "default-src 'self'; connect-src 'self' blob: https: http://127.0.0.1:* http://localhost:* wss://collab.geolibre.app ws://127.0.0.1:* ws://localhost:*; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/ https://cdn.jsdelivr.net/pyodide/ https://accounts.google.com; child-src 'self' http://127.0.0.1:* http://localhost:* https://accounts.google.com https://www.google.com; frame-src 'self' http://127.0.0.1:* http://localhost:* https://accounts.google.com https://www.google.com; worker-src blob: 'self'",
24+
"csp": "default-src 'self'; connect-src 'self' data: blob: https: http://127.0.0.1:* http://localhost:* wss://collab.geolibre.app ws://127.0.0.1:* ws://localhost:*; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/ https://cdn.jsdelivr.net/pyodide/ https://accounts.google.com; child-src 'self' http://127.0.0.1:* http://localhost:* https://accounts.google.com https://www.google.com; frame-src 'self' http://127.0.0.1:* http://localhost:* https://accounts.google.com https://www.google.com; worker-src blob: 'self'",
2525
"capabilities": ["default"]
2626
}
2727
},

apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
restoreThreeDTilesLayers,
2727
restoreVectorLayers,
2828
setBookmarkLabels,
29+
setLocalRasterFileReader,
30+
setLocalRasterPicker,
2931
setNonTiledRasterHandler,
3032
setTerrainMeasureLabels,
3133
setViewStateLabels,
@@ -57,6 +59,8 @@ import {
5759
loadDroppedPhotoPaths,
5860
loadDroppedRasterFiles,
5961
loadDroppedRasterPaths,
62+
pickLocalRasterFiles,
63+
readRasterFileAtPath,
6064
isLoadedImageOverlay,
6165
isLoadedModel,
6266
loadDroppedVectorFiles,
@@ -931,6 +935,21 @@ export function DesktopShell({
931935
}
932936
}, []);
933937

938+
// Let the raster plugin reach the local filesystem on desktop: re-read a
939+
// raster a saved project references by path, and open the native file dialog
940+
// instead of the panel's own <input type="file"> (whose File carries no path,
941+
// so the raster could never be restored). Both stay unregistered in the
942+
// browser, where the plugin keeps its existing behavior. See issue #1463.
943+
useEffect(() => {
944+
if (!isTauri()) return;
945+
setLocalRasterFileReader(readRasterFileAtPath);
946+
setLocalRasterPicker(pickLocalRasterFiles);
947+
return () => {
948+
setLocalRasterFileReader(null);
949+
setLocalRasterPicker(null);
950+
};
951+
}, []);
952+
934953
// When a GeoTIFF fails to load because it is striped (not a tiled COG), offer
935954
// to convert it to a COG in the browser and load the result. Works for both a
936955
// local file and a remote URL (issue #916). The raster plugin detects the case
@@ -1241,7 +1260,12 @@ export function DesktopShell({
12411260
if (!rasters.length) return 0;
12421261
const appAPI = createAppAPI(mapControllerRef);
12431262
for (const raster of rasters) {
1244-
await addRasterToMap(appAPI, raster.source, { name: raster.name });
1263+
// `path` is present only for a desktop pick/drop; it is what lets a saved
1264+
// project reload the raster instead of dropping it (issue #1463).
1265+
await addRasterToMap(appAPI, raster.source, {
1266+
name: raster.name,
1267+
...(raster.path ? { localPath: raster.path } : {}),
1268+
});
12451269
}
12461270
return rasters.length;
12471271
}, []);

apps/geolibre-desktop/src/lib/tauri-io.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,6 +2412,12 @@ export interface DroppedRaster {
24122412
* manages its object URL, matching how the Add Raster panel loads local files.
24132413
*/
24142414
source: File;
2415+
/**
2416+
* The absolute path the bytes were read from, when there is one (Tauri).
2417+
* Recorded on the layer so a saved project can reload the raster; absent for
2418+
* a browser drag-and-drop, which has no path.
2419+
*/
2420+
path?: string;
24152421
}
24162422

24172423
function fileBaseName(path: string): string {
@@ -2439,11 +2445,54 @@ export async function loadDroppedRasterPaths(paths: string[]): Promise<DroppedRa
24392445
rasters.push({
24402446
name,
24412447
source: new File([bytes], name, { type: "image/tiff" }),
2448+
path,
24422449
});
24432450
}
24442451
return rasters;
24452452
}
24462453

2454+
/**
2455+
* Read one raster file off disk into a browser `File`, for reloading a raster a
2456+
* saved project references by path (issue #1463). Rejects when the file is
2457+
* gone; the caller then drops that layer with a notice.
2458+
*
2459+
* @param path - The absolute path recorded when the raster was first added.
2460+
* @returns The file, named after its basename.
2461+
*/
2462+
export async function readRasterFileAtPath(path: string): Promise<File> {
2463+
const bytes = await readFile(path);
2464+
return new File([bytes], fileBaseName(path), { type: "image/tiff" });
2465+
}
2466+
2467+
/**
2468+
* Open a native file dialog for raster files and read each pick, keeping the
2469+
* absolute path alongside the bytes. Used in place of the raster panel's own
2470+
* `<input type="file">`, whose `File` carries no path. Resolves to an empty
2471+
* array when the dialog is cancelled or the app is not running under Tauri.
2472+
*
2473+
* @returns The picked rasters, each with its file and path.
2474+
*/
2475+
export async function pickLocalRasterFiles(): Promise<{ file: File; path: string }[]> {
2476+
if (!isTauri()) return [];
2477+
const selected = await open({
2478+
multiple: true,
2479+
filters: [{ name: "Rasters", extensions: [...RASTER_DROP_EXTENSIONS] }],
2480+
});
2481+
if (!selected) return [];
2482+
const paths = (Array.isArray(selected) ? selected : [selected]).filter(isRasterFileName);
2483+
const picked: { file: File; path: string }[] = [];
2484+
for (const path of paths) {
2485+
// Read each pick independently so one unreadable file does not abandon the
2486+
// rest of the selection, matching pickImageFilesWithFallback.
2487+
try {
2488+
picked.push({ file: await readRasterFileAtPath(path), path });
2489+
} catch (error) {
2490+
console.warn(`Could not read the selected raster "${path}".`, error);
2491+
}
2492+
}
2493+
return picked;
2494+
}
2495+
24472496
/**
24482497
* Open a multi-select image picker and read each pick into a browser `File`, so
24492498
* the geotagged-photo importer reads EXIF and renders thumbnails the same way on

docker/nginx.conf

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ server {
6161
add_header Cache-Control "no-cache, must-revalidate" always;
6262
add_header X-Content-Type-Options "nosniff" always;
6363
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
64-
# Closely mirrors the Tauri desktop CSP (the browser build additionally
65-
# permits data: URIs in connect-src). 'unsafe-eval' is required by the
64+
# Closely mirrors the Tauri desktop CSP. 'unsafe-eval' is required by the
6665
# @google/earthengine and maplibre-gl-geoagent bundles; connect-src
6766
# stays broad because users connect to arbitrary tile/WMS endpoints.
6867
# The http://localhost:* / http://127.0.0.1:* (and ws:// equivalents)
@@ -83,8 +82,11 @@ server {
8382
# mirror the change in the Tauri CSP in
8483
# apps/geolibre-desktop/src-tauri/tauri.conf.json as well --
8584
# wss://collab.geolibre.app is an example of a host duplicated in both.
86-
# (The browser build intentionally adds data: to connect-src; that
87-
# difference is documented in the "Closely mirrors" note above.)
85+
# `data:` must stay in connect-src in BOTH CSPs: MapLibre fetches an
86+
# image source's URL (it does not assign it to an <img>), so a data:
87+
# URI image -- a KML/KMZ <GroundOverlay>, a deck.gl icon atlas -- is
88+
# matched against connect-src, not img-src. Dropping it makes those
89+
# layers fail to load with an opaque "Load failed" (issue #1463).
8890
# The Tauri CSP additionally allows http://127.0.0.1:* / http://localhost:*
8991
# in frame-src/child-src so the desktop app can embed its locally
9092
# launched JupyterLab server in the Notebook panel. That is desktop-only

packages/plugins/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,14 @@ export {
189189
closeRasterLayerPanel,
190190
openRasterLayerPanel,
191191
restoreRasterLayers,
192+
setLocalRasterFileReader,
193+
setLocalRasterPicker,
192194
setNonTiledRasterHandler,
193195
setRasterPixelInspect,
196+
type LocalRasterFileReader,
197+
type LocalRasterPicker,
194198
type NonTiledRasterRequest,
199+
type PickedLocalRaster,
195200
} from "./plugins/maplibre-raster";
196201
export {
197202
RASTER_MAX_CLASSES,

0 commit comments

Comments
 (0)