Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add select interaction #277

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
139 changes: 139 additions & 0 deletions src/interaction/RSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React from 'react';
import {Collection, Feature, MapBrowserEvent} from 'ol';
import {default as Select, SelectEvent} from 'ol/interaction/Select';
import {StyleLike} from 'ol/style/Style';
import Geometry from 'ol/geom/Geometry';
import Layer from 'ol/layer/Layer';
import RenderFeature from 'ol/render/Feature';

import {default as RBaseInteraction} from './RBaseInteraction';

/**
* @propsfor RSelect
*/
export interface RSelectProps {
/**
* An optional OpenLayers condition to allow selection of the feature.
* Use this if you want to use different events for add and remove instead
* of toggle.
* @default never
*/
addCondition?: (e: MapBrowserEvent<UIEvent>) => boolean;

/**
* An optional OpenLayers condition.
* Clicking on a feature selects that feature and removes any that were in
* the selection.
* Clicking outside any feature removes all from the selection.
* See toggle, add, remove options for adding/removing extra features
* to/from the selection.
* @default singleClick
*/
condition?: (e: MapBrowserEvent<UIEvent>) => boolean;

/**
* If placed inside a vector layer, RSelect will only select features
* from that layer.
*
* If placed directly inside a map:
* Features from this list of layers may be selected.
* Alternatively, a filter function can be provided.
* The function will be called for each layer in the map and should
* return true for layers that you want to be selectable.
* If the option is absent, all visible layers will be considered
* selectable.
*/
layers?: Layer[] | ((f: Layer) => boolean);

/**
* Style for rendering while selected, supports only Openlayers styles.
* Once the interaction is finished, the resulting feature will adopt
* the style of its layer.
*/
style?: StyleLike;

/**
* An optional OpenLayers condition to allow deselection of the feature.
* Use this if you want to use different events for add and remove instead
* of toggle.
* @default never
*/
removeCondition?: (e: MapBrowserEvent<UIEvent>) => boolean;

/**
* An optional OpenLayers condition to allow toggling the selection.
* This is in addition to the condition event. See add and remove if
* you want to use different events instead of a toggle.
* @default shiftKeyOnly */
toggleCondition?: (e: MapBrowserEvent<UIEvent>) => boolean;

/**
* A boolean that determines if the default behaviour should select
* only single features or all (overlapping) features at the clicked
* map position.
* The default of false means single select.
* @default false
*/
multi?: boolean;

/**
* Collection where the interaction will place selected features.
* If not set the interaction will create a collection.
*/
features?: Collection<Feature<Geometry>> | Feature<Geometry>;

/**
* A function that takes a Feature and a Layer and returns true if the
* feature may be selected or false otherwise.
*/
filter?: (Feature: Feature<Geometry> | RenderFeature, layer: Layer) => boolean;

/**
* Hit-detection tolerance.
* Pixels inside the radius around the given position will be checked for
* features.
* @default 0
*/
hitTolerance?: number;

/**
* Triggered when feature(s) has been (de)selected.
*/
onSelect?: (this: RSelect, e: SelectEvent) => void;
}

/**
* Interaction for selecting vector features.
* When placed in a vector layer, the interaction will only select features
* from that layer. When placed directly inside a map, features from all
* visible layers may be selected, unless a filter array/function is provided.
*/
export default class RSelect extends RBaseInteraction<RSelectProps> {
protected static classProps = [
'addCondition',
'condition',
'layers',
'style',
'removeCondition',
'toggleCondition',
'multi',
'features',
'filter',
'hitTolerance'
];
ol: Select;

createOL(props: RSelectProps): Select {
this.classProps = RSelect.classProps;
let layers: typeof props.layers;
if (this.context?.vectorlayer) {
layers = [this.context.vectorlayer];
}
return new Select({
layers,
...Object.keys(props)
.filter((p) => this.classProps.includes(p))
.reduce((ac, p) => ({...ac, [p]: props[p]}), {})
});
}
}
1 change: 1 addition & 0 deletions src/interaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export {default as RPinchRotate} from './RPinchRotate';
export {default as RPinchZoom} from './RPinchZoom';
export {default as RKeyboardPan} from './RKeyboardPan';
export {default as RKeyboardZoom} from './RKeyboardZoom';
export {default as RSelect} from './RSelect';
73 changes: 73 additions & 0 deletions test/RInteractions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,79 @@
});
});

describe('<RSelect>', () => {
it('should create a Select interaction', async () => {
const ref = React.createRef() as React.RefObject<RInteraction.RSelect>;
const {container, unmount} = render(
<RMap {...common.mapProps}>
<RLayerVector>
<RInteraction.RSelect ref={ref} />
</RLayerVector>
</RMap>
);
expect(container.innerHTML).toMatchSnapshot();
expect(ref.current).toBeInstanceOf(RInteraction.RSelect);
unmount();
});
it('should support styles', async () => {
const ref = React.createRef() as React.RefObject<RInteraction.RSelect>;
const style = new Style({
stroke: new Stroke({
color: 'red',
width: 3
})
});
const {container, unmount} = render(
<RMap {...common.mapProps}>
<RLayerVector>
<RInteraction.RSelect ref={ref} style={style} />
</RLayerVector>
</RMap>
);
expect(container.innerHTML).toMatchSnapshot();
expect(ref.current).toBeInstanceOf(RInteraction.RSelect);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const styleResult = (ref.current?.ol as any).style_ as Style;
expect(styleResult.getStroke()?.getWidth?.()).toBe(3);
unmount();
});
it('can be used directly inside a map', async () => {
const ref = React.createRef() as React.RefObject<RInteraction.RSelect>;
const {container, unmount} = render(
<RMap {...common.mapProps}>
<RInteraction.RSelect ref={ref} />
<RLayerVector/>

Check failure on line 311 in test/RInteractions.test.tsx

View workflow job for this annotation

GitHub Actions / default

Insert `·`
</RMap>
);
expect(container.innerHTML).toMatchSnapshot();
expect(ref.current).toBeInstanceOf(RInteraction.RSelect);
// Should have no layer filter
expect((ref.current?.ol as any).layerFilter_?.()).toBe(true);

Check failure on line 317 in test/RInteractions.test.tsx

View workflow job for this annotation

GitHub Actions / default

Unexpected any. Specify a different type
unmount();
});
it('can be used inside a layer', async () => {
const ref = React.createRef() as React.RefObject<RInteraction.RSelect>;
const layerRef = React.createRef() as React.RefObject<RLayerVector>;
const otherLayerRef = React.createRef() as React.RefObject<RLayerVector>;
const {container, unmount} = render(
<RMap {...common.mapProps}>
<RLayerVector ref={layerRef}>
<RInteraction.RSelect ref={ref} />
</RLayerVector>
<RLayerVector ref={otherLayerRef} />
</RMap>
);
expect(container.innerHTML).toMatchSnapshot();
expect(ref.current).toBeInstanceOf(RInteraction.RSelect);
// Should include a filter for the containing layer
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((ref.current?.ol as any).layerFilter_?.(layerRef.current?.ol)).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((ref.current?.ol as any).layerFilter_?.(otherLayerRef.current?.ol)).toBe(false);
unmount();
});
});

describe('<RBaseInteraction>', () => {
it('should throw', async () => {
// eslint-disable-next-line no-console
Expand Down
8 changes: 8 additions & 0 deletions test/__snapshots__/RInteractions.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ exports[`<RModify> should create a Modify interaction 1`] = `"<div style="width:

exports[`<RModify> should support styles 1`] = `"<div style="width: 100px; height: 100px;"><div class="_rlayers_RLayerVector"></div><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`<RSelect> can be used directly inside a map 1`] = `"<div style="width: 100px; height: 100px;"><div class="_rlayers_RLayerVector"></div><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`<RSelect> can be used inside a layer 1`] = `"<div style="width: 100px; height: 100px;"><div class="_rlayers_RLayerVector"></div><div class="_rlayers_RLayerVector"></div><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`<RSelect> should create a Select interaction 1`] = `"<div style="width: 100px; height: 100px;"><div class="_rlayers_RLayerVector"></div><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`<RSelect> should support styles 1`] = `"<div style="width: 100px; height: 100px;"><div class="_rlayers_RLayerVector"></div><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`<RTranslate> should create a Translate interaction 1`] = `"<div style="width: 100px; height: 100px;"><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;

exports[`Default interactions should support manually adding all the default interactions 1`] = `"<div style="width: 100px; height: 100px;"><div class="ol-viewport" style="position: relative; overflow: hidden; width: 100%; height: 100%;"><div style="position: absolute; width: 100%; height: 100%; z-index: 0;" class="ol-unselectable ol-layers"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer"></div><div style="position: absolute; z-index: 0; width: 100%; height: 100%; pointer-events: none;" class="ol-overlaycontainer-stopevent"><div style="pointer-events: auto;" class="ol-zoom ol-unselectable ol-control"><button class="ol-zoom-in" type="button" title="Zoom in">+</button><button class="ol-zoom-out" type="button" title="Zoom out">–</button></div><div style="pointer-events: auto;" class="ol-rotate ol-unselectable ol-control ol-hidden"><button class="ol-rotate-reset" type="button" title="Reset rotation"><span class="ol-compass">⇧</span></button></div><div style="pointer-events: auto;" class="ol-attribution ol-unselectable ol-control ol-collapsed"><button type="button" aria-expanded="false" title="Attributions"><span class="ol-attribution-expand">i</span></button><ul></ul></div></div></div></div>"`;
Loading