[Maps] Add mvt support for ES doc sources (#75698)

This commit is contained in:
Thomas Neirynck 2020-09-01 14:17:52 -04:00 committed by GitHub
parent 981691d378
commit d7869dee6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1443 additions and 142 deletions

View file

@ -313,6 +313,7 @@
"file-type": "^10.9.0",
"font-awesome": "4.7.0",
"fp-ts": "^2.3.1",
"geojson-vt": "^3.2.1",
"get-port": "^5.0.0",
"getos": "^3.1.0",
"git-url-parse": "11.1.2",
@ -384,6 +385,7 @@
"ui-select": "0.19.8",
"uuid": "3.3.2",
"vscode-languageserver": "^5.2.1",
"vt-pbf": "^3.1.1",
"webpack": "^4.41.5",
"wellknown": "^0.5.0",
"xml2js": "^0.4.22",

View file

@ -33,6 +33,12 @@ export const MAP_PATH = 'map';
export const GIS_API_PATH = `api/${APP_ID}`;
export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`;
export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`;
export const API_ROOT_PATH = `/${GIS_API_PATH}`;
export const MVT_GETTILE_API_PATH = 'mvt/getTile';
export const MVT_SOURCE_LAYER_NAME = 'source_layer';
export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__';
export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__';
const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`;
export function getNewMapPath() {
@ -220,6 +226,7 @@ export enum SCALING_TYPES {
LIMIT = 'LIMIT',
CLUSTERS = 'CLUSTERS',
TOP_HITS = 'TOP_HITS',
MVT = 'MVT',
}
export const RGBA_0000 = 'rgba(0,0,0,0)';

View file

@ -18,7 +18,6 @@ export type MapFilters = {
refreshTimerLastTriggeredAt?: string;
timeFilters: TimeRange;
zoom: number;
geogridPrecision?: number;
};
type ESSearchSourceSyncMeta = {

View file

@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FeatureCollection, GeoJsonProperties } from 'geojson';
import { MapExtent } from './descriptor_types';
import { ES_GEO_FIELD_TYPE } from './constants';
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent;
@ -13,3 +15,11 @@ export function turfBboxToBounds(turfBbox: unknown): MapExtent;
export function clampToLatBounds(lat: number): number;
export function clampToLonBounds(lon: number): number;
export function hitsToGeoJson(
hits: Array<Record<string, unknown>>,
flattenHit: (elasticSearchHit: Record<string, unknown>) => GeoJsonProperties,
geoFieldName: string,
geoFieldType: ES_GEO_FIELD_TYPE,
epochMillisFields: string[]
): FeatureCollection;

View file

@ -15,18 +15,22 @@ import { IVectorSource } from '../sources/vector_source';
export class ESDocField extends AbstractField implements IField {
private readonly _source: IESSource;
private readonly _canReadFromGeoJson: boolean;
constructor({
fieldName,
source,
origin,
canReadFromGeoJson = true,
}: {
fieldName: string;
source: IESSource;
origin: FIELD_ORIGIN;
canReadFromGeoJson?: boolean;
}) {
super({ fieldName, origin });
this._source = source;
this._canReadFromGeoJson = canReadFromGeoJson;
}
canValueBeFormatted(): boolean {
@ -60,6 +64,10 @@ export class ESDocField extends AbstractField implements IField {
return true;
}
canReadFromGeoJson(): boolean {
return this._canReadFromGeoJson;
}
async getOrdinalFieldMetaRequest(): Promise<unknown> {
const indexPatternField = await this._getIndexPatternField();

View file

@ -128,32 +128,41 @@ describe('syncData', () => {
sinon.assert.notCalled(syncContext2.stopLoading);
});
it('Should resync when changes to source params', async () => {
const layer1: TiledVectorLayer = createLayer({}, {});
const syncContext1 = new MockSyncContext({ dataFilters: {} });
describe('Should resync when changes to source params: ', () => {
[
{ layerName: 'barfoo' },
{ urlTemplate: 'https://sub.example.com/{z}/{x}/{y}.pbf' },
{ minSourceZoom: 1 },
{ maxSourceZoom: 12 },
].forEach((changes) => {
it(`change in ${Object.keys(changes).join(',')}`, async () => {
const layer1: TiledVectorLayer = createLayer({}, {});
const syncContext1 = new MockSyncContext({ dataFilters: {} });
await layer1.syncData(syncContext1);
await layer1.syncData(syncContext1);
const dataRequestDescriptor: DataRequestDescriptor = {
data: defaultConfig,
dataId: 'source',
};
const layer2: TiledVectorLayer = createLayer(
{
__dataRequests: [dataRequestDescriptor],
},
{ layerName: 'barfoo' }
);
const syncContext2 = new MockSyncContext({ dataFilters: {} });
await layer2.syncData(syncContext2);
const dataRequestDescriptor: DataRequestDescriptor = {
data: defaultConfig,
dataId: 'source',
};
const layer2: TiledVectorLayer = createLayer(
{
__dataRequests: [dataRequestDescriptor],
},
changes
);
const syncContext2 = new MockSyncContext({ dataFilters: {} });
await layer2.syncData(syncContext2);
// @ts-expect-error
sinon.assert.calledOnce(syncContext2.startLoading);
// @ts-expect-error
sinon.assert.calledOnce(syncContext2.stopLoading);
// @ts-expect-error
sinon.assert.calledOnce(syncContext2.startLoading);
// @ts-expect-error
sinon.assert.calledOnce(syncContext2.stopLoading);
// @ts-expect-error
const call = syncContext2.stopLoading.getCall(0);
expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' });
// @ts-expect-error
const call = syncContext2.stopLoading.getCall(0);
expect(call.args[2]).toEqual({ ...defaultConfig, ...changes });
});
});
});
});

View file

@ -63,21 +63,24 @@ export class TiledVectorLayer extends VectorLayer {
);
const prevDataRequest = this.getSourceDataRequest();
const templateWithMeta = await this._source.getUrlTemplateWithMeta(searchFilters);
if (prevDataRequest) {
const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
const canSkipBecauseNoChanges =
data.layerName === this._source.getLayerName() &&
data.minSourceZoom === this._source.getMinZoom() &&
data.maxSourceZoom === this._source.getMaxZoom();
if (data) {
const canSkipBecauseNoChanges =
data.layerName === this._source.getLayerName() &&
data.minSourceZoom === this._source.getMinZoom() &&
data.maxSourceZoom === this._source.getMaxZoom() &&
data.urlTemplate === templateWithMeta.urlTemplate;
if (canSkipBecauseNoChanges) {
return null;
if (canSkipBecauseNoChanges) {
return null;
}
}
}
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
try {
const templateWithMeta = await this._source.getUrlTemplateWithMeta();
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, templateWithMeta, {});
} catch (error) {
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
@ -160,6 +163,11 @@ export class TiledVectorLayer extends VectorLayer {
return false;
}
if (!mbTileSource.tiles) {
// Expected source is not compatible, so remove.
return true;
}
const isSourceDifferent =
mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate ||
mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom ||

View file

@ -15,9 +15,11 @@ import {
SOURCE_BOUNDS_DATA_REQUEST_ID,
FEATURE_VISIBLE_PROPERTY_NAME,
EMPTY_FEATURE_COLLECTION,
KBN_TOO_MANY_FEATURES_PROPERTY,
LAYER_TYPE,
FIELD_ORIGIN,
LAYER_STYLE_TYPE,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
} from '../../../../common/constants';
import _ from 'lodash';
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
@ -777,6 +779,8 @@ export class VectorLayer extends AbstractLayer {
const sourceId = this.getId();
const fillLayerId = this._getMbPolygonLayerId();
const lineLayerId = this._getMbLineLayerId();
const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId();
const hasJoins = this.hasJoins();
if (!mbMap.getLayer(fillLayerId)) {
const mbLayer = {
@ -802,6 +806,30 @@ export class VectorLayer extends AbstractLayer {
}
mbMap.addLayer(mbLayer);
}
if (!mbMap.getLayer(tooManyFeaturesLayerId)) {
const mbLayer = {
id: tooManyFeaturesLayerId,
type: 'fill',
source: sourceId,
paint: {},
};
if (mvtSourceLayer) {
mbLayer['source-layer'] = mvtSourceLayer;
}
mbMap.addLayer(mbLayer);
mbMap.setFilter(tooManyFeaturesLayerId, [
'==',
['get', KBN_TOO_MANY_FEATURES_PROPERTY],
true,
]);
mbMap.setPaintProperty(
tooManyFeaturesLayerId,
'fill-pattern',
KBN_TOO_MANY_FEATURES_IMAGE_ID
);
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha());
}
this.getCurrentStyle().setMBPaintProperties({
alpha: this.getAlpha(),
mbMap,
@ -822,6 +850,9 @@ export class VectorLayer extends AbstractLayer {
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
}
this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId);
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
}
_syncStylePropertiesWithMb(mbMap) {
@ -836,6 +867,19 @@ export class VectorLayer extends AbstractLayer {
type: 'geojson',
data: EMPTY_FEATURE_COLLECTION,
});
} else if (mbSource.type !== 'geojson') {
// Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer.
this.getMbLayerIds().forEach((mbLayerId) => {
if (mbMap.getLayer(mbLayerId)) {
mbMap.removeLayer(mbLayerId);
}
});
mbMap.removeSource(this._getMbSourceId());
mbMap.addSource(this._getMbSourceId(), {
type: 'geojson',
data: EMPTY_FEATURE_COLLECTION,
});
}
}
@ -865,6 +909,10 @@ export class VectorLayer extends AbstractLayer {
return this.makeMbLayerId('fill');
}
_getMbTooManyFeaturesLayerId() {
return this.makeMbLayerId('toomanyfeatures');
}
getMbLayerIds() {
return [
this._getMbPointLayerId(),
@ -872,6 +920,7 @@ export class VectorLayer extends AbstractLayer {
this._getMbSymbolLayerId(),
this._getMbLineLayerId(),
this._getMbPolygonLayerId(),
this._getMbTooManyFeaturesLayerId(),
];
}

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { MapExtent, MapFilters } from '../../../../common/descriptor_types';
import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types';
jest.mock('../../../kibana_services');
@ -19,6 +19,7 @@ import { SearchSource } from '../../../../../../../src/plugins/data/public/searc
export class MockSearchSource {
setField = jest.fn();
setParent() {}
}
describe('ESGeoGridSource', () => {
@ -104,6 +105,9 @@ describe('ESGeoGridSource', () => {
async create() {
return mockSearchSource as SearchSource;
},
createEmpty() {
return mockSearchSource as SearchSource;
},
},
};
@ -120,7 +124,7 @@ describe('ESGeoGridSource', () => {
maxLat: 80,
};
const mapFilters: MapFilters = {
const mapFilters: VectorSourceRequestMeta = {
geogridPrecision: 4,
filters: [],
timeFilters: {
@ -128,8 +132,16 @@ describe('ESGeoGridSource', () => {
to: '15m',
mode: 'relative',
},
// extent,
extent,
applyGlobalQuery: true,
fieldNames: [],
buffer: extent,
sourceQuery: {
query: '',
language: 'KQL',
queryLastTriggeredAt: '2019-04-25T20:53:22.331Z',
},
sourceMeta: null,
zoom: 0,
};

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should disable clusters option when clustering is not supported 1`] = `
exports[`scaling form should disable clusters option when clustering is not supported 1`] = `
<Fragment>
<EuiTitle
size="xs"
@ -50,6 +50,29 @@ exports[`should disable clusters option when clustering is not supported 1`] = `
onChange={[Function]}
/>
</EuiToolTip>
<EuiToolTip
content={
<React.Fragment>
<EuiBetaBadge
label="beta"
/>
<EuiHorizontalRule
margin="xs"
/>
Use vector tiles for faster display of large datasets.
</React.Fragment>
}
delay="regular"
position="left"
>
<EuiRadio
checked={false}
disabled={false}
id="MVT"
label="Use vector tiles"
onChange={[Function]}
/>
</EuiToolTip>
</div>
</EuiFormRow>
<EuiFormRow
@ -70,7 +93,7 @@ exports[`should disable clusters option when clustering is not supported 1`] = `
</Fragment>
`;
exports[`should render 1`] = `
exports[`scaling form should disable mvt option when mvt is not supported 1`] = `
<Fragment>
<EuiTitle
size="xs"
@ -114,6 +137,19 @@ exports[`should render 1`] = `
label="Show clusters when results exceed 10000."
onChange={[Function]}
/>
<EuiToolTip
content="Simulated mvt disabled"
delay="regular"
position="left"
>
<EuiRadio
checked={false}
disabled={true}
id="MVT"
label="Use vector tiles"
onChange={[Function]}
/>
</EuiToolTip>
</div>
</EuiFormRow>
<EuiFormRow
@ -134,7 +170,94 @@ exports[`should render 1`] = `
</Fragment>
`;
exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
exports[`scaling form should render 1`] = `
<Fragment>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Scaling"
id="xpack.maps.esSearch.scaleTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<div>
<EuiRadio
checked={true}
id="LIMIT"
label="Limit results to 10000."
onChange={[Function]}
/>
<EuiRadio
checked={false}
id="TOP_HITS"
label="Show top hits per entity."
onChange={[Function]}
/>
<EuiRadio
checked={false}
disabled={false}
id="CLUSTERS"
label="Show clusters when results exceed 10000."
onChange={[Function]}
/>
<EuiToolTip
content={
<React.Fragment>
<EuiBetaBadge
label="beta"
/>
<EuiHorizontalRule
margin="xs"
/>
Use vector tiles for faster display of large datasets.
</React.Fragment>
}
delay="regular"
position="left"
>
<EuiRadio
checked={false}
disabled={false}
id="MVT"
label="Use vector tiles"
onChange={[Function]}
/>
</EuiToolTip>
</div>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSwitch
checked={true}
compressed={true}
label="Dynamically filter for data in the visible map area"
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`scaling form should render top hits form when scaling type is TOP_HITS 1`] = `
<Fragment>
<EuiTitle
size="xs"
@ -178,6 +301,29 @@ exports[`should render top hits form when scaling type is TOP_HITS 1`] = `
label="Show clusters when results exceed 10000."
onChange={[Function]}
/>
<EuiToolTip
content={
<React.Fragment>
<EuiBetaBadge
label="beta"
/>
<EuiHorizontalRule
margin="xs"
/>
Use vector tiles for faster display of large datasets.
</React.Fragment>
}
delay="regular"
position="left"
>
<EuiRadio
checked={false}
disabled={false}
id="MVT"
label="Use vector tiles"
onChange={[Function]}
/>
</EuiToolTip>
</div>
</EuiFormRow>
<EuiFormRow

View file

@ -92,11 +92,14 @@ exports[`should enable sort order select when sort field provided 1`] = `
/>
<EuiPanel>
<ScalingForm
clusteringDisabledReason={null}
filterByMapBounds={true}
indexPatternId="indexPattern1"
mvtDisabledReason={null}
onChange={[Function]}
scalingType="LIMIT"
supportsClustering={false}
supportsMvt={false}
termFields={null}
topHitsSize={1}
topHitsSplitField="trackId"
@ -199,11 +202,14 @@ exports[`should render update source editor 1`] = `
/>
<EuiPanel>
<ScalingForm
clusteringDisabledReason={null}
filterByMapBounds={true}
indexPatternId="indexPattern1"
mvtDisabledReason={null}
onChange={[Function]}
scalingType="LIMIT"
supportsClustering={false}
supportsMvt={false}
termFields={null}
topHitsSize={1}
topHitsSplitField="trackId"

View file

@ -19,6 +19,8 @@ import {
getTermsFields,
getGeoTileAggNotSupportedReason,
supportsGeoTileAgg,
supportsMvt,
getMvtDisabledReason,
} from '../../../index_pattern_util';
function doesGeoFieldSupportGeoTileAgg(indexPattern, geoFieldName) {
@ -147,6 +149,7 @@ export class CreateSourceEditor extends Component {
return null;
}
const mvtSupported = supportsMvt(this.state.indexPattern, this.state.geoFieldName);
return (
<Fragment>
<EuiSpacer size="m" />
@ -159,6 +162,8 @@ export class CreateSourceEditor extends Component {
this.state.indexPattern,
this.state.geoFieldName
)}
supportsMvt={mvtSupported}
mvtDisabledReason={mvtSupported ? null : getMvtDisabledReason()}
clusteringDisabledReason={
this.state.indexPattern
? getGeoTileAggNotSupportedReason(

View file

@ -14,13 +14,18 @@ import { ESSearchSource, sourceTitle } from './es_search_source';
import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer';
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) {
const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig);
return sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS
? BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors)
: VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
if (sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS) {
return BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
} else if (sourceDescriptor.scalingType === SCALING_TYPES.MVT) {
return TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
} else {
return VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
}
}
export const esDocumentsLayerWizardConfig: LayerWizard = {

View file

@ -5,11 +5,22 @@
*/
import { AbstractESSource } from '../es_source';
import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types';
import { ESSearchSourceDescriptor, MapFilters } from '../../../../common/descriptor_types';
import { ITiledSingleLayerVectorSource } from '../vector_source';
export class ESSearchSource extends AbstractESSource {
export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource {
static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor;
constructor(sourceDescriptor: Partial<ESSearchSourceDescriptor>, inspectorAdapters: unknown);
getFieldNames(): string[];
getUrlTemplateWithMeta(
searchFilters: MapFilters
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;
maxSourceZoom: number;
}>;
getLayerName(): string;
}

View file

@ -6,9 +6,10 @@
import _ from 'lodash';
import React from 'react';
import rison from 'rison-node';
import { AbstractESSource } from '../es_source';
import { getSearchService } from '../../../kibana_services';
import { getSearchService, getHttp } from '../../../kibana_services';
import { hitsToGeoJson } from '../../../../common/elasticsearch_geo_utils';
import { UpdateSourceEditor } from './update_source_editor';
import {
@ -18,6 +19,9 @@ import {
SORT_ORDER,
SCALING_TYPES,
VECTOR_SHAPE_TYPE,
MVT_SOURCE_LAYER_NAME,
GIS_API_PATH,
MVT_GETTILE_API_PATH,
} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
@ -96,6 +100,7 @@ export class ESSearchSource extends AbstractESSource {
return new ESDocField({
fieldName,
source: this,
canReadFromGeoJson: this._descriptor.scalingType !== SCALING_TYPES.MVT,
});
}
@ -448,9 +453,13 @@ export class ESSearchSource extends AbstractESSource {
}
isFilterByMapBounds() {
return this._descriptor.scalingType === SCALING_TYPES.CLUSTER
? true
: this._descriptor.filterByMapBounds;
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTER) {
return true;
} else if (this._descriptor.scalingType === SCALING_TYPES.MVT) {
return false;
} else {
return this._descriptor.filterByMapBounds;
}
}
async getLeftJoinFields() {
@ -553,11 +562,65 @@ export class ESSearchSource extends AbstractESSource {
}
getJoinsDisabledReason() {
return this._descriptor.scalingType === SCALING_TYPES.CLUSTERS
? i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
defaultMessage: 'Joins are not supported when scaling by clusters',
})
: null;
let reason;
if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) {
reason = i18n.translate('xpack.maps.source.esSearch.joinsDisabledReason', {
defaultMessage: 'Joins are not supported when scaling by clusters',
});
} 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',
});
} else {
reason = null;
}
return reason;
}
getLayerName() {
return MVT_SOURCE_LAYER_NAME;
}
async getUrlTemplateWithMeta(searchFilters) {
const indexPattern = await this.getIndexPattern();
const indexSettings = await loadIndexSettings(indexPattern.title);
const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields(
indexPattern,
searchFilters.fieldNames
);
const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source
const searchSource = await this.makeSearchSource(
searchFilters,
indexSettings.maxResultWindow,
initialSearchContext
);
searchSource.setField('fields', searchFilters.fieldNames); // Setting "fields" filters out unused scripted fields
if (sourceOnlyFields.length === 0) {
searchSource.setField('source', false); // do not need anything from _source
} else {
searchSource.setField('source', sourceOnlyFields);
}
if (this._hasSort()) {
searchSource.setField('sort', this._buildEsSort());
}
const dsl = await searchSource.getSearchRequestBody();
const risonDsl = rison.encode(dsl);
const mvtUrlServicePath = getHttp().basePath.prepend(
`/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}`
);
const urlTemplate = `${mvtUrlServicePath}?x={x}&y={y}&z={z}&geometryFieldName=${this._descriptor.geoField}&index=${indexPattern.title}&requestBody=${risonDsl}`;
return {
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate: urlTemplate,
};
}
}

View file

@ -0,0 +1,155 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants';
jest.mock('../../../kibana_services');
jest.mock('./load_index_settings');
import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services';
import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source';
// @ts-expect-error
import { loadIndexSettings } from './load_index_settings';
import { ESSearchSource } from './es_search_source';
import { VectorSourceRequestMeta } from '../../../../common/descriptor_types';
describe('ESSearchSource', () => {
it('constructor', () => {
const esSearchSource = new ESSearchSource({}, null);
expect(esSearchSource instanceof ESSearchSource).toBe(true);
});
describe('ITiledSingleLayerVectorSource', () => {
it('mb-source params', () => {
const esSearchSource = new ESSearchSource({}, null);
expect(esSearchSource.getMinZoom()).toBe(0);
expect(esSearchSource.getMaxZoom()).toBe(24);
expect(esSearchSource.getLayerName()).toBe('source_layer');
});
describe('getUrlTemplateWithMeta', () => {
const geoFieldName = 'bar';
const mockIndexPatternService = {
get() {
return {
title: 'foobar-title-*',
fields: {
getByName() {
return {
name: geoFieldName,
type: ES_GEO_FIELD_TYPE.GEO_SHAPE,
};
},
},
};
},
};
beforeEach(async () => {
const mockSearchSource = {
setField: jest.fn(),
getSearchRequestBody() {
return { foobar: 'ES_DSL_PLACEHOLDER', params: this.setField.mock.calls };
},
setParent() {},
};
const mockSearchService = {
searchSource: {
async create() {
return (mockSearchSource as unknown) as SearchSource;
},
createEmpty() {
return (mockSearchSource as unknown) as SearchSource;
},
},
};
// @ts-expect-error
getIndexPatternService.mockReturnValue(mockIndexPatternService);
// @ts-expect-error
getSearchService.mockReturnValue(mockSearchService);
loadIndexSettings.mockReturnValue({
maxResultWindow: 1000,
});
// @ts-expect-error
getHttp.mockReturnValue({
basePath: {
prepend(path: string) {
return `rootdir${path};`;
},
},
});
});
const searchFilters: VectorSourceRequestMeta = {
filters: [],
zoom: 0,
fieldNames: ['tooltipField', 'styleField'],
timeFilters: {
from: 'now',
to: '15m',
mode: 'relative',
},
sourceQuery: {
query: 'tooltipField: foobar',
language: 'KQL',
queryLastTriggeredAt: '2019-04-25T20:53:22.331Z',
},
sourceMeta: null,
applyGlobalQuery: true,
};
it('Should only include required props', async () => {
const esSearchSource = new ESSearchSource(
{ geoField: geoFieldName, indexPatternId: 'ipId' },
null
);
const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters);
expect(urlTemplateWithMeta.urlTemplate).toBe(
`rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&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',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fields,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))`
);
});
});
});
describe('isFilterByMapBounds', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
expect(esSearchSource.isFilterByMapBounds()).toBe(true);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
expect(esSearchSource.isFilterByMapBounds()).toBe(false);
});
});
describe('getJoinsDisabledReason', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
expect(esSearchSource.getJoinsDisabledReason()).toBe(null);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
expect(esSearchSource.getJoinsDisabledReason()).toBe(
'Joins are not supported when scaling by mvt vector tiles'
);
});
});
describe('getFields', () => {
it('default', () => {
const esSearchSource = new ESSearchSource({}, null);
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(true);
});
it('mvt', () => {
const esSearchSource = new ESSearchSource({ scalingType: SCALING_TYPES.MVT }, null);
const docField = esSearchSource.createField({ fieldName: 'prop1' });
expect(docField.canReadFromGeoJson()).toBe(false);
});
});
});

View file

@ -27,28 +27,46 @@ const defaultProps = {
termFields: [],
topHitsSplitField: null,
topHitsSize: 1,
supportsMvt: true,
mvtDisabledReason: null,
};
test('should render', async () => {
const component = shallow(<ScalingForm {...defaultProps} />);
describe('scaling form', () => {
test('should render', async () => {
const component = shallow(<ScalingForm {...defaultProps} />);
expect(component).toMatchSnapshot();
});
test('should disable clusters option when clustering is not supported', async () => {
const component = shallow(
<ScalingForm
{...defaultProps}
supportsClustering={false}
clusteringDisabledReason="Simulated clustering disabled"
/>
);
expect(component).toMatchSnapshot();
});
test('should render top hits form when scaling type is TOP_HITS', async () => {
const component = shallow(<ScalingForm {...defaultProps} scalingType={SCALING_TYPES.TOP_HITS} />);
expect(component).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
test('should disable clusters option when clustering is not supported', async () => {
const component = shallow(
<ScalingForm
{...defaultProps}
supportsClustering={false}
clusteringDisabledReason="Simulated clustering disabled"
/>
);
expect(component).toMatchSnapshot();
});
test('should render top hits form when scaling type is TOP_HITS', async () => {
const component = shallow(
<ScalingForm {...defaultProps} scalingType={SCALING_TYPES.TOP_HITS} />
);
expect(component).toMatchSnapshot();
});
test('should disable mvt option when mvt is not supported', async () => {
const component = shallow(
<ScalingForm
{...defaultProps}
supportsMvt={false}
mvtDisabledReason={'Simulated mvt disabled'}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -4,16 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, Component } from 'react';
import React, { Component, Fragment } from 'react';
import {
EuiFormRow,
EuiHorizontalRule,
EuiRadio,
EuiSpacer,
EuiSwitch,
EuiSwitchEvent,
EuiTitle,
EuiSpacer,
EuiHorizontalRule,
EuiRadio,
EuiToolTip,
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -24,8 +25,8 @@ import { ValidatedRange } from '../../../components/validated_range';
import {
DEFAULT_MAX_INNER_RESULT_WINDOW,
DEFAULT_MAX_RESULT_WINDOW,
SCALING_TYPES,
LAYER_TYPE,
SCALING_TYPES,
} from '../../../../common/constants';
// @ts-ignore
import { loadIndexSettings } from './load_index_settings';
@ -38,7 +39,9 @@ interface Props {
onChange: (args: OnSourceChangeArgs) => void;
scalingType: SCALING_TYPES;
supportsClustering: boolean;
supportsMvt: boolean;
clusteringDisabledReason?: string | null;
mvtDisabledReason?: string | null;
termFields: IFieldType[];
topHitsSplitField: string | null;
topHitsSize: number;
@ -80,8 +83,15 @@ export class ScalingForm extends Component<Props, State> {
}
_onScalingTypeChange = (optionId: string): void => {
const layerType =
optionId === SCALING_TYPES.CLUSTERS ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR;
let layerType;
if (optionId === SCALING_TYPES.CLUSTERS) {
layerType = LAYER_TYPE.BLENDED_VECTOR;
} else if (optionId === SCALING_TYPES.MVT) {
layerType = LAYER_TYPE.TILED_VECTOR;
} else {
layerType = LAYER_TYPE.VECTOR;
}
this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType });
};
@ -177,9 +187,47 @@ 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)}
disabled={!this.props.supportsMvt}
/>
);
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 !this.props.supportsMvt ? (
<EuiToolTip position="left" content={this.props.mvtDisabledReason}>
{mvtRadio}
</EuiToolTip>
) : (
<EuiToolTip position="left" content={enabledInfo}>
{mvtRadio}
</EuiToolTip>
);
}
render() {
let filterByBoundsSwitch;
if (this.props.scalingType !== SCALING_TYPES.CLUSTERS) {
if (
this.props.scalingType === SCALING_TYPES.TOP_HITS ||
this.props.scalingType === SCALING_TYPES.LIMIT
) {
filterByBoundsSwitch = (
<EuiFormRow>
<EuiSwitch
@ -194,9 +242,9 @@ export class ScalingForm extends Component<Props, State> {
);
}
let scalingForm = null;
let topHitsOptionsForm = null;
if (this.props.scalingType === SCALING_TYPES.TOP_HITS) {
scalingForm = (
topHitsOptionsForm = (
<Fragment>
<EuiHorizontalRule margin="xs" />
{this._renderTopHitsForm()}
@ -234,12 +282,12 @@ export class ScalingForm extends Component<Props, State> {
onChange={() => this._onScalingTypeChange(SCALING_TYPES.TOP_HITS)}
/>
{this._renderClusteringRadio()}
{this._renderMVTRadio()}
</div>
</EuiFormRow>
{filterByBoundsSwitch}
{scalingForm}
{topHitsOptionsForm}
</Fragment>
);
}

View file

@ -17,6 +17,8 @@ import {
getTermsFields,
getSourceFields,
supportsGeoTileAgg,
supportsMvt,
getMvtDisabledReason,
} from '../../../index_pattern_util';
import { SORT_ORDER } from '../../../../common/constants';
import { ESDocField } from '../../fields/es_doc_field';
@ -42,6 +44,9 @@ export class UpdateSourceEditor extends Component {
termFields: null,
sortFields: null,
supportsClustering: false,
supportsMvt: false,
mvtDisabledReason: null,
clusteringDisabledReason: null,
};
componentDidMount() {
@ -94,9 +99,12 @@ export class UpdateSourceEditor extends Component {
});
});
const mvtSupported = supportsMvt(indexPattern, geoField.name);
this.setState({
supportsClustering: supportsGeoTileAgg(geoField),
supportsMvt: mvtSupported,
clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField),
mvtDisabledReason: mvtSupported ? null : getMvtDisabledReason(),
sourceFields: sourceFields,
termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields
sortFields: indexPattern.fields.filter(
@ -207,7 +215,9 @@ export class UpdateSourceEditor extends Component {
onChange={this.props.onChange}
scalingType={this.props.scalingType}
supportsClustering={this.state.supportsClustering}
supportsMvt={this.state.supportsMvt}
clusteringDisabledReason={this.state.clusteringDisabledReason}
mvtDisabledReason={this.state.mvtDisabledReason}
termFields={this.state.termFields}
topHitsSplitField={this.props.topHitsSplitField}
topHitsSize={this.props.topHitsSize}

View file

@ -184,7 +184,7 @@ export class AbstractESSource extends AbstractVectorSource {
const minLon = esBounds.top_left.lon;
const maxLon = esBounds.bottom_right.lon;
return {
minLon: minLon > maxLon ? minLon - 360 : minLon,
minLon: minLon > maxLon ? minLon - 360 : minLon, //fixes an ES bbox to straddle dateline
maxLon,
minLat: esBounds.bottom_right.lat,
maxLat: esBounds.top_left.lat,

View file

@ -14,6 +14,7 @@ import {
MapExtent,
MapFilters,
MapQuery,
VectorSourceRequestMeta,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { VECTOR_SHAPE_TYPE } from '../../../../common/constants';
@ -64,7 +65,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
): MapExtent | null;
getGeoJsonWithMeta(
layerName: string,
searchFilters: MapFilters,
searchFilters: VectorSourceRequestMeta,
registerCancelCallback: (callback: () => void) => void
): Promise<GeoJsonWithMeta>;
@ -79,7 +80,9 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
}
export interface ITiledSingleLayerVectorSource extends IVectorSource {
getUrlTemplateWithMeta(): Promise<{
getUrlTemplateWithMeta(
searchFilters: VectorSourceRequestMeta
): Promise<{
layerName: string;
urlTemplate: string;
minSourceZoom: number;

View file

@ -14,6 +14,7 @@ import { FieldMetaOptions } from '../../../../../../common/descriptor_types';
type Props = {
fieldMetaOptions: FieldMetaOptions;
onChange: (fieldMetaOptions: FieldMetaOptions) => void;
switchDisabled: boolean;
};
export function CategoricalFieldMetaPopover(props: Props) {
@ -34,6 +35,7 @@ export function CategoricalFieldMetaPopover(props: Props) {
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>
</EuiFormRow>
</FieldMetaPopover>

View file

@ -40,6 +40,7 @@ type Props = {
fieldMetaOptions: FieldMetaOptions;
styleName: VECTOR_STYLES;
onChange: (fieldMetaOptions: FieldMetaOptions) => void;
switchDisabled: boolean;
};
export function OrdinalFieldMetaPopover(props: Props) {
@ -66,6 +67,7 @@ export function OrdinalFieldMetaPopover(props: Props) {
checked={props.fieldMetaOptions.isEnabled}
onChange={onIsEnabledChange}
compressed
disabled={props.switchDisabled}
/>
</EuiFormRow>

View file

@ -325,3 +325,29 @@ exports[`ordinal Should render ordinal legend as bands 1`] = `
</EuiFlexGroup>
</div>
`;
exports[`renderFieldMetaPopover Should disable toggle when field is not backed by geojson source 1`] = `
<OrdinalFieldMetaPopover
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
switchDisabled={true}
/>
`;
exports[`renderFieldMetaPopover Should enable toggle when field is backed by geojson-source 1`] = `
<OrdinalFieldMetaPopover
fieldMetaOptions={
Object {
"isEnabled": true,
}
}
onChange={[Function]}
styleName="lineColor"
switchDisabled={false}
/>
`;

View file

@ -577,3 +577,39 @@ test('Should read out ordinal type correctly', async () => {
expect(ordinalColorStyle2.isOrdinal()).toEqual(true);
expect(ordinalColorStyle2.isCategorical()).toEqual(false);
});
describe('renderFieldMetaPopover', () => {
test('Should enable toggle when field is backed by geojson-source', () => {
const colorStyle = makeProperty(
{
color: 'Blues',
type: undefined,
fieldMetaOptions,
},
undefined,
mockField
);
const legendRow = colorStyle.renderFieldMetaPopover(() => {});
expect(legendRow).toMatchSnapshot();
});
test('Should disable toggle when field is not backed by geojson source', () => {
const nonGeoJsonField = Object.create(mockField);
nonGeoJsonField.canReadFromGeoJson = () => {
return false;
};
const colorStyle = makeProperty(
{
color: 'Blues',
type: undefined,
fieldMetaOptions,
},
undefined,
nonGeoJsonField
);
const legendRow = colorStyle.renderFieldMetaPopover(() => {});
expect(legendRow).toMatchSnapshot();
});
});

View file

@ -328,16 +328,20 @@ export class DynamicStyleProperty<T>
return null;
}
const switchDisabled = !!this._field && !this._field.canReadFromGeoJson();
return this.isCategorical() ? (
<CategoricalFieldMetaPopover
fieldMetaOptions={this.getFieldMetaOptions()}
onChange={onFieldMetaOptionsChange}
switchDisabled={switchDisabled}
/>
) : (
<OrdinalFieldMetaPopover
fieldMetaOptions={this.getFieldMetaOptions()}
styleName={this.getStyleName()}
onChange={onFieldMetaOptionsChange}
switchDisabled={switchDisabled}
/>
);
}

View file

@ -4,32 +4,48 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { GEO_JSON_TYPE, FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants';
import {
GEO_JSON_TYPE,
FEATURE_VISIBLE_PROPERTY_NAME,
KBN_TOO_MANY_FEATURES_PROPERTY,
} from '../../../common/constants';
export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true];
const VISIBILITY_FILTER_CLAUSE = ['all', ['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]];
const TOO_MANY_FEATURES_FILTER = ['all', EXCLUDE_TOO_MANY_FEATURES_BOX];
const CLOSED_SHAPE_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
...TOO_MANY_FEATURES_FILTER,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
],
];
const VISIBLE_CLOSED_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, CLOSED_SHAPE_MB_FILTER];
const ALL_SHAPE_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
...TOO_MANY_FEATURES_FILTER,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON],
['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING],
],
];
const VISIBLE_ALL_SHAPE_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, ALL_SHAPE_MB_FILTER];
const POINT_MB_FILTER = [
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
...TOO_MANY_FEATURES_FILTER,
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POINT],
['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT],
],
];
const VISIBLE_POINT_MB_FILTER = [...VISIBILITY_FILTER_CLAUSE, POINT_MB_FILTER];

View file

@ -8,6 +8,7 @@ import _ from 'lodash';
import React from 'react';
import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants';
import { TooltipPopover } from './tooltip_popover';
import { EXCLUDE_TOO_MANY_FEATURES_BOX } from '../../../../classes/util/mb_filter_expressions';
function justifyAnchorLocation(mbLngLat, targetFeature) {
let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location
@ -79,7 +80,7 @@ export class TooltipControl extends React.Component {
// - As empty object literal
// To avoid ambiguity, normalize properties to empty object literal.
const mbProperties = mbFeature.properties ? mbFeature.properties : {};
//This keeps track of first properties (assuming these will be identical for features in different tiles
//This keeps track of first properties (assuming these will be identical for features in different tiles)
uniqueFeatures.push({
id: featureId,
layerId: layerId,
@ -175,7 +176,10 @@ export class TooltipControl extends React.Component {
y: mbLngLatPoint.y + PADDING,
},
];
return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds });
return this.props.mbMap.queryRenderedFeatures(mbBbox, {
layers: mbLayerIds,
filter: EXCLUDE_TOO_MANY_FEATURES_BOX,
});
}
render() {

View file

@ -10,7 +10,11 @@ import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/pub
import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils';
import { syncLayerOrder } from './sort_layers';
import { getGlyphUrl, isRetina } from '../../../meta';
import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants';
import {
DECIMAL_DEGREES_PRECISION,
KBN_TOO_MANY_FEATURES_IMAGE_ID,
ZOOM_PRECISION,
} from '../../../../common/constants';
import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp';
import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker';
import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js';
@ -143,6 +147,14 @@ export class MBMap extends React.Component {
mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left');
}
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;
mbMap.on('styleimagemissing', (e) => {
if (emptyImage) {

View file

@ -81,6 +81,16 @@ export function supportsGeoTileAgg(field?: IFieldType): boolean {
);
}
export function supportsMvt(indexPattern: IndexPattern, geoFieldName: string): boolean {
const field = indexPattern.fields.getByName(geoFieldName);
return !!field && field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE;
}
export function getMvtDisabledReason() {
return i18n.translate('xpack.maps.mbt.disabled', {
defaultMessage: 'Display as vector tiles is only supported for geo_shape field-types.',
});
}
// Returns filtered fields list containing only fields that exist in _source.
export function getSourceFields(fields: IFieldType[]): IFieldType[] {
return fields.filter((field) => {

View file

@ -0,0 +1 @@
{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":0,"hits":[{"_index":"poly","_id":"G7PRMXQBgyyZ-h5iYibj","_score":0,"_source":{"coordinates":{"coordinates":[[[-106.171875,36.59788913307022],[-50.625,-22.91792293614603],[4.921875,42.8115217450979],[-33.046875,63.54855223203644],[-66.796875,63.860035895395306],[-106.171875,36.59788913307022]]],"type":"polygon"}}}]}}

Binary file not shown.

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as path from 'path';
import * as fs from 'fs';
const search000path = path.resolve(__dirname, './json/0_0_0_search.json');
const search000raw = fs.readFileSync(search000path);
const search000json = JSON.parse((search000raw as unknown) as string);
export const TILE_SEARCHES = {
'0.0.0': {
countResponse: {
count: 1,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
},
searchResponse: search000json,
},
'1.1.0': {},
};

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getTile } from './get_tile';
import { TILE_SEARCHES } from './__tests__/tile_searches';
import { Logger } from 'src/core/server';
import * as path from 'path';
import * as fs from 'fs';
describe('getTile', () => {
const mockCallElasticsearch = jest.fn();
const requestBody = {
_source: { excludes: [] },
docvalue_fields: [],
query: { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
script_fields: {},
size: 10000,
stored_fields: ['*'],
};
const geometryFieldName = 'coordinates';
beforeEach(() => {
mockCallElasticsearch.mockReset();
});
test('0.0.0 - under limit', async () => {
mockCallElasticsearch.mockImplementation((type) => {
if (type === 'count') {
return TILE_SEARCHES['0.0.0'].countResponse;
} else if (type === 'search') {
return TILE_SEARCHES['0.0.0'].searchResponse;
} else {
throw new Error(`${type} not recognized`);
}
});
const tile = await getTile({
x: 0,
y: 0,
z: 0,
index: 'world_countries',
requestBody,
geometryFieldName,
logger: ({
info: () => {},
} as unknown) as Logger,
callElasticsearch: mockCallElasticsearch,
});
if (tile === null) {
throw new Error('Tile should be created');
}
const expectedPath = path.resolve(__dirname, './__tests__/pbf/0_0_0.pbf');
const expectedBin = fs.readFileSync(expectedPath, 'binary');
const expectedTile = Buffer.from(expectedBin, 'binary');
expect(expectedTile.equals(tile)).toBe(true);
});
});

View file

@ -0,0 +1,226 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-expect-error
import geojsonvt from 'geojson-vt';
// @ts-expect-error
import vtpbf from 'vt-pbf';
import { Logger } from 'src/core/server';
import { Feature, FeatureCollection, Polygon } from 'geojson';
import {
ES_GEO_FIELD_TYPE,
FEATURE_ID_PROPERTY_NAME,
KBN_TOO_MANY_FEATURES_PROPERTY,
MVT_SOURCE_LAYER_NAME,
} from '../../common/constants';
import { hitsToGeoJson } from '../../common/elasticsearch_geo_utils';
import { flattenHit } from './util';
interface ESBounds {
top_left: {
lon: number;
lat: number;
};
bottom_right: {
lon: number;
lat: number;
};
}
export async function getTile({
logger,
callElasticsearch,
index,
geometryFieldName,
x,
y,
z,
requestBody = {},
}: {
x: number;
y: number;
z: number;
geometryFieldName: string;
index: string;
callElasticsearch: (type: string, ...args: any[]) => Promise<unknown>;
logger: Logger;
requestBody: any;
}): Promise<Buffer | null> {
const geojsonBbox = tileToGeoJsonPolygon(x, y, z);
let resultFeatures: Feature[];
try {
let result;
try {
const geoShapeFilter = {
geo_shape: {
[geometryFieldName]: {
shape: geojsonBbox,
relation: 'INTERSECTS',
},
},
};
requestBody.query.bool.filter.push(geoShapeFilter);
const esSearchQuery = {
index,
body: requestBody,
};
const esCountQuery = {
index,
body: {
query: requestBody.query,
},
};
const countResult = await callElasticsearch('count', esCountQuery);
// @ts-expect-error
if (countResult.count > requestBody.size) {
// Generate "too many features"-bounds
const bboxAggName = 'data_bounds';
const bboxQuery = {
index,
body: {
size: 0,
query: requestBody.query,
aggs: {
[bboxAggName]: {
geo_bounds: {
field: geometryFieldName,
},
},
},
},
};
const bboxResult = await callElasticsearch('search', bboxQuery);
// @ts-expect-error
const bboxForData = esBboxToGeoJsonPolygon(bboxResult.aggregations[bboxAggName].bounds);
resultFeatures = [
{
type: 'Feature',
properties: {
[KBN_TOO_MANY_FEATURES_PROPERTY]: true,
},
geometry: bboxForData,
},
];
} else {
// Perform actual search
result = await callElasticsearch('search', esSearchQuery);
// Todo: pass in epochMillies-fields
const featureCollection = hitsToGeoJson(
// @ts-expect-error
result.hits.hits,
(hit: Record<string, unknown>) => {
return flattenHit(geometryFieldName, hit);
},
geometryFieldName,
ES_GEO_FIELD_TYPE.GEO_SHAPE,
[]
);
resultFeatures = featureCollection.features;
// Correct system-fields.
for (let i = 0; i < resultFeatures.length; i++) {
const props = resultFeatures[i].properties;
if (props !== null) {
props[FEATURE_ID_PROPERTY_NAME] = resultFeatures[i].id;
}
}
}
} catch (e) {
logger.warn(e.message);
throw e;
}
const featureCollection: FeatureCollection = {
features: resultFeatures,
type: '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;
}
} catch (e) {
logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`);
return null;
}
}
function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon {
const wLon = tile2long(x, z);
const sLat = tile2lat(y + 1, z);
const eLon = tile2long(x + 1, z);
const nLat = tile2lat(y, z);
return {
type: 'Polygon',
coordinates: [
[
[wLon, sLat],
[wLon, nLat],
[eLon, nLat],
[eLon, sLat],
[wLon, sLat],
],
],
};
}
function tile2long(x: number, z: number): number {
return (x / Math.pow(2, z)) * 360 - 180;
}
function tile2lat(y: number, z: number): number {
const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z);
return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
}
function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon {
let minLon = esBounds.top_left.lon;
const maxLon = esBounds.bottom_right.lon;
minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline
const minLat = esBounds.bottom_right.lat;
const maxLat = esBounds.top_left.lat;
return {
type: 'Polygon',
coordinates: [
[
[minLon, minLat],
[minLon, maxLat],
[maxLon, maxLat],
[maxLon, minLat],
[minLon, minLat],
],
],
};
}

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import rison from 'rison-node';
import { schema } from '@kbn/config-schema';
import { Logger } from 'src/core/server';
import { IRouter } from 'src/core/server';
import { MVT_GETTILE_API_PATH, API_ROOT_PATH } from '../../common/constants';
import { getTile } from './get_tile';
const CACHE_TIMEOUT = 0; // Todo. determine good value. Unsure about full-implications (e.g. wrt. time-based data).
export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRouter }) {
router.get(
{
path: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}`,
validate: {
query: schema.object({
x: schema.number(),
y: schema.number(),
z: schema.number(),
geometryFieldName: schema.string(),
requestBody: schema.string(),
index: schema.string(),
}),
},
},
async (context, request, response) => {
const { query } = request;
const callElasticsearch = async (type: string, ...args: any[]): Promise<unknown> => {
return await context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args);
};
const requestBodyDSL = rison.decode(query.requestBody);
const tile = await getTile({
logger,
callElasticsearch,
geometryFieldName: query.geometryFieldName,
x: query.x,
y: query.y,
z: query.z,
index: query.index,
requestBody: requestBodyDSL,
});
if (tile) {
return response.ok({
body: tile,
headers: {
'content-disposition': 'inline',
'content-length': `${tile.length}`,
'Content-Type': 'application/x-protobuf',
'Cache-Control': `max-age=${CACHE_TIMEOUT}`,
},
});
} else {
return response.ok({
headers: {
'content-disposition': 'inline',
'content-length': '0',
'Content-Type': 'application/x-protobuf',
'Cache-Control': `max-age=${CACHE_TIMEOUT}`,
},
});
}
}
);
}

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// 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
import { GeoJsonProperties } from 'geojson';
export function flattenHit(geometryField: string, hit: Record<string, unknown>): GeoJsonProperties {
const flat: GeoJsonProperties = {};
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: GeoJsonProperties,
path: string,
properties: Record<string, unknown> = {},
geometryField: string
): GeoJsonProperties {
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: GeoJsonProperties = {}, fields: Array<Record<string, unknown>>) {
accum = accum || {};
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const value = fields[key];
if (Array.isArray(value)) {
accum[key] = value[0];
} else {
accum[key] = value;
}
}
}
}

View file

@ -22,6 +22,7 @@ import {
EMS_SPRITES_PATH,
INDEX_SETTINGS_API_PATH,
FONTS_API_PATH,
API_ROOT_PATH,
} from '../common/constants';
import { EMSClient } from '@elastic/ems-client';
import fetch from 'node-fetch';
@ -30,8 +31,7 @@ import { getIndexPatternSettings } from './lib/get_index_pattern_settings';
import { schema } from '@kbn/config-schema';
import fs from 'fs';
import path from 'path';
const ROOT = `/${GIS_API_PATH}`;
import { initMVTRoutes } from './mvt/mvt_routes';
export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
let emsClient;
@ -69,7 +69,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`,
path: `${API_ROOT_PATH}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`,
validate: {
query: schema.object({
id: schema.maybe(schema.string()),
@ -109,7 +109,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`,
validate: false,
},
async (context, request, response) => {
@ -145,7 +145,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_CATALOGUE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_CATALOGUE_PATH}`,
validate: false,
},
async (context, request, { ok, badRequest }) => {
@ -181,7 +181,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
path: `${API_ROOT_PATH}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
validate: false,
},
async (context, request, { ok, badRequest }) => {
@ -213,7 +213,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
path: `${API_ROOT_PATH}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`,
validate: false,
},
async (context, request, { ok, badRequest }) => {
@ -257,7 +257,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`,
validate: {
query: schema.object({
id: schema.maybe(schema.string()),
@ -293,7 +293,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`,
validate: {
query: schema.object({
id: schema.string(),
@ -341,7 +341,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`,
validate: {
query: schema.object({
id: schema.string(),
@ -379,7 +379,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`,
validate: {
query: schema.object({
id: schema.string(),
@ -417,7 +417,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`,
validate: {
params: schema.object({
fontstack: schema.string(),
@ -439,7 +439,7 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
router.get(
{
path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`,
path: `${API_ROOT_PATH}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`,
validate: {
query: schema.object({
elastic_tile_service_tos: schema.maybe(schema.string()),
@ -591,4 +591,6 @@ export function initRoutes(router, licenseUid, mapConfig, kbnVersion, logger) {
return response.badRequest(`Cannot connect to EMS`);
}
}
initMVTRoutes({ router, logger });
}

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export default function ({ getService }) {
const supertest = getService('supertest');
describe('getTile', () => {
it('should validate params', async () => {
await supertest
.get(
`/api/maps/mvt/getTile?x=15&y=11&z=5&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))`
)
.set('kbn-xsrf', 'kibana')
.expect(200);
});
it('should not validate when required params are missing', async () => {
await supertest
.get(
`/api/maps/mvt/getTile?&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))`
)
.set('kbn-xsrf', 'kibana')
.expect(400);
});
});
}

View file

@ -16,6 +16,7 @@ export default function ({ loadTestFile, getService }) {
loadTestFile(require.resolve('./fonts_api'));
loadTestFile(require.resolve('./index_settings'));
loadTestFile(require.resolve('./migrations'));
loadTestFile(require.resolve('./get_tile'));
});
});
}

View file

@ -46,6 +46,7 @@ export default function ({ loadTestFile, getService }) {
loadTestFile(require.resolve('./es_geo_grid_source'));
loadTestFile(require.resolve('./es_pew_pew_source'));
loadTestFile(require.resolve('./joins'));
loadTestFile(require.resolve('./mvt_scaling'));
loadTestFile(require.resolve('./add_layer_panel'));
loadTestFile(require.resolve('./import_geojson'));
loadTestFile(require.resolve('./layer_errors'));

View file

@ -5,7 +5,6 @@
*/
import expect from '@kbn/expect';
import { set } from '@elastic/safer-lodash-set';
import { MAPBOX_STYLES } from './mapbox_styles';
@ -21,6 +20,7 @@ const VECTOR_SOURCE_ID = 'n1t6f';
const CIRCLE_STYLE_LAYER_INDEX = 0;
const FILL_STYLE_LAYER_INDEX = 2;
const LINE_STYLE_LAYER_INDEX = 3;
const TOO_MANY_FEATURES_LAYER_INDEX = 4;
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps']);
@ -87,28 +87,32 @@ export default function ({ getPageObjects, getService }) {
});
});
it('should style fills, points and lines independently', async () => {
it('should style fills, points, lines, and bounding-boxes independently', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => {
return mbLayer.id.startsWith(VECTOR_SOURCE_ID);
});
// Color is dynamically obtained from eui source lib
const dynamicColor =
layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX].paint['circle-stroke-color'];
//circle layer for points
expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(
set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor)
);
expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER);
//fill layer
expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER);
//line layer for borders
expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(
set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor)
);
expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER);
//Too many features layer (this is a static style config)
expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({
id: 'n1t6f_toomanyfeatures',
type: 'fill',
source: 'n1t6f',
minzoom: 0,
maxzoom: 24,
filter: ['==', ['get', '__kbn_too_many_features__'], true],
layout: { visibility: 'visible' },
paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 },
});
});
it('should flag only the joined features as visible', async () => {

View file

@ -14,7 +14,11 @@ export const MAPBOX_STYLES = {
filter: [
'all',
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
[
'all',
['!=', ['get', '__kbn_too_many_features__'], true],
['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
],
],
layout: { visibility: 'visible' },
paint: {
@ -84,7 +88,11 @@ export const MAPBOX_STYLES = {
filter: [
'all',
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
[
'all',
['!=', ['get', '__kbn_too_many_features__'], true],
['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
],
],
layout: { visibility: 'visible' },
paint: {
@ -151,20 +159,18 @@ export const MAPBOX_STYLES = {
'all',
['==', ['get', '__kbn_isvisibleduetojoin__'], true],
[
'any',
['==', ['geometry-type'], 'Polygon'],
['==', ['geometry-type'], 'MultiPolygon'],
['==', ['geometry-type'], 'LineString'],
['==', ['geometry-type'], 'MultiLineString'],
'all',
['!=', ['get', '__kbn_too_many_features__'], true],
[
'any',
['==', ['geometry-type'], 'Polygon'],
['==', ['geometry-type'], 'MultiPolygon'],
['==', ['geometry-type'], 'LineString'],
['==', ['geometry-type'], 'MultiLineString'],
],
],
],
layout: {
visibility: 'visible',
},
paint: {
/* 'line-color': '' */ // Obtained dynamically
'line-opacity': 0.75,
'line-width': 1,
},
layout: { visibility: 'visible' },
paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 },
},
};

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
const VECTOR_SOURCE_ID = 'caffa63a-ebfb-466d-8ff6-d797975b88ab';
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps']);
const inspector = getService('inspector');
describe('mvt geoshape layer', () => {
before(async () => {
await PageObjects.maps.loadSavedMap('geo_shape_mvt');
});
after(async () => {
await inspector.close();
});
it('should render with mvt-source', async () => {
const mapboxStyle = await PageObjects.maps.getMapboxStyle();
//Source should be correct
expect(mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0]).to.equal(
'/api/maps/mvt/getTile?x={x}&y={y}&z={z}&geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:(includes:!(geometry,prop1)),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(geometry,prop1))'
);
//Should correctly load meta for style-rule (sigma is set to 1, opacity to 1)
const fillLayer = mapboxStyle.layers.find((layer) => layer.id === VECTOR_SOURCE_ID + '_fill');
expect(fillLayer.paint).to.eql({
'fill-color': [
'interpolate',
['linear'],
[
'coalesce',
[
'case',
['==', ['get', 'prop1'], null],
0.3819660112501051,
[
'max',
['min', ['to-number', ['get', 'prop1']], 3.618033988749895],
1.381966011250105,
],
],
0.3819660112501051,
],
0.3819660112501051,
'rgba(0,0,0,0)',
1.381966011250105,
'#ecf1f7',
1.6614745084375788,
'#d9e3ef',
1.9409830056250525,
'#c5d5e7',
2.2204915028125263,
'#b2c7df',
2.5,
'#9eb9d8',
2.7795084971874737,
'#8bacd0',
3.0590169943749475,
'#769fc8',
3.338525491562421,
'#6092c0',
],
'fill-opacity': 1,
});
});
});
}

View file

@ -1008,6 +1008,40 @@
}
}
{
"type": "doc",
"value": {
"id": "map:bff99716-e3dc-11ea-87d0-0242ac130003",
"index": ".kibana",
"source": {
"map" : {
"description":"shapes with mvt scaling",
"layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"76b9fc1d-1e8a-4d2f-9f9e-6ba2b19f24bb\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]",
"mapStateJSON":"{\"zoom\":3.75,\"center\":{\"lon\":80.01106,\"lat\":3.65009},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
"title":"geo_shape_mvt",
"uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"
},
"type" : "map",
"references" : [
{
"id":"561253e0-f731-11e8-8487-11b9dd924f96",
"name":"layer_1_source_index_pattern",
"type":"index-pattern"
}
],
"migrationVersion" : {
"map" : "7.9.0"
},
"updated_at" : "2020-08-10T18:27:39.805Z"
}
}
}
{
"type": "doc",
"value": {