Skip to content

Commit

Permalink
[Maps] Add capability to delete features from layer & index (elastic#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Caldwell committed Jun 25, 2021
1 parent 41b015a commit 4b20ff3
Show file tree
Hide file tree
Showing 20 changed files with 314 additions and 7 deletions.
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) {
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

0 comments on commit 4b20ff3

Please sign in to comment.