mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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);
|
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) => {
|
test('should return undefined for none matching', async (done) => {
|
||||||
const filter = {
|
const filter = {
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -22,5 +22,18 @@ export const mapSpatialFilter = (filter: Filter) => {
|
||||||
value: '',
|
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;
|
throw filter;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { ReactNode } from 'react';
|
||||||
import { GeoJsonProperties } from 'geojson';
|
import { GeoJsonProperties } from 'geojson';
|
||||||
import { Geometry } from 'geojson';
|
import { Geometry } from 'geojson';
|
||||||
import { Query } from '../../../../../src/plugins/data/common';
|
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 = {
|
export type MapExtent = {
|
||||||
minLon: number;
|
minLon: number;
|
||||||
|
@ -70,9 +70,6 @@ export type DrawState = {
|
||||||
actionId: string;
|
actionId: string;
|
||||||
drawType: DRAW_TYPE;
|
drawType: DRAW_TYPE;
|
||||||
filterLabel?: string; // point radius filter alias
|
filterLabel?: string; // point radius filter alias
|
||||||
geoFieldName?: string;
|
|
||||||
geoFieldType?: ES_GEO_FIELD_TYPE;
|
|
||||||
geometryLabel?: string;
|
geometryLabel?: string;
|
||||||
indexPatternId?: string;
|
|
||||||
relation?: ES_SPATIAL_RELATIONS;
|
relation?: ES_SPATIAL_RELATIONS;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,9 +9,7 @@ import {
|
||||||
hitsToGeoJson,
|
hitsToGeoJson,
|
||||||
geoPointToGeometry,
|
geoPointToGeometry,
|
||||||
geoShapeToGeometry,
|
geoShapeToGeometry,
|
||||||
createExtentFilter,
|
|
||||||
roundCoordinates,
|
roundCoordinates,
|
||||||
extractFeaturesFromFilters,
|
|
||||||
makeESBbox,
|
makeESBbox,
|
||||||
scaleBounds,
|
scaleBounds,
|
||||||
} from './elasticsearch_geo_utils';
|
} 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', () => {
|
describe('roundCoordinates', () => {
|
||||||
it('should set coordinates precision', () => {
|
it('should set coordinates precision', () => {
|
||||||
const coordinates = [
|
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', () => {
|
describe('makeESBbox', () => {
|
||||||
it('Should invert Y-axis', () => {
|
it('Should invert Y-axis', () => {
|
||||||
const bbox = makeESBbox({
|
const bbox = makeESBbox({
|
||||||
|
|
|
@ -16,61 +16,13 @@ import { BBox } from '@turf/helpers';
|
||||||
import {
|
import {
|
||||||
DECIMAL_DEGREES_PRECISION,
|
DECIMAL_DEGREES_PRECISION,
|
||||||
ES_GEO_FIELD_TYPE,
|
ES_GEO_FIELD_TYPE,
|
||||||
ES_SPATIAL_RELATIONS,
|
|
||||||
GEO_JSON_TYPE,
|
GEO_JSON_TYPE,
|
||||||
POLYGON_COORDINATES_EXTERIOR_INDEX,
|
POLYGON_COORDINATES_EXTERIOR_INDEX,
|
||||||
LON_INDEX,
|
LON_INDEX,
|
||||||
LAT_INDEX,
|
LAT_INDEX,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { getEsSpatialRelationLabel } from '../i18n_getters';
|
|
||||||
import { Filter, FilterMeta, FILTERS } from '../../../../../src/plugins/data/common';
|
|
||||||
import { MapExtent } from '../descriptor_types';
|
import { MapExtent } from '../descriptor_types';
|
||||||
|
import { Coordinates, ESBBox, ESGeometry } from './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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureGeoField(type: string) {
|
function ensureGeoField(type: string) {
|
||||||
const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE];
|
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;
|
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 {
|
export function roundCoordinates(coordinates: Coordinates): void {
|
||||||
for (let i = 0; i < coordinates.length; i++) {
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
const value = coordinates[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 {
|
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent {
|
||||||
const width = bounds.maxLon - bounds.minLon;
|
const width = bounds.maxLon - bounds.minLon;
|
||||||
const height = bounds.maxLat - bounds.minLat;
|
const height = bounds.maxLat - bounds.minLat;
|
||||||
|
|
|
@ -8,4 +8,6 @@
|
||||||
export * from './es_agg_utils';
|
export * from './es_agg_utils';
|
||||||
export * from './convert_to_geojson';
|
export * from './convert_to_geojson';
|
||||||
export * from './elasticsearch_geo_utils';
|
export * from './elasticsearch_geo_utils';
|
||||||
|
export * from './spatial_filter_utils';
|
||||||
|
export * from './types';
|
||||||
export { isTotalHitsGreaterThan, TotalHits } from './total_hits';
|
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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`should not show "within" relation when filter geometry is not closed 1`] = `
|
exports[`render 1`] = `
|
||||||
<EuiForm>
|
<EuiForm>
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
describedByIds={Array []}
|
describedByIds={Array []}
|
||||||
|
@ -17,204 +17,6 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
|
||||||
value="My shape"
|
value="My shape"
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</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
|
<EuiFormRow
|
||||||
describedByIds={Array []}
|
describedByIds={Array []}
|
||||||
display="rowCompressed"
|
display="rowCompressed"
|
||||||
|
@ -268,7 +70,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`should render relation select without "within"-relation when geo field is geo_point 1`] = `
|
exports[`should render error message 1`] = `
|
||||||
<EuiForm>
|
<EuiForm>
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
describedByIds={Array []}
|
describedByIds={Array []}
|
||||||
|
@ -285,27 +87,6 @@ exports[`should render relation select without "within"-relation when geo field
|
||||||
value="My shape"
|
value="My shape"
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</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
|
<EuiFormRow
|
||||||
describedByIds={Array []}
|
describedByIds={Array []}
|
||||||
display="rowCompressed"
|
display="rowCompressed"
|
||||||
|
@ -328,6 +109,10 @@ exports[`should render relation select without "within"-relation when geo field
|
||||||
"text": "disjoint",
|
"text": "disjoint",
|
||||||
"value": "DISJOINT",
|
"value": "DISJOINT",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"text": "within",
|
||||||
|
"value": "WITHIN",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
value="INTERSECTS"
|
value="INTERSECTS"
|
||||||
|
@ -340,6 +125,9 @@ exports[`should render relation select without "within"-relation when geo field
|
||||||
<EuiSpacer
|
<EuiSpacer
|
||||||
size="m"
|
size="m"
|
||||||
/>
|
/>
|
||||||
|
<EuiFormErrorText>
|
||||||
|
Simulated error
|
||||||
|
</EuiFormErrorText>
|
||||||
<EuiTextAlign
|
<EuiTextAlign
|
||||||
textAlign="right"
|
textAlign="right"
|
||||||
>
|
>
|
||||||
|
|
|
@ -16,47 +16,28 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
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 { ActionSelect } from './action_select';
|
||||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonLabel: string;
|
buttonLabel: string;
|
||||||
geoFields: GeoFieldWithIndex[];
|
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
getActionContext?: () => ActionExecutionContext;
|
getActionContext?: () => ActionExecutionContext;
|
||||||
onSubmit: ({
|
onSubmit: ({ actionId, filterLabel }: { actionId: string; filterLabel: string }) => void;
|
||||||
actionId,
|
|
||||||
filterLabel,
|
|
||||||
indexPatternId,
|
|
||||||
geoFieldName,
|
|
||||||
}: {
|
|
||||||
actionId: string;
|
|
||||||
filterLabel: string;
|
|
||||||
indexPatternId: string;
|
|
||||||
geoFieldName: string;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
actionId: string;
|
actionId: string;
|
||||||
selectedField: GeoFieldWithIndex | undefined;
|
|
||||||
filterLabel: string;
|
filterLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DistanceFilterForm extends Component<Props, State> {
|
export class DistanceFilterForm extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
actionId: ACTION_GLOBAL_APPLY_FILTER,
|
actionId: ACTION_GLOBAL_APPLY_FILTER,
|
||||||
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
|
|
||||||
filterLabel: '',
|
filterLabel: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => {
|
|
||||||
this.setState({ selectedField });
|
|
||||||
};
|
|
||||||
|
|
||||||
_onFilterLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
|
_onFilterLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterLabel: e.target.value,
|
filterLabel: e.target.value,
|
||||||
|
@ -68,14 +49,9 @@ export class DistanceFilterForm extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
_onSubmit = () => {
|
_onSubmit = () => {
|
||||||
if (!this.state.selectedField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.props.onSubmit({
|
this.props.onSubmit({
|
||||||
actionId: this.state.actionId,
|
actionId: this.state.actionId,
|
||||||
filterLabel: this.state.filterLabel,
|
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>
|
</EuiFormRow>
|
||||||
|
|
||||||
<MultiIndexGeoFieldSelect
|
|
||||||
selectedField={this.state.selectedField}
|
|
||||||
fields={this.props.geoFields}
|
|
||||||
onChange={this._onGeoFieldChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
|
@ -111,7 +81,7 @@ export class DistanceFilterForm extends Component<Props, State> {
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<EuiTextAlign textAlign="right">
|
<EuiTextAlign textAlign="right">
|
||||||
<EuiButton size="s" fill onClick={this._onSubmit} isDisabled={!this.state.selectedField}>
|
<EuiButton size="s" fill onClick={this._onSubmit}>
|
||||||
{this.props.buttonLabel}
|
{this.props.buttonLabel}
|
||||||
</EuiButton>
|
</EuiButton>
|
||||||
</EuiTextAlign>
|
</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,
|
EuiFormErrorText,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
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 { getEsSpatialRelationLabel } from '../../common/i18n_getters';
|
||||||
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
|
|
||||||
import { ActionSelect } from './action_select';
|
import { ActionSelect } from './action_select';
|
||||||
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
|
||||||
|
|
||||||
export class GeometryFilterForm extends Component {
|
export class GeometryFilterForm extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
buttonLabel: PropTypes.string.isRequired,
|
buttonLabel: PropTypes.string.isRequired,
|
||||||
geoFields: PropTypes.array.isRequired,
|
|
||||||
getFilterActions: PropTypes.func,
|
getFilterActions: PropTypes.func,
|
||||||
getActionContext: PropTypes.func,
|
getActionContext: PropTypes.func,
|
||||||
intitialGeometryLabel: PropTypes.string.isRequired,
|
intitialGeometryLabel: PropTypes.string.isRequired,
|
||||||
|
@ -42,15 +40,10 @@ export class GeometryFilterForm extends Component {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
actionId: ACTION_GLOBAL_APPLY_FILTER,
|
actionId: ACTION_GLOBAL_APPLY_FILTER,
|
||||||
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
|
|
||||||
geometryLabel: this.props.intitialGeometryLabel,
|
geometryLabel: this.props.intitialGeometryLabel,
|
||||||
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
|
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGeoFieldChange = (selectedField) => {
|
|
||||||
this.setState({ selectedField });
|
|
||||||
};
|
|
||||||
|
|
||||||
_onGeometryLabelChange = (e) => {
|
_onGeometryLabelChange = (e) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
geometryLabel: e.target.value,
|
geometryLabel: e.target.value,
|
||||||
|
@ -71,29 +64,12 @@ export class GeometryFilterForm extends Component {
|
||||||
this.props.onSubmit({
|
this.props.onSubmit({
|
||||||
actionId: this.state.actionId,
|
actionId: this.state.actionId,
|
||||||
geometryLabel: this.state.geometryLabel,
|
geometryLabel: this.state.geometryLabel,
|
||||||
indexPatternId: this.state.selectedField.indexPatternId,
|
|
||||||
geoFieldName: this.state.selectedField.geoFieldName,
|
|
||||||
relation: this.state.relation,
|
relation: this.state.relation,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderRelationInput() {
|
_renderRelationInput() {
|
||||||
// relationship only used when filtering geo_shape fields
|
const options = Object.values(ES_SPATIAL_RELATIONS).map((relation) => {
|
||||||
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) => {
|
|
||||||
return {
|
return {
|
||||||
value: relation,
|
value: relation,
|
||||||
text: getEsSpatialRelationLabel(relation),
|
text: getEsSpatialRelationLabel(relation),
|
||||||
|
@ -137,12 +113,6 @@ export class GeometryFilterForm extends Component {
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
|
|
||||||
<MultiIndexGeoFieldSelect
|
|
||||||
selectedField={this.state.selectedField}
|
|
||||||
fields={this.props.geoFields}
|
|
||||||
onChange={this._onGeoFieldChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this._renderRelationInput()}
|
{this._renderRelationInput()}
|
||||||
|
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
@ -161,7 +131,7 @@ export class GeometryFilterForm extends Component {
|
||||||
size="s"
|
size="s"
|
||||||
fill
|
fill
|
||||||
onClick={this._onSubmit}
|
onClick={this._onSubmit}
|
||||||
isDisabled={!this.state.geometryLabel || !this.state.selectedField}
|
isDisabled={!this.state.geometryLabel}
|
||||||
isLoading={this.props.isLoading}
|
isLoading={this.props.isLoading}
|
||||||
>
|
>
|
||||||
{this.props.buttonLabel}
|
{this.props.buttonLabel}
|
||||||
|
|
|
@ -16,76 +16,14 @@ const defaultProps = {
|
||||||
onSubmit: () => {},
|
onSubmit: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should render relation select without "within"-relation when geo field is geo_point', async () => {
|
test('render', async () => {
|
||||||
const component = shallow(
|
const component = shallow(<GeometryFilterForm {...defaultProps} />);
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render error message', async () => {
|
test('should render error message', async () => {
|
||||||
const component = shallow(
|
const component = shallow(<GeometryFilterForm {...defaultProps} errorMsg="Simulated error" />);
|
||||||
<GeometryFilterForm
|
|
||||||
{...defaultProps}
|
|
||||||
geoFields={[
|
|
||||||
{
|
|
||||||
geoFieldName: 'my geo field',
|
|
||||||
geoFieldType: 'geo_point',
|
|
||||||
indexPatternTitle: 'My index',
|
|
||||||
indexPatternId: 1,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
errorMsg="Simulated error"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
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 { EditLayerPanel } from '../edit_layer_panel';
|
||||||
import { AddLayerPanel } from '../add_layer_panel';
|
import { AddLayerPanel } from '../add_layer_panel';
|
||||||
import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public';
|
import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public';
|
||||||
import { getIndexPatternsFromIds } from '../../index_pattern_util';
|
import { RawValue } from '../../../common/constants';
|
||||||
import { ES_GEO_FIELD_TYPE, RawValue } from '../../../common/constants';
|
|
||||||
import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public';
|
|
||||||
import { FLYOUT_STATE } from '../../reducers/ui';
|
import { FLYOUT_STATE } from '../../reducers/ui';
|
||||||
import { MapSettings } from '../../reducers/map';
|
import { MapSettings } from '../../reducers/map';
|
||||||
import { MapSettingsPanel } from '../map_settings_panel';
|
import { MapSettingsPanel } from '../map_settings_panel';
|
||||||
import { registerLayerWizards } from '../../classes/layers/load_layer_wizards';
|
import { registerLayerWizards } from '../../classes/layers/load_layer_wizards';
|
||||||
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
|
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
|
||||||
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
|
|
||||||
import { MapRefreshConfig } from '../../../common/descriptor_types';
|
import { MapRefreshConfig } from '../../../common/descriptor_types';
|
||||||
import { ILayer } from '../../classes/layers/layer';
|
import { ILayer } from '../../classes/layers/layer';
|
||||||
|
|
||||||
|
@ -58,7 +55,6 @@ export interface Props {
|
||||||
interface State {
|
interface State {
|
||||||
isInitialLoadRenderTimeoutComplete: boolean;
|
isInitialLoadRenderTimeoutComplete: boolean;
|
||||||
domId: string;
|
domId: string;
|
||||||
geoFields: GeoFieldWithIndex[];
|
|
||||||
showFitToBoundsButton: boolean;
|
showFitToBoundsButton: boolean;
|
||||||
showTimesliderButton: boolean;
|
showTimesliderButton: boolean;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +62,6 @@ interface State {
|
||||||
export class MapContainer extends Component<Props, State> {
|
export class MapContainer extends Component<Props, State> {
|
||||||
private _isMounted: boolean = false;
|
private _isMounted: boolean = false;
|
||||||
private _isInitalLoadRenderTimerStarted: boolean = false;
|
private _isInitalLoadRenderTimerStarted: boolean = false;
|
||||||
private _prevIndexPatternIds: string[] = [];
|
|
||||||
private _refreshTimerId: number | null = null;
|
private _refreshTimerId: number | null = null;
|
||||||
private _prevIsPaused: boolean | null = null;
|
private _prevIsPaused: boolean | null = null;
|
||||||
private _prevInterval: number | null = null;
|
private _prevInterval: number | null = null;
|
||||||
|
@ -74,7 +69,6 @@ export class MapContainer extends Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
isInitialLoadRenderTimeoutComplete: false,
|
isInitialLoadRenderTimeoutComplete: false,
|
||||||
domId: uuid(),
|
domId: uuid(),
|
||||||
geoFields: [],
|
|
||||||
showFitToBoundsButton: false,
|
showFitToBoundsButton: false,
|
||||||
showTimesliderButton: false,
|
showTimesliderButton: false,
|
||||||
};
|
};
|
||||||
|
@ -95,10 +89,6 @@ export class MapContainer extends Component<Props, State> {
|
||||||
this._isInitalLoadRenderTimerStarted = true;
|
this._isInitalLoadRenderTimerStarted = true;
|
||||||
this._startInitialLoadRenderTimer();
|
this._startInitialLoadRenderTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!this.props.addFilters) {
|
|
||||||
this._loadGeoFields(this.props.indexPatternIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
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 = () => {
|
_setRefreshTimer = () => {
|
||||||
const { isPaused, interval } = this.props.refreshConfig;
|
const { isPaused, interval } = this.props.refreshConfig;
|
||||||
|
|
||||||
|
@ -289,13 +245,11 @@ export class MapContainer extends Component<Props, State> {
|
||||||
getFilterActions={getFilterActions}
|
getFilterActions={getFilterActions}
|
||||||
getActionContext={getActionContext}
|
getActionContext={getActionContext}
|
||||||
onSingleValueTrigger={onSingleValueTrigger}
|
onSingleValueTrigger={onSingleValueTrigger}
|
||||||
geoFields={this.state.geoFields}
|
|
||||||
renderTooltipContent={renderTooltipContent}
|
renderTooltipContent={renderTooltipContent}
|
||||||
/>
|
/>
|
||||||
{!this.props.settings.hideToolbarOverlay && (
|
{!this.props.settings.hideToolbarOverlay && (
|
||||||
<ToolbarOverlay
|
<ToolbarOverlay
|
||||||
addFilters={addFilters}
|
addFilters={addFilters}
|
||||||
geoFields={this.state.geoFields}
|
|
||||||
getFilterActions={getFilterActions}
|
getFilterActions={getFilterActions}
|
||||||
getActionContext={getActionContext}
|
getActionContext={getActionContext}
|
||||||
showFitToBoundsButton={this.state.showFitToBoundsButton}
|
showFitToBoundsButton={this.state.showFitToBoundsButton}
|
||||||
|
|
|
@ -29,16 +29,12 @@ export interface Props {
|
||||||
drawState?: DrawState;
|
drawState?: DrawState;
|
||||||
isDrawingFilter: boolean;
|
isDrawingFilter: boolean;
|
||||||
mbMap: MbMap;
|
mbMap: MbMap;
|
||||||
|
geoFieldNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DrawFilterControl extends Component<Props, {}> {
|
export class DrawFilterControl extends Component<Props, {}> {
|
||||||
_onDraw = async (e: { features: Feature[] }) => {
|
_onDraw = async (e: { features: Feature[] }) => {
|
||||||
if (
|
if (!e.features.length || !this.props.drawState || !this.props.geoFieldNames.length) {
|
||||||
!e.features.length ||
|
|
||||||
!this.props.drawState ||
|
|
||||||
!this.props.drawState.geoFieldName ||
|
|
||||||
!this.props.drawState.indexPatternId
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +57,7 @@ export class DrawFilterControl extends Component<Props, {}> {
|
||||||
filter = createDistanceFilterWithMeta({
|
filter = createDistanceFilterWithMeta({
|
||||||
alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '',
|
alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '',
|
||||||
distanceKm,
|
distanceKm,
|
||||||
geoFieldName: this.props.drawState.geoFieldName,
|
geoFieldNames: this.props.geoFieldNames,
|
||||||
indexPatternId: this.props.drawState.indexPatternId,
|
|
||||||
point: [
|
point: [
|
||||||
_.round(circle.properties.center[0], precision),
|
_.round(circle.properties.center[0], precision),
|
||||||
_.round(circle.properties.center[1], precision),
|
_.round(circle.properties.center[1], precision),
|
||||||
|
@ -78,8 +73,7 @@ export class DrawFilterControl extends Component<Props, {}> {
|
||||||
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
|
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
|
||||||
? getBoundingBoxGeometry(geometry)
|
? getBoundingBoxGeometry(geometry)
|
||||||
: geometry,
|
: geometry,
|
||||||
indexPatternId: this.props.drawState.indexPatternId,
|
geoFieldNames: this.props.geoFieldNames,
|
||||||
geoFieldName: this.props.drawState.geoFieldName,
|
|
||||||
geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '',
|
geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '',
|
||||||
relation: this.props.drawState.relation
|
relation: this.props.drawState.relation
|
||||||
? this.props.drawState.relation
|
? this.props.drawState.relation
|
||||||
|
|
|
@ -10,13 +10,18 @@ import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { DrawFilterControl } from './draw_filter_control';
|
import { DrawFilterControl } from './draw_filter_control';
|
||||||
import { updateDrawState } from '../../../../actions';
|
import { updateDrawState } from '../../../../actions';
|
||||||
import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors';
|
import {
|
||||||
|
getDrawState,
|
||||||
|
isDrawingFilter,
|
||||||
|
getGeoFieldNames,
|
||||||
|
} from '../../../../selectors/map_selectors';
|
||||||
import { MapStoreState } from '../../../../reducers/store';
|
import { MapStoreState } from '../../../../reducers/store';
|
||||||
|
|
||||||
function mapStateToProps(state: MapStoreState) {
|
function mapStateToProps(state: MapStoreState) {
|
||||||
return {
|
return {
|
||||||
isDrawingFilter: isDrawingFilter(state),
|
isDrawingFilter: isDrawingFilter(state),
|
||||||
drawState: getDrawState(state),
|
drawState: getDrawState(state),
|
||||||
|
geoFieldNames: getGeoFieldNames(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ import {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
|
import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
|
||||||
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
|
|
||||||
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
|
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
|
||||||
import { MapExtentState } from '../../actions';
|
import { MapExtentState } from '../../actions';
|
||||||
import { TileStatusTracker } from './tile_status_tracker';
|
import { TileStatusTracker } from './tile_status_tracker';
|
||||||
|
@ -68,7 +67,6 @@ export interface Props {
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
getActionContext?: () => ActionExecutionContext;
|
getActionContext?: () => ActionExecutionContext;
|
||||||
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
|
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
|
||||||
geoFields: GeoFieldWithIndex[];
|
|
||||||
renderTooltipContent?: RenderToolTipContent;
|
renderTooltipContent?: RenderToolTipContent;
|
||||||
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
|
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
@ -432,7 +430,6 @@ export class MBMap extends Component<Props, State> {
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
onSingleValueTrigger={this.props.onSingleValueTrigger}
|
onSingleValueTrigger={this.props.onSingleValueTrigger}
|
||||||
geoFields={this.props.geoFields}
|
|
||||||
renderTooltipContent={this.props.renderTooltipContent}
|
renderTooltipContent={this.props.renderTooltipContent}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants';
|
import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants';
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import { GeometryFilterForm } from '../../../../components/geometry_filter_form';
|
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.
|
// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped.
|
||||||
const META_OVERHEAD = 100;
|
const META_OVERHEAD = 100;
|
||||||
|
@ -29,11 +28,11 @@ const META_OVERHEAD = 100;
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
geometry: Geometry;
|
geometry: Geometry;
|
||||||
geoFields: GeoFieldWithIndex[];
|
|
||||||
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
|
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
getActionContext?: () => ActionExecutionContext;
|
getActionContext?: () => ActionExecutionContext;
|
||||||
loadPreIndexedShape: () => Promise<PreIndexedShape | null>;
|
loadPreIndexedShape: () => Promise<PreIndexedShape | null>;
|
||||||
|
geoFieldNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -77,13 +76,9 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
|
||||||
|
|
||||||
_createFilter = async ({
|
_createFilter = async ({
|
||||||
geometryLabel,
|
geometryLabel,
|
||||||
indexPatternId,
|
|
||||||
geoFieldName,
|
|
||||||
relation,
|
relation,
|
||||||
}: {
|
}: {
|
||||||
geometryLabel: string;
|
geometryLabel: string;
|
||||||
indexPatternId: string;
|
|
||||||
geoFieldName: string;
|
|
||||||
relation: ES_SPATIAL_RELATIONS;
|
relation: ES_SPATIAL_RELATIONS;
|
||||||
}) => {
|
}) => {
|
||||||
this.setState({ errorMsg: undefined });
|
this.setState({ errorMsg: undefined });
|
||||||
|
@ -97,8 +92,7 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
|
||||||
preIndexedShape,
|
preIndexedShape,
|
||||||
geometry: this.props.geometry as Polygon,
|
geometry: this.props.geometry as Polygon,
|
||||||
geometryLabel,
|
geometryLabel,
|
||||||
indexPatternId,
|
geoFieldNames: this.props.geoFieldNames,
|
||||||
geoFieldName,
|
|
||||||
relation,
|
relation,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -130,7 +124,6 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
|
||||||
defaultMessage: 'Create filter',
|
defaultMessage: 'Create filter',
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
geoFields={this.props.geoFields}
|
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
intitialGeometryLabel={this.props.geometry.type.toLowerCase()}
|
intitialGeometryLabel={this.props.geometry.type.toLowerCase()}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
getLayerList,
|
getLayerList,
|
||||||
getOpenTooltips,
|
getOpenTooltips,
|
||||||
getHasLockedTooltips,
|
getHasLockedTooltips,
|
||||||
|
getGeoFieldNames,
|
||||||
isDrawingFilter,
|
isDrawingFilter,
|
||||||
} from '../../../selectors/map_selectors';
|
} from '../../../selectors/map_selectors';
|
||||||
import { MapStoreState } from '../../../reducers/store';
|
import { MapStoreState } from '../../../reducers/store';
|
||||||
|
@ -30,6 +31,7 @@ function mapStateToProps(state: MapStoreState) {
|
||||||
hasLockedTooltips: getHasLockedTooltips(state),
|
hasLockedTooltips: getHasLockedTooltips(state),
|
||||||
isDrawingFilter: isDrawingFilter(state),
|
isDrawingFilter: isDrawingFilter(state),
|
||||||
openTooltips: getOpenTooltips(state),
|
openTooltips: getOpenTooltips(state),
|
||||||
|
geoFieldNames: getGeoFieldNames(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ const defaultProps = {
|
||||||
layerList: [mockLayer],
|
layerList: [mockLayer],
|
||||||
isDrawingFilter: false,
|
isDrawingFilter: false,
|
||||||
addFilters: async () => {},
|
addFilters: async () => {},
|
||||||
geoFields: [],
|
geoFieldNames: [],
|
||||||
openTooltips: [],
|
openTooltips: [],
|
||||||
hasLockedTooltips: false,
|
hasLockedTooltips: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { Geometry } from 'geojson';
|
||||||
import { Filter } from 'src/plugins/data/public';
|
import { Filter } from 'src/plugins/data/public';
|
||||||
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
|
||||||
import {
|
import {
|
||||||
ES_GEO_FIELD_TYPE,
|
|
||||||
FEATURE_ID_PROPERTY_NAME,
|
FEATURE_ID_PROPERTY_NAME,
|
||||||
GEO_JSON_TYPE,
|
GEO_JSON_TYPE,
|
||||||
LON_INDEX,
|
LON_INDEX,
|
||||||
|
@ -37,7 +36,6 @@ import { FeatureGeometryFilterForm } from './features_tooltip';
|
||||||
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
|
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
|
||||||
import { ILayer } from '../../../classes/layers/layer';
|
import { ILayer } from '../../../classes/layers/layer';
|
||||||
import { IVectorLayer } from '../../../classes/layers/vector_layer';
|
import { IVectorLayer } from '../../../classes/layers/vector_layer';
|
||||||
import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
|
|
||||||
import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property';
|
import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property';
|
||||||
|
|
||||||
function justifyAnchorLocation(
|
function justifyAnchorLocation(
|
||||||
|
@ -70,7 +68,7 @@ export interface Props {
|
||||||
closeOnHoverTooltip: () => void;
|
closeOnHoverTooltip: () => void;
|
||||||
getActionContext?: () => ActionExecutionContext;
|
getActionContext?: () => ActionExecutionContext;
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
geoFields: GeoFieldWithIndex[];
|
geoFieldNames: string[];
|
||||||
hasLockedTooltips: boolean;
|
hasLockedTooltips: boolean;
|
||||||
isDrawingFilter: boolean;
|
isDrawingFilter: boolean;
|
||||||
layerList: ILayer[];
|
layerList: ILayer[];
|
||||||
|
@ -163,8 +161,10 @@ export class TooltipControl extends Component<Props, {}> {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
const geometry = this._getFeatureGeometry({ layerId, featureId });
|
const geometry = this._getFeatureGeometry({ layerId, featureId });
|
||||||
const geoFieldsForFeature = this._filterGeoFieldsByFeatureGeometry(geometry);
|
const isPolygon =
|
||||||
if (geometry && geoFieldsForFeature.length && this.props.addFilters) {
|
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({
|
actions.push({
|
||||||
label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', {
|
label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', {
|
||||||
defaultMessage: 'Filter by geometry',
|
defaultMessage: 'Filter by geometry',
|
||||||
|
@ -175,8 +175,8 @@ export class TooltipControl extends Component<Props, {}> {
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
this.props.closeOnClickTooltip(tooltipId);
|
this.props.closeOnClickTooltip(tooltipId);
|
||||||
}}
|
}}
|
||||||
geometry={geometry}
|
geometry={geometry!}
|
||||||
geoFields={geoFieldsForFeature}
|
geoFieldNames={this.props.geoFieldNames}
|
||||||
addFilters={this.props.addFilters}
|
addFilters={this.props.addFilters}
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
|
@ -191,29 +191,6 @@ export class TooltipControl extends Component<Props, {}> {
|
||||||
return actions;
|
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(
|
_getTooltipFeatures(
|
||||||
mbFeatures: MapboxGeoJSONFeature[],
|
mbFeatures: MapboxGeoJSONFeature[],
|
||||||
isLocked: boolean,
|
isLocked: boolean,
|
||||||
|
|
|
@ -29,18 +29,7 @@ exports[`Should show all controls 1`] = `
|
||||||
<Connect(FitToData) />
|
<Connect(FitToData) />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<Connect(ToolsControl)
|
<Connect(ToolsControl) />
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "myGeoFieldName",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "myIndex",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<Connect(TimesliderToggleButton) />
|
<Connect(TimesliderToggleButton) />
|
||||||
|
|
|
@ -5,4 +5,16 @@
|
||||||
* 2.0.
|
* 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 () => {
|
test('Should only show set view control', async () => {
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<ToolbarOverlay geoFields={[]} showFitToBoundsButton={false} showTimesliderButton={false} />
|
<ToolbarOverlay
|
||||||
|
showToolsControl={false}
|
||||||
|
showFitToBoundsButton={false}
|
||||||
|
showTimesliderButton={false}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should show all controls', async () => {
|
test('Should show all controls', async () => {
|
||||||
const geoFieldWithIndex = {
|
|
||||||
geoFieldName: 'myGeoFieldName',
|
|
||||||
geoFieldType: 'geo_point',
|
|
||||||
indexPatternTitle: 'myIndex',
|
|
||||||
indexPatternId: '1',
|
|
||||||
};
|
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<ToolbarOverlay
|
<ToolbarOverlay
|
||||||
addFilters={async (filters: Filter[], actionId: string) => {}}
|
addFilters={async (filters: Filter[], actionId: string) => {}}
|
||||||
geoFields={[geoFieldWithIndex]}
|
showToolsControl={true}
|
||||||
showFitToBoundsButton={true}
|
showFitToBoundsButton={true}
|
||||||
showTimesliderButton={true}
|
showTimesliderButton={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,11 +13,10 @@ import { SetViewControl } from './set_view_control';
|
||||||
import { ToolsControl } from './tools_control';
|
import { ToolsControl } from './tools_control';
|
||||||
import { FitToData } from './fit_to_data';
|
import { FitToData } from './fit_to_data';
|
||||||
import { TimesliderToggleButton } from './timeslider_toggle_button';
|
import { TimesliderToggleButton } from './timeslider_toggle_button';
|
||||||
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
addFilters?: ((filters: Filter[], actionId: string) => Promise<void>) | null;
|
addFilters?: ((filters: Filter[], actionId: string) => Promise<void>) | null;
|
||||||
geoFields: GeoFieldWithIndex[];
|
showToolsControl: boolean;
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
getActionContext?: () => ActionExecutionContext;
|
getActionContext?: () => ActionExecutionContext;
|
||||||
showFitToBoundsButton: boolean;
|
showFitToBoundsButton: boolean;
|
||||||
|
@ -26,10 +25,9 @@ export interface Props {
|
||||||
|
|
||||||
export function ToolbarOverlay(props: Props) {
|
export function ToolbarOverlay(props: Props) {
|
||||||
const toolsButton =
|
const toolsButton =
|
||||||
props.addFilters && props.geoFields.length ? (
|
props.addFilters && props.showToolsControl ? (
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<ToolsControl
|
<ToolsControl
|
||||||
geoFields={props.geoFields}
|
|
||||||
getFilterActions={props.getFilterActions}
|
getFilterActions={props.getFilterActions}
|
||||||
getActionContext={props.getActionContext}
|
getActionContext={props.getActionContext}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -56,16 +56,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
||||||
"content": <GeometryFilterForm
|
"content": <GeometryFilterForm
|
||||||
buttonLabel="Draw shape"
|
buttonLabel="Draw shape"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
intitialGeometryLabel="shape"
|
intitialGeometryLabel="shape"
|
||||||
isFilterGeometryClosed={true}
|
isFilterGeometryClosed={true}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -77,16 +67,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
||||||
"content": <GeometryFilterForm
|
"content": <GeometryFilterForm
|
||||||
buttonLabel="Draw bounds"
|
buttonLabel="Draw bounds"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
intitialGeometryLabel="bounds"
|
intitialGeometryLabel="bounds"
|
||||||
isFilterGeometryClosed={true}
|
isFilterGeometryClosed={true}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -98,16 +78,6 @@ exports[`Should render cancel button when drawing 1`] = `
|
||||||
"content": <DistanceFilterForm
|
"content": <DistanceFilterForm
|
||||||
buttonLabel="Draw distance"
|
buttonLabel="Draw distance"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
/>,
|
/>,
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
@ -187,16 +157,6 @@ exports[`renders 1`] = `
|
||||||
"content": <GeometryFilterForm
|
"content": <GeometryFilterForm
|
||||||
buttonLabel="Draw shape"
|
buttonLabel="Draw shape"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
intitialGeometryLabel="shape"
|
intitialGeometryLabel="shape"
|
||||||
isFilterGeometryClosed={true}
|
isFilterGeometryClosed={true}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -208,16 +168,6 @@ exports[`renders 1`] = `
|
||||||
"content": <GeometryFilterForm
|
"content": <GeometryFilterForm
|
||||||
buttonLabel="Draw bounds"
|
buttonLabel="Draw bounds"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
intitialGeometryLabel="bounds"
|
intitialGeometryLabel="bounds"
|
||||||
isFilterGeometryClosed={true}
|
isFilterGeometryClosed={true}
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -229,16 +179,6 @@ exports[`renders 1`] = `
|
||||||
"content": <DistanceFilterForm
|
"content": <DistanceFilterForm
|
||||||
buttonLabel="Draw distance"
|
buttonLabel="Draw distance"
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
geoFields={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"geoFieldName": "location",
|
|
||||||
"geoFieldType": "geo_point",
|
|
||||||
"indexPatternId": "1",
|
|
||||||
"indexPatternTitle": "my_index",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
/>,
|
/>,
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
|
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
|
||||||
import { DistanceFilterForm } from '../../../components/distance_filter_form';
|
import { DistanceFilterForm } from '../../../components/distance_filter_form';
|
||||||
import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
|
|
||||||
import { DrawState } from '../../../../common/descriptor_types';
|
import { DrawState } from '../../../../common/descriptor_types';
|
||||||
|
|
||||||
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
|
||||||
|
@ -54,7 +53,6 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate(
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
cancelDraw: () => void;
|
cancelDraw: () => void;
|
||||||
geoFields: GeoFieldWithIndex[];
|
|
||||||
initiateDraw: (drawState: DrawState) => void;
|
initiateDraw: (drawState: DrawState) => void;
|
||||||
isDrawingFilter: boolean;
|
isDrawingFilter: boolean;
|
||||||
getFilterActions?: () => Promise<Action[]>;
|
getFilterActions?: () => Promise<Action[]>;
|
||||||
|
@ -98,9 +96,6 @@ export class ToolsControl extends Component<Props, State> {
|
||||||
_initiateBoundsDraw = (options: {
|
_initiateBoundsDraw = (options: {
|
||||||
actionId: string;
|
actionId: string;
|
||||||
geometryLabel: string;
|
geometryLabel: string;
|
||||||
indexPatternId: string;
|
|
||||||
geoFieldName: string;
|
|
||||||
geoFieldType: ES_GEO_FIELD_TYPE;
|
|
||||||
relation: ES_SPATIAL_RELATIONS;
|
relation: ES_SPATIAL_RELATIONS;
|
||||||
}) => {
|
}) => {
|
||||||
this.props.initiateDraw({
|
this.props.initiateDraw({
|
||||||
|
@ -110,12 +105,7 @@ export class ToolsControl extends Component<Props, State> {
|
||||||
this._closePopover();
|
this._closePopover();
|
||||||
};
|
};
|
||||||
|
|
||||||
_initiateDistanceDraw = (options: {
|
_initiateDistanceDraw = (options: { actionId: string; filterLabel: string }) => {
|
||||||
actionId: string;
|
|
||||||
filterLabel: string;
|
|
||||||
indexPatternId: string;
|
|
||||||
geoFieldName: string;
|
|
||||||
}) => {
|
|
||||||
this.props.initiateDraw({
|
this.props.initiateDraw({
|
||||||
drawType: DRAW_TYPE.DISTANCE,
|
drawType: DRAW_TYPE.DISTANCE,
|
||||||
...options,
|
...options,
|
||||||
|
@ -154,7 +144,6 @@ export class ToolsControl extends Component<Props, State> {
|
||||||
<GeometryFilterForm
|
<GeometryFilterForm
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
buttonLabel={DRAW_SHAPE_LABEL_SHORT}
|
buttonLabel={DRAW_SHAPE_LABEL_SHORT}
|
||||||
geoFields={this.props.geoFields}
|
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
intitialGeometryLabel={i18n.translate(
|
intitialGeometryLabel={i18n.translate(
|
||||||
|
@ -174,7 +163,6 @@ export class ToolsControl extends Component<Props, State> {
|
||||||
<GeometryFilterForm
|
<GeometryFilterForm
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
buttonLabel={DRAW_BOUNDS_LABEL_SHORT}
|
buttonLabel={DRAW_BOUNDS_LABEL_SHORT}
|
||||||
geoFields={this.props.geoFields}
|
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
intitialGeometryLabel={i18n.translate(
|
intitialGeometryLabel={i18n.translate(
|
||||||
|
@ -194,7 +182,6 @@ export class ToolsControl extends Component<Props, State> {
|
||||||
<DistanceFilterForm
|
<DistanceFilterForm
|
||||||
className="mapDrawControl__geometryFilterForm"
|
className="mapDrawControl__geometryFilterForm"
|
||||||
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
|
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
|
||||||
geoFields={this.props.geoFields}
|
|
||||||
getFilterActions={this.props.getFilterActions}
|
getFilterActions={this.props.getFilterActions}
|
||||||
getActionContext={this.props.getActionContext}
|
getActionContext={this.props.getActionContext}
|
||||||
onSubmit={this._initiateDistanceDraw}
|
onSubmit={this._initiateDistanceDraw}
|
||||||
|
|
|
@ -461,7 +461,6 @@ export class MapEmbeddable
|
||||||
this._prevMapExtent = mapExtent;
|
this._prevMapExtent = mapExtent;
|
||||||
|
|
||||||
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
|
||||||
mapExtentFilter.meta.isMultiIndex = true;
|
|
||||||
mapExtentFilter.meta.controlledBy = this._controlledBy;
|
mapExtentFilter.meta.controlledBy = this._controlledBy;
|
||||||
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
|
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
|
||||||
defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}',
|
defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}',
|
||||||
|
|
|
@ -13300,7 +13300,6 @@
|
||||||
"xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド",
|
"xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド",
|
||||||
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "GeometryCollectionを convertESShapeToGeojsonGeometryに渡さないでください",
|
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "GeometryCollectionを convertESShapeToGeojsonGeometryに渡さないでください",
|
||||||
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "{geometryType} ジオメトリから Geojson に変換できません。サポートされていません",
|
"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.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値:{expectedTypes}、提供された値:{fieldType}",
|
||||||
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "サポートされていないジオメトリタイプ、期待値:{expectedTypes}、提供された値:{geometryType}",
|
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "サポートされていないジオメトリタイプ、期待値:{expectedTypes}、提供された値:{geometryType}",
|
||||||
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。",
|
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。",
|
||||||
|
@ -13480,7 +13479,6 @@
|
||||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
|
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
|
||||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "合計",
|
"xpack.maps.metricSelect.sumDropDownOptionLabel": "合計",
|
||||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語",
|
"xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語",
|
||||||
"xpack.maps.multiIndexFieldSelect.fieldLabel": "フィールドのフィルタリング",
|
|
||||||
"xpack.maps.mvtSource.addFieldLabel": "追加",
|
"xpack.maps.mvtSource.addFieldLabel": "追加",
|
||||||
"xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名",
|
"xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名",
|
||||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||||
|
|
|
@ -13476,7 +13476,6 @@
|
||||||
"xpack.maps.emsSource.tooltipsTitle": "工具提示字段",
|
"xpack.maps.emsSource.tooltipsTitle": "工具提示字段",
|
||||||
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "不应将 GeometryCollection 传递给 convertESShapeToGeojsonGeometry",
|
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "不应将 GeometryCollection 传递给 convertESShapeToGeojsonGeometry",
|
||||||
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "无法将 {geometryType} 几何图形转换成 geojson,不支持",
|
"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.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为 {expectedTypes},而提供的是 {fieldType}",
|
||||||
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "几何类型不受支持,应为 {expectedTypes},而提供的是 {geometryType}",
|
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "几何类型不受支持,应为 {expectedTypes},而提供的是 {geometryType}",
|
||||||
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。",
|
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。",
|
||||||
|
@ -13657,7 +13656,6 @@
|
||||||
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
|
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
|
||||||
"xpack.maps.metricSelect.sumDropDownOptionLabel": "求和",
|
"xpack.maps.metricSelect.sumDropDownOptionLabel": "求和",
|
||||||
"xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词",
|
"xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词",
|
||||||
"xpack.maps.multiIndexFieldSelect.fieldLabel": "筛选字段",
|
|
||||||
"xpack.maps.mvtSource.addFieldLabel": "添加",
|
"xpack.maps.mvtSource.addFieldLabel": "添加",
|
||||||
"xpack.maps.mvtSource.fieldPlaceholderText": "字段名称",
|
"xpack.maps.mvtSource.fieldPlaceholderText": "字段名称",
|
||||||
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
"xpack.maps.mvtSource.numberFieldLabel": "数字",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue