diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts index 479cbb140fc70..0ae1513ae5d1b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.test.ts @@ -54,6 +54,61 @@ describe('mapSpatialFilter()', () => { expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); }); + test('should return the key for matching multi field filter', async () => { + const filter = { + meta: { + alias: 'my spatial filter', + isMultiIndex: true, + type: FILTERS.SPATIAL_FILTER, + } as FilterMeta, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_distance: { + distance: '1000km', + location: [120, 30], + }, + }, + ], + }, + }, + ], + }, + }, + } as Filter; + const result = mapSpatialFilter(filter); + + expect(result).toHaveProperty('key', 'query'); + expect(result).toHaveProperty('value', ''); + expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER); + }); + test('should return undefined for none matching', async (done) => { const filter = { meta: { diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts index 0703bc055a39b..229257c1a7d81 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_spatial_filter.ts @@ -22,5 +22,18 @@ export const mapSpatialFilter = (filter: Filter) => { value: '', }; } + + if ( + filter.meta && + filter.meta.type === FILTERS.SPATIAL_FILTER && + filter.meta.isMultiIndex && + filter.query?.bool?.should + ) { + return { + key: 'query', + type: filter.meta.type, + value: '', + }; + } throw filter; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 102434ffda161..4092ba49888bc 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -11,7 +11,7 @@ import { ReactNode } from 'react'; import { GeoJsonProperties } from 'geojson'; import { Geometry } from 'geojson'; import { Query } from '../../../../../src/plugins/data/common'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; +import { DRAW_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; export type MapExtent = { minLon: number; @@ -70,9 +70,6 @@ export type DrawState = { actionId: string; drawType: DRAW_TYPE; filterLabel?: string; // point radius filter alias - geoFieldName?: string; - geoFieldType?: ES_GEO_FIELD_TYPE; geometryLabel?: string; - indexPatternId?: string; relation?: ES_SPATIAL_RELATIONS; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index c2ca952c3e8c9..e6b27e78fd36b 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -9,9 +9,7 @@ import { hitsToGeoJson, geoPointToGeometry, geoShapeToGeometry, - createExtentFilter, roundCoordinates, - extractFeaturesFromFilters, makeESBbox, scaleBounds, } from './elasticsearch_geo_utils'; @@ -388,94 +386,6 @@ describe('geoShapeToGeometry', () => { }); }); -describe('createExtentFilter', () => { - it('should return elasticsearch geo_bounding_box filter', () => { - const mapExtent = { - maxLat: 39, - maxLon: -83, - minLat: 35, - minLon: -89, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-89, 39], - bottom_right: [-83, 35], - }, - }); - }); - - it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => { - const mapExtent = { - maxLat: 120, - maxLon: 200, - minLat: -100, - minLon: -190, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-180, 89], - bottom_right: [180, -89], - }, - }); - }); - - it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { - const mapExtent = { - maxLat: 39, - maxLon: 200, - minLat: 35, - minLon: 100, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - const leftLon = filter.geo_bounding_box.location.top_left[0]; - const rightLon = filter.geo_bounding_box.location.bottom_right[0]; - expect(leftLon).toBeGreaterThan(rightLon); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [100, 39], - bottom_right: [-160, 35], - }, - }); - }); - - it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { - const mapExtent = { - maxLat: 39, - maxLon: -100, - minLat: 35, - minLon: -200, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - const leftLon = filter.geo_bounding_box.location.top_left[0]; - const rightLon = filter.geo_bounding_box.location.bottom_right[0]; - expect(leftLon).toBeGreaterThan(rightLon); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [160, 39], - bottom_right: [-100, 35], - }, - }); - }); - - it('should clamp longitudes to -180 to 180 when longitude wraps globe', () => { - const mapExtent = { - maxLat: 39, - maxLon: 209, - minLat: 35, - minLon: -191, - }; - const filter = createExtentFilter(mapExtent, [geoFieldName]); - expect(filter.geo_bounding_box).toEqual({ - location: { - top_left: [-180, 39], - bottom_right: [180, 35], - }, - }); - }); -}); - describe('roundCoordinates', () => { it('should set coordinates precision', () => { const coordinates = [ @@ -492,134 +402,6 @@ 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([]); - }); -}); - describe('makeESBbox', () => { it('Should invert Y-axis', () => { const bbox = makeESBbox({ diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index e47afce77f779..8033f8d187fd5 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -16,61 +16,13 @@ import { BBox } from '@turf/helpers'; import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, - ES_SPATIAL_RELATIONS, GEO_JSON_TYPE, POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, LAT_INDEX, } from '../constants'; -import { getEsSpatialRelationLabel } from '../i18n_getters'; -import { Filter, FilterMeta, FILTERS } from '../../../../../src/plugins/data/common'; import { MapExtent } from '../descriptor_types'; - -const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; - -type Coordinates = Position | Position[] | Position[][] | Position[][][]; - -// Elasticsearch stores more then just GeoJSON. -// 1) geometry.type as lower case string -// 2) circle and envelope types -interface ESGeometry { - type: string; - coordinates: Coordinates; -} - -export interface ESBBox { - top_left: number[]; - bottom_right: number[]; -} - -interface GeoShapeQueryBody { - shape?: Polygon; - relation?: ES_SPATIAL_RELATIONS; - indexed_shape?: PreIndexedShape; -} - -// Index signature explicitly states that anything stored in an object using a string conforms to the structure -// problem is that Elasticsearch signature also allows for other string keys to conform to other structures, like 'ignore_unmapped' -// Use intersection type to exclude certain properties from the index signature -// https://basarat.gitbook.io/typescript/type-system/index-signatures#excluding-certain-properties-from-the-index-signature -type GeoShapeQuery = { ignore_unmapped: boolean } & { [geoFieldName: string]: GeoShapeQueryBody }; - -export type GeoFilter = Filter & { - geo_bounding_box?: { - [geoFieldName: string]: ESBBox; - }; - geo_distance?: { - distance: string; - [geoFieldName: string]: Position | { lat: number; lon: number } | string; - }; - geo_shape?: GeoShapeQuery; -}; - -export interface PreIndexedShape { - index: string; - id: string | number; - path: string; -} +import { Coordinates, ESBBox, ESGeometry } from './types'; function ensureGeoField(type: string) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -349,136 +301,6 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBo return esBbox; } -export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter { - const esBbox = makeESBbox(mapExtent); - return geoFieldNames.length === 1 - ? { - geo_bounding_box: { - [geoFieldNames[0]]: esBbox, - }, - meta: { - alias: null, - disabled: false, - negate: false, - key: geoFieldNames[0], - }, - } - : { - query: { - bool: { - should: geoFieldNames.map((geoFieldName) => { - return { - bool: { - must: [ - { - exists: { - field: geoFieldName, - }, - }, - { - geo_bounding_box: { - [geoFieldName]: esBbox, - }, - }, - ], - }, - }; - }), - }, - }, - meta: { - alias: null, - disabled: false, - negate: false, - }, - }; -} - -export function createSpatialFilterWithGeometry({ - preIndexedShape, - geometry, - geometryLabel, - indexPatternId, - geoFieldName, - relation = ES_SPATIAL_RELATIONS.INTERSECTS, -}: { - preIndexedShape?: PreIndexedShape | null; - geometry: Polygon; - geometryLabel: string; - indexPatternId: string; - geoFieldName: string; - relation: ES_SPATIAL_RELATIONS; -}): GeoFilter { - const meta: FilterMeta = { - type: SPATIAL_FILTER_TYPE, - negate: false, - index: indexPatternId, - key: geoFieldName, - alias: `${geoFieldName} ${getEsSpatialRelationLabel(relation)} ${geometryLabel}`, - disabled: false, - }; - - const shapeQuery: GeoShapeQueryBody = { - relation, - }; - if (preIndexedShape) { - shapeQuery.indexed_shape = preIndexedShape; - } else { - shapeQuery.shape = geometry; - } - - return { - meta, - // Currently no way to create an object with exclude property from index signature - // typescript error for "ignore_unmapped is not assignable to type 'GeoShapeQueryBody'" expected" - // @ts-expect-error - geo_shape: { - ignore_unmapped: true, - [geoFieldName]: shapeQuery, - }, - }; -} - -export function createDistanceFilterWithMeta({ - alias, - distanceKm, - geoFieldName, - indexPatternId, - point, -}: { - alias: string; - distanceKm: number; - geoFieldName: string; - indexPatternId: string; - point: Position; -}): GeoFilter { - const meta: FilterMeta = { - 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(', '), - }, - }), - disabled: false, - }; - - return { - geo_distance: { - distance: `${distanceKm}km`, - [geoFieldName]: point, - }, - meta, - }; -} - export function roundCoordinates(coordinates: Coordinates): void { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; @@ -549,44 +371,6 @@ export function clamp(val: number, min: number, max: number): number { } } -export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { - const features: Feature[] = []; - filters - .filter((filter) => { - return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; - }) - .forEach((filter) => { - const geoFieldName = filter.meta.key!; - let geometry; - if (filter.geo_distance && filter.geo_distance[geoFieldName]) { - const distanceSplit = filter.geo_distance.distance.split('km'); - const distance = parseFloat(distanceSplit[0]); - const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); - geometry = circleFeature.geometry; - } else if ( - filter.geo_shape && - filter.geo_shape[geoFieldName] && - filter.geo_shape[geoFieldName].shape - ) { - geometry = filter.geo_shape[geoFieldName].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; -} - export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent { const width = bounds.maxLon - bounds.minLon; const height = bounds.maxLat - bounds.minLat; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts index 24dd56b217401..7073a4201f7a5 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -8,4 +8,6 @@ export * from './es_agg_utils'; export * from './convert_to_geojson'; export * from './elasticsearch_geo_utils'; +export * from './spatial_filter_utils'; +export * from './types'; export { isTotalHitsGreaterThan, TotalHits } from './total_hits'; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts new file mode 100644 index 0000000000000..d828aca4a1a00 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts @@ -0,0 +1,534 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Polygon } from 'geojson'; +import { + createDistanceFilterWithMeta, + createExtentFilter, + createSpatialFilterWithGeometry, + extractFeaturesFromFilters, +} from './spatial_filter_utils'; + +const geoFieldName = 'location'; + +describe('createExtentFilter', () => { + it('should return elasticsearch geo_bounding_box filter', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }); + }); + + it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => { + const mapExtent = { + maxLat: 120, + maxLon: 200, + minLat: -100, + minLon: -190, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 89], + bottom_right: [180, -89], + }, + }); + }); + + it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { + const mapExtent = { + maxLat: 39, + maxLon: 200, + minLat: 35, + minLon: 100, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [100, 39], + bottom_right: [-160, 35], + }, + }); + }); + + it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { + const mapExtent = { + maxLat: 39, + maxLon: -100, + minLat: 35, + minLon: -200, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [160, 39], + bottom_right: [-100, 35], + }, + }); + }); + + it('should clamp longitudes to -180 to 180 when longitude wraps globe', () => { + const mapExtent = { + maxLat: 39, + maxLon: 209, + minLat: 35, + minLon: -191, + }; + const filter = createExtentFilter(mapExtent, [geoFieldName]); + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 39], + bottom_right: [180, 35], + }, + }); + }); + + it('should support multiple geo fields', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + expect(createExtentFilter(mapExtent, [geoFieldName, 'myOtherLocation'])).toEqual({ + meta: { + alias: null, + disabled: false, + isMultiIndex: true, + negate: false, + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_bounding_box: { + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'myOtherLocation', + }, + }, + { + geo_bounding_box: { + myOtherLocation: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +describe('createSpatialFilterWithGeometry', () => { + it('should build filter for single field', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'intersects myShape', + disabled: false, + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + }, + 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, + }, + }); + }); + + it('should build filter for multiple field', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates', 'location'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'intersects myShape', + disabled: false, + isMultiIndex: true, + key: undefined, + negate: false, + type: 'spatial_filter', + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + 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, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_shape: { + location: { + 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, + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +describe('createDistanceFilterWithMeta', () => { + it('should build filter for single field', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [120, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'within 1000km of 120, 30', + disabled: false, + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + }, + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }); + }); + + it('should build filter for multiple field', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [120, 30], + distanceKm: 1000, + geoFieldNames: ['geo.coordinates', 'location'], + }); + expect(spatialFilter).toEqual({ + meta: { + alias: 'within 1000km of 120, 30', + disabled: false, + isMultiIndex: true, + key: undefined, + negate: false, + type: 'spatial_filter', + }, + query: { + bool: { + should: [ + { + bool: { + must: [ + { + exists: { + field: 'geo.coordinates', + }, + }, + { + geo_distance: { + distance: '1000km', + 'geo.coordinates': [120, 30], + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + exists: { + field: 'location', + }, + }, + { + geo_distance: { + distance: '1000km', + location: [120, 30], + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); +}); + +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 single field geo_distance filter to feature', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [-89.87125, 53.49454], + distanceKm: 1096, + geoFieldNames: ['geo.coordinates', 'location'], + }); + + const features = extractFeaturesFromFilters([spatialFilter]); + expect((features[0].geometry as Polygon).coordinates[0][0]).toEqual([ + -89.87125, + 63.35109118642093, + ]); + expect(features[0].properties).toEqual({ + filter: 'within 1096km of -89.87125, 53.49454', + }); + }); + + it('should convert multi field geo_distance filter to feature', () => { + const spatialFilter = createDistanceFilterWithMeta({ + point: [-89.87125, 53.49454], + distanceKm: 1096, + geoFieldNames: ['geo.coordinates', 'location'], + }); + + const features = extractFeaturesFromFilters([spatialFilter]); + expect((features[0].geometry as Polygon).coordinates[0][0]).toEqual([ + -89.87125, + 63.35109118642093, + ]); + expect(features[0].properties).toEqual({ + filter: 'within 1096km of -89.87125, 53.49454', + }); + }); + + it('should convert single field geo_shape filter to feature', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + 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], + ], + ], + } as Polygon, + properties: { + filter: 'intersects myShape', + }, + }, + ]); + }); + + it('should convert multi field geo_shape filter to feature', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + geometry: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates', 'location'], + }); + 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], + ], + ], + } as Polygon, + properties: { + filter: 'intersects myShape', + }, + }, + ]); + }); + + it('should ignore geo_shape filter with pre-index shape', () => { + const spatialFilter = createSpatialFilterWithGeometry({ + preIndexedShape: { + index: 'world_countries_v1', + id: 's5gldXEBkTB2HMwpC8y0', + path: 'coordinates', + }, + geometryLabel: 'myShape', + geoFieldNames: ['geo.coordinates'], + }); + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts new file mode 100644 index 0000000000000..70df9e9646f50 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Feature, Geometry, Polygon, Position } from 'geojson'; +// @ts-expect-error +import turfCircle from '@turf/circle'; +import { FilterMeta, FILTERS } from '../../../../../src/plugins/data/common'; +import { MapExtent } from '../descriptor_types'; +import { ES_SPATIAL_RELATIONS } from '../constants'; +import { getEsSpatialRelationLabel } from '../i18n_getters'; +import { GeoFilter, GeoShapeQueryBody, PreIndexedShape } from './types'; +import { makeESBbox } from './elasticsearch_geo_utils'; + +const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; + +// wrapper around boiler plate code for creating bool.should clause with nested bool.must clauses +// ensuring geoField exists prior to running geoField query +// This allows for writing a single geo filter that spans multiple indices with different geo fields. +function createMultiGeoFieldFilter( + geoFieldNames: string[], + meta: FilterMeta, + createGeoFilter: (geoFieldName: string) => Omit +): GeoFilter { + if (geoFieldNames.length === 0) { + throw new Error('Unable to create filter, geo fields not provided'); + } + + if (geoFieldNames.length === 1) { + const geoFilter = createGeoFilter(geoFieldNames[0]); + return { + meta: { + ...meta, + key: geoFieldNames[0], + }, + ...geoFilter, + }; + } + + return { + meta: { + ...meta, + key: undefined, + isMultiIndex: true, + }, + query: { + bool: { + should: geoFieldNames.map((geoFieldName) => { + return { + bool: { + must: [ + { + exists: { + field: geoFieldName, + }, + }, + createGeoFilter(geoFieldName), + ], + }, + }; + }), + }, + }, + }; +} + +export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter { + const esBbox = makeESBbox(mapExtent); + function createGeoFilter(geoFieldName: string) { + return { + geo_bounding_box: { + [geoFieldName]: esBbox, + }, + }; + } + + const meta: FilterMeta = { + alias: null, + disabled: false, + negate: false, + }; + + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +export function createSpatialFilterWithGeometry({ + preIndexedShape, + geometry, + geometryLabel, + geoFieldNames, + relation = ES_SPATIAL_RELATIONS.INTERSECTS, +}: { + preIndexedShape?: PreIndexedShape | null; + geometry?: Polygon; + geometryLabel: string; + geoFieldNames: string[]; + relation?: ES_SPATIAL_RELATIONS; +}): GeoFilter { + const meta: FilterMeta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + key: geoFieldNames.length === 1 ? geoFieldNames[0] : undefined, + alias: `${getEsSpatialRelationLabel(relation)} ${geometryLabel}`, + disabled: false, + }; + + function createGeoFilter(geoFieldName: string) { + const shapeQuery: GeoShapeQueryBody = { + relation, + }; + if (preIndexedShape) { + shapeQuery.indexed_shape = preIndexedShape; + } else if (geometry) { + shapeQuery.shape = geometry; + } else { + throw new Error('Must supply either preIndexedShape or geometry, you did not supply either'); + } + + return { + geo_shape: { + ignore_unmapped: true, + [geoFieldName]: shapeQuery, + }, + }; + } + + // Currently no way to create an object with exclude property from index signature + // typescript error for "ignore_unmapped is not assignable to type 'GeoShapeQueryBody'" expected" + // @ts-expect-error + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +export function createDistanceFilterWithMeta({ + alias, + distanceKm, + geoFieldNames, + point, +}: { + alias?: string; + distanceKm: number; + geoFieldNames: string[]; + point: Position; +}): GeoFilter { + const meta: FilterMeta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + alias: alias + ? alias + : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { + defaultMessage: 'within {distanceKm}km of {pointLabel}', + values: { + distanceKm, + pointLabel: point.join(', '), + }, + }), + disabled: false, + }; + + function createGeoFilter(geoFieldName: string) { + return { + geo_distance: { + distance: `${distanceKm}km`, + [geoFieldName]: point, + }, + }; + } + + return createMultiGeoFieldFilter(geoFieldNames, meta, createGeoFilter); +} + +function extractGeometryFromFilter(geoFieldName: string, filter: GeoFilter): Geometry | undefined { + if (filter.geo_distance && filter.geo_distance[geoFieldName]) { + const distanceSplit = filter.geo_distance.distance.split('km'); + const distance = parseFloat(distanceSplit[0]); + const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); + return circleFeature.geometry; + } + + if (filter.geo_shape && filter.geo_shape[geoFieldName] && filter.geo_shape[geoFieldName].shape) { + return filter.geo_shape[geoFieldName].shape; + } +} + +export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { + const features: Feature[] = []; + filters + .filter((filter) => { + return filter.meta.type === SPATIAL_FILTER_TYPE; + }) + .forEach((filter) => { + let geometry: Geometry | undefined; + if (filter.meta.isMultiIndex) { + const geoFieldName = filter?.query?.bool?.should?.[0]?.bool?.must?.[0]?.exists?.field; + const spatialClause = filter?.query?.bool?.should?.[0]?.bool?.must?.[1]; + if (geoFieldName && spatialClause) { + geometry = extractGeometryFromFilter(geoFieldName, spatialClause); + } + } else { + const geoFieldName = filter.meta.key; + if (geoFieldName) { + geometry = extractGeometryFromFilter(geoFieldName, filter); + } + } + + if (geometry) { + features.push({ + type: 'Feature', + geometry, + properties: { + filter: filter.meta.alias, + }, + }); + } + }); + + return features; +} diff --git a/x-pack/plugins/maps/common/elasticsearch_util/types.ts b/x-pack/plugins/maps/common/elasticsearch_util/types.ts new file mode 100644 index 0000000000000..bbb508ce69275 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/types.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Polygon, Position } from 'geojson'; +import { Filter } from '../../../../../src/plugins/data/common'; +import { ES_SPATIAL_RELATIONS } from '../constants'; + +export type Coordinates = Position | Position[] | Position[][] | Position[][][]; + +// Elasticsearch stores more then just GeoJSON. +// 1) geometry.type as lower case string +// 2) circle and envelope types +export interface ESGeometry { + type: string; + coordinates: Coordinates; +} + +export interface ESBBox { + top_left: number[]; + bottom_right: number[]; +} + +export interface GeoShapeQueryBody { + shape?: Polygon; + relation?: ES_SPATIAL_RELATIONS; + indexed_shape?: PreIndexedShape; +} + +// Index signature explicitly states that anything stored in an object using a string conforms to the structure +// problem is that Elasticsearch signature also allows for other string keys to conform to other structures, like 'ignore_unmapped' +// Use intersection type to exclude certain properties from the index signature +// https://basarat.gitbook.io/typescript/type-system/index-signatures#excluding-certain-properties-from-the-index-signature +type GeoShapeQuery = { ignore_unmapped: boolean } & { [geoFieldName: string]: GeoShapeQueryBody }; + +export type GeoFilter = Filter & { + geo_bounding_box?: { + [geoFieldName: string]: ESBBox; + }; + geo_distance?: { + distance: string; + [geoFieldName: string]: Position | { lat: number; lon: number } | string; + }; + geo_shape?: GeoShapeQuery; +}; + +export interface PreIndexedShape { + index: string; + id: string | number; + path: string; +} diff --git a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index ccbe4667b78ea..80238bf3a4d17 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should not show "within" relation when filter geometry is not closed 1`] = ` +exports[`render 1`] = ` - - - - - - - Create filter - - - -`; - -exports[`should render error message 1`] = ` - - - - - - - - - Simulated error - @@ -177,7 +70,7 @@ exports[`should render error message 1`] = ` `; -exports[`should render relation select when geo field is geo_shape 1`] = ` +exports[`should render error message 1`] = ` - - - - Create filter - - - -`; - -exports[`should render relation select without "within"-relation when geo field is geo_point 1`] = ` - - - - - - - - - - + + Simulated error + diff --git a/x-pack/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx index 14ae6b11b85c8..b5fdcbc46b932 100644 --- a/x-pack/plugins/maps/public/components/distance_filter_form.tsx +++ b/x-pack/plugins/maps/public/components/distance_filter_form.tsx @@ -16,47 +16,28 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; -import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; -import { GeoFieldWithIndex } from './geo_field_with_index'; import { ActionSelect } from './action_select'; import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; interface Props { className?: string; buttonLabel: string; - geoFields: GeoFieldWithIndex[]; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; - onSubmit: ({ - actionId, - filterLabel, - indexPatternId, - geoFieldName, - }: { - actionId: string; - filterLabel: string; - indexPatternId: string; - geoFieldName: string; - }) => void; + onSubmit: ({ actionId, filterLabel }: { actionId: string; filterLabel: string }) => void; } interface State { actionId: string; - selectedField: GeoFieldWithIndex | undefined; filterLabel: string; } export class DistanceFilterForm extends Component { state: State = { actionId: ACTION_GLOBAL_APPLY_FILTER, - selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, filterLabel: '', }; - _onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => { - this.setState({ selectedField }); - }; - _onFilterLabelChange = (e: ChangeEvent) => { this.setState({ filterLabel: e.target.value, @@ -68,14 +49,9 @@ export class DistanceFilterForm extends Component { }; _onSubmit = () => { - if (!this.state.selectedField) { - return; - } this.props.onSubmit({ actionId: this.state.actionId, filterLabel: this.state.filterLabel, - indexPatternId: this.state.selectedField.indexPatternId, - geoFieldName: this.state.selectedField.geoFieldName, }); }; @@ -95,12 +71,6 @@ export class DistanceFilterForm extends Component { /> - - { - + {this.props.buttonLabel} diff --git a/x-pack/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/plugins/maps/public/components/geo_field_with_index.ts deleted file mode 100644 index 5273bff44f8d7..0000000000000 --- a/x-pack/plugins/maps/public/components/geo_field_with_index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* 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 getIndexPatternService() to avoid -// PITA of looking up async state in downstream react consumers. -export type GeoFieldWithIndex = { - geoFieldName: string; - geoFieldType: string; - indexPatternTitle: string; - indexPatternId: string; -}; diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js index 624d3b60fe14b..2e13f63b79883 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.js @@ -18,16 +18,14 @@ import { EuiFormErrorText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; +import { ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; -import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; import { ActionSelect } from './action_select'; import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public'; export class GeometryFilterForm extends Component { static propTypes = { buttonLabel: PropTypes.string.isRequired, - geoFields: PropTypes.array.isRequired, getFilterActions: PropTypes.func, getActionContext: PropTypes.func, intitialGeometryLabel: PropTypes.string.isRequired, @@ -42,15 +40,10 @@ export class GeometryFilterForm extends Component { state = { actionId: ACTION_GLOBAL_APPLY_FILTER, - selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, }; - _onGeoFieldChange = (selectedField) => { - this.setState({ selectedField }); - }; - _onGeometryLabelChange = (e) => { this.setState({ geometryLabel: e.target.value, @@ -71,29 +64,12 @@ export class GeometryFilterForm extends Component { this.props.onSubmit({ actionId: this.state.actionId, geometryLabel: this.state.geometryLabel, - indexPatternId: this.state.selectedField.indexPatternId, - geoFieldName: this.state.selectedField.geoFieldName, relation: this.state.relation, }); }; _renderRelationInput() { - // relationship only used when filtering geo_shape fields - if (!this.state.selectedField) { - return null; - } - - const spatialRelations = - this.props.isFilterGeometryClosed && - this.state.selectedField.geoFieldType !== ES_GEO_FIELD_TYPE.GEO_POINT - ? Object.values(ES_SPATIAL_RELATIONS) - : Object.values(ES_SPATIAL_RELATIONS).filter((relation) => { - // - cannot filter by "within"-relation when filtering geometry is not closed - // - do not distinguish between intersects/within for filtering for points since they are equivalent - return relation !== ES_SPATIAL_RELATIONS.WITHIN; - }); - - const options = spatialRelations.map((relation) => { + const options = Object.values(ES_SPATIAL_RELATIONS).map((relation) => { return { value: relation, text: getEsSpatialRelationLabel(relation), @@ -137,12 +113,6 @@ export class GeometryFilterForm extends Component { /> - - {this._renderRelationInput()} {this.props.buttonLabel} diff --git a/x-pack/plugins/maps/public/components/geometry_filter_form.test.js b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js index d981caf944ab9..3ce79782788b0 100644 --- a/x-pack/plugins/maps/public/components/geometry_filter_form.test.js +++ b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js @@ -16,76 +16,14 @@ const defaultProps = { onSubmit: () => {}, }; -test('should render relation select without "within"-relation when geo field is geo_point', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); -}); - -test('should render relation select when geo field is geo_shape', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); -}); - -test('should not show "within" relation when filter geometry is not closed', async () => { - const component = shallow( - - ); +test('render', async () => { + const component = shallow(); expect(component).toMatchSnapshot(); }); test('should render error message', async () => { - const component = shallow( - - ); + const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx deleted file mode 100644 index 564b84ae84300..0000000000000 --- a/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -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: ( - - - {geoField.indexPatternTitle} - -
- {geoField.geoFieldName} -
- ), - value: createOptionId(geoField), - }; - }); - - return ( - - - - ); -} diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 02374932a4c70..26746d9ad2416 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -20,15 +20,12 @@ import { ToolbarOverlay } from '../toolbar_overlay'; import { EditLayerPanel } from '../edit_layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; -import { getIndexPatternsFromIds } from '../../index_pattern_util'; -import { ES_GEO_FIELD_TYPE, RawValue } from '../../../common/constants'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; +import { RawValue } from '../../../common/constants'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettings } from '../../reducers/map'; import { MapSettingsPanel } from '../map_settings_panel'; import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { MapRefreshConfig } from '../../../common/descriptor_types'; import { ILayer } from '../../classes/layers/layer'; @@ -58,7 +55,6 @@ export interface Props { interface State { isInitialLoadRenderTimeoutComplete: boolean; domId: string; - geoFields: GeoFieldWithIndex[]; showFitToBoundsButton: boolean; showTimesliderButton: boolean; } @@ -66,7 +62,6 @@ interface State { export class MapContainer extends Component { private _isMounted: boolean = false; private _isInitalLoadRenderTimerStarted: boolean = false; - private _prevIndexPatternIds: string[] = []; private _refreshTimerId: number | null = null; private _prevIsPaused: boolean | null = null; private _prevInterval: number | null = null; @@ -74,7 +69,6 @@ export class MapContainer extends Component { state: State = { isInitialLoadRenderTimeoutComplete: false, domId: uuid(), - geoFields: [], showFitToBoundsButton: false, showTimesliderButton: false, }; @@ -95,10 +89,6 @@ export class MapContainer extends Component { this._isInitalLoadRenderTimerStarted = true; this._startInitialLoadRenderTimer(); } - - if (!!this.props.addFilters) { - this._loadGeoFields(this.props.indexPatternIds); - } } componentWillUnmount() { @@ -151,40 +141,6 @@ export class MapContainer extends Component { } } - async _loadGeoFields(nextIndexPatternIds: string[]) { - if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { - // all ready loaded index pattern ids - return; - } - - this._prevIndexPatternIds = nextIndexPatternIds; - - const geoFields: GeoFieldWithIndex[] = []; - const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); - indexPatterns.forEach((indexPattern) => { - indexPattern.fields.forEach((field) => { - if ( - indexPattern.id && - !indexPatternsUtils.isNestedField(field) && - (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ) { - geoFields.push({ - geoFieldName: field.name, - geoFieldType: field.type, - indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id, - }); - } - }); - }); - - if (!this._isMounted) { - return; - } - - this.setState({ geoFields }); - } - _setRefreshTimer = () => { const { isPaused, interval } = this.props.refreshConfig; @@ -289,13 +245,11 @@ export class MapContainer extends Component { getFilterActions={getFilterActions} getActionContext={getActionContext} onSingleValueTrigger={onSingleValueTrigger} - geoFields={this.state.geoFields} renderTooltipContent={renderTooltipContent} /> {!this.props.settings.hideToolbarOverlay && ( { _onDraw = async (e: { features: Feature[] }) => { - if ( - !e.features.length || - !this.props.drawState || - !this.props.drawState.geoFieldName || - !this.props.drawState.indexPatternId - ) { + if (!e.features.length || !this.props.drawState || !this.props.geoFieldNames.length) { return; } @@ -61,8 +57,7 @@ export class DrawFilterControl extends Component { filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '', distanceKm, - geoFieldName: this.props.drawState.geoFieldName, - indexPatternId: this.props.drawState.indexPatternId, + geoFieldNames: this.props.geoFieldNames, point: [ _.round(circle.properties.center[0], precision), _.round(circle.properties.center[1], precision), @@ -78,8 +73,7 @@ export class DrawFilterControl extends Component { this.props.drawState.drawType === DRAW_TYPE.BOUNDS ? getBoundingBoxGeometry(geometry) : geometry, - indexPatternId: this.props.drawState.indexPatternId, - geoFieldName: this.props.drawState.geoFieldName, + geoFieldNames: this.props.geoFieldNames, geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '', relation: this.props.drawState.relation ? this.props.drawState.relation diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts index 17f4d919fb7e0..58ad530093557 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_filter_control/index.ts @@ -10,13 +10,18 @@ import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { DrawFilterControl } from './draw_filter_control'; import { updateDrawState } from '../../../../actions'; -import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors'; +import { + getDrawState, + isDrawingFilter, + getGeoFieldNames, +} from '../../../../selectors/map_selectors'; import { MapStoreState } from '../../../../reducers/store'; function mapStateToProps(state: MapStoreState) { return { isDrawingFilter: isDrawingFilter(state), drawState: getDrawState(state), + geoFieldNames: getGeoFieldNames(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 877de10e11383..9d8bce083c292 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -43,7 +43,6 @@ import { // @ts-expect-error } from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { MapExtentState } from '../../actions'; import { TileStatusTracker } from './tile_status_tracker'; @@ -68,7 +67,6 @@ export interface Props { getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; - geoFields: GeoFieldWithIndex[]; renderTooltipContent?: RenderToolTipContent; setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; } @@ -432,7 +430,6 @@ export class MBMap extends Component { getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} onSingleValueTrigger={this.props.onSingleValueTrigger} - geoFields={this.props.geoFields} renderTooltipContent={this.props.renderTooltipContent} /> ) : null; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx index 04e564943fa39..5f26715c86004 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/features_tooltip/feature_geometry_filter_form.tsx @@ -21,7 +21,6 @@ import { import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants'; // @ts-expect-error import { GeometryFilterForm } from '../../../../components/geometry_filter_form'; -import { GeoFieldWithIndex } from '../../../../components/geo_field_with_index'; // over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. const META_OVERHEAD = 100; @@ -29,11 +28,11 @@ const META_OVERHEAD = 100; interface Props { onClose: () => void; geometry: Geometry; - geoFields: GeoFieldWithIndex[]; addFilters: (filters: Filter[], actionId: string) => Promise; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; loadPreIndexedShape: () => Promise; + geoFieldNames: string[]; } interface State { @@ -77,13 +76,9 @@ export class FeatureGeometryFilterForm extends Component { _createFilter = async ({ geometryLabel, - indexPatternId, - geoFieldName, relation, }: { geometryLabel: string; - indexPatternId: string; - geoFieldName: string; relation: ES_SPATIAL_RELATIONS; }) => { this.setState({ errorMsg: undefined }); @@ -97,8 +92,7 @@ export class FeatureGeometryFilterForm extends Component { preIndexedShape, geometry: this.props.geometry as Polygon, geometryLabel, - indexPatternId, - geoFieldName, + geoFieldNames: this.props.geoFieldNames, relation, }); @@ -130,7 +124,6 @@ export class FeatureGeometryFilterForm extends Component { defaultMessage: 'Create filter', } )} - geoFields={this.props.geoFields} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} intitialGeometryLabel={this.props.geometry.type.toLowerCase()} diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts index 28510815d1a2e..2861c80306526 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts @@ -20,6 +20,7 @@ import { getLayerList, getOpenTooltips, getHasLockedTooltips, + getGeoFieldNames, isDrawingFilter, } from '../../../selectors/map_selectors'; import { MapStoreState } from '../../../reducers/store'; @@ -30,6 +31,7 @@ function mapStateToProps(state: MapStoreState) { hasLockedTooltips: getHasLockedTooltips(state), isDrawingFilter: isDrawingFilter(state), openTooltips: getOpenTooltips(state), + geoFieldNames: getGeoFieldNames(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index ac6e3cfcccf4e..a11000a48866f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -77,7 +77,7 @@ const defaultProps = { layerList: [mockLayer], isDrawingFilter: false, addFilters: async () => {}, - geoFields: [], + geoFieldNames: [], openTooltips: [], hasLockedTooltips: false, }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 09dd9ee4f51d9..e9af9dcc89e07 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -20,7 +20,6 @@ import { Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { - ES_GEO_FIELD_TYPE, FEATURE_ID_PROPERTY_NAME, GEO_JSON_TYPE, LON_INDEX, @@ -37,7 +36,6 @@ import { FeatureGeometryFilterForm } from './features_tooltip'; import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions'; import { ILayer } from '../../../classes/layers/layer'; import { IVectorLayer } from '../../../classes/layers/vector_layer'; -import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property'; function justifyAnchorLocation( @@ -70,7 +68,7 @@ export interface Props { closeOnHoverTooltip: () => void; getActionContext?: () => ActionExecutionContext; getFilterActions?: () => Promise; - geoFields: GeoFieldWithIndex[]; + geoFieldNames: string[]; hasLockedTooltips: boolean; isDrawingFilter: boolean; layerList: ILayer[]; @@ -163,8 +161,10 @@ export class TooltipControl extends Component { const actions = []; const geometry = this._getFeatureGeometry({ layerId, featureId }); - const geoFieldsForFeature = this._filterGeoFieldsByFeatureGeometry(geometry); - if (geometry && geoFieldsForFeature.length && this.props.addFilters) { + const isPolygon = + geometry && + (geometry.type === GEO_JSON_TYPE.POLYGON || geometry.type === GEO_JSON_TYPE.MULTI_POLYGON); + if (isPolygon && this.props.geoFieldNames.length && this.props.addFilters) { actions.push({ label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', { defaultMessage: 'Filter by geometry', @@ -175,8 +175,8 @@ export class TooltipControl extends Component { onClose={() => { this.props.closeOnClickTooltip(tooltipId); }} - geometry={geometry} - geoFields={geoFieldsForFeature} + geometry={geometry!} + geoFieldNames={this.props.geoFieldNames} addFilters={this.props.addFilters} getFilterActions={this.props.getFilterActions} getActionContext={this.props.getActionContext} @@ -191,29 +191,6 @@ export class TooltipControl extends Component { return actions; } - _filterGeoFieldsByFeatureGeometry(geometry: Geometry | null) { - if (!geometry) { - return []; - } - - // line geometry can only create filters for geo_shape fields. - if ( - geometry.type === GEO_JSON_TYPE.LINE_STRING || - geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING - ) { - return this.props.geoFields.filter(({ geoFieldType }) => { - return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE; - }); - } - - // TODO support geo distance filters for points - if (geometry.type === GEO_JSON_TYPE.POINT || geometry.type === GEO_JSON_TYPE.MULTI_POINT) { - return []; - } - - return this.props.geoFields; - } - _getTooltipFeatures( mbFeatures: MapboxGeoJSONFeature[], isLocked: boolean, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap index 168a070b07744..245cf6e5b2a48 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -29,18 +29,7 @@ exports[`Should show all controls 1`] = ` - + diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts index d1008edfd572d..7d176c13da049 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts @@ -5,4 +5,16 @@ * 2.0. */ -export { ToolbarOverlay } from './toolbar_overlay'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../reducers/store'; +import { getGeoFieldNames } from '../../selectors/map_selectors'; +import { ToolbarOverlay } from './toolbar_overlay'; + +function mapStateToProps(state: MapStoreState) { + return { + showToolsControl: getGeoFieldNames(state).length !== 0, + }; +} + +const connected = connect(mapStateToProps)(ToolbarOverlay); +export { connected as ToolbarOverlay }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx index 28b5ab9c78f40..9efbd82fe390a 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -21,22 +21,20 @@ import { ToolbarOverlay } from './toolbar_overlay'; test('Should only show set view control', async () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); test('Should show all controls', async () => { - const geoFieldWithIndex = { - geoFieldName: 'myGeoFieldName', - geoFieldType: 'geo_point', - indexPatternTitle: 'myIndex', - indexPatternId: '1', - }; const component = shallow( {}} - geoFields={[geoFieldWithIndex]} + showToolsControl={true} showFitToBoundsButton={true} showTimesliderButton={true} /> diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx index 41c6c1f7c4a7c..2b35793218969 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx @@ -13,11 +13,10 @@ import { SetViewControl } from './set_view_control'; import { ToolsControl } from './tools_control'; import { FitToData } from './fit_to_data'; import { TimesliderToggleButton } from './timeslider_toggle_button'; -import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; export interface Props { addFilters?: ((filters: Filter[], actionId: string) => Promise) | null; - geoFields: GeoFieldWithIndex[]; + showToolsControl: boolean; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; showFitToBoundsButton: boolean; @@ -26,10 +25,9 @@ export interface Props { export function ToolbarOverlay(props: Props) { const toolsButton = - props.addFilters && props.geoFields.length ? ( + props.addFilters && props.showToolsControl ? ( diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap index b6d217d690764..aa5c6aa42c77d 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap @@ -56,16 +56,6 @@ exports[`Should render cancel button when drawing 1`] = ` "content": , "id": 3, @@ -187,16 +157,6 @@ exports[`renders 1`] = ` "content": , "id": 3, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx index 6779fe945137e..9ea20914c6422 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -22,7 +22,6 @@ import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../ // @ts-expect-error import { GeometryFilterForm } from '../../../components/geometry_filter_form'; import { DistanceFilterForm } from '../../../components/distance_filter_form'; -import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; import { DrawState } from '../../../../common/descriptor_types'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { @@ -54,7 +53,6 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( export interface Props { cancelDraw: () => void; - geoFields: GeoFieldWithIndex[]; initiateDraw: (drawState: DrawState) => void; isDrawingFilter: boolean; getFilterActions?: () => Promise; @@ -98,9 +96,6 @@ export class ToolsControl extends Component { _initiateBoundsDraw = (options: { actionId: string; geometryLabel: string; - indexPatternId: string; - geoFieldName: string; - geoFieldType: ES_GEO_FIELD_TYPE; relation: ES_SPATIAL_RELATIONS; }) => { this.props.initiateDraw({ @@ -110,12 +105,7 @@ export class ToolsControl extends Component { this._closePopover(); }; - _initiateDistanceDraw = (options: { - actionId: string; - filterLabel: string; - indexPatternId: string; - geoFieldName: string; - }) => { + _initiateDistanceDraw = (options: { actionId: string; filterLabel: string }) => { this.props.initiateDraw({ drawType: DRAW_TYPE.DISTANCE, ...options, @@ -154,7 +144,6 @@ export class ToolsControl extends Component { { {