import _ from 'lodash' ; import { i18n } from '@kbn/i18n' ; import { parse } from 'wellknown' ; import turfCircle from '@turf/circle' ; import { Feature , FeatureCollection , Geometry , Polygon , Point , Position } from 'geojson' ; import { BBox } from '@turf/helpers' ; import { DECIMAL_DEGREES_PRECISION , ES_GEO_FIELD_TYPE , GEO_JSON_TYPE , POLYGON_COORDINATES_EXTERIOR_INDEX , LON_INDEX , LAT_INDEX , } from '../constants' ; import { MapExtent } from '../descriptor_types' ; import { Coordinates , ESBBox , ESGeometry } from './types' ; function ensureGeoField ( type ) { const expectedTypes = [ ES_GEO_FIELD_TYPE . GEO_POINT , ES_GEO_FIELD_TYPE . GEO_SHAPE ] ; if ( ! expectedTypes . includes ( type as ES_GEO_FIELD_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 as GEO_JSON_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 ) ; } } param { array } hits param { function } flattenHit param { string } geoFieldName param { string } geoFieldType returns { number } export function hitsToGeoJson ( hits , flattenHit , geoFieldName , geoFieldType , epochMillisFields ) { const features = [ ] ; const tmpGeometriesAccumulator = [ ] ; for ( let i = 0 ; i < hits . length ; i ++ ) { const properties = { ... flattenHit ( hits [ i ] ) } ; tmpGeometriesAccumulator . length = 0 ; ensureGeoField ( geoFieldType ) ; if ( geoFieldType === ES_GEO_FIELD_TYPE . GEO_POINT ) { geoPointToGeometry ( properties [ geoFieldName ] as string | string [ ] | undefined , tmpGeometriesAccumulator ) ; } else { geoShapeToGeometry ( properties [ geoFieldName ] as string | string [ ] | ESGeometry | ESGeometry [ ] | undefined , tmpGeometriesAccumulator ) ; } for ( let k = 0 ; k < epochMillisFields . length ; k ++ ) { const fieldName = epochMillisFields [ k ] ; if ( typeof properties [ fieldName ] === 'string' ) { properties [ fieldName ] = parseInt ( properties [ fieldName ] as string , 10 ) ; } } delete properties [ geoFieldName ] ; for ( let j = 0 ; j < tmpGeometriesAccumulator . length ; j ++ ) { features . push ( { type : 'Feature' , geometry : tmpGeometriesAccumulator [ j ] , id : `${ properties . _index }:${ properties . _id }:${ j }` , properties , } ) ; } } return { type : 'FeatureCollection' , features , } ; } export function geoPointToGeometry ( value , accumulator ) { if ( ! value ) { return ; } if ( Array . isArray ( value ) ) { for ( let i = 0 ; i < value . length ; i ++ ) { geoPointToGeometry ( value [ i ] , accumulator ) ; } return ; } const commaSplit = value . split ( ',' ) ; const lat = parseFloat ( commaSplit [ 0 ] ) ; const lon = parseFloat ( commaSplit [ 1 ] ) ; accumulator . push ( { type : GEO_JSON_TYPE . POINT , coordinates : [ lon , lat ] , } as Point ) ; } export function convertESShapeToGeojsonGeometry ( value ) { const geoJson = { type : value . type , coordinates : value . coordinates , } ; switch ( value . type ) { case 'point' : geoJson . type = GEO_JSON_TYPE . POINT ; break ; case 'linestring' : geoJson . type = GEO_JSON_TYPE . LINE_STRING ; break ; case 'polygon' : geoJson . type = GEO_JSON_TYPE . POLYGON ; break ; case 'multipoint' : geoJson . type = GEO_JSON_TYPE . MULTI_POINT ; break ; case 'multilinestring' : geoJson . type = GEO_JSON_TYPE . MULTI_LINE_STRING ; break ; case 'multipolygon' : geoJson . type = GEO_JSON_TYPE . MULTI_POLYGON ; break ; case 'geometrycollection' : case GEO_JSON_TYPE . GEOMETRY_COLLECTION : const invalidGeometrycollectionError = i18n . translate ( 'xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage' , { defaultMessage : `Should not pass GeometryCollection to convertESShapeToGeojsonGeometry` , } ) ; throw new Error ( invalidGeometrycollectionError ) ; case 'envelope' : const envelopeCoords = geoJson . coordinates as Position [ ] ; const polygon = formatEnvelopeAsPolygon ( { minLon : envelopeCoords [ 0 ] [ 0 ] , maxLon : envelopeCoords [ 1 ] [ 0 ] , minLat : envelopeCoords [ 1 ] [ 1 ] , maxLat : envelopeCoords [ 0 ] [ 1 ] , } ) ; geoJson . type = polygon . type ; geoJson . coordinates = polygon . coordinates ; break ; case 'circle' : 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 , } , } ) ; throw new Error ( errorMessage ) ; } return geoJson as unknown as Geometry ; } function convertWKTStringToGeojson ( value ) { try { return parse ( value ) ; } catch ( e ) { const errorMessage = i18n . translate ( 'xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage' , { defaultMessage : `Unable to convert {wkt} to geojson. Valid WKT expected.` , values : { wkt : value , } , } ) ; throw new Error ( errorMessage ) ; } } export function geoShapeToGeometry ( value , accumulator ) { if ( ! value ) { return ; } if ( Array . isArray ( value ) ) { for ( let i = 0 ; i < value . length ; i ++ ) { geoShapeToGeometry ( value [ i ] , accumulator ) ; } return ; } if ( typeof value === 'string' ) { const geoJson = convertWKTStringToGeojson ( value ) ; accumulator . push ( geoJson ) ; } else if ( value . type === GEO_JSON_TYPE . GEOMETRY_COLLECTION || value . type === 'geometrycollection' ) { const geometryCollection = value as unknown as { geometries : ESGeometry [ ] ; } ; for ( let i = 0 ; i < geometryCollection . geometries . length ; i ++ ) { geoShapeToGeometry ( geometryCollection . geometries [ i ] , accumulator ) ; } } else { const geoJson = convertESShapeToGeojsonGeometry ( value ) ; accumulator . push ( geoJson ) ; } } export function makeESBbox ( { maxLat , maxLon , minLat , minLon } ) { const bottom = clampToLatBounds ( minLat ) ; const top = clampToLatBounds ( maxLat ) ; let esBbox ; if ( maxLon - minLon >= 360 ) { esBbox = { top_left : [ - 180 , top ] , bottom_right : [ 180 , bottom ] , } ; } else { const newMinlon = ( ( minLon + 180 + 360 ) % 360 ) - 180 ; const newMaxlon = ( ( maxLon + 180 + 360 ) % 360 ) - 180 ; esBbox = { top_left : [ newMinlon , top ] , bottom_right : [ newMaxlon , bottom ] , } ; } return esBbox ; } 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 [ i ] = _ . round ( value , DECIMAL_DEGREES_PRECISION ) ; } } } export function getBoundingBoxGeometry ( geometry ) { ensureGeometryType ( geometry . type , [ GEO_JSON_TYPE . POLYGON ] ) ; const exterior = ( geometry as Polygon ) . 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 formatEnvelopeAsPolygon ( extent ) ; } export function formatEnvelopeAsPolygon ( { maxLat , maxLon , minLat , minLon } ) { const lonDelta = maxLon - minLon ; const left = lonDelta > 360 ? - 180 : minLon ; const right = lonDelta > 360 ? 180 : maxLon ; const top = clampToLatBounds ( maxLat ) ; const bottom = clampToLatBounds ( minLat ) ; const topLeft = [ left , top ] as Position ; const bottomLeft = [ left , bottom ] as Position ; const bottomRight = [ right , bottom ] as Position ; const topRight = [ right , top ] as Position ; return { type : GEO_JSON_TYPE . POLYGON , coordinates : [ [ topLeft , bottomLeft , bottomRight , topRight , topLeft ] ] , } as Polygon ; } export function clampToLatBounds ( lat ) { return clamp ( lat , - 89 , 89 ) ; } export function clampToLonBounds ( lon ) { return clamp ( lon , - 180 , 180 ) ; } export function clamp ( val , min , max ) { if ( val > max ) { return max ; } else if ( val < min ) { return min ; } else { return val ; } } export function scaleBounds ( bounds , scaleFactor ) { const width = bounds . maxLon - bounds . minLon ; const height = bounds . maxLat - bounds . minLat ; const newMinLon = bounds . minLon - width * scaleFactor ; const nexMaxLon = bounds . maxLon + width * scaleFactor ; const lonDelta = nexMaxLon - newMinLon ; const left = lonDelta > 360 ? - 180 : newMinLon ; const right = lonDelta > 360 ? 180 : nexMaxLon ; return { minLon : left , minLat : clampToLatBounds ( bounds . minLat - height * scaleFactor ) , maxLon : right , maxLat : clampToLonBounds ( bounds . maxLat + height * scaleFactor ) , } ; } export function turfBboxToBounds ( turfBbox ) { return { minLon : turfBbox [ 0 ] , minLat : turfBbox [ 1 ] , maxLon : turfBbox [ 2 ] , maxLat : turfBbox [ 3 ] , } ; }