[Maps] Hide feature when it has no corresponding term join (#36617) (#40145)

This commit is contained in:
Thomas Neirynck 2019-07-02 15:30:13 -04:00 committed by GitHub
parent 1288e82208
commit af0e157f99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 391 additions and 104 deletions

View file

@ -33,6 +33,7 @@ export const ZOOM_PRECISION = 2;
export const ES_SIZE_LIMIT = 10000;
export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__';
export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn__isvisible__';
export const ES_GEO_FIELD_TYPE = {
GEO_POINT: 'geo_point',

View file

@ -162,13 +162,9 @@ export function LayerSettings(props) {
</EuiFlexGroup>
<EuiSpacer size="m"/>
{renderLabel()}
{renderZoomSliders()}
{renderAlphaSlider()}
{renderApplyGlobalQueryCheckbox()}
</EuiPanel>

View file

@ -27,8 +27,12 @@ export class LeftInnerJoin {
return false;
}
getRightMetricFields() {
return this._rightSource.getMetricFields();
}
getJoinFields() {
return this._rightSource.getMetricFields().map(({ propertyKey: name, propertyLabel: label }) => {
return this.getRightMetricFields().map(({ propertyKey: name, propertyLabel: label }) => {
return { label, name };
});
}
@ -44,21 +48,20 @@ export class LeftInnerJoin {
return this._descriptor.leftField;
}
joinPropertiesToFeatureCollection(featureCollection, propertiesMap) {
const joinFields = this._rightSource.getMetricFields();
featureCollection.features.forEach(feature => {
// Clean up old join property values
joinFields.forEach(({ propertyKey }) => {
delete feature.properties[propertyKey];
const stylePropertyName = VectorStyle.getComputedFieldName(propertyKey);
delete feature.properties[stylePropertyName];
});
const joinKey = feature.properties[this._descriptor.leftField];
if (propertiesMap && propertiesMap.has(joinKey)) {
Object.assign(feature.properties, propertiesMap.get(joinKey));
}
});
joinPropertiesToFeature(feature, propertiesMap, rightMetricFields) {
for (let j = 0; j < rightMetricFields.length; j++) {
const { propertyKey } = rightMetricFields[j];
delete feature.properties[propertyKey];
const stylePropertyName = VectorStyle.getComputedFieldName(propertyKey);
delete feature.properties[stylePropertyName];
}
const joinKey = feature.properties[this._descriptor.leftField];
if (propertiesMap && propertiesMap.has(joinKey)) {
Object.assign(feature.properties, propertiesMap.get(joinKey));
return true;
} else {
return false;
}
}
getRightJoinSource() {

View file

@ -28,45 +28,42 @@ const leftJoin = new LeftInnerJoin({
}
});
describe('joinPropertiesToFeatureCollection', () => {
describe('joinPropertiesToFeature', () => {
const COUNT_PROPERTY_NAME = '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest';
it('Should add join property to features in feature collection', () => {
const featureCollection = {
features: [
{
properties: {
iso2: 'CN',
}
}
]
};
const feature = {
properties: {
iso2: 'CN',
}
}
;
const propertiesMap = new Map();
propertiesMap.set('CN', { [COUNT_PROPERTY_NAME]: 61 });
leftJoin.joinPropertiesToFeatureCollection(featureCollection, propertiesMap);
expect(featureCollection.features[0].properties).toEqual({
leftJoin.joinPropertiesToFeature(feature, propertiesMap, [{
propertyKey: COUNT_PROPERTY_NAME
}]);
expect(feature.properties).toEqual({
iso2: 'CN',
[COUNT_PROPERTY_NAME]: 61,
});
});
it('Should delete previous join property values from features in feature collection', () => {
const featureCollection = {
features: [
{
properties: {
iso2: 'CN',
[COUNT_PROPERTY_NAME]: 61,
[`__kbn__scaled(${COUNT_PROPERTY_NAME})`]: 1,
}
}
]
it('Should delete previous join property values from feature', () => {
const feature = {
properties: {
iso2: 'CN',
[COUNT_PROPERTY_NAME]: 61,
[`__kbn__scaled(${COUNT_PROPERTY_NAME})`]: 1,
}
};
const propertiesMap = new Map();
leftJoin.joinPropertiesToFeatureCollection(featureCollection, propertiesMap);
expect(featureCollection.features[0].properties).toEqual({
leftJoin.joinPropertiesToFeature(feature, propertiesMap, [{
propertyKey: COUNT_PROPERTY_NAME
}]);
expect(feature.properties).toEqual({
iso2: 'CN',
});
});

View file

@ -9,7 +9,12 @@ import React from 'react';
import { AbstractLayer } from './layer';
import { VectorStyle } from './styles/vector_style';
import { LeftInnerJoin } from './joins/left_inner_join';
import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, GEO_JSON_TYPE } from '../../../common/constants';
import {
GEO_JSON_TYPE,
FEATURE_ID_PROPERTY_NAME,
SOURCE_DATA_ID_ORIGIN,
FEATURE_VISIBLE_PROPERTY_NAME
} from '../../../common/constants';
import _ from 'lodash';
import { JoinTooltipProperty } from './tooltips/join_tooltip_property';
import { isRefreshOnlyQuery } from './util/is_refresh_only_query';
@ -21,28 +26,44 @@ const EMPTY_FEATURE_COLLECTION = {
features: []
};
const CLOSED_SHAPE_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON]
const VISIBILITY_FILTER_CLAUSE = ['all',
[
'==',
['get', FEATURE_VISIBLE_PROPERTY_NAME],
true
]
];
const ALL_SHAPE_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING]
const FILL_LAYER_MB_FILTER = [
...VISIBILITY_FILTER_CLAUSE,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON]
]
];
const POINT_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT]
const LINE_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING]
]
];
const POINT_LAYER_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT]
]
];
let idCounter = 0;
function generateNumericalId() {
const newId = idCounter < Number.MAX_SAFE_INTEGER ? idCounter : 0;
idCounter = newId + 1;
@ -68,7 +89,7 @@ export class VectorLayer extends AbstractLayer {
constructor(options) {
super(options);
this._joins = [];
this._joins = [];
if (options.layerDescriptor.joins) {
options.layerDescriptor.joins.forEach((joinDescriptor) => {
this._joins.push(new LeftInnerJoin(joinDescriptor, this._source.getInspectorAdapters()));
@ -115,25 +136,39 @@ export class VectorLayer extends AbstractLayer {
getCustomIconAndTooltipContent() {
const sourceDataRequest = this.getSourceDataRequest();
const featureCollection = sourceDataRequest ? sourceDataRequest.getData() : null;
const noResultsIcon = (
<EuiIcon
size="m"
color="subdued"
type="minusInCircle"
/>
);
if (!featureCollection || featureCollection.features.length === 0) {
return {
icon: (
<EuiIcon
size="m"
color="subdued"
type="minusInCircle"
/>
),
icon: noResultsIcon,
tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundTooltip', {
defaultMessage: `No results found.`
})
};
}
if (this._joins.length &&
!featureCollection.features.some((feature) => feature.properties[FEATURE_VISIBLE_PROPERTY_NAME])
) {
return {
icon: noResultsIcon,
tooltipContent: i18n.translate('xpack.maps.vectorLayer.noResultsFoundInJoinTooltip', {
defaultMessage: `No matching results found in term joins`
})
};
}
return {
icon: this._style.getIcon(),
tooltipContent: this._source.getSourceTooltipContent(sourceDataRequest)
};
}
getLayerTypeIconName() {
@ -153,7 +188,7 @@ export class VectorLayer extends AbstractLayer {
if (!featureCollection) {
return null;
}
const bbox = turf.bbox(featureCollection);
const bbox = turf.bbox(featureCollection);
return {
min_lon: bbox[0],
min_lat: bbox[1],
@ -331,7 +366,7 @@ export class VectorLayer extends AbstractLayer {
join: join,
propertiesMap: propertiesMap,
};
} catch(e) {
} catch (e) {
onLoadError(sourceDataId, requestToken, `Join error: ${e.message}`);
return {
dataHasChanged: false,
@ -368,6 +403,43 @@ export class VectorLayer extends AbstractLayer {
};
}
async _performInnerJoins(sourceResult, joinStates, updateSourceData) {
//should update the store if
//-- source result was refreshed
//-- any of the join configurations changed (joinState changed)
//-- visibility of any of the features has changed
let shouldUpdateStore = sourceResult.refreshed || joinStates.some((joinState) => joinState.dataHasChanged);
if (!shouldUpdateStore) {
return;
}
for (let i = 0; i < sourceResult.featureCollection.features.length; i++) {
const feature = sourceResult.featureCollection.features[i];
const oldVisbility = feature.properties[FEATURE_VISIBLE_PROPERTY_NAME];
let isFeatureVisible = true;
for (let j = 0; j < joinStates.length; j++) {
const joinState = joinStates[j];
const leftInnerJoin = joinState.join;
const rightMetricFields = leftInnerJoin.getRightMetricFields();
const canJoinOnCurrent = leftInnerJoin.joinPropertiesToFeature(feature, joinState.propertiesMap, rightMetricFields);
isFeatureVisible = isFeatureVisible && canJoinOnCurrent;
}
if (oldVisbility !== isFeatureVisible) {
shouldUpdateStore = true;
}
feature.properties[FEATURE_VISIBLE_PROPERTY_NAME] = isFeatureVisible;
}
if (shouldUpdateStore) {
updateSourceData({ ...sourceResult.featureCollection });
}
}
async _syncSource({ startLoading, stopLoading, onLoadError, dataFilters }) {
const requestToken = Symbol(`layer-source-refresh:${ this.getId()} - source`);
@ -394,7 +466,7 @@ export class VectorLayer extends AbstractLayer {
};
} catch (error) {
onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message);
return {
return {
refreshed: false
};
}
@ -415,26 +487,13 @@ export class VectorLayer extends AbstractLayer {
}
const sourceResult = await this._syncSource({ startLoading, stopLoading, onLoadError, dataFilters });
if (sourceResult.featureCollection && sourceResult.featureCollection.features.length) {
const joinStates = await this._syncJoins({ startLoading, stopLoading, onLoadError, dataFilters });
const activeJoinStates = joinStates.filter(joinState => {
// Perform join when
// - source data changed but join data has not
// - join data changed but source data has not
// - both source and join data changed
return sourceResult.refreshed || joinState.dataHasChanged;
});
if (activeJoinStates.length) {
activeJoinStates.forEach(joinState => {
joinState.join.joinPropertiesToFeatureCollection(
sourceResult.featureCollection,
joinState.propertiesMap);
});
updateSourceData(sourceResult.featureCollection);
}
if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) {
return;
}
const joinStates = await this._syncJoins({ startLoading, stopLoading, onLoadError, dataFilters });
await this._performInnerJoins(sourceResult, joinStates, updateSourceData);
}
_getSourceFeatureCollection() {
@ -507,7 +566,7 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
mbMap.setFilter(pointLayerId, POINT_MB_FILTER);
mbMap.setFilter(pointLayerId, POINT_LAYER_MB_FILTER);
}
this._style.setMBPaintPropertiesForPoints({
@ -528,7 +587,7 @@ export class VectorLayer extends AbstractLayer {
type: 'symbol',
source: sourceId,
});
mbMap.setFilter(symbolLayerId, POINT_MB_FILTER);
mbMap.setFilter(symbolLayerId, POINT_LAYER_MB_FILTER);
}
this._style.setMBSymbolPropertiesForPoints({
@ -549,7 +608,7 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
mbMap.setFilter(fillLayerId, CLOSED_SHAPE_MB_FILTER);
mbMap.setFilter(fillLayerId, FILL_LAYER_MB_FILTER);
}
if (!mbMap.getLayer(lineLayerId)) {
mbMap.addLayer({
@ -558,7 +617,7 @@ export class VectorLayer extends AbstractLayer {
source: sourceId,
paint: {}
});
mbMap.setFilter(lineLayerId, ALL_SHAPE_MB_FILTER);
mbMap.setFilter(lineLayerId, LINE_LAYER_MB_FILTER);
}
this._style.setMBPaintProperties({
alpha: this.getAlpha(),
@ -594,11 +653,11 @@ export class VectorLayer extends AbstractLayer {
}
_getMbPointLayerId() {
return this.getId() + '_circle';
return this.getId() + '_circle';
}
_getMbSymbolLayerId() {
return this.getId() + '_symbol';
return this.getId() + '_symbol';
}
_getMbLineLayerId() {
@ -630,7 +689,7 @@ export class VectorLayer extends AbstractLayer {
async getPropertiesForTooltip(properties) {
let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties);
let allTooltips = await this._source.filterAndFormatPropertiesToHtml(properties);
this._addJoinsToSourceTooltips(allTooltips);

View file

@ -6,6 +6,8 @@
import expect from '@kbn/expect';
import { MAPBOX_STYLES } from './mapbox_styles';
const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name';
const EXPECTED_JOIN_VALUES = {
alpha: 10,
@ -78,20 +80,28 @@ export default function ({ getPageObjects, getService }) {
});
//circle layer for points
// eslint-disable-next-line max-len
expect(layersForVectorSource[0]).to.eql({ 'id': 'n1t6f_circle', 'type': 'circle', 'source': 'n1t6f', 'minzoom': 0, 'maxzoom': 24, 'filter': ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], 'layout': { 'visibility': 'visible' }, 'paint': { 'circle-color': ['interpolate', ['linear'], ['coalesce', ['feature-state', '__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)'], -1], -1, 'rgba(0,0,0,0)', 0, '#f7faff', 0.125, '#ddeaf7', 0.25, '#c5daee', 0.375, '#9dc9e0', 0.5, '#6aadd5', 0.625, '#4191c5', 0.75, '#2070b4', 0.875, '#072f6b'], 'circle-opacity': 0.75, 'circle-stroke-color': '#FFFFFF', 'circle-stroke-opacity': 0.75, 'circle-stroke-width': 1, 'circle-radius': 10 } });
expect(layersForVectorSource[0]).to.eql(MAPBOX_STYLES.POINT_LAYER);
//fill layer
// eslint-disable-next-line max-len
expect(layersForVectorSource[1]).to.eql({ 'id': 'n1t6f_fill', 'type': 'fill', 'source': 'n1t6f', 'minzoom': 0, 'maxzoom': 24, 'filter': ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], 'layout': { 'visibility': 'visible' }, 'paint': { 'fill-color': ['interpolate', ['linear'], ['coalesce', ['feature-state', '__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)'], -1], -1, 'rgba(0,0,0,0)', 0, '#f7faff', 0.125, '#ddeaf7', 0.25, '#c5daee', 0.375, '#9dc9e0', 0.5, '#6aadd5', 0.625, '#4191c5', 0.75, '#2070b4', 0.875, '#072f6b'], 'fill-opacity': 0.75 } }
);
expect(layersForVectorSource[1]).to.eql(MAPBOX_STYLES.FILL_LAYER);
//line layer for borders
// eslint-disable-next-line max-len
expect(layersForVectorSource[2]).to.eql({ 'id': 'n1t6f_line', 'type': 'line', 'source': 'n1t6f', 'minzoom': 0, 'maxzoom': 24, 'filter': ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon'], ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']], 'layout': { 'visibility': 'visible' }, 'paint': { 'line-color': '#FFFFFF', 'line-opacity': 0.75, 'line-width': 1 } });
expect(layersForVectorSource[2]).to.eql(MAPBOX_STYLES.LINE_LAYER);
});
it('should flag only the joined features as visible', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID];
const visibilitiesOfFeatures = vectorSource.data.features.map(feature => {
return feature.properties.__kbn__isvisible__;
});
expect(visibilitiesOfFeatures).to.eql([true, true, true, false]);
});
describe('query bar', () => {
before(async () => {
await PageObjects.maps.setAndSubmitQuery('prop1 < 10 or _index : "geo_shapes*"');
@ -156,6 +166,19 @@ export default function ({ getPageObjects, getService }) {
const max = split[2];
expect(max).to.equal('12');
});
it('should flag only the joined features as visible', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID];
const visibilitiesOfFeatures = vectorSource.data.features.map(feature => {
return feature.properties.__kbn__isvisible__;
});
expect(visibilitiesOfFeatures).to.eql([false, false, true, false]);
});
});
describe('inspector', () => {

View file

@ -0,0 +1,208 @@
/*
* 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.
*/
export const MAPBOX_STYLES = {
POINT_LAYER: {
'id': 'n1t6f_circle',
'type': 'circle',
'source': 'n1t6f',
'minzoom': 0,
'maxzoom': 24,
'filter': [
'all',
[
'==',
['get', '__kbn__isvisible__'],
true
],
[
'any',
[
'==',
[
'geometry-type'
],
'Point'
],
[
'==',
[
'geometry-type'
],
'MultiPoint'
]
]
],
'layout': {
'visibility': 'visible'
},
'paint': {
'circle-color': [
'interpolate',
[
'linear'
],
[
'coalesce',
[
'feature-state',
'__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)'
],
-1
],
-1,
'rgba(0,0,0,0)',
0,
'#f7faff',
0.125,
'#ddeaf7',
0.25,
'#c5daee',
0.375,
'#9dc9e0',
0.5,
'#6aadd5',
0.625,
'#4191c5',
0.75,
'#2070b4',
0.875,
'#072f6b'
],
'circle-opacity': 0.75,
'circle-stroke-color': '#FFFFFF',
'circle-stroke-opacity': 0.75,
'circle-stroke-width': 1,
'circle-radius': 10
}
},
FILL_LAYER: {
'id': 'n1t6f_fill',
'type': 'fill',
'source': 'n1t6f',
'minzoom': 0,
'maxzoom': 24,
'filter': [
'all',
[
'==',
['get', '__kbn__isvisible__'],
true
],
[
'any',
[
'==',
[
'geometry-type'
],
'Polygon'
],
[
'==',
[
'geometry-type'
],
'MultiPolygon'
]
]
],
'layout': {
'visibility': 'visible'
},
'paint': {
'fill-color': [
'interpolate',
[
'linear'
],
[
'coalesce',
[
'feature-state',
'__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)'
],
-1
],
-1,
'rgba(0,0,0,0)',
0,
'#f7faff',
0.125,
'#ddeaf7',
0.25,
'#c5daee',
0.375,
'#9dc9e0',
0.5,
'#6aadd5',
0.625,
'#4191c5',
0.75,
'#2070b4',
0.875,
'#072f6b'
],
'fill-opacity': 0.75
}
},
LINE_LAYER: {
'id': 'n1t6f_line',
'type': 'line',
'source': 'n1t6f',
'minzoom': 0,
'maxzoom': 24,
'filter': [
'all',
[
'==',
['get', '__kbn__isvisible__'],
true
],
[
'any',
[
'==',
[
'geometry-type'
],
'Polygon'
],
[
'==',
[
'geometry-type'
],
'MultiPolygon'
],
[
'==',
[
'geometry-type'
],
'LineString'
],
[
'==',
[
'geometry-type'
],
'MultiLineString'
]
]
],
'layout': {
'visibility': 'visible'
},
'paint': {
'line-color': '#FFFFFF',
'line-opacity': 0.75,
'line-width': 1
}
}
};