[Maps] add draw control to create distance filter (#58163)

* [Maps] add distance filter to draw controls

* create distance filter

* update jest snapshot

* remove duplicated code

* reset circle draw when user hits escape

* i18n cleanup

* ts MultiIndexGeoFieldSelect

* ts DistanceFilterForm

* remove unused prop

* make interface a type

* move geo_field_with_index to components folder

* convert draw_circle to TS

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-03-16 13:33:40 -06:00 committed by GitHub
parent 6cd888f75f
commit 6cbfa274cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 636 additions and 282 deletions

View file

@ -17,49 +17,27 @@ exports[`should not render relation select when geo field is geo_point 1`] = `
value="My shape"
/>
</EuiFormRow>
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Filtered field"
labelType="label"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
compressed={true}
fullWidth={true}
hasDividers={true}
isInvalid={false}
isLoading={false}
itemClassName="mapGeometryFilter__geoFieldItem"
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiText
component="span"
size="s"
>
<EuiTextColor
color="subdued"
>
<small>
My index
</small>
</EuiTextColor>
<br />
my geo field
</EuiText>,
"value": "My index/my geo field",
},
]
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
valueOfSelected="My index/my geo field"
/>
</EuiFormRow>
}
/>
<EuiSpacer
size="m"
/>
@ -95,49 +73,27 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
value="My shape"
/>
</EuiFormRow>
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Filtered field"
labelType="label"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
compressed={true}
fullWidth={true}
hasDividers={true}
isInvalid={false}
isLoading={false}
itemClassName="mapGeometryFilter__geoFieldItem"
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiText
component="span"
size="s"
>
<EuiTextColor
color="subdued"
>
<small>
My index
</small>
</EuiTextColor>
<br />
my geo field
</EuiText>,
"value": "My index/my geo field",
},
]
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
valueOfSelected="My index/my geo field"
/>
</EuiFormRow>
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
@ -200,49 +156,27 @@ exports[`should render error message 1`] = `
value="My shape"
/>
</EuiFormRow>
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Filtered field"
labelType="label"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
compressed={true}
fullWidth={true}
hasDividers={true}
isInvalid={false}
isLoading={false}
itemClassName="mapGeometryFilter__geoFieldItem"
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiText
component="span"
size="s"
>
<EuiTextColor
color="subdued"
>
<small>
My index
</small>
</EuiTextColor>
<br />
my geo field
</EuiText>,
"value": "My index/my geo field",
},
]
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
valueOfSelected="My index/my geo field"
/>
</EuiFormRow>
}
/>
<EuiSpacer
size="m"
/>
@ -281,49 +215,27 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
value="My shape"
/>
</EuiFormRow>
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Filtered field"
labelType="label"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
compressed={true}
fullWidth={true}
hasDividers={true}
isInvalid={false}
isLoading={false}
itemClassName="mapGeometryFilter__geoFieldItem"
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiText
component="span"
size="s"
>
<EuiTextColor
color="subdued"
>
<small>
My index
</small>
</EuiTextColor>
<br />
my geo field
</EuiText>,
"value": "My index/my geo field",
},
]
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
valueOfSelected="My index/my geo field"
/>
</EuiFormRow>
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component, ChangeEvent } from 'react';
import {
EuiForm,
EuiFormRow,
EuiFieldText,
EuiButton,
EuiSpacer,
EuiTextAlign,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
import { GeoFieldWithIndex } from './geo_field_with_index';
interface Props {
className?: string;
buttonLabel: string;
geoFields: GeoFieldWithIndex[];
onSubmit: ({
filterLabel,
indexPatternId,
geoFieldName,
}: {
filterLabel: string;
indexPatternId: string;
geoFieldName: string;
}) => void;
}
interface State {
selectedField: GeoFieldWithIndex | undefined;
filterLabel: string;
}
export class DistanceFilterForm extends Component<Props, State> {
state = {
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
filterLabel: '',
};
_onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => {
this.setState({ selectedField });
};
_onFilterLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
filterLabel: e.target.value,
});
};
_onSubmit = () => {
if (!this.state.selectedField) {
return;
}
this.props.onSubmit({
filterLabel: this.state.filterLabel,
indexPatternId: this.state.selectedField.indexPatternId,
geoFieldName: this.state.selectedField.geoFieldName,
});
};
render() {
return (
<EuiForm className={this.props.className}>
<EuiFormRow
label={i18n.translate('xpack.maps.distanceFilterForm.filterLabelLabel', {
defaultMessage: 'Filter label',
})}
display="rowCompressed"
>
<EuiFieldText
compressed
value={this.state.filterLabel}
onChange={this._onFilterLabelChange}
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
selectedField={this.state.selectedField}
fields={this.props.geoFields}
onChange={this._onGeoFieldChange}
/>
<EuiSpacer size="m" />
<EuiTextAlign textAlign="right">
<EuiButton size="s" fill onClick={this._onSubmit} isDisabled={!this.state.selectedField}>
{this.props.buttonLabel}
</EuiButton>
</EuiTextAlign>
</EuiForm>
);
}
}

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to:
// 1) Combine the geo field along with associated index pattern state.
// 2) Package asynchronously looked up state via indexPatternService to avoid
// PITA of looking up async state in downstream react consumers.
export type GeoFieldWithIndex = {
geoFieldName: string;
geoFieldType: string;
indexPatternTitle: string;
indexPatternId: string;
};

View file

@ -9,9 +9,6 @@ import PropTypes from 'prop-types';
import {
EuiForm,
EuiFormRow,
EuiSuperSelect,
EuiTextColor,
EuiText,
EuiFieldText,
EuiButton,
EuiSelect,
@ -22,20 +19,7 @@ import {
import { i18n } from '@kbn/i18n';
import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants';
import { getEsSpatialRelationLabel } from '../../common/i18n_getters';
const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions
function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) {
return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`;
}
function splitIndexGeoFieldName(value) {
const split = value.split(GEO_FIELD_VALUE_DELIMITER);
return {
indexPatternTitle: split[0],
geoFieldName: split[1],
};
}
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
export class GeometryFilterForm extends Component {
static propTypes = {
@ -52,27 +36,13 @@ export class GeometryFilterForm extends Component {
};
state = {
geoFieldTag: this.props.geoFields.length
? createIndexGeoFieldName(this.props.geoFields[0])
: '',
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
geometryLabel: this.props.intitialGeometryLabel,
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
};
_getSelectedGeoField = () => {
if (!this.state.geoFieldTag) {
return null;
}
const { indexPatternTitle, geoFieldName } = splitIndexGeoFieldName(this.state.geoFieldTag);
return this.props.geoFields.find(option => {
return option.indexPatternTitle === indexPatternTitle && option.geoFieldName === geoFieldName;
});
};
_onGeoFieldChange = selectedValue => {
this.setState({ geoFieldTag: selectedValue });
_onGeoFieldChange = selectedField => {
this.setState({ selectedField });
};
_onGeometryLabelChange = e => {
@ -88,25 +58,21 @@ export class GeometryFilterForm extends Component {
};
_onSubmit = () => {
const geoField = this._getSelectedGeoField();
this.props.onSubmit({
geometryLabel: this.state.geometryLabel,
indexPatternId: geoField.indexPatternId,
geoFieldName: geoField.geoFieldName,
geoFieldType: geoField.geoFieldType,
indexPatternId: this.state.selectedField.indexPatternId,
geoFieldName: this.state.selectedField.geoFieldName,
geoFieldType: this.state.selectedField.geoFieldType,
relation: this.state.relation,
});
};
_renderRelationInput() {
if (!this.state.geoFieldTag) {
return null;
}
const { geoFieldType } = this._getSelectedGeoField();
// relationship only used when filtering geo_shape fields
if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) {
if (
!this.state.selectedField ||
this.state.selectedField.geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT
) {
return null;
}
@ -141,20 +107,6 @@ export class GeometryFilterForm extends Component {
}
render() {
const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => {
return {
inputDisplay: (
<EuiText size="s" component="span">
<EuiTextColor color="subdued">
<small>{indexPatternTitle}</small>
</EuiTextColor>
<br />
{geoFieldName}
</EuiText>
),
value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName }),
};
});
let error;
if (this.props.errorMsg) {
error = <EuiFormErrorText>{this.props.errorMsg}</EuiFormErrorText>;
@ -174,24 +126,11 @@ export class GeometryFilterForm extends Component {
/>
</EuiFormRow>
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
label={i18n.translate('xpack.maps.geometryFilterForm.geoFieldLabel', {
defaultMessage: 'Filtered field',
})}
display="rowCompressed"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
options={options}
valueOfSelected={this.state.geoFieldTag}
onChange={this._onGeoFieldChange}
hasDividers={true}
fullWidth={true}
compressed={true}
itemClassName="mapGeometryFilter__geoFieldItem"
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
selectedField={this.state.selectedField}
fields={this.props.geoFields}
onChange={this._onGeoFieldChange}
/>
{this._renderRelationInput()}
@ -204,7 +143,7 @@ export class GeometryFilterForm extends Component {
size="s"
fill
onClick={this._onSubmit}
isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag}
isDisabled={!this.state.geometryLabel || !this.state.selectedField}
isLoading={this.props.isLoading}
>
{this.props.buttonLabel}

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GeoFieldWithIndex } from './geo_field_with_index';
const OPTION_ID_DELIMITER = '/';
function createOptionId(geoField: GeoFieldWithIndex): string {
// Namespace field with indexPatterId to avoid collisions between field names
return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`;
}
function splitOptionId(optionId: string) {
const split = optionId.split(OPTION_ID_DELIMITER);
return {
indexPatternId: split[0],
geoFieldName: split[1],
};
}
interface Props {
fields: GeoFieldWithIndex[];
onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void;
selectedField: GeoFieldWithIndex | undefined;
}
export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) {
function onFieldSelect(selectedOptionId: string) {
const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId);
const newSelectedField = fields.find(field => {
return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName;
});
onChange(newSelectedField);
}
const options = fields.map((geoField: GeoFieldWithIndex) => {
return {
inputDisplay: (
<EuiText size="s">
<EuiTextColor color="subdued">
<small>{geoField.indexPatternTitle}</small>
</EuiTextColor>
<br />
{geoField.geoFieldName}
</EuiText>
),
value: createOptionId(geoField),
};
});
return (
<EuiFormRow
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
label={i18n.translate('xpack.maps.multiIndexFieldSelect.fieldLabel', {
defaultMessage: 'Filtering field',
})}
display="rowCompressed"
>
<EuiSuperSelect
className="mapGeometryFilter__geoFieldSuperSelect"
options={options}
valueOfSelected={selectedField ? createOptionId(selectedField) : ''}
onChange={onFieldSelect}
hasDividers={true}
fullWidth={true}
compressed={true}
itemClassName="mapGeometryFilter__geoFieldItem"
/>
</EuiFormRow>
);
}

View file

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
// @ts-ignore
import turf from 'turf';
// @ts-ignore
import turfCircle from '@turf/circle';
type DrawCircleState = {
circle: {
properties: {
center: {} | null;
radiusKm: number;
};
id: string | number;
incomingCoords: (coords: unknown[]) => void;
toGeoJSON: () => unknown;
};
};
type MouseEvent = {
lngLat: {
lng: number;
lat: number;
};
};
export const DrawCircle = {
onSetup() {
// @ts-ignore
const circle: unknown = this.newFeature({
type: 'Feature',
properties: {
center: null,
radiusKm: 0,
},
geometry: {
type: 'Polygon',
coordinates: [[]],
},
});
// @ts-ignore
this.addFeature(circle);
// @ts-ignore
this.clearSelectedFeatures();
// @ts-ignore
this.updateUIClasses({ mouse: 'add' });
// @ts-ignore
this.setActionableState({
trash: true,
});
return {
circle,
};
},
onKeyUp(state: DrawCircleState, e: { keyCode: number }) {
if (e.keyCode === 27) {
// clear point when user hits escape
state.circle.properties.center = null;
state.circle.properties.radiusKm = 0;
state.circle.incomingCoords([[]]);
}
},
onClick(state: DrawCircleState, e: MouseEvent) {
if (!state.circle.properties.center) {
// first click, start circle
state.circle.properties.center = [e.lngLat.lng, e.lngLat.lat];
} else {
// second click, finish draw
// @ts-ignore
this.updateUIClasses({ mouse: 'pointer' });
state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [
e.lngLat.lng,
e.lngLat.lat,
]);
// @ts-ignore
this.changeMode('simple_select', { featuresId: state.circle.id });
}
},
onMouseMove(state: DrawCircleState, e: MouseEvent) {
if (!state.circle.properties.center) {
// circle not started, nothing to update
return;
}
const mouseLocation = [e.lngLat.lng, e.lngLat.lat];
state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation);
const newCircleFeature = turfCircle(
state.circle.properties.center,
state.circle.properties.radiusKm
);
state.circle.incomingCoords(newCircleFeature.geometry.coordinates);
},
onStop(state: DrawCircleState) {
// @ts-ignore
this.updateUIClasses({ mouse: 'none' });
// @ts-ignore
this.activateUIButton();
// @ts-ignore
if (this.getFeature(state.circle.id) === undefined) return;
if (state.circle.properties.center && state.circle.properties.radiusKm > 0) {
// @ts-ignore
this.map.fire('draw.create', {
features: [state.circle.toGeoJSON()],
});
} else {
// @ts-ignore
this.deleteFeature([state.circle.id], { silent: true });
// @ts-ignore
this.changeMode('simple_select', {}, { silent: true });
}
},
toDisplayFeatures(
state: DrawCircleState,
geojson: { properties: { active: string } },
display: (geojson: unknown) => unknown
) {
if (state.circle.properties.center) {
geojson.properties.active = 'true';
return display(geojson);
}
},
onTrash(state: DrawCircleState) {
// @ts-ignore
this.deleteFeature([state.circle.id], { silent: true });
// @ts-ignore
this.changeMode('simple_select');
},
};

View file

@ -9,7 +9,9 @@ import React from 'react';
import { DRAW_TYPE } from '../../../../../common/constants';
import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified';
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import { DrawCircle } from './draw_circle';
import {
createDistanceFilterWithMeta,
createSpatialFilterWithBoundingBox,
createSpatialFilterWithGeometry,
getBoundingBoxGeometry,
@ -19,6 +21,7 @@ import { DrawTooltip } from './draw_tooltip';
const mbDrawModes = MapboxDraw.modes;
mbDrawModes.draw_rectangle = DrawRectangle;
mbDrawModes.draw_circle = DrawCircle;
export class DrawControl extends React.Component {
constructor() {
@ -60,7 +63,21 @@ export class DrawControl extends React.Component {
return;
}
const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS;
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
const circle = e.features[0];
roundCoordinates(circle.properties.center);
const filter = createDistanceFilterWithMeta({
alias: this.props.drawState.filterLabel,
distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2),
geoFieldName: this.props.drawState.geoFieldName,
indexPatternId: this.props.drawState.indexPatternId,
point: circle.properties.center,
});
this.props.addFilters([filter]);
this.props.disableDrawState();
return;
}
const geometry = e.features[0].geometry;
// MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number
roundCoordinates(geometry.coordinates);
@ -73,15 +90,16 @@ export class DrawControl extends React.Component {
geometryLabel: this.props.drawState.geometryLabel,
relation: this.props.drawState.relation,
};
const filter = isBoundingBox
? createSpatialFilterWithBoundingBox({
...options,
geometry: getBoundingBoxGeometry(geometry),
})
: createSpatialFilterWithGeometry({
...options,
geometry,
});
const filter =
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? createSpatialFilterWithBoundingBox({
...options,
geometry: getBoundingBoxGeometry(geometry),
})
: createSpatialFilterWithGeometry({
...options,
geometry,
});
this.props.addFilters([filter]);
} catch (error) {
// TODO notify user why filter was not created
@ -109,11 +127,14 @@ export class DrawControl extends React.Component {
this.props.mbMap.getCanvas().style.cursor = 'crosshair';
this.props.mbMap.on('draw.create', this._onDraw);
}
const mbDrawMode =
this.props.drawState.drawType === DRAW_TYPE.POLYGON
? this._mbDrawControl.modes.DRAW_POLYGON
: 'draw_rectangle';
this._mbDrawControl.changeMode(mbDrawMode);
if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
this._mbDrawControl.changeMode('draw_rectangle');
} else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
this._mbDrawControl.changeMode('draw_circle');
} else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) {
this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON);
}
}
render() {

View file

@ -42,14 +42,24 @@ export class DrawTooltip extends Component {
}
render() {
const instructions =
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
defaultMessage: 'Click to start rectangle. Click again to finish.',
})
: i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
defaultMessage: 'Click to add vertex. Double click to finish.',
});
let instructions;
if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) {
instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', {
defaultMessage:
'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.',
});
} else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', {
defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.',
});
} else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) {
instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', {
defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.',
});
} else {
// unknown draw type, tooltip not needed
return null;
}
const tooltipAnchor = (
<div style={{ height: '26px', width: '26px', background: 'transparent' }} />

View file

@ -41,6 +41,10 @@ exports[`Should render cancel button when drawing 1`] = `
"name": "Draw bounds to filter data",
"panel": 2,
},
Object {
"name": "Draw distance to filter data",
"panel": 3,
},
],
"title": "Tools",
},
@ -86,6 +90,25 @@ exports[`Should render cancel button when drawing 1`] = `
"id": 2,
"title": "Draw bounds",
},
Object {
"content": <DistanceFilterForm
buttonLabel="Draw distance"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
onSubmit={[Function]}
/>,
"id": 3,
"title": "Draw distance",
},
]
}
/>
@ -144,6 +167,10 @@ exports[`renders 1`] = `
"name": "Draw bounds to filter data",
"panel": 2,
},
Object {
"name": "Draw distance to filter data",
"panel": 3,
},
],
"title": "Tools",
},
@ -189,6 +216,25 @@ exports[`renders 1`] = `
"id": 2,
"title": "Draw bounds",
},
Object {
"content": <DistanceFilterForm
buttonLabel="Draw distance"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
onSubmit={[Function]}
/>,
"id": 3,
"title": "Draw distance",
},
]
}
/>

View file

@ -14,9 +14,10 @@ import {
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DRAW_TYPE } from '../../../../common/constants';
import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants';
import { FormattedMessage } from '@kbn/i18n/react';
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
import { DistanceFilterForm } from '../../../components/distance_filter_form';
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
defaultMessage: 'Draw shape to filter data',
@ -26,6 +27,10 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa
defaultMessage: 'Draw bounds to filter data',
});
const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', {
defaultMessage: 'Draw distance to filter data',
});
const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', {
defaultMessage: 'Draw shape',
});
@ -34,6 +39,13 @@ const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBo
defaultMessage: 'Draw bounds',
});
const DRAW_DISTANCE_LABEL_SHORT = i18n.translate(
'xpack.maps.toolbarOverlay.drawDistanceLabelShort',
{
defaultMessage: 'Draw distance',
}
);
export class ToolsControl extends Component {
state = {
isPopoverOpen: false,
@ -65,23 +77,43 @@ export class ToolsControl extends Component {
this._closePopover();
};
_initiateDistanceDraw = options => {
this.props.initiateDraw({
drawType: DRAW_TYPE.DISTANCE,
...options,
});
this._closePopover();
};
_getDrawPanels() {
const tools = [
{
name: DRAW_SHAPE_LABEL,
panel: 1,
},
{
name: DRAW_BOUNDS_LABEL,
panel: 2,
},
];
const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => {
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT;
});
if (hasGeoPoints) {
tools.push({
name: DRAW_DISTANCE_LABEL,
panel: 3,
});
}
return [
{
id: 0,
title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', {
defaultMessage: 'Tools',
}),
items: [
{
name: DRAW_SHAPE_LABEL,
panel: 1,
},
{
name: DRAW_BOUNDS_LABEL,
panel: 2,
},
],
items: tools,
},
{
id: 1,
@ -119,6 +151,20 @@ export class ToolsControl extends Component {
/>
),
},
{
id: 3,
title: DRAW_DISTANCE_LABEL_SHORT,
content: (
<DistanceFilterForm
className="mapDrawControl__geometryFilterForm"
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
geoFields={this.props.geoFields.filter(({ geoFieldType }) => {
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT;
})}
onSubmit={this._initiateDistanceDraw}
/>
),
},
];
}

View file

@ -344,6 +344,39 @@ function createGeometryFilterWithMeta({
return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta });
}
export function createDistanceFilterWithMeta({
alias,
distanceKm,
geoFieldName,
indexPatternId,
point,
}) {
const meta = {
type: SPATIAL_FILTER_TYPE,
negate: false,
index: indexPatternId,
key: geoFieldName,
alias: alias
? alias
: i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', {
defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}',
values: {
distanceKm,
geoFieldName,
pointLabel: point.join(','),
},
}),
};
return {
geo_distance: {
distance: `${distanceKm}km`,
[geoFieldName]: point,
},
meta,
};
}
export function roundCoordinates(coordinates) {
for (let i = 0; i < coordinates.length; i++) {
const value = coordinates[i];

View file

@ -196,6 +196,7 @@
"@scant/router": "^0.1.0",
"@slack/webhook": "^5.0.0",
"@turf/boolean-contains": "6.0.1",
"@turf/circle": "6.0.1",
"angular": "^1.7.9",
"angular-resource": "1.7.9",
"angular-sanitize": "1.7.9",

View file

@ -120,6 +120,7 @@ export const EMPTY_FEATURE_COLLECTION = {
export const DRAW_TYPE = {
BOUNDS: 'BOUNDS',
DISTANCE: 'DISTANCE',
POLYGON: 'POLYGON',
};

View file

@ -7073,7 +7073,6 @@
"xpack.maps.feature.appDescription": "Elasticsearch と Elastic Maps Service の地理空間データを閲覧します",
"xpack.maps.featureRegistry.mapsFeatureName": "マップ",
"xpack.maps.geoGrid.resolutionLabel": "グリッド解像度",
"xpack.maps.geometryFilterForm.geoFieldLabel": "フィルタリングされたフィールド",
"xpack.maps.geometryFilterForm.geometryLabelLabel": "ジオメトリラベル",
"xpack.maps.geometryFilterForm.relationLabel": "空間関係",
"xpack.maps.heatmap.colorRampLabel": "色の範囲",

View file

@ -7073,7 +7073,6 @@
"xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic 地图服务浏览地理空间数据",
"xpack.maps.featureRegistry.mapsFeatureName": "Maps",
"xpack.maps.geoGrid.resolutionLabel": "网格分辨率",
"xpack.maps.geometryFilterForm.geoFieldLabel": "已筛选字段",
"xpack.maps.geometryFilterForm.geometryLabelLabel": "几何标签",
"xpack.maps.geometryFilterForm.relationLabel": "空间关系",
"xpack.maps.heatmap.colorRampLabel": "颜色范围",

View file

@ -4086,6 +4086,22 @@
"@turf/helpers" "6.x"
"@turf/invariant" "6.x"
"@turf/circle@6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.0.1.tgz#0ab72083373ae3c76b700c17a504ab1b5c0910b9"
integrity sha512-pF9XsYtCvY9ZyNqJ3hFYem9VaiGdVNQb0SFq/zzDMwH3iWZPPJQHnnDB/3e8RD1VDtBBov9p5uO2k7otsfezjw==
dependencies:
"@turf/destination" "6.x"
"@turf/helpers" "6.x"
"@turf/destination@6.x":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a"
integrity sha512-MroK4nRdp7as174miCAugp8Uvorhe6rZ7MJiC9Hb4+hZR7gNFJyVKmkdDDXIoCYs6MJQsx0buI+gsCpKwgww0Q==
dependencies:
"@turf/helpers" "6.x"
"@turf/invariant" "6.x"
"@turf/helpers@6.x":
version "6.1.4"
resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836"