[Maps] Add mvt support for ES doc sources (#75698)
This commit is contained in:
parent
981691d378
commit
d7869dee6e
|
@ -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",
|
||||
|
|
|
@ -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)';
|
||||
|
|
|
@ -18,7 +18,6 @@ export type MapFilters = {
|
|||
refreshTimerLastTriggeredAt?: string;
|
||||
timeFilters: TimeRange;
|
||||
zoom: number;
|
||||
geogridPrecision?: number;
|
||||
};
|
||||
|
||||
type ESSearchSourceSyncMeta = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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"}}}]}}
|
BIN
x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf
Normal file
BIN
x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf
Normal file
Binary file not shown.
28
x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts
Normal file
28
x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts
Normal 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': {},
|
||||
};
|
63
x-pack/plugins/maps/server/mvt/get_tile.test.ts
Normal file
63
x-pack/plugins/maps/server/mvt/get_tile.test.ts
Normal 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);
|
||||
});
|
||||
});
|
226
x-pack/plugins/maps/server/mvt/get_tile.ts
Normal file
226
x-pack/plugins/maps/server/mvt/get_tile.ts
Normal 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],
|
||||
],
|
||||
],
|
||||
};
|
||||
}
|
73
x-pack/plugins/maps/server/mvt/mvt_routes.ts
Normal file
73
x-pack/plugins/maps/server/mvt/mvt_routes.ts
Normal 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}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
72
x-pack/plugins/maps/server/mvt/util.ts
Normal file
72
x-pack/plugins/maps/server/mvt/util.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
|
|
29
x-pack/test/api_integration/apis/maps/get_tile.js
Normal file
29
x-pack/test/api_integration/apis/maps/get_tile.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
};
|
||||
|
|
75
x-pack/test/functional/apps/maps/mvt_scaling.js
Normal file
75
x-pack/test/functional/apps/maps/mvt_scaling.js
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue