mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
1e718a5572
commit
33fd1bdff0
83 changed files with 1201 additions and 2056 deletions
|
@ -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].
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 = '_';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
|
@ -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);
|
|
@ -13,3 +13,4 @@ export {
|
|||
VectorLayerArguments,
|
||||
NO_RESULTS_ICON_AND_TOOLTIPCONTENT,
|
||||
} from './vector_layer';
|
||||
export { getFeatureId } from './assign_feature_ids';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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[];
|
|
@ -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];
|
||||
|
|
@ -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 = {
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -155,6 +155,8 @@ export class CreateSourceEditor extends Component {
|
|||
)
|
||||
: null
|
||||
}
|
||||
hasJoins={false}
|
||||
clearJoins={() => {}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -26,6 +26,8 @@ const defaultProps = {
|
|||
scalingType: SCALING_TYPES.LIMIT,
|
||||
supportsClustering: true,
|
||||
termFields: [],
|
||||
hasJoins: false,
|
||||
clearJoins: () => {},
|
||||
};
|
||||
|
||||
describe('scaling form', () => {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -82,6 +82,10 @@ export class MVTSingleLayerVectorSource
|
|||
.filter((f) => f !== null) as MVTField[];
|
||||
}
|
||||
|
||||
isMvt() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async supportsFitToBounds() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -90,9 +90,7 @@ export function JoinEditor({ joins, layer, onChange, leftJoinFields, layerDispla
|
|||
) : (
|
||||
<Fragment>
|
||||
{renderJoins()}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTextAlign textAlign="center">
|
||||
<EuiButtonEmpty
|
||||
onClick={addJoin}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -36,6 +36,19 @@ const mockLayer = {
|
|||
canShowTooltip: () => {
|
||||
return true;
|
||||
},
|
||||
getMbTooltipLayerIds: () => {
|
||||
return ['foo', 'bar'];
|
||||
},
|
||||
getSource: () => {
|
||||
return {
|
||||
isMvt: () => {
|
||||
return false;
|
||||
},
|
||||
isESSource: () => {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
getFeatureById: () => {
|
||||
return {
|
||||
geometry: {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
64
x-pack/plugins/maps/server/mvt/get_grid_tile.ts
Normal file
64
x-pack/plugins/maps/server/mvt/get_grid_tile.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue