Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {GeolocateWidget as _GeolocateWidget} from './geolocate-widget';
// Utility widgets
export {FullscreenWidget} from './fullscreen-widget';
export {ScreenshotWidget} from './screenshot-widget';
export {SelectionWidget} from './selection-widget';
export {LoadingWidget as _LoadingWidget} from './loading-widget';
export {FpsWidget as _FpsWidget} from './fps-widget';
export {ThemeWidget as _ThemeWidget} from './theme-widget';
Expand All @@ -29,6 +30,7 @@ export type {FullscreenWidgetProps} from './fullscreen-widget';
export type {CompassWidgetProps} from './compass-widget';
export type {ZoomWidgetProps} from './zoom-widget';
export type {ScreenshotWidgetProps} from './screenshot-widget';
export type {SelectionWidgetProps} from './selection-widget';
export type {ResetViewWidgetProps} from './reset-view-widget';
export type {GeolocateWidgetProps} from './geolocate-widget';
export type {LoadingWidgetProps} from './loading-widget';
Expand Down
116 changes: 116 additions & 0 deletions modules/widgets/src/selection-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {Widget, WidgetProps, type WidgetPlacement, type Layer} from '@deck.gl/core';
import {render} from 'preact';
import {IconButton} from './lib/components/icon-button';

export type SelectionWidgetProps = WidgetProps & {
/** Widget positioning within the view. Default 'top-left'. */
placement?: WidgetPlacement;
/** Tooltip message */
label?: string;
/** Type of selection interaction. Defaults to 'rectangle'. */
selectionType?: 'rectangle' | 'lasso';
layerIds?: string[];
/** Called when the selection is completed. */
onSelect?: (objects: any[], polygon?: number[][]) => void;
SelectionLayer: Layer;
};

/**
* Adds a button that lets the user draw a selection region within the view.
* Once a selection is made the layer is removed and {@link onSelect} is called.
*/
export class SelectionWidget extends Widget<SelectionWidgetProps> {
static defaultProps: Required<SelectionWidgetProps> = {
...Widget.defaultProps,
id: 'selection',
placement: 'top-left',
label: 'Select',
selectionType: 'rectangle',
onSelect: undefined!,
SelectionLayer: undefined!
};

className = 'deck-widget-selection';
placement: WidgetPlacement = 'top-left';
private active = false;
private selectionLayer: Layer | null = null;

constructor(props: SelectionWidgetProps) {
super(props, SelectionWidget.defaultProps);
this.placement = props.placement ?? this.placement;
}

setProps(props: Partial<SelectionWidgetProps>): void {
this.placement = props.placement ?? this.placement;
super.setProps(props);
}

onAdd(): void {
this.updateHTML();
}

onRemove(): void {
this.stopSelection();
}

onRenderHTML(rootElement: HTMLElement): void {
render(
<IconButton
className={this.active ? 'deck-widget-selection-active' : 'deck-widget-selection'}
label={this.props.label}
onClick={this.handleClick}
/>,
rootElement
);
}

private handleClick = (): void => {
if (this.active) {
this.stopSelection();
} else {
this.startSelection();
}
};

private startSelection(): void {
const deck = this.deck;
if (!deck || this.active) {
return;
}

this.active = true;
const layerId = `${this.id}-layer`;

// @ts-expect-error
this.selectionLayer = new this.props.SelectionLayer({
id: layerId,
selectionType: this.props.selectionType,
layerIds: this.props.layerIds,
onSelect: (info: any) => {
this.props.onSelect?.(info, info.polygon);
this.stopSelection();
}
});

deck.setProps({
layers: [...(deck.props.layers || []), this.selectionLayer]
});

this.updateHTML();
}

private stopSelection(): void {
const deck = this.deck;
if (!deck || !this.selectionLayer) {
this.active = false;
this.updateHTML();
return;
}

const layers = (deck.props.layers || []).filter(l => l !== this.selectionLayer);
deck.setProps({layers});
this.selectionLayer = null;
this.active = false;
this.updateHTML();
}
}
30 changes: 29 additions & 1 deletion test/apps/widgets-example-9.2/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
} from '@deck.gl/widgets';
import '@deck.gl/widgets/stylesheet.css';

import {SelectionWidget} from '@deck.gl/widgets';
import {SelectionLayer} from '@deck.gl-community/editable-layers';

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
const COUNTRIES =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line
Expand Down Expand Up @@ -138,7 +141,32 @@ const deck = new Deck({
orientation: 'vertical',
onChange: ratio => deck.setProps({views: getViewsForSplit(ratio * 100)})
}),
new _StatsWidget({type: 'luma'})
new _StatsWidget({type: 'luma'}),
new SelectionWidget({
viewId: 'left-map',
SelectionLayer,
layerIds: ['airports'],
onSelect: (info: PickingInfo) => {
console.log('Selected:', info);
// if (info.layer?.id === 'airports' && info.object) {
// return {
// id: info.object.properties.abbrev,
// name: info.object.properties.name,
// type: info.object.properties.type,
// featureclass: info.object.properties.featureclass,
// location: info.object.properties.location
// };
// }
// return null;
}
// onSelectionChange: (info: PickingInfo) => {
// if (info.layer?.id === 'airports' && info.object) {
// console.log('Selected:', info.object.properties.name);
// } else {
// console.log('Selection cleared');
// }
// }
})
]
});

Expand Down
5 changes: 4 additions & 1 deletion test/apps/widgets-example-9.2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"build": "vite build"
},
"dependencies": {
"@deck.gl-community/editable-layers": "^9.0.3",
"@deck.gl-community/layers": "^9.0.3",
"@deck.gl/core": "^9.1.0",
"@deck.gl/layers": "^9.1.0",
"@deck.gl/widgets": "^9.1.0"
"@deck.gl/widgets": "^9.1.0",
"preact": "^10.17.0"
},
"devDependencies": {
"vite": "^4.0.0"
Expand Down
24 changes: 24 additions & 0 deletions test/modules/widgets/selection-widget.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'tape-promise/tape';
import {SelectionWidget} from '@deck.gl/widgets';

class DummyDeck {
props: {layers: any[]} = {layers: []};
setProps(props: any) {
this.props = {...this.props, ...props};
}
}

test('SelectionWidget adds and removes SelectionLayer', t => {
const deck = new DummyDeck();
const widget = new SelectionWidget({id: 'sel', label: 'Select'});
widget.onAdd({deck: deck as any, viewId: null});

(widget as any).startSelection();
t.ok((widget as any).selectionLayer, 'layer is created');
t.is(deck.props.layers.includes((widget as any).selectionLayer), true, 'layer added');

(widget as any).stopSelection();
t.is((widget as any).selectionLayer, null, 'layer cleared');
t.is(deck.props.layers.includes((widget as any).selectionLayer), false, 'layer removed');
t.end();
});
Loading