Skip to content

Commit 0eae29b

Browse files
authored
feat: Streamlined rectangle drag-to-filter for map layers (#3402)
* feat: streamlined rectangle selection for filtering map layers Improve the "Draw on Map" rectangle selection UX with a smoother click-and-drag workflow that automatically filters all visible layers. - Default to "Draw Rectangle" mode when toggling on "Draw on Map" - Enable drag-to-draw for rectangles (click-hold-drag-release) - Auto-apply polygon filter to all visible layers on rectangle completion - Switch to edit/move mode after drawing so users can reposition - Update tooltip to reflect new drag interaction - Preserve original click-to-draw behavior for polygon drawing Made-with: Cursor * feat: support both click-move-click and drag-to-draw for rectangle selection Add DrawRectangleModeExtended that removes the mutually exclusive dragToDraw guards so both gestures work: existing users can still click-move-click while new users can click-drag-release. Made-with: Cursor * feat: enhance polygon filter functionality with layer visibility support Updated the setPolygonFilterAllLayersUpdater function to allow filtering based on visible layer IDs. This change improves the flexibility of the polygon filter by enabling it to utilize a provided list of visible layers, falling back to the existing visible layers in the state if the list is not available. - Changed variable declaration from 'let' to 'const' for newState. - Introduced a check for visibleLayerIds to streamline layer filtering.
1 parent a9ca4a8 commit 0eae29b

10 files changed

Lines changed: 125 additions & 9 deletions

File tree

src/actions/src/action-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export const ActionTypes = {
189189
// geo-operations
190190
SET_FEATURES: `${ACTION_PREFIX}SET_FEATURES`,
191191
SET_POLYGON_FILTER_LAYER: `${ACTION_PREFIX}SET_POLYGON_FILTER_LAYER`,
192+
SET_POLYGON_FILTER_ALL_LAYERS: `${ACTION_PREFIX}SET_POLYGON_FILTER_ALL_LAYERS`,
192193
DELETE_FEATURE: `${ACTION_PREFIX}DELETE_FEATURE`,
193194
TOGGLE_EDITOR_VISIBILITY: `${ACTION_PREFIX}TOGGLE_EDITOR_VISIBILITY`,
194195

src/actions/src/vis-state-actions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,27 @@ export function setPolygonFilterLayer(
14011401
};
14021402
}
14031403

1404+
export type SetPolygonFilterAllLayersUpdaterAction = {
1405+
feature: Feature;
1406+
};
1407+
/**
1408+
* Apply the provided feature as a polygon filter to all layers.
1409+
* @memberof visStateActions
1410+
* @param feature
1411+
* @returns action
1412+
*/
1413+
export function setPolygonFilterAllLayers(
1414+
feature: Feature
1415+
): Merge<
1416+
SetPolygonFilterAllLayersUpdaterAction,
1417+
{type: typeof ActionTypes.SET_POLYGON_FILTER_ALL_LAYERS}
1418+
> {
1419+
return {
1420+
type: ActionTypes.SET_POLYGON_FILTER_ALL_LAYERS,
1421+
feature
1422+
};
1423+
}
1424+
14041425
export type SetSelectedFeatureUpdaterAction = {
14051426
feature: Feature | null;
14061427
selectionContext?: FeatureSelectionContext;

src/components/src/map-container.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ export default function MapContainerFactory(
908908
editorMenuActive,
909909
onSetFeatures: setFeatures,
910910
setSelectedFeature,
911+
onApplyPolygonFilterAll: visStateActions.setPolygonFilterAllLayers,
911912
// @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
912913
featureCollection: this.featureCollectionSelector(this.props),
913914
selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(

src/components/src/map/map-draw-panel.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ function MapDrawPanelFactory(
4646
actionIcons = defaultActionIcons
4747
}) => {
4848
const isActive = mapControls?.mapDraw?.active;
49-
const onToggleMenuPanel = useCallback(
50-
() => onToggleMapControl('mapDraw'),
51-
[onToggleMapControl]
52-
);
49+
const onToggleMenuPanel = useCallback(() => {
50+
if (!isActive) {
51+
onSetEditorMode(EDITOR_MODES.DRAW_RECTANGLE);
52+
}
53+
onToggleMapControl('mapDraw');
54+
}, [isActive, onToggleMapControl, onSetEditorMode]);
5355
if (!mapControls?.mapDraw?.show) {
5456
return null;
5557
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright contributors to the kepler.gl project
3+
4+
import {DrawRectangleMode} from '@deck.gl-community/editable-layers';
5+
6+
/**
7+
* Extends DrawRectangleMode to support both click-move-click and
8+
* click-drag-release interactions for drawing rectangles.
9+
*
10+
* The base TwoClickPolygonMode treats these as mutually exclusive via
11+
* the `dragToDraw` modeConfig flag. This subclass removes those guards
12+
* so both gestures work simultaneously:
13+
* - Click → move → click (original two-click behavior)
14+
* - Mousedown → drag → mouseup (drag-to-draw behavior)
15+
*/
16+
export class DrawRectangleModeExtended extends DrawRectangleMode {
17+
handleClick(event, props) {
18+
// Always handle clicks (no dragToDraw guard)
19+
this.addClickSequence(event);
20+
this.checkAndFinishPolygon(props);
21+
}
22+
23+
handleStartDragging(event, _props) {
24+
// Always handle drag start (no dragToDraw guard)
25+
this.addClickSequence(event);
26+
event.cancelPan();
27+
}
28+
29+
handleStopDragging(event, props) {
30+
// Always handle drag stop (no dragToDraw guard)
31+
this.addClickSequence(event);
32+
this.checkAndFinishPolygon(props);
33+
}
34+
}

src/layers/src/editor-layer/editor-layer-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,12 @@ export function getTooltip(
162162
return null;
163163
}
164164

165-
return getTooltipObject('Click to start new feature', theme, {
165+
const tooltipText =
166+
editor.mode === EDITOR_MODES.DRAW_RECTANGLE
167+
? 'Click or drag to draw rectangle'
168+
: 'Click to start new feature';
169+
170+
return getTooltipObject(tooltipText, theme, {
166171
leftOfCursor: closeToLeftEdge,
167172
aboveCursor: closeToBottomEdge
168173
});

src/layers/src/editor-layer/editor-layer.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
DrawPolygonMode,
88
TranslateMode,
99
CompositeMode,
10-
DrawRectangleMode,
1110
GeoJsonEditMode
1211
} from '@deck.gl-community/editable-layers';
1312
import {PathStyleExtension} from '@deck.gl/extensions';
@@ -19,6 +18,7 @@ import {generateHashId} from '@kepler.gl/common-utils';
1918
import {EDIT_TYPES} from './constants';
2019
import {LINE_STYLE, FEATURE_STYLE, EDIT_HANDLE_STYLE} from './feature-styles';
2120
import {ModifyModeExtended} from './modify-mode-extended';
21+
import {DrawRectangleModeExtended} from './draw-rectangle-mode-extended';
2222
import {isDrawingActive} from './editor-layer-utils';
2323

2424
const DEFAULT_COMPOSITE_MODE = new CompositeMode([
@@ -31,6 +31,7 @@ export type GetEditorLayerProps = {
3131
editor: Editor;
3232
onSetFeatures: (features: Feature[]) => any;
3333
setSelectedFeature: (feature: Feature | null, selectionContext?: FeatureSelectionContext) => any;
34+
onApplyPolygonFilterAll?: (feature: Feature) => any;
3435
viewport: Viewport;
3536
featureCollection: {
3637
type: string;
@@ -55,6 +56,7 @@ export function getEditorLayer({
5556
editor,
5657
onSetFeatures,
5758
setSelectedFeature,
59+
onApplyPolygonFilterAll,
5860
featureCollection,
5961
selectedFeatureIndexes,
6062
viewport
@@ -66,7 +68,7 @@ export function getEditorLayer({
6668
// @ts-ignore
6769
if (editorMode === EDITOR_MODES.DRAW_POLYGON) mode = DrawPolygonMode;
6870
// @ts-ignore
69-
else if (editorMode === EDITOR_MODES.DRAW_RECTANGLE) mode = DrawRectangleMode;
71+
else if (editorMode === EDITOR_MODES.DRAW_RECTANGLE) mode = DrawRectangleModeExtended;
7072
}
7173

7274
// @ts-ignore
@@ -100,7 +102,13 @@ export function getEditorLayer({
100102
if (lastFeature.properties) lastFeature.properties.isClosed = true;
101103
lastFeature.id = generateHashId(6);
102104
onSetFeatures(updatedData.features as unknown as Feature[]);
103-
setSelectedFeature(lastFeature as unknown as Feature);
105+
106+
const isRectangle = lastFeature.properties?.shape === 'Rectangle';
107+
if (isRectangle && onApplyPolygonFilterAll) {
108+
onApplyPolygonFilterAll(lastFeature as unknown as Feature);
109+
} else {
110+
setSelectedFeature(lastFeature as unknown as Feature);
111+
}
104112
}
105113
break;
106114
}

src/reducers/src/layer-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ export type ComputeDeckLayersProps = {
388388
feature: Feature | null,
389389
selectionContext?: FeatureSelectionContext
390390
) => any;
391+
onApplyPolygonFilterAll?: (feature: Feature) => any;
391392
featureCollection: {
392393
type: string;
393394
features: Feature[];

src/reducers/src/vis-state-updaters.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3188,7 +3188,12 @@ export function setFeaturesUpdater(
31883188
...state.editor,
31893189
// only save none filter features to editor
31903190
features: features.filter(f => !getFilterIdInFeature(f)),
3191-
mode: lastFeature && lastFeature.properties?.isClosed ? EDITOR_MODES.EDIT : state.editor.mode
3191+
mode:
3192+
lastFeature && lastFeature.properties?.isClosed
3193+
? lastFeature.properties?.shape === 'Rectangle'
3194+
? state.editor.mode
3195+
: EDITOR_MODES.EDIT
3196+
: state.editor.mode
31923197
}
31933198
};
31943199

@@ -3361,6 +3366,42 @@ export function setPolygonFilterLayerUpdater(
33613366
});
33623367
}
33633368

3369+
/**
3370+
* Apply polygon filter to all visible layers
3371+
* @memberof visStateUpdaters
3372+
*/
3373+
export function setPolygonFilterAllLayersUpdater(
3374+
state: VisState,
3375+
payload: VisStateActions.SetPolygonFilterAllLayersUpdaterAction
3376+
): VisState {
3377+
const {feature} = payload;
3378+
3379+
const newFilter = generatePolygonFilter([], feature);
3380+
const filterIdx = state.filters.length;
3381+
3382+
const newState: VisState = {
3383+
...state,
3384+
filters: [...state.filters, newFilter],
3385+
editor: {
3386+
...state.editor,
3387+
features: state.editor.features.filter(f => f.id !== feature.id),
3388+
selectedFeature: newFilter.value,
3389+
mode: EDITOR_MODES.EDIT
3390+
}
3391+
};
3392+
3393+
const visibleLayerIds = get(payload, 'visibleLayerIds');
3394+
const allLayerIds = Array.isArray(visibleLayerIds)
3395+
? visibleLayerIds
3396+
: state.layers.filter(l => l.config.isVisible).map(l => l.id);
3397+
3398+
return setFilterUpdater(newState, {
3399+
idx: filterIdx,
3400+
prop: 'layerId',
3401+
value: allLayerIds
3402+
});
3403+
}
3404+
33643405
/**
33653406
* @memberof visStateUpdaters
33663407
* @public

src/reducers/src/vis-state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ const actionHandler = {
121121

122122
[ActionTypes.SET_POLYGON_FILTER_LAYER]: visStateUpdaters.setPolygonFilterLayerUpdater,
123123

124+
[ActionTypes.SET_POLYGON_FILTER_ALL_LAYERS]: visStateUpdaters.setPolygonFilterAllLayersUpdater,
125+
124126
[ActionTypes.SET_SELECTED_FEATURE]: visStateUpdaters.setSelectedFeatureUpdater,
125127

126128
[ActionTypes.SET_EDITOR_MODE]: visStateUpdaters.setEditorModeUpdater,

0 commit comments

Comments
 (0)