Skip to content

Commit c9fbfec

Browse files
lhawkman27lhawkinsclaude
authored
Add lasso spatial selection tool to map charts (#354)
* feat: Add lasso select tool button to map controls Add LassoIcon SVG and lasso toggle button to MapControls component with onLassoToggle/isLassoActive props. Wire up placeholder handlers in both the Multi map and standalone GeoSetLayer views. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Wire up lasso select tool with layer picker dropdown Connect the lasso button in MapControls to actual state management in Multi.tsx. Clicking the lasso button now opens a dropdown listing all map layers; selecting a layer activates lasso mode for that layer. Includes styled dropdown panel with outside-click dismissal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Increase lasso select icon size for better visibility Bump LassoIcon SVG dimensions from 16x16 to 18x18 to improve visual consistency with other map control icons. Update wiki to document the lasso select tool in the feature comparison table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add library-based lasso drawing with freehand/polygon modes Integrate @deck.gl-community/editable-layers for proper lasso polygon drawing, replacing placeholder wiring. Users can now switch between freehand (drag) and click-to-draw polygon modes. Multi-layer charts support checkbox-based layer selection for lasso targets. Adds MapControls unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Extract useLassoSelection hook and fix lasso/ruler bugs Extract duplicated lasso state management from GeoSetLayer.tsx and Multi.tsx into a shared useLassoSelection hook. Fix escape key not exiting lasso mode in single-layer charts, and add reciprocal mode deactivation so lasso and ruler can never be active simultaneously. Replace `any` type on LassoOverlay mode state with explicit union type. Add tests for useLassoSelection (14) and useLassoLayer (7) hooks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add lasso point-in-polygon selection with CSV/Excel export Wire up lasso polygon completion to filter features using Turf.js point-in-polygon queries. Both single-layer and multi-layer charts now show a results bar with feature count and export options (CSV, Excel via xlsx). Add lassoPolygon, selectedFeatures, and clearSelection state to useLassoSelection hook. Update docs with new utility files and lasso feature in README. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Persist lasso polygon display and improve results bar UX Keep the completed lasso polygon visible (dashed outline + fill) after drawing finishes so users can see their selection area. Reposition the results bar from bottom-center to top-left, refine its styling, and auto-clear selection after CSV/Excel export. Add debug console logging for lasso selections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: Add missing lasso component entries to Development Guide Add LassoOverlay, LassoResultsBar, and useLassoSelection hook to the Key Utilities table in the Development Guide wiki page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Switch lasso to single-layer selection and geometry-aware filtering Replace multi-layer checkbox selection with single-layer radio selection for the lasso tool. Upgrade lasso filtering from centroid-based to geometry-aware intersection — points use direct containment, polygons are selected when ≥50% of their area overlaps the lasso region via @turf/intersect. Add @turf/area, @turf/intersect, @turf/boolean-within dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: Update lasso documentation for single-layer selection and geometry filtering Reflect the switch from multi-layer to single-layer selection in the Development Guide and inline comments. Update lassoSelection.ts description to mention geometry-aware intersection filtering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Anchor lasso results bar near end of drawn polygon Position the LassoResultsBar at the screen coordinates of the last lasso polygon point instead of a fixed top-left corner. Add anchorPosition state to useLassoSelection hook, project the final polygon coordinate via WebMercatorViewport, and pass it through to both single-layer and multi-layer map components. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Stabilize lasso memoization, fix broken tests, and simplify code Fix critical memoization cascade where EditableGeoJsonLayer was reconstructed every render due to unstable onPolygonComplete references. Update useLassoSelection tests to match current single-select API. Extract LassoDropdown component, remove debug console statements, unused deps, and drifting anchor positioning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Improve lasso UX with dashed outlines, activation flow, and anchored results - Add dashed stroke styling to lasso polygon and guide lines - Delay draw mode activation to prevent dropdown close click from placing a vertex - Reset drawing state when results are cleared for seamless re-lasso - Activate lasso on outside click when a layer is already selected - Anchor results bar near the end of the drawn polygon instead of fixed position - Add defaultLayerName option to auto-select preferred layer - Preserve selected layer when toggling lasso off - Rename "Freehand" mode label to "Click-and-drag" - Remove unused @turf/boolean-within dependency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: Add LassoDropdown to Development Guide key utilities table Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Highlight lasso-selected features and dim unselected layers Add a GeoJsonLayer overlay in DeckGLContainer that renders lasso-selected features with their original colors on top of dimmed data layers (opacity 0.15). Both Multi and GeoSetLayer pass selectedFeatures to the container. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add per-layer lasso-selectable option and improve lasso geometry support Add a configurable "Lasso Selectable" toggle to the layer settings popover, letting users control which layers appear in the lasso tool's dropdown. Also adds LineString/MultiLineString geometry support to lasso selection, prevents feature popups while lasso is active, removes the disabled "Download as image" placeholder, and simplifies default layer selection by removing the hardcoded defaultLayerName preference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add cursor-following hint tooltip for lasso tool Shows contextual help text near the cursor when the lasso tool is active and no features are selected yet — "Double-click or click first point to close" for polygon mode, "Click and drag to draw selection" for freehand mode. Tooltip disappears once a selection is made or the mouse leaves the map. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Simplify lasso logic, fix bugs, and add test coverage - Consolidate duplicated state resets into shared resetLassoState() - Fix category key normalization inconsistency between single/multi-layer views - Fix CSV formula injection by guarding =, +, -, @ prefixes in escapeCSV - Fix MapControls tests to match actual "Click-and-drag" label - Fix handleLassoToggle to clear selectedLassoLayerId (matching test intent) - Suppress feature click popups during lasso mode in GeoSetLayer - Guard against setState-on-unmount in Excel export async handler - Extract DASH_PROPS constant in LassoOverlay (eliminates 4x duplication) - Simplify Multi.tsx to call filterFeaturesInLasso directly (single-layer) - Remove redundant count prop from LassoResultsBar - Stabilize selectedFeaturesArr reference in DeckGLContainer with useMemo - Remove unnecessary transitive deps from root package.json - Add unit tests for lassoSelection.ts and lassoExport.ts (22 new tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Extract shared hooks, icons, and constants from lasso code Split useLassoSelection into focused useLassoActivation and useLassoResults sub-hooks. Extract useClickOutside to deduplicate outside-click handling. Consolidate SVG icons into shared icons.tsx. Replace magic color/timing values with named constants. Cache XLSX dynamic import. Add console.warn to silent catch blocks and expand test coverage for Polygon/MultiLineString geometry types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Fix lasso bugs, extract shared helper, and clean up code - Fix auto-select bug after toggle cycle by adding resetCount dep - Extract shared buildLassoResult helper to deduplicate onPolygonComplete - Remove dead code (filterMultiLayerFeaturesInLasso, LassoSelectionResult) - Add categoryName/strokeColor to GeoJsonFeature type (remove as-any casts) - Fix LassoOverlay race condition with drawModeRef during activation delay - Fix CSV formula guard to skip numeric values like -5.2 - Normalize highlight layer colors to 4-element RGBA arrays - Combine duplicate @deck.gl/layers imports in LassoOverlay Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Fix category normalization bug, replace vulnerable xlsx, and improve lasso code Normalize category keys in Multi.tsx when building hiddenCategoryKeys so hidden categories are correctly filtered during multi-layer lasso selection. Replace xlsx (CVE-2023-30533) with exceljs for Excel export. Extract shared fullReset in useLassoSelection, stabilize useClickOutside with a callback ref, add explanatory comments for eslint-disable, and use CloseIcon in LassoDropdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add circle and rectangle lasso draw modes, improve spatial accuracy Add circle and rectangle as new lasso shape options alongside freehand and polygon. Improve line/multiline selection using geometric intersection instead of vertex-only checks, handle self-intersecting freehand polygons via unkinkPolygon, and batch async filtering to avoid UI freezes on large datasets. Store the results-bar anchor as a geo coordinate so it tracks pan/zoom. Persist draw mode to sessionStorage, add ARIA attributes for accessibility, harden Excel export against formula injection, and add LassoDropdown and LassoResultsBar test coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Remove frozen feature collection, disable lasso tooltips, and fix hook ordering Remove Object.freeze from EMPTY_FEATURE_COLLECTION to avoid mutation conflicts with nebula.gl, suppress edit layer tooltips, and reorder anchorPosition useMemo to correct hook dependency placement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Preserve layer selection when toggling lasso off Change handleLassoToggle to only deactivate and clear results instead of doing a full reset, so the user's layer selection is retained. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Fix TypeScript errors in lasso test files Remove unused `style` variable in LassoResultsBar test and add explicit type annotations for `mockLayerCalls` and `props` in useLassoLayer test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Fix pre-existing TS2352 type error in legacy Contour layer Cast d.position through unknown before asserting as Position, since number[] is not directly assignable to readonly [number, number]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Add toast notifications for lasso CSV and Excel exports Show success/error toasts via useToasts when exporting lasso selections, replacing silent console.error handling. Add test coverage for all four toast scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Replace exceljs with xlsx for Excel export Switch back to xlsx for lazy-loaded Excel export in the lasso tool. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Improve lasso results bar positioning, export filtering, and UX - Clamp results bar within map bounds and flip menu when near edge - Re-enable map panning after lasso drawing completes - Use live viewport for anchor projection so bar tracks pan/zoom - Show results bar with "0 Items Selected" when polygon drawn but no features matched; hide export menu when count is zero - Filter internal layer properties (fillColor, strokeColor, etc.) from CSV/Excel exports - Add type declarations for turf and editable-layers packages - Make toast callbacks optional props instead of using useToasts hook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Guard results bar on anchor position and optimize lasso filtering Only render LassoResultsBar when anchorPosition is computed to prevent flash at (0,0). Run spatial filtering synchronously for datasets up to 5000 features to eliminate async overhead; batch with yields above that. Extract prepareLassoPolygons helper for clarity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Filter dynamic color_ prefixed keys and pointSize from lasso exports Add isInternalKey helper that checks both the static INTERNAL_KEYS set and color_ prefixed keys generated by layer processing. Add pointSize to the exclusion list. Formatting cleanup in escapeCSV. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Switch lasso export to allowlist of user-configured query columns Build exportColumnNames in transformProps from dimension, hover, feature info, color-by, point-size, and text-label columns. Pass allowlist through to LassoResultsBar and export utilities so only raw data columns appear in CSV/Excel output. Falls back to all properties when no allowlist is available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Update LassoResultsBar tests for prop-based toasts and allowedColumns Pass toast callbacks as props instead of mocking useToasts hook. Update export assertions to include the new allowedColumns parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Simplify export allowlist to hover and feature info columns Remove dimension, colorByValue, pointSize, and textLabel from export columns — users only need data columns, not rendering config inputs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: Add lasso select tool user guide to wiki pages Add a Lasso Select Tool section to GeoSet Map Layer with usage steps, draw modes, selection behavior by geometry type, export column details, and per-layer config. Add a shorter reference section to GeoSet Multi Map. Update Development Guide to note the export allowlist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Show lasso polygon area and switch toasts to antd message Display calculated area (acres/sq mi) on the lasso results bar and replace prop-based toast callbacks with antd's built-in message API for simpler, self-contained export notifications. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Suppress hover tooltips during lasso draw mode Hide hover tooltips while lasso mode is active to prevent them from flickering over the drawing surface. Clear any visible tooltip on lasso activation. Also bump the activation delay from 300ms to 450ms to better prevent the dropdown-close click from placing a vertex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: lhawkins <lhawkins@teamraft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e6a4b25 commit c9fbfec

39 files changed

Lines changed: 6627 additions & 1508 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ GeoSet bridges the gap between Superset and GIS tooling. This is accomplished by
3838
- Point size scaling by value (number or percentile bounds)
3939
- Collapsible legend with layer toggling and dynamic iconography
4040
- Native dashboard integration
41+
- Lasso select with CSV/Excel export
4142
- Text Overlay Formatting
4243

4344
<p align="center">

superset-frontend/package-lock.json

Lines changed: 2700 additions & 1407 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

superset-frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@
151151
"immer": "^10.1.1",
152152
"interweave": "^13.1.0",
153153
"jquery": "^3.7.1",
154-
"jspdf": "^3.0.1",
155154
"js-levenshtein": "^1.1.6",
156155
"js-yaml-loader": "^1.2.2",
157156
"json-bigint": "^1.0.0",
158157
"json-stringify-pretty-compact": "^2.0.0",
158+
"jspdf": "^3.0.1",
159159
"lodash": "^4.17.21",
160160
"luxon": "^3.5.0",
161161
"mapbox-gl": "^3.13.0",

superset-frontend/plugins/geoset-map-chart/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
],
2626
"dependencies": {
2727
"@ant-design/icons": "^5.6.1",
28+
"@deck.gl-community/editable-layers": "^9.2.8",
2829
"@deck.gl/aggregation-layers": "^9.1.14",
2930
"@deck.gl/core": "^9.1.14",
3031
"@deck.gl/extensions": "^9.1.14",
@@ -33,6 +34,13 @@
3334
"@mapbox/geojson-extent": "^1.0.1",
3435
"@material-ui/icons": "^4.11.3",
3536
"@math.gl/web-mercator": "^4.1.0",
37+
"@turf/area": "^7.2.0",
38+
"@turf/boolean-intersects": "^7.2.0",
39+
"@turf/boolean-point-in-polygon": "^7.2.0",
40+
"@turf/centroid": "^7.2.0",
41+
"@turf/helpers": "^7.2.0",
42+
"@turf/intersect": "^7.2.0",
43+
"@turf/unkink-polygon": "^7.2.0",
3644
"@types/d3-array": "^2.0.0",
3745
"@types/d3-interpolate": "^3.0.4",
3846
"@types/d3-scale-chromatic": "^3.1.0",

superset-frontend/plugins/geoset-map-chart/src/DeckGLContainer.tsx

Lines changed: 130 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import { StaticMap, MapRef } from 'react-map-gl';
3535
import DeckGL from '@deck.gl/react';
3636
import type { Deck, Layer } from '@deck.gl/core';
37+
import { GeoJsonLayer } from '@deck.gl/layers';
3738
import { JsonObject, JsonValue, styled } from '@superset-ui/core';
3839
import Tooltip, { TooltipProps } from './components/Tooltip';
3940
import 'mapbox-gl/dist/mapbox-gl.css';
@@ -42,8 +43,9 @@ import {
4243
isValidViewport,
4344
toNumericViewport,
4445
} from './utils/fitViewport';
45-
import { LayerState } from './types';
46+
import { GeoJsonFeature, LassoDrawMode, LayerState } from './types';
4647
import { MeasureState, useMeasureLayers } from './components/MeasureOverlay';
48+
import { LASSO_CURSOR, useLassoLayer } from './components/useLassoLayer';
4749
import { Coordinate } from './utils/measureDistance';
4850

4951
const TICK = 250; // milliseconds
@@ -65,6 +67,11 @@ export type DeckGLContainerProps = {
6567
onMeasureDragStart?: (coordinate: Coordinate) => void;
6668
onMeasureDrag?: (coordinate: Coordinate) => void;
6769
onMeasureDragEnd?: (coordinate: Coordinate) => void;
70+
lassoIsActive?: boolean;
71+
lassoDrawMode?: LassoDrawMode;
72+
lassoPolygon?: Coordinate[] | null;
73+
onLassoComplete?: (polygon: Coordinate[]) => void;
74+
selectedFeatures?: GeoJsonFeature[];
6875
onEmptyClick?: () => void;
6976
};
7077

@@ -106,6 +113,27 @@ const MeasureTooltip = styled.div`
106113
}
107114
`;
108115

116+
const LASSO_HINT_TEXT: Record<LassoDrawMode, string> = {
117+
freehand: 'Click and drag to draw selection',
118+
polygon: 'Double-click or click first point to close',
119+
circle: 'Click and drag to draw circle',
120+
rectangle: 'Click and drag to draw rectangle',
121+
};
122+
123+
const LassoHintTooltip = styled.div`
124+
position: absolute;
125+
background: ${({ theme }) => theme.colorBgElevated};
126+
color: ${({ theme }) => theme.colorTextSecondary};
127+
border: 1px solid ${({ theme }) => theme.colorBorder};
128+
padding: 4px 8px;
129+
border-radius: 4px;
130+
font-size: 11px;
131+
pointer-events: none;
132+
z-index: 100;
133+
white-space: nowrap;
134+
transform: translate(16px, 16px);
135+
`;
136+
109137
// Custom ruler cursor as a data URI SVG
110138
// The cursor is a small ruler icon with a crosshair at the click point (top-left)
111139
// Uses white fill with black stroke for visibility on any background
@@ -186,6 +214,10 @@ export const DeckGLContainer = memo(
186214
const currentViewport = useRef<Viewport>(props.viewport);
187215
const pendingSaveTime = useRef<number | null>(null);
188216
const [tooltip, setTooltip] = useState<TooltipProps['tooltip']>(null);
217+
const [lassoMousePos, setLassoMousePos] = useState<{
218+
x: number;
219+
y: number;
220+
} | null>(null);
189221
const [viewState, setViewState] = useState(() => props.viewport);
190222
const [mapReady, setMapReady] = useState(false);
191223

@@ -369,16 +401,72 @@ export const DeckGLContainer = memo(
369401
distance,
370402
} = useMeasureLayers(measureState, project);
371403

404+
// Get lasso editable layer (library-based drawing)
405+
const lassoIsActive = props.lassoIsActive ?? false;
406+
const lassoDrawMode = props.lassoDrawMode ?? 'freehand';
407+
const lassoPolygon = props.lassoPolygon ?? null;
408+
const noopLassoComplete = useCallback(() => {}, []);
409+
const { layers: lassoLayers } = useLassoLayer(
410+
lassoIsActive,
411+
props.onLassoComplete ?? noopLassoComplete,
412+
lassoDrawMode,
413+
lassoPolygon,
414+
);
415+
416+
const selectedFeaturesArr = useMemo(
417+
() => props.selectedFeatures ?? [],
418+
[props.selectedFeatures],
419+
);
420+
const hasSelection = selectedFeaturesArr.length > 0;
421+
422+
// Build a highlight layer from lasso-selected features
423+
const highlightLayer = useMemo(() => {
424+
if (!hasSelection) return [];
425+
return [
426+
new GeoJsonLayer({
427+
id: 'lasso-highlight',
428+
data: {
429+
type: 'FeatureCollection' as const,
430+
features: selectedFeaturesArr,
431+
},
432+
getFillColor: (f: any) => {
433+
const c = f.color ?? f.properties?.color;
434+
if (!c) return [255, 200, 0, 200];
435+
return c.length >= 4 ? c : [...c, 200];
436+
},
437+
getLineColor: (f: any) => {
438+
const c = f.strokeColor ?? f.properties?.strokeColor;
439+
if (!c) return [40, 40, 40, 220];
440+
return c.length >= 4 ? c : [...c, 220];
441+
},
442+
getPointRadius: (f: any) => f.sizeValue ?? 4,
443+
pointRadiusMinPixels: 4,
444+
lineWidthMinPixels: 1,
445+
pickable: false,
446+
}),
447+
];
448+
}, [hasSelection, selectedFeaturesArr]);
449+
372450
const allLayers = useMemo(() => {
373451
if (!layerStates || layerStates.length === 0) {
374-
return measureLayers as Layer[];
452+
return [...measureLayers, ...highlightLayer, ...lassoLayers] as Layer[];
375453
}
376-
const layers = layerStates
454+
let layers = layerStates
377455
.map(ls => ls?.layer)
378456
.filter(Boolean) as Layer[];
379457

380-
return [...layers, ...measureLayers] as Layer[];
381-
}, [layerStates, measureLayers]);
458+
// Dim all data layers when there's an active lasso selection
459+
if (hasSelection) {
460+
layers = layers.map(l => l.clone({ opacity: 0.15 }));
461+
}
462+
463+
return [
464+
...layers,
465+
...highlightLayer,
466+
...measureLayers,
467+
...lassoLayers,
468+
] as Layer[];
469+
}, [layerStates, measureLayers, lassoLayers, highlightLayer, hasSelection]);
382470

383471
useEffect(() => {
384472
if (!props.layerStates) return;
@@ -539,6 +627,7 @@ export const DeckGLContainer = memo(
539627
// Clear tooltip when mouse leaves the map container
540628
const handleMouseLeave = useCallback(() => {
541629
setTooltip(null);
630+
setLassoMousePos(null);
542631
}, []);
543632

544633
// Track drag state for measurement - use refs to avoid stale closure issues
@@ -551,6 +640,8 @@ export const DeckGLContainer = memo(
551640
(info: any) => {
552641
// Don't handle click if a drag was in progress (threshold was exceeded)
553642
if (measureDragRef.current) return;
643+
// Suppress normal clicks during lasso mode
644+
if (lassoIsActive) return;
554645
if (measureState.isActive && onMeasureClick && info.coordinate) {
555646
onMeasureClick(info.coordinate as Coordinate);
556647
}
@@ -559,21 +650,25 @@ export const DeckGLContainer = memo(
559650
onEmptyClick();
560651
}
561652
},
562-
[measureState.isActive, onMeasureClick, onEmptyClick],
653+
[measureState.isActive, lassoIsActive, onMeasureClick, onEmptyClick],
563654
);
564655

565-
// Cursor style for measure mode - use custom ruler cursor
656+
// Cursor style for measure/lasso modes
566657
const getCursor = useCallback(
567-
() => (measureState.isActive ? RULER_CURSOR : 'grab'),
568-
[measureState.isActive],
658+
() =>
659+
lassoIsActive
660+
? LASSO_CURSOR
661+
: measureState.isActive
662+
? RULER_CURSOR
663+
: 'grab',
664+
[measureState.isActive, lassoIsActive],
569665
);
570666

571667
// Handle mouse down for drag-to-measure
572668
const handleMouseDown = useCallback(
573669
(e: React.MouseEvent) => {
574670
if (!measureState.isActive) return;
575671

576-
// Store initial position - don't start drag yet
577672
const rect = e.currentTarget.getBoundingClientRect();
578673
mouseDownPosRef.current = {
579674
x: e.clientX - rect.left,
@@ -584,9 +679,18 @@ export const DeckGLContainer = memo(
584679
[measureState.isActive],
585680
);
586681

587-
// Handle mouse move for drag-to-measure
682+
// Handle mouse move for drag-to-measure and lasso hint tooltip
588683
const handleMouseMove = useCallback(
589684
(e: React.MouseEvent) => {
685+
// Track cursor for lasso hint tooltip
686+
if (lassoIsActive) {
687+
const rect = e.currentTarget.getBoundingClientRect();
688+
setLassoMousePos({
689+
x: e.clientX - rect.left,
690+
y: e.clientY - rect.top,
691+
});
692+
}
693+
590694
if (!measureState.isActive || !mouseDownPosRef.current) return;
591695

592696
const map = mapRef.current?.getMap();
@@ -596,38 +700,32 @@ export const DeckGLContainer = memo(
596700
const x = e.clientX - rect.left;
597701
const y = e.clientY - rect.top;
598702

599-
// Check if we've exceeded drag threshold
600703
if (!measureDragRef.current) {
601704
const dx = x - mouseDownPosRef.current.x;
602705
const dy = y - mouseDownPosRef.current.y;
603-
const distance = Math.sqrt(dx * dx + dy * dy);
604-
605-
if (distance >= DRAG_THRESHOLD) {
606-
// Start drag from the initial mouse down position
706+
if (Math.sqrt(dx * dx + dy * dy) >= DRAG_THRESHOLD) {
607707
measureDragRef.current = true;
608708
const startLngLat = map.unproject([
609709
mouseDownPosRef.current.x,
610710
mouseDownPosRef.current.y,
611711
]);
612712
props.onMeasureDragStart?.([startLngLat.lng, startLngLat.lat]);
613713
} else {
614-
return; // Haven't moved enough yet
714+
return;
615715
}
616716
}
617717

618-
// Continue drag
619718
const lngLat = map.unproject([x, y]);
620719
props.onMeasureDrag?.([lngLat.lng, lngLat.lat]);
621720
},
622-
[measureState.isActive, props.onMeasureDragStart, props.onMeasureDrag],
721+
[measureState.isActive, lassoIsActive, props.onMeasureDragStart, props.onMeasureDrag],
623722
);
624723

625724
// Handle mouse up for drag-to-measure
626725
const handleMouseUp = useCallback(
627726
(e: React.MouseEvent) => {
628727
if (!measureState.isActive) return;
629728

630-
// Only finalize drag if we actually started dragging
631729
if (measureDragRef.current) {
632730
const map = mapRef.current?.getMap();
633731
if (map) {
@@ -639,15 +737,17 @@ export const DeckGLContainer = memo(
639737
}
640738
}
641739

642-
// Reset refs
643740
measureDragRef.current = false;
644741
mouseDownPosRef.current = null;
645742
},
646743
[measureState.isActive, props.onMeasureDragEnd],
647744
);
648745

649-
// Disable map panning when in measure mode
650-
const controllerOptions = measureState.isActive
746+
// Disable map panning when actively drawing (measure or lasso).
747+
// Re-enable after lasso drawing completes so user can pan/zoom the selection.
748+
const isActivelyDrawing =
749+
measureState.isActive || (lassoIsActive && !lassoPolygon);
750+
const controllerOptions = isActivelyDrawing
651751
? { dragPan: false, dragRotate: false }
652752
: true;
653753

@@ -740,6 +840,13 @@ export const DeckGLContainer = memo(
740840
{distance}
741841
</MeasureTooltip>
742842
)}
843+
{lassoIsActive && lassoMousePos && !selectedFeaturesArr.length && (
844+
<LassoHintTooltip
845+
style={{ left: lassoMousePos.x, top: lassoMousePos.y }}
846+
>
847+
{LASSO_HINT_TEXT[lassoDrawMode]}
848+
</LassoHintTooltip>
849+
)}
743850
<ScaleControlContainer>
744851
<ScaleBar $width={scaleInfo.width}>{scaleInfo.label}</ScaleBar>
745852
</ScaleControlContainer>

0 commit comments

Comments
 (0)