Skip to content

Commit 89ced12

Browse files
committed
feat(processing): add "Draw on map" bbox for the subset tools
Alongside "Use map extent", the COG/WMS/XYZ subset tools now offer "Draw on map": rubber-band a box on the map to fill the bbox (and bbox_crs 4326). This is only workable now that the panel is non-modal. Reuses the print-extent draw helper (pan/zoom suspended during the drag, box preview, Escape/blur handling); the drawn box is cleared afterward. Escape during a draw cancels the draw instead of closing the panel, and closing the panel or toggling the button aborts an in-flight draw. The fill/validation is shared with "Use map extent".
1 parent dc8d8c1 commit 89ced12

2 files changed

Lines changed: 108 additions & 22 deletions

File tree

apps/geolibre-desktop/src/components/processing/ProcessingDialog.tsx

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
Search,
4545
Server,
4646
ServerOff,
47+
SquareDashed,
4748
X,
4849
} from "lucide-react";
4950
import {
@@ -63,6 +64,7 @@ import {
6364
type FileDialogFilter,
6465
} from "../../lib/tauri-io";
6566
import { fetchableUrl } from "../../lib/url-utils";
67+
import { clearPrintExtent, drawPrintExtent } from "../../lib/print-extent";
6668
import { startGeoLibreSidecar, stopGeoLibreSidecar } from "../../lib/sidecar";
6769
import { SidecarHelpBanner } from "./SidecarHelpBanner";
6870

@@ -475,6 +477,9 @@ export function ProcessingDialog({
475477
left: number;
476478
top: number;
477479
} | null>(null);
480+
// True while a "Draw on map" rubber-band is in progress.
481+
const [drawing, setDrawing] = useState(false);
482+
const drawAbortRef = useRef<AbortController | null>(null);
478483

479484
const onDragStart = (event: React.PointerEvent) => {
480485
// Never begin a drag from an interactive control: the pointer capture would
@@ -556,17 +561,19 @@ export function ProcessingDialog({
556561
};
557562

558563
// Escape closes the panel, preserving the affordance the Radix modal provided.
559-
// Guarded on `open` so it doesn't intercept Escape for the rest of the app.
564+
// Guarded on `open` so it doesn't intercept Escape for the rest of the app, and
565+
// suppressed while drawing so Escape cancels the in-progress rubber-band (the
566+
// draw helper's own Escape handler) instead of closing the whole panel.
560567
useEffect(() => {
561568
if (!open) return;
562569
const onKey = (event: KeyboardEvent) => {
563-
if (event.key === "Escape" && !event.defaultPrevented) {
570+
if (event.key === "Escape" && !event.defaultPrevented && !drawing) {
564571
setProcessingOpen(false);
565572
}
566573
};
567574
window.addEventListener("keydown", onKey);
568575
return () => window.removeEventListener("keydown", onKey);
569-
}, [open, setProcessingOpen]);
576+
}, [open, drawing, setProcessingOpen]);
570577

571578
// Re-clamp an explicit size and dragged position when the viewport shrinks, so
572579
// a resized/moved panel can't be left oversized or partly off-screen after a
@@ -1001,21 +1008,21 @@ export function ProcessingDialog({
10011008
// view (GeoLibre#1213). The map reads in EPSG:4326, so the bbox is written as
10021009
// WGS84 `west,south,east,north` and the CRS is set to 4326 in the same gesture
10031010
// to keep the pair consistent (a stale `bbox_crs` would misread the extent).
1004-
const handleUseMapExtent = () => {
1005-
// Clear any stale banner (e.g. a prior "map not ready") so a later success
1006-
// doesn't leave it lingering, mirroring RasterSubsetPanel.handleUseView.
1011+
// Validate a WGS84 box and write it into the `bbox`/`bbox_crs` fields. Shared
1012+
// by "Use map extent" (current view) and "Draw on map" (rubber-band). Rejects
1013+
// an unnormalized box - a view/box wrapping 180° yields west >= east, and at
1014+
// low zoom (multiple world copies) getBounds() corners can fall outside
1015+
// ±180°/±90° while still ordered - which the subset extractors mis-clip or
1016+
// reject, matching RasterSubsetPanel.parseBbox's ordering + range checks.
1017+
const applyBboxExtent = (
1018+
bounds: [number, number, number, number] | undefined,
1019+
): void => {
10071020
setError(null);
1008-
const bounds = mapControllerRef.current?.readView().bbox;
10091021
if (!bounds) {
10101022
setError(t("processing.whitebox.mapExtentUnavailable"));
10111023
return;
10121024
}
10131025
const [west, south, east, north] = bounds;
1014-
// getBounds() is not normalized: a view wrapping 180° yields west >= east,
1015-
// and at low zoom (multiple world copies) the corners can fall outside
1016-
// ±180°/±90° while still ordered. Either produces a box the subset
1017-
// extractors mis-clip or reject, so block it rather than filling a silently
1018-
// wrong bbox, matching RasterSubsetPanel.parseBbox's ordering + range checks.
10191026
if (
10201027
!(west < east) ||
10211028
!(south < north) ||
@@ -1034,6 +1041,49 @@ export function ProcessingDialog({
10341041
updateValue("bbox_crs", String(4326));
10351042
};
10361043

1044+
const handleUseMapExtent = () => {
1045+
applyBboxExtent(mapControllerRef.current?.readView().bbox);
1046+
};
1047+
1048+
// Rubber-band a box on the map to fill the bbox (only workable because the
1049+
// panel is now non-modal). Reuses the print-extent draw helper, which suspends
1050+
// pan/zoom during the drag, previews the box, and handles Escape/blur. Toggling
1051+
// the button (or closing the panel) aborts an in-flight draw.
1052+
const handleDrawBbox = async () => {
1053+
const map = mapControllerRef.current?.getMap();
1054+
if (!map) {
1055+
setError(t("processing.whitebox.mapExtentUnavailable"));
1056+
return;
1057+
}
1058+
if (drawing) {
1059+
drawAbortRef.current?.abort();
1060+
return;
1061+
}
1062+
setError(null);
1063+
const controller = new AbortController();
1064+
drawAbortRef.current = controller;
1065+
setDrawing(true);
1066+
try {
1067+
const extent = await drawPrintExtent(map, { signal: controller.signal });
1068+
if (controller.signal.aborted) return;
1069+
if (extent) applyBboxExtent(extent);
1070+
} finally {
1071+
clearPrintExtent(map);
1072+
if (drawAbortRef.current === controller) {
1073+
drawAbortRef.current = null;
1074+
setDrawing(false);
1075+
}
1076+
}
1077+
};
1078+
1079+
// Abort an in-flight draw when the panel closes or the component unmounts, so
1080+
// the map isn't left in draw mode after the panel is gone.
1081+
useEffect(() => {
1082+
if (open) return;
1083+
drawAbortRef.current?.abort();
1084+
}, [open]);
1085+
useEffect(() => () => drawAbortRef.current?.abort(), []);
1086+
10371087
const handleRunLocalChange = (nextRunLocal: boolean) => {
10381088
setRunLocal(nextRunLocal);
10391089
// A `vector_out` param holds an output-format string in WASM mode but a
@@ -1605,6 +1655,12 @@ export function ProcessingDialog({
16051655
? handleUseMapExtent
16061656
: undefined
16071657
}
1658+
onDrawMapExtent={
1659+
isMapExtentParameter(selectedTool, param)
1660+
? handleDrawBbox
1661+
: undefined
1662+
}
1663+
drawingMapExtent={drawing}
16081664
/>
16091665
))
16101666
)}
@@ -1717,6 +1773,11 @@ interface ParameterFieldProps {
17171773
/** When set, renders a "Use map extent" button that fills this bbox field
17181774
* (and its companion CRS) from the current map view. */
17191775
onUseMapExtent?: () => void;
1776+
/** When set, renders a "Draw on map" button that fills this bbox field by
1777+
* rubber-banding a box on the map. */
1778+
onDrawMapExtent?: () => void;
1779+
/** Whether a draw is currently in progress (toggles the button's label/state). */
1780+
drawingMapExtent?: boolean;
17201781
toolId: string;
17211782
runLocal: boolean;
17221783
value: unknown;
@@ -1728,6 +1789,8 @@ function ParameterField({
17281789
onChange,
17291790
onPickFile,
17301791
onUseMapExtent,
1792+
onDrawMapExtent,
1793+
drawingMapExtent,
17311794
toolId,
17321795
runLocal,
17331796
value,
@@ -1788,16 +1851,36 @@ function ParameterField({
17881851
onChange(event.target.value)
17891852
}
17901853
/>
1791-
<Button
1792-
type="button"
1793-
variant="outline"
1794-
size="sm"
1795-
className="justify-self-start"
1796-
onClick={onUseMapExtent}
1797-
>
1798-
<Scan className="h-3.5 w-3.5" aria-hidden="true" />
1799-
{t("processing.whitebox.useMapExtent")}
1800-
</Button>
1854+
<div className="flex flex-wrap gap-2">
1855+
<Button
1856+
type="button"
1857+
variant="outline"
1858+
size="sm"
1859+
onClick={onUseMapExtent}
1860+
>
1861+
<Scan className="h-3.5 w-3.5" aria-hidden="true" />
1862+
{t("processing.whitebox.useMapExtent")}
1863+
</Button>
1864+
{onDrawMapExtent ? (
1865+
<Button
1866+
type="button"
1867+
variant={drawingMapExtent ? "secondary" : "outline"}
1868+
size="sm"
1869+
aria-pressed={drawingMapExtent}
1870+
onClick={onDrawMapExtent}
1871+
>
1872+
<SquareDashed className="h-3.5 w-3.5" aria-hidden="true" />
1873+
{drawingMapExtent
1874+
? t("processing.whitebox.drawingBbox")
1875+
: t("processing.whitebox.drawBbox")}
1876+
</Button>
1877+
) : null}
1878+
</div>
1879+
{drawingMapExtent ? (
1880+
<p className="text-xs text-muted-foreground">
1881+
{t("processing.whitebox.drawBboxHint")}
1882+
</p>
1883+
) : null}
18011884
</div>
18021885
) : isDataInputParameter(param) && availableLayers.length > 0 ? (
18031886
<LayerOrPathInput

apps/geolibre-desktop/src/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,9 @@
25532553
"toolsAvailable": "{{count}} tools available.",
25542554
"runtimeUnavailable": "Whitebox runtime is unavailable.",
25552555
"useMapExtent": "Use map extent",
2556+
"drawBbox": "Draw on map",
2557+
"drawingBbox": "Drawing…",
2558+
"drawBboxHint": "Drag a box on the map to set the bounding box. Press Esc to cancel.",
25562559
"mapExtentPlaceholder": "minX,minY,maxX,maxY",
25572560
"mapExtentUnavailable": "The map view is not available yet. Wait for the map to load, then try again.",
25582561
"mapExtentInvalid": "The current map view can't be used as a bounding box: it wraps the 180° meridian or extends beyond valid longitude/latitude bounds. Adjust the view, then try again."

0 commit comments

Comments
 (0)