import React , { ReactElement } from 'react' ; import { i18n } from '@kbn/i18n' ; import rison from 'rison-node' ; import { Feature } from 'geojson' ; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey' ; import { makeESBbox } from '../../../../common/elasticsearch_util' ; import { convertCompositeRespToGeoJson , convertRegularRespToGeoJson } from './convert_to_geojson' ; import { UpdateSourceEditor } from './update_source_editor' ; import { DEFAULT_MAX_BUCKETS_LIMIT , ES_GEO_FIELD_TYPE , GEOCENTROID_AGG_NAME , GEOTILE_GRID_AGG_NAME , GIS_API_PATH , GRID_RESOLUTION , MVT_GETGRIDTILE_API_PATH , RENDER_AS , SOURCE_TYPES , VECTOR_SHAPE_TYPE , } from '../../../../common/constants' ; import { getDataSourceLabel , getDataViewLabel } from '../../../../common/i18n_getters' ; import { AbstractESAggSource } from '../es_agg_source' ; import { DataRequestAbortError } from '../../util/data_request' ; import { registerSource } from '../source_registry' ; import { LICENSED_FEATURES } from '../../../licensed_features' ; import { getHttp } from '../../../kibana_services' ; import { GeoJsonWithMeta , IMvtVectorSource } from '../vector_source' ; import { ESGeoGridSourceDescriptor , MapExtent , VectorSourceRequestMeta , } from '../../../../common/descriptor_types' ; import { ImmutableSourceProperty , SourceEditorArgs } from '../source' ; import { ISearchSource } from '../../../../../../../src/plugins/data/common/search/search_source' ; import { IndexPattern } from '../../../../../../../src/plugins/data/common' ; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters' ; import { isValidStringConfig } from '../../util/valid_string_config' ; type ESGeoGridSourceSyncMeta = Pick < ESGeoGridSourceDescriptor , 'requestType' | 'resolution' > ; const MAX_GEOTILE_LEVEL = 29 ; export const clustersTitle = i18n . translate ( 'xpack.maps.source.esGridClustersTitle' , { defaultMessage : 'Clusters and grids' , } ) ; export const heatmapTitle = i18n . translate ( 'xpack.maps.source.esGridHeatmapTitle' , { defaultMessage : 'Heat map' , } ) ; export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSource { static createDescriptor ( descriptor ) { const normalizedDescriptor = AbstractESAggSource . createDescriptor ( descriptor ) ; if ( ! isValidStringConfig ( normalizedDescriptor . geoField ) ) { throw new Error ( 'Cannot create an ESGeoGridSourceDescriptor without a geoField' ) ; } return { ... normalizedDescriptor , type : SOURCE_TYPES . ES_GEO_GRID , geoField : normalizedDescriptor . geoField ! , requestType : descriptor . requestType || RENDER_AS . POINT , resolution : descriptor . resolution ? descriptor . resolution : GRID_RESOLUTION . COARSE , } ; } readonly _descriptor : ESGeoGridSourceDescriptor ; constructor ( descriptor , inspectorAdapters ? ) { const sourceDescriptor = ESGeoGridSource . createDescriptor ( descriptor ) ; super ( sourceDescriptor , inspectorAdapters ) ; this . _descriptor = sourceDescriptor ; } renderSourceSettingsEditor ( sourceEditorArgs ) { return ( < UpdateSourceEditor currentLayerType = { sourceEditorArgs . currentLayerType } indexPatternId = { this . getIndexPatternId ( ) } onChange = { sourceEditorArgs . onChange } metrics = { this . _descriptor . metrics } renderAs = { this . _descriptor . requestType } resolution = { this . _descriptor . resolution } / > ) ; } getSyncMeta ( ) { return { requestType : this . _descriptor . requestType , resolution : this . _descriptor . resolution , } ; } async getImmutableProperties ( ) { let indexPatternName = this . getIndexPatternId ( ) ; try { const indexPattern = await this . getIndexPattern ( ) ; indexPatternName = indexPattern . title ; } catch ( error ) { } return [ { label : getDataSourceLabel ( ) , value : this . _descriptor . requestType === RENDER_AS . HEATMAP ? heatmapTitle : clustersTitle , } , { label : getDataViewLabel ( ) , value : indexPatternName , } , { label : i18n . translate ( 'xpack.maps.source.esGrid.geospatialFieldLabel' , { defaultMessage : 'Geospatial field' , } ) , value : this . _descriptor . geoField , } , ] ; } isMvt ( ) { if ( this . _descriptor . requestType === RENDER_AS . HEATMAP ) { return true ; } return this . _descriptor . resolution === GRID_RESOLUTION . SUPER_FINE ; } getFieldNames ( ) { return this . getMetricFields ( ) . map ( ( esAggMetricField ) => esAggMetricField . getName ( ) ) ; } isGeoGridPrecisionAware ( ) { if ( this . isMvt ( ) ) { return false ; } else { return true ; } } showJoinEditor ( ) { return false ; } getGridResolution ( ) { return this . _descriptor . resolution ; } getGeoGridPrecision ( zoom ) { if ( this . _descriptor . resolution === GRID_RESOLUTION . SUPER_FINE ) { return NaN ; } const targetGeotileLevel = Math . ceil ( zoom ) + this . _getGeoGridPrecisionResolutionDelta ( ) ; return Math . min ( targetGeotileLevel , MAX_GEOTILE_LEVEL ) ; } _getGeoGridPrecisionResolutionDelta ( ) { if ( this . _descriptor . resolution === GRID_RESOLUTION . COARSE ) { return 2 ; } if ( this . _descriptor . resolution === GRID_RESOLUTION . FINE ) { return 3 ; } if ( this . _descriptor . resolution === GRID_RESOLUTION . MOST_FINE ) { return 4 ; } if ( this . _descriptor . resolution === GRID_RESOLUTION . SUPER_FINE ) { return 8 ; } throw new Error ( i18n . translate ( 'xpack.maps.source.esGrid.resolutionParamErrorMessage' , { defaultMessage : `Grid resolution param not recognized: {resolution}` , values : { resolution : this . _descriptor . resolution , } , } ) ) ; } async _compositeAggRequest ( { searchSource , searchSessionId , indexPattern , precision , layerName , registerCancelCallback , bucketsPerGrid , isRequestStillActive , bufferedExtent , } ) { const gridsPerRequest = Math . floor ( DEFAULT_MAX_BUCKETS_LIMIT / bucketsPerGrid ) ; const aggs = { compositeSplit : { composite : { size : gridsPerRequest , sources : [ { [ GEOTILE_GRID_AGG_NAME ] : { geotile_grid : { bounds : makeESBbox ( bufferedExtent ) , field : this . _descriptor . geoField , precision , } , } , } , ] , } , aggs : { [ GEOCENTROID_AGG_NAME ] : { geo_centroid : { field : this . _descriptor . geoField , } , } , ... this . getValueAggsDsl ( indexPattern ) , } , } , } ; const features = [ ] ; let requestCount = 0 ; let afterKey = null ; while ( true ) { if ( ! isRequestStillActive ( ) ) { throw new DataRequestAbortError ( ) ; } requestCount ++ ; if ( requestCount > 5 ) { throw new Error ( i18n . translate ( 'xpack.maps.source.esGrid.compositePaginationErrorMessage' , { defaultMessage : `{layerName} is causing too many requests. Reduce "Grid resolution" and/or reduce the number of top term "Metrics".` , values : { layerName } , } ) ) ; } if ( afterKey ) { aggs . compositeSplit . composite . after = afterKey ; } searchSource . setField ( 'aggs' , aggs ) ; const requestId = afterKey ? `${ this . getId ( ) } afterKey ${ afterKey . geoSplit }` : this . getId ( ) ; const esResponse = await this . _runEsQuery ( { requestId , requestName : `${ layerName } (${ requestCount })` , searchSource , registerCancelCallback , requestDescription : i18n . translate ( 'xpack.maps.source.esGrid.compositeInspectorDescription' , { defaultMessage : 'Elasticsearch geo grid aggregation request: {requestId}' , values : { requestId } , } ) , searchSessionId , } ) ; features . push ( ... convertCompositeRespToGeoJson ( esResponse , this . _descriptor . requestType ) ) ; const aggr = esResponse . aggregations ?. compositeSplit as estypes . AggregationsCompositeAggregate ; afterKey = aggr . after_key ; if ( aggr . buckets . length < gridsPerRequest ) { break ; } } return features ; } _addNonCompositeAggsToSearchSource ( searchSource , indexPattern , precision , bufferedExtent ? ) { searchSource . setField ( 'aggs' , { [ GEOTILE_GRID_AGG_NAME ] : { geotile_grid : { bounds : bufferedExtent ? makeESBbox ( bufferedExtent ) : null , field : this . _descriptor . geoField , precision , size : DEFAULT_MAX_BUCKETS_LIMIT , shard_size : DEFAULT_MAX_BUCKETS_LIMIT , } , aggs : { [ GEOCENTROID_AGG_NAME ] : { geo_centroid : { field : this . _descriptor . geoField , } , } , ... this . getValueAggsDsl ( indexPattern ) , } , } , } ) ; } async _nonCompositeAggRequest ( { searchSource , searchSessionId , indexPattern , precision , layerName , registerCancelCallback , bufferedExtent , } ) { this . _addNonCompositeAggsToSearchSource ( searchSource , indexPattern , precision , bufferedExtent ) ; const esResponse = await this . _runEsQuery ( { requestId : this . getId ( ) , requestName : layerName , searchSource , registerCancelCallback , requestDescription : i18n . translate ( 'xpack.maps.source.esGrid.inspectorDescription' , { defaultMessage : 'Elasticsearch geo grid aggregation request' , } ) , searchSessionId , } ) ; return convertRegularRespToGeoJson ( esResponse , this . _descriptor . requestType ) ; } async getGeoJsonWithMeta ( layerName , searchFilters , registerCancelCallback , isRequestStillActive ) { const indexPattern = await this . getIndexPattern ( ) ; const searchSource = await this . makeSearchSource ( searchFilters , 0 ) ; searchSource . setField ( 'trackTotalHits' , false ) ; let bucketsPerGrid = 1 ; this . getMetricFields ( ) . forEach ( ( metricField ) => { bucketsPerGrid += metricField . getBucketCount ( ) ; } ) ; let features ; if ( searchFilters . buffer ) { features = bucketsPerGrid === 1 ? await this . _nonCompositeAggRequest ( { searchSource , searchSessionId : searchFilters . searchSessionId , indexPattern , precision : searchFilters . geogridPrecision || 0 , layerName , registerCancelCallback , bufferedExtent : searchFilters . buffer , } ) : await this . _compositeAggRequest ( { searchSource , searchSessionId : searchFilters . searchSessionId , indexPattern , precision : searchFilters . geogridPrecision || 0 , layerName , registerCancelCallback , bucketsPerGrid , isRequestStillActive , bufferedExtent : searchFilters . buffer , } ) ; } else { throw new Error ( 'Cannot get GeoJson without searchFilter.buffer' ) ; } return { data : { type : 'FeatureCollection' , features , } , meta : { areResultsTrimmed : false , } , } as GeoJsonWithMeta ; } getTileSourceLayer ( ) { return 'aggs' ; } async getTileUrl ( searchFilters , refreshToken ) { const indexPattern = await this . getIndexPattern ( ) ; const searchSource = await this . makeSearchSource ( searchFilters , 0 ) ; searchSource . setField ( 'aggs' , this . getValueAggsDsl ( indexPattern ) ) ; const dsl = searchSource . getSearchRequestBody ( ) ; const risonDsl = rison . encode ( dsl ) ; const mvtUrlServicePath = getHttp ( ) . basePath . prepend ( `/${ GIS_API_PATH }/${ MVT_GETGRIDTILE_API_PATH }/{z}/{x}/{y}.pbf` ) ; const requestType = this . _descriptor . requestType === RENDER_AS . GRID ? RENDER_AS . GRID : RENDER_AS . POINT ; return `${ mvtUrlServicePath }\
?geometryFieldName=${ this . _descriptor . geoField }\
&index=${ indexPattern . title }\
&gridPrecision=${ this . _getGeoGridPrecisionResolutionDelta ( ) }\
&requestBody=${ risonDsl }\
&requestType=${ requestType }\
&token=${ refreshToken }` ; } isFilterByMapBounds ( ) { if ( this . isMvt ( ) ) { return false ; } else { return true ; } } hasTooltipProperties ( ) { return true ; } async getSupportedShapeTypes ( ) { if ( this . _descriptor . requestType === RENDER_AS . GRID ) { return [ VECTOR_SHAPE_TYPE . POLYGON ] ; } return [ VECTOR_SHAPE_TYPE . POINT ] ; } async getLicensedFeatures ( ) { const geoField = await this . _getGeoField ( ) ; return geoField . type === ES_GEO_FIELD_TYPE . GEO_SHAPE ? [ LICENSED_FEATURES . GEO_SHAPE_AGGS_GEO_TILE ] : [ ] ; } } registerSource ( { ConstructorFunction : ESGeoGridSource , type : SOURCE_TYPES . ES_GEO_GRID , } ) ;