mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] Show spatial filters on map to provide context when for active filters (#63406)
* [Maps] show spatial filters * pass data into __dataRequests * extractFeaturesFromFilters * geo_shape support * putting it all together * lower alpha * update removeOrphanedSourcesAndLayers to avoid removing spatialFiltersLayer * change array iteration to forEach * use less precision when distance filter covers larger distances * fix double import * add map settings for to configure spatial filters layer * add map settings alpha slider * finish rest of map settings * review feedback Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
65264aa790
commit
f7ea9b99ba
16 changed files with 463 additions and 91 deletions
|
@ -215,3 +215,5 @@ export enum SCALING_TYPES {
|
|||
}
|
||||
|
||||
export const RGBA_0000 = 'rgba(0,0,0,0)';
|
||||
|
||||
export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID';
|
||||
|
|
46
x-pack/plugins/maps/public/components/alpha_slider.tsx
Normal file
46
x-pack/plugins/maps/public/components/alpha_slider.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { ValidatedRange } from './validated_range';
|
||||
|
||||
interface Props {
|
||||
alpha: number;
|
||||
onChange: (alpha: number) => void;
|
||||
}
|
||||
|
||||
export function AlphaSlider({ alpha, onChange }: Props) {
|
||||
const onAlphaChange = (newAlpha: number) => {
|
||||
onChange(newAlpha / 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerTransparencyLabel', {
|
||||
defaultMessage: 'Opacity',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<ValidatedRange
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={Math.round(alpha * 100)}
|
||||
onChange={onAlphaChange}
|
||||
showInput
|
||||
showRange
|
||||
compressed
|
||||
append={i18n.translate('xpack.maps.layerPanel.settingsPanel.percentageLabel', {
|
||||
defaultMessage: '%',
|
||||
description: 'Percentage',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -8,7 +8,7 @@ import React, { Fragment } from 'react';
|
|||
|
||||
import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { ValidatedRange } from '../../../components/validated_range';
|
||||
import { AlphaSlider } from '../../../components/alpha_slider';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -24,8 +24,7 @@ export function LayerSettings(props) {
|
|||
};
|
||||
|
||||
const onAlphaChange = alpha => {
|
||||
const alphaDecimal = alpha / 100;
|
||||
props.updateAlpha(props.layerId, alphaDecimal);
|
||||
props.updateAlpha(props.layerId, alpha);
|
||||
};
|
||||
|
||||
const renderZoomSliders = () => {
|
||||
|
@ -64,34 +63,6 @@ export function LayerSettings(props) {
|
|||
);
|
||||
};
|
||||
|
||||
const renderAlphaSlider = () => {
|
||||
const alphaPercent = Math.round(props.alpha * 100);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerTransparencyLabel', {
|
||||
defaultMessage: 'Opacity',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<ValidatedRange
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={alphaPercent}
|
||||
onChange={onAlphaChange}
|
||||
showInput
|
||||
showRange
|
||||
compressed
|
||||
append={i18n.translate('xpack.maps.layerPanel.settingsPanel.percentageLabel', {
|
||||
defaultMessage: '%',
|
||||
description: 'Percentage',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPanel>
|
||||
|
@ -107,7 +78,7 @@ export function LayerSettings(props) {
|
|||
<EuiSpacer size="m" />
|
||||
{renderLabel()}
|
||||
{renderZoomSliders()}
|
||||
{renderAlphaSlider()}
|
||||
<AlphaSlider alpha={props.alpha} onChange={onAlphaChange} />
|
||||
</EuiPanel>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
|
|
@ -64,13 +64,28 @@ export class DrawControl extends React.Component {
|
|||
|
||||
if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) {
|
||||
const circle = e.features[0];
|
||||
roundCoordinates(circle.properties.center);
|
||||
const distanceKm = _.round(
|
||||
circle.properties.radiusKm,
|
||||
circle.properties.radiusKm > 10 ? 0 : 2
|
||||
);
|
||||
// Only include as much precision as needed for distance
|
||||
let precision = 2;
|
||||
if (distanceKm <= 1) {
|
||||
precision = 5;
|
||||
} else if (distanceKm <= 10) {
|
||||
precision = 4;
|
||||
} else if (distanceKm <= 100) {
|
||||
precision = 3;
|
||||
}
|
||||
const filter = createDistanceFilterWithMeta({
|
||||
alias: this.props.drawState.filterLabel,
|
||||
distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2),
|
||||
distanceKm,
|
||||
geoFieldName: this.props.drawState.geoFieldName,
|
||||
indexPatternId: this.props.drawState.indexPatternId,
|
||||
point: circle.properties.center,
|
||||
point: [
|
||||
_.round(circle.properties.center[0], precision),
|
||||
_.round(circle.properties.center[1], precision),
|
||||
],
|
||||
});
|
||||
this.props.addFilters([filter]);
|
||||
this.props.disableDrawState();
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
isInteractiveDisabled,
|
||||
isTooltipControlDisabled,
|
||||
isViewControlHidden,
|
||||
getSpatialFiltersLayer,
|
||||
getMapSettings,
|
||||
} from '../../../selectors/map_selectors';
|
||||
|
||||
|
@ -33,6 +34,7 @@ function mapStateToProps(state = {}) {
|
|||
isMapReady: getMapReady(state),
|
||||
settings: getMapSettings(state),
|
||||
layerList: getLayerList(state),
|
||||
spatialFiltersLayer: getSpatialFiltersLayer(state),
|
||||
goto: getGoto(state),
|
||||
inspectorAdapters: getInspectorAdapters(state),
|
||||
scrollZoom: getScrollZoom(state),
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils';
|
||||
import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants';
|
||||
import _ from 'lodash';
|
||||
|
||||
class MockMbMap {
|
||||
|
@ -121,7 +122,8 @@ function makeMultiSourceMockLayer(layerId) {
|
|||
);
|
||||
}
|
||||
|
||||
describe('mb/utils', () => {
|
||||
describe('removeOrphanedSourcesAndLayers', () => {
|
||||
const spatialFilterLayer = makeMultiSourceMockLayer(SPATIAL_FILTERS_LAYER_ID);
|
||||
test('should remove foo and bar layer', async () => {
|
||||
const bazLayer = makeSingleSourceMockLayer('baz');
|
||||
const fooLayer = makeSingleSourceMockLayer('foo');
|
||||
|
@ -133,7 +135,7 @@ describe('mb/utils', () => {
|
|||
const currentStyle = getMockStyle(currentLayerList);
|
||||
const mockMbMap = new MockMbMap(currentStyle);
|
||||
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList);
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer);
|
||||
const removedStyle = mockMbMap.getStyle();
|
||||
|
||||
const nextStyle = getMockStyle(nextLayerList);
|
||||
|
@ -151,7 +153,7 @@ describe('mb/utils', () => {
|
|||
const currentStyle = getMockStyle(currentLayerList);
|
||||
const mockMbMap = new MockMbMap(currentStyle);
|
||||
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList);
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer);
|
||||
const removedStyle = mockMbMap.getStyle();
|
||||
|
||||
const nextStyle = getMockStyle(nextLayerList);
|
||||
|
@ -169,13 +171,23 @@ describe('mb/utils', () => {
|
|||
const currentStyle = getMockStyle(currentLayerList);
|
||||
const mockMbMap = new MockMbMap(currentStyle);
|
||||
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList);
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer);
|
||||
const removedStyle = mockMbMap.getStyle();
|
||||
|
||||
const nextStyle = getMockStyle(nextLayerList);
|
||||
expect(removedStyle).toEqual(nextStyle);
|
||||
});
|
||||
|
||||
test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', async () => {
|
||||
const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]);
|
||||
const mockMbMap = new MockMbMap(styleWithSpatialFilters);
|
||||
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, [], spatialFilterLayer);
|
||||
expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncLayerOrderForSingleLayer', () => {
|
||||
test('should move bar layer in front of foo layer', async () => {
|
||||
const fooLayer = makeSingleSourceMockLayer('foo');
|
||||
const barLayer = makeSingleSourceMockLayer('bar');
|
||||
|
@ -250,40 +262,4 @@ describe('mb/utils', () => {
|
|||
const nextStyle = getMockStyle(nextLayerListOrder);
|
||||
expect(orderedStyle).toEqual(nextStyle);
|
||||
});
|
||||
|
||||
test('should reorder foo and bar and remove baz', async () => {
|
||||
const bazLayer = makeSingleSourceMockLayer('baz');
|
||||
const fooLayer = makeSingleSourceMockLayer('foo');
|
||||
const barLayer = makeSingleSourceMockLayer('bar');
|
||||
|
||||
const currentLayerOrder = [bazLayer, fooLayer, barLayer];
|
||||
const nextLayerListOrder = [barLayer, fooLayer];
|
||||
|
||||
const currentStyle = getMockStyle(currentLayerOrder);
|
||||
const mockMbMap = new MockMbMap(currentStyle);
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder);
|
||||
syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder);
|
||||
const orderedStyle = mockMbMap.getStyle();
|
||||
|
||||
const nextStyle = getMockStyle(nextLayerListOrder);
|
||||
expect(orderedStyle).toEqual(nextStyle);
|
||||
});
|
||||
|
||||
test('should reorder foo and bar and remove baz, when having multi-source multi-layer data', async () => {
|
||||
const bazLayer = makeMultiSourceMockLayer('baz');
|
||||
const fooLayer = makeSingleSourceMockLayer('foo');
|
||||
const barLayer = makeMultiSourceMockLayer('bar');
|
||||
|
||||
const currentLayerOrder = [bazLayer, fooLayer, barLayer];
|
||||
const nextLayerListOrder = [barLayer, fooLayer];
|
||||
|
||||
const currentStyle = getMockStyle(currentLayerOrder);
|
||||
const mockMbMap = new MockMbMap(currentStyle);
|
||||
removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder);
|
||||
syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder);
|
||||
const orderedStyle = mockMbMap.getStyle();
|
||||
|
||||
const nextStyle = getMockStyle(nextLayerListOrder);
|
||||
expect(orderedStyle).toEqual(nextStyle);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,11 +7,16 @@
|
|||
import _ from 'lodash';
|
||||
import { RGBAImage } from './image_utils';
|
||||
|
||||
export function removeOrphanedSourcesAndLayers(mbMap, layerList) {
|
||||
export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) {
|
||||
const mbStyle = mbMap.getStyle();
|
||||
|
||||
const mbLayerIdsToRemove = [];
|
||||
mbStyle.layers.forEach(mbLayer => {
|
||||
// ignore mapbox layers from spatial filter layer
|
||||
if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = layerList.find(layer => {
|
||||
return layer.ownsMbLayerId(mbLayer.id);
|
||||
});
|
||||
|
@ -24,6 +29,11 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList) {
|
|||
const mbSourcesToRemove = [];
|
||||
for (const mbSourceId in mbStyle.sources) {
|
||||
if (mbStyle.sources.hasOwnProperty(mbSourceId)) {
|
||||
// ignore mapbox sources from spatial filter layer
|
||||
if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layer = layerList.find(layer => {
|
||||
return layer.ownsMbSourceId(mbSourceId);
|
||||
});
|
||||
|
@ -35,6 +45,21 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList) {
|
|||
mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId));
|
||||
}
|
||||
|
||||
export function moveLayerToTop(mbMap, layer) {
|
||||
const mbStyle = mbMap.getStyle();
|
||||
|
||||
if (!mbStyle.layers || mbStyle.layers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
layer.getMbLayerIds().forEach(mbLayerId => {
|
||||
const mbLayer = mbMap.getLayer(mbLayerId);
|
||||
if (mbLayer) {
|
||||
mbMap.moveLayer(mbLayerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is function assumes only a single layer moved in the layerList, compared to mbMap
|
||||
* It is optimized to minimize the amount of mbMap.moveLayer calls.
|
||||
|
@ -47,9 +72,12 @@ export function syncLayerOrderForSingleLayer(mbMap, layerList) {
|
|||
}
|
||||
|
||||
const mbLayers = mbMap.getStyle().layers.slice();
|
||||
const layerIds = mbLayers.map(mbLayer => {
|
||||
const layerIds = [];
|
||||
mbLayers.forEach(mbLayer => {
|
||||
const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id));
|
||||
return layer.getId();
|
||||
if (layer) {
|
||||
layerIds.push(layer.getId());
|
||||
}
|
||||
});
|
||||
|
||||
const currentLayerOrderLayerIds = _.uniq(layerIds);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
syncLayerOrderForSingleLayer,
|
||||
removeOrphanedSourcesAndLayers,
|
||||
addSpritesheetToMap,
|
||||
moveLayerToTop,
|
||||
} from './utils';
|
||||
import { getGlyphUrl, isRetina } from '../../../meta';
|
||||
import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants';
|
||||
|
@ -74,7 +75,7 @@ export class MBMapContainer extends React.Component {
|
|||
}
|
||||
|
||||
_debouncedSync = _.debounce(() => {
|
||||
if (this._isMounted || !this.props.isMapReady) {
|
||||
if (this._isMounted && this.props.isMapReady) {
|
||||
if (!this.state.hasSyncedLayerList) {
|
||||
this.setState(
|
||||
{
|
||||
|
@ -86,6 +87,7 @@ export class MBMapContainer extends React.Component {
|
|||
}
|
||||
);
|
||||
}
|
||||
this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap);
|
||||
this._syncSettings();
|
||||
}
|
||||
}, 256);
|
||||
|
@ -260,9 +262,14 @@ export class MBMapContainer extends React.Component {
|
|||
};
|
||||
|
||||
_syncMbMapWithLayerList = () => {
|
||||
removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList);
|
||||
removeOrphanedSourcesAndLayers(
|
||||
this.state.mbMap,
|
||||
this.props.layerList,
|
||||
this.props.spatialFiltersLayer
|
||||
);
|
||||
this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap));
|
||||
syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList);
|
||||
moveLayerToTop(this.state.mbMap, this.props.spatialFiltersLayer);
|
||||
};
|
||||
|
||||
_syncMbMapWithInspector = () => {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { MapSettings } from '../../reducers/map';
|
||||
import { NavigationPanel } from './navigation_panel';
|
||||
import { SpatialFiltersPanel } from './spatial_filters_panel';
|
||||
|
||||
interface Props {
|
||||
cancelChanges: () => void;
|
||||
|
@ -60,6 +61,8 @@ export function MapSettingsPanel({
|
|||
<div className="mapLayerPanel__body">
|
||||
<div className="mapLayerPanel__bodyOverflow">
|
||||
<NavigationPanel settings={settings} updateMapSetting={updateMapSetting} />
|
||||
<EuiSpacer size="s" />
|
||||
<SpatialFiltersPanel settings={settings} updateMapSetting={updateMapSetting} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { MapSettings } from '../../reducers/map';
|
||||
import { AlphaSlider } from '../../components/alpha_slider';
|
||||
import { MbValidatedColorPicker } from '../../layers/styles/vector/components/color/mb_validated_color_picker';
|
||||
|
||||
interface Props {
|
||||
settings: MapSettings;
|
||||
updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void;
|
||||
}
|
||||
|
||||
export function SpatialFiltersPanel({ settings, updateMapSetting }: Props) {
|
||||
const onAlphaChange = (alpha: number) => {
|
||||
updateMapSetting('spatialFiltersAlpa', alpha);
|
||||
};
|
||||
|
||||
const onFillColorChange = (color: string) => {
|
||||
updateMapSetting('spatialFiltersFillColor', color);
|
||||
};
|
||||
|
||||
const onLineColorChange = (color: string) => {
|
||||
updateMapSetting('spatialFiltersLineColor', color);
|
||||
};
|
||||
|
||||
const onShowSpatialFiltersChange = (event: EuiSwitchEvent) => {
|
||||
updateMapSetting('showSpatialFilters', event.target.checked);
|
||||
};
|
||||
|
||||
const renderStyleInputs = () => {
|
||||
if (!settings.showSpatialFilters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlphaSlider alpha={settings.spatialFiltersAlpa} onChange={onAlphaChange} />
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.mapSettingsPanel.spatialFiltersFillColorLabel', {
|
||||
defaultMessage: 'Fill color',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<MbValidatedColorPicker
|
||||
color={settings.spatialFiltersFillColor}
|
||||
onChange={onFillColorChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.maps.mapSettingsPanel.spatialFiltersLineColorLabel', {
|
||||
defaultMessage: 'Border color',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<MbValidatedColorPicker
|
||||
color={settings.spatialFiltersLineColor}
|
||||
onChange={onLineColorChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.maps.mapSettingsPanel.spatialFiltersTitle"
|
||||
defaultMessage="Spatial filters"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.maps.mapSettingsPanel.showSpatialFiltersLabel', {
|
||||
defaultMessage: 'Show spatial filters on map',
|
||||
})}
|
||||
checked={settings.showSpatialFilters}
|
||||
onChange={onShowSpatialFiltersChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{renderStyleInputs()}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '../common/constants';
|
||||
import { getEsSpatialRelationLabel } from '../common/i18n_getters';
|
||||
import { SPATIAL_FILTER_TYPE } from './kibana_services';
|
||||
import turfCircle from '@turf/circle';
|
||||
|
||||
function ensureGeoField(type) {
|
||||
const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE];
|
||||
|
@ -330,7 +331,7 @@ export function createDistanceFilterWithMeta({
|
|||
values: {
|
||||
distanceKm,
|
||||
geoFieldName,
|
||||
pointLabel: point.join(','),
|
||||
pointLabel: point.join(', '),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -451,3 +452,40 @@ export function clamp(val, min, max) {
|
|||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFeaturesFromFilters(filters) {
|
||||
const features = [];
|
||||
filters
|
||||
.filter(filter => {
|
||||
return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE;
|
||||
})
|
||||
.forEach(filter => {
|
||||
let geometry;
|
||||
if (filter.geo_distance && filter.geo_distance[filter.meta.key]) {
|
||||
const distanceSplit = filter.geo_distance.distance.split('km');
|
||||
const distance = parseFloat(distanceSplit[0]);
|
||||
const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance);
|
||||
geometry = circleFeature.geometry;
|
||||
} else if (
|
||||
filter.geo_shape &&
|
||||
filter.geo_shape[filter.meta.key] &&
|
||||
filter.geo_shape[filter.meta.key].shape
|
||||
) {
|
||||
geometry = filter.geo_shape[filter.meta.key].shape;
|
||||
} else {
|
||||
// do not know how to convert spatial filter to geometry
|
||||
// this includes pre-indexed shapes
|
||||
return;
|
||||
}
|
||||
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry,
|
||||
properties: {
|
||||
filter: filter.meta.alias,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return features;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
createExtentFilter,
|
||||
convertMapExtentToPolygon,
|
||||
roundCoordinates,
|
||||
extractFeaturesFromFilters,
|
||||
} from './elasticsearch_geo_utils';
|
||||
import { indexPatterns } from '../../../../src/plugins/data/public';
|
||||
|
||||
|
@ -503,3 +504,131 @@ describe('roundCoordinates', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFeaturesFromFilters', () => {
|
||||
it('should ignore non-spatial filers', () => {
|
||||
const phraseFilter = {
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
key: 'machine.os',
|
||||
negate: false,
|
||||
params: {
|
||||
query: 'ios',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'machine.os': 'ios',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should convert geo_distance filter to feature', () => {
|
||||
const spatialFilter = {
|
||||
geo_distance: {
|
||||
distance: '1096km',
|
||||
'geo.coordinates': [-89.87125, 53.49454],
|
||||
},
|
||||
meta: {
|
||||
alias: 'geo.coordinates within 1096km of -89.87125,53.49454',
|
||||
disabled: false,
|
||||
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
key: 'geo.coordinates',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
const features = extractFeaturesFromFilters([spatialFilter]);
|
||||
expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]);
|
||||
expect(features[0].properties).toEqual({
|
||||
filter: 'geo.coordinates within 1096km of -89.87125,53.49454',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert geo_shape filter to feature', () => {
|
||||
const spatialFilter = {
|
||||
geo_shape: {
|
||||
'geo.coordinates': {
|
||||
relation: 'INTERSECTS',
|
||||
shape: {
|
||||
coordinates: [
|
||||
[
|
||||
[-101.21639, 48.1413],
|
||||
[-101.21639, 41.84905],
|
||||
[-90.95149, 41.84905],
|
||||
[-90.95149, 48.1413],
|
||||
[-101.21639, 48.1413],
|
||||
],
|
||||
],
|
||||
type: 'Polygon',
|
||||
},
|
||||
},
|
||||
ignore_unmapped: true,
|
||||
},
|
||||
meta: {
|
||||
alias: 'geo.coordinates in bounds',
|
||||
disabled: false,
|
||||
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
key: 'geo.coordinates',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractFeaturesFromFilters([spatialFilter])).toEqual([
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[-101.21639, 48.1413],
|
||||
[-101.21639, 41.84905],
|
||||
[-90.95149, 41.84905],
|
||||
[-90.95149, 48.1413],
|
||||
[-101.21639, 48.1413],
|
||||
],
|
||||
],
|
||||
},
|
||||
properties: {
|
||||
filter: 'geo.coordinates in bounds',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore geo_shape filter with pre-index shape', () => {
|
||||
const spatialFilter = {
|
||||
geo_shape: {
|
||||
'geo.coordinates': {
|
||||
indexed_shape: {
|
||||
id: 's5gldXEBkTB2HMwpC8y0',
|
||||
index: 'world_countries_v1',
|
||||
path: 'coordinates',
|
||||
},
|
||||
relation: 'INTERSECTS',
|
||||
},
|
||||
ignore_unmapped: true,
|
||||
},
|
||||
meta: {
|
||||
alias: 'geo.coordinates in multipolygon',
|
||||
disabled: false,
|
||||
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
key: 'geo.coordinates',
|
||||
negate: false,
|
||||
type: 'spatial_filter',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,5 +11,9 @@ export function getDefaultMapSettings(): MapSettings {
|
|||
return {
|
||||
maxZoom: MAX_ZOOM,
|
||||
minZoom: MIN_ZOOM,
|
||||
showSpatialFilters: true,
|
||||
spatialFiltersAlpa: 0.3,
|
||||
spatialFiltersFillColor: '#DA8B45',
|
||||
spatialFiltersLineColor: '#DA8B45',
|
||||
};
|
||||
}
|
||||
|
|
4
x-pack/plugins/maps/public/reducers/map.d.ts
vendored
4
x-pack/plugins/maps/public/reducers/map.d.ts
vendored
|
@ -42,6 +42,10 @@ export type MapContext = {
|
|||
export type MapSettings = {
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
showSpatialFilters: boolean;
|
||||
spatialFiltersAlpa: number;
|
||||
spatialFiltersFillColor: string;
|
||||
spatialFiltersLineColor: string;
|
||||
};
|
||||
|
||||
export type MapState = {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { AnyAction } from 'redux';
|
|||
import { MapCenter } from '../../common/descriptor_types';
|
||||
import { MapStoreState } from '../reducers/store';
|
||||
import { MapSettings } from '../reducers/map';
|
||||
import { IVectorLayer } from '../layers/vector_layer';
|
||||
|
||||
export function getHiddenLayerIds(state: MapStoreState): string[];
|
||||
|
||||
|
@ -20,3 +21,5 @@ export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[
|
|||
export function getMapSettings(state: MapStoreState): MapSettings;
|
||||
|
||||
export function hasMapSettingsChanges(state: MapStoreState): boolean;
|
||||
|
||||
export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer;
|
||||
|
|
|
@ -6,27 +6,26 @@
|
|||
|
||||
import { createSelector } from 'reselect';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { TileLayer } from '../layers/tile_layer';
|
||||
|
||||
import { VectorTileLayer } from '../layers/vector_tile_layer';
|
||||
|
||||
import { VectorLayer } from '../layers/vector_layer';
|
||||
|
||||
import { HeatmapLayer } from '../layers/heatmap_layer';
|
||||
|
||||
import { BlendedVectorLayer } from '../layers/blended_vector_layer';
|
||||
|
||||
import { getTimeFilter } from '../kibana_services';
|
||||
|
||||
import { getInspectorAdapters } from '../reducers/non_serializable_instances';
|
||||
import { TiledVectorLayer } from '../layers/tiled_vector_layer';
|
||||
|
||||
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util';
|
||||
|
||||
import { InnerJoin } from '../layers/joins/inner_join';
|
||||
|
||||
import { getSourceByType } from '../layers/sources/source_registry';
|
||||
import { GeojsonFileSource } from '../layers/sources/client_file_source';
|
||||
import {
|
||||
LAYER_TYPE,
|
||||
SOURCE_DATA_ID_ORIGIN,
|
||||
STYLE_TYPE,
|
||||
VECTOR_STYLES,
|
||||
SPATIAL_FILTERS_LAYER_ID,
|
||||
} from '../../common/constants';
|
||||
import { extractFeaturesFromFilters } from '../elasticsearch_geo_utils';
|
||||
|
||||
function createLayerInstance(layerDescriptor, inspectorAdapters) {
|
||||
const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters);
|
||||
|
@ -195,6 +194,53 @@ export const getDataFilters = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const getSpatialFiltersLayer = createSelector(
|
||||
getFilters,
|
||||
getMapSettings,
|
||||
(filters, settings) => {
|
||||
const featureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
features: extractFeaturesFromFilters(filters),
|
||||
};
|
||||
const geoJsonSourceDescriptor = GeojsonFileSource.createDescriptor(
|
||||
featureCollection,
|
||||
'spatialFilters'
|
||||
);
|
||||
|
||||
return new VectorLayer({
|
||||
layerDescriptor: {
|
||||
id: SPATIAL_FILTERS_LAYER_ID,
|
||||
visible: settings.showSpatialFilters,
|
||||
alpha: settings.spatialFiltersAlpa,
|
||||
type: LAYER_TYPE.VECTOR,
|
||||
__dataRequests: [
|
||||
{
|
||||
dataId: SOURCE_DATA_ID_ORIGIN,
|
||||
data: featureCollection,
|
||||
},
|
||||
],
|
||||
style: {
|
||||
properties: {
|
||||
[VECTOR_STYLES.FILL_COLOR]: {
|
||||
type: STYLE_TYPE.STATIC,
|
||||
options: {
|
||||
color: settings.spatialFiltersFillColor,
|
||||
},
|
||||
},
|
||||
[VECTOR_STYLES.LINE_COLOR]: {
|
||||
type: STYLE_TYPE.STATIC,
|
||||
options: {
|
||||
color: settings.spatialFiltersLineColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
source: new GeojsonFileSource(geoJsonSourceDescriptor),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const getLayerList = createSelector(
|
||||
getLayerListRaw,
|
||||
getInspectorAdapters,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue