Skip to content

Commit 86143f7

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/issue-819-pixel-time-series
# Conflicts: # apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
2 parents c318381 + 261beac commit 86143f7

46 files changed

Lines changed: 5024 additions & 137 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/geolibre-desktop/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@tauri-apps/plugin-opener": "^2.5.4",
5151
"apache-arrow": "^17.0.0",
5252
"dompurify": "^3.4.10",
53+
"exifr": "^7.1.3",
5354
"fflate": "^0.8.3",
5455
"gdal3.js": "^2.8.1",
5556
"i18next": "^25.10.10",
@@ -67,11 +68,11 @@
6768
"maplibre-gl-geo-editor": "^0.9.1",
6869
"maplibre-gl-geoagent": "^0.4.4",
6970
"maplibre-gl-lidar": "^0.16.2",
70-
"maplibre-gl-splat": "^0.2.8",
7171
"maplibre-gl-nasa-earthdata": "^0.1.4",
7272
"maplibre-gl-national-map": "^0.1.1",
7373
"maplibre-gl-planetary-computer": "^0.3.0",
7474
"maplibre-gl-raster": "^0.6.3",
75+
"maplibre-gl-splat": "^0.2.8",
7576
"maplibre-gl-streetview": "^0.7.0",
7677
"maplibre-gl-swipe": "^0.9.1",
7778
"maplibre-gl-time-slider": "^1.1.0",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DelimitedTextSource } from "./add-data/sources/DelimitedTextSource";
1818
import { GeoRssSource } from "./add-data/sources/GeoRssSource";
1919
import { GpxSource } from "./add-data/sources/GpxSource";
2020
import { MbtilesSource } from "./add-data/sources/MbtilesSource";
21+
import { PhotosSource } from "./add-data/sources/PhotosSource";
2122
import { PostgresSource } from "./add-data/sources/PostgresSource";
2223
import { VideoSource } from "./add-data/sources/VideoSource";
2324
import { WfsSource } from "./add-data/sources/WfsSource";
@@ -64,6 +65,8 @@ function renderSource(
6465
return <GeoRssSource />;
6566
case "delimited-text":
6667
return <DelimitedTextSource />;
68+
case "photos":
69+
return <PhotosSource />;
6770
case "mbtiles":
6871
return <MbtilesSource />;
6972
case "arcgis":

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

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ import {
4242
} from "react";
4343
import {
4444
isTauri,
45+
loadDroppedPhotoFiles,
46+
loadDroppedPhotoPaths,
4547
loadDroppedRasterFiles,
4648
loadDroppedRasterPaths,
4749
loadDroppedVectorFiles,
4850
loadDroppedVectorPaths,
4951
type DroppedRaster,
5052
} from "../../lib/tauri-io";
53+
import {
54+
isPhotoDropFileName,
55+
type GeotaggedPhotoResult,
56+
} from "../../lib/geotagged-photos";
5157
import type { LargeVectorDataset } from "../../lib/duckdb-vector-guard";
5258
import i18n from "../../i18n";
5359
import {
@@ -63,6 +69,7 @@ import {
6369
createAppAPI,
6470
getPluginManager,
6571
useExternalPluginsReady,
72+
useSwipeSplitViewExclusivity,
6673
} from "../../hooks/usePlugins";
6774
import { registerMbtilesProtocol } from "../../lib/mbtiles";
6875
import { hasReverseGeocodeConsent } from "../../lib/reverse-geocode-consent";
@@ -81,6 +88,7 @@ import { CollaborateDialog } from "./CollaborateDialog";
8188
import { useCollaboration } from "../../hooks/useCollaboration";
8289
import { MapModeBanner } from "./MapModeBanner";
8390
import { PixelTimeSeriesControl } from "./PixelTimeSeriesControl";
91+
import { MapContextMenu } from "./MapContextMenu";
8492
import { MapGrid } from "./MapGrid";
8593
import { RemoteCursorsOverlay } from "./RemoteCursorsOverlay";
8694
import { useCommandBridge } from "../../hooks/useCommandBridge";
@@ -529,6 +537,9 @@ export function DesktopShell({
529537
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
530538
const diagnostics = useDiagnosticsSnapshot();
531539
const externalPluginsReady = useExternalPluginsReady(mapControllerRef);
540+
// Keep Layer Swipe and split view mutually exclusive (#844): entering a
541+
// multi-pane grid turns the swipe slider off.
542+
useSwipeSplitViewExclusivity(mapControllerRef);
532543
// Live-collaboration session. Owned here (rather than in TopToolbar) so both
533544
// the Collaborate dialog and the on-canvas status badge share one socket, and
534545
// so the dialog stays mounted in toolbar-hidden layouts.
@@ -903,6 +914,32 @@ export function DesktopShell({
903914
[addGeoJsonLayer],
904915
);
905916

917+
const addDroppedPhotos = useCallback(
918+
(result: GeotaggedPhotoResult | null): number => {
919+
if (!result || result.located === 0) return 0;
920+
const layerId = addGeoJsonLayer(
921+
t("addData.photos.defaultName"),
922+
result.featureCollection,
923+
);
924+
const layer = useAppStore
925+
.getState()
926+
.layers.find((existing) => existing.id === layerId);
927+
if (layer) mapControllerRef.current?.fitLayer(layer);
928+
// Report skipped (no-GPS) photos too, mirroring the Add Data dialog's
929+
// summary, so a partially-skipped drop isn't silent.
930+
const summary = t("addData.photos.addedSummary", {
931+
count: result.located,
932+
});
933+
const skippedNote =
934+
result.skipped > 0
935+
? ` ${t("addData.photos.skippedNote", { count: result.skipped })}`
936+
: "";
937+
setDropMessage(summary + skippedNote);
938+
return result.located;
939+
},
940+
[addGeoJsonLayer, t],
941+
);
942+
906943
const addDroppedRasters = useCallback(
907944
async (rasters: DroppedRaster[]): Promise<number> => {
908945
if (!rasters.length) return 0;
@@ -1054,19 +1091,37 @@ export function DesktopShell({
10541091
}
10551092
}
10561093

1057-
if (otherPaths.length > 0) {
1094+
// Geotagged photos become their own point layer; TIFF stays on the
1095+
// raster path. Handle them before the vector/raster pipeline so a
1096+
// dropped .jpg isn't routed to the DuckDB vector loader.
1097+
const photoResult = await loadDroppedPhotoPaths(otherPaths);
1098+
const photoCount = addDroppedPhotos(photoResult);
1099+
// Surface a clear message when every dropped photo lacked GPS, so
1100+
// the drop doesn't complete silently.
1101+
if (photoResult && photoCount === 0 && photoResult.total > 0) {
1102+
setDropError(
1103+
t("addData.photos.errorNoGps", { count: photoResult.total }),
1104+
);
1105+
}
1106+
const restPaths = otherPaths.filter(
1107+
(path) => !isPhotoDropFileName(path),
1108+
);
1109+
1110+
if (restPaths.length > 0) {
10581111
const rasterCount = await addDroppedRasters(
1059-
await loadDroppedRasterPaths(otherPaths),
1112+
await loadDroppedRasterPaths(restPaths),
10601113
);
1061-
const importedLayers = await loadDroppedVectorPaths(otherPaths, {
1114+
const importedLayers = await loadDroppedVectorPaths(restPaths, {
10621115
onLargeDataset: confirmLargeVectorDataset,
10631116
});
10641117
// See the browser handler: skip finishDrop's empty-input error
1065-
// when PBF files were present (even if rejected/failed).
1118+
// when PBF or photo files were present (even if rejected/failed).
1119+
// See the browser handler: suppress the empty-input error when
1120+
// photos were present so it can't clobber the GPS error above.
10661121
if (
10671122
importedLayers.length > 0 ||
10681123
rasterCount > 0 ||
1069-
pbfPaths.length === 0
1124+
(pbfPaths.length === 0 && photoResult === null)
10701125
) {
10711126
finishDrop(importedLayers, rasterCount);
10721127
}
@@ -1098,7 +1153,11 @@ export function DesktopShell({
10981153
disposed = true;
10991154
unlisten?.();
11001155
};
1101-
}, [clearDropMessageLater, finishDrop, addDroppedRasters, addGeoJsonLayer]);
1156+
}, [clearDropMessageLater,
1157+
finishDrop,
1158+
addDroppedRasters,
1159+
addDroppedPhotos,
1160+
addGeoJsonLayer]);
11021161

11031162
const handleDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
11041163
if (!hasDroppedFiles(event)) return;
@@ -1185,22 +1244,42 @@ export function DesktopShell({
11851244
);
11861245
}
11871246

1188-
if (otherFiles.length > 0) {
1247+
// Geotagged photos (JPEG/PNG/WebP/HEIC) become a single point layer of
1248+
// their own; TIFF is left to the raster path. Handle them before the
1249+
// vector/raster pipeline so a .jpg isn't sent to the DuckDB vector
1250+
// loader (which would fail).
1251+
const photoResult = await loadDroppedPhotoFiles(otherFiles);
1252+
const photoCount = addDroppedPhotos(photoResult);
1253+
// Surface a clear message when every dropped photo lacked GPS, so the
1254+
// drop doesn't complete silently.
1255+
if (photoResult && photoCount === 0 && photoResult.total > 0) {
1256+
setDropError(
1257+
t("addData.photos.errorNoGps", { count: photoResult.total }),
1258+
);
1259+
}
1260+
const restFiles = otherFiles.filter(
1261+
(file) => !isPhotoDropFileName(file.name),
1262+
);
1263+
1264+
if (restFiles.length > 0) {
11891265
const rasterCount = await addDroppedRasters(
1190-
loadDroppedRasterFiles(otherFiles),
1266+
loadDroppedRasterFiles(restFiles),
11911267
);
1192-
const importedLayers = await loadDroppedVectorFiles(otherFiles, {
1268+
const importedLayers = await loadDroppedVectorFiles(restFiles, {
11931269
onLargeDataset: confirmLargeVectorDataset,
11941270
});
11951271
// Call finishDrop (which reports success or throws the empty-input
11961272
// error) only when the other files produced something, or when the
1197-
// drop contained no PBF files at all. If PBF files were present —
1273+
// drop contained no PBF/photo files at all. If those were present —
11981274
// even if they were all rejected or failed — its empty-input error
1199-
// would wrongly clobber the PBF outcome.
1275+
// would wrongly clobber their outcome.
1276+
// Suppress finishDrop's empty-input error whenever photos were
1277+
// present (photoResult !== null) — even if all lacked GPS — so its
1278+
// generic message can't clobber the specific GPS error set above.
12001279
if (
12011280
importedLayers.length > 0 ||
12021281
rasterCount > 0 ||
1203-
pbfFiles.length === 0
1282+
(pbfFiles.length === 0 && photoResult === null)
12041283
) {
12051284
finishDrop(importedLayers, rasterCount);
12061285
}
@@ -1214,7 +1293,11 @@ export function DesktopShell({
12141293
clearDropMessageLater();
12151294
}
12161295
},
1217-
[clearDropMessageLater, finishDrop, addDroppedRasters, addGeoJsonLayer],
1296+
[clearDropMessageLater,
1297+
finishDrop,
1298+
addDroppedRasters,
1299+
addDroppedPhotos,
1300+
addGeoJsonLayer],
12181301
);
12191302

12201303
const startLayerPanelResize = useCallback(
@@ -1565,6 +1648,10 @@ export function DesktopShell({
15651648
onControllerReady={handleMapControllerReady}
15661649
/>
15671650
<RemoteCursorsOverlay mapControllerRef={mapControllerRef} />
1651+
<MapContextMenu
1652+
mapControllerRef={mapControllerRef}
1653+
mapReadyGeneration={mapReadyGeneration}
1654+
/>
15681655
<BoundsRestrictionIndicator />
15691656
{/* Isolate the collaboration badge in its own boundary: it renders
15701657
over the map, so a fault here must never take down the map

0 commit comments

Comments
 (0)