[Maps] Use ES mvt (#114553)

* tmp

* tmp

* tmp

* tmp

* tmp

* use es naming

* typo

* organize files for clarity

* plugin for hits

* tmp

* initial styling

* more boilerplate

* tmp

* temp

* add size support

* remove junk

* tooltip

* edits

* too many features

* rename for clarity

* typing

* tooltip improvements

* icon

* callouts

* align count handling

* typechecks

* i18n

* tmp

* type fixes

* linting

* convert to ts and disable option

* readd test dependencies

* typescheck

* update yarn lock

* fix typecheck

* update snapshot

* fix snapshot

* fix snapshot

* fix snapshot

* fix snapshot

* fix test

* fix tests

* fix test

* add key

* fix integration test

* move test

* use centroid placement

* more text fixes

* more test fixes

* Remove top terms aggregations when switching to super fine resolution (#114667)

* [Maps] MVT metrics

* remove js file

* updateSourceProps

* i18n cleanup

* mvt labels

* remove isPointsOnly from IVectorSource interface

* move get_centroid_featues to vector_layer since its no longer used in server

* labels

* warn users when selecting scaling type that does not support term joins

* clean up scaling_form

* remove IField.isCountable method

* move pluck code from common to dynamic_style_property

* move convert_to_geojson to es_geo_grid_source folder

* remove getMbFeatureIdPropertyName from IVectorLayer

* clean up cleanTooltipStateForLayer

* use euiWarningColor for too many features outline

* update jest snapshots and eslint fixes

* update docs for incomplete data changes

* move tooManyFeatures MB layer definition from VectorLayer to TiledVectorLayer, clean up VectorSource interface

* remove commented out filter in tooltip_control add api docs for getMbLayerIds and getMbTooltipLayerIds

* revert changing getSourceTooltipContent to getSourceTooltipConfigFromGeoJson

* replace DEFAULT_MAX_RESULT_WINDOW with loading maxResultWindow as data request

* clean up

* eslint

* remove unused constants from Kibana MVT implemenation and tooManyFeaturesImage

* add better should method for tiled_vector_layer.getCustomIconAndTooltipContent jest test

* fix tooltips not being displayed for super-fine clusters and grids

* fix check in getFeatureId for es_Search_sources only

* eslint, remove __kbn_metadata_feature__ filter from mapbox style expects

* remove geoFieldType paramter for tile API

* remove searchSessionId from MVT url since its no longer used

* tslint

* vector tile scaling option copy update

* fix getTile and getGridTile API integration tests

* remove size from _mvt request body, size provided in query

* eslint, fix test expect

* stablize jest test

* track total hits for _mvt request

* track total hits take 2

* align vector tile copy

* eslint

* revert change to EsSearchSource._loadTooltipProperties with regards to handling undefined _index. MVT now provides _index

* clean up

* only send metric aggregations to mvt/getGridTile endpoint

* update snapshot, update getGridTile URLs in tests

* update request URL for getGridTile

* eslint

Co-authored-by: Nathan Reese <reese.nathan@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Thomas Neirynck 2021-10-25 12:41:04 -04:00 committed by GitHub
parent 1e718a5572
commit 33fd1bdff0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 1201 additions and 2056 deletions

View file

@ -27,9 +27,9 @@ Results exceeding `index.max_result_window` are not displayed.
* *Show clusters when results exceed 10,000* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents.
* *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles.
* *Use vector tiles.* Vector tiles partition your map into tiles.
Each tile request is limited to the `index.max_result_window` index setting.
Tiles exceeding `index.max_result_window` have a visual indicator when there are too many features to display.
When a tile exceeds `index.max_result_window`, results exceeding `index.max_result_window` are not contained in the tile and a dashed rectangle outlining the bounding box containing all geo values within the tile is displayed.
*EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service].

View file

@ -166,7 +166,6 @@
"@mapbox/geojson-rewind": "^0.5.0",
"@mapbox/mapbox-gl-draw": "1.3.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mapbox/vector-tile": "1.3.1",
"@reduxjs/toolkit": "^1.6.1",
"@slack/webhook": "^5.0.4",
"@turf/along": "6.0.1",
@ -460,6 +459,7 @@
"@kbn/test": "link:bazel-bin/packages/kbn-test",
"@kbn/test-subj-selector": "link:bazel-bin/packages/kbn-test-subj-selector",
"@loaders.gl/polyfills": "^2.3.5",
"@mapbox/vector-tile": "1.3.1",
"@microsoft/api-documenter": "7.7.2",
"@microsoft/api-extractor": "7.7.0",
"@octokit/rest": "^16.35.0",

View file

@ -47,14 +47,7 @@ export const CHECK_IS_DRAWING_INDEX = `/${GIS_API_PATH}/checkIsDrawingIndex`;
export const MVT_GETTILE_API_PATH = 'mvt/getTile';
export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile';
export const MVT_SOURCE_LAYER_NAME = 'source_layer';
// Identifies vector tile "too many features" feature.
// "too many features" feature is a box showing area that contains too many features for single ES search response
export const KBN_METADATA_FEATURE = '__kbn_metadata_feature__';
export const KBN_FEATURE_COUNT = '__kbn_feature_count__';
export const KBN_IS_TILE_COMPLETE = '__kbn_is_tile_complete__';
export const KBN_VECTOR_SHAPE_TYPE_COUNTS = '__kbn_vector_shape_type_counts__';
export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__';
// Identifies centroid feature.
// Centroids are a single point for representing lines, multiLines, polygons, and multiPolygons
export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__';
@ -119,7 +112,6 @@ export const DEFAULT_MAX_RESULT_WINDOW = 10000;
export const DEFAULT_MAX_INNER_RESULT_WINDOW = 100;
export const DEFAULT_MAX_BUCKETS_LIMIT = 65535;
export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__';
export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__';
export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_';

View file

@ -10,21 +10,13 @@
import { Query } from 'src/plugins/data/public';
import { Feature } from 'geojson';
import {
FieldMeta,
HeatmapStyleDescriptor,
StyleDescriptor,
VectorStyleDescriptor,
} from './style_property_descriptor_types';
import { DataRequestDescriptor } from './data_request_descriptor_types';
import { AbstractSourceDescriptor, TermJoinSourceDescriptor } from './source_descriptor_types';
import { VectorShapeTypeCounts } from '../get_geometry_counts';
import {
KBN_FEATURE_COUNT,
KBN_IS_TILE_COMPLETE,
KBN_METADATA_FEATURE,
KBN_VECTOR_SHAPE_TYPE_COUNTS,
LAYER_TYPE,
} from '../constants';
import { LAYER_TYPE } from '../constants';
export type Attribution = {
label: string;
@ -38,11 +30,8 @@ export type JoinDescriptor = {
export type TileMetaFeature = Feature & {
properties: {
[KBN_METADATA_FEATURE]: true;
[KBN_IS_TILE_COMPLETE]: boolean;
[KBN_FEATURE_COUNT]: number;
[KBN_VECTOR_SHAPE_TYPE_COUNTS]: VectorShapeTypeCounts;
fieldMeta?: FieldMeta;
'hits.total.relation': string;
'hits.total.value': number;
};
};

View file

@ -6,7 +6,6 @@
*/
export * from './es_agg_utils';
export * from './convert_to_geojson';
export * from './elasticsearch_geo_utils';
export * from './spatial_filter_utils';
export * from './types';

View file

@ -1,45 +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 { Feature } from 'geojson';
import { GEO_JSON_TYPE, VECTOR_SHAPE_TYPE } from './constants';
export interface VectorShapeTypeCounts {
[VECTOR_SHAPE_TYPE.POINT]: number;
[VECTOR_SHAPE_TYPE.LINE]: number;
[VECTOR_SHAPE_TYPE.POLYGON]: number;
}
export function countVectorShapeTypes(features: Feature[]): VectorShapeTypeCounts {
const vectorShapeTypeCounts: VectorShapeTypeCounts = {
[VECTOR_SHAPE_TYPE.POINT]: 0,
[VECTOR_SHAPE_TYPE.LINE]: 0,
[VECTOR_SHAPE_TYPE.POLYGON]: 0,
};
for (let i = 0; i < features.length; i++) {
const feature: Feature = features[i];
if (
feature.geometry.type === GEO_JSON_TYPE.POINT ||
feature.geometry.type === GEO_JSON_TYPE.MULTI_POINT
) {
vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.POINT] += 1;
} else if (
feature.geometry.type === GEO_JSON_TYPE.LINE_STRING ||
feature.geometry.type === GEO_JSON_TYPE.MULTI_LINE_STRING
) {
vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.LINE] += 1;
} else if (
feature.geometry.type === GEO_JSON_TYPE.POLYGON ||
feature.geometry.type === GEO_JSON_TYPE.MULTI_POLYGON
) {
vectorShapeTypeCounts[VECTOR_SHAPE_TYPE.POLYGON] += 1;
}
}
return vectorShapeTypeCounts;
}

View file

@ -1,46 +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 { Feature } from 'geojson';
import { CategoryFieldMeta } from './descriptor_types';
export function pluckCategoryFieldMeta(
features: Feature[],
name: string,
size: number
): CategoryFieldMeta | null {
const counts = new Map();
for (let i = 0; i < features.length; i++) {
const feature = features[i];
const term = feature.properties ? feature.properties[name] : undefined;
// properties object may be sparse, so need to check if the field is effectively present
if (typeof term !== undefined) {
if (counts.has(term)) {
counts.set(term, counts.get(term) + 1);
} else {
counts.set(term, 1);
}
}
}
return trimCategories(counts, size);
}
export function trimCategories(counts: Map<string, number>, size: number): CategoryFieldMeta {
const ordered = [];
for (const [key, value] of counts) {
ordered.push({ key, count: value });
}
ordered.sort((a, b) => {
return b.count - a.count;
});
const truncated = ordered.slice(0, size);
return {
categories: truncated,
} as CategoryFieldMeta;
}

View file

@ -1,34 +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 { Feature } from 'geojson';
import { RangeFieldMeta } from './descriptor_types';
export function pluckRangeFieldMeta(
features: Feature[],
name: string,
parseValue: (rawValue: unknown) => number
): RangeFieldMeta | null {
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < features.length; i++) {
const feature = features[i];
const newValue = feature.properties ? parseValue(feature.properties[name]) : NaN;
if (!isNaN(newValue)) {
min = Math.min(min, newValue);
max = Math.max(max, newValue);
}
}
return min === Infinity || max === -Infinity
? null
: ({
min,
max,
delta: max - min,
} as RangeFieldMeta);
}

View file

@ -282,10 +282,10 @@ function endDataLoad(
if (dataId === SOURCE_DATA_REQUEST_ID) {
const features = data && 'features' in data ? (data as FeatureCollection).features : [];
const layer = getLayerById(layerId, getState());
const eventHandlers = getEventHandlers(getState());
if (eventHandlers && eventHandlers.onDataLoadEnd) {
const layer = getLayerById(layerId, getState());
const resultMeta: ResultMeta = {};
if (layer && layer.getType() === LAYER_TYPE.VECTOR) {
const featuresWithoutCentroids = features.filter((feature) => {
@ -301,7 +301,9 @@ function endDataLoad(
});
}
dispatch(updateTooltipStateForLayer(layerId, features));
if (layer) {
dispatch(updateTooltipStateForLayer(layer, features));
}
}
dispatch({
@ -344,7 +346,10 @@ function onDataLoadError(
});
}
dispatch(updateTooltipStateForLayer(layerId));
const layer = getLayerById(layerId, getState());
if (layer) {
dispatch(updateTooltipStateForLayer(layer));
}
}
dispatch({
@ -359,7 +364,10 @@ function onDataLoadError(
}
export function updateSourceDataRequest(layerId: string, newData: object) {
return (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => {
return (
dispatch: ThunkDispatch<MapStoreState, void, AnyAction>,
getState: () => MapStoreState
) => {
dispatch({
type: UPDATE_SOURCE_DATA_REQUEST,
dataId: SOURCE_DATA_REQUEST_ID,
@ -368,7 +376,10 @@ export function updateSourceDataRequest(layerId: string, newData: object) {
});
if ('features' in newData) {
dispatch(updateTooltipStateForLayer(layerId, (newData as FeatureCollection).features));
const layer = getLayerById(layerId, getState());
if (layer) {
dispatch(updateTooltipStateForLayer(layer, (newData as FeatureCollection).features));
}
}
dispatch(updateStyleMeta(layerId));

View file

@ -51,6 +51,7 @@ import {
} from '../../common/descriptor_types';
import { ILayer } from '../classes/layers/layer';
import { IVectorLayer } from '../classes/layers/vector_layer';
import { OnSourceChangeArgs } from '../classes/sources/source';
import { DRAW_MODE, LAYER_STYLE_TYPE, LAYER_TYPE } from '../../common/constants';
import { IVectorStyle } from '../classes/styles/vector/vector_style';
import { notifyLicensedFeatureUsage } from '../licensed_features';
@ -217,7 +218,7 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) {
}
if (!makeVisible) {
dispatch(updateTooltipStateForLayer(layerId));
dispatch(updateTooltipStateForLayer(layer));
}
dispatch({
@ -323,18 +324,17 @@ function updateMetricsProp(layerId: string, value: unknown) {
) => {
const layer = getLayerById(layerId, getState());
const previousFields = await (layer as IVectorLayer).getFields();
await dispatch({
dispatch({
type: UPDATE_SOURCE_PROP,
layerId,
propName: 'metrics',
value,
});
await dispatch(updateStyleProperties(layerId, previousFields as IESAggField[]));
dispatch(syncDataForLayerId(layerId, false));
};
}
export function updateSourceProp(
function updateSourcePropWithoutSync(
layerId: string,
propName: string,
value: unknown,
@ -356,6 +356,28 @@ export function updateSourceProp(
if (newLayerType) {
dispatch(updateLayerType(layerId, newLayerType));
}
};
}
export function updateSourceProp(
layerId: string,
propName: string,
value: unknown,
newLayerType?: LAYER_TYPE
) {
return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => {
await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType));
dispatch(syncDataForLayerId(layerId, false));
};
}
export function updateSourceProps(layerId: string, sourcePropChanges: OnSourceChangeArgs[]) {
return async (dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) => {
// Using for loop to ensure update completes before starting next update
for (let i = 0; i < sourcePropChanges.length; i++) {
const { propName, value, newLayerType } = sourcePropChanges[i];
await dispatch(updateSourcePropWithoutSync(layerId, propName, value, newLayerType));
}
dispatch(syncDataForLayerId(layerId, false));
};
}
@ -504,7 +526,7 @@ function removeLayerFromLayerList(layerId: string) {
layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => {
dispatch(cancelRequest(requestToken));
});
dispatch(updateTooltipStateForLayer(layerId));
dispatch(updateTooltipStateForLayer(layerGettingRemoved));
layerGettingRemoved.destroy();
dispatch({
type: REMOVE_LAYER,

View file

@ -63,7 +63,7 @@ import { INITIAL_LOCATION } from '../../common/constants';
import { updateTooltipStateForLayer } from './tooltip_actions';
import { VectorLayer } from '../classes/layers/vector_layer';
import { SET_DRAW_MODE } from './ui_actions';
import { expandToTileBoundaries } from '../../common/geo_tile_utils';
import { expandToTileBoundaries } from '../classes/util/geo_tile_utils';
import { getToasts } from '../kibana_services';
export function setMapInitError(errorMessage: string) {
@ -171,7 +171,7 @@ export function mapExtentChanged(mapExtentState: MapExtentState) {
if (prevZoom !== nextZoom) {
getLayerList(getState()).map((layer) => {
if (!layer.showAtZoomLevel(nextZoom)) {
dispatch(updateTooltipStateForLayer(layer.getId()));
dispatch(updateTooltipStateForLayer(layer));
}
});
}

View file

@ -10,9 +10,11 @@ import { Dispatch } from 'redux';
import { Feature } from 'geojson';
import { getOpenTooltips } from '../selectors/map_selectors';
import { SET_OPEN_TOOLTIPS } from './map_action_constants';
import { FEATURE_ID_PROPERTY_NAME, FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants';
import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants';
import { TooltipFeature, TooltipState } from '../../common/descriptor_types';
import { MapStoreState } from '../reducers/store';
import { ILayer } from '../classes/layers/layer';
import { IVectorLayer, getFeatureId, isVectorLayer } from '../classes/layers/vector_layer';
export function closeOnClickTooltip(tooltipId: string) {
return (dispatch: Dispatch, getState: () => MapStoreState) => {
@ -62,13 +64,17 @@ export function openOnHoverTooltip(tooltipState: TooltipState) {
};
}
export function updateTooltipStateForLayer(layerId: string, layerFeatures: Feature[] = []) {
export function updateTooltipStateForLayer(layer: ILayer, layerFeatures: Feature[] = []) {
return (dispatch: Dispatch, getState: () => MapStoreState) => {
if (!isVectorLayer(layer)) {
return;
}
const openTooltips = getOpenTooltips(getState())
.map((tooltipState) => {
const nextFeatures: TooltipFeature[] = [];
tooltipState.features.forEach((tooltipFeature) => {
if (tooltipFeature.layerId !== layerId) {
if (tooltipFeature.layerId !== layer.getId()) {
// feature from another layer, keep it
nextFeatures.push(tooltipFeature);
}
@ -79,7 +85,8 @@ export function updateTooltipStateForLayer(layerId: string, layerFeatures: Featu
? layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME]
: true;
return (
isVisible && layerFeature.properties![FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id
isVisible &&
getFeatureId(layerFeature, (layer as IVectorLayer).getSource()) === tooltipFeature.id
);
});

View file

@ -34,6 +34,10 @@ export class AggField extends CountAggField {
return !!this._esDocField;
}
getMbFieldName(): string {
return this._source.isMvt() ? this.getName() + '.value' : this.getName();
}
supportsFieldMeta(): boolean {
// count and sum aggregations are not within field bounds so they do not support field meta.
return !isMetricCountable(this._getAggType());

View file

@ -43,6 +43,10 @@ export class CountAggField implements IESAggField {
return this._source.getAggKey(this._getAggType(), this.getRootName());
}
getMbFieldName(): string {
return this._source.isMvt() ? '_count' : this.getName();
}
getRootName(): string {
return '';
}

View file

@ -23,6 +23,10 @@ export class TopTermPercentageField implements IESAggField {
return this._topTermAggField.getSource();
}
getMbFieldName(): string {
return this.getName();
}
getOrigin(): FIELD_ORIGIN {
return this._topTermAggField.getOrigin();
}

View file

@ -11,6 +11,7 @@ import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'
export interface IField {
getName(): string;
getMbFieldName(): string;
getRootName(): string;
canValueBeFormatted(): boolean;
getLabel(): Promise<string>;
@ -50,6 +51,10 @@ export class AbstractField implements IField {
return this._fieldName;
}
getMbFieldName(): string {
return this.getName();
}
getRootName(): string {
return this.getName();
}

View file

@ -63,13 +63,18 @@ export interface ILayer {
getStyleForEditing(): IStyle;
getCurrentStyle(): IStyle;
getImmutableSourceProperties(): Promise<ImmutableSourceProperty[]>;
renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement<any> | null;
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement<any> | null;
isLayerLoading(): boolean;
isLoadingBounds(): boolean;
isFilteredByGlobalTime(): Promise<boolean>;
hasErrors(): boolean;
getErrors(): string;
/*
* ILayer.getMbLayerIds returns a list of all mapbox layers assoicated with this layer.
*/
getMbLayerIds(): string[];
ownsMbLayerId(mbLayerId: string): boolean;
ownsMbSourceId(mbSourceId: string): boolean;
syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void;
@ -77,7 +82,7 @@ export interface ILayer {
isInitialDataLoadComplete(): boolean;
getIndexPatternIds(): string[];
getQueryableIndexPatternIds(): string[];
getType(): LAYER_TYPE | undefined;
getType(): LAYER_TYPE;
isVisible(): boolean;
cloneDescriptor(): Promise<LayerDescriptor>;
renderStyleEditor(
@ -325,9 +330,8 @@ export class AbstractLayer implements ILayer {
return await source.getImmutableProperties();
}
renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
const source = this.getSourceForEditing();
return source.renderSourceSettingsEditor({ onChange, currentLayerType: this._descriptor.type });
renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs) {
return this.getSourceForEditing().renderSourceSettingsEditor(sourceEditorArgs);
}
getPrevRequestToken(dataId: string): symbol | undefined {
@ -437,7 +441,7 @@ export class AbstractLayer implements ILayer {
mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
}
getType(): LAYER_TYPE | undefined {
getType(): LAYER_TYPE {
return this._descriptor.type as LAYER_TYPE;
}

View file

@ -1,9 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`icon should use no data icon 1`] = `
<span
color="subdued"
data-euiicon-type="minusInCircle"
size="m"
exports[`getCustomIconAndTooltipContent Layers with non-elasticsearch sources should display icon 1`] = `
<PolygonIcon
style={
Object {
"fill": "#54B399",
"stroke": "#41937c",
"strokeWidth": "1px",
}
}
/>
`;

View file

@ -95,8 +95,8 @@ describe('visiblity', () => {
});
});
describe('icon', () => {
it('should use no data icon', async () => {
describe('getCustomIconAndTooltipContent', () => {
it('Layers with non-elasticsearch sources should display icon', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
const iconAndTooltipContent = layer.getCustomIconAndTooltipContent();

View file

@ -7,39 +7,44 @@
import type {
Map as MbMap,
AnyLayer as MbLayer,
GeoJSONSource as MbGeoJSONSource,
VectorSource as MbVectorSource,
} from '@kbn/mapbox-gl';
import { Feature } from 'geojson';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { parse as parseUrl } from 'url';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID, SOURCE_TYPES } from '../../../../common/constants';
import {
KBN_FEATURE_COUNT,
KBN_IS_TILE_COMPLETE,
KBN_METADATA_FEATURE,
LAYER_TYPE,
SOURCE_DATA_REQUEST_ID,
} from '../../../../common/constants';
import {
NO_RESULTS_ICON_AND_TOOLTIPCONTENT,
VectorLayer,
VectorLayerArguments,
NO_RESULTS_ICON_AND_TOOLTIPCONTENT,
} from '../vector_layer';
import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source';
import { DataRequestContext } from '../../../actions';
import {
Timeslice,
StyleMetaDescriptor,
TileMetaFeature,
Timeslice,
VectorLayerDescriptor,
VectorSourceRequestMeta,
TileMetaFeature,
} from '../../../../common/descriptor_types';
import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types';
import { ESSearchSource } from '../../sources/es_search_source';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { CustomIconAndTooltipContent } from '../layer';
const ES_MVT_META_LAYER_NAME = 'meta';
const ES_MVT_HITS_TOTAL_RELATION = 'hits.total.relation';
const ES_MVT_HITS_TOTAL_VALUE = 'hits.total.value';
const MAX_RESULT_WINDOW_DATA_REQUEST_ID = 'maxResultWindow';
/*
* MVT vector layer
*/
export class TiledVectorLayer extends VectorLayer {
static type = LAYER_TYPE.TILED_VECTOR;
@ -70,13 +75,46 @@ export class TiledVectorLayer extends VectorLayer {
}
getCustomIconAndTooltipContent(): CustomIconAndTooltipContent {
const tileMetas = this._getMetaFromTiles();
if (!tileMetas.length) {
const icon = this.getCurrentStyle().getIcon();
if (!this.getSource().isESSource()) {
// Only ES-sources can have a special meta-tile, not 3rd party vector tile sources
return {
icon,
tooltipContent: null,
areResultsTrimmed: false,
};
}
//
// TODO ES MVT specific - move to es_tiled_vector_layer implementation
//
const tileMetaFeatures = this._getMetaFromTiles();
if (!tileMetaFeatures.length) {
return NO_RESULTS_ICON_AND_TOOLTIPCONTENT;
}
const totalFeaturesCount: number = tileMetas.reduce((acc: number, tileMeta: Feature) => {
const count = tileMeta && tileMeta.properties ? tileMeta.properties[KBN_FEATURE_COUNT] : 0;
if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) {
// aggregation ES sources are never trimmed
return {
icon,
tooltipContent: null,
areResultsTrimmed: false,
};
}
const maxResultWindow = this._getMaxResultWindow();
if (maxResultWindow === undefined) {
return {
icon,
tooltipContent: null,
areResultsTrimmed: false,
};
}
const totalFeaturesCount: number = tileMetaFeatures.reduce((acc: number, tileMeta: Feature) => {
const count =
tileMeta && tileMeta.properties ? tileMeta.properties[ES_MVT_HITS_TOTAL_VALUE] : 0;
return count + acc;
}, 0);
@ -84,12 +122,16 @@ export class TiledVectorLayer extends VectorLayer {
return NO_RESULTS_ICON_AND_TOOLTIPCONTENT;
}
const isIncomplete: boolean = tileMetas.some((tileMeta: Feature) => {
return !tileMeta?.properties?.[KBN_IS_TILE_COMPLETE];
const isIncomplete: boolean = tileMetaFeatures.some((tileMeta: TileMetaFeature) => {
if (tileMeta?.properties?.[ES_MVT_HITS_TOTAL_RELATION] === 'gte') {
return tileMeta?.properties?.[ES_MVT_HITS_TOTAL_VALUE] >= maxResultWindow + 1;
} else {
return false;
}
});
return {
icon: this.getCurrentStyle().getIcon(),
icon,
tooltipContent: isIncomplete
? i18n.translate('xpack.maps.tiles.resultsTrimmedMsg', {
defaultMessage: `Results limited to {count} documents.`,
@ -107,6 +149,27 @@ export class TiledVectorLayer extends VectorLayer {
};
}
_getMaxResultWindow(): number | undefined {
const dataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID);
if (!dataRequest) {
return;
}
const data = dataRequest.getData() as { maxResultWindow: number } | undefined;
return data ? data.maxResultWindow : undefined;
}
async _syncMaxResultWindow({ startLoading, stopLoading }: DataRequestContext) {
const prevDataRequest = this.getDataRequest(MAX_RESULT_WINDOW_DATA_REQUEST_ID);
if (prevDataRequest) {
return;
}
const requestToken = Symbol(`${this.getId()}-${MAX_RESULT_WINDOW_DATA_REQUEST_ID}`);
startLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken);
const maxResultWindow = await (this.getSource() as ESSearchSource).getMaxResultWindow();
stopLoading(MAX_RESULT_WINDOW_DATA_REQUEST_ID, requestToken, { maxResultWindow });
}
async _syncMVTUrlTemplate({
startLoading,
stopLoading,
@ -141,6 +204,7 @@ export class TiledVectorLayer extends VectorLayer {
},
});
const canSkip = noChangesInSourceState && noChangesInSearchState;
if (canSkip) {
return null;
}
@ -180,6 +244,9 @@ export class TiledVectorLayer extends VectorLayer {
}
async syncData(syncContext: DataRequestContext) {
if (this.getSource().getType() === SOURCE_TYPES.ES_SEARCH) {
await this._syncMaxResultWindow(syncContext);
}
await this._syncSourceStyleMeta(syncContext, this._source, this._style as IVectorStyle);
await this._syncSourceFormatters(syncContext, this._source, this._style as IVectorStyle);
await this._syncMVTUrlTemplate(syncContext);
@ -213,10 +280,18 @@ export class TiledVectorLayer extends VectorLayer {
});
}
getMbLayerIds() {
return [...super.getMbLayerIds(), this._getMbTooManyFeaturesLayerId()];
}
ownsMbSourceId(mbSourceId: string): boolean {
return this._getMbSourceId() === mbSourceId;
}
_getMbTooManyFeaturesLayerId() {
return this.makeMbLayerId('toomanyfeatures');
}
_syncStylePropertiesWithMb(mbMap: MbMap) {
// @ts-ignore
const mbSource = mbMap.getSource(this._getMbSourceId());
@ -236,10 +311,52 @@ export class TiledVectorLayer extends VectorLayer {
this._setMbPointsProperties(mbMap, sourceMeta.layerName);
this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName);
this._setMbCentroidProperties(mbMap, sourceMeta.layerName);
this._setMbLabelProperties(mbMap, sourceMeta.layerName);
this._syncTooManyFeaturesProperties(mbMap);
}
// TODO ES MVT specific - move to es_tiled_vector_layer implementation
_syncTooManyFeaturesProperties(mbMap: MbMap) {
if (this.getSource().getType() !== SOURCE_TYPES.ES_SEARCH) {
return;
}
const maxResultWindow = this._getMaxResultWindow();
if (maxResultWindow === undefined) {
return;
}
const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId();
if (!mbMap.getLayer(tooManyFeaturesLayerId)) {
const mbTooManyFeaturesLayer: MbLayer = {
id: tooManyFeaturesLayerId,
type: 'line',
source: this.getId(),
paint: {},
};
mbTooManyFeaturesLayer['source-layer'] = ES_MVT_META_LAYER_NAME;
mbMap.addLayer(mbTooManyFeaturesLayer);
mbMap.setFilter(tooManyFeaturesLayerId, [
'all',
['==', ['get', ES_MVT_HITS_TOTAL_RELATION], 'gte'],
['>=', ['get', ES_MVT_HITS_TOTAL_VALUE], maxResultWindow + 1],
]);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-color', euiThemeVars.euiColorWarning);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-width', 3);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-dasharray', [2, 1]);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'line-opacity', this.getAlpha());
}
this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId);
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
}
queryTileMetaFeatures(mbMap: MbMap): TileMetaFeature[] | null {
if (!this.getSource().isESSource()) {
return null;
}
// @ts-ignore
const mbSource = mbMap.getSource(this._getMbSourceId());
if (!mbSource) {
@ -259,26 +376,38 @@ export class TiledVectorLayer extends VectorLayer {
// querySourceFeatures can return duplicated features when features cross tile boundaries.
// Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile
const mbFeatures = mbMap.querySourceFeatures(this._getMbSourceId(), {
sourceLayer: sourceMeta.layerName,
filter: ['==', ['get', KBN_METADATA_FEATURE], true],
sourceLayer: ES_MVT_META_LAYER_NAME,
});
const metaFeatures: TileMetaFeature[] = mbFeatures.map((mbFeature: Feature) => {
const metaFeatures: Array<TileMetaFeature | null> = (
mbFeatures as unknown as TileMetaFeature[]
).map((mbFeature: TileMetaFeature | null) => {
const parsedProperties: Record<string, unknown> = {};
for (const key in mbFeature.properties) {
if (mbFeature.properties.hasOwnProperty(key)) {
parsedProperties[key] = JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson
for (const key in mbFeature?.properties) {
if (mbFeature?.properties.hasOwnProperty(key)) {
parsedProperties[key] =
typeof mbFeature.properties[key] === 'string' ||
typeof mbFeature.properties[key] === 'number' ||
typeof mbFeature.properties[key] === 'boolean'
? mbFeature.properties[key]
: JSON.parse(mbFeature.properties[key]); // mvt properties cannot be nested geojson
}
}
return {
type: 'Feature',
id: mbFeature.id,
geometry: mbFeature.geometry,
properties: parsedProperties,
} as TileMetaFeature;
try {
return {
type: 'Feature',
id: mbFeature?.id,
geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries
properties: parsedProperties,
} as TileMetaFeature;
} catch (e) {
return null;
}
});
return metaFeatures as TileMetaFeature[];
const filtered = metaFeatures.filter((f) => f !== null);
return filtered as TileMetaFeature[];
}
_requiresPrevSourceCleanup(mbMap: MbMap): boolean {
@ -317,8 +446,13 @@ export class TiledVectorLayer extends VectorLayer {
const mbLayer = mbMap.getLayer(layerIds[i]);
// The mapbox type in the spec is specified with `source-layer`
// but the programmable JS-object uses camelcase `sourceLayer`
// @ts-expect-error
if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) {
if (
mbLayer &&
// @ts-expect-error
mbLayer.sourceLayer !== tiledSourceMeta.layerName &&
// @ts-expect-error
mbLayer.sourceLayer !== ES_MVT_META_LAYER_NAME
) {
// If the source-pointer of one of the layers is stale, they will all be stale.
// In this case, all the mb-layers need to be removed and re-added.
return true;

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { assignFeatureIds } from './assign_feature_ids';
import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { assignFeatureIds, GEOJSON_FEATURE_ID_PROPERTY_NAME } from './assign_feature_ids';
import { FeatureCollection, Feature, Point } from 'geojson';
const featureId = 'myFeature1';
@ -34,7 +33,7 @@ test('should provide unique id when feature.id is not provided', () => {
expect(typeof feature1.id).toBe('number');
expect(typeof feature2.id).toBe('number');
// @ts-ignore
expect(feature1.id).toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]);
expect(feature1.id).toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]);
expect(feature1.id).not.toBe(feature2.id);
});
@ -53,9 +52,9 @@ test('should preserve feature id when provided', () => {
const feature1 = updatedFeatureCollection.features[0];
expect(typeof feature1.id).toBe('number');
// @ts-ignore
expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]);
expect(feature1.id).not.toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]);
// @ts-ignore
expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId);
expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(featureId);
});
test('should preserve feature id for falsy value', () => {
@ -73,9 +72,9 @@ test('should preserve feature id for falsy value', () => {
const feature1 = updatedFeatureCollection.features[0];
expect(typeof feature1.id).toBe('number');
// @ts-ignore
expect(feature1.id).not.toBe(feature1.properties[FEATURE_ID_PROPERTY_NAME]);
expect(feature1.id).not.toBe(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]);
// @ts-ignore
expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(0);
expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(0);
});
test('should not modify original feature properties', () => {
@ -94,6 +93,6 @@ test('should not modify original feature properties', () => {
const updatedFeatureCollection = assignFeatureIds(featureCollection);
const feature1 = updatedFeatureCollection.features[0];
// @ts-ignore
expect(feature1.properties[FEATURE_ID_PROPERTY_NAME]).toBe(featureId);
expect(featureProperties).not.toHaveProperty(FEATURE_ID_PROPERTY_NAME);
expect(feature1.properties[GEOJSON_FEATURE_ID_PROPERTY_NAME]).toBe(featureId);
expect(featureProperties).not.toHaveProperty(GEOJSON_FEATURE_ID_PROPERTY_NAME);
});

View file

@ -7,7 +7,11 @@
import _ from 'lodash';
import { FeatureCollection, Feature } from 'geojson';
import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { SOURCE_TYPES } from '../../../../common/constants';
import { IVectorSource } from '../../sources/vector_source';
export const GEOJSON_FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__';
export const ES_MVT_FEATURE_ID_PROPERTY_NAME = '_id';
let idCounter = 0;
@ -43,7 +47,7 @@ export function assignFeatureIds(featureCollection: FeatureCollection): FeatureC
geometry: feature.geometry, // do not copy geometry, this object can be massive
properties: {
// preserve feature id provided by source so features can be referenced across fetches
[FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id,
[GEOJSON_FEATURE_ID_PROPERTY_NAME]: feature.id == null ? numericId : feature.id,
// create new object for properties so original is not polluted with kibana internal props
...feature.properties,
},
@ -56,3 +60,13 @@ export function assignFeatureIds(featureCollection: FeatureCollection): FeatureC
features,
};
}
export function getFeatureId(feature: Feature, source: IVectorSource): string | number | undefined {
if (!source.isMvt()) {
return feature.properties?.[GEOJSON_FEATURE_ID_PROPERTY_NAME];
}
return source.getType() === SOURCE_TYPES.ES_SEARCH
? feature.properties?.[ES_MVT_FEATURE_ID_PROPERTY_NAME]
: feature.id;
}

View file

@ -44,35 +44,6 @@ test('should not create centroid feature for point and multipoint', () => {
expect(centroidFeatures.length).toBe(0);
});
test('should not create centroid for the metadata polygon', () => {
const polygonFeature: Feature = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[
[35, 10],
[45, 45],
[15, 40],
[10, 20],
[35, 10],
],
],
},
properties: {
__kbn_metadata_feature__: true,
prop0: 'value0',
prop1: 0.0,
},
};
const featureCollection: FeatureCollection = {
type: 'FeatureCollection',
features: [polygonFeature],
};
const centroidFeatures = getCentroidFeatures(featureCollection);
expect(centroidFeatures.length).toBe(0);
});
test('should create centroid feature for line (even number of points)', () => {
const lineFeature: Feature = {
type: 'Feature',

View file

@ -21,18 +21,13 @@ import turfArea from '@turf/area';
import turfCenterOfMass from '@turf/center-of-mass';
import turfLength from '@turf/length';
import { lineString, polygon } from '@turf/helpers';
import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE, KBN_METADATA_FEATURE } from './constants';
import { GEO_JSON_TYPE, KBN_IS_CENTROID_FEATURE } from '../../../../common/constants';
export function getCentroidFeatures(featureCollection: FeatureCollection): Feature[] {
const centroids = [];
for (let i = 0; i < featureCollection.features.length; i++) {
const feature = featureCollection.features[i];
// do not add centroid for kibana added features
if (feature.properties?.[KBN_METADATA_FEATURE]) {
continue;
}
const centroid = getCentroid(feature);
if (centroid) {
centroids.push(centroid);

View file

@ -13,3 +13,4 @@ export {
VectorLayerArguments,
NO_RESULTS_ICON_AND_TOOLTIPCONTENT,
} from './vector_layer';
export { getFeatureId } from './assign_feature_ids';

View file

@ -24,7 +24,7 @@ import { DataRequestContext } from '../../../actions';
import { IVectorSource } from '../../sources/vector_source';
import { DataRequestAbortError } from '../../util/data_request';
import { DataRequest } from '../../util/data_request';
import { getCentroidFeatures } from '../../../../common/get_centroid_features';
import { getCentroidFeatures } from './get_centroid_features';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { assignFeatureIds } from './assign_feature_ids';

View file

@ -21,20 +21,16 @@ import { AbstractLayer } from '../layer';
import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style';
import {
AGG_TYPE,
FEATURE_ID_PROPERTY_NAME,
SOURCE_META_DATA_REQUEST_ID,
SOURCE_FORMATTERS_DATA_REQUEST_ID,
FEATURE_VISIBLE_PROPERTY_NAME,
EMPTY_FEATURE_COLLECTION,
KBN_METADATA_FEATURE,
LAYER_TYPE,
FIELD_ORIGIN,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
FieldFormatter,
SOURCE_TYPES,
STYLE_TYPE,
SUPPORTS_FEATURE_EDITING_REQUEST_ID,
KBN_IS_TILE_COMPLETE,
VECTOR_STYLES,
} from '../../../../common/constants';
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
@ -46,7 +42,7 @@ import {
} from '../../util/can_skip_fetch';
import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds';
import {
getCentroidFilterExpression,
getLabelFilterExpression,
getFillFilterExpression,
getLineFilterExpression,
getPointFilterExpression,
@ -80,6 +76,7 @@ import { addGeoJsonMbSource, getVectorSourceBounds, syncVectorSource } from './u
import { JoinState, performInnerJoins } from './perform_inner_joins';
import { buildVectorRequestMeta } from '../build_vector_request_meta';
import { getJoinAggKey } from '../../../../common/get_agg_key';
import { getFeatureId } from './assign_feature_ids';
export function isVectorLayer(layer: ILayer) {
return (layer as IVectorLayer).canShowTooltip !== undefined;
@ -93,6 +90,12 @@ export interface VectorLayerArguments {
}
export interface IVectorLayer extends ILayer {
/*
* IVectorLayer.getMbLayerIds returns a list of mapbox layers assoicated with this layer for identifing features with tooltips.
* Must return ILayer.getMbLayerIds or a subset of ILayer.getMbLayerIds.
*/
getMbTooltipLayerIds(): string[];
getFields(): Promise<IField[]>;
getStyleEditorFields(): Promise<IField[]>;
getJoins(): InnerJoin[];
@ -118,6 +121,9 @@ export const NO_RESULTS_ICON_AND_TOOLTIPCONTENT = {
}),
};
/*
* Geojson vector layer
*/
export class VectorLayer extends AbstractLayer implements IVectorLayer {
static type = LAYER_TYPE.VECTOR;
@ -589,6 +595,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
timeFilters: nextMeta.timeFilters,
searchSessionId: dataFilters.searchSessionId,
});
stopLoading(dataRequestId, requestToken, styleMeta, nextMeta);
} catch (error) {
if (!(error instanceof DataRequestAbortError)) {
@ -774,6 +781,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
_getSourceFeatureCollection() {
if (this.getSource().isMvt()) {
return null;
}
const sourceDataRequest = this.getSourceDataRequest();
return sourceDataRequest ? (sourceDataRequest.getData() as FeatureCollection) : null;
}
@ -946,7 +956,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
const sourceId = this.getId();
const fillLayerId = this._getMbPolygonLayerId();
const lineLayerId = this._getMbLineLayerId();
const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId();
const hasJoins = this.hasJoins();
if (!mbMap.getLayer(fillLayerId)) {
@ -973,29 +982,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
mbMap.addLayer(mbLayer);
}
if (!mbMap.getLayer(tooManyFeaturesLayerId)) {
const mbLayer: MbLayer = {
id: tooManyFeaturesLayerId,
type: 'fill',
source: sourceId,
paint: {},
};
if (mvtSourceLayer) {
mbLayer['source-layer'] = mvtSourceLayer;
}
mbMap.addLayer(mbLayer);
mbMap.setFilter(tooManyFeaturesLayerId, [
'all',
['==', ['get', KBN_METADATA_FEATURE], true],
['==', ['get', KBN_IS_TILE_COMPLETE], false],
]);
mbMap.setPaintProperty(
tooManyFeaturesLayerId,
'fill-pattern',
KBN_TOO_MANY_FEATURES_IMAGE_ID
);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha());
}
this.getCurrentStyle().setMBPaintProperties({
alpha: this.getAlpha(),
@ -1017,21 +1003,18 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
}
this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId);
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
}
_setMbCentroidProperties(
_setMbLabelProperties(
mbMap: MbMap,
mvtSourceLayer?: string,
timesliceMaskConfig?: TimesliceMaskConfig
) {
const centroidLayerId = this._getMbCentroidLayerId();
const centroidLayer = mbMap.getLayer(centroidLayerId);
if (!centroidLayer) {
const labelLayerId = this._getMbLabelLayerId();
const labelLayer = mbMap.getLayer(labelLayerId);
if (!labelLayer) {
const mbLayer: MbLayer = {
id: centroidLayerId,
id: labelLayerId,
type: 'symbol',
source: this.getId(),
};
@ -1041,27 +1024,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
mbMap.addLayer(mbLayer);
}
const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig);
if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) {
mbMap.setFilter(centroidLayerId, filterExpr);
const isSourceGeoJson = !this.getSource().isMvt();
const filterExpr = getLabelFilterExpression(
this.hasJoins(),
isSourceGeoJson,
timesliceMaskConfig
);
if (!_.isEqual(filterExpr, mbMap.getFilter(labelLayerId))) {
mbMap.setFilter(labelLayerId, filterExpr);
}
this.getCurrentStyle().setMBPropertiesForLabelText({
alpha: this.getAlpha(),
mbMap,
textLayerId: centroidLayerId,
textLayerId: labelLayerId,
});
this.syncVisibilityWithMb(mbMap, centroidLayerId);
mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom());
this.syncVisibilityWithMb(mbMap, labelLayerId);
mbMap.setLayerZoomRange(labelLayerId, this.getMinZoom(), this.getMaxZoom());
}
_syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) {
const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice);
this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig);
this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig);
// centroid layers added after polygon layers to ensure they are on top of polygon layers
this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig);
// label layers added after geometry layers to ensure they are on top
this._setMbLabelProperties(mbMap, undefined, timesliceMaskConfig);
}
_getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined {
@ -1092,8 +1080,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return this.makeMbLayerId('text');
}
_getMbCentroidLayerId() {
return this.makeMbLayerId('centroid');
// _getMbTextLayerId is labels for Points and MultiPoints
// _getMbLabelLayerId is labels for not Points and MultiPoints
// _getMbLabelLayerId used to be called _getMbCentroidLayerId
// TODO merge textLayer and labelLayer into single layer
_getMbLabelLayerId() {
return this.makeMbLayerId('label');
}
_getMbSymbolLayerId() {
@ -1108,22 +1100,21 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
return this.makeMbLayerId('fill');
}
_getMbTooManyFeaturesLayerId() {
return this.makeMbLayerId('toomanyfeatures');
}
getMbLayerIds() {
getMbTooltipLayerIds() {
return [
this._getMbPointLayerId(),
this._getMbTextLayerId(),
this._getMbCentroidLayerId(),
this._getMbLabelLayerId(),
this._getMbSymbolLayerId(),
this._getMbLineLayerId(),
this._getMbPolygonLayerId(),
this._getMbTooManyFeaturesLayerId(),
];
}
getMbLayerIds() {
return this.getMbTooltipLayerIds();
}
ownsMbLayerId(mbLayerId: string) {
return this.getMbLayerIds().includes(mbLayerId);
}
@ -1170,7 +1161,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
}
const targetFeature = featureCollection.features.find((feature) => {
return feature.properties?.[FEATURE_ID_PROPERTY_NAME] === id;
return getFeatureId(feature, this.getSource()) === id;
});
return targetFeature ? targetFeature : null;
}

View file

@ -134,14 +134,14 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE
return valueAggsDsl;
}
async getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]> {
async getTooltipProperties(mbProperties: GeoJsonProperties): Promise<ITooltipProperty[]> {
const metricFields = await this.getFields();
const promises: Array<Promise<ITooltipProperty>> = [];
metricFields.forEach((metricField) => {
let value;
for (const key in properties) {
if (properties.hasOwnProperty(key) && metricField.getName() === key) {
value = properties[key];
for (const key in mbProperties) {
if (mbProperties.hasOwnProperty(key) && metricField.getMbFieldName() === key) {
value = mbProperties[key];
break;
}
}

View file

@ -1,73 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`resolution editor should add super-fine option 1`] = `
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Grid resolution"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "coarse",
"value": "COARSE",
},
Object {
"text": "fine",
"value": "FINE",
},
Object {
"text": "finest",
"value": "MOST_FINE",
},
Object {
"text": "super fine (beta)",
"value": "SUPER_FINE",
},
]
}
value="COARSE"
/>
</EuiFormRow>
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Grid resolution"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "coarse",
"value": "COARSE",
},
Object {
"text": "fine",
"value": "FINE",
},
Object {
"text": "finest",
"value": "MOST_FINE",
},
Object {
"text": "super fine",
"value": "SUPER_FINE",
},
]
}
value="COARSE"
/>
</EuiFormRow>
</Fragment>
`;
exports[`resolution editor should omit super-fine option 1`] = `
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Grid resolution"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "coarse",
"value": "COARSE",
},
Object {
"text": "fine",
"value": "FINE",
},
Object {
"text": "finest",
"value": "MOST_FINE",
},
]
}
value="COARSE"
/>
</EuiFormRow>
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Grid resolution"
labelType="label"
>
<EuiSelect
compressed={true}
onChange={[Function]}
options={
Array [
Object {
"text": "coarse",
"value": "COARSE",
},
Object {
"text": "fine",
"value": "FINE",
},
Object {
"text": "finest",
"value": "MOST_FINE",
},
]
}
value="COARSE"
/>
</EuiFormRow>
</Fragment>
`;

View file

@ -19,9 +19,9 @@ exports[`source editor geo_grid_source default vector layer config should allow
/>
<MetricsEditor
allowMultipleMetrics={true}
fields={null}
fields={Array []}
key="12345"
metrics={Array []}
metricsFilter={null}
onChange={[Function]}
/>
</EuiPanel>
@ -45,6 +45,7 @@ exports[`source editor geo_grid_source default vector layer config should allow
/>
<ResolutionEditor
includeSuperFine={true}
metrics={Array []}
onChange={[Function]}
resolution="COARSE"
/>
@ -79,7 +80,8 @@ exports[`source editor geo_grid_source should put limitations based on heatmap-r
/>
<MetricsEditor
allowMultipleMetrics={false}
fields={null}
fields={Array []}
key="12345"
metrics={Array []}
metricsFilter={[Function]}
onChange={[Function]}
@ -105,6 +107,7 @@ exports[`source editor geo_grid_source should put limitations based on heatmap-r
/>
<ResolutionEditor
includeSuperFine={false}
metrics={Array []}
onChange={[Function]}
resolution="COARSE"
/>

View file

@ -6,7 +6,7 @@
*/
import { Feature } from 'geojson';
import { RENDER_AS } from '../constants';
import { RENDER_AS } from '../../../../common/constants';
export function convertCompositeRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[];
export function convertRegularRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[];

View file

@ -6,10 +6,13 @@
*/
import _ from 'lodash';
import { RENDER_AS, GEOTILE_GRID_AGG_NAME, GEOCENTROID_AGG_NAME } from '../constants';
import { getTileBoundingBox } from '../geo_tile_utils';
import { extractPropertiesFromBucket } from './es_agg_utils';
import { clamp } from './elasticsearch_geo_utils';
import {
RENDER_AS,
GEOTILE_GRID_AGG_NAME,
GEOCENTROID_AGG_NAME,
} from '../../../../common/constants';
import { getTileBoundingBox } from '../../util/geo_tile_utils';
import { clamp, extractPropertiesFromBucket } from '../../../../common/elasticsearch_util';
const GRID_BUCKET_KEYS_TO_IGNORE = ['key', GEOCENTROID_AGG_NAME];

View file

@ -7,7 +7,7 @@
// @ts-ignore
import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson';
import { RENDER_AS } from '../constants';
import { RENDER_AS } from '../../../../common/constants';
describe('convertCompositeRespToGeoJson', () => {
const esResponse = {

View file

@ -296,7 +296,7 @@ describe('ESGeoGridSource', () => {
);
it('getLayerName', () => {
expect(mvtGeogridSource.getLayerName()).toBe('source_layer');
expect(mvtGeogridSource.getLayerName()).toBe('aggs');
});
it('getMinZoom', () => {
@ -312,28 +312,13 @@ describe('ESGeoGridSource', () => {
vectorSourceRequestMeta
);
expect(urlTemplateWithMeta.layerName).toBe('source_layer');
expect(urlTemplateWithMeta.layerName).toBe('aggs');
expect(urlTemplateWithMeta.minSourceZoom).toBe(0);
expect(urlTemplateWithMeta.maxSourceZoom).toBe(24);
expect(urlTemplateWithMeta.urlTemplate).toEqual(
"rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
"rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':())))&requestType=heatmap"
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta({
...vectorSourceRequestMeta,
searchSessionId: '1',
});
expect(
urlTemplateWithMeta.urlTemplate.startsWith(
"rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1"
)
).toBe(true);
expect(urlTemplateWithMeta.urlTemplate.endsWith('&searchSessionId=1')).toBe(true);
});
});
describe('Gold+ usage', () => {

View file

@ -11,12 +11,8 @@ import { i18n } from '@kbn/i18n';
import rison from 'rison-node';
import { Feature } from 'geojson';
import type { estypes } from '@elastic/elasticsearch';
import {
convertCompositeRespToGeoJson,
convertRegularRespToGeoJson,
makeESBbox,
} from '../../../../common/elasticsearch_util';
// @ts-expect-error
import { makeESBbox } from '../../../../common/elasticsearch_util';
import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson';
import { UpdateSourceEditor } from './update_source_editor';
import {
DEFAULT_MAX_BUCKETS_LIMIT,
@ -26,7 +22,6 @@ import {
GIS_API_PATH,
GRID_RESOLUTION,
MVT_GETGRIDTILE_API_PATH,
MVT_SOURCE_LAYER_NAME,
MVT_TOKEN_PARAM_NAME,
RENDER_AS,
SOURCE_TYPES,
@ -55,6 +50,8 @@ import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/
type ESGeoGridSourceSyncMeta = Pick<ESGeoGridSourceDescriptor, 'requestType'>;
const ES_MVT_AGGS_LAYER_NAME = 'aggs';
export const MAX_GEOTILE_LEVEL = 29;
export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', {
@ -140,6 +137,10 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
];
}
isMvt() {
return this._descriptor.resolution === GRID_RESOLUTION.SUPER_FINE;
}
getFieldNames() {
return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName());
}
@ -305,8 +306,8 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
_addNonCompositeAggsToSearchSource(
searchSource: ISearchSource,
indexPattern: IndexPattern,
precision: number | null,
bufferedExtent?: MapExtent | null
precision: number,
bufferedExtent?: MapExtent
) {
searchSource.setField('aggs', {
[GEOTILE_GRID_AGG_NAME]: {
@ -419,7 +420,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
}
getLayerName(): string {
return MVT_SOURCE_LAYER_NAME;
return ES_MVT_AGGS_LAYER_NAME;
}
async getUrlTemplateWithMeta(
@ -427,14 +428,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
): Promise<ITiledSingleLayerMvtParams> {
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
this._addNonCompositeAggsToSearchSource(
searchSource,
indexPattern,
null, // needs to be set server-side
null // needs to be stripped server-side
);
searchSource.setField('aggs', this.getValueAggsDsl(indexPattern));
const dsl = searchSource.getSearchRequestBody();
const risonDsl = rison.encode(dsl);
@ -443,22 +437,18 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
`/${GIS_API_PATH}/${MVT_GETGRIDTILE_API_PATH}/{z}/{x}/{y}.pbf`
);
const geoField = await this._getGeoField();
const urlTemplate = `${mvtUrlServicePath}\
?geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&requestBody=${risonDsl}\
&requestType=${this._descriptor.requestType}\
&geoFieldType=${geoField.type}`;
&requestType=${this._descriptor.requestType}`;
return {
refreshTokenParamName: MVT_TOKEN_PARAM_NAME,
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate: searchFilters.searchSessionId
? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}`
: urlTemplate,
urlTemplate,
};
}

View file

@ -1,61 +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 { GRID_RESOLUTION } from '../../../../common/constants';
import { EuiSelect, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const BASE_OPTIONS = [
{
value: GRID_RESOLUTION.COARSE,
text: i18n.translate('xpack.maps.source.esGrid.coarseDropdownOption', {
defaultMessage: 'coarse',
}),
},
{
value: GRID_RESOLUTION.FINE,
text: i18n.translate('xpack.maps.source.esGrid.fineDropdownOption', {
defaultMessage: 'fine',
}),
},
{
value: GRID_RESOLUTION.MOST_FINE,
text: i18n.translate('xpack.maps.source.esGrid.finestDropdownOption', {
defaultMessage: 'finest',
}),
},
];
export function ResolutionEditor({ resolution, onChange, includeSuperFine }) {
const options = [...BASE_OPTIONS];
if (includeSuperFine) {
options.push({
value: GRID_RESOLUTION.SUPER_FINE,
text: i18n.translate('xpack.maps.source.esGrid.superFineDropDownOption', {
defaultMessage: 'super fine (beta)',
}),
});
}
return (
<EuiFormRow
label={i18n.translate('xpack.maps.geoGrid.resolutionLabel', {
defaultMessage: 'Grid resolution',
})}
display="columnCompressed"
>
<EuiSelect
options={options}
value={resolution}
onChange={(e) => onChange(e.target.value)}
compressed
/>
</EuiFormRow>
);
}

View file

@ -8,7 +8,6 @@
import React from 'react';
import { shallow } from 'enzyme';
// @ts-expect-error
import { ResolutionEditor } from './resolution_editor';
import { GRID_RESOLUTION } from '../../../../common/constants';
@ -16,6 +15,7 @@ const defaultProps = {
resolution: GRID_RESOLUTION.COARSE,
onChange: () => {},
includeSuperFine: false,
metrics: [],
};
describe('resolution editor', () => {

View file

@ -0,0 +1,161 @@
/*
* 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, { ChangeEvent, Component } from 'react';
import { EuiConfirmModal, EuiSelect, EuiFormRow } 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';
const BASE_OPTIONS = [
{
value: GRID_RESOLUTION.COARSE,
text: i18n.translate('xpack.maps.source.esGrid.coarseDropdownOption', {
defaultMessage: 'coarse',
}),
},
{
value: GRID_RESOLUTION.FINE,
text: i18n.translate('xpack.maps.source.esGrid.fineDropdownOption', {
defaultMessage: 'fine',
}),
},
{
value: GRID_RESOLUTION.MOST_FINE,
text: i18n.translate('xpack.maps.source.esGrid.finestDropdownOption', {
defaultMessage: 'finest',
}),
},
];
function isUnsupportedVectorTileMetric(metric: AggDescriptor) {
return metric.type === AGG_TYPE.TERMS;
}
interface Props {
includeSuperFine: boolean;
resolution: GRID_RESOLUTION;
onChange: (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => void;
metrics: AggDescriptor[];
}
interface State {
showModal: boolean;
}
export class ResolutionEditor extends Component<Props, State> {
private readonly _options = [...BASE_OPTIONS];
constructor(props: Props) {
super(props);
this.state = {
showModal: false,
};
if (props.includeSuperFine) {
this._options.push({
value: GRID_RESOLUTION.SUPER_FINE,
text: i18n.translate('xpack.maps.source.esGrid.superFineDropDownOption', {
defaultMessage: 'super fine',
}),
});
}
}
_onResolutionChange = (e: ChangeEvent<HTMLSelectElement>) => {
const resolution = e.target.value as GRID_RESOLUTION;
if (resolution === GRID_RESOLUTION.SUPER_FINE) {
const hasUnsupportedMetrics = this.props.metrics.find(isUnsupportedVectorTileMetric);
if (hasUnsupportedMetrics) {
this.setState({ showModal: true });
return;
}
}
this.props.onChange(resolution, this.props.metrics);
};
_closeModal = () => {
this.setState({
showModal: false,
});
};
_acceptModal = () => {
this._closeModal();
const supportedMetrics = this.props.metrics.filter((metric) => {
return !isUnsupportedVectorTileMetric(metric);
});
this.props.onChange(
GRID_RESOLUTION.SUPER_FINE,
supportedMetrics.length ? supportedMetrics : [{ type: AGG_TYPE.COUNT }]
);
};
_renderModal() {
return this.state.showModal ? (
<EuiConfirmModal
title={i18n.translate('xpack.maps.source.esGrid.vectorTileModal.title', {
defaultMessage: `'Top terms' metrics not supported`,
})}
onCancel={this._closeModal}
onConfirm={this._acceptModal}
cancelButtonText={i18n.translate(
'xpack.maps.source.esGrid.vectorTileModal.cancelBtnLabel',
{
defaultMessage: 'Cancel',
}
)}
confirmButtonText={i18n.translate(
'xpack.maps.source.esGrid.vectorTileModal.confirmBtnLabel',
{
defaultMessage: 'Accept',
}
)}
buttonColor="danger"
defaultFocusedButton="cancel"
>
<p>
<FormattedMessage
id="xpack.maps.source.esGrid.vectorTileModal.message"
defaultMessage="Super fine grid resolution uses vector tiles from the Elasticsearch vector tile API. Elasticsearch vector tile API does not support 'Top terms' metric. Switching to super fine grid resolution will remove all 'Top terms' metrics from your layer configuration."
/>
</p>
</EuiConfirmModal>
) : null;
}
render() {
const helpText =
this.props.resolution === GRID_RESOLUTION.SUPER_FINE
? i18n.translate('xpack.maps.source.esGrid.superFineHelpText', {
defaultMessage: 'Super fine grid resolution uses vector tiles.',
})
: undefined;
return (
<>
{this._renderModal()}
<EuiFormRow
label={i18n.translate('xpack.maps.geoGrid.resolutionLabel', {
defaultMessage: 'Grid resolution',
})}
helpText={helpText}
display="columnCompressed"
>
<EuiSelect
options={this._options}
value={this.props.resolution}
onChange={this._onResolutionChange}
compressed
/>
</EuiFormRow>
</>
);
}
}

View file

@ -8,14 +8,19 @@
import React from 'react';
import { shallow } from 'enzyme';
// @ts-expect-error
import { UpdateSourceEditor } from './update_source_editor';
import { GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants';
jest.mock('uuid/v4', () => {
return function () {
return '12345';
};
});
const defaultProps = {
currentLayerType: LAYER_TYPE.VECTOR,
indexPatternId: 'foobar',
onChange: () => {},
onChange: async () => {},
metrics: [],
renderAs: RENDER_AS.POINT,
resolution: GRID_RESOLUTION.COARSE,

View file

@ -7,20 +7,40 @@
import React, { Fragment, Component } from 'react';
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 { GRID_RESOLUTION, LAYER_TYPE } from '../../../../common/constants';
import { AGG_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';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { isMetricCountable } from '../../util/is_metric_countable';
import { indexPatterns } from '../../../../../../../src/plugins/data/public';
import { IndexPatternField, indexPatterns } from '../../../../../../../src/plugins/data/public';
import { RenderAsSelect } from './render_as_select';
import { AggDescriptor } from '../../../../common/descriptor_types';
import { OnSourceChangeArgs } from '../source';
export class UpdateSourceEditor extends Component {
state = {
fields: null,
interface Props {
currentLayerType?: string;
indexPatternId: string;
onChange: (...args: OnSourceChangeArgs[]) => Promise<void>;
metrics: AggDescriptor[];
renderAs: RENDER_AS;
resolution: GRID_RESOLUTION;
}
interface State {
metricsEditorKey: string;
fields: IndexPatternField[];
loadError?: string;
}
export class UpdateSourceEditor extends Component<Props, State> {
private _isMounted?: boolean;
state: State = {
fields: [],
metricsEditorKey: uuid(),
};
componentDidMount() {
@ -54,11 +74,11 @@ export class UpdateSourceEditor extends Component {
});
}
_onMetricsChange = (metrics) => {
_onMetricsChange = (metrics: AggDescriptor[]) => {
this.props.onChange({ propName: 'metrics', value: metrics });
};
_onResolutionChange = (resolution) => {
_onResolutionChange = async (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => {
let newLayerType;
if (
this.props.currentLayerType === LAYER_TYPE.VECTOR ||
@ -76,22 +96,36 @@ export class UpdateSourceEditor extends Component {
throw new Error('Unexpected layer-type');
}
this.props.onChange({ propName: 'resolution', value: resolution, newLayerType });
await this.props.onChange(
{ propName: 'metrics', value: metrics },
{ propName: 'resolution', value: resolution, newLayerType }
);
// Metrics editor persists metrics in state.
// Reset metricsEditorKey to force new instance and new internal state with latest metrics
this.setState({ metricsEditorKey: uuid() });
};
_onRequestTypeSelect = (requestType) => {
_onRequestTypeSelect = (requestType: RENDER_AS) => {
this.props.onChange({ propName: 'requestType', value: requestType });
};
_getMetricsFilter() {
if (this.props.currentLayerType === LAYER_TYPE.HEATMAP) {
return (metric: EuiComboBoxOptionOption<AGG_TYPE>) => {
// these are countable metrics, where blending heatmap color blobs make sense
return metric.value ? isMetricCountable(metric.value) : false;
};
}
if (this.props.resolution === GRID_RESOLUTION.SUPER_FINE) {
return (metric: EuiComboBoxOptionOption<AGG_TYPE>) => {
return metric.value !== AGG_TYPE.TERMS;
};
}
}
_renderMetricsPanel() {
const metricsFilter =
this.props.currentLayerType === LAYER_TYPE.HEATMAP
? (metric) => {
//these are countable metrics, where blending heatmap color blobs make sense
return isMetricCountable(metric.value);
}
: null;
const allowMultipleMetrics = this.props.currentLayerType !== LAYER_TYPE.HEATMAP;
return (
<EuiPanel>
<EuiTitle size="xs">
@ -101,8 +135,9 @@ export class UpdateSourceEditor extends Component {
</EuiTitle>
<EuiSpacer size="m" />
<MetricsEditor
allowMultipleMetrics={allowMultipleMetrics}
metricsFilter={metricsFilter}
key={this.state.metricsEditorKey}
allowMultipleMetrics={this.props.currentLayerType !== LAYER_TYPE.HEATMAP}
metricsFilter={this._getMetricsFilter()}
fields={this.state.fields}
metrics={this.props.metrics}
onChange={this._onMetricsChange}
@ -131,6 +166,7 @@ export class UpdateSourceEditor extends Component {
includeSuperFine={this.props.currentLayerType !== LAYER_TYPE.HEATMAP}
resolution={this.props.resolution}
onChange={this._onResolutionChange}
metrics={this.props.metrics}
/>
<RenderAsSelect
isColumnCompressed

View file

@ -155,6 +155,8 @@ export class CreateSourceEditor extends Component {
)
: null
}
hasJoins={false}
clearJoins={() => {}}
/>
</Fragment>
);

View file

@ -31,7 +31,7 @@ describe('ESSearchSource', () => {
const esSearchSource = new ESSearchSource(mockDescriptor);
expect(esSearchSource.getMinZoom()).toBe(0);
expect(esSearchSource.getMaxZoom()).toBe(24);
expect(esSearchSource.getLayerName()).toBe('source_layer');
expect(esSearchSource.getLayerName()).toBe('hits');
});
describe('getUrlTemplateWithMeta', () => {
@ -117,21 +117,7 @@ describe('ESSearchSource', () => {
});
const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters);
expect(urlTemplateWithMeta.urlTemplate).toBe(
`rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape`
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
const esSearchSource = new ESSearchSource({
geoField: geoFieldName,
indexPatternId: 'ipId',
});
const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta({
...searchFilters,
searchSessionId: '1',
});
expect(urlTemplateWithMeta.urlTemplate).toBe(
`rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape&searchSessionId=1`
`rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))`
);
});
});
@ -162,7 +148,7 @@ describe('ESSearchSource', () => {
scalingType: SCALING_TYPES.MVT,
});
expect(esSearchSource.getJoinsDisabledReason()).toBe(
'Joins are not supported when scaling by mvt vector tiles'
'Joins are not supported when scaling by vector tiles'
);
});
});

View file

@ -9,8 +9,8 @@ import _ from 'lodash';
import React, { ReactElement } from 'react';
import rison from 'rison-node';
import { i18n } from '@kbn/i18n';
import type { Filter, IndexPatternField, IndexPattern } from 'src/plugins/data/public';
import { GeoJsonProperties, Geometry, Position } from 'geojson';
import type { Filter, IndexPatternField, IndexPattern } from 'src/plugins/data/public';
import { esFilters } from '../../../../../../../src/plugins/data/public';
import { AbstractESSource } from '../es_source';
import {
@ -35,7 +35,6 @@ import {
FIELD_ORIGIN,
GIS_API_PATH,
MVT_GETTILE_API_PATH,
MVT_SOURCE_LAYER_NAME,
MVT_TOKEN_PARAM_NAME,
SCALING_TYPES,
SOURCE_TYPES,
@ -54,14 +53,17 @@ import {
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters';
import { TimeRange } from '../../../../../../../src/plugins/data/common';
import {
SortDirection,
SortDirectionNumeric,
TimeRange,
} from '../../../../../../../src/plugins/data/common';
import { ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { IField } from '../../fields/field';
import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source';
import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
import { DataRequest } from '../../util/data_request';
import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common';
import { isValidStringConfig } from '../../util/valid_string_config';
import { TopHitsUpdateSourceEditor } from './top_hits';
import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields';
@ -83,6 +85,8 @@ type ESSearchSourceSyncMeta = Pick<
| 'topHitsSize'
>;
const ES_MVT_HITS_LAYER_NAME = 'hits';
export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined {
const timeRangeBounds = getTimeFilter().calculateBounds(timerange);
return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined
@ -185,6 +189,8 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
sortOrder={this._descriptor.sortOrder}
scalingType={this._descriptor.scalingType}
filterByMapBounds={this.isFilterByMapBounds()}
hasJoins={sourceEditorArgs.hasJoins}
clearJoins={sourceEditorArgs.clearJoins}
/>
);
}
@ -211,6 +217,10 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
return [this._descriptor.geoField];
}
isMvt() {
return this._descriptor.scalingType === SCALING_TYPES.MVT;
}
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
let indexPatternName = this.getIndexPatternId();
let geoFieldType = '';
@ -748,7 +758,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
});
} else if (this._descriptor.scalingType === SCALING_TYPES.MVT) {
reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReasonMvt', {
defaultMessage: 'Joins are not supported when scaling by mvt vector tiles',
defaultMessage: 'Joins are not supported when scaling by vector tiles',
});
} else {
reason = null;
@ -757,7 +767,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
}
getLayerName(): string {
return MVT_SOURCE_LAYER_NAME;
return ES_MVT_HITS_LAYER_NAME;
}
async _getEditableIndex(): Promise<string> {
@ -828,22 +838,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
`/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}/{z}/{x}/{y}.pbf`
);
const geoField = await this._getGeoField();
const urlTemplate = `${mvtUrlServicePath}\
?geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&requestBody=${risonDsl}\
&geoFieldType=${geoField.type}`;
&requestBody=${risonDsl}`;
return {
refreshTokenParamName: MVT_TOKEN_PARAM_NAME,
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate: searchFilters.searchSessionId
? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}`
: urlTemplate,
urlTemplate,
};
}

View file

@ -35,6 +35,8 @@ export class UpdateSourceEditor extends Component {
sortOrder: PropTypes.string.isRequired,
scalingType: PropTypes.string.isRequired,
source: PropTypes.object,
hasJoins: PropTypes.bool.isRequired,
clearJoins: PropTypes.func.isRequired,
};
state = {
@ -205,6 +207,8 @@ export class UpdateSourceEditor extends Component {
scalingType={this.props.scalingType}
supportsClustering={this.state.supportsClustering}
clusteringDisabledReason={this.state.clusteringDisabledReason}
hasJoins={this.props.hasJoins}
clearJoins={this.props.clearJoins}
/>
</EuiPanel>
);

View file

@ -46,17 +46,7 @@ exports[`scaling form should disable clusters option when clustering is not supp
/>
</EuiToolTip>
<EuiToolTip
content={
<React.Fragment>
<EuiBetaBadge
label="beta"
/>
<EuiHorizontalRule
margin="xs"
/>
Use vector tiles for faster display of large datasets.
</React.Fragment>
}
content="Use vector tiles for faster display of large datasets."
delay="regular"
display="inlineBlock"
position="left"
@ -127,17 +117,7 @@ exports[`scaling form should render 1`] = `
onChange={[Function]}
/>
<EuiToolTip
content={
<React.Fragment>
<EuiBetaBadge
label="beta"
/>
<EuiHorizontalRule
margin="xs"
/>
Use vector tiles for faster display of large datasets.
</React.Fragment>
}
content="Use vector tiles for faster display of large datasets."
delay="regular"
display="inlineBlock"
position="left"

View file

@ -26,6 +26,8 @@ const defaultProps = {
scalingType: SCALING_TYPES.LIMIT,
supportsClustering: true,
termFields: [],
hasJoins: false,
clearJoins: () => {},
};
describe('scaling form', () => {

View file

@ -7,15 +7,14 @@
import React, { Component, Fragment } from 'react';
import {
EuiConfirmModal,
EuiFormRow,
EuiHorizontalRule,
EuiRadio,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
EuiTitle,
EuiToolTip,
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -35,15 +34,20 @@ interface Props {
scalingType: SCALING_TYPES;
supportsClustering: boolean;
clusteringDisabledReason?: string | null;
hasJoins: boolean;
clearJoins: () => void;
}
interface State {
nextScalingType?: SCALING_TYPES;
maxResultWindow: string;
showModal: boolean;
}
export class ScalingForm extends Component<Props, State> {
state = {
state: State = {
maxResultWindow: DEFAULT_MAX_RESULT_WINDOW.toLocaleString(),
showModal: false,
};
_isMounted = false;
@ -68,7 +72,15 @@ export class ScalingForm extends Component<Props, State> {
}
}
_onScalingTypeChange = (optionId: string): void => {
_onScalingTypeSelect = (optionId: SCALING_TYPES): void => {
if (this.props.hasJoins && optionId !== SCALING_TYPES.LIMIT) {
this._openModal(optionId);
} else {
this._onScalingTypeChange(optionId);
}
};
_onScalingTypeChange = (optionId: SCALING_TYPES): void => {
let layerType;
if (optionId === SCALING_TYPES.CLUSTERS) {
layerType = LAYER_TYPE.BLENDED_VECTOR;
@ -85,6 +97,69 @@ export class ScalingForm extends Component<Props, State> {
this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked });
};
_openModal = (optionId: SCALING_TYPES) => {
this.setState({
nextScalingType: optionId,
showModal: true,
});
};
_closeModal = () => {
this.setState({
nextScalingType: undefined,
showModal: false,
});
};
_acceptModal = () => {
this.props.clearJoins();
this._onScalingTypeChange(this.state.nextScalingType!);
this._closeModal();
};
_renderModal() {
if (!this.state.showModal || this.state.nextScalingType === undefined) {
return null;
}
const scalingOptionLabel =
this.state.nextScalingType === SCALING_TYPES.CLUSTERS
? i18n.translate('xpack.maps.source.esSearch.scalingModal.clusters', {
defaultMessage: `clusters`,
})
: i18n.translate('xpack.maps.source.esSearch.scalingModal.vectorTiles', {
defaultMessage: `vector tiles`,
});
return (
<EuiConfirmModal
title={i18n.translate('xpack.maps.source.esSearch.scalingModal.title', {
defaultMessage: `Term joins not supported`,
})}
onCancel={this._closeModal}
onConfirm={this._acceptModal}
cancelButtonText={i18n.translate('xpack.maps.source.esSearch.scalingModal.cancelBtnLabel', {
defaultMessage: 'Cancel',
})}
confirmButtonText={i18n.translate(
'xpack.maps.source.esSearch.scalingModal.confirmBtnLabel',
{
defaultMessage: 'Accept',
}
)}
buttonColor="danger"
defaultFocusedButton="cancel"
>
<p>
<FormattedMessage
id="xpack.maps.source.esSearch.scalingModal.message"
defaultMessage="Scaling option {scalingOptionLabel} does not support term joins. Switching to {scalingOptionLabel} will remove all term joins from your layer configuration."
values={{ scalingOptionLabel }}
/>
</p>
</EuiConfirmModal>
);
}
_renderClusteringRadio() {
const clusteringRadio = (
<EuiRadio
@ -94,7 +169,7 @@ export class ScalingForm extends Component<Props, State> {
values: { maxResultWindow: this.state.maxResultWindow },
})}
checked={this.props.scalingType === SCALING_TYPES.CLUSTERS}
onChange={() => this._onScalingTypeChange(SCALING_TYPES.CLUSTERS)}
onChange={() => this._onScalingTypeSelect(SCALING_TYPES.CLUSTERS)}
disabled={!this.props.supportsClustering}
/>
);
@ -108,36 +183,6 @@ export class ScalingForm extends Component<Props, State> {
);
}
_renderMVTRadio() {
const labelText = i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', {
defaultMessage: 'Use vector tiles',
});
const mvtRadio = (
<EuiRadio
id={SCALING_TYPES.MVT}
label={labelText}
checked={this.props.scalingType === SCALING_TYPES.MVT}
onChange={() => this._onScalingTypeChange(SCALING_TYPES.MVT)}
/>
);
const enabledInfo = (
<>
<EuiBetaBadge label={'beta'} />
<EuiHorizontalRule margin="xs" />
{i18n.translate('xpack.maps.source.esSearch.mvtDescription', {
defaultMessage: 'Use vector tiles for faster display of large datasets.',
})}
</>
);
return (
<EuiToolTip position="left" content={enabledInfo}>
{mvtRadio}
</EuiToolTip>
);
}
render() {
let filterByBoundsSwitch;
if (this.props.scalingType === SCALING_TYPES.LIMIT) {
@ -157,6 +202,7 @@ export class ScalingForm extends Component<Props, State> {
return (
<Fragment>
{this._renderModal()}
<EuiTitle size="xs">
<h5>
<FormattedMessage id="xpack.maps.esSearch.scaleTitle" defaultMessage="Scaling" />
@ -174,10 +220,24 @@ export class ScalingForm extends Component<Props, State> {
values: { maxResultWindow: this.state.maxResultWindow },
})}
checked={this.props.scalingType === SCALING_TYPES.LIMIT}
onChange={() => this._onScalingTypeChange(SCALING_TYPES.LIMIT)}
onChange={() => this._onScalingTypeSelect(SCALING_TYPES.LIMIT)}
/>
{this._renderClusteringRadio()}
{this._renderMVTRadio()}
<EuiToolTip
position="left"
content={i18n.translate('xpack.maps.source.esSearch.mvtDescription', {
defaultMessage: 'Use vector tiles for faster display of large datasets.',
})}
>
<EuiRadio
id={SCALING_TYPES.MVT}
label={i18n.translate('xpack.maps.source.esSearch.useMVTVectorTiles', {
defaultMessage: 'Use vector tiles',
})}
checked={this.props.scalingType === SCALING_TYPES.MVT}
onChange={() => this._onScalingTypeSelect(SCALING_TYPES.MVT)}
/>
</EuiToolTip>
</div>
</EuiFormRow>

View file

@ -20,7 +20,7 @@ import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters';
import { createExtentFilter } from '../../../../common/elasticsearch_util';
import { copyPersistentState } from '../../../reducers/copy_persistent_state';
import { DataRequestAbortError } from '../../util/data_request';
import { expandToTileBoundaries } from '../../../../common/geo_tile_utils';
import { expandToTileBoundaries } from '../../util/geo_tile_utils';
import { IVectorSource } from '../vector_source';
import { TimeRange } from '../../../../../../../src/plugins/data/common';
import {

View file

@ -5,7 +5,6 @@ exports[`should render error for dupes 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
@ -88,7 +87,6 @@ exports[`should render error for dupes 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="1"
>
<EuiFlexItem>
<EuiFieldText
@ -196,7 +194,6 @@ exports[`should render error for empty name 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
@ -304,7 +301,6 @@ exports[`should render field editor 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
@ -387,7 +383,6 @@ exports[`should render field editor 1`] = `
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="1"
>
<EuiFlexItem>
<EuiFieldText

View file

@ -178,14 +178,14 @@ export class MVTFieldConfigEditor extends Component<Props, State> {
_renderFieldConfig() {
return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => {
return (
<>
<EuiFlexGroup key={index} gutterSize="xs" alignItems="center">
<Fragment key={index}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem>{this._renderFieldNameInput(mvtFieldConfig, index)}</EuiFlexItem>
<EuiFlexItem>{this._renderFieldTypeDropDown(mvtFieldConfig, index)}</EuiFlexItem>
<EuiFlexItem grow={false}>{this._renderFieldButtonDelete(index)}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={'xs'} />
</>
</Fragment>
);
});
}

View file

@ -82,6 +82,10 @@ export class MVTSingleLayerVectorSource
.filter((f) => f !== null) as MVTField[];
}
isMvt() {
return true;
}
async supportsFitToBounds() {
return false;
}

View file

@ -29,8 +29,10 @@ export type OnSourceChangeArgs = {
};
export type SourceEditorArgs = {
onChange: (...args: OnSourceChangeArgs[]) => void;
currentLayerType?: string;
clearJoins: () => void;
currentLayerType: string;
hasJoins: boolean;
onChange: (...args: OnSourceChangeArgs[]) => Promise<void>;
};
export type ImmutableSourceProperty = {
@ -43,6 +45,7 @@ export interface ISource {
destroy(): void;
getDisplayName(): Promise<string>;
getInspectorAdapters(): Adapters | undefined;
getType(): string;
isFieldAware(): boolean;
isFilterByMapBounds(): boolean;
isGeoGridPrecisionAware(): boolean;
@ -101,6 +104,10 @@ export class AbstractSource implements ISource {
return this._inspectorAdapters;
}
getType(): string {
return this._descriptor.type;
}
async getDisplayName(): Promise<string> {
return '';
}

View file

@ -9,7 +9,7 @@ import type { Query } from 'src/plugins/data/common';
import { FeatureCollection, GeoJsonProperties, Geometry, Position } from 'geojson';
import { Filter, TimeRange } from 'src/plugins/data/public';
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property';
import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property';
import { AbstractSource, ISource } from '../source';
import { IField } from '../../fields/field';
import {
@ -44,6 +44,7 @@ export interface BoundsRequestMeta {
}
export interface IVectorSource extends ISource {
isMvt(): boolean;
getTooltipProperties(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
layerDataFilters: BoundsRequestMeta,
@ -89,6 +90,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
return [];
}
isMvt() {
return false;
}
createField({ fieldName }: { fieldName: string }): IField {
throw new Error('Not implemented');
}

View file

@ -384,546 +384,6 @@ exports[`should render 1`] = `
</Fragment>
`;
exports[`should render line-style with label properties when ES-source is rendered as mvt 1`] = `
<Fragment>
<VectorStyleColorEditor
defaultDynamicStyleOptions={
Object {
"color": "Blues",
"colorCategory": "palette_0",
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
}
}
defaultStaticStyleOptions={
Object {
"color": "#41937c",
}
}
disabled={false}
disabledBy="lineWidth"
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticColorProperty {
"_options": Object {
"color": "#41937c",
},
"_styleName": "lineColor",
}
}
swatches={
Array [
"#41937c",
"#4379aa",
"#c83868",
"#7751a4",
"#ba6b95",
"#c9ad31",
"#a69168",
"#c57127",
"#885145",
"#e1401f",
"#000",
"#FFF",
]
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleSizeEditor
defaultDynamicStyleOptions={
Object {
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
"maxSize": 10,
"minSize": 1,
}
}
defaultStaticStyleOptions={
Object {
"size": 1,
}
}
disabled={false}
disabledBy="iconSize"
fields={Array []}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticSizeProperty {
"_options": Object {
"size": 1,
},
"_styleName": "lineWidth",
}
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleLabelEditor
defaultDynamicStyleOptions={
Object {
"field": undefined,
}
}
defaultStaticStyleOptions={
Object {
"value": "",
}
}
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticTextProperty {
"_options": Object {
"value": "",
},
"_styleName": "labelText",
}
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleColorEditor
defaultDynamicStyleOptions={
Object {
"color": "Blues",
"colorCategory": "palette_0",
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
}
}
defaultStaticStyleOptions={
Object {
"color": "#000000",
}
}
disabled={true}
disabledBy="labelText"
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticColorProperty {
"_options": Object {
"color": "#000000",
},
"_styleName": "labelColor",
}
}
swatches={
Array [
"#41937c",
"#4379aa",
"#c83868",
"#7751a4",
"#ba6b95",
"#c9ad31",
"#a69168",
"#c57127",
"#885145",
"#e1401f",
"#000",
"#FFF",
]
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleSizeEditor
defaultDynamicStyleOptions={
Object {
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
"maxSize": 32,
"minSize": 7,
}
}
defaultStaticStyleOptions={
Object {
"size": 14,
}
}
disabled={true}
disabledBy="labelText"
fields={Array []}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticSizeProperty {
"_options": Object {
"size": 14,
},
"_styleName": "labelSize",
}
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleColorEditor
defaultDynamicStyleOptions={
Object {
"color": "Blues",
"colorCategory": "palette_0",
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
}
}
defaultStaticStyleOptions={
Object {
"color": "#FFFFFF",
}
}
disabled={true}
disabledBy="labelText"
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticColorProperty {
"_options": Object {
"color": "#FFFFFF",
},
"_styleName": "labelBorderColor",
}
}
swatches={
Array [
"#41937c",
"#4379aa",
"#c83868",
"#7751a4",
"#ba6b95",
"#c9ad31",
"#a69168",
"#c57127",
"#885145",
"#e1401f",
"#000",
"#FFF",
]
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleLabelBorderSizeEditor
disabled={true}
disabledBy="labelText"
handlePropertyChange={[Function]}
styleProperty={
LabelBorderSizeProperty {
"_labelSizeProperty": StaticSizeProperty {
"_options": Object {
"size": 14,
},
"_styleName": "labelSize",
},
"_options": Object {
"size": "SMALL",
},
"_styleName": "labelBorderSize",
}
}
/>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="columnCompressedSwitch"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Apply global time to style metadata requests"
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render polygon-style without label properties when 3rd party mvt 1`] = `
<Fragment>
<VectorStyleColorEditor
defaultDynamicStyleOptions={
Object {
"color": "Blues",
"colorCategory": "palette_0",
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
}
}
defaultStaticStyleOptions={
Object {
"color": "#54B399",
}
}
disabled={false}
disabledBy="iconSize"
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticColorProperty {
"_options": Object {
"color": "#54B399",
},
"_styleName": "fillColor",
}
}
swatches={
Array [
"#54B399",
"#6092C0",
"#D36086",
"#9170B8",
"#CA8EAE",
"#D6BF57",
"#B9A888",
"#DA8B45",
"#AA6556",
"#E7664C",
]
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleColorEditor
defaultDynamicStyleOptions={
Object {
"color": "Blues",
"colorCategory": "palette_0",
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
}
}
defaultStaticStyleOptions={
Object {
"color": "#41937c",
}
}
disabled={false}
disabledBy="lineWidth"
fields={
Array [
Object {
"label": "field0",
"name": "field0",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
Object {
"label": "field1",
"name": "field1",
"origin": "source",
"supportsAutoDomain": true,
"type": "string",
},
]
}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticColorProperty {
"_options": Object {
"color": "#41937c",
},
"_styleName": "lineColor",
}
}
swatches={
Array [
"#41937c",
"#4379aa",
"#c83868",
"#7751a4",
"#ba6b95",
"#c9ad31",
"#a69168",
"#c57127",
"#885145",
"#e1401f",
"#000",
"#FFF",
]
}
/>
<EuiSpacer
size="m"
/>
<VectorStyleSizeEditor
defaultDynamicStyleOptions={
Object {
"field": undefined,
"fieldMetaOptions": Object {
"isEnabled": true,
"sigma": 3,
},
"maxSize": 10,
"minSize": 1,
}
}
defaultStaticStyleOptions={
Object {
"size": 1,
}
}
disabled={false}
disabledBy="iconSize"
fields={Array []}
onDynamicStyleChange={[Function]}
onStaticStyleChange={[Function]}
styleProperty={
StaticSizeProperty {
"_options": Object {
"size": 1,
},
"_styleName": "lineWidth",
}
}
/>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="columnCompressedSwitch"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Apply global time to style metadata requests"
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render with no style fields 1`] = `
<Fragment>
<VectorStyleColorEditor

View file

@ -14,7 +14,6 @@ import { IVectorSource } from '../../../sources/vector_source';
import {
FIELD_ORIGIN,
LAYER_STYLE_TYPE,
LAYER_TYPE,
VECTOR_SHAPE_TYPE,
VECTOR_STYLES,
} from '../../../../../common/constants';
@ -31,12 +30,7 @@ jest.mock('../../../../kibana_services', () => {
class MockField extends AbstractField {}
function createLayerMock(
numFields: number,
supportedShapeTypes: VECTOR_SHAPE_TYPE[],
layerType: LAYER_TYPE = LAYER_TYPE.VECTOR,
isESSource: boolean = false
) {
function createLayerMock(numFields: number, supportedShapeTypes: VECTOR_SHAPE_TYPE[]) {
const fields: IField[] = [];
for (let i = 0; i < numFields; i++) {
fields.push(new MockField({ fieldName: `field${i}`, origin: FIELD_ORIGIN.SOURCE }));
@ -45,17 +39,11 @@ function createLayerMock(
getStyleEditorFields: async () => {
return fields;
},
getType() {
return layerType;
},
getSource: () => {
return {
getSupportedShapeTypes: async () => {
return supportedShapeTypes;
},
isESSource() {
return isESSource;
},
} as unknown as IVectorSource;
},
} as unknown as IVectorLayer;
@ -111,35 +99,3 @@ test('should render with no style fields', async () => {
expect(component).toMatchSnapshot();
});
test('should render polygon-style without label properties when 3rd party mvt', async () => {
const component = shallow(
<VectorStyleEditor
{...defaultProps}
layer={createLayerMock(2, [VECTOR_SHAPE_TYPE.POLYGON], LAYER_TYPE.TILED_VECTOR, false)}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('should render line-style with label properties when ES-source is rendered as mvt', async () => {
const component = shallow(
<VectorStyleEditor
{...defaultProps}
layer={createLayerMock(2, [VECTOR_SHAPE_TYPE.LINE], LAYER_TYPE.TILED_VECTOR, true)}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});

View file

@ -25,7 +25,6 @@ import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_palettes';
import {
LABEL_BORDER_SIZES,
LAYER_TYPE,
STYLE_TYPE,
VECTOR_SHAPE_TYPE,
VECTOR_STYLES,
@ -258,18 +257,7 @@ export class VectorStyleEditor extends Component<Props, State> {
);
}
_renderLabelProperties(isPoints: boolean) {
if (
!isPoints &&
this.props.layer.getType() === LAYER_TYPE.TILED_VECTOR &&
!this.props.layer.getSource().isESSource()
) {
// This handles and edge-case
// 3rd party lines and polygons from mvt sources cannot be labeled, because they do not have label-centroid geometries inside the tile.
// These label-centroids are only added for ES-sources
return;
}
_renderLabelProperties() {
const hasLabel = this._hasLabel();
const hasLabelBorder = this._hasLabelBorder();
return (
@ -468,7 +456,7 @@ export class VectorStyleEditor extends Component<Props, State> {
/>
<EuiSpacer size="m" />
{this._renderLabelProperties(true)}
{this._renderLabelProperties()}
</Fragment>
);
}
@ -482,7 +470,7 @@ export class VectorStyleEditor extends Component<Props, State> {
{this._renderLineWidth()}
<EuiSpacer size="m" />
{this._renderLabelProperties(false)}
{this._renderLabelProperties()}
</Fragment>
);
}
@ -499,7 +487,7 @@ export class VectorStyleEditor extends Component<Props, State> {
{this._renderLineWidth()}
<EuiSpacer size="m" />
{this._renderLabelProperties(false)}
{this._renderLabelProperties()}
</Fragment>
);
}

View file

@ -135,7 +135,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
_getMbColor() {
if (!this.getFieldName()) {
if (!this.getMbFieldName()) {
return null;
}
@ -145,7 +145,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
_getOrdinalColorMbExpression() {
const targetName = this.getFieldName();
const targetName = this.getMbFieldName();
if (this._options.useCustomColorRamp) {
if (!this._options.customColorRamp || !this._options.customColorRamp.length) {
// custom color ramp config is not complete
@ -321,7 +321,7 @@ export class DynamicColorProperty extends DynamicStyleProperty<ColorDynamicOptio
}
mbStops.push(defaultColor); // last color is default color
return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops];
return ['match', ['to-string', ['get', this.getMbFieldName()]], ...mbStops];
}
_getOrdinalBreaks(symbolId?: string): Break[] {

View file

@ -86,7 +86,7 @@ export class DynamicIconProperty extends DynamicStyleProperty<IconDynamicOptions
if (fallbackSymbolId) {
mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); // last item is fallback style for anything that does not match provided stops
}
return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops];
return ['match', ['to-string', ['get', this.getMbFieldName()]], ...mbStops];
}
_getMbIconAnchorExpression() {
@ -106,7 +106,7 @@ export class DynamicIconProperty extends DynamicStyleProperty<IconDynamicOptions
if (fallbackSymbolId) {
mbStops.push(getMakiSymbolAnchor(fallbackSymbolId)); // last item is fallback style for anything that does not match provided stops
}
return ['match', ['to-string', ['get', this.getFieldName()]], ...mbStops];
return ['match', ['to-string', ['get', this.getMbFieldName()]], ...mbStops];
}
_isIconDynamicConfigComplete() {

View file

@ -66,7 +66,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty<SizeDynamicOptions
const rangeFieldMeta = this.getRangeFieldMeta();
if (this._isSizeDynamicConfigComplete() && rangeFieldMeta) {
const halfIconPixels = this.getIconPixelSize() / 2;
const targetName = this.getFieldName();
const targetName = this.getMbFieldName();
// Using property state instead of feature-state because layout properties do not support feature-state
mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [
'interpolate',
@ -115,7 +115,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty<SizeDynamicOptions
}
return this._getMbDataDrivenSize({
targetName: this.getFieldName(),
targetName: this.getMbFieldName(),
minSize: this._options.minSize,
maxSize: this._options.maxSize,
minValue: rangeFieldMeta.min,

View file

@ -27,7 +27,6 @@ import {
OrdinalDataMappingPopover,
} from '../components/data_mapping';
import {
Category,
CategoryFieldMeta,
FieldMetaOptions,
PercentilesFieldMeta,
@ -40,16 +39,12 @@ import { IVectorLayer } from '../../../layers/vector_layer';
import { InnerJoin } from '../../../joins/inner_join';
import { IVectorStyle } from '../vector_style';
import { getComputedFieldName } from '../style_util';
import { pluckRangeFieldMeta } from '../../../../../common/pluck_range_field_meta';
import {
pluckCategoryFieldMeta,
trimCategories,
} from '../../../../../common/pluck_category_field_meta';
export interface IDynamicStyleProperty<T> extends IStyleProperty<T> {
getFieldMetaOptions(): FieldMetaOptions;
getField(): IField | null;
getFieldName(): string;
getMbFieldName(): string;
getFieldOrigin(): FIELD_ORIGIN | null;
getRangeFieldMeta(): RangeFieldMeta | null;
getCategoryFieldMeta(): CategoryFieldMeta | null;
@ -63,7 +58,7 @@ export interface IDynamicStyleProperty<T> extends IStyleProperty<T> {
getFieldMetaRequest(): Promise<unknown | null>;
pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null;
pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null;
pluckOrdinalStyleMetaFromTileMetaFeatures(features: TileMetaFeature[]): RangeFieldMeta | null;
pluckOrdinalStyleMetaFromTileMetaFeatures(metaFeatures: TileMetaFeature[]): RangeFieldMeta | null;
pluckCategoricalStyleMetaFromTileMetaFeatures(
features: TileMetaFeature[]
): CategoryFieldMeta | null;
@ -213,6 +208,10 @@ export class DynamicStyleProperty<T>
return this._field ? this._field.getName() : '';
}
getMbFieldName() {
return this._field ? this._field.getMbFieldName() : '';
}
isDynamic() {
return true;
}
@ -314,54 +313,36 @@ export class DynamicStyleProperty<T>
return null;
}
const name = this.getFieldName();
const mbFieldName = this.getMbFieldName();
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < metaFeatures.length; i++) {
const fieldMeta = metaFeatures[i].properties.fieldMeta;
if (fieldMeta && fieldMeta[name] && fieldMeta[name].range) {
min = Math.min(fieldMeta[name].range?.min as number, min);
max = Math.max(fieldMeta[name].range?.max as number, max);
const fieldMeta = metaFeatures[i].properties;
const minField = `aggregations.${mbFieldName}.min`;
const maxField = `aggregations.${mbFieldName}.max`;
if (
fieldMeta &&
typeof fieldMeta[minField] === 'number' &&
typeof fieldMeta[maxField] === 'number'
) {
min = Math.min(fieldMeta[minField] as number, min);
max = Math.max(fieldMeta[maxField] as number, max);
}
}
return {
min,
max,
delta: max - min,
};
return min === Infinity || max === -Infinity
? null
: {
min,
max,
delta: max - min,
};
}
pluckCategoricalStyleMetaFromTileMetaFeatures(
metaFeatures: TileMetaFeature[]
): CategoryFieldMeta | null {
const size = this.getNumberOfCategories();
if (!this.isCategorical() || size <= 0) {
return null;
}
const name = this.getFieldName();
const counts = new Map<string, number>();
for (let i = 0; i < metaFeatures.length; i++) {
const fieldMeta = metaFeatures[i].properties.fieldMeta;
if (fieldMeta && fieldMeta[name] && fieldMeta[name].categories) {
const categoryFieldMeta: CategoryFieldMeta = fieldMeta[name]
.categories as CategoryFieldMeta;
for (let c = 0; c < categoryFieldMeta.categories.length; c++) {
const category: Category = categoryFieldMeta.categories[c];
// properties object may be sparse, so need to check if the field is effectively present
if (typeof category.key !== undefined) {
if (counts.has(category.key)) {
counts.set(category.key, (counts.get(category.key) as number) + category.count);
} else {
counts.set(category.key, category.count);
}
}
}
}
}
return trimCategories(counts, size);
return null;
}
pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null {
@ -370,9 +351,24 @@ export class DynamicStyleProperty<T>
}
const name = this.getFieldName();
return pluckRangeFieldMeta(features, name, (rawValue: unknown) => {
return parseFloat(rawValue as string);
});
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < features.length; i++) {
const feature = features[i];
const newValue = feature.properties ? parseFloat(feature.properties[name]) : NaN;
if (!isNaN(newValue)) {
min = Math.min(min, newValue);
max = Math.max(max, newValue);
}
}
return min === Infinity || max === -Infinity
? null
: {
min,
max,
delta: max - min,
};
}
pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null {
@ -381,7 +377,32 @@ export class DynamicStyleProperty<T>
return null;
}
return pluckCategoryFieldMeta(features, this.getFieldName(), size);
const counts = new Map();
for (let i = 0; i < features.length; i++) {
const feature = features[i];
const term = feature.properties ? feature.properties[this.getFieldName()] : undefined;
// properties object may be sparse, so need to check if the field is effectively present
if (typeof term !== undefined) {
if (counts.has(term)) {
counts.set(term, counts.get(term) + 1);
} else {
counts.set(term, 1);
}
}
}
const ordered = [];
for (const [key, value] of counts) {
ordered.push({ key, count: value });
}
ordered.sort((a, b) => {
return b.count - a.count;
});
const truncated = ordered.slice(0, size);
return {
categories: truncated,
} as CategoryFieldMeta;
}
_pluckOrdinalStyleMetaFromFieldMetaData(styleMetaData: StyleMetaData): RangeFieldMeta | null {
@ -487,7 +508,7 @@ export class DynamicStyleProperty<T>
targetName = getComputedFieldName(this.getStyleName(), this._field.getName());
} else {
// Non-geojson sources (e.g. 3rd party mvt or ES-source as mvt)
targetName = this._field.getName();
targetName = this._field.getMbFieldName();
}
}
return targetName;

View file

@ -16,7 +16,6 @@ import {
FIELD_ORIGIN,
GEO_JSON_TYPE,
KBN_IS_CENTROID_FEATURE,
KBN_VECTOR_SHAPE_TYPE_COUNTS,
LAYER_STYLE_TYPE,
SOURCE_FORMATTERS_DATA_REQUEST_ID,
STYLE_TYPE,
@ -76,7 +75,6 @@ import { IVectorLayer } from '../../layers/vector_layer';
import { IVectorSource } from '../../sources/vector_source';
import { createStyleFieldsHelper, StyleFieldsHelper } from './style_fields_helper';
import { IESAggField } from '../../fields/agg';
import { VectorShapeTypeCounts } from '../../../../common/get_geometry_counts';
const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT];
const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING];
@ -92,9 +90,8 @@ export interface IVectorStyle extends IStyle {
previousFields: IField[],
mapColors: string[]
): Promise<{ hasChanges: boolean; nextStyleDescriptor?: VectorStyleDescriptor }>;
isTimeAware: () => boolean;
getIcon: () => ReactElement<any>;
getIconFromGeometryTypes: (isLinesOnly: boolean, isPointsOnly: boolean) => ReactElement<any>;
isTimeAware(): boolean;
getIcon(): ReactElement<any>;
hasLegendDetails: () => Promise<boolean>;
renderLegendDetails: () => ReactElement<any>;
clearFeatureState: (featureCollection: FeatureCollection, mbMap: MbMap, sourceId: string) => void;
@ -492,50 +489,16 @@ export class VectorStyle implements IVectorStyle {
}
async pluckStyleMetaFromTileMeta(metaFeatures: TileMetaFeature[]): Promise<StyleMetaDescriptor> {
const shapeTypeCountMeta: VectorShapeTypeCounts = metaFeatures.reduce(
(accumulator: VectorShapeTypeCounts, tileMeta: TileMetaFeature) => {
if (
!tileMeta ||
!tileMeta.properties ||
!tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS]
) {
return accumulator;
}
accumulator[VECTOR_SHAPE_TYPE.POINT] +=
tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.POINT];
accumulator[VECTOR_SHAPE_TYPE.LINE] +=
tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.LINE];
accumulator[VECTOR_SHAPE_TYPE.POLYGON] +=
tileMeta.properties[KBN_VECTOR_SHAPE_TYPE_COUNTS][VECTOR_SHAPE_TYPE.POLYGON];
return accumulator;
},
{
[VECTOR_SHAPE_TYPE.POLYGON]: 0,
[VECTOR_SHAPE_TYPE.LINE]: 0,
[VECTOR_SHAPE_TYPE.POINT]: 0,
}
);
const isLinesOnly =
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] > 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] === 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] === 0;
const isPointsOnly =
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] === 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] > 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] === 0;
const isPolygonsOnly =
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.LINE] === 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POINT] === 0 &&
shapeTypeCountMeta[VECTOR_SHAPE_TYPE.POLYGON] > 0;
const supportedShapeTypes = await this._source.getSupportedShapeTypes();
const styleMeta: StyleMetaDescriptor = {
geometryTypes: {
isPointsOnly,
isLinesOnly,
isPolygonsOnly,
isPointsOnly:
supportedShapeTypes.length === 1 && supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.POINT),
isLinesOnly:
supportedShapeTypes.length === 1 && supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.LINE),
isPolygonsOnly:
supportedShapeTypes.length === 1 &&
supportedShapeTypes.includes(VECTOR_SHAPE_TYPE.POLYGON),
},
fieldMeta: {},
};
@ -737,7 +700,7 @@ export class VectorStyle implements IVectorStyle {
: (this._iconStyleProperty as StaticIconProperty).getOptions().value;
}
getIconFromGeometryTypes(isLinesOnly: boolean, isPointsOnly: boolean) {
_getIconFromGeometryTypes(isLinesOnly: boolean, isPointsOnly: boolean) {
let strokeColor;
if (isLinesOnly) {
strokeColor = extractColorFromStyleProperty(
@ -771,7 +734,7 @@ export class VectorStyle implements IVectorStyle {
getIcon() {
const isLinesOnly = this._getIsLinesOnly();
const isPointsOnly = this._getIsPointsOnly();
return this.getIconFromGeometryTypes(isLinesOnly, isPointsOnly);
return this._getIconFromGeometryTypes(isLinesOnly, isPointsOnly);
}
_getLegendDetailStyleProperties = () => {

View file

@ -6,9 +6,9 @@
*/
import _ from 'lodash';
import { DECIMAL_DEGREES_PRECISION } from './constants';
import { clampToLatBounds } from './elasticsearch_util';
import { MapExtent } from './descriptor_types';
import { DECIMAL_DEGREES_PRECISION } from '../../../common/constants';
import { clampToLatBounds } from '../../../common/elasticsearch_util';
import { MapExtent } from '../../../common/descriptor_types';
const ZOOM_TILE_KEY_INDEX = 0;
const X_TILE_KEY_INDEX = 1;

View file

@ -9,7 +9,6 @@ import {
GEO_JSON_TYPE,
FEATURE_VISIBLE_PROPERTY_NAME,
KBN_IS_CENTROID_FEATURE,
KBN_METADATA_FEATURE,
} from '../../../common/constants';
import { Timeslice } from '../../../common/descriptor_types';
@ -19,7 +18,6 @@ export interface TimesliceMaskConfig {
timeslice: Timeslice;
}
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_METADATA_FEATURE], true];
export const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true];
function getFilterExpression(
@ -56,7 +54,6 @@ export function getFillFilterExpression(
): unknown[] {
return getFilterExpression(
[
EXCLUDE_TOO_MANY_FEATURES_BOX,
EXCLUDE_CENTROID_FEATURES,
[
'any',
@ -75,7 +72,6 @@ export function getLineFilterExpression(
): unknown[] {
return getFilterExpression(
[
EXCLUDE_TOO_MANY_FEATURES_BOX,
EXCLUDE_CENTROID_FEATURES,
[
'any',
@ -96,7 +92,6 @@ export function getPointFilterExpression(
): unknown[] {
return getFilterExpression(
[
EXCLUDE_TOO_MANY_FEATURES_BOX,
EXCLUDE_CENTROID_FEATURES,
[
'any',
@ -109,13 +104,17 @@ export function getPointFilterExpression(
);
}
export function getCentroidFilterExpression(
export function getLabelFilterExpression(
hasJoins: boolean,
isSourceGeoJson: boolean,
timesliceMaskConfig?: TimesliceMaskConfig
): unknown[] {
return getFilterExpression(
[EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]],
hasJoins,
timesliceMaskConfig
);
const filters: unknown[] = [];
// centroids added for geojson sources only
if (isSourceGeoJson) {
filters.push(['==', ['get', KBN_IS_CENTROID_FEATURE], true]);
}
return getFilterExpression(filters, hasJoins, timesliceMaskConfig);
}

View file

@ -98,7 +98,9 @@ exports[`EditLayerPanel is rendered 1`] = `
"getId": [Function],
"getImmutableSourceProperties": [Function],
"getLayerTypeIconName": [Function],
"getType": [Function],
"hasErrors": [Function],
"hasJoins": [Function],
"renderSourceSettingsEditor": [Function],
"showJoinEditor": [Function],
"supportsElasticsearchFilters": [Function],
@ -119,7 +121,9 @@ exports[`EditLayerPanel is rendered 1`] = `
"getId": [Function],
"getImmutableSourceProperties": [Function],
"getLayerTypeIconName": [Function],
"getType": [Function],
"hasErrors": [Function],
"hasJoins": [Function],
"renderSourceSettingsEditor": [Function],
"showJoinEditor": [Function],
"supportsElasticsearchFilters": [Function],

View file

@ -48,6 +48,7 @@ jest.mock('../../kibana_services', () => {
import React from 'react';
import { shallow } from 'enzyme';
import { LAYER_TYPE } from '../../../common/constants';
import { ILayer } from '../../classes/layers/layer';
import { EditLayerPanel } from './edit_layer_panel';
@ -55,6 +56,9 @@ const mockLayer = {
getId: () => {
return '1';
},
getType: () => {
return LAYER_TYPE.VECTOR;
},
getDisplayName: () => {
return 'layer 1';
},
@ -79,6 +83,9 @@ const mockLayer = {
hasErrors: () => {
return false;
},
hasJoins: () => {
return false;
},
supportsFitToBounds: () => {
return true;
},
@ -87,7 +94,8 @@ const mockLayer = {
const defaultProps = {
selectedLayer: mockLayer,
fitToBounds: () => {},
updateSourceProp: () => {},
updateSourceProps: async () => {},
clearJoins: () => {},
};
describe('EditLayerPanel', () => {

View file

@ -30,7 +30,6 @@ import { StyleSettings } from './style_settings';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
import { LAYER_TYPE } from '../../../common/constants';
import { getData, getCore } from '../../kibana_services';
import { ILayer } from '../../classes/layers/layer';
import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer';
@ -40,13 +39,9 @@ import { IField } from '../../classes/fields/field';
const localStorage = new Storage(window.localStorage);
export interface Props {
clearJoins: (layer: ILayer) => void;
selectedLayer?: ILayer;
updateSourceProp: (
layerId: string,
propName: string,
value: unknown,
newLayerType?: LAYER_TYPE
) => void;
updateSourceProps: (layerId: string, sourcePropChanges: OnSourceChangeArgs[]) => Promise<void>;
}
interface State {
@ -141,9 +136,12 @@ export class EditLayerPanel extends Component<Props, State> {
}
_onSourceChange = (...args: OnSourceChangeArgs[]) => {
for (let i = 0; i < args.length; i++) {
const { propName, value, newLayerType } = args[i];
this.props.updateSourceProp(this.props.selectedLayer!.getId(), propName, value, newLayerType);
return this.props.updateSourceProps(this.props.selectedLayer!.getId(), args);
};
_clearJoins = () => {
if (this.props.selectedLayer) {
this.props.clearJoins(this.props.selectedLayer);
}
};
@ -279,6 +277,11 @@ export class EditLayerPanel extends Component<Props, State> {
/>
{this.props.selectedLayer.renderSourceSettingsEditor({
clearJoins: this._clearJoins,
currentLayerType: this.props.selectedLayer.getType(),
hasJoins: isVectorLayer(this.props.selectedLayer)
? (this.props.selectedLayer as IVectorLayer).hasJoins()
: false,
onChange: this._onSourceChange,
})}

View file

@ -9,11 +9,12 @@ import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { connect } from 'react-redux';
import { EditLayerPanel } from './edit_layer_panel';
import { LAYER_TYPE } from '../../../common/constants';
import { getSelectedLayer } from '../../selectors/map_selectors';
import { updateSourceProp } from '../../actions';
import { setJoinsForLayer, updateSourceProps } from '../../actions';
import { MapStoreState } from '../../reducers/store';
import { ILayer } from '../../classes/layers/layer';
import { isVectorLayer, IVectorLayer } from '../../classes/layers/vector_layer';
import { OnSourceChangeArgs } from '../../classes/sources/source';
function mapStateToProps(state: MapStoreState) {
const selectedLayer = getSelectedLayer(state);
@ -31,8 +32,11 @@ function mapStateToProps(state: MapStoreState) {
function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) {
return {
updateSourceProp: (id: string, propName: string, value: unknown, newLayerType?: LAYER_TYPE) =>
dispatch(updateSourceProp(id, propName, value, newLayerType)),
clearJoins: (layer: ILayer) => {
dispatch(setJoinsForLayer(layer, []));
},
updateSourceProps: async (id: string, sourcePropChanges: OnSourceChangeArgs[]) =>
await dispatch(updateSourceProps(id, sourcePropChanges)),
};
}

View file

@ -90,9 +90,7 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla
) : (
<Fragment>
{renderJoins()}
<EuiSpacer size="s" />
<EuiTextAlign textAlign="center">
<EuiButtonEmpty
onClick={addJoin}

View file

@ -18,10 +18,7 @@ import { getToasts } from '../../../../kibana_services';
import { DrawControl } from '../';
import { DRAW_MODE, DRAW_SHAPE } from '../../../../../common/constants';
import { ILayer } from '../../../../classes/layers/layer';
import {
EXCLUDE_CENTROID_FEATURES,
EXCLUDE_TOO_MANY_FEATURES_BOX,
} from '../../../../classes/util/mb_filter_expressions';
import { EXCLUDE_CENTROID_FEATURES } from '../../../../classes/util/mb_filter_expressions';
const geoJSONReader = new jsts.io.GeoJSONReader();
@ -105,7 +102,7 @@ export class DrawFeatureControl extends Component<Props, {}> {
] as [MbPoint, MbPoint];
const selectedFeatures = this.props.mbMap.queryRenderedFeatures(mbBbox, {
layers: mbEditLayerIds,
filter: ['all', EXCLUDE_TOO_MANY_FEATURES_BOX, EXCLUDE_CENTROID_FEATURES],
filter: ['all', EXCLUDE_CENTROID_FEATURES],
});
if (!selectedFeatures.length) {
return;

View file

@ -33,7 +33,6 @@ import {
} from '../../../common/descriptor_types';
import {
DECIMAL_DEGREES_PRECISION,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
LAYER_TYPE,
RawValue,
ZOOM_PRECISION,
@ -209,14 +208,6 @@ export class MbMap extends Component<Props, State> {
},
});
const tooManyFeaturesImageSrc =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC';
const tooManyFeaturesImage = new Image();
tooManyFeaturesImage.onload = () => {
mbMap.addImage(KBN_TOO_MANY_FEATURES_IMAGE_ID, tooManyFeaturesImage);
};
tooManyFeaturesImage.src = tooManyFeaturesImageSrc;
let emptyImage: HTMLImageElement;
mbMap.on('styleimagemissing', (e: unknown) => {
if (emptyImage) {

View file

@ -36,6 +36,19 @@ const mockLayer = {
canShowTooltip: () => {
return true;
},
getMbTooltipLayerIds: () => {
return ['foo', 'bar'];
},
getSource: () => {
return {
isMvt: () => {
return false;
},
isESSource: () => {
return false;
},
};
},
getFeatureById: () => {
return {
geometry: {

View file

@ -19,12 +19,7 @@ import uuid from 'uuid/v4';
import { Geometry } from 'geojson';
import { Filter } from 'src/plugins/data/public';
import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
import {
FEATURE_ID_PROPERTY_NAME,
GEO_JSON_TYPE,
LON_INDEX,
RawValue,
} from '../../../../common/constants';
import { GEO_JSON_TYPE, LON_INDEX, RawValue } from '../../../../common/constants';
import {
GEOMETRY_FILTER_ACTION,
TooltipFeature,
@ -33,9 +28,8 @@ import {
} from '../../../../common/descriptor_types';
import { TooltipPopover } from './tooltip_popover';
import { FeatureGeometryFilterForm } from './features_tooltip';
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../classes/util/mb_filter_expressions';
import { ILayer } from '../../../classes/layers/layer';
import { IVectorLayer, isVectorLayer } from '../../../classes/layers/vector_layer';
import { IVectorLayer, isVectorLayer, getFeatureId } from '../../../classes/layers/vector_layer';
import { RenderToolTipContent } from '../../../classes/tooltips/tooltip_property';
function justifyAnchorLocation(
@ -132,7 +126,13 @@ export class TooltipControl extends Component<Props, {}> {
}) as IVectorLayer;
}
_loadPreIndexedShape = async ({ layerId, featureId }: { layerId: string; featureId: string }) => {
_loadPreIndexedShape = async ({
layerId,
featureId,
}: {
layerId: string;
featureId?: string | number;
}) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer || typeof featureId === 'undefined') {
return null;
@ -152,7 +152,7 @@ export class TooltipControl extends Component<Props, {}> {
tooltipId,
}: {
layerId: string;
featureId: string;
featureId?: string | number;
tooltipId: string;
}): TooltipFeatureAction[] {
const actions = [];
@ -203,7 +203,8 @@ export class TooltipControl extends Component<Props, {}> {
if (!layer) {
break;
}
const featureId = mbFeature.properties?.[FEATURE_ID_PROPERTY_NAME];
const featureId = getFeatureId(mbFeature, layer.getSource());
const layerId = layer.getId();
let match = false;
for (let j = 0; j < uniqueFeatures.length; j++) {
@ -284,9 +285,10 @@ export class TooltipControl extends Component<Props, {}> {
}
const targetMbFeature = mbFeatures[0];
if (this.props.openTooltips[0] && this.props.openTooltips[0].features.length) {
const layer = this._getLayerByMbLayerId(targetMbFeature.layer.id);
if (layer && this.props.openTooltips[0] && this.props.openTooltips[0].features.length) {
const firstFeature = this.props.openTooltips[0].features[0];
if (targetMbFeature.properties?.[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) {
if (getFeatureId(targetMbFeature, layer.getSource()) === firstFeature.id) {
// ignore hover events when hover tooltip is all ready opened for feature
return;
}
@ -312,7 +314,7 @@ export class TooltipControl extends Component<Props, {}> {
(accumulator: string[], layer: ILayer) => {
// tooltips are only supported for vector layers, filter out all other layer types
return layer.isVisible() && isVectorLayer(layer)
? accumulator.concat(layer.getMbLayerIds())
? accumulator.concat((layer as IVectorLayer).getMbTooltipLayerIds())
: accumulator;
},
[]
@ -347,7 +349,6 @@ export class TooltipControl extends Component<Props, {}> {
] as [MbPoint, MbPoint];
return this.props.mbMap.queryRenderedFeatures(mbBbox, {
layers: mbLayerIds,
filter: EXCLUDE_TOO_MANY_FEATURES_BOX,
});
}

View file

@ -20,14 +20,17 @@ export const setInternalRepository = (
};
export const getInternalRepository = () => internalRepository;
let esClient: ElasticsearchClient;
let indexPatternsService: IndexPatternsCommonService;
export const setIndexPatternsService = async (
indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'],
elasticsearchClient: ElasticsearchClient
) => {
esClient = elasticsearchClient;
indexPatternsService = await indexPatternsServiceFactory(
new SavedObjectsClient(getInternalRepository()),
elasticsearchClient
);
};
export const getIndexPatternsService = () => indexPatternsService;
export const getESClient = () => esClient;

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 { Logger } from 'src/core/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import { RENDER_AS } from '../../common/constants';
function isAbortError(error: Error) {
return error.message === 'Request aborted' || error.message === 'Aborted';
}
export async function getEsGridTile({
logger,
context,
index,
geometryFieldName,
x,
y,
z,
requestBody = {},
requestType = RENDER_AS.POINT,
}: {
x: number;
y: number;
z: number;
geometryFieldName: string;
index: string;
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
requestType: RENDER_AS.GRID | RENDER_AS.POINT;
}): Promise<Buffer | null> {
try {
const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`;
const body = {
size: 0, // no hits
grid_precision: 7,
exact_bounds: false,
extent: 4096, // full resolution,
query: requestBody.query,
grid_type: requestType === RENDER_AS.GRID ? 'grid' : 'centroid',
aggs: requestBody.aggs,
fields: requestBody.fields,
runtime_mappings: requestBody.runtime_mappings,
};
const tile = await context.core.elasticsearch.client.asCurrentUser.transport.request({
method: 'GET',
path,
body,
});
return tile.body as unknown as Buffer;
} catch (e) {
if (!isAbortError(e)) {
// These are often circuit breaking exceptions
// Should return a tile with some error message
logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`);
}
return null;
}
}

View file

@ -5,53 +5,15 @@
* 2.0.
*/
// @ts-expect-error
import geojsonvt from 'geojson-vt';
// @ts-expect-error
import vtpbf from 'vt-pbf';
import _ from 'lodash';
import { Logger } from 'src/core/server';
import type { DataRequestHandlerContext } from 'src/plugins/data/server';
import { Feature, FeatureCollection, Polygon } from 'geojson';
import { countVectorShapeTypes } from '../../common/get_geometry_counts';
import {
COUNT_PROP_NAME,
ES_GEO_FIELD_TYPE,
FEATURE_ID_PROPERTY_NAME,
GEOTILE_GRID_AGG_NAME,
KBN_FEATURE_COUNT,
KBN_IS_TILE_COMPLETE,
KBN_METADATA_FEATURE,
KBN_VECTOR_SHAPE_TYPE_COUNTS,
MAX_ZOOM,
MVT_SOURCE_LAYER_NAME,
RENDER_AS,
SUPER_FINE_ZOOM_DELTA,
VECTOR_SHAPE_TYPE,
} from '../../common/constants';
import {
createExtentFilter,
convertRegularRespToGeoJson,
hitsToGeoJson,
isTotalHitsGreaterThan,
formatEnvelopeAsPolygon,
TotalHits,
} from '../../common/elasticsearch_util';
import { flattenHit } from './util';
import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils';
import { getCentroidFeatures } from '../../common/get_centroid_features';
import { pluckRangeFieldMeta } from '../../common/pluck_range_field_meta';
import { FieldMeta, TileMetaFeature } from '../../common/descriptor_types';
import { pluckCategoryFieldMeta } from '../../common/pluck_category_field_meta';
// heuristic. largest color-palette has 30 colors. 1 color is used for 'other'.
const TERM_COUNT = 30 - 1;
function isAbortError(error: Error) {
return error.message === 'Request aborted' || error.message === 'Aborted';
}
export async function getGridTile({
export async function getEsTile({
logger,
context,
index,
@ -60,9 +22,6 @@ export async function getGridTile({
y,
z,
requestBody = {},
requestType = RENDER_AS.POINT,
searchSessionId,
abortSignal,
}: {
x: number;
y: number;
@ -72,388 +31,32 @@ export async function getGridTile({
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
requestType: RENDER_AS.GRID | RENDER_AS.POINT;
geoFieldType: ES_GEO_FIELD_TYPE;
searchSessionId?: string;
abortSignal: AbortSignal;
}): Promise<Buffer | null> {
try {
const tileBounds: ESBounds = tileToESBbox(x, y, z);
requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds));
requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min(
z + SUPER_FINE_ZOOM_DELTA,
MAX_ZOOM
);
requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds;
requestBody.track_total_hits = false;
const response = await context
.search!.search(
{
params: {
index,
body: requestBody,
},
},
{
sessionId: searchSessionId,
legacyHitsTotal: false,
abortSignal,
}
)
.toPromise();
const features: Feature[] = convertRegularRespToGeoJson(response.rawResponse, requestType);
if (features.length) {
const bounds = formatEnvelopeAsPolygon({
maxLat: tileBounds.top_left.lat,
minLat: tileBounds.bottom_right.lat,
maxLon: tileBounds.bottom_right.lon,
minLon: tileBounds.top_left.lon,
});
const fieldNames = new Set<string>();
features.forEach((feature) => {
for (const key in feature.properties) {
if (feature.properties.hasOwnProperty(key) && key !== 'key' && key !== 'gridCentroid') {
fieldNames.add(key);
}
}
});
const fieldMeta: FieldMeta = {};
fieldNames.forEach((fieldName: string) => {
const rangeMeta = pluckRangeFieldMeta(features, fieldName, (rawValue: unknown) => {
if (fieldName === COUNT_PROP_NAME) {
return parseFloat(rawValue as string);
} else if (typeof rawValue === 'number') {
return rawValue;
} else if (rawValue) {
return parseFloat((rawValue as { value: string }).value);
} else {
return NaN;
}
});
const categoryMeta = pluckCategoryFieldMeta(features, fieldName, TERM_COUNT);
if (!fieldMeta[fieldName]) {
fieldMeta[fieldName] = {};
}
if (rangeMeta) {
fieldMeta[fieldName].range = rangeMeta;
}
if (categoryMeta) {
fieldMeta[fieldName].categories = categoryMeta;
}
});
const metaDataFeature: TileMetaFeature = {
type: 'Feature',
properties: {
[KBN_METADATA_FEATURE]: true,
[KBN_FEATURE_COUNT]: features.length,
[KBN_IS_TILE_COMPLETE]: true,
[KBN_VECTOR_SHAPE_TYPE_COUNTS]:
requestType === RENDER_AS.GRID
? {
[VECTOR_SHAPE_TYPE.POINT]: 0,
[VECTOR_SHAPE_TYPE.LINE]: 0,
[VECTOR_SHAPE_TYPE.POLYGON]: features.length,
}
: {
[VECTOR_SHAPE_TYPE.POINT]: features.length,
[VECTOR_SHAPE_TYPE.LINE]: 0,
[VECTOR_SHAPE_TYPE.POLYGON]: 0,
},
fieldMeta,
},
geometry: bounds,
};
features.push(metaDataFeature);
}
const featureCollection: FeatureCollection = {
features,
type: 'FeatureCollection',
const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`;
let fields = _.uniq(requestBody.docvalue_fields.concat(requestBody.stored_fields));
fields = fields.filter((f) => f !== geometryFieldName);
const body = {
grid_precision: 0, // no aggs
exact_bounds: true,
extent: 4096, // full resolution,
query: requestBody.query,
fields,
runtime_mappings: requestBody.runtime_mappings,
track_total_hits: requestBody.size + 1,
};
return createMvtTile(featureCollection, z, x, y);
const tile = await context.core.elasticsearch.client.asCurrentUser.transport.request({
method: 'GET',
path,
body,
});
return tile.body as unknown as Buffer;
} catch (e) {
if (!isAbortError(e)) {
// These are often circuit breaking exceptions
// Should return a tile with some error message
logger.warn(`Cannot generate grid-tile for ${z}/${x}/${y}: ${e.message}`);
logger.warn(`Cannot generate ES-grid-tile for ${z}/${x}/${y}: ${e.message}`);
}
return null;
}
}
export async function getTile({
logger,
context,
index,
geometryFieldName,
x,
y,
z,
requestBody = {},
geoFieldType,
searchSessionId,
abortSignal,
}: {
x: number;
y: number;
z: number;
geometryFieldName: string;
index: string;
context: DataRequestHandlerContext;
logger: Logger;
requestBody: any;
geoFieldType: ES_GEO_FIELD_TYPE;
searchSessionId?: string;
abortSignal: AbortSignal;
}): Promise<Buffer | null> {
let features: Feature[];
try {
requestBody.query.bool.filter.push(
getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z))
);
const searchOptions = {
sessionId: searchSessionId,
legacyHitsTotal: false,
abortSignal,
};
const countResponse = await context
.search!.search(
{
params: {
index,
body: {
size: 0,
query: requestBody.query,
track_total_hits: requestBody.size + 1,
},
},
},
searchOptions
)
.toPromise();
if (
isTotalHitsGreaterThan(
countResponse.rawResponse.hits.total as unknown as TotalHits,
requestBody.size
)
) {
// Generate "too many features"-bounds
const bboxResponse = await context
.search!.search(
{
params: {
index,
body: {
size: 0,
query: requestBody.query,
aggs: {
data_bounds: {
geo_bounds: {
field: geometryFieldName,
},
},
},
track_total_hits: false,
},
},
},
searchOptions
)
.toPromise();
const metaDataFeature: TileMetaFeature = {
type: 'Feature',
properties: {
[KBN_METADATA_FEATURE]: true,
[KBN_IS_TILE_COMPLETE]: false,
[KBN_FEATURE_COUNT]: 0,
[KBN_VECTOR_SHAPE_TYPE_COUNTS]: {
[VECTOR_SHAPE_TYPE.POINT]: 0,
[VECTOR_SHAPE_TYPE.LINE]: 0,
[VECTOR_SHAPE_TYPE.POLYGON]: 0,
},
},
geometry: esBboxToGeoJsonPolygon(
// @ts-expect-error @elastic/elasticsearch no way to declare aggregations for search response
bboxResponse.rawResponse.aggregations.data_bounds.bounds,
tileToESBbox(x, y, z)
),
};
features = [metaDataFeature];
} else {
const documentsResponse = await context
.search!.search(
{
params: {
index,
body: {
...requestBody,
track_total_hits: false,
},
},
},
searchOptions
)
.toPromise();
const featureCollection = hitsToGeoJson(
// @ts-expect-error hitsToGeoJson should be refactored to accept estypes.SearchHit
documentsResponse.rawResponse.hits.hits,
(hit: Record<string, unknown>) => {
return flattenHit(geometryFieldName, hit);
},
geometryFieldName,
geoFieldType,
[]
);
features = featureCollection.features;
// Correct system-fields.
for (let i = 0; i < features.length; i++) {
const props = features[i].properties;
if (props !== null) {
props[FEATURE_ID_PROPERTY_NAME] = features[i].id;
}
}
const counts = countVectorShapeTypes(features);
const fieldNames = new Set<string>();
features.forEach((feature) => {
for (const key in feature.properties) {
if (
feature.properties.hasOwnProperty(key) &&
key !== '_index' &&
key !== '_id' &&
key !== FEATURE_ID_PROPERTY_NAME
) {
fieldNames.add(key);
}
}
});
const fieldMeta: FieldMeta = {};
fieldNames.forEach((fieldName: string) => {
const rangeMeta = pluckRangeFieldMeta(features, fieldName, (rawValue: unknown) => {
return typeof rawValue === 'number' ? rawValue : NaN;
});
const categoryMeta = pluckCategoryFieldMeta(features, fieldName, TERM_COUNT);
if (!fieldMeta[fieldName]) {
fieldMeta[fieldName] = {};
}
if (rangeMeta) {
fieldMeta[fieldName].range = rangeMeta;
}
if (categoryMeta) {
fieldMeta[fieldName].categories = categoryMeta;
}
});
const metadataFeature: TileMetaFeature = {
type: 'Feature',
properties: {
[KBN_METADATA_FEATURE]: true,
[KBN_IS_TILE_COMPLETE]: true,
[KBN_VECTOR_SHAPE_TYPE_COUNTS]: counts,
[KBN_FEATURE_COUNT]: features.length,
fieldMeta,
},
geometry: esBboxToGeoJsonPolygon(tileToESBbox(x, y, z), tileToESBbox(x, y, z)),
};
features.push(metadataFeature);
}
const featureCollection: FeatureCollection = {
features,
type: 'FeatureCollection',
};
return createMvtTile(featureCollection, z, x, y);
} catch (e) {
if (!isAbortError(e)) {
logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`);
}
return null;
}
}
function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown {
const tileExtent = {
minLon: tileBounds.top_left.lon,
minLat: tileBounds.bottom_right.lat,
maxLon: tileBounds.bottom_right.lon,
maxLat: tileBounds.top_left.lat,
};
const tileExtentFilter = createExtentFilter(tileExtent, [geometryFieldName]);
return tileExtentFilter.query;
}
function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon {
// Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds.
let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon);
const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon);
minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline
const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat);
const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat);
return {
type: 'Polygon',
coordinates: [
[
[minLon, minLat],
[minLon, maxLat],
[maxLon, maxLat],
[maxLon, minLat],
[minLon, minLat],
],
],
};
}
function createMvtTile(
featureCollection: FeatureCollection,
z: number,
x: number,
y: number
): Buffer | null {
featureCollection.features.push(...getCentroidFeatures(featureCollection));
const tileIndex = geojsonvt(featureCollection, {
maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24
tolerance: 3, // simplification tolerance (higher means simpler)
extent: 4096, // tile extent (both width and height)
buffer: 64, // tile buffer on each side
debug: 0, // logging level (0 to disable, 1 or 2)
lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features
promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId`
generateId: false, // whether to generate feature ids. Cannot be used with `promoteId`
indexMaxZoom: 5, // max zoom in the initial tile index
indexMaxPoints: 100000, // max number of points per tile in the index
});
const tile = tileIndex.getTile(z, x, y);
if (tile) {
const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 });
return Buffer.from(pbf);
} else {
return null;
}
}

View file

@ -16,10 +16,10 @@ import {
MVT_GETTILE_API_PATH,
API_ROOT_PATH,
MVT_GETGRIDTILE_API_PATH,
ES_GEO_FIELD_TYPE,
RENDER_AS,
} from '../../common/constants';
import { getGridTile, getTile } from './get_tile';
import { getEsTile } from './get_tile';
import { getEsGridTile } from './get_grid_tile';
const CACHE_TIMEOUT_SECONDS = 60 * 60;
@ -43,8 +43,6 @@ export function initMVTRoutes({
geometryFieldName: schema.string(),
requestBody: schema.string(),
index: schema.string(),
geoFieldType: schema.string(),
searchSessionId: schema.maybe(schema.string()),
token: schema.maybe(schema.string()),
}),
},
@ -56,14 +54,15 @@ export function initMVTRoutes({
) => {
const { query, params } = request;
const abortController = new AbortController();
request.events.aborted$.subscribe(() => {
abortController.abort();
});
// todo - replace with direct abortion of raw transport request
// const abortController = new AbortController();
// request.events.aborted$.subscribe(() => {
// abortController.abort();
// });
const requestBodyDSL = rison.decode(query.requestBody as string);
const tile = await getTile({
const tile = await getEsTile({
logger,
context,
geometryFieldName: query.geometryFieldName as string,
@ -72,9 +71,6 @@ export function initMVTRoutes({
z: parseInt((params as any).z, 10) as number,
index: query.index as string,
requestBody: requestBodyDSL as any,
geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE,
searchSessionId: query.searchSessionId,
abortSignal: abortController.signal,
});
return sendResponse(response, tile);
@ -95,8 +91,6 @@ export function initMVTRoutes({
requestBody: schema.string(),
index: schema.string(),
requestType: schema.string(),
geoFieldType: schema.string(),
searchSessionId: schema.maybe(schema.string()),
token: schema.maybe(schema.string()),
}),
},
@ -107,14 +101,16 @@ export function initMVTRoutes({
response: KibanaResponseFactory
) => {
const { query, params } = request;
const abortController = new AbortController();
request.events.aborted$.subscribe(() => {
abortController.abort();
});
// todo - replace with direct abortion of raw transport request
// const abortController = new AbortController();
// request.events.aborted$.subscribe(() => {
// abortController.abort();
// });
const requestBodyDSL = rison.decode(query.requestBody as string);
const tile = await getGridTile({
const tile = await getEsGridTile({
logger,
context,
geometryFieldName: query.geometryFieldName as string,
@ -124,9 +120,6 @@ export function initMVTRoutes({
index: query.index as string,
requestBody: requestBodyDSL as any,
requestType: query.requestType as RENDER_AS.POINT | RENDER_AS.GRID,
geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE,
searchSessionId: query.searchSessionId,
abortSignal: abortController.signal,
});
return sendResponse(response, tile);

View file

@ -1,75 +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.
*/
// This implementation:
// - does not include meta-fields
// - does not validate the schema against the index-pattern (e.g. nested fields)
// In the context of .mvt this is sufficient:
// - only fields from the response are packed in the tile (more efficient)
// - query-dsl submitted from the client, which was generated by the IndexPattern
// todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26
export function flattenHit(
geometryField: string,
hit: Record<string, unknown>
): Record<string, any> {
const flat: Record<string, any> = {};
if (hit) {
flattenSource(flat, '', hit._source as Record<string, unknown>, geometryField);
if (hit.fields) {
flattenFields(flat, hit.fields as Array<Record<string, unknown>>);
}
// Attach meta fields
flat._index = hit._index;
flat._id = hit._id;
}
return flat;
}
function flattenSource(
accum: Record<string, any>,
path: string,
properties: Record<string, unknown> = {},
geometryField: string
): Record<string, any> {
accum = accum || {};
for (const key in properties) {
if (properties.hasOwnProperty(key)) {
const newKey = path ? path + '.' + key : key;
let value;
if (geometryField === newKey) {
value = properties[key]; // do not deep-copy the geometry
} else if (properties[key] !== null && typeof value === 'object' && !Array.isArray(value)) {
value = flattenSource(
accum,
newKey,
properties[key] as Record<string, unknown>,
geometryField
);
} else {
value = properties[key];
}
accum[newKey] = value;
}
}
return accum;
}
function flattenFields(accum: Record<string, any> = {}, fields: Array<Record<string, unknown>>) {
accum = accum || {};
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const value = fields[key];
if (Array.isArray(value)) {
accum[key] = value[0];
} else {
accum[key] = value;
}
}
}
}

View file

@ -8,10 +8,6 @@
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
import {
KBN_IS_CENTROID_FEATURE,
MVT_SOURCE_LAYER_NAME,
} from '../../../../plugins/maps/common/constants';
export default function ({ getService }) {
const supertest = getService('supertest');
@ -23,45 +19,53 @@ export default function ({ getService }) {
`/api/maps/mvt/getGridTile/3/2/3.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),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\
&geoFieldType=geo_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))\
&requestType=point`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(2);
// 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({ doc_count: 1, avg_of_bytes: 9252 });
expect(clusterFeature.properties).to.eql({ _count: 1, 'avg_of_bytes.value': 9252 });
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]);
// Metadata feature
const metadataFeature = layer.feature(1);
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).to.eql({
__kbn_metadata_feature__: true,
__kbn_feature_count__: 1,
__kbn_is_tile_complete__: true,
__kbn_vector_shape_type_counts__: '{"POINT":1,"LINE":0,"POLYGON":0}',
fieldMeta:
'{"doc_count":{"range":{"min":1,"max":1,"delta":0},"categories":{"categories":[{"key":1,"count":1}]}},"avg_of_bytes":{"range":{"min":9252,"max":9252,"delta":0},"categories":{"categories":[{"key":9252,"count":1}]}}}',
});
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: 0 },
{ x: 4096, y: 0 },
{ x: 4096, y: 4096 },
{ x: 0, y: 4096 },
{ x: 4096, y: 4096 },
{ x: 4096, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 4096 },
],
]);
});
@ -72,65 +76,62 @@ export default function ({ getService }) {
`/api/maps/mvt/getGridTile/3/2/3.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),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\
&geoFieldType=geo_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))\
&requestType=grid`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(3);
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({ doc_count: 1, avg_of_bytes: 9252 });
expect(gridFeature.properties).to.eql({ _count: 1, 'avg_of_bytes.value': 9252 });
expect(gridFeature.loadGeometry()).to.eql([
[
{ x: 96, y: 640 },
{ x: 96, y: 672 },
{ x: 64, y: 672 },
{ x: 64, y: 640 },
{ x: 96, y: 672 },
{ x: 96, y: 640 },
{ x: 64, y: 640 },
{ x: 64, y: 672 },
],
]);
// Metadata feature
const metadataFeature = layer.feature(1);
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).to.eql({
__kbn_metadata_feature__: true,
__kbn_feature_count__: 1,
__kbn_is_tile_complete__: true,
__kbn_vector_shape_type_counts__: '{"POINT":0,"LINE":0,"POLYGON":1}',
fieldMeta:
'{"doc_count":{"range":{"min":1,"max":1,"delta":0},"categories":{"categories":[{"key":1,"count":1}]}},"avg_of_bytes":{"range":{"min":9252,"max":9252,"delta":0},"categories":{"categories":[{"key":9252,"count":1}]}}}',
});
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: 0 },
{ x: 4096, y: 0 },
{ x: 4096, y: 4096 },
{ x: 0, y: 4096 },
{ x: 4096, y: 4096 },
{ x: 4096, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 4096 },
],
]);
const clusterFeature = layer.feature(2);
expect(clusterFeature.type).to.be(1);
expect(clusterFeature.extent).to.be(4096);
expect(clusterFeature.id).to.be(undefined);
expect(clusterFeature.properties).to.eql({
doc_count: 1,
avg_of_bytes: 9252,
[KBN_IS_CENTROID_FEATURE]: true,
});
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 80, y: 656 }]]);
});
});
}

View file

@ -8,7 +8,6 @@
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
import { MVT_SOURCE_LAYER_NAME } from '../../../../plugins/maps/common/constants';
function findFeature(layer, callbackFn) {
for (let i = 0; i < layer.length; i++) {
@ -23,22 +22,21 @@ export default function ({ getService }) {
const supertest = getService('supertest');
describe('getTile', () => {
it('should return vector tile containing document', async () => {
it('should return ES vector tile containing documents and metadata', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getTile/2/1/1.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),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:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\
&geoFieldType=geo_point`
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),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:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(3); // 2 docs + the metadata feature
const layer = jsonTile.layers.hits;
expect(layer.length).to.be(2); // 2 docs
// Verify ES document
@ -50,82 +48,32 @@ export default function ({ getService }) {
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({
__kbn__feature_id__: 'logstash-2015.09.20:AU_x3_BsGFA8no6Qjjug:0',
_id: 'AU_x3_BsGFA8no6Qjjug',
_index: 'logstash-2015.09.20',
bytes: 9252,
['machine.os.raw']: 'ios',
'machine.os.raw': 'ios',
});
expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]);
// Verify metadata feature
const metadataFeature = findFeature(layer, (feature) => {
return feature.properties.__kbn_metadata_feature__;
});
const metaDataLayer = jsonTile.layers.meta;
const metadataFeature = metaDataLayer.feature(0);
expect(metadataFeature).not.to.be(undefined);
expect(metadataFeature.type).to.be(3);
expect(metadataFeature.extent).to.be(4096);
expect(metadataFeature.id).to.be(undefined);
const fieldMeta = JSON.parse(metadataFeature.properties.fieldMeta);
delete metadataFeature.properties.fieldMeta;
expect(metadataFeature.properties).to.eql({
__kbn_feature_count__: 2,
__kbn_is_tile_complete__: true,
__kbn_metadata_feature__: true,
__kbn_vector_shape_type_counts__: '{"POINT":2,"LINE":0,"POLYGON":0}',
});
expect(fieldMeta.bytes.range).to.eql({
min: 9252,
max: 9583,
delta: 331,
});
expect(fieldMeta.bytes.categories.categories.length).to.be(2);
expect(fieldMeta['machine.os.raw'].categories.categories.length).to.be(2);
expect(metadataFeature.loadGeometry()).to.eql([
[
{ x: 0, y: 4096 },
{ x: 0, y: 0 },
{ x: 4096, y: 0 },
{ x: 4096, y: 4096 },
{ x: 0, y: 4096 },
],
]);
});
it('should return vector tile containing bounds when count exceeds size', async () => {
const resp = await supertest
// requestBody sets size=1 to force count exceeded
.get(
`/api/maps/mvt/getTile/2/1/1.pbf\
?geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),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:(),size:1,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\
&geoFieldType=geo_point`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
// This is dropping some irrelevant properties from the comparison
expect(metadataFeature.properties['hits.total.relation']).to.eql('eq');
expect(metadataFeature.properties['hits.total.value']).to.eql(2);
expect(metadataFeature.properties.timed_out).to.eql(false);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(1);
const metadataFeature = layer.feature(0);
expect(metadataFeature.type).to.be(3);
expect(metadataFeature.extent).to.be(4096);
expect(metadataFeature.id).to.be(undefined);
expect(metadataFeature.properties).to.eql({
__kbn_metadata_feature__: true,
__kbn_feature_count__: 0,
__kbn_is_tile_complete__: false,
__kbn_vector_shape_type_counts__: '{"POINT":0,"LINE":0,"POLYGON":0}',
});
expect(metadataFeature.loadGeometry()).to.eql([
[
{ x: 44, y: 2382 },
{ x: 44, y: 1913 },
{ x: 550, y: 1913 },
{ x: 550, y: 2382 },
{ x: 550, y: 1913 },
{ x: 44, y: 1913 },
{ x: 44, y: 2382 },
],
]);

View file

@ -6,10 +6,6 @@
*/
import expect from '@kbn/expect';
import {
KBN_IS_TILE_COMPLETE,
KBN_METADATA_FEATURE,
} from '../../../../plugins/maps/common/constants';
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps']);
@ -44,7 +40,6 @@ export default function ({ getPageObjects, getService }) {
maxzoom: 24,
filter: [
'all',
['!=', ['get', '__kbn_metadata_feature__'], true],
['!=', ['get', '__kbn_is_centroid_feature__'], true],
['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
@ -125,7 +120,6 @@ export default function ({ getPageObjects, getService }) {
maxzoom: 24,
filter: [
'all',
['!=', ['get', '__kbn_metadata_feature__'], true],
['!=', ['get', '__kbn_is_centroid_feature__'], true],
['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
@ -202,7 +196,6 @@ export default function ({ getPageObjects, getService }) {
maxzoom: 24,
filter: [
'all',
['!=', ['get', '__kbn_metadata_feature__'], true],
['!=', ['get', '__kbn_is_centroid_feature__'], true],
[
'any',
@ -217,26 +210,5 @@ export default function ({ getPageObjects, getService }) {
paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 },
});
});
it('should style incomplete data layer as expected', async () => {
const layer = mapboxStyle.layers.find((mbLayer) => {
return mbLayer.id === 'n1t6f_toomanyfeatures';
});
expect(layer).to.eql({
id: 'n1t6f_toomanyfeatures',
type: 'fill',
source: 'n1t6f',
minzoom: 0,
maxzoom: 24,
filter: [
'all',
['==', ['get', KBN_METADATA_FEATURE], true],
['==', ['get', KBN_IS_TILE_COMPLETE], false],
],
layout: { visibility: 'visible' },
paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 },
});
});
});
}

View file

@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }) {
//Source should be correct
expect(
mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0].startsWith(
`/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape`
`/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))`
)
).to.equal(true);
@ -77,5 +77,34 @@ export default function ({ getPageObjects, getService }) {
'fill-opacity': 1,
});
});
it('Style should include toomanyfeatures layer', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
const layer = mapboxStyle.layers.find((mbLayer) => {
return mbLayer.id === `${VECTOR_SOURCE_ID}_toomanyfeatures`;
});
expect(layer).to.eql({
id: 'caffa63a-ebfb-466d-8ff6-d797975b88ab_toomanyfeatures',
type: 'line',
source: 'caffa63a-ebfb-466d-8ff6-d797975b88ab',
'source-layer': 'meta',
minzoom: 0,
maxzoom: 24,
filter: [
'all',
['==', ['get', 'hits.total.relation'], 'gte'],
['>=', ['get', 'hits.total.value'], 10002],
],
layout: { visibility: 'visible' },
paint: {
'line-color': '#fec514',
'line-width': 3,
'line-dasharray': [2, 1],
'line-opacity': 1,
},
});
});
});
}

View file

@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) {
//Source should be correct
expect(
mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith(
`/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),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:!('*'))&requestType=grid&geoFieldType=geo_point`
`/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&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:!('*'))&requestType=grid`
)
).to.equal(true);
@ -51,9 +51,9 @@ export default function ({ getPageObjects, getService }) {
'coalesce',
[
'case',
['==', ['get', 'max_of_bytes'], null],
['==', ['get', 'max_of_bytes.value'], null],
1622,
['max', ['min', ['to-number', ['get', 'max_of_bytes']], 9790], 1623],
['max', ['min', ['to-number', ['get', 'max_of_bytes.value']], 9790], 1623],
],
1622,
],