mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Maps] spatially filter by all geo fields (#100735)
* [Maps] spatial filter by all geo fields * replace geoFields with geoFieldNames * update mapSpatialFilter to be able to reconize multi field filters * add check for geoFieldNames * i18n fixes and fix GeometryFilterForm jest test * tslint * tslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7f8f89ed99
commit
14442b78de
32 changed files with 944 additions and 1093 deletions
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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, 'meta'>
|
||||
): 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;
|
||||
}
|
54
x-pack/plugins/maps/common/elasticsearch_util/types.ts
Normal file
54
x-pack/plugins/maps/common/elasticsearch_util/types.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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`] = `
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
@ -17,204 +17,6 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Spatial relation"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSelect
|
||||
compressed={true}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"text": "intersects",
|
||||
"value": "INTERSECTS",
|
||||
},
|
||||
Object {
|
||||
"text": "disjoint",
|
||||
"value": "DISJOINT",
|
||||
},
|
||||
]
|
||||
}
|
||||
value="INTERSECTS"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<ActionSelect
|
||||
onChange={[Function]}
|
||||
value="ACTION_GLOBAL_APPLY_FILTER"
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiTextAlign
|
||||
textAlign="right"
|
||||
>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Create filter
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should render error message 1`] = `
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Geometry label"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={true}
|
||||
onChange={[Function]}
|
||||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Spatial relation"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiSelect
|
||||
compressed={true}
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"text": "intersects",
|
||||
"value": "INTERSECTS",
|
||||
},
|
||||
Object {
|
||||
"text": "disjoint",
|
||||
"value": "DISJOINT",
|
||||
},
|
||||
]
|
||||
}
|
||||
value="INTERSECTS"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<ActionSelect
|
||||
onChange={[Function]}
|
||||
value="ACTION_GLOBAL_APPLY_FILTER"
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFormErrorText>
|
||||
Simulated error
|
||||
</EuiFormErrorText>
|
||||
<EuiTextAlign
|
||||
textAlign="right"
|
||||
>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Create filter
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should render relation select when geo field is geo_shape 1`] = `
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Geometry label"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={true}
|
||||
onChange={[Function]}
|
||||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_shape",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
|
@ -268,7 +70,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
|
|||
</EuiForm>
|
||||
`;
|
||||
|
||||
exports[`should render relation select without "within"-relation when geo field is geo_point 1`] = `
|
||||
exports[`should render error message 1`] = `
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
|
@ -285,27 +87,6 @@ exports[`should render relation select without "within"-relation when geo field
|
|||
value="My shape"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<MultiIndexGeoFieldSelect
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={[Function]}
|
||||
selectedField={
|
||||
Object {
|
||||
"geoFieldName": "my geo field",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": 1,
|
||||
"indexPatternTitle": "My index",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="rowCompressed"
|
||||
|
@ -328,6 +109,10 @@ exports[`should render relation select without "within"-relation when geo field
|
|||
"text": "disjoint",
|
||||
"value": "DISJOINT",
|
||||
},
|
||||
Object {
|
||||
"text": "within",
|
||||
"value": "WITHIN",
|
||||
},
|
||||
]
|
||||
}
|
||||
value="INTERSECTS"
|
||||
|
@ -340,6 +125,9 @@ exports[`should render relation select without "within"-relation when geo field
|
|||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFormErrorText>
|
||||
Simulated error
|
||||
</EuiFormErrorText>
|
||||
<EuiTextAlign
|
||||
textAlign="right"
|
||||
>
|
||||
|
|
|
@ -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<Action[]>;
|
||||
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<Props, State> {
|
||||
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<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
filterLabel: e.target.value,
|
||||
|
@ -68,14 +49,9 @@ export class DistanceFilterForm extends Component<Props, State> {
|
|||
};
|
||||
|
||||
_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<Props, State> {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<MultiIndexGeoFieldSelect
|
||||
selectedField={this.state.selectedField}
|
||||
fields={this.props.geoFields}
|
||||
onChange={this._onGeoFieldChange}
|
||||
/>
|
||||
|
||||
<ActionSelect
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
|
@ -111,7 +81,7 @@ export class DistanceFilterForm extends Component<Props, State> {
|
|||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiTextAlign textAlign="right">
|
||||
<EuiButton size="s" fill onClick={this._onSubmit} isDisabled={!this.state.selectedField}>
|
||||
<EuiButton size="s" fill onClick={this._onSubmit}>
|
||||
{this.props.buttonLabel}
|
||||
</EuiButton>
|
||||
</EuiTextAlign>
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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 {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<MultiIndexGeoFieldSelect
|
||||
selectedField={this.state.selectedField}
|
||||
fields={this.props.geoFields}
|
||||
onChange={this._onGeoFieldChange}
|
||||
/>
|
||||
|
||||
{this._renderRelationInput()}
|
||||
|
||||
<ActionSelect
|
||||
|
@ -161,7 +131,7 @@ export class GeometryFilterForm extends Component {
|
|||
size="s"
|
||||
fill
|
||||
onClick={this._onSubmit}
|
||||
isDisabled={!this.state.geometryLabel || !this.state.selectedField}
|
||||
isDisabled={!this.state.geometryLabel}
|
||||
isLoading={this.props.isLoading}
|
||||
>
|
||||
{this.props.buttonLabel}
|
||||
|
|
|
@ -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(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_point',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render relation select when geo field is geo_shape', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_shape',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show "within" relation when filter geometry is not closed', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_shape',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1,
|
||||
},
|
||||
]}
|
||||
isFilterGeometryClosed={false}
|
||||
/>
|
||||
);
|
||||
test('render', async () => {
|
||||
const component = shallow(<GeometryFilterForm {...defaultProps} />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render error message', async () => {
|
||||
const component = shallow(
|
||||
<GeometryFilterForm
|
||||
{...defaultProps}
|
||||
geoFields={[
|
||||
{
|
||||
geoFieldName: 'my geo field',
|
||||
geoFieldType: 'geo_point',
|
||||
indexPatternTitle: 'My index',
|
||||
indexPatternId: 1,
|
||||
},
|
||||
]}
|
||||
errorMsg="Simulated error"
|
||||
/>
|
||||
);
|
||||
const component = shallow(<GeometryFilterForm {...defaultProps} errorMsg="Simulated error" />);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -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: (
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{geoField.indexPatternTitle}</small>
|
||||
</EuiTextColor>
|
||||
<br />
|
||||
{geoField.geoFieldName}
|
||||
</EuiText>
|
||||
),
|
||||
value: createOptionId(geoField),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="mapGeometryFilter__geoFieldSuperSelectWrapper"
|
||||
label={i18n.translate('xpack.maps.multiIndexFieldSelect.fieldLabel', {
|
||||
defaultMessage: 'Filtering field',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiSuperSelect
|
||||
className="mapGeometryFilter__geoFieldSuperSelect"
|
||||
options={options}
|
||||
valueOfSelected={selectedField ? createOptionId(selectedField) : ''}
|
||||
onChange={onFieldSelect}
|
||||
hasDividers={true}
|
||||
fullWidth={true}
|
||||
compressed={true}
|
||||
itemClassName="mapGeometryFilter__geoFieldItem"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
|
@ -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<Props, State> {
|
||||
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<Props, State> {
|
|||
state: State = {
|
||||
isInitialLoadRenderTimeoutComplete: false,
|
||||
domId: uuid(),
|
||||
geoFields: [],
|
||||
showFitToBoundsButton: false,
|
||||
showTimesliderButton: false,
|
||||
};
|
||||
|
@ -95,10 +89,6 @@ export class MapContainer extends Component<Props, State> {
|
|||
this._isInitalLoadRenderTimerStarted = true;
|
||||
this._startInitialLoadRenderTimer();
|
||||
}
|
||||
|
||||
if (!!this.props.addFilters) {
|
||||
this._loadGeoFields(this.props.indexPatternIds);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -151,40 +141,6 @@ export class MapContainer extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
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<Props, State> {
|
|||
getFilterActions={getFilterActions}
|
||||
getActionContext={getActionContext}
|
||||
onSingleValueTrigger={onSingleValueTrigger}
|
||||
geoFields={this.state.geoFields}
|
||||
renderTooltipContent={renderTooltipContent}
|
||||
/>
|
||||
{!this.props.settings.hideToolbarOverlay && (
|
||||
<ToolbarOverlay
|
||||
addFilters={addFilters}
|
||||
geoFields={this.state.geoFields}
|
||||
getFilterActions={getFilterActions}
|
||||
getActionContext={getActionContext}
|
||||
showFitToBoundsButton={this.state.showFitToBoundsButton}
|
||||
|
|
|
@ -29,16 +29,12 @@ export interface Props {
|
|||
drawState?: DrawState;
|
||||
isDrawingFilter: boolean;
|
||||
mbMap: MbMap;
|
||||
geoFieldNames: string[];
|
||||
}
|
||||
|
||||
export class DrawFilterControl extends Component<Props, {}> {
|
||||
_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<Props, {}> {
|
|||
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<Props, {}> {
|
|||
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
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Action[]>;
|
||||
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<Props, State> {
|
|||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
onSingleValueTrigger={this.props.onSingleValueTrigger}
|
||||
geoFields={this.props.geoFields}
|
||||
renderTooltipContent={this.props.renderTooltipContent}
|
||||
/>
|
||||
) : null;
|
||||
|
|
|
@ -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<void>;
|
||||
getFilterActions?: () => Promise<Action[]>;
|
||||
getActionContext?: () => ActionExecutionContext;
|
||||
loadPreIndexedShape: () => Promise<PreIndexedShape | null>;
|
||||
geoFieldNames: string[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -77,13 +76,9 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
|
|||
|
||||
_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<Props, State> {
|
|||
preIndexedShape,
|
||||
geometry: this.props.geometry as Polygon,
|
||||
geometryLabel,
|
||||
indexPatternId,
|
||||
geoFieldName,
|
||||
geoFieldNames: this.props.geoFieldNames,
|
||||
relation,
|
||||
});
|
||||
|
||||
|
@ -130,7 +124,6 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
|
|||
defaultMessage: 'Create filter',
|
||||
}
|
||||
)}
|
||||
geoFields={this.props.geoFields}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
intitialGeometryLabel={this.props.geometry.type.toLowerCase()}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ const defaultProps = {
|
|||
layerList: [mockLayer],
|
||||
isDrawingFilter: false,
|
||||
addFilters: async () => {},
|
||||
geoFields: [],
|
||||
geoFieldNames: [],
|
||||
openTooltips: [],
|
||||
hasLockedTooltips: false,
|
||||
};
|
||||
|
|
|
@ -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<Action[]>;
|
||||
geoFields: GeoFieldWithIndex[];
|
||||
geoFieldNames: string[];
|
||||
hasLockedTooltips: boolean;
|
||||
isDrawingFilter: boolean;
|
||||
layerList: ILayer[];
|
||||
|
@ -163,8 +161,10 @@ export class TooltipControl extends Component<Props, {}> {
|
|||
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<Props, {}> {
|
|||
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<Props, {}> {
|
|||
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,
|
||||
|
|
|
@ -29,18 +29,7 @@ exports[`Should show all controls 1`] = `
|
|||
<Connect(FitToData) />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(ToolsControl)
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "myGeoFieldName",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "myIndex",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Connect(ToolsControl) />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Connect(TimesliderToggleButton) />
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -21,22 +21,20 @@ import { ToolbarOverlay } from './toolbar_overlay';
|
|||
|
||||
test('Should only show set view control', async () => {
|
||||
const component = shallow(
|
||||
<ToolbarOverlay geoFields={[]} showFitToBoundsButton={false} showTimesliderButton={false} />
|
||||
<ToolbarOverlay
|
||||
showToolsControl={false}
|
||||
showFitToBoundsButton={false}
|
||||
showTimesliderButton={false}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should show all controls', async () => {
|
||||
const geoFieldWithIndex = {
|
||||
geoFieldName: 'myGeoFieldName',
|
||||
geoFieldType: 'geo_point',
|
||||
indexPatternTitle: 'myIndex',
|
||||
indexPatternId: '1',
|
||||
};
|
||||
const component = shallow(
|
||||
<ToolbarOverlay
|
||||
addFilters={async (filters: Filter[], actionId: string) => {}}
|
||||
geoFields={[geoFieldWithIndex]}
|
||||
showToolsControl={true}
|
||||
showFitToBoundsButton={true}
|
||||
showTimesliderButton={true}
|
||||
/>
|
||||
|
|
|
@ -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<void>) | null;
|
||||
geoFields: GeoFieldWithIndex[];
|
||||
showToolsControl: boolean;
|
||||
getFilterActions?: () => Promise<Action[]>;
|
||||
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 ? (
|
||||
<EuiFlexItem>
|
||||
<ToolsControl
|
||||
geoFields={props.geoFields}
|
||||
getFilterActions={props.getFilterActions}
|
||||
getActionContext={props.getActionContext}
|
||||
/>
|
||||
|
|
|
@ -56,16 +56,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw shape"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="shape"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
|
@ -77,16 +67,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw bounds"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="bounds"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
|
@ -98,16 +78,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
|||
"content": <DistanceFilterForm
|
||||
buttonLabel="Draw distance"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 3,
|
||||
|
@ -187,16 +157,6 @@ exports[`renders 1`] = `
|
|||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw shape"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="shape"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
|
@ -208,16 +168,6 @@ exports[`renders 1`] = `
|
|||
"content": <GeometryFilterForm
|
||||
buttonLabel="Draw bounds"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
intitialGeometryLabel="bounds"
|
||||
isFilterGeometryClosed={true}
|
||||
onSubmit={[Function]}
|
||||
|
@ -229,16 +179,6 @@ exports[`renders 1`] = `
|
|||
"content": <DistanceFilterForm
|
||||
buttonLabel="Draw distance"
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
geoFields={
|
||||
Array [
|
||||
Object {
|
||||
"geoFieldName": "location",
|
||||
"geoFieldType": "geo_point",
|
||||
"indexPatternId": "1",
|
||||
"indexPatternTitle": "my_index",
|
||||
},
|
||||
]
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
/>,
|
||||
"id": 3,
|
||||
|
|
|
@ -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<Action[]>;
|
||||
|
@ -98,9 +96,6 @@ export class ToolsControl extends Component<Props, State> {
|
|||
_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<Props, State> {
|
|||
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<Props, State> {
|
|||
<GeometryFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={DRAW_SHAPE_LABEL_SHORT}
|
||||
geoFields={this.props.geoFields}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
intitialGeometryLabel={i18n.translate(
|
||||
|
@ -174,7 +163,6 @@ export class ToolsControl extends Component<Props, State> {
|
|||
<GeometryFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={DRAW_BOUNDS_LABEL_SHORT}
|
||||
geoFields={this.props.geoFields}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
intitialGeometryLabel={i18n.translate(
|
||||
|
@ -194,7 +182,6 @@ export class ToolsControl extends Component<Props, State> {
|
|||
<DistanceFilterForm
|
||||
className="mapDrawControl__geometryFilterForm"
|
||||
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
|
||||
geoFields={this.props.geoFields}
|
||||
getFilterActions={this.props.getFilterActions}
|
||||
getActionContext={this.props.getActionContext}
|
||||
onSubmit={this._initiateDistanceDraw}
|
||||
|
|
|
@ -461,7 +461,6 @@ export class MapEmbeddable
|
|||
this._prevMapExtent = mapExtent;
|
||||
|
||||
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
||||
mapExtentFilter.meta.isMultiIndex = true;
|
||||
mapExtentFilter.meta.controlledBy = this._controlledBy;
|
||||
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
|
||||
defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}',
|
||||
|
|
|
@ -13300,7 +13300,6 @@
|
|||
"xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド",
|
||||
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "GeometryCollectionを convertESShapeToGeojsonGeometryに渡さないでください",
|
||||
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "{geometryType} ジオメトリから Geojson に変換できません。サポートされていません",
|
||||
"xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel} の {distanceKm}km 以内にある {geoFieldName}",
|
||||
"xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値:{expectedTypes}、提供された値:{fieldType}",
|
||||
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "サポートされていないジオメトリタイプ、期待値:{expectedTypes}、提供された値:{geometryType}",
|
||||
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。",
|
||||
|
@ -13480,7 +13479,6 @@
|
|||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
|
||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "合計",
|
||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語",
|
||||
"xpack.maps.multiIndexFieldSelect.fieldLabel": "フィールドのフィルタリング",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "追加",
|
||||
"xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名",
|
||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||
|
|
|
@ -13476,7 +13476,6 @@
|
|||
"xpack.maps.emsSource.tooltipsTitle": "工具提示字段",
|
||||
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "不应将 GeometryCollection 传递给 convertESShapeToGeojsonGeometry",
|
||||
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "无法将 {geometryType} 几何图形转换成 geojson,不支持",
|
||||
"xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel} {distanceKm}km 内的 {geoFieldName}",
|
||||
"xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为 {expectedTypes},而提供的是 {fieldType}",
|
||||
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "几何类型不受支持,应为 {expectedTypes},而提供的是 {geometryType}",
|
||||
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。",
|
||||
|
@ -13657,7 +13656,6 @@
|
|||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
|
||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "求和",
|
||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词",
|
||||
"xpack.maps.multiIndexFieldSelect.fieldLabel": "筛选字段",
|
||||
"xpack.maps.mvtSource.addFieldLabel": "添加",
|
||||
"xpack.maps.mvtSource.fieldPlaceholderText": "字段名称",
|
||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue