[Maps] Add support for geohex_grid aggregation (#127170)

* [Maps] hex bin gridding

* remove console.log

* disable hexbins for license and geo_shape

* fix jest tests

* copy cleanup

* label

* update clusters SVG with hexbins

* show as tooltip

* documenation updates

* copy updates

* add API test for hex

* test cleanup

* eslint

* eslint and functional test fixes

* eslint, copy updates, and more doc updates

* fix i18n error

* consolidate isMvt logic

* copy review feedback

* use 3 stop scale for hexs

* jest snapshot updates

* Update x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx

Co-authored-by: Nick Peihl <nickpeihl@gmail.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Nick Peihl <nickpeihl@gmail.com>
This commit is contained in:
Nathan Reese 2022-03-23 08:20:10 -06:00 committed by GitHub
parent 7c31c87496
commit 75f8ac424e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 751 additions and 309 deletions

View file

@ -42,22 +42,24 @@ image::maps/images/grid_to_docs.gif[]
[role="xpack"]
[[maps-grid-aggregation]]
=== Grid aggregation
=== Clusters
Grid aggregation layers use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell.
Clusters use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] or {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell.
Symbolize grid aggregation metrics as:
Symbolize cluster metrics as:
*Clusters*:: Creates a <<vector-layer, vector layer>> with a cluster symbol for each gridded cell.
*Clusters*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<vector-layer, vector layer>> with a cluster symbol for each gridded cell.
The cluster location is the weighted centroid for all documents in the gridded cell.
*Grid rectangles*:: Creates a <<vector-layer, vector layer>> with a bounding box polygon for each gridded cell.
*Grids*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<vector-layer, vector layer>> with a bounding box polygon for each gridded cell.
*Heat map*:: Creates a <<heatmap-layer, heat map layer>> that clusters the weighted centroids for each gridded cell.
*Heat map*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<heatmap-layer, heat map layer>> that clusters the weighted centroids for each gridded cell.
To enable a grid aggregation layer:
*Hexbins*:: Uses {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into H3 hexagon grids. Creates a <<vector-layer, vector layer>> with a hexagon polygon for each gridded cell.
. Click *Add layer*, then select the *Clusters and grids* or *Heat map* layer.
To enable a clusters layer:
. Click *Add layer*, then select the *Clusters* or *Heat map* layer.
To enable a blended layer that dynamically shows clusters or documents:

View file

@ -128,7 +128,7 @@ traffic. Larger circles will symbolize grids with
more total bytes transferred, and smaller circles will symbolize
grids with less bytes transferred.
. Click **Add layer**, and select **Clusters and grids**.
. Click **Add layer**, and select **Clusters**.
. Set **Data view** to **kibana_sample_data_logs**.
. Click **Add layer**.
. In **Layer settings**, set:

View file

@ -11,7 +11,7 @@ To add a vector layer to your map, click *Add layer*, then select one of the fol
*Choropleth*:: Shaded areas to compare statistics across boundaries.
*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell.
*Clusters*:: Geospatial data grouped in grids with metrics for each gridded cell.
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape].
*Create index*:: Draw shapes on the map and index in Elasticsearch.

View file

@ -164,6 +164,7 @@ export enum RENDER_AS {
HEATMAP = 'heatmap',
POINT = 'point',
GRID = 'grid',
HEX = 'hex',
}
export enum GRID_RESOLUTION {

View file

@ -27,6 +27,7 @@
"presentationUtil"
],
"optionalPlugins": [
"cloud",
"customIntegrations",
"home",
"savedObjectsTagging",

View file

@ -12,27 +12,24 @@ export const ClustersLayerIcon: FunctionComponent = () => (
xmlns="http://www.w3.org/2000/svg"
width="49"
height="25"
fill="none"
viewBox="0 0 49 25"
className="mapLayersWizardIcon"
>
<circle cx="17.867" cy="5.032" r="2.045" className="mapLayersWizardIcon__highlight" />
<circle cx="13.367" cy="11.717" r="2.045" className="mapLayersWizardIcon__highlight" />
<circle cx="14.594" cy="19.901" r="3.273" className="mapLayersWizardIcon__highlight" />
<circle cx="4.776" cy="18.398" r="2.662" className="mapLayersWizardIcon__highlight" />
<circle cx="6.619" cy="5.649" r="2.662" className="mapLayersWizardIcon__highlight" />
<path className="mapLayersWizardIcon__highlight" d="M31.114 7.454h5.455v5.455h-5.455z" />
<path fill="#98A2B3" d="M31.114 12.909h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M42.023 12.909h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M42.023 12.909h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M42.023 12.909h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M42.023 12.909h5.455v5.455h-5.455zm-5.454-5.455h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M36.569 7.454h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M36.569 7.454h5.455v5.455h-5.455z" />
<path fill="#69707D" d="M36.569 7.454h5.455v5.455h-5.455z" />
<path fill="#98A2B3" d="M42.023 7.454h5.455v5.455h-5.455z" />
<path fill="#98A2B3" d="M36.569 1.999h5.455v5.455h-5.455z" />
<path className="mapLayersWizardIcon__highlight" d="M36.569 12.909h5.455v5.455h-5.455z" />
<path fill="#D3DAE6" d="M36.569 18.363h5.455v5.455h-5.455z" />
<path
fill="#69707D"
d="M37.864 5.114v2.84l2.227 1.137 2.227-1.136V5.114l-2.227-1.137-2.227 1.137zM22.273 5.682h4.454v4.545h-4.454V5.682zM22.273 10.227v4.546h-4.455v-4.546h4.455zM31.182 14.773h-4.455v4.545h4.455v-4.545zM44.545 17.045v2.841l2.228 1.137L49 19.886v-2.84l-2.227-1.137-2.228 1.136zM37.864 15.91v-2.842l2.227-1.136 2.227 1.136v2.841l-2.227 1.136-2.227-1.136z"
/>
<path
className="mapLayersWizardIcon__highlight"
d="M11.693 3.41c.923 0 1.67-.764 1.67-1.705 0-.942-.747-1.705-1.67-1.705-.922 0-1.67.763-1.67 1.705 0 .94.748 1.704 1.67 1.704zM3.898 6.818c1.537 0 2.784-1.272 2.784-2.84 0-1.57-1.247-2.842-2.784-2.842-1.538 0-2.784 1.272-2.784 2.841 0 1.57 1.246 2.841 2.784 2.841zM9.466 13.068c1.23 0 2.227-1.017 2.227-2.272 0-1.256-.997-2.273-2.227-2.273S7.239 9.54 7.239 10.796c0 1.255.997 2.272 2.227 2.272zM2.227 18.182c1.23 0 2.228-1.018 2.228-2.273 0-1.255-.998-2.273-2.228-2.273C.997 13.636 0 14.654 0 15.91c0 1.255.997 2.273 2.227 2.273zM10.023 25c1.845 0 3.34-1.526 3.34-3.41 0-1.882-1.495-3.408-3.34-3.408-1.845 0-3.341 1.526-3.341 3.409 0 1.883 1.496 3.409 3.34 3.409zM17.818 5.682h4.455v4.545h-4.455V5.682zM26.727 10.227h4.455v4.546h-4.455v-4.546zM26.727 14.773v4.545h-4.454v-4.545h4.454zM40.09 17.046v2.84l2.228 1.137 2.227-1.137v-2.84l-2.227-1.137-2.227 1.136zM35.636 11.932V9.09l2.228-1.136L40.09 9.09v2.84l-2.227 1.137-2.228-1.136zM42.318 5.114v2.84l2.227 1.137 2.228-1.136V5.114l-2.228-1.137-2.227 1.137z"
/>
<path
fill="#98A2B3"
d="M40.09 1.136v2.841l2.228 1.137 2.227-1.137v-2.84L42.318 0l-2.227 1.136zM26.727 1.136h-4.454v4.546h4.454v4.545h-4.454v4.546h-4.455v4.545h4.455v-4.545h4.454v-4.546h4.455V5.682h-4.455V1.136zM40.09 9.09v2.842l2.228 1.136 2.227-1.136V9.09l-2.227-1.136-2.227 1.136zM37.864 23.864v-2.841l2.227-1.137 2.227 1.137v2.84L40.091 25l-2.227-1.136z"
/>
<path
fill="#D3DAE6"
d="M44.545 3.977v-2.84L46.773 0 49 1.136v2.841l-2.227 1.137-2.228-1.137zM42.318 13.068v2.841l2.227 1.136 2.228-1.136v-2.84l-2.228-1.137-2.227 1.136zM26.727 19.318h-4.454v4.546h4.454v-4.546zM42.318 21.023v2.84L44.545 25l2.228-1.136v-2.841l-2.228-1.137-2.227 1.137z"
/>
</svg>
);

View file

@ -1,6 +1,158 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
exports[`should render 3 tick slider when renderAs is HEX 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Resolution"
labelType="label"
>
<EuiRange
compressed={true}
fullWidth={false}
isLoading={false}
levels={Array []}
max={3}
min={1}
onChange={[Function]}
showInput={false}
showLabels={false}
showRange={false}
showTicks={true}
showValue={false}
step={1}
tickInterval={1}
ticks={
Array [
Object {
"label": "low",
"value": 1,
},
Object {
"label": "",
"value": 2,
},
Object {
"label": "high",
"value": 3,
},
]
}
value={1}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render 4 tick slider when renderAs is GRID 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Resolution"
labelType="label"
>
<EuiRange
compressed={true}
fullWidth={false}
isLoading={false}
levels={Array []}
max={4}
min={1}
onChange={[Function]}
showInput={false}
showLabels={false}
showRange={false}
showTicks={true}
showValue={false}
step={1}
tickInterval={1}
ticks={
Array [
Object {
"label": "low",
"value": 1,
},
Object {
"label": "",
"value": 2,
},
Object {
"label": "",
"value": 3,
},
Object {
"label": "high",
"value": 4,
},
]
}
value={1}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render 4 tick slider when renderAs is HEATMAP 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Resolution"
labelType="label"
>
<EuiRange
compressed={true}
fullWidth={false}
isLoading={false}
levels={Array []}
max={4}
min={1}
onChange={[Function]}
showInput={false}
showLabels={false}
showRange={false}
showTicks={true}
showValue={false}
step={1}
tickInterval={1}
ticks={
Array [
Object {
"label": "low",
"value": 1,
},
Object {
"label": "",
"value": 2,
},
Object {
"label": "",
"value": 3,
},
Object {
"label": "high",
"value": 4,
},
]
}
value={1}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render 4 tick slider when renderAs is POINT 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}

View file

@ -41,9 +41,9 @@ exports[`source editor geo_grid_source should not allow editing multiple metrics
size="m"
/>
<ResolutionEditor
isHeatmap={true}
metrics={Array []}
onChange={[Function]}
renderAs="point"
resolution="COARSE"
/>
<RenderAsSelect
@ -92,16 +92,16 @@ exports[`source editor geo_grid_source should render editor 1`] = `
size="xs"
>
<h6>
Clusters and grids
Clusters
</h6>
</EuiTitle>
<EuiSpacer
size="m"
/>
<ResolutionEditor
isHeatmap={false}
metrics={Array []}
onChange={[Function]}
renderAs="point"
resolution="COARSE"
/>
<RenderAsSelect

View file

@ -11,7 +11,7 @@ import React from 'react';
import { CreateSourceEditor } from './create_source_editor';
import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source';
import { LayerWizard, RenderWizardArguments } from '../../layers';
import { GeoJsonVectorLayer } from '../../layers/vector_layer';
import { GeoJsonVectorLayer, MvtVectorLayer } from '../../layers/vector_layer';
import {
ESGeoGridSourceDescriptor,
ColorDynamicOptions,
@ -38,7 +38,8 @@ export const clustersLayerWizardConfig: LayerWizard = {
order: 10,
categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH],
description: i18n.translate('xpack.maps.source.esGridClustersDescription', {
defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell',
defaultMessage:
'Group Elasticsearch documents into grids and hexagons and display metrics for each group',
}),
icon: ClustersLayerIcon,
renderWizard: ({ previewLayers }: RenderWizardArguments) => {
@ -48,62 +49,71 @@ export const clustersLayerWizardConfig: LayerWizard = {
return;
}
const defaultDynamicProperties = getDefaultDynamicProperties();
const layerDescriptor = GeoJsonVectorLayer.createDescriptor({
sourceDescriptor: ESGeoGridSource.createDescriptor({
...sourceConfig,
resolution: GRID_RESOLUTION.FINE,
}),
style: VectorStyle.createDescriptor({
// @ts-ignore
[VECTOR_STYLES.FILL_COLOR]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!
.options as ColorDynamicOptions),
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
color: NUMERICAL_COLOR_PALETTES[0].value,
type: COLOR_MAP_TYPE.ORDINAL,
},
},
[VECTOR_STYLES.LINE_COLOR]: {
type: STYLE_TYPE.STATIC,
options: {
color: '#FFF',
},
},
[VECTOR_STYLES.LINE_WIDTH]: {
type: STYLE_TYPE.STATIC,
options: {
size: 0,
},
},
[VECTOR_STYLES.ICON_SIZE]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions),
maxSize: 24,
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
},
},
[VECTOR_STYLES.LABEL_TEXT]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options,
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
},
},
}),
const sourceDescriptor = ESGeoGridSource.createDescriptor({
...sourceConfig,
resolution: GRID_RESOLUTION.FINE,
});
const defaultDynamicProperties = getDefaultDynamicProperties();
const style = VectorStyle.createDescriptor({
// @ts-ignore
[VECTOR_STYLES.FILL_COLOR]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions),
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
color: NUMERICAL_COLOR_PALETTES[0].value,
type: COLOR_MAP_TYPE.ORDINAL,
},
},
[VECTOR_STYLES.LINE_COLOR]: {
type: STYLE_TYPE.STATIC,
options: {
color: '#FFF',
},
},
[VECTOR_STYLES.LINE_WIDTH]: {
type: STYLE_TYPE.STATIC,
options: {
size: 0,
},
},
[VECTOR_STYLES.ICON_SIZE]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions),
maxSize: 24,
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
},
},
[VECTOR_STYLES.LABEL_TEXT]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options,
field: {
name: COUNT_PROP_NAME,
origin: FIELD_ORIGIN.SOURCE,
},
},
},
});
const layerDescriptor =
sourceDescriptor.requestType === RENDER_AS.HEX
? MvtVectorLayer.createDescriptor({
sourceDescriptor,
style,
})
: GeoJsonVectorLayer.createDescriptor({
sourceDescriptor,
style,
});
previewLayers([layerDescriptor]);
};

View file

@ -51,10 +51,15 @@ export class CreateSourceEditor extends Component {
);
};
_onGeoFieldSelect = (geoField) => {
_onGeoFieldSelect = (geoFieldName) => {
const geoField =
this.state.indexPattern && geoFieldName
? this.state.indexPattern.fields.getByName(geoFieldName)
: undefined;
this.setState(
{
geoField,
geoField: geoFieldName,
geoFieldType: geoField ? geoField.type : undefined,
},
this.previewLayer
);
@ -85,7 +90,7 @@ export class CreateSourceEditor extends Component {
return (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esGeoGrid.geofieldLabel', {
defaultMessage: 'Geospatial field',
defaultMessage: 'Cluster field',
})}
>
<SingleFieldSelect
@ -110,7 +115,11 @@ export class CreateSourceEditor extends Component {
}
return (
<RenderAsSelect renderAs={this.state.requestType} onChange={this._onRequestTypeSelect} />
<RenderAsSelect
geoFieldType={this.state.geoFieldType}
renderAs={this.state.requestType}
onChange={this._onRequestTypeSelect}
/>
);
}

View file

@ -316,7 +316,7 @@ describe('ESGeoGridSource', () => {
const tileUrl = await mvtGeogridSource.getTileUrl(vectorSourceRequestMeta, '1234');
expect(tileUrl).toEqual(
"rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&requestType=point&token=1234"
"rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'1'%3A('0'%3Asize%2C'1'%3A0)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A''))%2C'6'%3A('0'%3Aaggs%2C'1'%3A())))&renderAs=heatmap&token=1234"
);
});
});

View file

@ -45,13 +45,14 @@ import { DataView } from '../../../../../../../src/plugins/data/common';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { isValidStringConfig } from '../../util/valid_string_config';
import { makePublicExecutionContext } from '../../../util';
import { isMvt } from './is_mvt';
type ESGeoGridSourceSyncMeta = Pick<ESGeoGridSourceDescriptor, 'requestType' | 'resolution'>;
const MAX_GEOTILE_LEVEL = 29;
export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', {
defaultMessage: 'Clusters and grids',
defaultMessage: 'Clusters',
});
export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', {
@ -87,6 +88,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
return (
<UpdateSourceEditor
currentLayerType={sourceEditorArgs.currentLayerType}
geoFieldName={this.getGeoFieldName()}
indexPatternId={this.getIndexPatternId()}
onChange={sourceEditorArgs.onChange}
metrics={this._descriptor.metrics}
@ -123,19 +125,15 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
},
{
label: i18n.translate('xpack.maps.source.esGrid.geospatialFieldLabel', {
defaultMessage: 'Geospatial field',
defaultMessage: 'Cluster field',
}),
value: this._descriptor.geoField,
},
];
}
isMvt() {
// heatmap uses MVT regardless of resolution because heatmap only supports counting metrics
if (this._descriptor.requestType === RENDER_AS.HEATMAP) {
return true;
}
return this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE;
isMvt(): boolean {
return isMvt(this._descriptor.requestType, this._descriptor.resolution);
}
getFieldNames() {
@ -172,6 +170,27 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
}
_getGeoGridPrecisionResolutionDelta() {
// Hexagon resolutions do not scale evenly to zoom levels.
// zoomX and zoomX + 1 may result in the same hexagon resolution.
// To avoid FINE and MOST_FINE providing potenitally the same resolution,
// use 3 level resolution system that increases zoom + 3 per resolution step.
if (this._descriptor.requestType === RENDER_AS.HEX) {
if (this._descriptor.resolution === GRID_RESOLUTION.COARSE) {
return 2;
}
if (
this._descriptor.resolution === GRID_RESOLUTION.FINE ||
this._descriptor.resolution === GRID_RESOLUTION.MOST_FINE
) {
return 5;
}
if (this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE) {
return 8;
}
}
if (this._descriptor.resolution === GRID_RESOLUTION.COARSE) {
return 2;
}
@ -452,15 +471,12 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
`/${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=${encodeMvtResponseBody(searchSource.getSearchRequestBody())}\
&requestType=${requestType}\
&renderAs=${this._descriptor.requestType}\
&token=${refreshToken}`;
}
@ -479,7 +495,10 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
}
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]> {
if (this._descriptor.requestType === RENDER_AS.GRID) {
if (
this._descriptor.requestType === RENDER_AS.GRID ||
this._descriptor.requestType === RENDER_AS.HEX
) {
return [VECTOR_SHAPE_TYPE.POLYGON];
}

View file

@ -0,0 +1,23 @@
/*
* 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 { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants';
export function isMvt(renderAs: RENDER_AS, resolution: GRID_RESOLUTION): boolean {
// heatmap uses MVT regardless of resolution because heatmap only supports counting metrics
if (renderAs === RENDER_AS.HEATMAP) {
return true;
}
// hex uses MVT regardless of resolution because hex never supported "top terms" metric
if (renderAs === RENDER_AS.HEX) {
return true;
}
// point and grid only use mvt at high resolution because lower resolutions may contain mvt unsupported "top terms" metric
return resolution === GRID_RESOLUTION.SUPER_FINE;
}

View file

@ -1,68 +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, EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RENDER_AS } from '../../../../common/constants';
const options = [
{
id: RENDER_AS.POINT,
label: i18n.translate('xpack.maps.source.esGeoGrid.pointsDropdownOption', {
defaultMessage: 'clusters',
}),
value: RENDER_AS.POINT,
},
{
id: RENDER_AS.GRID,
label: i18n.translate('xpack.maps.source.esGeoGrid.gridRectangleDropdownOption', {
defaultMessage: 'grids',
}),
value: RENDER_AS.GRID,
},
];
export function RenderAsSelect(props: {
renderAs: RENDER_AS;
onChange: (newValue: RENDER_AS) => void;
isColumnCompressed?: boolean;
}) {
const currentOption = options.find((option) => option.value === props.renderAs) || options[0];
if (props.renderAs === RENDER_AS.HEATMAP) {
return null;
}
function onChange(id: string) {
const data = options.find((option) => option.id === id);
if (data) {
props.onChange(data.value as RENDER_AS);
}
}
return (
<EuiFormRow
label={i18n.translate('xpack.maps.source.esGeoGrid.showAsLabel', {
defaultMessage: 'Show as',
})}
display={props.isColumnCompressed ? 'columnCompressed' : 'row'}
>
<EuiButtonGroup
type="single"
legend={i18n.translate('xpack.maps.source.esGeoGrid.showAsSelector', {
defaultMessage: 'Choose the display method',
})}
options={options}
idSelected={currentOption.id}
onChange={onChange}
isFullWidth={true}
buttonSize="compressed"
/>
</EuiFormRow>
);
}

View file

@ -0,0 +1,23 @@
/*
* 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';
export const CLUSTER_LABEL = i18n.translate('xpack.maps.source.esGeoGrid.pointsDropdownOption', {
defaultMessage: 'Clusters',
});
export const GRID_LABEL = i18n.translate(
'xpack.maps.source.esGeoGrid.gridRectangleDropdownOption',
{
defaultMessage: 'Grids',
}
);
export const HEX_LABEL = i18n.translate('xpack.maps.source.esGeoGrid.hexDropdownOption', {
defaultMessage: 'Hexagons',
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { RenderAsSelect } from './render_as_select';

View file

@ -0,0 +1,92 @@
/*
* 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, EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ES_GEO_FIELD_TYPE, RENDER_AS } from '../../../../../common/constants';
import { getIsCloud } from '../../../../kibana_services';
import { getIsGoldPlus } from '../../../../licensed_features';
import { CLUSTER_LABEL, GRID_LABEL, HEX_LABEL } from './i18n_constants';
import { ShowAsLabel } from './show_as_label';
interface Props {
geoFieldType?: ES_GEO_FIELD_TYPE;
renderAs: RENDER_AS;
onChange: (newValue: RENDER_AS) => void;
isColumnCompressed?: boolean;
}
export function RenderAsSelect(props: Props) {
if (props.renderAs === RENDER_AS.HEATMAP) {
return null;
}
let isHexDisabled = false;
let hexDisabledReason = '';
if (!getIsCloud() && !getIsGoldPlus()) {
isHexDisabled = true;
hexDisabledReason = i18n.translate('xpack.maps.hexbin.license.disabledReason', {
defaultMessage: '{hexLabel} is a subscription feature.',
values: { hexLabel: HEX_LABEL },
});
} else if (props.geoFieldType !== ES_GEO_FIELD_TYPE.GEO_POINT) {
isHexDisabled = true;
hexDisabledReason = i18n.translate('xpack.maps.hexbin.geoShape.disabledReason', {
defaultMessage: `{hexLabel} requires a 'geo_point' cluster field.`,
values: { hexLabel: HEX_LABEL },
});
}
const options = [
{
id: RENDER_AS.POINT,
label: CLUSTER_LABEL,
value: RENDER_AS.POINT,
},
{
id: RENDER_AS.GRID,
label: GRID_LABEL,
value: RENDER_AS.GRID,
},
{
id: RENDER_AS.HEX,
label: HEX_LABEL,
value: RENDER_AS.HEX,
isDisabled: isHexDisabled,
},
];
function onChange(id: string) {
const data = options.find((option) => option.id === id);
if (data) {
props.onChange(data.value as RENDER_AS);
}
}
const currentOption = options.find((option) => option.value === props.renderAs) || options[0];
const selectLabel = (
<ShowAsLabel isHexDisabled={isHexDisabled} hexDisabledReason={hexDisabledReason} />
);
return (
<EuiFormRow label={selectLabel} display={props.isColumnCompressed ? 'columnCompressed' : 'row'}>
<EuiButtonGroup
type="single"
legend={i18n.translate('xpack.maps.source.esGeoGrid.showAsSelector', {
defaultMessage: 'Choose the display method',
})}
options={options}
idSelected={currentOption.id}
onChange={onChange}
isFullWidth={true}
buttonSize="compressed"
/>
</EuiFormRow>
);
}

View file

@ -0,0 +1,64 @@
/*
* 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 { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CLUSTER_LABEL, GRID_LABEL, HEX_LABEL } from './i18n_constants';
interface Props {
isHexDisabled: boolean;
hexDisabledReason: string;
}
export function ShowAsLabel(props: Props) {
return (
<EuiToolTip
content={
<EuiText>
<dl>
<dt>{CLUSTER_LABEL}</dt>
<dd>
<p>
<FormattedMessage
id="xpack.maps.source.esGeoGrid.clusterDescription"
defaultMessage="Group documents into grids with a weighted center for each grid cell."
/>
</p>
</dd>
<dt>{GRID_LABEL}</dt>
<dd>
<p>
<FormattedMessage
id="xpack.maps.source.esGeoGrid.gridDescription"
defaultMessage="Group documents into grids."
/>
</p>
</dd>
<dt>{HEX_LABEL}</dt>
<dd>
<p>
<FormattedMessage
id="xpack.maps.source.esGeoGrid.hexDescription"
defaultMessage="Group documents into hexagons."
/>
</p>
{props.isHexDisabled ? <em>{props.hexDisabledReason}</em> : null}
</dd>
</dl>
</EuiText>
}
>
<span>
<FormattedMessage id="xpack.maps.source.esGeoGrid.showAsLabel" defaultMessage="Show as" />{' '}
<EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
);
}

View file

@ -9,16 +9,30 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ResolutionEditor } from './resolution_editor';
import { GRID_RESOLUTION } from '../../../../common/constants';
import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants';
const defaultProps = {
isHeatmap: false,
resolution: GRID_RESOLUTION.COARSE,
onChange: () => {},
metrics: [],
};
test('render', () => {
const component = shallow(<ResolutionEditor {...defaultProps} />);
test('should render 4 tick slider when renderAs is POINT', () => {
const component = shallow(<ResolutionEditor renderAs={RENDER_AS.POINT} {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should render 4 tick slider when renderAs is GRID', () => {
const component = shallow(<ResolutionEditor renderAs={RENDER_AS.GRID} {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should render 4 tick slider when renderAs is HEATMAP', () => {
const component = shallow(<ResolutionEditor renderAs={RENDER_AS.HEATMAP} {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should render 3 tick slider when renderAs is HEX', () => {
const component = shallow(<ResolutionEditor renderAs={RENDER_AS.HEX} {...defaultProps} />);
expect(component).toMatchSnapshot();
});

View file

@ -10,46 +10,15 @@ import { EuiConfirmModal, EuiFormRow, EuiRange } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { AggDescriptor } from '../../../../common/descriptor_types';
import { AGG_TYPE, GRID_RESOLUTION } from '../../../../common/constants';
function resolutionToSliderValue(resolution: GRID_RESOLUTION) {
if (resolution === GRID_RESOLUTION.SUPER_FINE) {
return 4;
}
if (resolution === GRID_RESOLUTION.MOST_FINE) {
return 3;
}
if (resolution === GRID_RESOLUTION.FINE) {
return 2;
}
return 1;
}
function sliderValueToResolution(value: number) {
if (value === 4) {
return GRID_RESOLUTION.SUPER_FINE;
}
if (value === 3) {
return GRID_RESOLUTION.MOST_FINE;
}
if (value === 2) {
return GRID_RESOLUTION.FINE;
}
return GRID_RESOLUTION.COARSE;
}
import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants';
import { isMvt } from './is_mvt';
function isUnsupportedVectorTileMetric(metric: AggDescriptor) {
return metric.type === AGG_TYPE.TERMS;
}
interface Props {
isHeatmap: boolean;
renderAs: RENDER_AS;
resolution: GRID_RESOLUTION;
onChange: (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => void;
metrics: AggDescriptor[];
@ -64,9 +33,70 @@ export class ResolutionEditor extends Component<Props, State> {
showModal: false,
};
_getScale() {
return this.props.renderAs === RENDER_AS.HEX
? {
[GRID_RESOLUTION.SUPER_FINE]: 3,
[GRID_RESOLUTION.MOST_FINE]: 2,
[GRID_RESOLUTION.FINE]: 2,
[GRID_RESOLUTION.COARSE]: 1,
}
: {
[GRID_RESOLUTION.SUPER_FINE]: 4,
[GRID_RESOLUTION.MOST_FINE]: 3,
[GRID_RESOLUTION.FINE]: 2,
[GRID_RESOLUTION.COARSE]: 1,
};
}
_getTicks() {
const scale = this._getScale();
const unlabeledTicks = [
{
label: '',
value: scale[GRID_RESOLUTION.FINE],
},
];
if (scale[GRID_RESOLUTION.FINE] !== scale[GRID_RESOLUTION.MOST_FINE]) {
unlabeledTicks.push({
label: '',
value: scale[GRID_RESOLUTION.MOST_FINE],
});
}
return [
{
label: i18n.translate('xpack.maps.source.esGrid.lowLabel', {
defaultMessage: `low`,
}),
value: scale[GRID_RESOLUTION.COARSE],
},
...unlabeledTicks,
{
label: i18n.translate('xpack.maps.source.esGrid.highLabel', {
defaultMessage: `high`,
}),
value: scale[GRID_RESOLUTION.SUPER_FINE],
},
];
}
_resolutionToSliderValue(resolution: GRID_RESOLUTION): number {
const scale = this._getScale();
return scale[resolution];
}
_sliderValueToResolution(value: number): GRID_RESOLUTION {
const scale = this._getScale();
const resolution = Object.keys(scale).find((key) => {
return scale[key as GRID_RESOLUTION] === value;
});
return resolution ? (resolution as GRID_RESOLUTION) : GRID_RESOLUTION.COARSE;
}
_onResolutionChange = (event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLButtonElement>) => {
const resolution = sliderValueToResolution(parseInt(event.currentTarget.value, 10));
if (!this.props.isHeatmap && resolution === GRID_RESOLUTION.SUPER_FINE) {
const resolution = this._sliderValueToResolution(parseInt(event.currentTarget.value, 10));
if (isMvt(this.props.renderAs, resolution)) {
const hasUnsupportedMetrics = this.props.metrics.find(isUnsupportedVectorTileMetric);
if (hasUnsupportedMetrics) {
this.setState({ showModal: true });
@ -129,11 +159,13 @@ export class ResolutionEditor extends Component<Props, State> {
render() {
const helpText =
!this.props.isHeatmap && this.props.resolution === GRID_RESOLUTION.SUPER_FINE
(this.props.renderAs === RENDER_AS.POINT || this.props.renderAs === RENDER_AS.GRID) &&
this.props.resolution === GRID_RESOLUTION.SUPER_FINE
? i18n.translate('xpack.maps.source.esGrid.superFineHelpText', {
defaultMessage: 'High resolution uses vector tiles.',
})
: undefined;
const ticks = this._getTicks();
return (
<>
{this._renderModal()}
@ -145,28 +177,13 @@ export class ResolutionEditor extends Component<Props, State> {
display="columnCompressed"
>
<EuiRange
value={resolutionToSliderValue(this.props.resolution)}
value={this._resolutionToSliderValue(this.props.resolution)}
onChange={this._onResolutionChange}
min={1}
max={4}
max={ticks.length}
showTicks
tickInterval={1}
ticks={[
{
label: i18n.translate('xpack.maps.source.esGrid.lowLabel', {
defaultMessage: `low`,
}),
value: 1,
},
{ label: '', value: 2 },
{ label: '', value: 3 },
{
label: i18n.translate('xpack.maps.source.esGrid.highLabel', {
defaultMessage: `high`,
}),
value: 4,
},
]}
ticks={ticks}
compressed
/>
</EuiFormRow>

View file

@ -19,6 +19,7 @@ jest.mock('uuid/v4', () => {
const defaultProps = {
currentLayerType: LAYER_TYPE.GEOJSON_VECTOR,
geoFieldName: 'myLocation',
indexPatternId: 'foobar',
onChange: async () => {},
metrics: [],

View file

@ -11,7 +11,13 @@ import uuid from 'uuid/v4';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui';
import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters';
import { AGG_TYPE, GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants';
import {
AGG_TYPE,
ES_GEO_FIELD_TYPE,
GRID_RESOLUTION,
LAYER_TYPE,
RENDER_AS,
} from '../../../../common/constants';
import { MetricsEditor } from '../../../components/metrics_editor';
import { getIndexPatternService } from '../../../kibana_services';
import { ResolutionEditor } from './resolution_editor';
@ -21,9 +27,11 @@ import { RenderAsSelect } from './render_as_select';
import { AggDescriptor } from '../../../../common/descriptor_types';
import { OnSourceChangeArgs } from '../source';
import { clustersTitle, heatmapTitle } from './es_geo_grid_source';
import { isMvt } from './is_mvt';
interface Props {
currentLayerType?: string;
geoFieldName: string;
indexPatternId: string;
onChange: (...args: OnSourceChangeArgs[]) => Promise<void>;
metrics: AggDescriptor[];
@ -32,6 +40,7 @@ interface Props {
}
interface State {
geoFieldType?: ES_GEO_FIELD_TYPE;
metricsEditorKey: string;
fields: IndexPatternField[];
loadError?: string;
@ -70,30 +79,42 @@ export class UpdateSourceEditor extends Component<Props, State> {
return;
}
const geoField = indexPattern.fields.getByName(this.props.geoFieldName);
this.setState({
fields: indexPattern.fields.filter((field) => !indexPatterns.isNestedField(field)),
geoFieldType: geoField ? (geoField.type as ES_GEO_FIELD_TYPE) : undefined,
});
}
_getNewLayerType(renderAs: RENDER_AS, resolution: GRID_RESOLUTION): LAYER_TYPE | undefined {
let nextLayerType: LAYER_TYPE | undefined;
if (renderAs === RENDER_AS.HEATMAP) {
nextLayerType = LAYER_TYPE.HEATMAP;
} else if (isMvt(renderAs, resolution)) {
nextLayerType = LAYER_TYPE.MVT_VECTOR;
} else {
nextLayerType = LAYER_TYPE.GEOJSON_VECTOR;
}
// only return newLayerType if there is a change from current layer type
return nextLayerType !== undefined && nextLayerType !== this.props.currentLayerType
? nextLayerType
: undefined;
}
_onMetricsChange = (metrics: AggDescriptor[]) => {
this.props.onChange({ propName: 'metrics', value: metrics });
};
_onResolutionChange = async (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => {
let newLayerType;
if (
this.props.currentLayerType === LAYER_TYPE.GEOJSON_VECTOR ||
this.props.currentLayerType === LAYER_TYPE.MVT_VECTOR
) {
newLayerType =
resolution === GRID_RESOLUTION.SUPER_FINE
? LAYER_TYPE.MVT_VECTOR
: LAYER_TYPE.GEOJSON_VECTOR;
}
await this.props.onChange(
{ propName: 'metrics', value: metrics },
{ propName: 'resolution', value: resolution, newLayerType }
{
propName: 'resolution',
value: resolution,
newLayerType: this._getNewLayerType(this.props.renderAs, resolution),
}
);
// Metrics editor persists metrics in state.
@ -102,7 +123,11 @@ export class UpdateSourceEditor extends Component<Props, State> {
};
_onRequestTypeSelect = (requestType: RENDER_AS) => {
this.props.onChange({ propName: 'requestType', value: requestType });
this.props.onChange({
propName: 'requestType',
value: requestType,
newLayerType: this._getNewLayerType(requestType, this.props.resolution),
});
};
_getMetricsFilter() {
@ -155,13 +180,14 @@ export class UpdateSourceEditor extends Component<Props, State> {
</EuiTitle>
<EuiSpacer size="m" />
<ResolutionEditor
isHeatmap={this.props.currentLayerType === LAYER_TYPE.HEATMAP}
renderAs={this.props.renderAs}
resolution={this.props.resolution}
onChange={this._onResolutionChange}
metrics={this.props.metrics}
/>
<RenderAsSelect
isColumnCompressed
geoFieldType={this.state.geoFieldType}
renderAs={this.props.renderAs}
onChange={this._onRequestTypeSelect}
/>

View file

@ -341,7 +341,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource
getGeoFieldName(): string {
if (!this._descriptor.geoField) {
throw new Error('Should not call');
throw new Error(`Required field 'geoField' not provided in '_descriptor'`);
}
return this._descriptor.geoField;
}

View file

@ -23,6 +23,12 @@ export function setStartServices(core: CoreStart, plugins: MapsPluginStartDepend
emsSettings = mapsEms.createEMSSettings();
}
let isCloudEnabled = false;
export function setIsCloudEnabled(enabled: boolean) {
isCloudEnabled = enabled;
}
export const getIsCloud = () => isCloudEnabled;
export const getIndexNameFormComponent = () => pluginsStart.fileUpload.IndexNameFormComponent;
export const getFileUploadComponent = () => pluginsStart.fileUpload.FileUploadComponent;
export const getIndexPatternService = () => pluginsStart.data.indexPatterns;

View file

@ -22,7 +22,7 @@ import type {
} from '../../../../src/core/public';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
import { MapInspectorView } from './inspector/map_inspector_view';
import { setMapAppConfig, setStartServices } from './kibana_services';
import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_services';
import { featureCatalogueEntry } from './feature_catalogue_entry';
import { getMapsVisTypeAlias } from './maps_vis_type_alias';
import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
@ -74,11 +74,13 @@ import {
} from './legacy_visualizations';
import type { SecurityPluginStart } from '../../security/public';
import type { SpacesPluginStart } from '../../spaces/public';
import type { CloudSetup } from '../../cloud/public';
import type { LensPublicSetup } from '../../lens/public';
import { setupLensChoroplethChart } from './lens';
export interface MapsPluginSetupDependencies {
cloud?: CloudSetup;
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
inspector: InspectorSetupContract;
home?: HomePublicPluginSetup;
@ -193,6 +195,8 @@ export class MapsPlugin
plugins.expressions.registerRenderer(tileMapRenderer);
plugins.visualizations.createBaseVisualization(tileMapVisType);
setIsCloudEnabled(!!plugins.cloud?.isCloudEnabled);
return {
registerLayerWizard: registerLayerWizardExternal,
registerSource,

View file

@ -23,7 +23,7 @@ export async function getEsGridTile({
y,
z,
requestBody = {},
requestType = RENDER_AS.POINT,
renderAs = RENDER_AS.POINT,
gridPrecision,
abortController,
}: {
@ -37,7 +37,7 @@ export async function getEsGridTile({
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
requestType: RENDER_AS.GRID | RENDER_AS.POINT;
renderAs: RENDER_AS;
gridPrecision: number;
abortController: AbortController;
}): Promise<Stream | null> {
@ -49,7 +49,8 @@ export async function getEsGridTile({
exact_bounds: false,
extent: 4096, // full resolution,
query: requestBody.query,
grid_type: requestType === RENDER_AS.GRID ? 'grid' : 'centroid',
grid_agg: renderAs === RENDER_AS.HEX ? 'geohex' : 'geotile',
grid_type: renderAs === RENDER_AS.GRID || renderAs === RENDER_AS.HEX ? 'grid' : 'centroid',
aggs: requestBody.aggs,
fields: requestBody.fields,
runtime_mappings: requestBody.runtime_mappings,

View file

@ -88,7 +88,7 @@ export function initMVTRoutes({
geometryFieldName: schema.string(),
requestBody: schema.string(),
index: schema.string(),
requestType: schema.string(),
renderAs: schema.string(),
token: schema.maybe(schema.string()),
gridPrecision: schema.number(),
}),
@ -114,7 +114,7 @@ export function initMVTRoutes({
z: parseInt((params as any).z, 10) as number,
index: query.index as string,
requestBody: decodeMvtResponseBody(query.requestBody as string) as any,
requestType: query.requestType as RENDER_AS.POINT | RENDER_AS.GRID,
renderAs: query.renderAs as RENDER_AS,
gridPrecision: parseInt(query.gridPrecision, 10),
abortController,
});

View file

@ -32,6 +32,7 @@
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../cloud/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../lens/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },

View file

@ -13,16 +13,15 @@ export default function ({ getService }) {
const supertest = getService('supertest');
describe('getGridTile', () => {
it('should return vector tile containing cluster features', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getGridTile/3/2/3.pbf\
const URL = `/api/maps/mvt/getGridTile/3/2/3.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&gridPrecision=8\
&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\
&requestType=point`
)
&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))`;
it('should return vector tile with expected headers', async () => {
const resp = await supertest
.get(URL + '&renderAs=point')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
@ -31,6 +30,14 @@ export default function ({ getService }) {
expect(resp.headers['content-disposition']).to.be('inline');
expect(resp.headers['content-type']).to.be('application/x-protobuf');
expect(resp.headers['cache-control']).to.be('public, max-age=3600');
});
it('should return vector tile containing clusters when renderAs is "point"', async () => {
const resp = await supertest
.get(URL + '&renderAs=point')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
@ -46,58 +53,43 @@ export default function ({ getService }) {
_key: '11/517/809',
'avg_of_bytes.value': 9252,
});
// assert feature geometry is weighted centroid
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]);
// Metadata feature
const metaDataLayer = jsonTile.layers.meta;
expect(metaDataLayer.length).to.be(1);
const metadataFeature = metaDataLayer.feature(0);
expect(metadataFeature.type).to.be(3);
expect(metadataFeature.extent).to.be(4096);
expect(metadataFeature.properties['aggregations._count.avg']).to.eql(1);
expect(metadataFeature.properties['aggregations._count.count']).to.eql(1);
expect(metadataFeature.properties['aggregations._count.min']).to.eql(1);
expect(metadataFeature.properties['aggregations._count.sum']).to.eql(1);
expect(metadataFeature.properties['aggregations.avg_of_bytes.avg']).to.eql(9252);
expect(metadataFeature.properties['aggregations.avg_of_bytes.count']).to.eql(1);
expect(metadataFeature.properties['aggregations.avg_of_bytes.max']).to.eql(9252);
expect(metadataFeature.properties['aggregations.avg_of_bytes.min']).to.eql(9252);
expect(metadataFeature.properties['aggregations.avg_of_bytes.sum']).to.eql(9252);
expect(metadataFeature.properties['hits.total.relation']).to.eql('eq');
expect(metadataFeature.properties['hits.total.value']).to.eql(1);
expect(metadataFeature.loadGeometry()).to.eql([
[
{ x: 0, y: 4096 },
{ x: 4096, y: 4096 },
{ x: 4096, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 4096 },
],
]);
});
it('should return vector tile containing grid features', async () => {
it('should return vector tile containing clusters with renderAs is "heatmap"', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getGridTile/3/2/3.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&gridPrecision=8\
&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\
&requestType=grid`
)
.get(URL + '&renderAs=heatmap')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
expect(resp.headers['content-encoding']).to.be('gzip');
expect(resp.headers['content-disposition']).to.be('inline');
expect(resp.headers['content-type']).to.be('application/x-protobuf');
expect(resp.headers['cache-control']).to.be('public, max-age=3600');
const jsonTile = new VectorTile(new Protobuf(resp.body));
// Cluster feature
const layer = jsonTile.layers.aggs;
expect(layer.length).to.be(1);
const clusterFeature = layer.feature(0);
expect(clusterFeature.type).to.be(1);
expect(clusterFeature.extent).to.be(4096);
expect(clusterFeature.id).to.be(undefined);
expect(clusterFeature.properties).to.eql({
_count: 1,
_key: '11/517/809',
'avg_of_bytes.value': 9252,
});
// assert feature geometry is weighted centroid
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]);
});
it('should return vector tile containing grid features when renderAs is "grid"', async () => {
const resp = await supertest
.get(URL + '&renderAs=grid')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.aggs;
@ -112,6 +104,8 @@ export default function ({ getService }) {
_key: '11/517/809',
'avg_of_bytes.value': 9252,
});
// assert feature geometry is grid
expect(gridFeature.loadGeometry()).to.eql([
[
{ x: 80, y: 672 },
@ -121,6 +115,51 @@ export default function ({ getService }) {
{ x: 80, y: 672 },
],
]);
});
it('should return vector tile containing hexegon features when renderAs is "hex"', async () => {
const resp = await supertest
.get(URL + '&renderAs=hex')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.aggs;
expect(layer.length).to.be(1);
const gridFeature = layer.feature(0);
expect(gridFeature.type).to.be(3);
expect(gridFeature.extent).to.be(4096);
expect(gridFeature.id).to.be(undefined);
expect(gridFeature.properties).to.eql({
_count: 1,
_key: '85264a33fffffff',
'avg_of_bytes.value': 9252,
});
// assert feature geometry is hex
expect(gridFeature.loadGeometry()).to.eql([
[
{ x: 102, y: 669 },
{ x: 99, y: 659 },
{ x: 89, y: 657 },
{ x: 83, y: 664 },
{ x: 86, y: 674 },
{ x: 96, y: 676 },
{ x: 102, y: 669 },
],
]);
});
it('should return vector tile with meta layer', async () => {
const resp = await supertest
.get(URL + '&renderAs=point')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
// Metadata feature
const metaDataLayer = jsonTile.layers.meta;

View file

@ -47,7 +47,7 @@ export default function ({ getPageObjects, getService }) {
geometryFieldName: 'geo.coordinates',
index: 'logstash-*',
gridPrecision: 8,
requestType: 'grid',
renderAs: 'grid',
requestBody: `(_source:(excludes:!()),aggs:(max_of_bytes:(max:(field:bytes))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))`,
});