mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
1d8bc7ede1
commit
d34408876a
21 changed files with 229 additions and 22 deletions
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'))`
|
||||
)
|
||||
|
|
|
@ -45,6 +45,7 @@ export default function ({ getPageObjects, getService }) {
|
|||
|
||||
expect(searchParams).to.eql({
|
||||
geometryFieldName: 'geo.coordinates',
|
||||
hasLabels: 'false',
|
||||
index: 'logstash-*',
|
||||
gridPrecision: 8,
|
||||
renderAs: 'grid',
|
||||
|
|
|
@ -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))',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue