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

[Maps] Add capability to delete features from layer & index #103145

Merged
merged 13 commits into from
Jun 25, 2021
Merged
1 change: 1 addition & 0 deletions x-pack/plugins/maps/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export enum DRAW_SHAPE {
POINT = 'POINT',
LINE = 'LINE',
SIMPLE_SELECT = 'SIMPLE_SELECT',
DELETE = 'DELETE',
}

export const AGG_DELIMITER = '_of_';
Expand Down
19 changes: 19 additions & 0 deletions x-pack/plugins/maps/public/actions/map_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,22 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) {
await dispatch(syncDataForLayer(layer, true));
};
}

export function deleteFeatureFromIndex(featureId: string) {
return async (
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
getState: () => MapStoreState
) => {
const editState = getEditState(getState());
const layerId = editState ? editState.layerId : undefined;
if (!layerId) {
return;
}
const layer = getLayerById(layerId, getState());
if (!layer || !(layer instanceof VectorLayer)) {
return;
}
await layer.deleteFeature(featureId);
await dispatch(syncDataForLayer(layer, true));
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface IVectorLayer extends ILayer {
supportsFeatureEditing(): boolean;
getLeftJoinFields(): Promise<IField[]>;
addFeature(geometry: Geometry | Position[]): Promise<void>;
deleteFeature(featureId: string): Promise<void>;
}

export class VectorLayer extends AbstractLayer implements IVectorLayer {
Expand Down Expand Up @@ -1156,4 +1157,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
const layerSource = this.getSource();
await layerSource.addFeature(geometry);
}

async deleteFeature(featureId: string) {
const layerSource = this.getSource();
await layerSource.deleteFeature(featureId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { isValidStringConfig } from '../../util/valid_string_config';
import { TopHitsUpdateSourceEditor } from './top_hits';
import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields';
import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source';
import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit';
import { addFeatureToIndex, deleteFeatureFromIndex, getMatchingIndexes } from './util/feature_edit';

export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined {
const timeRangeBounds = getTimeFilter().calculateBounds(timerange);
Expand Down Expand Up @@ -716,6 +716,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName());
}

async deleteFeature(featureId: string) {
const indexPattern = await this.getIndexPattern();
await deleteFeatureFromIndex(indexPattern.title, featureId);
}

async getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<ITiledSingleLayerMvtParams> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export const addFeatureToIndex = async (
});
};

export const deleteFeatureFromIndex = async (indexName: string, featureId: string) => {
return await getHttp().fetch({
path: `${INDEX_FEATURE_PATH}/${featureId}`,
method: 'DELETE',
body: JSON.stringify({
index: indexName,
}),
});
};

export const getMatchingIndexes = async (indexPattern: string) => {
return await getHttp().fetch({
path: `${GET_MATCHING_INDEXES_PATH}/${indexPattern}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ export class MVTSingleLayerVectorSource
throw new Error('Does not implement addFeature');
}

deleteFeature(featureId: string): Promise<void> {
throw new Error('Does not implement deleteFeature');
}

getMVTFields(): MVTField[] {
return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
return new MVTField({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface IVectorSource extends ISource {
getTimesliceMaskFieldName(): Promise<string | null>;
supportsFeatureEditing(): Promise<boolean>;
addFeature(geometry: Geometry | Position[]): Promise<void>;
deleteFeature(featureId: string): Promise<void>;
}

export class AbstractVectorSource extends AbstractSource implements IVectorSource {
Expand Down Expand Up @@ -165,6 +166,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
throw new Error('Should implement VectorSource#addFeature');
}

async deleteFeature(featureId: string): Promise<void> {
throw new Error('Should implement VectorSource#deleteFeature');
}

async supportsFeatureEditing(): Promise<boolean> {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface TimesliceMaskConfig {
}

export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
export const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];

function getFilterExpression(
filters: unknown[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw';
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { Feature } from 'geojson';
import { MapMouseEvent } from '@kbn/mapbox-gl';
import { DRAW_SHAPE } from '../../../../common/constants';
import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle';
import { DrawTooltip } from './draw_tooltip';
Expand All @@ -37,6 +38,7 @@ mbDrawModes[DRAW_CIRCLE] = DrawCircle;
export interface Props {
drawShape?: DRAW_SHAPE;
onDraw: (event: { features: Feature[] }, drawControl?: MapboxDraw) => void;
onClick?: (event: MapMouseEvent, drawControl?: MapboxDraw) => void;
mbMap: MbMap;
enable: boolean;
updateEditShape: (shapeToDraw: DRAW_SHAPE) => void;
Expand Down Expand Up @@ -68,6 +70,12 @@ export class DrawControl extends Component<Props> {
this.props.onDraw(event, this._mbDrawControl);
};

_onClick = (event: MapMouseEvent) => {
if (this.props.onClick) {
this.props.onClick(event, this._mbDrawControl);
}
};

// debounce with zero timeout needed to allow mapbox-draw finish logic to complete
// before _removeDrawControl is called
_syncDrawControl = _.debounce(() => {
Expand Down Expand Up @@ -96,6 +104,9 @@ export class DrawControl extends Component<Props> {
this.props.mbMap.getCanvas().style.cursor = '';
this.props.mbMap.off('draw.modechange', this._onModeChange);
this.props.mbMap.off('draw.create', this._onDraw);
if (this.props.onClick) {
kindsun marked this conversation as resolved.
Show resolved Hide resolved
this.props.mbMap.off('click', this._onClick);
}
this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID);
this.props.mbMap.removeControl(this._mbDrawControl);
this._mbDrawControlAdded = false;
Expand Down Expand Up @@ -131,6 +142,9 @@ export class DrawControl extends Component<Props> {
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.modechange', this._onModeChange);
this.props.mbMap.on('draw.create', this._onDraw);
if (this.props.onClick) {
this.props.mbMap.on('click', this._onClick);
}
}

const { DRAW_LINE_STRING, DRAW_POLYGON, DRAW_POINT, SIMPLE_SELECT } = this._mbDrawControl.modes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@
*/

import React, { Component } from 'react';
import { Map as MbMap } from 'mapbox-gl';
import { Map as MbMap, Point as MbPoint } from 'mapbox-gl';
// @ts-expect-error
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { Feature, Geometry, Position } from 'geojson';
import { i18n } from '@kbn/i18n';
// @ts-expect-error
import * as jsts from 'jsts';
import { MapMouseEvent } from '@kbn/mapbox-gl';
import { getToasts } from '../../../../kibana_services';
import { DrawControl } from '../';
import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common';
import { ILayer } from '../../../../classes/layers/layer';
import {
EXCLUDE_CENTROID_FEATURES,
EXCLUDE_TOO_MANY_FEATURES_BOX,
} from '../../../../classes/util/mb_filter_expressions';

const geoJSONReader = new jsts.io.GeoJSONReader();

export interface ReduxStateProps {
drawShape?: DRAW_SHAPE;
drawMode: DRAW_MODE;
editLayer: ILayer | undefined;
}

export interface ReduxDispatchProps {
addNewFeatureToIndex: (geometry: Geometry | Position[]) => void;
deleteFeatureFromIndex: (featureId: string) => void;
disableDrawState: () => void;
}

Expand Down Expand Up @@ -75,11 +83,58 @@ export class DrawFeatureControl extends Component<Props, {}> {
}
};

_onClick = async (event: MapMouseEvent, drawControl?: MapboxDraw) => {
const mbLngLatPoint: MbPoint = event.point;
if (!this.props.editLayer) {
return;
}
const mbEditLayerIds = this.props.editLayer
.getMbLayerIds()
.filter((mbLayerId) => !!this.props.mbMap.getLayer(mbLayerId));
const PADDING = 2; // in pixels
const mbBbox = [
{
x: mbLngLatPoint.x - PADDING,
y: mbLngLatPoint.y - PADDING,
},
{
x: mbLngLatPoint.x + PADDING,
y: mbLngLatPoint.y + PADDING,
},
] as [MbPoint, MbPoint];
const selectedFeatures = this.props.mbMap.queryRenderedFeatures(mbBbox, {
layers: mbEditLayerIds,
filter: ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES],
});
if (!selectedFeatures.length) {
return;
}
const topMostFeature = selectedFeatures[0];

try {
if (!(topMostFeature.properties && topMostFeature.properties._id)) {
throw Error(`Associated Elasticsearch document id not found`);
}
const docId = topMostFeature.properties._id;
this.props.deleteFeatureFromIndex(docId);
} catch (error) {
getToasts().addWarning(
i18n.translate('xpack.maps.drawFeatureControl.unableToDeleteFeature', {
defaultMessage: `Unable to delete feature, error: '{errorMsg}'.`,
values: {
errorMsg: error.message,
},
})
);
}
};

render() {
return (
<DrawControl
drawShape={this.props.drawShape}
onDraw={this._onDraw}
onClick={this._onClick}
mbMap={this.props.mbMap}
enable={true}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ import {
ReduxStateProps,
OwnProps,
} from './draw_feature_control';
import { addNewFeatureToIndex, updateEditShape } from '../../../../actions';
import { addNewFeatureToIndex, deleteFeatureFromIndex, updateEditShape } from '../../../../actions';
import { MapStoreState } from '../../../../reducers/store';
import { getEditState } from '../../../../selectors/map_selectors';
import { getEditState, getLayerById } from '../../../../selectors/map_selectors';
import { getDrawMode } from '../../../../selectors/ui_selectors';

function mapStateToProps(state: MapStoreState): ReduxStateProps {
const editState = getEditState(state);
const editLayer = editState ? getLayerById(editState.layerId, state) : undefined;
return {
drawShape: editState ? editState.drawShape : undefined,
drawMode: getDrawMode(state),
editLayer,
};
}

Expand All @@ -35,6 +37,9 @@ function mapDispatchToProps(
addNewFeatureToIndex(geometry: Geometry | Position[]) {
dispatch(addNewFeatureToIndex(geometry));
},
deleteFeatureFromIndex(featureId: string) {
dispatch(deleteFeatureFromIndex(featureId));
},
disableDrawState() {
dispatch(updateEditShape(null));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export class DrawTooltip extends Component<Props, State> {
instructions = i18n.translate('xpack.maps.drawTooltip.pointInstructions', {
defaultMessage: 'Click to create point.',
});
} else if (this.props.drawShape === DRAW_SHAPE.DELETE) {
instructions = i18n.translate('xpack.maps.drawTooltip.deleteInstructions', {
defaultMessage: 'Click feature to delete.',
});
} else {
// unknown draw type, tooltip not needed
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function FeatureEditTools(props: Props) {
const drawCircleSelected = props.drawShape === DRAW_SHAPE.DISTANCE;
const drawBBoxSelected = props.drawShape === DRAW_SHAPE.BOUNDS;
const drawPointSelected = props.drawShape === DRAW_SHAPE.POINT;
const deleteSelected = props.drawShape === DRAW_SHAPE.DELETE;

return (
<EuiPanel paddingSize="none" className="mapToolbarOverlay__buttonGroup">
Expand Down Expand Up @@ -117,6 +118,24 @@ export function FeatureEditTools(props: Props) {
isSelected={drawPointSelected}
display={drawPointSelected ? 'fill' : 'empty'}
/>
<EuiButtonIcon
key="delete"
size="s"
onClick={() => props.setDrawShape(DRAW_SHAPE.DELETE)}
iconType="trash"
aria-label={i18n.translate(
'xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeLabel',
{
defaultMessage: 'Delete point or shape',
}
)}
title={i18n.translate('xpack.maps.toolbarOverlay.featureDraw.deletePointOrShapeTitle', {
defaultMessage: 'Delete point or shape',
})}
aria-pressed={deleteSelected}
isSelected={deleteSelected}
display={deleteSelected ? 'fill' : 'empty'}
/>

<EuiButtonIcon
key="exit"
Expand Down
Loading