[maps] Use label features from ES vector tile search API to fix multiple labels (#132080)

* [maps] mvt labels

* eslint

* only request labels when needed

* update vector tile integration tests for hasLabels parameter

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* fix tests

* fix test

* only add _mvt_label_position filter when vector tiles are from ES vector tile search API

* review feedback

* include hasLabels in source data

* fix jest test

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2022-05-20 08:06:25 -06:00 committed by GitHub
parent 1d8bc7ede1
commit d34408876a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 229 additions and 22 deletions

View file

@ -21,6 +21,7 @@ export function getAggsTileRequest({
encodedRequestBody,
geometryFieldName,
gridPrecision,
hasLabels,
index,
renderAs = RENDER_AS.POINT,
x,
@ -30,6 +31,7 @@ export function getAggsTileRequest({
encodedRequestBody: string;
geometryFieldName: string;
gridPrecision: number;
hasLabels: boolean;
index: string;
renderAs: RENDER_AS;
x: number;
@ -50,6 +52,7 @@ export function getAggsTileRequest({
aggs: requestBody.aggs,
fields: requestBody.fields,
runtime_mappings: requestBody.runtime_mappings,
with_labels: hasLabels,
},
};
}
@ -57,6 +60,7 @@ export function getAggsTileRequest({
export function getHitsTileRequest({
encodedRequestBody,
geometryFieldName,
hasLabels,
index,
x,
y,
@ -64,6 +68,7 @@ export function getHitsTileRequest({
}: {
encodedRequestBody: string;
geometryFieldName: string;
hasLabels: boolean;
index: string;
x: number;
y: number;
@ -86,6 +91,7 @@ export function getHitsTileRequest({
),
runtime_mappings: requestBody.runtime_mappings,
track_total_hits: typeof requestBody.size === 'number' ? requestBody.size + 1 : false,
with_labels: hasLabels,
},
};
}

View file

@ -87,6 +87,7 @@ export class HeatmapLayer extends AbstractLayer {
async syncData(syncContext: DataRequestContext) {
await syncMvtSourceData({
hasLabels: false,
layerId: this.getId(),
layerName: await this.getDisplayName(),
prevDataRequest: this.getSourceDataRequest(),

View file

@ -52,6 +52,7 @@ describe('syncMvtSourceData', () => {
const syncContext = new MockSyncContext({ dataFilters: {} });
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: undefined,
@ -82,6 +83,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf',
refreshToken: '12345',
hasLabels: false,
});
});
@ -99,6 +101,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -112,6 +115,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -142,6 +146,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -155,6 +160,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -182,6 +188,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -195,6 +202,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -230,6 +238,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -243,6 +252,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'barfoo', // tileSourceLayer is different then mockSource
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -270,6 +280,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -283,6 +294,7 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -310,6 +322,7 @@ describe('syncMvtSourceData', () => {
};
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
@ -323,6 +336,49 @@ describe('syncMvtSourceData', () => {
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
requestMeta: { ...prevRequestMeta },
source: mockSource,
syncContext,
});
// @ts-expect-error
sinon.assert.calledOnce(syncContext.startLoading);
// @ts-expect-error
sinon.assert.calledOnce(syncContext.stopLoading);
});
test('Should re-sync when hasLabel state changes', async () => {
const syncContext = new MockSyncContext({ dataFilters: {} });
const prevRequestMeta = {
...syncContext.dataFilters,
applyGlobalQuery: true,
applyGlobalTime: true,
applyForceRefresh: true,
fieldNames: [],
sourceMeta: {},
isForceRefresh: false,
isFeatureEditorOpenForLayer: false,
};
await syncMvtSourceData({
hasLabels: true,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: {
getMeta: () => {
return prevRequestMeta;
},
getData: () => {
return {
tileMinZoom: 4,
tileMaxZoom: 14,
tileSourceLayer: 'aggs',
tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345',
refreshToken: '12345',
hasLabels: false,
};
},
} as unknown as DataRequest,
@ -340,6 +396,7 @@ describe('syncMvtSourceData', () => {
const syncContext = new MockSyncContext({ dataFilters: {} });
await syncMvtSourceData({
hasLabels: false,
layerId: 'layer1',
layerName: 'my layer',
prevDataRequest: undefined,

View file

@ -20,9 +20,11 @@ export interface MvtSourceData {
tileMaxZoom: number;
tileUrl: string;
refreshToken: string;
hasLabels: boolean;
}
export async function syncMvtSourceData({
hasLabels,
layerId,
layerName,
prevDataRequest,
@ -30,6 +32,7 @@ export async function syncMvtSourceData({
source,
syncContext,
}: {
hasLabels: boolean;
layerId: string;
layerName: string;
prevDataRequest: DataRequest | undefined;
@ -56,7 +59,10 @@ export async function syncMvtSourceData({
},
});
const canSkip =
!syncContext.forceRefreshDueToDrawing && noChangesInSourceState && noChangesInSearchState;
!syncContext.forceRefreshDueToDrawing &&
noChangesInSourceState &&
noChangesInSearchState &&
prevData.hasLabels === hasLabels;
if (canSkip) {
return;
@ -72,7 +78,7 @@ export async function syncMvtSourceData({
? uuid()
: prevData.refreshToken;
const tileUrl = await source.getTileUrl(requestMeta, refreshToken);
const tileUrl = await source.getTileUrl(requestMeta, refreshToken, hasLabels);
if (source.isESSource()) {
syncContext.inspectorAdapters.vectorTiles.addLayer(layerId, layerName, tileUrl);
}
@ -82,6 +88,7 @@ export async function syncMvtSourceData({
tileMinZoom: source.getMinZoom(),
tileMaxZoom: source.getMaxZoom(),
refreshToken,
hasLabels,
};
syncContext.stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, sourceData, {});
} catch (error) {

View file

@ -219,6 +219,7 @@ export class MvtVectorLayer extends AbstractVectorLayer {
await this._syncSupportsFeatureEditing({ syncContext, source: this.getSource() });
await syncMvtSourceData({
hasLabels: this.getCurrentStyle().hasLabels(),
layerId: this.getId(),
layerName: await this.getDisplayName(),
prevDataRequest: this.getSourceDataRequest(),

View file

@ -736,7 +736,10 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
}
}
const isSourceGeoJson = !this.getSource().isMvt();
const filterExpr = getPointFilterExpression(
isSourceGeoJson,
this.getSource().isESSource(),
this._getJoinFilterExpression(),
timesliceMaskConfig
);
@ -843,6 +846,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer {
const isSourceGeoJson = !this.getSource().isMvt();
const filterExpr = getLabelFilterExpression(
isSourceGeoJson,
this.getSource().isESSource(),
this._getJoinFilterExpression(),
timesliceMaskConfig
);

View file

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

View file

@ -471,7 +471,11 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
return 'aggs';
}
async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise<string> {
async getTileUrl(
searchFilters: VectorSourceRequestMeta,
refreshToken: string,
hasLabels: boolean
): Promise<string> {
const indexPattern = await this.getIndexPattern();
const searchSource = await this.makeSearchSource(searchFilters, 0);
searchSource.setField('aggs', this.getValueAggsDsl(indexPattern));
@ -484,6 +488,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo
?geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&gridPrecision=${this._getGeoGridPrecisionResolutionDelta()}\
&hasLabels=${hasLabels}\
&requestBody=${encodeMvtResponseBody(searchSource.getSearchRequestBody())}\
&renderAs=${this._descriptor.requestType}\
&token=${refreshToken}`;

View file

@ -114,9 +114,9 @@ describe('ESSearchSource', () => {
geoField: geoFieldName,
indexPatternId: 'ipId',
});
const tileUrl = await esSearchSource.getTileUrl(searchFilters, '1234');
const tileUrl = await esSearchSource.getTileUrl(searchFilters, '1234', false);
expect(tileUrl).toBe(
`rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'1'%3A('0'%3Asize%2C'1'%3A1000)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A'tooltipField%3A%20foobar'))%2C'6'%3A('0'%3AfieldsFromSource%2C'1'%3A!(tooltipField%2CstyleField))%2C'7'%3A('0'%3Asource%2C'1'%3A!(tooltipField%2CstyleField))))&token=1234`
`rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&hasLabels=false&requestBody=(foobar%3AES_DSL_PLACEHOLDER%2Cparams%3A('0'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'1'%3A('0'%3Asize%2C'1'%3A1000)%2C'2'%3A('0'%3Afilter%2C'1'%3A!())%2C'3'%3A('0'%3Aquery)%2C'4'%3A('0'%3Aindex%2C'1'%3A(fields%3A()%2Ctitle%3A'foobar-title-*'))%2C'5'%3A('0'%3Aquery%2C'1'%3A(language%3AKQL%2Cquery%3A'tooltipField%3A%20foobar'))%2C'6'%3A('0'%3AfieldsFromSource%2C'1'%3A!(tooltipField%2CstyleField))%2C'7'%3A('0'%3Asource%2C'1'%3A!(tooltipField%2CstyleField))))&token=1234`
);
});
});

View file

@ -810,7 +810,11 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
return 'hits';
}
async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise<string> {
async getTileUrl(
searchFilters: VectorSourceRequestMeta,
refreshToken: string,
hasLabels: boolean
): Promise<string> {
const indexPattern = await this.getIndexPattern();
const indexSettings = await loadIndexSettings(indexPattern.title);
@ -847,6 +851,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource
return `${mvtUrlServicePath}\
?geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&hasLabels=${hasLabels}\
&requestBody=${encodeMvtResponseBody(searchSource.getSearchRequestBody())}\
&token=${refreshToken}`;
}

View file

@ -13,7 +13,11 @@ export interface IMvtVectorSource extends IVectorSource {
* IMvtVectorSource.getTileUrl returns the tile source URL.
* Append refreshToken as a URL parameter to force tile re-fetch on refresh (not required)
*/
getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise<string>;
getTileUrl(
searchFilters: VectorSourceRequestMeta,
refreshToken: string,
hasLabels: boolean
): Promise<string>;
/*
* Tile vector sources can contain multiple layers. For example, elasticsearch _mvt tiles contain the layers "hits", "aggs", and "meta".

View file

@ -94,9 +94,9 @@ export function makeMbClampedNumberExpression({
];
}
export function getHasLabel(label: StaticTextProperty | DynamicTextProperty) {
export function getHasLabel(label: StaticTextProperty | DynamicTextProperty): boolean {
return label.isDynamic()
? label.isComplete()
: (label as StaticTextProperty).getOptions().value != null &&
(label as StaticTextProperty).getOptions().value.length;
(label as StaticTextProperty).getOptions().value.length > 0;
}

View file

@ -115,6 +115,12 @@ export interface IVectorStyle extends IStyle {
mbMap: MbMap,
mbSourceId: string
) => boolean;
/*
* Returns true when "Label" style configuration is complete and map shows a label for layer features.
*/
hasLabels: () => boolean;
arePointsSymbolizedAsCircles: () => boolean;
setMBPaintProperties: ({
alpha,
@ -674,14 +680,14 @@ export class VectorStyle implements IVectorStyle {
}
_getLegendDetailStyleProperties = () => {
const hasLabel = getHasLabel(this._labelStyleProperty);
const hasLabels = this.hasLabels();
return this.getDynamicPropertiesArray().filter((styleProperty) => {
const styleName = styleProperty.getStyleName();
if ([VECTOR_STYLES.ICON_ORIENTATION, VECTOR_STYLES.LABEL_TEXT].includes(styleName)) {
return false;
}
if (!hasLabel && LABEL_STYLES.includes(styleName)) {
if (!hasLabels && LABEL_STYLES.includes(styleName)) {
// do not render legend for label styles when there is no label
return false;
}
@ -768,6 +774,10 @@ export class VectorStyle implements IVectorStyle {
return !this._symbolizeAsStyleProperty.isSymbolizedAsIcon();
}
hasLabels() {
return getHasLabel(this._labelStyleProperty);
}
setMBPaintProperties({
alpha,
mbMap,

View file

@ -55,7 +55,7 @@ export function getFillFilterExpression(
): FilterSpecification {
return getFilterExpression(
[
// explicit EXCLUDE_CENTROID_FEATURES filter not needed. Centroids are points and are filtered out by geometry narrowing
// explicit "exclude centroid features" filter not needed. Label features are points and are filtered out by geometry narrowing
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
@ -73,7 +73,7 @@ export function getLineFilterExpression(
): FilterSpecification {
return getFilterExpression(
[
// explicit EXCLUDE_CENTROID_FEATURES filter not needed. Centroids are points and are filtered out by geometry narrowing
// explicit "exclude centroid features" filter not needed. Label features are points and are filtered out by geometry narrowing
[
'any',
['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON],
@ -94,18 +94,25 @@ const IS_POINT_FEATURE = [
];
export function getPointFilterExpression(
isSourceGeoJson: boolean,
isESSource: boolean,
joinFilter?: FilterSpecification,
timesliceMaskConfig?: TimesliceMaskConfig
): FilterSpecification {
return getFilterExpression(
[EXCLUDE_CENTROID_FEATURES, IS_POINT_FEATURE],
joinFilter,
timesliceMaskConfig
);
const filters: FilterSpecification[] = [];
if (isSourceGeoJson) {
filters.push(EXCLUDE_CENTROID_FEATURES);
} else if (!isSourceGeoJson && isESSource) {
filters.push(['!=', ['get', '_mvt_label_position'], true]);
}
filters.push(IS_POINT_FEATURE);
return getFilterExpression(filters, joinFilter, timesliceMaskConfig);
}
export function getLabelFilterExpression(
isSourceGeoJson: boolean,
isESSource: boolean,
joinFilter?: FilterSpecification,
timesliceMaskConfig?: TimesliceMaskConfig
): FilterSpecification {
@ -116,6 +123,8 @@ export function getLabelFilterExpression(
// For GeoJSON sources, show label for centroid features or point/multi-point features only.
// no explicit isCentroidFeature filter is needed, centroids are points and are included in the geometry filter.
filters.push(IS_POINT_FEATURE);
} else if (!isSourceGeoJson && isESSource) {
filters.push(['==', ['get', '_mvt_label_position'], true]);
}
return getFilterExpression(filters, joinFilter, timesliceMaskConfig);

View file

@ -11,7 +11,7 @@ test('Should return elasticsearch vector tile request for aggs tiles', () => {
expect(
getTileRequest({
layerId: '1',
tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`,
tileUrl: `/pof/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=false&index=kibana_sample_data_logs&gridPrecision=8&requestBody=(_source%3A(excludes%3A!())%2Caggs%3A()%2Cfields%3A!((field%3A'%40timestamp'%2Cformat%3Adate_time)%2C(field%3Atimestamp%2Cformat%3Adate_time)%2C(field%3Autc_time%2Cformat%3Adate_time))%2Cquery%3A(bool%3A(filter%3A!((match_phrase%3A(machine.os.keyword%3Aios))%2C(range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A'2022-04-22T16%3A46%3A00.744Z'%2Clte%3A'2022-04-29T16%3A46%3A05.345Z'))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A'emit(doc%5B!'timestamp!'%5D.value.getHour())%3B')%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A0%2Cstored_fields%3A!('*'))&renderAs=heatmap&token=e8bff005-ccea-464a-ae56-2061b4f8ce68`,
x: 3,
y: 0,
z: 2,
@ -71,6 +71,7 @@ test('Should return elasticsearch vector tile request for aggs tiles', () => {
type: 'long',
},
},
with_labels: false,
},
});
});
@ -79,7 +80,7 @@ test('Should return elasticsearch vector tile request for hits tiles', () => {
expect(
getTileRequest({
layerId: '1',
tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`,
tileUrl: `http://localhost:5601/pof/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&hasLabels=true&index=kibana_sample_data_logs&requestBody=(_source%3A!f%2Cdocvalue_fields%3A!()%2Cquery%3A(bool%3A(filter%3A!((range%3A(timestamp%3A(format%3Astrict_date_optional_time%2Cgte%3A%272022-04-22T16%3A46%3A00.744Z%27%2Clte%3A%272022-04-29T16%3A46%3A05.345Z%27))))%2Cmust%3A!()%2Cmust_not%3A!()%2Cshould%3A!()))%2Cruntime_mappings%3A(hour_of_day%3A(script%3A(source%3A%27emit(doc%5B!%27timestamp!%27%5D.value.getHour())%3B%27)%2Ctype%3Along))%2Cscript_fields%3A()%2Csize%3A10000%2Cstored_fields%3A!(geo.coordinates))&token=415049b6-bb0a-444a-a7b9-89717db5183c`,
x: 0,
y: 0,
z: 2,
@ -118,6 +119,7 @@ test('Should return elasticsearch vector tile request for hits tiles', () => {
},
},
track_total_hits: 10001,
with_labels: true,
},
});
});

View file

@ -35,11 +35,16 @@ export function getTileRequest(tileRequest: TileRequest): { path?: string; body?
}
const geometryFieldName = searchParams.get('geometryFieldName') as string;
const hasLabels = searchParams.has('hasLabels')
? searchParams.get('hasLabels') === 'true'
: false;
if (tileRequest.tileUrl.includes(MVT_GETGRIDTILE_API_PATH)) {
return getAggsTileRequest({
encodedRequestBody,
geometryFieldName,
gridPrecision: parseInt(searchParams.get('gridPrecision') as string, 10),
hasLabels,
index,
renderAs: searchParams.get('renderAs') as RENDER_AS,
x: tileRequest.x,
@ -52,6 +57,7 @@ export function getTileRequest(tileRequest: TileRequest): { path?: string; body?
return getHitsTileRequest({
encodedRequestBody,
geometryFieldName,
hasLabels,
index,
x: tileRequest.x,
y: tileRequest.y,

View file

@ -44,6 +44,7 @@ export function initMVTRoutes({
}),
query: schema.object({
geometryFieldName: schema.string(),
hasLabels: schema.boolean(),
requestBody: schema.string(),
index: schema.string(),
token: schema.maybe(schema.string()),
@ -65,6 +66,7 @@ export function initMVTRoutes({
tileRequest = getHitsTileRequest({
encodedRequestBody: query.requestBody as string,
geometryFieldName: query.geometryFieldName as string,
hasLabels: query.hasLabels as boolean,
index: query.index as string,
x,
y,
@ -102,6 +104,7 @@ export function initMVTRoutes({
}),
query: schema.object({
geometryFieldName: schema.string(),
hasLabels: schema.boolean(),
requestBody: schema.string(),
index: schema.string(),
renderAs: schema.string(),
@ -126,6 +129,7 @@ export function initMVTRoutes({
encodedRequestBody: query.requestBody as string,
geometryFieldName: query.geometryFieldName as string,
gridPrecision: parseInt(query.gridPrecision, 10),
hasLabels: query.hasLabels as boolean,
index: query.index as string,
renderAs: query.renderAs as RENDER_AS,
x,

View file

@ -9,12 +9,22 @@ import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
function findFeature(layer, callbackFn) {
for (let i = 0; i < layer.length; i++) {
const feature = layer.feature(i);
if (callbackFn(feature)) {
return feature;
}
}
}
export default function ({ getService }) {
const supertest = getService('supertest');
describe('getGridTile', () => {
const URL = `/api/maps/mvt/getGridTile/3/2/3.pbf\
?geometryFieldName=geo.coordinates\
&hasLabels=false\
&index=logstash-*\
&gridPrecision=8\
&requestBody=(_source:(excludes:!()),aggs:(avg_of_bytes:(avg:(field:bytes))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))`;
@ -152,6 +162,33 @@ export default function ({ getService }) {
]);
});
it('should return vector tile containing label features when hasLabels is true', async () => {
const resp = await supertest
.get(URL.replace('hasLabels=false', 'hasLabels=true') + '&renderAs=hex')
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.aggs;
expect(layer.length).to.be(2);
const labelFeature = findFeature(layer, (feature) => {
return feature.properties._mvt_label_position === true;
});
expect(labelFeature).not.to.be(undefined);
expect(labelFeature.type).to.be(1);
expect(labelFeature.extent).to.be(4096);
expect(labelFeature.id).to.be(undefined);
expect(labelFeature.properties).to.eql({
_count: 1,
_key: '85264a33fffffff',
'avg_of_bytes.value': 9252,
_mvt_label_position: true,
});
expect(labelFeature.loadGeometry()).to.eql([[{ x: 93, y: 667 }]]);
});
it('should return vector tile with meta layer', async () => {
const resp = await supertest
.get(URL + '&renderAs=point')

View file

@ -27,6 +27,7 @@ export default function ({ getService }) {
.get(
`/api/maps/mvt/getTile/2/1/1.pbf\
?geometryFieldName=geo.coordinates\
&hasLabels=false\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),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,'@timestamp'))`
)
@ -85,11 +86,57 @@ export default function ({ getService }) {
]);
});
it('should return ES vector tile containing label features when hasLabels is true', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getTile/2/1/1.pbf\
?geometryFieldName=geo.coordinates\
&hasLabels=true\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),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,'@timestamp'))`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
expect(resp.headers['content-encoding']).to.be('gzip');
expect(resp.headers['content-disposition']).to.be('inline');
expect(resp.headers['content-type']).to.be('application/x-protobuf');
expect(resp.headers['cache-control']).to.be('public, max-age=3600');
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers.hits;
expect(layer.length).to.be(4); // 2 docs + 2 label features
// Verify ES document
const feature = findFeature(layer, (feature) => {
return (
feature.properties._id === 'AU_x3_BsGFA8no6Qjjug' &&
feature.properties._mvt_label_position === true
);
});
expect(feature).not.to.be(undefined);
expect(feature.type).to.be(1);
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({
'@timestamp': '1442709961071',
_id: 'AU_x3_BsGFA8no6Qjjug',
_index: 'logstash-2015.09.20',
bytes: 9252,
'machine.os.raw': 'ios',
_mvt_label_position: true,
});
expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]);
});
it('should return error when index does not exist', async () => {
await supertest
.get(
`/api/maps/mvt/getTile/2/1/1.pbf\
?geometryFieldName=geo.coordinates\
&hasLabels=false\
&index=notRealIndex\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),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,'@timestamp'))`
)

View file

@ -45,6 +45,7 @@ export default function ({ getPageObjects, getService }) {
expect(searchParams).to.eql({
geometryFieldName: 'geo.coordinates',
hasLabels: 'false',
index: 'logstash-*',
gridPrecision: 8,
renderAs: 'grid',

View file

@ -50,6 +50,7 @@ export default function ({ getPageObjects, getService }) {
expect(searchParams).to.eql({
geometryFieldName: 'geometry',
hasLabels: 'false',
index: 'geo_shapes*',
requestBody:
'(_source:!f,docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))',