From f52eabbc20f1591f3e5a7af0810509b50d48ff48 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 10:56:02 -0600 Subject: [PATCH 1/6] [Maps] refactor createShapeFilterWithMeta to support more than just polygons --- .../legacy/plugins/maps/common/constants.js | 11 + .../plugins/maps/common/i18n_getters.js | 25 ++ .../connected_components/map/mb/view.js | 68 ++--- .../maps/public/elasticsearch_geo_utils.js | 252 +++++++++++------- .../public/elasticsearch_geo_utils.test.js | 31 ++- 5 files changed, 248 insertions(+), 139 deletions(-) diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index d7f7e353799d7b..a94f33760e4acd 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -46,6 +46,13 @@ export const ES_GEO_FIELD_TYPE = { GEO_SHAPE: 'geo_shape' }; +export const ES_SPATIAL_RELATIONS = { + INTERSECTS: 'INTERSECTS', + DISJOINT: 'DISJOINT', + WITHIN: 'WITHIN', + CONTAINS: 'CONTAINS' +}; + export const GEO_JSON_TYPE = { POINT: 'Point', MULTI_POINT: 'MultiPoint', @@ -56,6 +63,10 @@ export const GEO_JSON_TYPE = { GEOMETRY_COLLECTION: 'GeometryCollection', }; +export const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; +export const LON_INDEX = 0; +export const LAT_INDEX = 1; + export const EMPTY_FEATURE_COLLECTION = { type: 'FeatureCollection', features: [] diff --git a/x-pack/legacy/plugins/maps/common/i18n_getters.js b/x-pack/legacy/plugins/maps/common/i18n_getters.js index 0055c899e52c72..80027f0958b6bf 100644 --- a/x-pack/legacy/plugins/maps/common/i18n_getters.js +++ b/x-pack/legacy/plugins/maps/common/i18n_getters.js @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { ES_SPATIAL_RELATIONS } from './constants'; + export function getAppTitle() { return i18n.translate('xpack.maps.appTitle', { defaultMessage: 'Maps' @@ -24,3 +26,26 @@ export function getUrlLabel() { defaultMessage: 'Url' }); } + +export function getEsSpatialRelationLabel(spatialRelation) { + switch (spatialRelation) { + case ES_SPATIAL_RELATIONS.INTERSECTS: + return i18n.translate('xpack.maps.common.esSpatialRelation.intersectsLabel', { + defaultMessage: 'intersects' + }); + case ES_SPATIAL_RELATIONS.DISJOINT: + return i18n.translate('xpack.maps.common.esSpatialRelation.disjointLabel', { + defaultMessage: 'disjoint' + }); + case ES_SPATIAL_RELATIONS.WITHIN: + return i18n.translate('xpack.maps.common.esSpatialRelation.withinLabel', { + defaultMessage: 'WITHIN' + }); + case ES_SPATIAL_RELATIONS.CONTAINS: + return i18n.translate('xpack.maps.common.esSpatialRelation.containsLabel', { + defaultMessage: 'contains' + }); + default: + return spatialRelation; + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 07d6d189437b0f..a569f903da1bb8 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -23,11 +23,16 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { FeatureTooltip } from '../feature_tooltip'; import { DRAW_TYPE } from '../../../actions/map_actions'; -import { createShapeFilterWithMeta, createExtentFilterWithMeta } from '../../../elasticsearch_geo_utils'; +import { + createGeometryFilterWithMeta, + getBoundingBoxGeometry, + setPrecision +} from '../../../elasticsearch_geo_utils'; import chrome from 'ui/chrome'; import { spritesheet } from '@elastic/maki'; import sprites1 from '@elastic/maki/dist/sprite@1.png'; import sprites2 from '@elastic/maki/dist/sprite@2.png'; +import { i18n } from '@kbn/i18n'; const isRetina = window.devicePixelRatio === 2; const mbDrawModes = MapboxDraw.modes; @@ -84,40 +89,43 @@ export class MBMapContainer extends React.Component { this.props.setTooltipState(null); }; - _onDraw = async (e) => { - + _onDraw = (e) => { if (!e.features.length) { return; } - const { geoField, geoFieldType, indexPatternId, drawType } = this.props.drawState; - this.props.disableDrawState(); - - - let filter; - if (drawType === DRAW_TYPE.POLYGON) { - filter = createShapeFilterWithMeta(e.features[0].geometry, indexPatternId, geoField, geoFieldType); - } else if (drawType === DRAW_TYPE.BOUNDS) { - const coordinates = e.features[0].geometry.coordinates[0]; - const extent = { - minLon: coordinates[0][0], - minLat: coordinates[0][1], - maxLon: coordinates[0][0], - maxLat: coordinates[0][1] - }; - for (let i = 1; i < coordinates.length; i++) { - extent.minLon = Math.min(coordinates[i][0], extent.minLon); - extent.minLat = Math.min(coordinates[i][1], extent.minLat); - extent.maxLon = Math.max(coordinates[i][0], extent.maxLon); - extent.maxLat = Math.max(coordinates[i][1], extent.maxLat); - } - filter = createExtentFilterWithMeta(extent, indexPatternId, geoField, geoFieldType); - } - if (!filter) { - return; - } + const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS; + const geometry = e.features[0].geometry; + // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number + setPrecision(geometry.coordinates); + + // TODO allow user to set geojson label when initiating draw + const geometryLabel = isBoundingBox + ? i18n.translate('xpack.maps.drawControl.defaultEnvelopeLabel', { + defaultMessage: 'extent' + }) + : i18n.translate('xpack.maps.drawControl.defaultShapeLabel', { + defaultMessage: 'shape' + }); - this.props.addFilters([filter]); + try { + const filter = createGeometryFilterWithMeta({ + geometry: isBoundingBox + ? getBoundingBoxGeometry(geometry) + : geometry, + geometryLabel, + indexPatternId: this.props.drawState.indexPatternId, + geoFieldName: this.props.drawState.geoField, + geoFieldType: this.props.drawState.geoFieldType, + isBoundingBox, + }); + this.props.addFilters([filter]); + } catch (error) { + // TODO notify user why filter was not created + console.log(error); + } finally { + this.props.disableDrawState(); + } }; _debouncedSync = _.debounce(() => { diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 084d652c5b47e4..575f70ac4e6b1f 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -8,7 +8,43 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { parse } from 'wellknown'; import { decodeGeoHash } from 'ui/utils/decode_geo_hash'; -import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, GEO_JSON_TYPE } from '../common/constants'; +import { + DECIMAL_DEGREES_PRECISION, + ES_GEO_FIELD_TYPE, + ES_SPATIAL_RELATIONS, + GEO_JSON_TYPE, + POLYGON_COORDINATES_EXTERIOR_INDEX, + LON_INDEX, + LAT_INDEX, +} from '../common/constants'; +import { getEsSpatialRelationLabel } from '../common/i18n_getters'; + +function ensureGeoField(type) { + const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; + if (!expectedTypes.includes(type)) { + const errorMessage = i18n.translate('xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage', { + defaultMessage: 'Unsupported field type, expected: {expectedTypes}, you provided: {fieldType}', + values: { + fieldType: type, + expectedTypes: expectedTypes.join(',') + } + }); + throw new Error(errorMessage); + } +} + +function ensureGeometryType(type, expectedTypes) { + if (!expectedTypes.includes(type)) { + const errorMessage = i18n.translate('xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage', { + defaultMessage: 'Unsupported geometry type, expected: {expectedTypes}, you provided: {geometryType}', + values: { + geometryType: type, + expectedTypes: expectedTypes.join(',') + } + }); + throw new Error(errorMessage); + } +} /** * Converts Elasticsearch search results into GeoJson FeatureCollection @@ -28,17 +64,14 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { const properties = flattenHit(hits[i]); tmpGeometriesAccumulator.length = 0;//truncate accumulator + + ensureGeoField(geoFieldType); if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { geoPointToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); } else { - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.unsupportedFieldTypeErrorMessage', { - defaultMessage: 'Unsupported field type, expected: geo_shape or geo_point, you provided: {geoFieldType}', - values: { geoFieldType } - }); - throw new Error(errorMessage); + geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); } + // don't include geometry field value in properties delete properties[geoFieldName]; @@ -92,7 +125,7 @@ export function geoPointToGeometry(value, accumulator) { } if (!Array.isArray(value)) { - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.unsupportedGeoPointValueErrorMessage', { + const errorMessage = i18n.translate('xpack.maps.es_geo_utils.unsupportedGeoPointValueErrorMessage', { defaultMessage: `Unsupported geo_point value: {geoPointValue}`, values: { geoPointValue: value @@ -153,7 +186,7 @@ export function convertESShapeToGeojsonGeometry(value) { break; case 'envelope': case 'circle': - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.convert.unsupportedGeometryTypeErrorMessage', { + const errorMessage = i18n.translate('xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', { defaultMessage: `Unable to convert {geometryType} geometry to geojson, not supported`, values: { geometryType: geoJson.type @@ -168,7 +201,7 @@ function convertWKTStringToGeojson(value) { try { return parse(value); } catch (e) { - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.wkt.invalidWKTErrorMessage', { + const errorMessage = i18n.translate('xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage', { defaultMessage: `Unable to convert {wkt} to geojson. Valid WKT expected.`, values: { wkt: value @@ -178,7 +211,6 @@ function convertWKTStringToGeojson(value) { } } - export function geoShapeToGeometry(value, accumulator) { if (!value) { @@ -203,118 +235,134 @@ export function geoShapeToGeometry(value, accumulator) { accumulator.push(geoJson); } -const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; -const TOP_LEFT_INDEX = 0; -const BOTTOM_RIGHT_INDEX = 2; +function createGeoBoundBoxFilter(geometry, geoFieldName, filterProps = {}) { + ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); + + const TOP_LEFT_INDEX = 0; + const BOTTOM_RIGHT_INDEX = 2; + const verticies = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; + return { + geo_bounding_box: { + [geoFieldName]: { + top_left: verticies[TOP_LEFT_INDEX], + bottom_right: verticies[BOTTOM_RIGHT_INDEX] + } + }, + ...filterProps + }; +} export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) { + ensureGeoField(geoFieldType); + const safePolygon = convertMapExtentToPolygon(mapExtent); + if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - const verticies = safePolygon.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; - return { - geo_bounding_box: { - [geoFieldName]: { - top_left: verticies[TOP_LEFT_INDEX], - bottom_right: verticies[BOTTOM_RIGHT_INDEX] - } + return createGeoBoundBoxFilter(safePolygon, geoFieldName); + } + + return { + geo_shape: { + [geoFieldName]: { + shape: safePolygon, + relation: ES_SPATIAL_RELATIONS.INTERSECTS } - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { + } + }; +} + +export function createGeometryFilterWithMeta({ + geometry, + geometryLabel, + indexPatternId, + geoFieldName, + geoFieldType, + relation = ES_SPATIAL_RELATIONS.INTERSECTS, + isBoundingBox = false, +}) { + + ensureGeoField(geoFieldType); + + const relationLabel = geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT + ? i18n.translate('xpack.maps.es_geo_utils.shapeFilter.geoPointRelationLabel', { + defaultMessage: 'in' + }) + : getEsSpatialRelationLabel(relation); + const meta = { + negate: false, + index: indexPatternId, + alias: `${geoFieldName} ${relationLabel} ${geometryLabel}` + }; + + if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { return { + meta, geo_shape: { + ignore_unmapped: true, [geoFieldName]: { - shape: safePolygon, - relation: 'INTERSECTS' + shape: geometry, + relation } } }; - } else { - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.extent.unsupportedGeoFieldTypeErrorMessage', { - defaultMessage: `Unsupported field type, expected: geo_shape or geo_point, you provided: {geoFieldType}`, - values: { geoFieldType } - }); - throw new Error(errorMessage); } -} + // geo_points supports limited geometry types + // TODO add support for multi_polygon + ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); -export function createExtentFilterWithMeta(mapExtent, indexPatternId, geoFieldName, geoFieldType) { - - const roundedExtent = { - minLon: _.round(mapExtent.minLon, DECIMAL_DEGREES_PRECISION), - minLat: _.round(mapExtent.minLat, DECIMAL_DEGREES_PRECISION), - maxLon: _.round(mapExtent.maxLon, DECIMAL_DEGREES_PRECISION), - maxLat: _.round(mapExtent.maxLat, DECIMAL_DEGREES_PRECISION) - }; + if (isBoundingBox) { + return createGeoBoundBoxFilter(geometry, geoFieldName, { meta }); + } - const filter = createExtentFilter(roundedExtent, geoFieldName, geoFieldType); - filter.meta = { - negate: false, - index: indexPatternId, - alias: i18n.translate('xpack.maps.elasticsearch_geo_utils.extentFilter.aliasTitle', { - defaultMessage: `extent at {coordinate}`, - values: { - coordinate: `[${roundedExtent.minLon}, ${roundedExtent.minLat}, ${roundedExtent.maxLon}, ${roundedExtent.maxLat}]` + return { + meta, + geo_polygon: { + ignore_unmapped: true, + [geoFieldName]: { + points: geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX].map(coordinatePair => { + return { + lon: coordinatePair[LON_INDEX], + lat: coordinatePair[LAT_INDEX] + }; + }) } - }) + } }; - return filter; } -export function createShapeFilterWithMeta(geojsonPolygon, indexPatternId, geoFieldName, geoFieldType) { - - const filter = { - meta: { - negate: false, - index: indexPatternId, - alias: i18n.translate('xpack.maps.elasticsearch_geo_utils.shapeFilter.aliasTitle', { - defaultMessage: `shape at {coordinate}`, - values: { - // eslint-disable-next-line max-len - coordinate: `${_.round(geojsonPolygon.coordinates[0][0][0], DECIMAL_DEGREES_PRECISION)}, ${_.round(geojsonPolygon.coordinates[0][0][1], DECIMAL_DEGREES_PRECISION)}` - } - }) +export function setPrecision(coordinates, precision = DECIMAL_DEGREES_PRECISION) { + coordinates.forEach((value, index) => { + if (Array.isArray(value)) { + setPrecision(value); + } else if (!isNaN(value)) { + coordinates[index] = _.round(value, precision); } - }; - - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - const pointsArray = geojsonPolygon.coordinates[0].map(coordinatePair => { - return { - lon: _.round(coordinatePair[0], DECIMAL_DEGREES_PRECISION), - lat: _.round(coordinatePair[1], DECIMAL_DEGREES_PRECISION) - }; - }); - filter.geo_polygon = { - ignore_unmapped: true, - [geoFieldName]: { - points: pointsArray - } - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonCoordinateArray = geojsonPolygon.coordinates[0].map(coordinatePair => { - return [_.round(coordinatePair[0], DECIMAL_DEGREES_PRECISION), _.round(coordinatePair[1], DECIMAL_DEGREES_PRECISION)]; - }); - filter.geo_shape = { - ignore_unmapped: true, - [geoFieldName]: { - shape: { - type: 'Polygon', - coordinates: [geojsonCoordinateArray] - }, - relation: 'INTERSECTS' - } - }; - } else { - const errorMessage = i18n.translate('xpack.maps.elasticsearch_geo_utils.shape.unsupportedGeoFieldTypeErrorMessage', { - defaultMessage: `Unsupported field type, expected: geo_shape or geo_point, you provided: {geoFieldType}`, - values: { geoFieldType } - }); - throw new Error(errorMessage); - } - return filter; + }); } +/* + * returns Polygon geometry where coordinates define a bounding box that contains the input geometry + */ +export function getBoundingBoxGeometry(geometry) { + ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); + + const exterior = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; + const extent = { + minLon: exterior[0][LON_INDEX], + minLat: exterior[0][LAT_INDEX], + maxLon: exterior[0][LON_INDEX], + maxLat: exterior[0][LAT_INDEX] + }; + for (let i = 1; i < exterior.length; i++) { + extent.minLon = Math.min(exterior[i][LON_INDEX], extent.minLon); + extent.minLat = Math.min(exterior[i][LAT_INDEX], extent.minLat); + extent.maxLon = Math.max(exterior[i][LON_INDEX], extent.maxLon); + extent.maxLat = Math.max(exterior[i][LAT_INDEX], extent.maxLat); + } + return convertMapExtentToPolygon(extent); +} function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons @@ -328,7 +376,7 @@ function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { const bottomRight = [right, bottom]; const topRight = [right, top]; return { - 'type': 'polygon', + 'type': GEO_JSON_TYPE.POLYGON, 'coordinates': [ [ topLeft, bottomLeft, bottomRight, topRight, topLeft ] ] diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index dfffcb65279f49..fe08be86aa3914 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -12,6 +12,7 @@ import { geoShapeToGeometry, createExtentFilter, convertMapExtentToPolygon, + setPrecision, } from './elasticsearch_geo_utils'; import { flattenHitWrapper } from 'ui/index_patterns'; @@ -332,7 +333,7 @@ describe('createExtentFilter', () => { 'coordinates': [ [[-89, 39], [-89, 35], [-83, 35], [-83, 39], [-89, 39]] ], - 'type': 'polygon' + 'type': 'Polygon' } } } @@ -355,7 +356,7 @@ describe('createExtentFilter', () => { 'coordinates': [ [[-180, 39], [-180, 35], [180, 35], [180, 39], [-180, 39]] ], - 'type': 'polygon' + 'type': 'Polygon' } } } @@ -372,7 +373,7 @@ describe('convertMapExtentToPolygon', () => { minLon: 90, }; expect(convertMapExtentToPolygon(bounds)).toEqual({ - 'type': 'polygon', + 'type': 'Polygon', 'coordinates': [ [[90, 10], [90, -10], [100, -10], [100, 10], [90, 10]] ] @@ -387,7 +388,7 @@ describe('convertMapExtentToPolygon', () => { minLon: -400, }; expect(convertMapExtentToPolygon(bounds)).toEqual({ - 'type': 'polygon', + 'type': 'Polygon', 'coordinates': [ [[-180, 10], [-180, -10], [180, -10], [180, 10], [-180, 10]] ] @@ -402,7 +403,7 @@ describe('convertMapExtentToPolygon', () => { minLon: -400, }; expect(convertMapExtentToPolygon(bounds)).toEqual({ - 'type': 'polygon', + 'type': 'Polygon', 'coordinates': [ [[-180, 10], [-180, -10], [180, -10], [180, 10], [-180, 10]] ] @@ -417,7 +418,7 @@ describe('convertMapExtentToPolygon', () => { minLon: 170, }; expect(convertMapExtentToPolygon(bounds)).toEqual({ - 'type': 'polygon', + 'type': 'Polygon', 'coordinates': [ [[170, 10], [170, -10], [-170, -10], [-170, 10], [170, 10]] ] @@ -432,10 +433,26 @@ describe('convertMapExtentToPolygon', () => { minLon: -190, }; expect(convertMapExtentToPolygon(bounds)).toEqual({ - 'type': 'polygon', + 'type': 'Polygon', 'coordinates': [ [[170, 10], [170, -10], [-170, -10], [-170, 10], [170, 10]] ] }); }); }); + +describe('setPrecision', () => { + it('should set coordinates precision', () => { + const coordinates = [ + [110.21515290475513, 40.23193047044205], + [-105.30620093073654, 40.23193047044205], + [-105.30620093073654, 30.647128842617803] + ]; + setPrecision(coordinates); + expect(coordinates).toEqual([ + [110.21515, 40.23193], + [-105.30620, 40.23193], + [-105.3062, 30.64713] + ]); + }); +}); From 34eff664f091afe9acb6695da35799c859c809cf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 11:07:22 -0600 Subject: [PATCH 2/6] rename setPrecision to roundCoordinates --- .../map/feature_geometry_filter_form.js | 148 ++++++++++++++++++ .../connected_components/map/mb/view.js | 4 +- .../maps/public/elasticsearch_geo_utils.js | 4 +- .../public/elasticsearch_geo_utils.test.js | 6 +- 4 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js new file mode 100644 index 00000000000000..60505e9f81c8fc --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiIcon, + EuiForm, + EuiFormRow, + EuiSuperSelect, + EuiTextColor, + EuiText, + EuiFieldText, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +const GEO_FIELD_VALUE_DELIMITER = '//'; // `/` is not allowed in index pattern name so should not have collisions + +function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) { + return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`; +} + +export class FeatureGeometryFilterForm extends Component { + + // TODO add ability to specify spatial relationship + + state = { + geoField: createIndexGeoFieldName(this.props.geoFields[0]), + geometryName: this.props.feature.geometry.type.toLowerCase(), + }; + + _onGeoFieldChange = selectedValue => { + this.setState({ geoField: selectedValue }); + } + + _onGeometryNameChange = e => { + this.setState({ + geometryName: e.target.value, + }); + }; + + _createFilter = () => { + + } + + _renderHeader() { + return ( + + ); + } + + _renderForm() { + const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => { + const value = createIndexGeoFieldName({ indexPatternTitle, geoFieldName }); + let inputDisplay; + if (value === this.state.geoField) { + // do not show index name in select box to avoid clipping + inputDisplay = geoFieldName; + } else { + inputDisplay = ( + + + {indexPatternTitle} + +
+ {geoFieldName} +
+ ); + } + return { + inputDisplay, + value + }; + }); + return ( + + + + + + + + + + + + ); + } + + render() { + return ( + + {this._renderHeader()} + {this._renderForm()} + + ); + } + +} + diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index a569f903da1bb8..09e48e15185a8b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -26,7 +26,7 @@ import { DRAW_TYPE } from '../../../actions/map_actions'; import { createGeometryFilterWithMeta, getBoundingBoxGeometry, - setPrecision + roundCoordinates } from '../../../elasticsearch_geo_utils'; import chrome from 'ui/chrome'; import { spritesheet } from '@elastic/maki'; @@ -97,7 +97,7 @@ export class MBMapContainer extends React.Component { const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS; const geometry = e.features[0].geometry; // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number - setPrecision(geometry.coordinates); + roundCoordinates(geometry.coordinates); // TODO allow user to set geojson label when initiating draw const geometryLabel = isBoundingBox diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 575f70ac4e6b1f..b0a91a56e70f79 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -331,10 +331,10 @@ export function createGeometryFilterWithMeta({ }; } -export function setPrecision(coordinates, precision = DECIMAL_DEGREES_PRECISION) { +export function roundCoordinates(coordinates, precision = DECIMAL_DEGREES_PRECISION) { coordinates.forEach((value, index) => { if (Array.isArray(value)) { - setPrecision(value); + roundCoordinates(value); } else if (!isNaN(value)) { coordinates[index] = _.round(value, precision); } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js index fe08be86aa3914..14973a1e86297b 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -12,7 +12,7 @@ import { geoShapeToGeometry, createExtentFilter, convertMapExtentToPolygon, - setPrecision, + roundCoordinates, } from './elasticsearch_geo_utils'; import { flattenHitWrapper } from 'ui/index_patterns'; @@ -441,14 +441,14 @@ describe('convertMapExtentToPolygon', () => { }); }); -describe('setPrecision', () => { +describe('roundCoordinates', () => { it('should set coordinates precision', () => { const coordinates = [ [110.21515290475513, 40.23193047044205], [-105.30620093073654, 40.23193047044205], [-105.30620093073654, 30.647128842617803] ]; - setPrecision(coordinates); + roundCoordinates(coordinates); expect(coordinates).toEqual([ [110.21515, 40.23193], [-105.30620, 40.23193], From 19448ae05dc8615307d5609911c5d6c6ecc9f560 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 11:14:01 -0600 Subject: [PATCH 3/6] i18n cleanup --- x-pack/plugins/translations/translations/ja-JP.json | 8 -------- x-pack/plugins/translations/translations/zh-CN.json | 8 -------- 2 files changed, 16 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 46592b31d0bb2c..a7310b1fed816d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5452,14 +5452,6 @@ "xpack.maps.appTitle": "Maps", "xpack.maps.badge.readOnly.text": "読み込み専用", "xpack.maps.badge.readOnly.tooltip": "マップを保存できませんで", - "xpack.maps.elasticsearch_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "{geometryType} ジオメトリから Geojson に変換できません。サポートされていません", - "xpack.maps.elasticsearch_geo_utils.extent.unsupportedGeoFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値: geo_shape または geo_point、入力値: {geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.extentFilter.aliasTitle": "{coordinate}で拡張", - "xpack.maps.elasticsearch_geo_utils.shape.unsupportedGeoFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値: geo_shape または geo_point、入力値: {geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.shapeFilter.aliasTitle": "{coordinate} で形成", - "xpack.maps.elasticsearch_geo_utils.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値: geo_shape または geo_point、入力値: {geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.unsupportedGeoPointValueErrorMessage": "サポートされていない geo_point 値: {geoPointValue}", - "xpack.maps.elasticsearch_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。", "xpack.maps.esSearch.featureCountMsg": "{count} 件のドキュメントが見つかりました。", "xpack.maps.esSearch.resultsTrimmedMsg": "結果は初めの {count} 件のドキュメントに制限されています。", "xpack.maps.esSearch.topHitsEntitiesCountMsg": "{entityCount} 件のエントリーを発見.", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 41facad426c47d..7d99e2d3ce6d62 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5594,14 +5594,6 @@ "xpack.maps.appTitle": "Maps", "xpack.maps.badge.readOnly.text": "只读", "xpack.maps.badge.readOnly.tooltip": "无法保存地图", - "xpack.maps.elasticsearch_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "无法将 {geometryType} 几何图形转换成 geojson,不支持", - "xpack.maps.elasticsearch_geo_utils.extent.unsupportedGeoFieldTypeErrorMessage": "字段类型不受支持,应为:geo_shape 或 geo_point,而您提供的是:{geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.extentFilter.aliasTitle": "位于 {coordinate} 的范围", - "xpack.maps.elasticsearch_geo_utils.shape.unsupportedGeoFieldTypeErrorMessage": "字段类型不受支持,应为:geo_shape 或 geo_point,而您提供的是:{geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.shapeFilter.aliasTitle": "位于 {coordinate} 的形状", - "xpack.maps.elasticsearch_geo_utils.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为:geo_shape 或 geo_point,而您提供的是:{geoFieldType}", - "xpack.maps.elasticsearch_geo_utils.unsupportedGeoPointValueErrorMessage": "不受支持的 geo_point 值:{geoPointValue}", - "xpack.maps.elasticsearch_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。", "xpack.maps.esSearch.featureCountMsg": "找到 {count} 个文档。", "xpack.maps.esSearch.resultsTrimmedMsg": "结果仅限于前 {count} 个文档。", "xpack.maps.esSearch.topHitsEntitiesCountMsg": "找到 {entityCount} 个实体。", From e0d5f2d615bb4e02d9b63cdaa64909b5efea2e5a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 11:14:58 -0600 Subject: [PATCH 4/6] remove unused file --- .../map/feature_geometry_filter_form.js | 148 ------------------ 1 file changed, 148 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js deleted file mode 100644 index 60505e9f81c8fc..00000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_geometry_filter_form.js +++ /dev/null @@ -1,148 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component, Fragment } from 'react'; -import { - EuiIcon, - EuiForm, - EuiFormRow, - EuiSuperSelect, - EuiTextColor, - EuiText, - EuiFieldText, - EuiButton, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -const GEO_FIELD_VALUE_DELIMITER = '//'; // `/` is not allowed in index pattern name so should not have collisions - -function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) { - return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`; -} - -export class FeatureGeometryFilterForm extends Component { - - // TODO add ability to specify spatial relationship - - state = { - geoField: createIndexGeoFieldName(this.props.geoFields[0]), - geometryName: this.props.feature.geometry.type.toLowerCase(), - }; - - _onGeoFieldChange = selectedValue => { - this.setState({ geoField: selectedValue }); - } - - _onGeometryNameChange = e => { - this.setState({ - geometryName: e.target.value, - }); - }; - - _createFilter = () => { - - } - - _renderHeader() { - return ( - - ); - } - - _renderForm() { - const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => { - const value = createIndexGeoFieldName({ indexPatternTitle, geoFieldName }); - let inputDisplay; - if (value === this.state.geoField) { - // do not show index name in select box to avoid clipping - inputDisplay = geoFieldName; - } else { - inputDisplay = ( - - - {indexPatternTitle} - -
- {geoFieldName} -
- ); - } - return { - inputDisplay, - value - }; - }); - return ( - - - - - - - - - - - - ); - } - - render() { - return ( - - {this._renderHeader()} - {this._renderForm()} - - ); - } - -} - From ed8fe0d3b33e6999e4656d94ec0ebf0f2cbe88b3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 14:49:58 -0600 Subject: [PATCH 5/6] review feedback --- .../public/connected_components/map/mb/view.js | 7 ++++--- .../maps/public/elasticsearch_geo_utils.js | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 09e48e15185a8b..36af3949a4bbfc 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -16,7 +16,8 @@ import { import { DECIMAL_DEGREES_PRECISION, FEATURE_ID_PROPERTY_NAME, - ZOOM_PRECISION + ZOOM_PRECISION, + LON_INDEX } from '../../../../common/constants'; import mapboxgl from 'mapbox-gl'; import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; @@ -241,8 +242,8 @@ export class MBMapContainer extends React.Component { // Ensure that if the map is zoomed out such that multiple // copies of the feature are visible, the popup appears // over the copy being pointed to. - while (Math.abs(mbLngLat.lng - coordinates[0]) > 180) { - coordinates[0] += mbLngLat.lng > coordinates[0] ? 360 : -360; + while (Math.abs(mbLngLat.lng - coordinates[LON_INDEX]) > 180) { + coordinates[0] += mbLngLat.lng > coordinates[LON_INDEX] ? 360 : -360; } popupAnchorLocation = coordinates; diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index b0a91a56e70f79..f7ca45a092ad4b 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -137,8 +137,8 @@ export function geoPointToGeometry(value, accumulator) { if (value.length === 2 && typeof value[0] === 'number' && typeof value[1] === 'number') { - const lat = value[1]; - const lon = value[0]; + const lat = value[LAT_INDEX]; + const lon = value[LON_INDEX]; accumulator.push(pointGeometryFactory(lat, lon)); return; } @@ -331,14 +331,15 @@ export function createGeometryFilterWithMeta({ }; } -export function roundCoordinates(coordinates, precision = DECIMAL_DEGREES_PRECISION) { - coordinates.forEach((value, index) => { +export function roundCoordinates(coordinates) { + for (let i = 0; i < coordinates.length; i++) { + const value = coordinates[i]; if (Array.isArray(value)) { roundCoordinates(value); } else if (!isNaN(value)) { - coordinates[index] = _.round(value, precision); + coordinates[i] = _.round(value, DECIMAL_DEGREES_PRECISION); } - }); + } } /* @@ -354,7 +355,7 @@ export function getBoundingBoxGeometry(geometry) { maxLon: exterior[0][LON_INDEX], maxLat: exterior[0][LAT_INDEX] }; - for (let i = 1; i < exterior.length; i++) { + for (let i = 1; i < exterior.length; i++) { extent.minLon = Math.min(exterior[i][LON_INDEX], extent.minLon); extent.minLat = Math.min(exterior[i][LAT_INDEX], extent.minLat); extent.maxLon = Math.max(exterior[i][LON_INDEX], extent.maxLon); From f9d6ca811dd2d076889ea538bdbdf27e96f32f3e Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 9 Aug 2019 15:29:23 -0600 Subject: [PATCH 6/6] split filter create into two functions --- .../connected_components/map/mb/view.js | 36 ++++++++++--------- .../maps/public/elasticsearch_geo_utils.js | 10 +++++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 36af3949a4bbfc..a583692101f2d5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -25,7 +25,8 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { FeatureTooltip } from '../feature_tooltip'; import { DRAW_TYPE } from '../../../actions/map_actions'; import { - createGeometryFilterWithMeta, + createSpatialFilterWithBoundingBox, + createSpatialFilterWithGeometry, getBoundingBoxGeometry, roundCoordinates } from '../../../elasticsearch_geo_utils'; @@ -100,26 +101,27 @@ export class MBMapContainer extends React.Component { // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number roundCoordinates(geometry.coordinates); - // TODO allow user to set geojson label when initiating draw - const geometryLabel = isBoundingBox - ? i18n.translate('xpack.maps.drawControl.defaultEnvelopeLabel', { - defaultMessage: 'extent' - }) - : i18n.translate('xpack.maps.drawControl.defaultShapeLabel', { - defaultMessage: 'shape' - }); - try { - const filter = createGeometryFilterWithMeta({ - geometry: isBoundingBox - ? getBoundingBoxGeometry(geometry) - : geometry, - geometryLabel, + const options = { indexPatternId: this.props.drawState.indexPatternId, geoFieldName: this.props.drawState.geoField, geoFieldType: this.props.drawState.geoFieldType, - isBoundingBox, - }); + }; + const filter = isBoundingBox + ? createSpatialFilterWithBoundingBox({ + ...options, + geometryLabel: i18n.translate('xpack.maps.drawControl.defaultEnvelopeLabel', { + defaultMessage: 'extent' + }), + geometry: getBoundingBoxGeometry(geometry) + }) + : createSpatialFilterWithGeometry({ + ...options, + geometryLabel: i18n.translate('xpack.maps.drawControl.defaultShapeLabel', { + defaultMessage: 'shape' + }), + geometry + }); this.props.addFilters([filter]); } catch (error) { // TODO notify user why filter was not created diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index f7ca45a092ad4b..34f7ddf7700fed 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -271,7 +271,15 @@ export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) { }; } -export function createGeometryFilterWithMeta({ +export function createSpatialFilterWithBoundingBox(options) { + return createGeometryFilterWithMeta({ ...options, isBoundingBox: true }); +} + +export function createSpatialFilterWithGeometry(options) { + return createGeometryFilterWithMeta(options); +} + +function createGeometryFilterWithMeta({ geometry, geometryLabel, indexPatternId,