[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:
Nathan Reese 2021-06-02 06:17:23 -06:00 committed by GitHub
parent 7f8f89ed99
commit 14442b78de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 944 additions and 1093 deletions

View file

@ -54,6 +54,61 @@ describe('mapSpatialFilter()', () => {
expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER);
});
test('should return the key for matching multi field filter', async () => {
const filter = {
meta: {
alias: 'my spatial filter',
isMultiIndex: true,
type: FILTERS.SPATIAL_FILTER,
} as FilterMeta,
query: {
bool: {
should: [
{
bool: {
must: [
{
exists: {
field: 'geo.coordinates',
},
},
{
geo_distance: {
distance: '1000km',
'geo.coordinates': [120, 30],
},
},
],
},
},
{
bool: {
must: [
{
exists: {
field: 'location',
},
},
{
geo_distance: {
distance: '1000km',
location: [120, 30],
},
},
],
},
},
],
},
},
} as Filter;
const result = mapSpatialFilter(filter);
expect(result).toHaveProperty('key', 'query');
expect(result).toHaveProperty('value', '');
expect(result).toHaveProperty('type', FILTERS.SPATIAL_FILTER);
});
test('should return undefined for none matching', async (done) => {
const filter = {
meta: {

View file

@ -22,5 +22,18 @@ export const mapSpatialFilter = (filter: Filter) => {
value: '',
};
}
if (
filter.meta &&
filter.meta.type === FILTERS.SPATIAL_FILTER &&
filter.meta.isMultiIndex &&
filter.query?.bool?.should
) {
return {
key: 'query',
type: filter.meta.type,
value: '',
};
}
throw filter;
};

View file

@ -11,7 +11,7 @@ import { ReactNode } from 'react';
import { GeoJsonProperties } from 'geojson';
import { Geometry } from 'geojson';
import { Query } from '../../../../../src/plugins/data/common';
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
import { DRAW_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
export type MapExtent = {
minLon: number;
@ -70,9 +70,6 @@ export type DrawState = {
actionId: string;
drawType: DRAW_TYPE;
filterLabel?: string; // point radius filter alias
geoFieldName?: string;
geoFieldType?: ES_GEO_FIELD_TYPE;
geometryLabel?: string;
indexPatternId?: string;
relation?: ES_SPATIAL_RELATIONS;
};

View file

@ -9,9 +9,7 @@ import {
hitsToGeoJson,
geoPointToGeometry,
geoShapeToGeometry,
createExtentFilter,
roundCoordinates,
extractFeaturesFromFilters,
makeESBbox,
scaleBounds,
} from './elasticsearch_geo_utils';
@ -388,94 +386,6 @@ describe('geoShapeToGeometry', () => {
});
});
describe('createExtentFilter', () => {
it('should return elasticsearch geo_bounding_box filter', () => {
const mapExtent = {
maxLat: 39,
maxLon: -83,
minLat: 35,
minLon: -89,
};
const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-89, 39],
bottom_right: [-83, 35],
},
});
});
it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => {
const mapExtent = {
maxLat: 120,
maxLon: 200,
minLat: -100,
minLon: -190,
};
const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-180, 89],
bottom_right: [180, -89],
},
});
});
it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => {
const mapExtent = {
maxLat: 39,
maxLon: 200,
minLat: 35,
minLon: 100,
};
const filter = createExtentFilter(mapExtent, [geoFieldName]);
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [100, 39],
bottom_right: [-160, 35],
},
});
});
it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => {
const mapExtent = {
maxLat: 39,
maxLon: -100,
minLat: 35,
minLon: -200,
};
const filter = createExtentFilter(mapExtent, [geoFieldName]);
const leftLon = filter.geo_bounding_box.location.top_left[0];
const rightLon = filter.geo_bounding_box.location.bottom_right[0];
expect(leftLon).toBeGreaterThan(rightLon);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [160, 39],
bottom_right: [-100, 35],
},
});
});
it('should clamp longitudes to -180 to 180 when longitude wraps globe', () => {
const mapExtent = {
maxLat: 39,
maxLon: 209,
minLat: 35,
minLon: -191,
};
const filter = createExtentFilter(mapExtent, [geoFieldName]);
expect(filter.geo_bounding_box).toEqual({
location: {
top_left: [-180, 39],
bottom_right: [180, 35],
},
});
});
});
describe('roundCoordinates', () => {
it('should set coordinates precision', () => {
const coordinates = [
@ -492,134 +402,6 @@ describe('roundCoordinates', () => {
});
});
describe('extractFeaturesFromFilters', () => {
it('should ignore non-spatial filers', () => {
const phraseFilter = {
meta: {
alias: null,
disabled: false,
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
key: 'machine.os',
negate: false,
params: {
query: 'ios',
},
type: 'phrase',
},
query: {
match_phrase: {
'machine.os': 'ios',
},
},
};
expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]);
});
it('should convert geo_distance filter to feature', () => {
const spatialFilter = {
geo_distance: {
distance: '1096km',
'geo.coordinates': [-89.87125, 53.49454],
},
meta: {
alias: 'geo.coordinates within 1096km of -89.87125,53.49454',
disabled: false,
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
key: 'geo.coordinates',
negate: false,
type: 'spatial_filter',
value: '',
},
};
const features = extractFeaturesFromFilters([spatialFilter]);
expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]);
expect(features[0].properties).toEqual({
filter: 'geo.coordinates within 1096km of -89.87125,53.49454',
});
});
it('should convert geo_shape filter to feature', () => {
const spatialFilter = {
geo_shape: {
'geo.coordinates': {
relation: 'INTERSECTS',
shape: {
coordinates: [
[
[-101.21639, 48.1413],
[-101.21639, 41.84905],
[-90.95149, 41.84905],
[-90.95149, 48.1413],
[-101.21639, 48.1413],
],
],
type: 'Polygon',
},
},
ignore_unmapped: true,
},
meta: {
alias: 'geo.coordinates in bounds',
disabled: false,
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
key: 'geo.coordinates',
negate: false,
type: 'spatial_filter',
value: '',
},
};
expect(extractFeaturesFromFilters([spatialFilter])).toEqual([
{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[
[-101.21639, 48.1413],
[-101.21639, 41.84905],
[-90.95149, 41.84905],
[-90.95149, 48.1413],
[-101.21639, 48.1413],
],
],
},
properties: {
filter: 'geo.coordinates in bounds',
},
},
]);
});
it('should ignore geo_shape filter with pre-index shape', () => {
const spatialFilter = {
geo_shape: {
'geo.coordinates': {
indexed_shape: {
id: 's5gldXEBkTB2HMwpC8y0',
index: 'world_countries_v1',
path: 'coordinates',
},
relation: 'INTERSECTS',
},
ignore_unmapped: true,
},
meta: {
alias: 'geo.coordinates in multipolygon',
disabled: false,
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
key: 'geo.coordinates',
negate: false,
type: 'spatial_filter',
value: '',
},
};
expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]);
});
});
describe('makeESBbox', () => {
it('Should invert Y-axis', () => {
const bbox = makeESBbox({

View file

@ -16,61 +16,13 @@ import { BBox } from '@turf/helpers';
import {
DECIMAL_DEGREES_PRECISION,
ES_GEO_FIELD_TYPE,
ES_SPATIAL_RELATIONS,
GEO_JSON_TYPE,
POLYGON_COORDINATES_EXTERIOR_INDEX,
LON_INDEX,
LAT_INDEX,
} from '../constants';
import { getEsSpatialRelationLabel } from '../i18n_getters';
import { Filter, FilterMeta, FILTERS } from '../../../../../src/plugins/data/common';
import { MapExtent } from '../descriptor_types';
const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER;
type Coordinates = Position | Position[] | Position[][] | Position[][][];
// Elasticsearch stores more then just GeoJSON.
// 1) geometry.type as lower case string
// 2) circle and envelope types
interface ESGeometry {
type: string;
coordinates: Coordinates;
}
export interface ESBBox {
top_left: number[];
bottom_right: number[];
}
interface GeoShapeQueryBody {
shape?: Polygon;
relation?: ES_SPATIAL_RELATIONS;
indexed_shape?: PreIndexedShape;
}
// Index signature explicitly states that anything stored in an object using a string conforms to the structure
// problem is that Elasticsearch signature also allows for other string keys to conform to other structures, like 'ignore_unmapped'
// Use intersection type to exclude certain properties from the index signature
// https://basarat.gitbook.io/typescript/type-system/index-signatures#excluding-certain-properties-from-the-index-signature
type GeoShapeQuery = { ignore_unmapped: boolean } & { [geoFieldName: string]: GeoShapeQueryBody };
export type GeoFilter = Filter & {
geo_bounding_box?: {
[geoFieldName: string]: ESBBox;
};
geo_distance?: {
distance: string;
[geoFieldName: string]: Position | { lat: number; lon: number } | string;
};
geo_shape?: GeoShapeQuery;
};
export interface PreIndexedShape {
index: string;
id: string | number;
path: string;
}
import { Coordinates, ESBBox, ESGeometry } from './types';
function ensureGeoField(type: string) {
const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE];
@ -349,136 +301,6 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBo
return esBbox;
}
export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter {
const esBbox = makeESBbox(mapExtent);
return geoFieldNames.length === 1
? {
geo_bounding_box: {
[geoFieldNames[0]]: esBbox,
},
meta: {
alias: null,
disabled: false,
negate: false,
key: geoFieldNames[0],
},
}
: {
query: {
bool: {
should: geoFieldNames.map((geoFieldName) => {
return {
bool: {
must: [
{
exists: {
field: geoFieldName,
},
},
{
geo_bounding_box: {
[geoFieldName]: esBbox,
},
},
],
},
};
}),
},
},
meta: {
alias: null,
disabled: false,
negate: false,
},
};
}
export function createSpatialFilterWithGeometry({
preIndexedShape,
geometry,
geometryLabel,
indexPatternId,
geoFieldName,
relation = ES_SPATIAL_RELATIONS.INTERSECTS,
}: {
preIndexedShape?: PreIndexedShape | null;
geometry: Polygon;
geometryLabel: string;
indexPatternId: string;
geoFieldName: string;
relation: ES_SPATIAL_RELATIONS;
}): GeoFilter {
const meta: FilterMeta = {
type: SPATIAL_FILTER_TYPE,
negate: false,
index: indexPatternId,
key: geoFieldName,
alias: `${geoFieldName} ${getEsSpatialRelationLabel(relation)} ${geometryLabel}`,
disabled: false,
};
const shapeQuery: GeoShapeQueryBody = {
relation,
};
if (preIndexedShape) {
shapeQuery.indexed_shape = preIndexedShape;
} else {
shapeQuery.shape = geometry;
}
return {
meta,
// Currently no way to create an object with exclude property from index signature
// typescript error for "ignore_unmapped is not assignable to type 'GeoShapeQueryBody'" expected"
// @ts-expect-error
geo_shape: {
ignore_unmapped: true,
[geoFieldName]: shapeQuery,
},
};
}
export function createDistanceFilterWithMeta({
alias,
distanceKm,
geoFieldName,
indexPatternId,
point,
}: {
alias: string;
distanceKm: number;
geoFieldName: string;
indexPatternId: string;
point: Position;
}): GeoFilter {
const meta: FilterMeta = {
type: SPATIAL_FILTER_TYPE,
negate: false,
index: indexPatternId,
key: geoFieldName,
alias: alias
? alias
: i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', {
defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}',
values: {
distanceKm,
geoFieldName,
pointLabel: point.join(', '),
},
}),
disabled: false,
};
return {
geo_distance: {
distance: `${distanceKm}km`,
[geoFieldName]: point,
},
meta,
};
}
export function roundCoordinates(coordinates: Coordinates): void {
for (let i = 0; i < coordinates.length; i++) {
const value = coordinates[i];
@ -549,44 +371,6 @@ export function clamp(val: number, min: number, max: number): number {
}
}
export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] {
const features: Feature[] = [];
filters
.filter((filter) => {
return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE;
})
.forEach((filter) => {
const geoFieldName = filter.meta.key!;
let geometry;
if (filter.geo_distance && filter.geo_distance[geoFieldName]) {
const distanceSplit = filter.geo_distance.distance.split('km');
const distance = parseFloat(distanceSplit[0]);
const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance);
geometry = circleFeature.geometry;
} else if (
filter.geo_shape &&
filter.geo_shape[geoFieldName] &&
filter.geo_shape[geoFieldName].shape
) {
geometry = filter.geo_shape[geoFieldName].shape;
} else {
// do not know how to convert spatial filter to geometry
// this includes pre-indexed shapes
return;
}
features.push({
type: 'Feature',
geometry,
properties: {
filter: filter.meta.alias,
},
});
});
return features;
}
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent {
const width = bounds.maxLon - bounds.minLon;
const height = bounds.maxLat - bounds.minLat;

View file

@ -8,4 +8,6 @@
export * from './es_agg_utils';
export * from './convert_to_geojson';
export * from './elasticsearch_geo_utils';
export * from './spatial_filter_utils';
export * from './types';
export { isTotalHitsGreaterThan, TotalHits } from './total_hits';

View file

@ -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([]);
});
});

View file

@ -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;
}

View 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;
}

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not show "within" relation when filter geometry is not closed 1`] = `
exports[`render 1`] = `
<EuiForm>
<EuiFormRow
describedByIds={Array []}
@ -17,204 +17,6 @@ exports[`should not show "within" relation when filter geometry is not closed 1`
value="My shape"
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Spatial relation"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "intersects",
"value": "INTERSECTS",
},
Object {
"text": "disjoint",
"value": "DISJOINT",
},
]
}
value="INTERSECTS"
/>
</EuiFormRow>
<ActionSelect
onChange={[Function]}
value="ACTION_GLOBAL_APPLY_FILTER"
/>
<EuiSpacer
size="m"
/>
<EuiTextAlign
textAlign="right"
>
<EuiButton
fill={true}
isDisabled={false}
onClick={[Function]}
size="s"
>
Create filter
</EuiButton>
</EuiTextAlign>
</EuiForm>
`;
exports[`should render error message 1`] = `
<EuiForm>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Geometry label"
labelType="label"
>
<EuiFieldText
compressed={true}
onChange={[Function]}
value="My shape"
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Spatial relation"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "intersects",
"value": "INTERSECTS",
},
Object {
"text": "disjoint",
"value": "DISJOINT",
},
]
}
value="INTERSECTS"
/>
</EuiFormRow>
<ActionSelect
onChange={[Function]}
value="ACTION_GLOBAL_APPLY_FILTER"
/>
<EuiSpacer
size="m"
/>
<EuiFormErrorText>
Simulated error
</EuiFormErrorText>
<EuiTextAlign
textAlign="right"
>
<EuiButton
fill={true}
isDisabled={false}
onClick={[Function]}
size="s"
>
Create filter
</EuiButton>
</EuiTextAlign>
</EuiForm>
`;
exports[`should render relation select when geo field is geo_shape 1`] = `
<EuiForm>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Geometry label"
labelType="label"
>
<EuiFieldText
compressed={true}
onChange={[Function]}
value="My shape"
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_shape",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
@ -268,7 +70,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = `
</EuiForm>
`;
exports[`should render relation select without "within"-relation when geo field is geo_point 1`] = `
exports[`should render error message 1`] = `
<EuiForm>
<EuiFormRow
describedByIds={Array []}
@ -285,27 +87,6 @@ exports[`should render relation select without "within"-relation when geo field
value="My shape"
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
fields={
Array [
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
},
]
}
onChange={[Function]}
selectedField={
Object {
"geoFieldName": "my geo field",
"geoFieldType": "geo_point",
"indexPatternId": 1,
"indexPatternTitle": "My index",
}
}
/>
<EuiFormRow
describedByIds={Array []}
display="rowCompressed"
@ -328,6 +109,10 @@ exports[`should render relation select without "within"-relation when geo field
"text": "disjoint",
"value": "DISJOINT",
},
Object {
"text": "within",
"value": "WITHIN",
},
]
}
value="INTERSECTS"
@ -340,6 +125,9 @@ exports[`should render relation select without "within"-relation when geo field
<EuiSpacer
size="m"
/>
<EuiFormErrorText>
Simulated error
</EuiFormErrorText>
<EuiTextAlign
textAlign="right"
>

View file

@ -16,47 +16,28 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
import { GeoFieldWithIndex } from './geo_field_with_index';
import { ActionSelect } from './action_select';
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
interface Props {
className?: string;
buttonLabel: string;
geoFields: GeoFieldWithIndex[];
getFilterActions?: () => Promise<Action[]>;
getActionContext?: () => ActionExecutionContext;
onSubmit: ({
actionId,
filterLabel,
indexPatternId,
geoFieldName,
}: {
actionId: string;
filterLabel: string;
indexPatternId: string;
geoFieldName: string;
}) => void;
onSubmit: ({ actionId, filterLabel }: { actionId: string; filterLabel: string }) => void;
}
interface State {
actionId: string;
selectedField: GeoFieldWithIndex | undefined;
filterLabel: string;
}
export class DistanceFilterForm extends Component<Props, State> {
state: State = {
actionId: ACTION_GLOBAL_APPLY_FILTER,
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
filterLabel: '',
};
_onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => {
this.setState({ selectedField });
};
_onFilterLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
filterLabel: e.target.value,
@ -68,14 +49,9 @@ export class DistanceFilterForm extends Component<Props, State> {
};
_onSubmit = () => {
if (!this.state.selectedField) {
return;
}
this.props.onSubmit({
actionId: this.state.actionId,
filterLabel: this.state.filterLabel,
indexPatternId: this.state.selectedField.indexPatternId,
geoFieldName: this.state.selectedField.geoFieldName,
});
};
@ -95,12 +71,6 @@ export class DistanceFilterForm extends Component<Props, State> {
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
selectedField={this.state.selectedField}
fields={this.props.geoFields}
onChange={this._onGeoFieldChange}
/>
<ActionSelect
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
@ -111,7 +81,7 @@ export class DistanceFilterForm extends Component<Props, State> {
<EuiSpacer size="m" />
<EuiTextAlign textAlign="right">
<EuiButton size="s" fill onClick={this._onSubmit} isDisabled={!this.state.selectedField}>
<EuiButton size="s" fill onClick={this._onSubmit}>
{this.props.buttonLabel}
</EuiButton>
</EuiTextAlign>

View file

@ -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;
};

View file

@ -18,16 +18,14 @@ import {
EuiFormErrorText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants';
import { ES_SPATIAL_RELATIONS } from '../../common/constants';
import { getEsSpatialRelationLabel } from '../../common/i18n_getters';
import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select';
import { ActionSelect } from './action_select';
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../src/plugins/data/public';
export class GeometryFilterForm extends Component {
static propTypes = {
buttonLabel: PropTypes.string.isRequired,
geoFields: PropTypes.array.isRequired,
getFilterActions: PropTypes.func,
getActionContext: PropTypes.func,
intitialGeometryLabel: PropTypes.string.isRequired,
@ -42,15 +40,10 @@ export class GeometryFilterForm extends Component {
state = {
actionId: ACTION_GLOBAL_APPLY_FILTER,
selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined,
geometryLabel: this.props.intitialGeometryLabel,
relation: ES_SPATIAL_RELATIONS.INTERSECTS,
};
_onGeoFieldChange = (selectedField) => {
this.setState({ selectedField });
};
_onGeometryLabelChange = (e) => {
this.setState({
geometryLabel: e.target.value,
@ -71,29 +64,12 @@ export class GeometryFilterForm extends Component {
this.props.onSubmit({
actionId: this.state.actionId,
geometryLabel: this.state.geometryLabel,
indexPatternId: this.state.selectedField.indexPatternId,
geoFieldName: this.state.selectedField.geoFieldName,
relation: this.state.relation,
});
};
_renderRelationInput() {
// relationship only used when filtering geo_shape fields
if (!this.state.selectedField) {
return null;
}
const spatialRelations =
this.props.isFilterGeometryClosed &&
this.state.selectedField.geoFieldType !== ES_GEO_FIELD_TYPE.GEO_POINT
? Object.values(ES_SPATIAL_RELATIONS)
: Object.values(ES_SPATIAL_RELATIONS).filter((relation) => {
// - cannot filter by "within"-relation when filtering geometry is not closed
// - do not distinguish between intersects/within for filtering for points since they are equivalent
return relation !== ES_SPATIAL_RELATIONS.WITHIN;
});
const options = spatialRelations.map((relation) => {
const options = Object.values(ES_SPATIAL_RELATIONS).map((relation) => {
return {
value: relation,
text: getEsSpatialRelationLabel(relation),
@ -137,12 +113,6 @@ export class GeometryFilterForm extends Component {
/>
</EuiFormRow>
<MultiIndexGeoFieldSelect
selectedField={this.state.selectedField}
fields={this.props.geoFields}
onChange={this._onGeoFieldChange}
/>
{this._renderRelationInput()}
<ActionSelect
@ -161,7 +131,7 @@ export class GeometryFilterForm extends Component {
size="s"
fill
onClick={this._onSubmit}
isDisabled={!this.state.geometryLabel || !this.state.selectedField}
isDisabled={!this.state.geometryLabel}
isLoading={this.props.isLoading}
>
{this.props.buttonLabel}

View file

@ -16,76 +16,14 @@ const defaultProps = {
onSubmit: () => {},
};
test('should render relation select without "within"-relation when geo field is geo_point', async () => {
const component = shallow(
<GeometryFilterForm
{...defaultProps}
geoFields={[
{
geoFieldName: 'my geo field',
geoFieldType: 'geo_point',
indexPatternTitle: 'My index',
indexPatternId: 1,
},
]}
/>
);
expect(component).toMatchSnapshot();
});
test('should render relation select when geo field is geo_shape', async () => {
const component = shallow(
<GeometryFilterForm
{...defaultProps}
geoFields={[
{
geoFieldName: 'my geo field',
geoFieldType: 'geo_shape',
indexPatternTitle: 'My index',
indexPatternId: 1,
},
]}
/>
);
expect(component).toMatchSnapshot();
});
test('should not show "within" relation when filter geometry is not closed', async () => {
const component = shallow(
<GeometryFilterForm
{...defaultProps}
geoFields={[
{
geoFieldName: 'my geo field',
geoFieldType: 'geo_shape',
indexPatternTitle: 'My index',
indexPatternId: 1,
},
]}
isFilterGeometryClosed={false}
/>
);
test('render', async () => {
const component = shallow(<GeometryFilterForm {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should render error message', async () => {
const component = shallow(
<GeometryFilterForm
{...defaultProps}
geoFields={[
{
geoFieldName: 'my geo field',
geoFieldType: 'geo_point',
indexPatternTitle: 'My index',
indexPatternId: 1,
},
]}
errorMsg="Simulated error"
/>
);
const component = shallow(<GeometryFilterForm {...defaultProps} errorMsg="Simulated error" />);
expect(component).toMatchSnapshot();
});

View file

@ -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>
);
}

View file

@ -20,15 +20,12 @@ import { ToolbarOverlay } from '../toolbar_overlay';
import { EditLayerPanel } from '../edit_layer_panel';
import { AddLayerPanel } from '../add_layer_panel';
import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public';
import { getIndexPatternsFromIds } from '../../index_pattern_util';
import { ES_GEO_FIELD_TYPE, RawValue } from '../../../common/constants';
import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public';
import { RawValue } from '../../../common/constants';
import { FLYOUT_STATE } from '../../reducers/ui';
import { MapSettings } from '../../reducers/map';
import { MapSettingsPanel } from '../map_settings_panel';
import { registerLayerWizards } from '../../classes/layers/load_layer_wizards';
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
import { MapRefreshConfig } from '../../../common/descriptor_types';
import { ILayer } from '../../classes/layers/layer';
@ -58,7 +55,6 @@ export interface Props {
interface State {
isInitialLoadRenderTimeoutComplete: boolean;
domId: string;
geoFields: GeoFieldWithIndex[];
showFitToBoundsButton: boolean;
showTimesliderButton: boolean;
}
@ -66,7 +62,6 @@ interface State {
export class MapContainer extends Component<Props, State> {
private _isMounted: boolean = false;
private _isInitalLoadRenderTimerStarted: boolean = false;
private _prevIndexPatternIds: string[] = [];
private _refreshTimerId: number | null = null;
private _prevIsPaused: boolean | null = null;
private _prevInterval: number | null = null;
@ -74,7 +69,6 @@ export class MapContainer extends Component<Props, State> {
state: State = {
isInitialLoadRenderTimeoutComplete: false,
domId: uuid(),
geoFields: [],
showFitToBoundsButton: false,
showTimesliderButton: false,
};
@ -95,10 +89,6 @@ export class MapContainer extends Component<Props, State> {
this._isInitalLoadRenderTimerStarted = true;
this._startInitialLoadRenderTimer();
}
if (!!this.props.addFilters) {
this._loadGeoFields(this.props.indexPatternIds);
}
}
componentWillUnmount() {
@ -151,40 +141,6 @@ export class MapContainer extends Component<Props, State> {
}
}
async _loadGeoFields(nextIndexPatternIds: string[]) {
if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) {
// all ready loaded index pattern ids
return;
}
this._prevIndexPatternIds = nextIndexPatternIds;
const geoFields: GeoFieldWithIndex[] = [];
const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds);
indexPatterns.forEach((indexPattern) => {
indexPattern.fields.forEach((field) => {
if (
indexPattern.id &&
!indexPatternsUtils.isNestedField(field) &&
(field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE)
) {
geoFields.push({
geoFieldName: field.name,
geoFieldType: field.type,
indexPatternTitle: indexPattern.title,
indexPatternId: indexPattern.id,
});
}
});
});
if (!this._isMounted) {
return;
}
this.setState({ geoFields });
}
_setRefreshTimer = () => {
const { isPaused, interval } = this.props.refreshConfig;
@ -289,13 +245,11 @@ export class MapContainer extends Component<Props, State> {
getFilterActions={getFilterActions}
getActionContext={getActionContext}
onSingleValueTrigger={onSingleValueTrigger}
geoFields={this.state.geoFields}
renderTooltipContent={renderTooltipContent}
/>
{!this.props.settings.hideToolbarOverlay && (
<ToolbarOverlay
addFilters={addFilters}
geoFields={this.state.geoFields}
getFilterActions={getFilterActions}
getActionContext={getActionContext}
showFitToBoundsButton={this.state.showFitToBoundsButton}

View file

@ -29,16 +29,12 @@ export interface Props {
drawState?: DrawState;
isDrawingFilter: boolean;
mbMap: MbMap;
geoFieldNames: string[];
}
export class DrawFilterControl extends Component<Props, {}> {
_onDraw = async (e: { features: Feature[] }) => {
if (
!e.features.length ||
!this.props.drawState ||
!this.props.drawState.geoFieldName ||
!this.props.drawState.indexPatternId
) {
if (!e.features.length || !this.props.drawState || !this.props.geoFieldNames.length) {
return;
}
@ -61,8 +57,7 @@ export class DrawFilterControl extends Component<Props, {}> {
filter = createDistanceFilterWithMeta({
alias: this.props.drawState.filterLabel ? this.props.drawState.filterLabel : '',
distanceKm,
geoFieldName: this.props.drawState.geoFieldName,
indexPatternId: this.props.drawState.indexPatternId,
geoFieldNames: this.props.geoFieldNames,
point: [
_.round(circle.properties.center[0], precision),
_.round(circle.properties.center[1], precision),
@ -78,8 +73,7 @@ export class DrawFilterControl extends Component<Props, {}> {
this.props.drawState.drawType === DRAW_TYPE.BOUNDS
? getBoundingBoxGeometry(geometry)
: geometry,
indexPatternId: this.props.drawState.indexPatternId,
geoFieldName: this.props.drawState.geoFieldName,
geoFieldNames: this.props.geoFieldNames,
geometryLabel: this.props.drawState.geometryLabel ? this.props.drawState.geometryLabel : '',
relation: this.props.drawState.relation
? this.props.drawState.relation

View file

@ -10,13 +10,18 @@ import { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import { DrawFilterControl } from './draw_filter_control';
import { updateDrawState } from '../../../../actions';
import { getDrawState, isDrawingFilter } from '../../../../selectors/map_selectors';
import {
getDrawState,
isDrawingFilter,
getGeoFieldNames,
} from '../../../../selectors/map_selectors';
import { MapStoreState } from '../../../../reducers/store';
function mapStateToProps(state: MapStoreState) {
return {
isDrawingFilter: isDrawingFilter(state),
drawState: getDrawState(state),
geoFieldNames: getGeoFieldNames(state),
};
}

View file

@ -43,7 +43,6 @@ import {
// @ts-expect-error
} from './utils';
import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public';
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property';
import { MapExtentState } from '../../actions';
import { TileStatusTracker } from './tile_status_tracker';
@ -68,7 +67,6 @@ export interface Props {
getFilterActions?: () => Promise<Action[]>;
getActionContext?: () => ActionExecutionContext;
onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
geoFields: GeoFieldWithIndex[];
renderTooltipContent?: RenderToolTipContent;
setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void;
}
@ -432,7 +430,6 @@ export class MBMap extends Component<Props, State> {
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
onSingleValueTrigger={this.props.onSingleValueTrigger}
geoFields={this.props.geoFields}
renderTooltipContent={this.props.renderTooltipContent}
/>
) : null;

View file

@ -21,7 +21,6 @@ import {
import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../../common/constants';
// @ts-expect-error
import { GeometryFilterForm } from '../../../../components/geometry_filter_form';
import { GeoFieldWithIndex } from '../../../../components/geo_field_with_index';
// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped.
const META_OVERHEAD = 100;
@ -29,11 +28,11 @@ const META_OVERHEAD = 100;
interface Props {
onClose: () => void;
geometry: Geometry;
geoFields: GeoFieldWithIndex[];
addFilters: (filters: Filter[], actionId: string) => Promise<void>;
getFilterActions?: () => Promise<Action[]>;
getActionContext?: () => ActionExecutionContext;
loadPreIndexedShape: () => Promise<PreIndexedShape | null>;
geoFieldNames: string[];
}
interface State {
@ -77,13 +76,9 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
_createFilter = async ({
geometryLabel,
indexPatternId,
geoFieldName,
relation,
}: {
geometryLabel: string;
indexPatternId: string;
geoFieldName: string;
relation: ES_SPATIAL_RELATIONS;
}) => {
this.setState({ errorMsg: undefined });
@ -97,8 +92,7 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
preIndexedShape,
geometry: this.props.geometry as Polygon,
geometryLabel,
indexPatternId,
geoFieldName,
geoFieldNames: this.props.geoFieldNames,
relation,
});
@ -130,7 +124,6 @@ export class FeatureGeometryFilterForm extends Component<Props, State> {
defaultMessage: 'Create filter',
}
)}
geoFields={this.props.geoFields}
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
intitialGeometryLabel={this.props.geometry.type.toLowerCase()}

View file

@ -20,6 +20,7 @@ import {
getLayerList,
getOpenTooltips,
getHasLockedTooltips,
getGeoFieldNames,
isDrawingFilter,
} from '../../../selectors/map_selectors';
import { MapStoreState } from '../../../reducers/store';
@ -30,6 +31,7 @@ function mapStateToProps(state: MapStoreState) {
hasLockedTooltips: getHasLockedTooltips(state),
isDrawingFilter: isDrawingFilter(state),
openTooltips: getOpenTooltips(state),
geoFieldNames: getGeoFieldNames(state),
};
}

View file

@ -77,7 +77,7 @@ const defaultProps = {
layerList: [mockLayer],
isDrawingFilter: false,
addFilters: async () => {},
geoFields: [],
geoFieldNames: [],
openTooltips: [],
hasLockedTooltips: false,
};

View file

@ -20,7 +20,6 @@ import { Geometry } from 'geojson';
import { Filter } from 'src/plugins/data/public';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
import {
ES_GEO_FIELD_TYPE,
FEATURE_ID_PROPERTY_NAME,
GEO_JSON_TYPE,
LON_INDEX,
@ -37,7 +36,6 @@ import { FeatureGeometryFilterForm } from './features_tooltip';
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
import { ILayer } from '../../../classes/layers/layer';
import { IVectorLayer } from '../../../classes/layers/vector_layer';
import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property';
function justifyAnchorLocation(
@ -70,7 +68,7 @@ export interface Props {
closeOnHoverTooltip: () => void;
getActionContext?: () => ActionExecutionContext;
getFilterActions?: () => Promise<Action[]>;
geoFields: GeoFieldWithIndex[];
geoFieldNames: string[];
hasLockedTooltips: boolean;
isDrawingFilter: boolean;
layerList: ILayer[];
@ -163,8 +161,10 @@ export class TooltipControl extends Component<Props, {}> {
const actions = [];
const geometry = this._getFeatureGeometry({ layerId, featureId });
const geoFieldsForFeature = this._filterGeoFieldsByFeatureGeometry(geometry);
if (geometry && geoFieldsForFeature.length && this.props.addFilters) {
const isPolygon =
geometry &&
(geometry.type === GEO_JSON_TYPE.POLYGON || geometry.type === GEO_JSON_TYPE.MULTI_POLYGON);
if (isPolygon && this.props.geoFieldNames.length && this.props.addFilters) {
actions.push({
label: i18n.translate('xpack.maps.tooltip.action.filterByGeometryLabel', {
defaultMessage: 'Filter by geometry',
@ -175,8 +175,8 @@ export class TooltipControl extends Component<Props, {}> {
onClose={() => {
this.props.closeOnClickTooltip(tooltipId);
}}
geometry={geometry}
geoFields={geoFieldsForFeature}
geometry={geometry!}
geoFieldNames={this.props.geoFieldNames}
addFilters={this.props.addFilters}
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
@ -191,29 +191,6 @@ export class TooltipControl extends Component<Props, {}> {
return actions;
}
_filterGeoFieldsByFeatureGeometry(geometry: Geometry | null) {
if (!geometry) {
return [];
}
// line geometry can only create filters for geo_shape fields.
if (
geometry.type === GEO_JSON_TYPE.LINE_STRING ||
geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING
) {
return this.props.geoFields.filter(({ geoFieldType }) => {
return geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE;
});
}
// TODO support geo distance filters for points
if (geometry.type === GEO_JSON_TYPE.POINT || geometry.type === GEO_JSON_TYPE.MULTI_POINT) {
return [];
}
return this.props.geoFields;
}
_getTooltipFeatures(
mbFeatures: MapboxGeoJSONFeature[],
isLocked: boolean,

View file

@ -29,18 +29,7 @@ exports[`Should show all controls 1`] = `
<Connect(FitToData) />
</EuiFlexItem>
<EuiFlexItem>
<Connect(ToolsControl)
geoFields={
Array [
Object {
"geoFieldName": "myGeoFieldName",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "myIndex",
},
]
}
/>
<Connect(ToolsControl) />
</EuiFlexItem>
<EuiFlexItem>
<Connect(TimesliderToggleButton) />

View file

@ -5,4 +5,16 @@
* 2.0.
*/
export { ToolbarOverlay } from './toolbar_overlay';
import { connect } from 'react-redux';
import { MapStoreState } from '../../reducers/store';
import { getGeoFieldNames } from '../../selectors/map_selectors';
import { ToolbarOverlay } from './toolbar_overlay';
function mapStateToProps(state: MapStoreState) {
return {
showToolsControl: getGeoFieldNames(state).length !== 0,
};
}
const connected = connect(mapStateToProps)(ToolbarOverlay);
export { connected as ToolbarOverlay };

View file

@ -21,22 +21,20 @@ import { ToolbarOverlay } from './toolbar_overlay';
test('Should only show set view control', async () => {
const component = shallow(
<ToolbarOverlay geoFields={[]} showFitToBoundsButton={false} showTimesliderButton={false} />
<ToolbarOverlay
showToolsControl={false}
showFitToBoundsButton={false}
showTimesliderButton={false}
/>
);
expect(component).toMatchSnapshot();
});
test('Should show all controls', async () => {
const geoFieldWithIndex = {
geoFieldName: 'myGeoFieldName',
geoFieldType: 'geo_point',
indexPatternTitle: 'myIndex',
indexPatternId: '1',
};
const component = shallow(
<ToolbarOverlay
addFilters={async (filters: Filter[], actionId: string) => {}}
geoFields={[geoFieldWithIndex]}
showToolsControl={true}
showFitToBoundsButton={true}
showTimesliderButton={true}
/>

View file

@ -13,11 +13,10 @@ import { SetViewControl } from './set_view_control';
import { ToolsControl } from './tools_control';
import { FitToData } from './fit_to_data';
import { TimesliderToggleButton } from './timeslider_toggle_button';
import { GeoFieldWithIndex } from '../../components/geo_field_with_index';
export interface Props {
addFilters?: ((filters: Filter[], actionId: string) => Promise<void>) | null;
geoFields: GeoFieldWithIndex[];
showToolsControl: boolean;
getFilterActions?: () => Promise<Action[]>;
getActionContext?: () => ActionExecutionContext;
showFitToBoundsButton: boolean;
@ -26,10 +25,9 @@ export interface Props {
export function ToolbarOverlay(props: Props) {
const toolsButton =
props.addFilters && props.geoFields.length ? (
props.addFilters && props.showToolsControl ? (
<EuiFlexItem>
<ToolsControl
geoFields={props.geoFields}
getFilterActions={props.getFilterActions}
getActionContext={props.getActionContext}
/>

View file

@ -56,16 +56,6 @@ exports[`Should render cancel button when drawing 1`] = `
"content": <GeometryFilterForm
buttonLabel="Draw shape"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
intitialGeometryLabel="shape"
isFilterGeometryClosed={true}
onSubmit={[Function]}
@ -77,16 +67,6 @@ exports[`Should render cancel button when drawing 1`] = `
"content": <GeometryFilterForm
buttonLabel="Draw bounds"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
intitialGeometryLabel="bounds"
isFilterGeometryClosed={true}
onSubmit={[Function]}
@ -98,16 +78,6 @@ exports[`Should render cancel button when drawing 1`] = `
"content": <DistanceFilterForm
buttonLabel="Draw distance"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
onSubmit={[Function]}
/>,
"id": 3,
@ -187,16 +157,6 @@ exports[`renders 1`] = `
"content": <GeometryFilterForm
buttonLabel="Draw shape"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
intitialGeometryLabel="shape"
isFilterGeometryClosed={true}
onSubmit={[Function]}
@ -208,16 +168,6 @@ exports[`renders 1`] = `
"content": <GeometryFilterForm
buttonLabel="Draw bounds"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
intitialGeometryLabel="bounds"
isFilterGeometryClosed={true}
onSubmit={[Function]}
@ -229,16 +179,6 @@ exports[`renders 1`] = `
"content": <DistanceFilterForm
buttonLabel="Draw distance"
className="mapDrawControl__geometryFilterForm"
geoFields={
Array [
Object {
"geoFieldName": "location",
"geoFieldType": "geo_point",
"indexPatternId": "1",
"indexPatternTitle": "my_index",
},
]
}
onSubmit={[Function]}
/>,
"id": 3,

View file

@ -22,7 +22,6 @@ import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../
// @ts-expect-error
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
import { DistanceFilterForm } from '../../../components/distance_filter_form';
import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
import { DrawState } from '../../../../common/descriptor_types';
const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', {
@ -54,7 +53,6 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate(
export interface Props {
cancelDraw: () => void;
geoFields: GeoFieldWithIndex[];
initiateDraw: (drawState: DrawState) => void;
isDrawingFilter: boolean;
getFilterActions?: () => Promise<Action[]>;
@ -98,9 +96,6 @@ export class ToolsControl extends Component<Props, State> {
_initiateBoundsDraw = (options: {
actionId: string;
geometryLabel: string;
indexPatternId: string;
geoFieldName: string;
geoFieldType: ES_GEO_FIELD_TYPE;
relation: ES_SPATIAL_RELATIONS;
}) => {
this.props.initiateDraw({
@ -110,12 +105,7 @@ export class ToolsControl extends Component<Props, State> {
this._closePopover();
};
_initiateDistanceDraw = (options: {
actionId: string;
filterLabel: string;
indexPatternId: string;
geoFieldName: string;
}) => {
_initiateDistanceDraw = (options: { actionId: string; filterLabel: string }) => {
this.props.initiateDraw({
drawType: DRAW_TYPE.DISTANCE,
...options,
@ -154,7 +144,6 @@ export class ToolsControl extends Component<Props, State> {
<GeometryFilterForm
className="mapDrawControl__geometryFilterForm"
buttonLabel={DRAW_SHAPE_LABEL_SHORT}
geoFields={this.props.geoFields}
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
intitialGeometryLabel={i18n.translate(
@ -174,7 +163,6 @@ export class ToolsControl extends Component<Props, State> {
<GeometryFilterForm
className="mapDrawControl__geometryFilterForm"
buttonLabel={DRAW_BOUNDS_LABEL_SHORT}
geoFields={this.props.geoFields}
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
intitialGeometryLabel={i18n.translate(
@ -194,7 +182,6 @@ export class ToolsControl extends Component<Props, State> {
<DistanceFilterForm
className="mapDrawControl__geometryFilterForm"
buttonLabel={DRAW_DISTANCE_LABEL_SHORT}
geoFields={this.props.geoFields}
getFilterActions={this.props.getFilterActions}
getActionContext={this.props.getActionContext}
onSubmit={this._initiateDistanceDraw}

View file

@ -461,7 +461,6 @@ export class MapEmbeddable
this._prevMapExtent = mapExtent;
const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames);
mapExtentFilter.meta.isMultiIndex = true;
mapExtentFilter.meta.controlledBy = this._controlledBy;
mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', {
defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}',

View file

@ -13300,7 +13300,6 @@
"xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド",
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "GeometryCollectionを convertESShapeToGeojsonGeometryに渡さないでください",
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "{geometryType} ジオメトリから Geojson に変換できません。サポートされていません",
"xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel} の {distanceKm}km 以内にある {geoFieldName}",
"xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値:{expectedTypes}、提供された値:{fieldType}",
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "サポートされていないジオメトリタイプ、期待値:{expectedTypes}、提供された値:{geometryType}",
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。",
@ -13480,7 +13479,6 @@
"xpack.maps.metricSelect.selectAggregationPlaceholder": "集約を選択",
"xpack.maps.metricSelect.sumDropDownOptionLabel": "合計",
"xpack.maps.metricSelect.termsDropDownOptionLabel": "トップ用語",
"xpack.maps.multiIndexFieldSelect.fieldLabel": "フィールドのフィルタリング",
"xpack.maps.mvtSource.addFieldLabel": "追加",
"xpack.maps.mvtSource.fieldPlaceholderText": "フィールド名",
"xpack.maps.mvtSource.numberFieldLabel": "数字",

View file

@ -13476,7 +13476,6 @@
"xpack.maps.emsSource.tooltipsTitle": "工具提示字段",
"xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "不应将 GeometryCollection 传递给 convertESShapeToGeojsonGeometry",
"xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "无法将 {geometryType} 几何图形转换成 geojson不支持",
"xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel} {distanceKm}km 内的 {geoFieldName}",
"xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为 {expectedTypes},而提供的是 {fieldType}",
"xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "几何类型不受支持,应为 {expectedTypes},而提供的是 {geometryType}",
"xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。",
@ -13657,7 +13656,6 @@
"xpack.maps.metricSelect.selectAggregationPlaceholder": "选择聚合",
"xpack.maps.metricSelect.sumDropDownOptionLabel": "求和",
"xpack.maps.metricSelect.termsDropDownOptionLabel": "热门词",
"xpack.maps.multiIndexFieldSelect.fieldLabel": "筛选字段",
"xpack.maps.mvtSource.addFieldLabel": "添加",
"xpack.maps.mvtSource.fieldPlaceholderText": "字段名称",
"xpack.maps.mvtSource.numberFieldLabel": "数字",