Skip to content

Commit d7869de

Browse files
[Maps] Add mvt support for ES doc sources (#75698)
1 parent 981691d commit d7869de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1444
-143
lines changed

x-pack/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@
313313
"file-type": "^10.9.0",
314314
"font-awesome": "4.7.0",
315315
"fp-ts": "^2.3.1",
316+
"geojson-vt": "^3.2.1",
316317
"get-port": "^5.0.0",
317318
"getos": "^3.1.0",
318319
"git-url-parse": "11.1.2",
@@ -384,6 +385,7 @@
384385
"ui-select": "0.19.8",
385386
"uuid": "3.3.2",
386387
"vscode-languageserver": "^5.2.1",
388+
"vt-pbf": "^3.1.1",
387389
"webpack": "^4.41.5",
388390
"wellknown": "^0.5.0",
389391
"xml2js": "^0.4.22",

x-pack/plugins/maps/common/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export const MAP_PATH = 'map';
3333
export const GIS_API_PATH = `api/${APP_ID}`;
3434
export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`;
3535
export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`;
36+
export const API_ROOT_PATH = `/${GIS_API_PATH}`;
37+
38+
export const MVT_GETTILE_API_PATH = 'mvt/getTile';
39+
export const MVT_SOURCE_LAYER_NAME = 'source_layer';
40+
export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__';
41+
export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__';
3642

3743
const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`;
3844
export function getNewMapPath() {
@@ -220,6 +226,7 @@ export enum SCALING_TYPES {
220226
LIMIT = 'LIMIT',
221227
CLUSTERS = 'CLUSTERS',
222228
TOP_HITS = 'TOP_HITS',
229+
MVT = 'MVT',
223230
}
224231

225232
export const RGBA_0000 = 'rgba(0,0,0,0)';

x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export type MapFilters = {
1818
refreshTimerLastTriggeredAt?: string;
1919
timeFilters: TimeRange;
2020
zoom: number;
21-
geogridPrecision?: number;
2221
};
2322

2423
type ESSearchSourceSyncMeta = {

x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { FeatureCollection, GeoJsonProperties } from 'geojson';
78
import { MapExtent } from './descriptor_types';
9+
import { ES_GEO_FIELD_TYPE } from './constants';
810

911
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent;
1012

@@ -13,3 +15,11 @@ export function turfBboxToBounds(turfBbox: unknown): MapExtent;
1315
export function clampToLatBounds(lat: number): number;
1416

1517
export function clampToLonBounds(lon: number): number;
18+
19+
export function hitsToGeoJson(
20+
hits: Array<Record<string, unknown>>,
21+
flattenHit: (elasticSearchHit: Record<string, unknown>) => GeoJsonProperties,
22+
geoFieldName: string,
23+
geoFieldType: ES_GEO_FIELD_TYPE,
24+
epochMillisFields: string[]
25+
): FeatureCollection;

x-pack/plugins/maps/public/classes/fields/es_doc_field.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@ import { IVectorSource } from '../sources/vector_source';
1515

1616
export class ESDocField extends AbstractField implements IField {
1717
private readonly _source: IESSource;
18+
private readonly _canReadFromGeoJson: boolean;
1819

1920
constructor({
2021
fieldName,
2122
source,
2223
origin,
24+
canReadFromGeoJson = true,
2325
}: {
2426
fieldName: string;
2527
source: IESSource;
2628
origin: FIELD_ORIGIN;
29+
canReadFromGeoJson?: boolean;
2730
}) {
2831
super({ fieldName, origin });
2932
this._source = source;
33+
this._canReadFromGeoJson = canReadFromGeoJson;
3034
}
3135

3236
canValueBeFormatted(): boolean {
@@ -60,6 +64,10 @@ export class ESDocField extends AbstractField implements IField {
6064
return true;
6165
}
6266

67+
canReadFromGeoJson(): boolean {
68+
return this._canReadFromGeoJson;
69+
}
70+
6371
async getOrdinalFieldMetaRequest(): Promise<unknown> {
6472
const indexPatternField = await this._getIndexPatternField();
6573

x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -128,32 +128,41 @@ describe('syncData', () => {
128128
sinon.assert.notCalled(syncContext2.stopLoading);
129129
});
130130

131-
it('Should resync when changes to source params', async () => {
132-
const layer1: TiledVectorLayer = createLayer({}, {});
133-
const syncContext1 = new MockSyncContext({ dataFilters: {} });
134-
135-
await layer1.syncData(syncContext1);
136-
137-
const dataRequestDescriptor: DataRequestDescriptor = {
138-
data: defaultConfig,
139-
dataId: 'source',
140-
};
141-
const layer2: TiledVectorLayer = createLayer(
142-
{
143-
__dataRequests: [dataRequestDescriptor],
144-
},
145-
{ layerName: 'barfoo' }
146-
);
147-
const syncContext2 = new MockSyncContext({ dataFilters: {} });
148-
await layer2.syncData(syncContext2);
149-
150-
// @ts-expect-error
151-
sinon.assert.calledOnce(syncContext2.startLoading);
152-
// @ts-expect-error
153-
sinon.assert.calledOnce(syncContext2.stopLoading);
154-
155-
// @ts-expect-error
156-
const call = syncContext2.stopLoading.getCall(0);
157-
expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' });
131+
describe('Should resync when changes to source params: ', () => {
132+
[
133+
{ layerName: 'barfoo' },
134+
{ urlTemplate: 'https://sub.example.com/{z}/{x}/{y}.pbf' },
135+
{ minSourceZoom: 1 },
136+
{ maxSourceZoom: 12 },
137+
].forEach((changes) => {
138+
it(`change in ${Object.keys(changes).join(',')}`, async () => {
139+
const layer1: TiledVectorLayer = createLayer({}, {});
140+
const syncContext1 = new MockSyncContext({ dataFilters: {} });
141+
142+
await layer1.syncData(syncContext1);
143+
144+
const dataRequestDescriptor: DataRequestDescriptor = {
145+
data: defaultConfig,
146+
dataId: 'source',
147+
};
148+
const layer2: TiledVectorLayer = createLayer(
149+
{
150+
__dataRequests: [dataRequestDescriptor],
151+
},
152+
changes
153+
);
154+
const syncContext2 = new MockSyncContext({ dataFilters: {} });
155+
await layer2.syncData(syncContext2);
156+
157+
// @ts-expect-error
158+
sinon.assert.calledOnce(syncContext2.startLoading);
159+
// @ts-expect-error
160+
sinon.assert.calledOnce(syncContext2.stopLoading);
161+
162+
// @ts-expect-error
163+
const call = syncContext2.stopLoading.getCall(0);
164+
expect(call.args[2]).toEqual({ ...defaultConfig, ...changes });
165+
});
166+
});
158167
});
159168
});

x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,24 @@ export class TiledVectorLayer extends VectorLayer {
6363
);
6464
const prevDataRequest = this.getSourceDataRequest();
6565

66+
const templateWithMeta = await this._source.getUrlTemplateWithMeta(searchFilters);
6667
if (prevDataRequest) {
6768
const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
68-
const canSkipBecauseNoChanges =
69-
data.layerName === this._source.getLayerName() &&
70-
data.minSourceZoom === this._source.getMinZoom() &&
71-
data.maxSourceZoom === this._source.getMaxZoom();
72-
73-
if (canSkipBecauseNoChanges) {
74-
return null;
69+
if (data) {
70+
const canSkipBecauseNoChanges =
71+
data.layerName === this._source.getLayerName() &&
72+
data.minSourceZoom === this._source.getMinZoom() &&
73+
data.maxSourceZoom === this._source.getMaxZoom() &&
74+
data.urlTemplate === templateWithMeta.urlTemplate;
75+
76+
if (canSkipBecauseNoChanges) {
77+
return null;
78+
}
7579
}
7680
}
7781

7882
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
7983
try {
80-
const templateWithMeta = await this._source.getUrlTemplateWithMeta();
8184
stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, templateWithMeta, {});
8285
} catch (error) {
8386
onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message);
@@ -160,6 +163,11 @@ export class TiledVectorLayer extends VectorLayer {
160163
return false;
161164
}
162165

166+
if (!mbTileSource.tiles) {
167+
// Expected source is not compatible, so remove.
168+
return true;
169+
}
170+
163171
const isSourceDifferent =
164172
mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate ||
165173
mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom ||

x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import {
1515
SOURCE_BOUNDS_DATA_REQUEST_ID,
1616
FEATURE_VISIBLE_PROPERTY_NAME,
1717
EMPTY_FEATURE_COLLECTION,
18+
KBN_TOO_MANY_FEATURES_PROPERTY,
1819
LAYER_TYPE,
1920
FIELD_ORIGIN,
2021
LAYER_STYLE_TYPE,
22+
KBN_TOO_MANY_FEATURES_IMAGE_ID,
2123
} from '../../../../common/constants';
2224
import _ from 'lodash';
2325
import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property';
@@ -777,6 +779,8 @@ export class VectorLayer extends AbstractLayer {
777779
const sourceId = this.getId();
778780
const fillLayerId = this._getMbPolygonLayerId();
779781
const lineLayerId = this._getMbLineLayerId();
782+
const tooManyFeaturesLayerId = this._getMbTooManyFeaturesLayerId();
783+
780784
const hasJoins = this.hasJoins();
781785
if (!mbMap.getLayer(fillLayerId)) {
782786
const mbLayer = {
@@ -802,6 +806,30 @@ export class VectorLayer extends AbstractLayer {
802806
}
803807
mbMap.addLayer(mbLayer);
804808
}
809+
if (!mbMap.getLayer(tooManyFeaturesLayerId)) {
810+
const mbLayer = {
811+
id: tooManyFeaturesLayerId,
812+
type: 'fill',
813+
source: sourceId,
814+
paint: {},
815+
};
816+
if (mvtSourceLayer) {
817+
mbLayer['source-layer'] = mvtSourceLayer;
818+
}
819+
mbMap.addLayer(mbLayer);
820+
mbMap.setFilter(tooManyFeaturesLayerId, [
821+
'==',
822+
['get', KBN_TOO_MANY_FEATURES_PROPERTY],
823+
true,
824+
]);
825+
mbMap.setPaintProperty(
826+
tooManyFeaturesLayerId,
827+
'fill-pattern',
828+
KBN_TOO_MANY_FEATURES_IMAGE_ID
829+
);
830+
mbMap.setPaintProperty(tooManyFeaturesLayerId, 'fill-opacity', this.getAlpha());
831+
}
832+
805833
this.getCurrentStyle().setMBPaintProperties({
806834
alpha: this.getAlpha(),
807835
mbMap,
@@ -822,6 +850,9 @@ export class VectorLayer extends AbstractLayer {
822850
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
823851
mbMap.setFilter(lineLayerId, lineFilterExpr);
824852
}
853+
854+
this.syncVisibilityWithMb(mbMap, tooManyFeaturesLayerId);
855+
mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom());
825856
}
826857

827858
_syncStylePropertiesWithMb(mbMap) {
@@ -836,6 +867,19 @@ export class VectorLayer extends AbstractLayer {
836867
type: 'geojson',
837868
data: EMPTY_FEATURE_COLLECTION,
838869
});
870+
} else if (mbSource.type !== 'geojson') {
871+
// Recreate source when existing source is not geojson. This can occur when layer changes from tile layer to vector layer.
872+
this.getMbLayerIds().forEach((mbLayerId) => {
873+
if (mbMap.getLayer(mbLayerId)) {
874+
mbMap.removeLayer(mbLayerId);
875+
}
876+
});
877+
878+
mbMap.removeSource(this._getMbSourceId());
879+
mbMap.addSource(this._getMbSourceId(), {
880+
type: 'geojson',
881+
data: EMPTY_FEATURE_COLLECTION,
882+
});
839883
}
840884
}
841885

@@ -865,13 +909,18 @@ export class VectorLayer extends AbstractLayer {
865909
return this.makeMbLayerId('fill');
866910
}
867911

912+
_getMbTooManyFeaturesLayerId() {
913+
return this.makeMbLayerId('toomanyfeatures');
914+
}
915+
868916
getMbLayerIds() {
869917
return [
870918
this._getMbPointLayerId(),
871919
this._getMbTextLayerId(),
872920
this._getMbSymbolLayerId(),
873921
this._getMbLineLayerId(),
874922
this._getMbPolygonLayerId(),
923+
this._getMbTooManyFeaturesLayerId(),
875924
];
876925
}
877926

x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
import { MapExtent, MapFilters } from '../../../../common/descriptor_types';
6+
import { MapExtent, VectorSourceRequestMeta } from '../../../../common/descriptor_types';
77

88
jest.mock('../../../kibana_services');
99

@@ -19,6 +19,7 @@ import { SearchSource } from '../../../../../../../src/plugins/data/public/searc
1919

2020
export class MockSearchSource {
2121
setField = jest.fn();
22+
setParent() {}
2223
}
2324

2425
describe('ESGeoGridSource', () => {
@@ -104,6 +105,9 @@ describe('ESGeoGridSource', () => {
104105
async create() {
105106
return mockSearchSource as SearchSource;
106107
},
108+
createEmpty() {
109+
return mockSearchSource as SearchSource;
110+
},
107111
},
108112
};
109113

@@ -120,16 +124,24 @@ describe('ESGeoGridSource', () => {
120124
maxLat: 80,
121125
};
122126

123-
const mapFilters: MapFilters = {
127+
const mapFilters: VectorSourceRequestMeta = {
124128
geogridPrecision: 4,
125129
filters: [],
126130
timeFilters: {
127131
from: 'now',
128132
to: '15m',
129133
mode: 'relative',
130134
},
131-
// extent,
135+
extent,
136+
applyGlobalQuery: true,
137+
fieldNames: [],
132138
buffer: extent,
139+
sourceQuery: {
140+
query: '',
141+
language: 'KQL',
142+
queryLastTriggeredAt: '2019-04-25T20:53:22.331Z',
143+
},
144+
sourceMeta: null,
133145
zoom: 0,
134146
};
135147

0 commit comments

Comments
 (0)