[Maps] Add styling and tooltip support to mapbox mvt vector tile sources (#64488)

* tmp commit

* rename

* more boilerpalte

* more boiler

* more boilerpalte

* typing

* fix import

* boilerplate

* more boiler

* enable custom palettes

* fix label text and orientation

* fix merge errors

* remove dupe import

* stash commit

* tmp commit

* debounce settings

* return null

* slight rearrangement

* tooltip guard

* minor tweaks

* feedback

* ts fixes

* ts fixes

* more ts fixes

* ts fixes

* jest test

* fix typo

* spacing

* fix typing

* add unit test

* add more tests

* add snapshot test

* add snapshot

* add field editor snapshot test

* fix snapshot

* add snapshot

* remove unused import

* test stub for mvt layer

fix optional param

more checks

* add snapshot test

more unit tests

more unit tests

ts fixes

* add data syncing unit test

* fix autorefactor

* fix merge and replace snapshots

* field editor changes

* field editor changes

* ts fixes

* update snapshots

* fix things

* fix names

* fix tooltip

* add more error handling

* improve copy

* styling changes

* style option box a little better

* ts fixes

* fix console error

* remove mbProperties from interface

* remove unused method

* remove cruft

* rename for consistency

* remove unused param

* feedback

* feedback

* ensure properties are always present

* handle possible null values

* feedback

* typo

* update SIEM

* feedback

* remove cruft

* remove unused translations

* feedback

* improve readability

* fix brittle test

* fix snapshot after master merge

* remove unused method

* feedback

* revert some feedback

* remove micro-optimization

* initialize in constructor

* simplify wording

* add snapshot

* naming

* add clarifying comment

* remove unused import

* sanitize tooltips

* remove cruft

* feedback

* fix typo

* remove export

* Design fixes

* clean up supportsAutoDomain

* remove patch.txt

* cleanup

* clean-up

* Merge in styling changes

* Tweak message format

* fix broken import

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: miukimiu <elizabet.oliveira@elastic.co>
Co-authored-by: Nathan Reese <reese.nathan@gmail.com>
This commit is contained in:
Thomas Neirynck 2020-07-02 09:24:56 -04:00 committed by GitHub
parent 3f808688e1
commit 9c76f19186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2407 additions and 380 deletions

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import React, { Component } from 'react';
import React, { Component, ReactNode } from 'react';
import { EuiFormRow, EuiDualRange } from '@elastic/eui';
import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row';
import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range';
@ -32,7 +32,7 @@ export type ValueMember = EuiDualRangeProps['value'][0];
interface Props extends Omit<EuiDualRangeProps, 'value' | 'onChange' | 'min' | 'max'> {
value?: Value;
allowEmptyRange?: boolean;
label?: string;
label?: string | ReactNode;
formRowDisplay?: EuiFormRowDisplayKeys;
onChange?: (val: [string, string]) => void;
min?: number;

View file

@ -223,6 +223,11 @@ export enum SCALING_TYPES {
export const RGBA_0000 = 'rgba(0,0,0,0)';
export enum MVT_FIELD_TYPE {
STRING = 'String',
NUMBER = 'Number',
}
export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID';
export enum INITIAL_LOCATION {

View file

@ -5,6 +5,6 @@
*/
export * from './data_request_descriptor_types';
export * from './descriptor_types';
export * from './sources';
export * from './map_descriptor';
export * from './style_property_descriptor_types';

View file

@ -5,6 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { GeoJsonProperties } from 'geojson';
import { Query } from '../../../../../src/plugins/data/common';
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
@ -39,8 +40,9 @@ export type Goto = {
};
export type TooltipFeature = {
id: number;
id?: number | string;
layerId: string;
mbProperties: GeoJsonProperties;
};
export type TooltipState = {

View file

@ -7,7 +7,14 @@
import { FeatureCollection } from 'geojson';
import { Query } from 'src/plugins/data/public';
import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
import {
AGG_TYPE,
GRID_RESOLUTION,
RENDER_AS,
SORT_ORDER,
SCALING_TYPES,
MVT_FIELD_TYPE,
} from '../constants';
import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types';
import { DataRequestDescriptor } from './data_request_descriptor_types';
@ -96,18 +103,34 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor &
urlTemplate: string;
};
export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & {
export type MVTFieldDescriptor = {
name: string;
type: MVT_FIELD_TYPE;
};
export type TiledSingleLayerVectorSourceSettings = {
urlTemplate: string;
layerName: string;
// These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate.
// These are _not_ the visible zoom-range of the data on a map.
// Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset.
// e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels
// These are important so mapbox does not issue invalid requests based on the zoom level.
// Tiled layer data cannot be displayed at lower levels of zoom than that they are stored in the tileset.
// e.g. building footprints at level 14 cannot be displayed at level 0.
minSourceZoom: number;
// Tiled layer data can be displayed at higher levels of zoom than that they are stored in the tileset.
// e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels
maxSourceZoom: number;
fields: MVTFieldDescriptor[];
};
export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor &
TiledSingleLayerVectorSourceSettings & {
tooltipProperties: string[];
};
export type GeojsonFileSourceDescriptor = {
__featureCollection: FeatureCollection;
name: string;

View file

@ -128,6 +128,10 @@ export class ESAggField implements IESAggField {
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null;
}
supportsAutoDomain(): boolean {
return true;
}
}
export function esAggFieldsFactory(

View file

@ -20,6 +20,12 @@ export interface IField {
isValid(): boolean;
getOrdinalFieldMetaRequest(): Promise<unknown>;
getCategoricalFieldMetaRequest(size: number): Promise<unknown>;
// Determines whether Maps-app can automatically determine the domain of the field-values
// if this is not the case (e.g. for .mvt tiled data),
// then styling properties that require the domain to be known cannot use this property.
supportsAutoDomain(): boolean;
supportsFieldMeta(): boolean;
}
@ -80,4 +86,8 @@ export class AbstractField implements IField {
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
return null;
}
supportsAutoDomain(): boolean {
return true;
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 { AbstractField, IField } from './field';
import { FIELD_ORIGIN, MVT_FIELD_TYPE } from '../../../common/constants';
import { ITiledSingleLayerVectorSource, IVectorSource } from '../sources/vector_source';
import { MVTFieldDescriptor } from '../../../common/descriptor_types';
export class MVTField extends AbstractField implements IField {
private readonly _source: ITiledSingleLayerVectorSource;
private readonly _type: MVT_FIELD_TYPE;
constructor({
fieldName,
type,
source,
origin,
}: {
fieldName: string;
source: ITiledSingleLayerVectorSource;
origin: FIELD_ORIGIN;
type: MVT_FIELD_TYPE;
}) {
super({ fieldName, origin });
this._source = source;
this._type = type;
}
getMVTFieldDescriptor(): MVTFieldDescriptor {
return {
type: this._type,
name: this.getName(),
};
}
getSource(): IVectorSource {
return this._source;
}
async getDataType(): Promise<string> {
if (this._type === MVT_FIELD_TYPE.STRING) {
return 'string';
} else if (this._type === MVT_FIELD_TYPE.NUMBER) {
return 'number';
} else {
throw new Error(`Unrecognized MVT field-type ${this._type}`);
}
}
async getLabel(): Promise<string> {
return this.getName();
}
supportsAutoDomain() {
return false;
}
}

View file

@ -60,6 +60,10 @@ export class TopTermPercentageField implements IESAggField {
return 0;
}
supportsAutoDomain(): boolean {
return true;
}
supportsFieldMeta(): boolean {
return false;
}

View file

@ -0,0 +1,40 @@
/*
* 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 sinon from 'sinon';
import { DataRequestContext } from '../../../actions';
import { DataMeta, MapFilters } from '../../../../common/descriptor_types';
export class MockSyncContext implements DataRequestContext {
dataFilters: MapFilters;
isRequestStillActive: (dataId: string, requestToken: symbol) => boolean;
onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => void;
registerCancelCallback: (requestToken: symbol, callback: () => void) => void;
startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => void;
stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => void;
updateSourceData: (newData: unknown) => void;
constructor({ dataFilters }: { dataFilters: Partial<MapFilters> }) {
const mapFilters: MapFilters = {
filters: [],
timeFilters: {
from: 'now',
to: '15m',
mode: 'relative',
},
zoom: 0,
...dataFilters,
};
this.dataFilters = mapFilters;
this.isRequestStillActive = sinon.spy();
this.onLoadError = sinon.spy();
this.registerCancelCallback = sinon.spy();
this.startLoading = sinon.spy();
this.stopLoading = sinon.spy();
this.updateSourceData = sinon.spy();
}
}

View file

@ -16,7 +16,7 @@ import {
import { getFileUploadComponent } from '../../../kibana_services';
import { GeojsonFileSource } from '../../sources/geojson_file_source';
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
// @ts-ignore
// @ts-expect-error
import { createDefaultLayerDescriptor } from '../../sources/es_search_source';
import { RenderWizardArguments } from '../../layers/layer_wizard_registry';

View file

@ -91,7 +91,7 @@ export class HeatmapLayer extends VectorLayer {
resolution: this.getSource().getGridResolution(),
});
mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
}
getLayerTypeIconName() {

View file

@ -325,27 +325,28 @@ export class AbstractLayer implements ILayer {
return this._source.getMinZoom();
}
_getMbSourceId() {
return this.getId();
}
_requiresPrevSourceCleanup(mbMap: unknown) {
return false;
}
_removeStaleMbSourcesAndLayers(mbMap: unknown) {
if (this._requiresPrevSourceCleanup(mbMap)) {
// @ts-ignore
// @ts-expect-error
const mbStyle = mbMap.getStyle();
// @ts-ignore
// @ts-expect-error
mbStyle.layers.forEach((mbLayer) => {
// @ts-ignore
if (this.ownsMbLayerId(mbLayer.id)) {
// @ts-ignore
// @ts-expect-error
mbMap.removeLayer(mbLayer.id);
}
});
// @ts-ignore
Object.keys(mbStyle.sources).some((mbSourceId) => {
// @ts-ignore
if (this.ownsMbSourceId(mbSourceId)) {
// @ts-ignore
// @ts-expect-error
mbMap.removeSource(mbSourceId);
}
});
@ -429,7 +430,7 @@ export class AbstractLayer implements ILayer {
throw new Error('Should implement AbstractLayer#ownsMbLayerId');
}
ownsMbSourceId(sourceId: string): boolean {
ownsMbSourceId(mbSourceId: string): boolean {
throw new Error('Should implement AbstractLayer#ownsMbSourceId');
}

View file

@ -74,8 +74,8 @@ export class TileLayer extends AbstractLayer {
return;
}
const sourceId = this.getId();
mbMap.addSource(sourceId, {
const mbSourceId = this._getMbSourceId();
mbMap.addSource(mbSourceId, {
type: 'raster',
tiles: [tmsSourceData.url],
tileSize: 256,
@ -85,7 +85,7 @@ export class TileLayer extends AbstractLayer {
mbMap.addLayer({
id: mbLayerId,
type: 'raster',
source: sourceId,
source: mbSourceId,
minzoom: this._descriptor.minZoom,
maxzoom: this._descriptor.maxZoom,
});

View file

@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`icon should use vector icon 1`] = `
<div
data-euiicon-type="vector"
size="m"
/>
`;

View file

@ -0,0 +1,163 @@
/*
* 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 { MockSyncContext } from '../__tests__/mock_sync_context';
import sinon from 'sinon';
jest.mock('../../../kibana_services', () => {
return {
getUiSettings() {
return {
get() {
return false;
},
};
},
};
});
import { shallow } from 'enzyme';
import { Feature } from 'geojson';
import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source';
import {
DataRequestDescriptor,
TiledSingleLayerVectorSourceDescriptor,
VectorLayerDescriptor,
} from '../../../../common/descriptor_types';
import { SOURCE_TYPES } from '../../../../common/constants';
import { TiledVectorLayer } from './tiled_vector_layer';
const defaultConfig = {
urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
layerName: 'foobar',
minSourceZoom: 4,
maxSourceZoom: 14,
};
function createLayer(
layerOptions: Partial<VectorLayerDescriptor> = {},
sourceOptions: Partial<TiledSingleLayerVectorSourceDescriptor> = {}
): TiledVectorLayer {
const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = {
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
...defaultConfig,
fields: [],
tooltipProperties: [],
...sourceOptions,
};
const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor);
const defaultLayerOptions = {
...layerOptions,
sourceDescriptor,
};
const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions);
return new TiledVectorLayer({ layerDescriptor, source: mvtSource });
}
describe('visiblity', () => {
it('should get minzoom from source', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
expect(layer.getMinZoom()).toEqual(4);
});
it('should get maxzoom from default', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
expect(layer.getMaxZoom()).toEqual(24);
});
it('should get maxzoom from layer options', async () => {
const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {});
expect(layer.getMaxZoom()).toEqual(10);
});
});
describe('icon', () => {
it('should use vector icon', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
const iconAndTooltipContent = layer.getCustomIconAndTooltipContent();
const component = shallow(iconAndTooltipContent.icon);
expect(component).toMatchSnapshot();
});
});
describe('getFeatureById', () => {
it('should return null feature', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
const feature = layer.getFeatureById('foobar') as Feature;
expect(feature).toEqual(null);
});
});
describe('syncData', () => {
it('Should sync with source-params', async () => {
const layer: TiledVectorLayer = createLayer({}, {});
const syncContext = new MockSyncContext({ dataFilters: {} });
await layer.syncData(syncContext);
// @ts-expect-error
sinon.assert.calledOnce(syncContext.startLoading);
// @ts-expect-error
sinon.assert.calledOnce(syncContext.stopLoading);
// @ts-expect-error
const call = syncContext.stopLoading.getCall(0);
expect(call.args[2]).toEqual(defaultConfig);
});
it('Should not resync when no changes to source params', async () => {
const layer1: TiledVectorLayer = createLayer({}, {});
const syncContext1 = new MockSyncContext({ dataFilters: {} });
await layer1.syncData(syncContext1);
const dataRequestDescriptor: DataRequestDescriptor = {
data: { ...defaultConfig },
dataId: 'source',
};
const layer2: TiledVectorLayer = createLayer(
{
__dataRequests: [dataRequestDescriptor],
},
{}
);
const syncContext2 = new MockSyncContext({ dataFilters: {} });
await layer2.syncData(syncContext2);
// @ts-expect-error
sinon.assert.notCalled(syncContext2.startLoading);
// @ts-expect-error
sinon.assert.notCalled(syncContext2.stopLoading);
});
it('Should resync when changes to source params', async () => {
const layer1: TiledVectorLayer = createLayer({}, {});
const syncContext1 = new MockSyncContext({ dataFilters: {} });
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);
// @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' });
});
});

View file

@ -6,31 +6,30 @@
import React from 'react';
import { EuiIcon } from '@elastic/eui';
import { Feature } from 'geojson';
import { VectorStyle } from '../../styles/vector/vector_style';
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants';
import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer';
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { ITiledSingleLayerVectorSource } from '../../sources/vector_source';
import { DataRequestContext } from '../../../actions';
import { ISource } from '../../sources/source';
import {
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor';
import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types';
export class TiledVectorLayer extends VectorLayer {
static type = LAYER_TYPE.TILED_VECTOR;
static createDescriptor(
descriptor: Partial<VectorLayerDescriptor>,
mapColors: string[]
mapColors?: string[]
): VectorLayerDescriptor {
const layerDescriptor = super.createDescriptor(descriptor, mapColors);
layerDescriptor.type = TiledVectorLayer.type;
if (!layerDescriptor.style) {
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors);
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []);
layerDescriptor.style = VectorStyle.createDescriptor(styleProperties);
}
@ -64,13 +63,16 @@ export class TiledVectorLayer extends VectorLayer {
);
const prevDataRequest = this.getSourceDataRequest();
const canSkip = await canSkipSourceUpdate({
source: this._source as ISource,
prevDataRequest,
nextMeta: searchFilters,
});
if (canSkip) {
return null;
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 (canSkipBecauseNoChanges) {
return null;
}
}
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
@ -89,37 +91,41 @@ export class TiledVectorLayer extends VectorLayer {
}
_syncSourceBindingWithMb(mbMap: unknown) {
// @ts-ignore
const mbSource = mbMap.getSource(this.getId());
if (!mbSource) {
const sourceDataRequest = this.getSourceDataRequest();
if (!sourceDataRequest) {
// this is possible if the layer was invisible at startup.
// the actions will not perform any data=syncing as an optimization when a layer is invisible
// when turning the layer back into visible, it's possible the url has not been resovled yet.
return;
}
const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
if (!sourceMeta) {
return;
}
const sourceId = this.getId();
// @ts-ignore
mbMap.addSource(sourceId, {
type: 'vector',
tiles: [sourceMeta.urlTemplate],
minzoom: sourceMeta.minSourceZoom,
maxzoom: sourceMeta.maxSourceZoom,
});
// @ts-expect-error
const mbSource = mbMap.getSource(this._getMbSourceId());
if (mbSource) {
return;
}
const sourceDataRequest = this.getSourceDataRequest();
if (!sourceDataRequest) {
// this is possible if the layer was invisible at startup.
// the actions will not perform any data=syncing as an optimization when a layer is invisible
// when turning the layer back into visible, it's possible the url has not been resovled yet.
return;
}
const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
if (!sourceMeta) {
return;
}
const mbSourceId = this._getMbSourceId();
// @ts-expect-error
mbMap.addSource(mbSourceId, {
type: 'vector',
tiles: [sourceMeta.urlTemplate],
minzoom: sourceMeta.minSourceZoom,
maxzoom: sourceMeta.maxSourceZoom,
});
}
ownsMbSourceId(mbSourceId: string): boolean {
return this._getMbSourceId() === mbSourceId;
}
_syncStylePropertiesWithMb(mbMap: unknown) {
// @ts-ignore
const mbSource = mbMap.getSource(this.getId());
const mbSource = mbMap.getSource(this._getMbSourceId());
if (!mbSource) {
return;
}
@ -129,32 +135,52 @@ export class TiledVectorLayer extends VectorLayer {
return;
}
const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
if (sourceMeta.layerName === '') {
return;
}
this._setMbPointsProperties(mbMap, sourceMeta.layerName);
this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName);
}
_requiresPrevSourceCleanup(mbMap: unknown): boolean {
// @ts-ignore
const mbTileSource = mbMap.getSource(this.getId());
// @ts-expect-error
const mbTileSource = mbMap.getSource(this._getMbSourceId());
if (!mbTileSource) {
return false;
}
const dataRequest = this.getSourceDataRequest();
if (!dataRequest) {
return false;
}
const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig;
if (
mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate &&
mbTileSource.minzoom === tiledSourceMeta.minSourceZoom &&
mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom
) {
// TileURL and zoom-range captures all the state. If this does not change, no updates are required.
if (!tiledSourceMeta) {
return false;
}
return true;
const isSourceDifferent =
mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate ||
mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom ||
mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom;
if (isSourceDifferent) {
return true;
}
const layerIds = this.getMbLayerIds();
for (let i = 0; i < layerIds.length; i++) {
// @ts-expect-error
const mbLayer = mbMap.getLayer(layerIds[i]);
if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) {
// If the source-pointer of one of the layers is stale, they will all be stale.
// In this case, all the mb-layers need to be removed and re-added.
return true;
}
}
return false;
}
syncLayerWithMB(mbMap: unknown) {
@ -171,4 +197,8 @@ export class TiledVectorLayer extends VectorLayer {
// higher resolution vector tiles cannot be displayed at lower-res
return Math.max(this._source.getMinZoom(), super.getMinZoom());
}
getFeatureById(id: string | number): Feature | null {
return null;
}
}

View file

@ -5,6 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { Feature, GeoJsonProperties } from 'geojson';
import { AbstractLayer } from '../layer';
import { IVectorSource } from '../../sources/vector_source';
import {
@ -17,6 +18,7 @@ import { IJoin } from '../../joins/join';
import { IVectorStyle } from '../../styles/vector/vector_style';
import { IField } from '../../fields/field';
import { DataRequestContext } from '../../../actions';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
export type VectorLayerArguments = {
source: IVectorSource;
@ -31,6 +33,8 @@ export interface IVectorLayer extends ILayer {
getValidJoins(): IJoin[];
getSource(): IVectorSource;
getStyle(): IVectorStyle;
getFeatureById(id: string | number): Feature | null;
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
}
export class VectorLayer extends AbstractLayer implements IVectorLayer {
@ -75,4 +79,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
getSource(): IVectorSource;
getStyle(): IVectorStyle;
getFeatureById(id: string | number): Feature | null;
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
}

View file

@ -672,10 +672,10 @@ export class VectorLayer extends AbstractLayer {
}
this.syncVisibilityWithMb(mbMap, markerLayerId);
mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setLayerZoomRange(markerLayerId, this.getMinZoom(), this.getMaxZoom());
if (markerLayerId !== textLayerId) {
this.syncVisibilityWithMb(mbMap, textLayerId);
mbMap.setLayerZoomRange(textLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setLayerZoomRange(textLayerId, this.getMinZoom(), this.getMaxZoom());
}
}
@ -802,14 +802,14 @@ export class VectorLayer extends AbstractLayer {
});
this.syncVisibilityWithMb(mbMap, fillLayerId);
mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
const fillFilterExpr = getFillFilterExpression(hasJoins);
if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
mbMap.setFilter(fillLayerId, fillFilterExpr);
}
this.syncVisibilityWithMb(mbMap, lineLayerId);
mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
const lineFilterExpr = getLineFilterExpression(hasJoins);
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
@ -822,9 +822,9 @@ export class VectorLayer extends AbstractLayer {
}
_syncSourceBindingWithMb(mbMap) {
const mbSource = mbMap.getSource(this.getId());
const mbSource = mbMap.getSource(this._getMbSourceId());
if (!mbSource) {
mbMap.addSource(this.getId(), {
mbMap.addSource(this._getMbSourceId(), {
type: 'geojson',
data: EMPTY_FEATURE_COLLECTION,
});
@ -891,16 +891,17 @@ export class VectorLayer extends AbstractLayer {
}
async getPropertiesForTooltip(properties) {
let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties);
this._addJoinsToSourceTooltips(allTooltips);
const vectorSource = this.getSource();
let allProperties = await vectorSource.filterAndFormatPropertiesToHtml(properties);
this._addJoinsToSourceTooltips(allProperties);
for (let i = 0; i < this.getJoins().length; i++) {
const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip(
properties
);
allTooltips = [...allTooltips, ...propsFromJoin];
allProperties = [...allProperties, ...propsFromJoin];
}
return allTooltips;
return allProperties;
}
canShowTooltip() {
@ -912,7 +913,7 @@ export class VectorLayer extends AbstractLayer {
getFeatureById(id) {
const featureCollection = this._getSourceFeatureCollection();
if (!featureCollection) {
return;
return null;
}
return featureCollection.features.find((feature) => {

View file

@ -15,7 +15,7 @@ import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/vi
interface Props {
layerId: string;
onChange: (args: OnSourceChangeArgs) => void;
onChange: (...args: OnSourceChangeArgs[]) => void;
source: IEmsFileSource;
tooltipFields: IField[];
}

View file

@ -0,0 +1,491 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render error for dupes 1`] = `
<Fragment>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
aria-label="Fieldname"
compressed={true}
isInvalid={true}
onChange={[Function]}
placeholder="Field name"
value="foo"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperSelect
compressed={true}
fullWidth={false}
hasDividers={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="string"
/>
</EuiFlexItem>
<EuiFlexItem>
string
</EuiFlexItem>
</EuiFlexGroup>,
"value": "String",
},
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="number"
/>
</EuiFlexItem>
<EuiFlexItem>
number
</EuiFlexItem>
</EuiFlexGroup>,
"value": "Number",
},
]
}
valueOfSelected="String"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove field"
color="danger"
iconType="trash"
onClick={[Function]}
title="Remove field"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="1"
>
<EuiFlexItem>
<EuiFieldText
aria-label="Fieldname"
compressed={true}
isInvalid={true}
onChange={[Function]}
placeholder="Field name"
value="foo"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperSelect
compressed={true}
fullWidth={false}
hasDividers={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="string"
/>
</EuiFlexItem>
<EuiFlexItem>
string
</EuiFlexItem>
</EuiFlexGroup>,
"value": "String",
},
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="number"
/>
</EuiFlexItem>
<EuiFlexItem>
number
</EuiFlexItem>
</EuiFlexGroup>,
"value": "Number",
},
]
}
valueOfSelected="Number"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove field"
color="danger"
iconType="trash"
onClick={[Function]}
title="Remove field"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceAround"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
iconType="plusInCircleFilled"
onClick={[Function]}
size="xs"
>
Add
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`should render error for empty name 1`] = `
<Fragment>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
aria-label="Fieldname"
compressed={true}
isInvalid={true}
onChange={[Function]}
placeholder="Field name"
value=""
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperSelect
compressed={true}
fullWidth={false}
hasDividers={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="string"
/>
</EuiFlexItem>
<EuiFlexItem>
string
</EuiFlexItem>
</EuiFlexGroup>,
"value": "String",
},
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="number"
/>
</EuiFlexItem>
<EuiFlexItem>
number
</EuiFlexItem>
</EuiFlexGroup>,
"value": "Number",
},
]
}
valueOfSelected="String"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove field"
color="danger"
iconType="trash"
onClick={[Function]}
title="Remove field"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceAround"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
iconType="plusInCircleFilled"
onClick={[Function]}
size="xs"
>
Add
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`should render field editor 1`] = `
<Fragment>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="0"
>
<EuiFlexItem>
<EuiFieldText
aria-label="Fieldname"
compressed={true}
isInvalid={false}
onChange={[Function]}
placeholder="Field name"
value="foo"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperSelect
compressed={true}
fullWidth={false}
hasDividers={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="string"
/>
</EuiFlexItem>
<EuiFlexItem>
string
</EuiFlexItem>
</EuiFlexGroup>,
"value": "String",
},
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="number"
/>
</EuiFlexItem>
<EuiFlexItem>
number
</EuiFlexItem>
</EuiFlexGroup>,
"value": "Number",
},
]
}
valueOfSelected="String"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove field"
color="danger"
iconType="trash"
onClick={[Function]}
title="Remove field"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
key="1"
>
<EuiFlexItem>
<EuiFieldText
aria-label="Fieldname"
compressed={true}
isInvalid={false}
onChange={[Function]}
placeholder="Field name"
value="bar"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSuperSelect
compressed={true}
fullWidth={false}
hasDividers={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="string"
/>
</EuiFlexItem>
<EuiFlexItem>
string
</EuiFlexItem>
</EuiFlexGroup>,
"value": "String",
},
Object {
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="s"
>
<EuiFlexItem
grow={null}
>
<FieldIcon
fill="none"
type="number"
/>
</EuiFlexItem>
<EuiFlexItem>
number
</EuiFlexItem>
</EuiFlexGroup>,
"value": "Number",
},
]
}
valueOfSelected="Number"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove field"
color="danger"
iconType="trash"
onClick={[Function]}
title="Remove field"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
<EuiSpacer
size="s"
/>
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceAround"
>
<EuiFlexItem
grow={false}
>
<EuiButtonEmpty
iconType="plusInCircleFilled"
onClick={[Function]}
size="xs"
>
Add
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;

View file

@ -0,0 +1,211 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should not render fields-editor when there is no layername 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Tile layer"
labelType="label"
>
<EuiFieldText
compressed={true}
isInvalid={true}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content="Zoom levels where the layer is present in the tiles. This does not correspond directly to visibility. Layer data from lower levels can always be displayed at higher zoom levels (but not vice versa)."
delay="regular"
position="top"
>
<span>
Available levels
<EuiIcon
color="subdued"
type="questionInCircle"
/>
</span>
</EuiToolTip>
}
max={24}
min={0}
onChange={[Function]}
prepend="Zoom"
showInput="inputWithPopover"
showLabels={true}
value={
Array [
4,
14,
]
}
/>
</Fragment>
`;
exports[`should render with fields 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Tile layer"
labelType="label"
>
<EuiFieldText
compressed={true}
isInvalid={false}
onChange={[Function]}
value="foobar"
/>
</EuiFormRow>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content="Zoom levels where the layer is present in the tiles. This does not correspond directly to visibility. Layer data from lower levels can always be displayed at higher zoom levels (but not vice versa)."
delay="regular"
position="top"
>
<span>
Available levels
<EuiIcon
color="subdued"
type="questionInCircle"
/>
</span>
</EuiToolTip>
}
max={24}
min={0}
onChange={[Function]}
prepend="Zoom"
showInput="inputWithPopover"
showLabels={true}
value={
Array [
4,
14,
]
}
/>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content={
<React.Fragment>
Fields which are available in
<React.Fragment>
<b>
foobar
</b>
.
</React.Fragment>
These can be used for tooltips and dynamic styling.
</React.Fragment>
}
delay="regular"
position="top"
>
<span>
Fields
<EuiIcon
color="subdued"
type="questionInCircle"
/>
</span>
</EuiToolTip>
}
labelType="label"
>
<MVTFieldConfigEditor
fields={Array []}
onChange={[Function]}
/>
</EuiFormRow>
</Fragment>
`;
exports[`should render without fields 1`] = `
<Fragment>
<EuiFormRow
describedByIds={Array []}
display="columnCompressed"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label="Tile layer"
labelType="label"
>
<EuiFieldText
compressed={true}
isInvalid={false}
onChange={[Function]}
value="foobar"
/>
</EuiFormRow>
<ValidatedDualRange
allowEmptyRange={false}
compressed={true}
formRowDisplay="columnCompressed"
fullWidth={false}
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content="Zoom levels where the layer is present in the tiles. This does not correspond directly to visibility. Layer data from lower levels can always be displayed at higher zoom levels (but not vice versa)."
delay="regular"
position="top"
>
<span>
Available levels
<EuiIcon
color="subdued"
type="questionInCircle"
/>
</span>
</EuiToolTip>
}
max={24}
min={0}
onChange={[Function]}
prepend="Zoom"
showInput="inputWithPopover"
showLabels={true}
value={
Array [
4,
14,
]
}
/>
</Fragment>
`;

View file

@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render source creation editor (fields should _not_ be included) 1`] = `
<EuiPanel>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText="URL of the .mvt vector tile service. e.g. http://company.com/{z}/{x}/{y}.pbf"
label="Url"
labelType="label"
>
<EuiFieldText
compressed={true}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<MVTSingleLayerSourceSettings
fields={Array []}
handleChange={[Function]}
layerName=""
maxSourceZoom={24}
minSourceZoom={0}
showFields={false}
/>
</EuiPanel>
`;

View file

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render update source editor (fields _should_ be included) 1`] = `
<Fragment>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Source settings"
id="xpack.maps.mvtSource.sourceSettings"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<MVTSingleLayerSourceSettings
fields={Array []}
handleChange={[Function]}
layerName="foobar"
maxSourceZoom={14}
minSourceZoom={4}
showFields={true}
/>
</EuiPanel>
<EuiSpacer
size="s"
/>
<EuiPanel>
<EuiTitle
size="xs"
>
<h5>
<FormattedMessage
defaultMessage="Tooltip fields"
id="xpack.maps.mvtSource.tooltipsTitle"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSpacer
size="m"
/>
<TooltipSelector
fields={Array []}
onChange={[Function]}
tooltipFields={Array []}
/>
</EuiPanel>
<EuiSpacer
size="s"
/>
</Fragment>
`;

View file

@ -6,23 +6,21 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
MVTSingleLayerVectorSourceEditor,
MVTSingleLayerVectorSourceConfig,
} from './mvt_single_layer_vector_source_editor';
import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor';
import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types';
export const mvtVectorSourceWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', {
defaultMessage: 'Vector source wizard',
defaultMessage: 'Data service implementing the Mapbox vector tile specification',
}),
icon: 'grid',
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => {
const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => {
const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig);
const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
previewLayers([layerDescriptor]);

View file

@ -0,0 +1,57 @@
/*
* 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.
*/
jest.mock('../../../kibana_services', () => ({}));
import React from 'react';
import { shallow } from 'enzyme';
import { MVTFieldConfigEditor } from './mvt_field_config_editor';
import { MVT_FIELD_TYPE } from '../../../../common/constants';
test('should render field editor', async () => {
const fields = [
{
name: 'foo',
type: MVT_FIELD_TYPE.STRING,
},
{
name: 'bar',
type: MVT_FIELD_TYPE.NUMBER,
},
];
const component = shallow(<MVTFieldConfigEditor fields={fields} onChange={() => {}} />);
expect(component).toMatchSnapshot();
});
test('should render error for empty name', async () => {
const fields = [
{
name: '',
type: MVT_FIELD_TYPE.STRING,
},
];
const component = shallow(<MVTFieldConfigEditor fields={fields} onChange={() => {}} />);
expect(component).toMatchSnapshot();
});
test('should render error for dupes', async () => {
const fields = [
{
name: 'foo',
type: MVT_FIELD_TYPE.STRING,
},
{
name: 'foo',
type: MVT_FIELD_TYPE.NUMBER,
},
];
const component = shallow(<MVTFieldConfigEditor fields={fields} onChange={() => {}} />);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,210 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import React, { ChangeEvent, Component, Fragment } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiSuperSelect,
EuiFieldText,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
import { FieldIcon } from '../../../../../../../src/plugins/kibana_react/public';
import { MVT_FIELD_TYPE } from '../../../../common/constants';
function makeOption({
value,
icon,
message,
}: {
value: MVT_FIELD_TYPE;
icon: string;
message: string;
}) {
return {
value,
inputDisplay: (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={null}>
<FieldIcon type={icon} fill="none" />
</EuiFlexItem>
<EuiFlexItem>{message}</EuiFlexItem>
</EuiFlexGroup>
),
};
}
const FIELD_TYPE_OPTIONS = [
{
value: MVT_FIELD_TYPE.STRING,
icon: 'string',
message: i18n.translate('xpack.maps.mvtSource.stringFieldLabel', {
defaultMessage: 'string',
}),
},
{
value: MVT_FIELD_TYPE.NUMBER,
icon: 'number',
message: i18n.translate('xpack.maps.mvtSource.numberFieldLabel', {
defaultMessage: 'number',
}),
},
].map(makeOption);
interface Props {
fields: MVTFieldDescriptor[];
onChange: (fields: MVTFieldDescriptor[]) => void;
}
interface State {
currentFields: MVTFieldDescriptor[];
}
export class MVTFieldConfigEditor extends Component<Props, State> {
state: State = {
currentFields: _.cloneDeep(this.props.fields),
};
_notifyChange = _.debounce(() => {
const invalid = this.state.currentFields.some((field: MVTFieldDescriptor) => {
return field.name === '';
});
if (!invalid) {
this.props.onChange(this.state.currentFields);
}
});
_fieldChange(newFields: MVTFieldDescriptor[]) {
this.setState(
{
currentFields: newFields,
},
this._notifyChange
);
}
_removeField(index: number) {
const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice();
newFields.splice(index, 1);
this._fieldChange(newFields);
}
_addField = () => {
const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice();
newFields.push({
type: MVT_FIELD_TYPE.STRING,
name: '',
});
this._fieldChange(newFields);
};
_renderFieldTypeDropDown(mvtFieldConfig: MVTFieldDescriptor, index: number) {
const onChange = (type: MVT_FIELD_TYPE) => {
const newFields = this.state.currentFields.slice();
newFields[index] = {
type,
name: newFields[index].name,
};
this._fieldChange(newFields);
};
return (
<EuiSuperSelect
options={FIELD_TYPE_OPTIONS}
valueOfSelected={mvtFieldConfig.type}
onChange={(value) => onChange(value)}
compressed
/>
);
}
_renderFieldButtonDelete(index: number) {
return (
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={() => {
this._removeField(index);
}}
title={i18n.translate('xpack.maps.mvtSource.trashButtonTitle', {
defaultMessage: 'Remove field',
})}
aria-label={i18n.translate('xpack.maps.mvtSource.trashButtonAriaLabel', {
defaultMessage: 'Remove field',
})}
/>
);
}
_renderFieldNameInput(mvtFieldConfig: MVTFieldDescriptor, index: number) {
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const name = e.target.value;
const newFields = this.state.currentFields.slice();
newFields[index] = {
name,
type: newFields[index].type,
};
this._fieldChange(newFields);
};
const emptyName = mvtFieldConfig.name === '';
const hasDupes =
this.state.currentFields.filter((field) => field.name === mvtFieldConfig.name).length > 1;
return (
<EuiFieldText
value={mvtFieldConfig.name}
onChange={onChange}
aria-label={'Fieldname'}
placeholder={i18n.translate('xpack.maps.mvtSource.fieldPlaceholderText', {
defaultMessage: 'Field name',
})}
isInvalid={emptyName || hasDupes}
compressed
/>
);
}
_renderFieldConfig() {
return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => {
return (
<>
<EuiFlexGroup key={index} gutterSize="xs" alignItems="center">
<EuiFlexItem>{this._renderFieldNameInput(mvtFieldConfig, index)}</EuiFlexItem>
<EuiFlexItem>{this._renderFieldTypeDropDown(mvtFieldConfig, index)}</EuiFlexItem>
<EuiFlexItem grow={false}>{this._renderFieldButtonDelete(index)}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={'xs'} />
</>
);
});
}
render() {
return (
<Fragment>
{this._renderFieldConfig()}
<EuiSpacer size={'s'} />
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this._addField} size="xs" iconType="plusInCircleFilled">
{i18n.translate('xpack.maps.mvtSource.addFieldLabel', {
defaultMessage: 'Add',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.
*/
jest.mock('../../../kibana_services', () => ({}));
import React from 'react';
import { shallow } from 'enzyme';
import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
const defaultSettings = {
handleChange: () => {},
layerName: 'foobar',
fields: [],
minSourceZoom: 4,
maxSourceZoom: 14,
showFields: true,
};
test('should render with fields', async () => {
const component = shallow(<MVTSingleLayerSourceSettings {...defaultSettings} />);
expect(component).toMatchSnapshot();
});
test('should render without fields', async () => {
const settings = { ...defaultSettings, showFields: false };
const component = shallow(<MVTSingleLayerSourceSettings {...settings} />);
expect(component).toMatchSnapshot();
});
test('should not render fields-editor when there is no layername', async () => {
const settings = { ...defaultSettings, layerName: '' };
const component = shallow(<MVTSingleLayerSourceSettings {...settings} />);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,191 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import React, { Fragment, Component, ChangeEvent } from 'react';
import { EuiFieldText, EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public';
import { MVTFieldConfigEditor } from './mvt_field_config_editor';
import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
export type MVTSettings = {
layerName: string;
fields: MVTFieldDescriptor[];
minSourceZoom: number;
maxSourceZoom: number;
};
interface State {
currentLayerName: string;
currentMinSourceZoom: number;
currentMaxSourceZoom: number;
currentFields: MVTFieldDescriptor[];
}
interface Props {
handleChange: (args: MVTSettings) => void;
layerName: string;
fields: MVTFieldDescriptor[];
minSourceZoom: number;
maxSourceZoom: number;
showFields: boolean;
}
export class MVTSingleLayerSourceSettings extends Component<Props, State> {
// Tracking in state to allow for debounce.
// Changes to layer-name and/or min/max zoom require heavy operation at map-level (removing and re-adding all sources/layers)
// To preserve snappyness of typing, debounce the dispatches.
state = {
currentLayerName: this.props.layerName,
currentMinSourceZoom: this.props.minSourceZoom,
currentMaxSourceZoom: this.props.maxSourceZoom,
currentFields: _.cloneDeep(this.props.fields),
};
_handleChange = _.debounce(() => {
this.props.handleChange({
layerName: this.state.currentLayerName,
minSourceZoom: this.state.currentMinSourceZoom,
maxSourceZoom: this.state.currentMaxSourceZoom,
fields: this.state.currentFields,
});
}, 200);
_handleLayerNameInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ currentLayerName: e.target.value }, this._handleChange);
};
_handleFieldChange = (fields: MVTFieldDescriptor[]) => {
this.setState({ currentFields: fields }, this._handleChange);
};
_handleZoomRangeChange = (e: Value) => {
this.setState(
{
currentMinSourceZoom: parseInt(e[0] as string, 10),
currentMaxSourceZoom: parseInt(e[1] as string, 10),
},
this._handleChange
);
};
render() {
const preMessage = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage',
{
defaultMessage: 'Fields which are available in ',
}
);
const message = (
<>
<b>{this.state.currentLayerName}</b>.{' '}
</>
);
const postMessage = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage',
{
defaultMessage: 'These can be used for tooltips and dynamic styling.',
}
);
const fieldEditor =
this.props.showFields && this.state.currentLayerName !== '' ? (
<EuiFormRow
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content={
<>
{preMessage}
{message}
{postMessage}
</>
}
>
<span>
{i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage',
{
defaultMessage: 'Fields',
}
)}{' '}
<EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
}
>
<MVTFieldConfigEditor
fields={this.state.currentFields.slice()}
onChange={this._handleFieldChange}
/>
</EuiFormRow>
) : null;
return (
<Fragment>
<EuiFormRow
display="columnCompressed"
label={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage',
{
defaultMessage: 'Tile layer',
}
)}
>
<EuiFieldText
value={this.state.currentLayerName}
onChange={this._handleLayerNameInputChange}
isInvalid={this.state.currentLayerName === ''}
compressed
/>
</EuiFormRow>
<ValidatedDualRange
label={
<EuiToolTip
anchorClassName="eui-alignMiddle"
content={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeHelpMessage',
{
defaultMessage:
'Zoom levels where the layer is present in the tiles. This does not correspond directly to visibility. Layer data from lower levels can always be displayed at higher zoom levels (but not vice versa).',
}
)}
>
<span>
{i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeTopMessage',
{
defaultMessage: 'Available levels',
}
)}{' '}
<EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
}
formRowDisplay="columnCompressed"
value={[this.state.currentMinSourceZoom, this.state.currentMaxSourceZoom]}
min={MIN_ZOOM}
max={MAX_ZOOM}
onChange={this._handleZoomRangeChange}
allowEmptyRange={false}
showInput="inputWithPopover"
compressed
showLabels
prepend={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage',
{
defaultMessage: 'Zoom',
}
)}
/>
{fieldEditor}
</Fragment>
);
}
}

View file

@ -0,0 +1,91 @@
/*
* 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 { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
import { MVT_FIELD_TYPE, SOURCE_TYPES } from '../../../../common/constants';
import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types';
const descriptor: TiledSingleLayerVectorSourceDescriptor = {
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
layerName: 'foobar',
minSourceZoom: 4,
maxSourceZoom: 14,
fields: [],
tooltipProperties: [],
};
describe('getUrlTemplateWithMeta', () => {
it('should echo configuration', async () => {
const source = new MVTSingleLayerVectorSource(descriptor);
const config = await source.getUrlTemplateWithMeta();
expect(config.urlTemplate).toEqual(descriptor.urlTemplate);
expect(config.layerName).toEqual(descriptor.layerName);
expect(config.minSourceZoom).toEqual(descriptor.minSourceZoom);
expect(config.maxSourceZoom).toEqual(descriptor.maxSourceZoom);
});
});
describe('canFormatFeatureProperties', () => {
it('false if no tooltips', async () => {
const source = new MVTSingleLayerVectorSource(descriptor);
expect(source.canFormatFeatureProperties()).toEqual(false);
});
it('true if tooltip', async () => {
const descriptorWithTooltips = {
...descriptor,
fields: [{ name: 'foobar', type: MVT_FIELD_TYPE.STRING }],
tooltipProperties: ['foobar'],
};
const source = new MVTSingleLayerVectorSource(descriptorWithTooltips);
expect(source.canFormatFeatureProperties()).toEqual(true);
});
});
describe('filterAndFormatPropertiesToHtml', () => {
const descriptorWithFields = {
...descriptor,
fields: [
{
name: 'foo',
type: MVT_FIELD_TYPE.STRING,
},
{
name: 'food',
type: MVT_FIELD_TYPE.STRING,
},
{
name: 'fooz',
type: MVT_FIELD_TYPE.NUMBER,
},
],
tooltipProperties: ['foo', 'fooz'],
};
it('should get tooltipproperties', async () => {
const source = new MVTSingleLayerVectorSource(descriptorWithFields);
const tooltipProperties = await source.filterAndFormatPropertiesToHtml({
foo: 'bar',
fooz: 123,
});
expect(tooltipProperties.length).toEqual(2);
expect(tooltipProperties[0].getPropertyName()).toEqual('foo');
expect(tooltipProperties[0].getHtmlDisplayValue()).toEqual('bar');
expect(tooltipProperties[1].getPropertyName()).toEqual('fooz');
expect(tooltipProperties[1].getHtmlDisplayValue()).toEqual('123');
});
});
describe('getImmutableSourceProperties', () => {
it('should only show immutable props', async () => {
const source = new MVTSingleLayerVectorSource(descriptor);
const properties = await source.getImmutableProperties();
expect(properties).toEqual([
{ label: 'Data source', value: '.pbf vector tiles' },
{ label: 'Url', value: 'https://example.com/{x}/{y}/{z}.pbf' },
]);
});
});

View file

@ -1,161 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import { AbstractSource, ImmutableSourceProperty } from '../source';
import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source';
import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { IField } from '../../fields/field';
import { registerSource } from '../source_registry';
import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
import {
MapExtent,
TiledSingleLayerVectorSourceDescriptor,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor';
import { ITooltipProperty } from '../../tooltips/tooltip_property';
export const sourceTitle = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
{
defaultMessage: 'Vector Tile Layer',
}
);
export class MVTSingleLayerVectorSource extends AbstractSource
implements ITiledSingleLayerVectorSource {
static createDescriptor({
urlTemplate,
layerName,
minSourceZoom,
maxSourceZoom,
}: MVTSingleLayerVectorSourceConfig) {
return {
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
id: uuid(),
urlTemplate,
layerName,
minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom),
maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom),
};
}
readonly _descriptor: TiledSingleLayerVectorSourceDescriptor;
constructor(
sourceDescriptor: TiledSingleLayerVectorSourceDescriptor,
inspectorAdapters?: object
) {
super(sourceDescriptor, inspectorAdapters);
this._descriptor = sourceDescriptor;
}
renderSourceSettingsEditor() {
return null;
}
getFieldNames(): string[] {
return [];
}
getGeoJsonWithMeta(
layerName: 'string',
searchFilters: unknown[],
registerCancelCallback: (callback: () => void) => void
): Promise<GeoJsonWithMeta> {
// todo: remove this method
// This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource.
throw new Error('Does not implement getGeoJsonWithMeta');
}
async getFields(): Promise<IField[]> {
return [];
}
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
return [
{ label: getDataSourceLabel(), value: sourceTitle },
{ label: getUrlLabel(), value: this._descriptor.urlTemplate },
{
label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', {
defaultMessage: 'Layer name',
}),
value: this._descriptor.layerName,
},
{
label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', {
defaultMessage: 'Min zoom',
}),
value: this._descriptor.minSourceZoom.toString(),
},
{
label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', {
defaultMessage: 'Max zoom',
}),
value: this._descriptor.maxSourceZoom.toString(),
},
];
}
async getDisplayName(): Promise<string> {
return this._descriptor.layerName;
}
async getUrlTemplateWithMeta() {
return {
urlTemplate: this._descriptor.urlTemplate,
layerName: this._descriptor.layerName,
minSourceZoom: this._descriptor.minSourceZoom,
maxSourceZoom: this._descriptor.maxSourceZoom,
};
}
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]> {
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
canFormatFeatureProperties() {
return false;
}
getMinZoom() {
return this._descriptor.minSourceZoom;
}
getMaxZoom() {
return this._descriptor.maxSourceZoom;
}
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
): MapExtent | null {
return null;
}
getFieldByName(fieldName: string): IField | null {
return null;
}
getSyncMeta(): VectorSourceSyncMeta {
return null;
}
getApplyGlobalQuery(): boolean {
return false;
}
async filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]> {
return [];
}
}
registerSource({
ConstructorFunction: MVTSingleLayerVectorSource,
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
});

View file

@ -0,0 +1,222 @@
/*
* 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 { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
import React from 'react';
import { GeoJsonProperties } from 'geojson';
import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source';
import {
FIELD_ORIGIN,
MAX_ZOOM,
MIN_ZOOM,
SOURCE_TYPES,
VECTOR_SHAPE_TYPE,
} from '../../../../common/constants';
import { registerSource } from '../source_registry';
import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
import {
MapExtent,
MVTFieldDescriptor,
TiledSingleLayerVectorSourceDescriptor,
VectorSourceSyncMeta,
} from '../../../../common/descriptor_types';
import { MVTField } from '../../fields/mvt_field';
import { UpdateSourceEditor } from './update_source_editor';
import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property';
export const sourceTitle = i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
{
defaultMessage: '.pbf vector tiles',
}
);
export class MVTSingleLayerVectorSource extends AbstractSource
implements ITiledSingleLayerVectorSource {
static createDescriptor({
urlTemplate,
layerName,
minSourceZoom,
maxSourceZoom,
fields,
tooltipProperties,
}: Partial<TiledSingleLayerVectorSourceDescriptor>) {
return {
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
id: uuid(),
urlTemplate: urlTemplate ? urlTemplate : '',
layerName: layerName ? layerName : '',
minSourceZoom:
typeof minSourceZoom === 'number' ? Math.max(MIN_ZOOM, minSourceZoom) : MIN_ZOOM,
maxSourceZoom:
typeof maxSourceZoom === 'number' ? Math.min(MAX_ZOOM, maxSourceZoom) : MAX_ZOOM,
fields: fields ? fields : [],
tooltipProperties: tooltipProperties ? tooltipProperties : [],
};
}
readonly _descriptor: TiledSingleLayerVectorSourceDescriptor;
readonly _tooltipFields: MVTField[];
constructor(
sourceDescriptor: TiledSingleLayerVectorSourceDescriptor,
inspectorAdapters?: object
) {
super(sourceDescriptor, inspectorAdapters);
this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor);
this._tooltipFields = this._descriptor.tooltipProperties
.map((fieldName) => {
return this.getFieldByName(fieldName);
})
.filter((f) => f !== null) as MVTField[];
}
async supportsFitToBounds() {
return false;
}
renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
return (
<UpdateSourceEditor onChange={onChange} tooltipFields={this._tooltipFields} source={this} />
);
}
getFieldNames(): string[] {
return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
return field.name;
});
}
getMVTFields(): MVTField[] {
return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
return new MVTField({
fieldName: field.name,
type: field.type,
source: this,
origin: FIELD_ORIGIN.SOURCE,
});
});
}
getFieldByName(fieldName: string): MVTField | null {
try {
return this.createField({ fieldName });
} catch (e) {
return null;
}
}
createField({ fieldName }: { fieldName: string }): MVTField {
const field = this._descriptor.fields.find((f: MVTFieldDescriptor) => {
return f.name === fieldName;
});
if (!field) {
throw new Error(`Cannot create field for fieldName ${fieldName}`);
}
return new MVTField({
fieldName: field.name,
type: field.type,
source: this,
origin: FIELD_ORIGIN.SOURCE,
});
}
getGeoJsonWithMeta(
layerName: 'string',
searchFilters: unknown[],
registerCancelCallback: (callback: () => void) => void
): Promise<GeoJsonWithMeta> {
// Having this method here is a consequence of ITiledSingleLayerVectorSource extending IVectorSource.
throw new Error('Does not implement getGeoJsonWithMeta');
}
async getFields(): Promise<MVTField[]> {
return this.getMVTFields();
}
getLayerName(): string {
return this._descriptor.layerName;
}
async getImmutableProperties(): Promise<ImmutableSourceProperty[]> {
return [
{ label: getDataSourceLabel(), value: sourceTitle },
{ label: getUrlLabel(), value: this._descriptor.urlTemplate },
];
}
async getDisplayName(): Promise<string> {
return this.getLayerName();
}
async getUrlTemplateWithMeta() {
return {
urlTemplate: this._descriptor.urlTemplate,
layerName: this._descriptor.layerName,
minSourceZoom: this._descriptor.minSourceZoom,
maxSourceZoom: this._descriptor.maxSourceZoom,
};
}
async getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]> {
return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
}
canFormatFeatureProperties() {
return !!this._tooltipFields.length;
}
getMinZoom() {
return this._descriptor.minSourceZoom;
}
getMaxZoom() {
return this._descriptor.maxSourceZoom;
}
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
): MapExtent | null {
return null;
}
getSyncMeta(): VectorSourceSyncMeta {
return null;
}
getApplyGlobalQuery(): boolean {
return false;
}
async filterAndFormatPropertiesToHtml(
properties: GeoJsonProperties,
featureId?: string | number
): Promise<ITooltipProperty[]> {
const tooltips = [];
for (const key in properties) {
if (properties.hasOwnProperty(key)) {
for (let i = 0; i < this._tooltipFields.length; i++) {
const mvtField = this._tooltipFields[i];
if (mvtField.getName() === key) {
const tooltip = new TooltipProperty(key, key, properties[key]);
tooltips.push(tooltip);
break;
}
}
}
}
return tooltips;
}
}
registerSource({
ConstructorFunction: MVTSingleLayerVectorSource,
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
});

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
jest.mock('../../../kibana_services', () => ({}));
import React from 'react';
import { shallow } from 'enzyme';
import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor';
test('should render source creation editor (fields should _not_ be included)', async () => {
const component = shallow(<MVTSingleLayerVectorSourceEditor onSourceConfigChange={() => {}} />);
expect(component).toMatchSnapshot();
});

View file

@ -10,17 +10,14 @@ import _ from 'lodash';
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public';
import {
MVTFieldDescriptor,
TiledSingleLayerVectorSourceSettings,
} from '../../../../common/descriptor_types';
import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
export type MVTSingleLayerVectorSourceConfig = {
urlTemplate: string;
layerName: string;
minSourceZoom: number;
maxSourceZoom: number;
};
export interface Props {
onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void;
interface Props {
onSourceConfigChange: (sourceConfig: TiledSingleLayerVectorSourceSettings) => void;
}
interface State {
@ -28,6 +25,7 @@ interface State {
layerName: string;
minSourceZoom: number;
maxSourceZoom: number;
fields?: MVTFieldDescriptor[];
}
export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
@ -36,6 +34,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
layerName: '',
minSourceZoom: MIN_ZOOM,
maxSourceZoom: MAX_ZOOM,
fields: [],
};
_sourceConfigChange = _.debounce(() => {
@ -50,6 +49,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
layerName: this.state.layerName,
minSourceZoom: this.state.minSourceZoom,
maxSourceZoom: this.state.maxSourceZoom,
fields: this.state.fields,
});
}
}, 200);
@ -64,23 +64,13 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
);
};
_handleLayerNameInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const layerName = e.target.value;
this.setState(
{
layerName,
},
() => this._sourceConfigChange()
);
};
_handleZoomRangeChange = (e: Value) => {
const minSourceZoom = parseInt(e[0] as string, 10);
const maxSourceZoom = parseInt(e[1] as string, 10);
if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) {
this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange());
}
_handleChange = (state: {
layerName: string;
fields: MVTFieldDescriptor[];
minSourceZoom: number;
maxSourceZoom: number;
}) => {
this.setState(state, () => this._sourceConfigChange());
};
render() {
@ -90,37 +80,30 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', {
defaultMessage: 'Url',
})}
>
<EuiFieldText value={this.state.urlTemplate} onChange={this._handleUrlTemplateChange} />
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage',
helpText={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage',
{
defaultMessage: 'Layer name',
defaultMessage: 'URL of the .mvt vector tile service. e.g. {url}',
values: {
url: 'http://company.com/{z}/{x}/{y}.pbf',
},
}
)}
>
<EuiFieldText value={this.state.layerName} onChange={this._handleLayerNameInputChange} />
<EuiFieldText
value={this.state.urlTemplate}
onChange={this._handleUrlTemplateChange}
compressed
/>
</EuiFormRow>
<ValidatedDualRange
label=""
formRowDisplay="columnCompressed"
min={MIN_ZOOM}
max={MAX_ZOOM}
value={[this.state.minSourceZoom, this.state.maxSourceZoom]}
showInput="inputWithPopover"
showRange
showLabels
onChange={this._handleZoomRangeChange}
allowEmptyRange={false}
compressed
prepend={i18n.translate(
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage',
{
defaultMessage: 'Zoom levels',
}
)}
<MVTSingleLayerSourceSettings
handleChange={this._handleChange}
layerName={this.state.layerName}
fields={this.state.fields}
minSourceZoom={this.state.minSourceZoom}
maxSourceZoom={this.state.maxSourceZoom}
showFields={false}
/>
</EuiPanel>
);

View file

@ -0,0 +1,16 @@
/*
* 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 { MVTFieldDescriptor } from '../../../../common/descriptor_types';
export interface MVTSingleLayerVectorSourceConfig {
urlTemplate: string;
layerName: string;
minSourceZoom: number;
maxSourceZoom: number;
fields?: MVTFieldDescriptor[];
tooltipProperties?: string[];
}

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
jest.mock('../../../kibana_services', () => ({}));
import React from 'react';
import { shallow } from 'enzyme';
import { UpdateSourceEditor } from './update_source_editor';
import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types';
import { SOURCE_TYPES } from '../../../../common/constants';
const descriptor: TiledSingleLayerVectorSourceDescriptor = {
type: SOURCE_TYPES.MVT_SINGLE_LAYER,
urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
layerName: 'foobar',
minSourceZoom: 4,
maxSourceZoom: 14,
fields: [],
tooltipProperties: [],
};
test('should render update source editor (fields _should_ be included)', async () => {
const source = new MVTSingleLayerVectorSource(descriptor);
const component = shallow(
<UpdateSourceEditor source={source} tooltipFields={[]} onChange={() => {}} />
);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,136 @@
/*
* 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 React, { Component, Fragment } from 'react';
import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TooltipSelector } from '../../../components/tooltip_selector';
import { MVTField } from '../../fields/mvt_field';
import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
import { MVTSettings, MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
interface Props {
tooltipFields: MVTField[];
onChange: (...args: OnSourceChangeArgs[]) => void;
source: MVTSingleLayerVectorSource;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface State {}
export class UpdateSourceEditor extends Component<Props, State> {
_onTooltipPropertiesSelect = (propertyNames: string[]) => {
this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
};
_handleChange = (settings: MVTSettings) => {
const changes: OnSourceChangeArgs[] = [];
if (settings.layerName !== this.props.source.getLayerName()) {
changes.push({ propName: 'layerName', value: settings.layerName });
}
if (settings.minSourceZoom !== this.props.source.getMinZoom()) {
changes.push({ propName: 'minSourceZoom', value: settings.minSourceZoom });
}
if (settings.maxSourceZoom !== this.props.source.getMaxZoom()) {
changes.push({ propName: 'maxSourceZoom', value: settings.maxSourceZoom });
}
if (!_.isEqual(settings.fields, this._getFieldDescriptors())) {
changes.push({ propName: 'fields', value: settings.fields });
// Remove dangling tooltips.
// This behaves similar to how stale styling properties are removed (e.g. on metric-change in agg sources)
const sanitizedTooltips = [];
for (let i = 0; i < this.props.tooltipFields.length; i++) {
const tooltipName = this.props.tooltipFields[i].getName();
for (let j = 0; j < settings.fields.length; j++) {
if (settings.fields[j].name === tooltipName) {
sanitizedTooltips.push(tooltipName);
break;
}
}
}
if (!_.isEqual(sanitizedTooltips, this.props.tooltipFields)) {
changes.push({ propName: 'tooltipProperties', value: sanitizedTooltips });
}
}
this.props.onChange(...changes);
};
_getFieldDescriptors(): MVTFieldDescriptor[] {
return this.props.source.getMVTFields().map((field: MVTField) => {
return field.getMVTFieldDescriptor();
});
}
_renderSourceSettingsCard() {
const fieldDescriptors: MVTFieldDescriptor[] = this._getFieldDescriptors();
return (
<Fragment>
<EuiPanel>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.maps.mvtSource.sourceSettings"
defaultMessage="Source settings"
/>
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<MVTSingleLayerSourceSettings
handleChange={this._handleChange}
layerName={this.props.source.getLayerName() || ''}
fields={fieldDescriptors}
minSourceZoom={this.props.source.getMinZoom()}
maxSourceZoom={this.props.source.getMaxZoom()}
showFields={true}
/>
</EuiPanel>
<EuiSpacer size="s" />
</Fragment>
);
}
_renderTooltipSelectionCard() {
return (
<Fragment>
<EuiPanel>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.maps.mvtSource.tooltipsTitle"
defaultMessage="Tooltip fields"
/>
</h5>
</EuiTitle>
<EuiSpacer size="m" />
<TooltipSelector
tooltipFields={this.props.tooltipFields} // selected fields in the tooltip
onChange={this._onTooltipPropertiesSelect}
fields={this.props.source.getMVTFields()} // all the fields in the source
/>
</EuiPanel>
<EuiSpacer size="s" />
</Fragment>
);
}
render() {
return (
<Fragment>
{this._renderSourceSettingsCard()}
{this._renderTooltipSelectionCard()}
</Fragment>
);
}
}

View file

@ -17,7 +17,7 @@ import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
export type SourceEditorArgs = {
onChange: (args: OnSourceChangeArgs) => void;
onChange: (...args: OnSourceChangeArgs[]) => void;
};
export type ImmutableSourceProperty = {

View file

@ -5,7 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import { FeatureCollection } from 'geojson';
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
import { Filter, TimeRange } from 'src/plugins/data/public';
import { AbstractSource, ISource } from '../source';
import { IField } from '../../fields/field';
@ -35,7 +35,7 @@ export type BoundsFilters = {
};
export interface IVectorSource extends ISource {
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
@ -51,10 +51,12 @@ export interface IVectorSource extends ISource {
getSyncMeta(): VectorSourceSyncMeta;
getFieldNames(): string[];
getApplyGlobalQuery(): boolean;
createField({ fieldName }: { fieldName: string }): IField;
canFormatFeatureProperties(): boolean;
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
@ -72,6 +74,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
canFormatFeatureProperties(): boolean;
getApplyGlobalQuery(): boolean;
getFieldNames(): string[];
createField({ fieldName }: { fieldName: string }): IField;
}
export interface ITiledSingleLayerVectorSource extends IVectorSource {
@ -83,4 +86,5 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource {
}>;
getMinZoom(): number;
getMaxZoom(): number;
getLayerName(): string;
}

View file

@ -89,7 +89,7 @@ export class ColorMapSelect extends Component {
};
_renderColorStopsInput() {
if (!this.props.useCustomColorMap) {
if (!this.props.isCustomOnly && !this.props.useCustomColorMap) {
return null;
}
@ -102,7 +102,7 @@ export class ColorMapSelect extends Component {
swatches={this.props.swatches}
/>
);
} else
} else {
colorStopEditor = (
<ColorStopsCategorical
colorStops={this.state.customColorMap}
@ -112,6 +112,7 @@ export class ColorMapSelect extends Component {
swatches={this.props.swatches}
/>
);
}
return (
<EuiFlexGroup>
@ -121,6 +122,10 @@ export class ColorMapSelect extends Component {
}
_renderColorMapSelections() {
if (this.props.isCustomOnly) {
return null;
}
const colorMapOptionsWithCustom = [
{
value: CUSTOM_COLOR_MAP,
@ -146,19 +151,22 @@ export class ColorMapSelect extends Component {
) : null;
return (
<EuiFlexGroup gutterSize={'none'}>
{toggle}
<EuiFlexItem>
<EuiSuperSelect
compressed
options={colorMapOptionsWithCustom}
onChange={this._onColorMapSelect}
valueOfSelected={valueOfSelected}
hasDividers={true}
data-test-subj={`colorMapSelect_${this.props.styleProperty.getStyleName()}`}
/>
</EuiFlexItem>
</EuiFlexGroup>
<Fragment>
<EuiFlexGroup gutterSize={'none'}>
{toggle}
<EuiFlexItem>
<EuiSuperSelect
compressed
options={colorMapOptionsWithCustom}
onChange={this._onColorMapSelect}
valueOfSelected={valueOfSelected}
hasDividers={true}
data-test-subj={`colorMapSelect_${this.props.styleProperty.getStyleName()}`}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</Fragment>
);
}
@ -166,7 +174,6 @@ export class ColorMapSelect extends Component {
return (
<Fragment>
{this._renderColorMapSelections()}
<EuiSpacer size="s" />
{this._renderColorStopsInput()}
</Fragment>
);

View file

@ -90,6 +90,7 @@ export function DynamicColorForm({
if (styleProperty.isOrdinal()) {
return (
<ColorMapSelect
isCustomOnly={!field.supportsAutoDomain}
colorMapOptions={COLOR_GRADIENTS}
customOptionLabel={i18n.translate('xpack.maps.style.customColorRampLabel', {
defaultMessage: 'Custom color ramp',
@ -108,6 +109,7 @@ export function DynamicColorForm({
} else if (styleProperty.isCategorical()) {
return (
<ColorMapSelect
isCustomOnly={!field.supportsAutoDomain}
colorMapOptions={COLOR_PALETTES}
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
defaultMessage: 'Custom color palette',

View file

@ -81,9 +81,16 @@ export function FieldSelect({ fields, selectedFieldName, onChange, styleName, ..
let selectedOption;
if (selectedFieldName) {
selectedOption = fields.find((field) => {
const field = fields.find((field) => {
return field.name === selectedFieldName;
});
//Do not spread in all the other unused values (e.g. type, supportsAutoDomain etc...)
if (field) {
selectedOption = {
value: field.value,
label: field.label,
};
}
}
return (

View file

@ -46,19 +46,16 @@ export class StyleMapSelect extends Component {
};
_renderCustomStopsInput() {
if (!this.props.useCustomMap) {
return !this.props.isCustomOnly && !this.props.useCustomMap
? null
: this.props.renderCustomStopsInput(this._onCustomMapChange);
}
_renderMapSelect() {
if (this.props.isCustomOnly) {
return null;
}
return (
<Fragment>
<EuiSpacer size="s" />
{this.props.renderCustomStopsInput(this._onCustomMapChange)}
</Fragment>
);
}
render() {
const mapOptionsWithCustom = [
{
value: CUSTOM_MAP,
@ -87,6 +84,15 @@ export class StyleMapSelect extends Component {
hasDividers={true}
compressed
/>
<EuiSpacer size="s" />
</Fragment>
);
}
render() {
return (
<Fragment>
{this._renderMapSelect()}
{this._renderCustomStopsInput()}
</Fragment>
);

View file

@ -36,17 +36,20 @@ export function DynamicIconForm({
};
function renderIconMapSelect() {
if (!styleOptions.field || !styleOptions.field.name) {
const field = styleProperty.getField();
if (!field) {
return null;
}
return (
<IconMapSelect
{...styleOptions}
useCustomIconMap={_.get(styleOptions, 'useCustomColorRamp', false)}
styleProperty={styleProperty}
onChange={onIconMapChange}
isDarkMode={isDarkMode}
symbolOptions={symbolOptions}
isCustomOnly={!field.supportsAutoDomain()}
/>
);
}

View file

@ -8,8 +8,8 @@ import React from 'react';
import { StyleMapSelect } from '../style_map_select';
import { i18n } from '@kbn/i18n';
import { getIconPaletteOptions } from '../../symbol_utils';
import { IconStops } from './icon_stops';
import { getIconPaletteOptions } from '../../symbol_utils';
export function IconMapSelect({
customIconStops,
@ -19,6 +19,7 @@ export function IconMapSelect({
styleProperty,
symbolOptions,
useCustomIconMap,
isCustomOnly,
}) {
function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) {
onChange({
@ -52,6 +53,7 @@ export function IconMapSelect({
useCustomMap={useCustomIconMap}
selectedMapId={iconPaletteId}
renderCustomStopsInput={renderCustomIconStopsInput}
isCustomOnly={isCustomOnly}
/>
);
}

View file

@ -62,6 +62,7 @@ export class VectorStyleEditor extends Component {
name: field.getName(),
origin: field.getOrigin(),
type: await field.getDataType(),
supportsAutoDomain: field.supportsAutoDomain(),
};
};
@ -109,7 +110,9 @@ export class VectorStyleEditor extends Component {
}
_getOrdinalFields() {
return [...this.state.dateFields, ...this.state.numberFields];
return [...this.state.dateFields, ...this.state.numberFields].filter((field) => {
return field.supportsAutoDomain;
});
}
_handleSelectedFeatureChange = (selectedFeature) => {

View file

@ -10,11 +10,10 @@ import { VECTOR_STYLES } from '../../../../../common/constants';
export class DynamicOrientationProperty extends DynamicStyleProperty {
syncIconRotationWithMb(symbolLayerId, mbMap) {
if (this._options.field && this._options.field.name) {
const targetName = getComputedFieldName(
VECTOR_STYLES.ICON_ORIENTATION,
this._options.field.name
);
if (this._field && this._field.isValid()) {
const targetName = this._field.supportsAutoDomain()
? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName())
: this._field.getName();
// Using property state instead of feature-state because layout properties do not support feature-state
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]);
} else {

View file

@ -10,7 +10,11 @@ import { getComputedFieldName } from '../style_util';
export class DynamicTextProperty extends DynamicStyleProperty {
syncTextFieldWithMb(mbLayerId, mbMap) {
if (this._field && this._field.isValid()) {
const targetName = getComputedFieldName(this._styleName, this._options.field.name);
// Fields that support auto-domain are normalized with a field-formatter and stored into a computed-field
// Otherwise, the raw value is just carried over and no computed field is created.
const targetName = this._field.supportsAutoDomain()
? getComputedFieldName(this._styleName, this.getFieldName())
: this._field.getName();
mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']);
} else {
mbMap.setLayoutProperty(mbLayerId, 'text-field', null);

View file

@ -19,7 +19,7 @@ export interface ITooltipProperty {
export interface LoadFeatureProps {
layerId: string;
featureId: number;
featureId?: number | string;
}
export interface FeatureGeometry {

View file

@ -43,16 +43,16 @@ export class LayerPanel extends React.Component {
componentDidMount() {
this._isMounted = true;
this.loadDisplayName();
this.loadImmutableSourceProperties();
this.loadLeftJoinFields();
this._loadDisplayName();
this._loadImmutableSourceProperties();
this._loadLeftJoinFields();
}
componentWillUnmount() {
this._isMounted = false;
}
loadDisplayName = async () => {
_loadDisplayName = async () => {
if (!this.props.selectedLayer) {
return;
}
@ -63,7 +63,7 @@ export class LayerPanel extends React.Component {
}
};
loadImmutableSourceProperties = async () => {
_loadImmutableSourceProperties = async () => {
if (!this.props.selectedLayer) {
return;
}
@ -74,7 +74,7 @@ export class LayerPanel extends React.Component {
}
};
async loadLeftJoinFields() {
async _loadLeftJoinFields() {
if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
return;
}
@ -97,8 +97,11 @@ export class LayerPanel extends React.Component {
}
}
_onSourceChange = ({ propName, value, newLayerType }) => {
this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType);
_onSourceChange = (...args) => {
for (let i = 0; i < args.length; i++) {
const { propName, value, newLayerType } = args[i];
this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType);
}
};
_renderFilterSection() {

View file

@ -31,14 +31,15 @@ export class FeatureProperties extends React.Component {
this._isMounted = false;
}
_loadProperties = () => {
_loadProperties = async () => {
this._fetchProperties({
nextFeatureId: this.props.featureId,
nextLayerId: this.props.layerId,
mbProperties: this.props.mbProperties,
});
};
_fetchProperties = async ({ nextLayerId, nextFeatureId }) => {
_fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => {
if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) {
// do not reload same feature properties
return;
@ -64,6 +65,7 @@ export class FeatureProperties extends React.Component {
properties = await this.props.loadFeatureProperties({
layerId: nextLayerId,
featureId: nextFeatureId,
mbProperties: mbProperties,
});
} catch (error) {
if (this._isMounted) {

View file

@ -132,6 +132,7 @@ export class FeaturesTooltip extends React.Component {
<FeatureProperties
featureId={this.state.currentFeature.id}
layerId={this.state.currentFeature.layerId}
mbProperties={this.state.currentFeature.mbProperties}
loadFeatureProperties={this.props.loadFeatureProperties}
showFilterButtons={!!this.props.addFilters && this.props.isLocked}
onCloseTooltip={this.props.closeTooltip}

View file

@ -53,7 +53,7 @@ export class TooltipControl extends React.Component {
});
}
_getIdsForFeatures(mbFeatures) {
_getTooltipFeatures(mbFeatures) {
const uniqueFeatures = [];
//there may be duplicates in the results from mapbox
//this is because mapbox returns the results per tile
@ -72,9 +72,18 @@ export class TooltipControl extends React.Component {
}
}
if (!match) {
// "tags" (aka properties) are optional in .mvt tiles.
// It's not entirely clear how mapbox-gl handles those.
// - As null value (as defined in https://tools.ietf.org/html/rfc7946#section-3.2)
// - As undefined value
// - 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
uniqueFeatures.push({
id: featureId,
layerId: layerId,
mbProperties,
});
}
}
@ -89,7 +98,7 @@ export class TooltipControl extends React.Component {
this._updateHoverTooltipState.cancel(); //ignore any possible moves
const mbFeatures = this._getFeaturesUnderPointer(e.point);
const mbFeatures = this._getMbFeaturesUnderPointer(e.point);
if (!mbFeatures.length) {
// No features at click location so there is no tooltip to open
return;
@ -98,9 +107,9 @@ export class TooltipControl extends React.Component {
const targetMbFeataure = mbFeatures[0];
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure);
const features = this._getIdsForFeatures(mbFeatures);
const features = this._getTooltipFeatures(mbFeatures);
this.props.openOnClickTooltip({
features: features,
features,
location: popupAnchorLocation,
});
};
@ -111,7 +120,7 @@ export class TooltipControl extends React.Component {
return;
}
const mbFeatures = this._getFeaturesUnderPointer(e.point);
const mbFeatures = this._getMbFeaturesUnderPointer(e.point);
if (!mbFeatures.length) {
this.props.closeOnHoverTooltip();
return;
@ -127,7 +136,7 @@ export class TooltipControl extends React.Component {
}
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature);
const features = this._getIdsForFeatures(mbFeatures);
const features = this._getTooltipFeatures(mbFeatures);
this.props.openOnHoverTooltip({
features: features,
location: popupAnchorLocation,
@ -149,7 +158,7 @@ export class TooltipControl extends React.Component {
});
}
_getFeaturesUnderPointer(mbLngLatPoint) {
_getMbFeaturesUnderPointer(mbLngLatPoint) {
if (!this.props.mbMap) {
return [];
}

View file

@ -236,7 +236,7 @@ describe('TooltipControl', () => {
sinon.assert.notCalled(closeOnClickTooltipStub);
sinon.assert.calledWith(openOnClickTooltipStub, {
features: [{ id: 1, layerId: 'tfi3f' }],
features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }],
location: [100, 30],
});
});

View file

@ -58,7 +58,7 @@ export class TooltipPopover extends Component {
// Mapbox feature geometry is from vector tile and is not the same as the original geometry.
_loadFeatureGeometry = ({ layerId, featureId }) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer) {
if (!tooltipLayer || typeof featureId === 'undefined') {
return null;
}
@ -70,22 +70,24 @@ export class TooltipPopover extends Component {
return targetFeature.geometry;
};
_loadFeatureProperties = async ({ layerId, featureId }) => {
_loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer) {
return [];
}
const targetFeature = tooltipLayer.getFeatureById(featureId);
if (!targetFeature) {
return [];
let targetFeature;
if (typeof featureId !== 'undefined') {
targetFeature = tooltipLayer.getFeatureById(featureId);
}
return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties);
const properties = targetFeature ? targetFeature.properties : mbProperties;
return await tooltipLayer.getPropertiesForTooltip(properties);
};
_loadPreIndexedShape = async ({ layerId, featureId }) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer) {
if (!tooltipLayer || typeof featureId === 'undefined') {
return null;
}

View file

@ -7,7 +7,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { MapToolTipComponent } from './map_tool_tip';
import { MapFeature } from '../types';
import { TooltipFeature } from '../../../../../../maps/common/descriptor_types';
describe('MapToolTip', () => {
test('placeholder component renders correctly against snapshot', () => {
@ -18,10 +18,11 @@ describe('MapToolTip', () => {
test('full component renders correctly against snapshot', () => {
const addFilters = jest.fn();
const closeTooltip = jest.fn();
const features: MapFeature[] = [
const features: TooltipFeature[] = [
{
id: 1,
layerId: 'layerId',
mbProperties: {},
},
];
const getLayerName = jest.fn();

View file

@ -36,11 +36,6 @@ export type SetQuery = (params: {
refetch: inputsModel.Refetch;
}) => void;
export interface MapFeature {
id: number;
layerId: string;
}
export interface FeatureGeometry {
coordinates: [number];
type: string;

View file

@ -9214,9 +9214,6 @@
"xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです",
"xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス",
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報",
"xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "レイヤー名",
"xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大ズーム",
"xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小ズーム",
"xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "ベトルタイルレイヤー",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名",

View file

@ -9218,9 +9218,6 @@
"xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴",
"xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务",
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置",
"xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "图层名称",
"xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大缩放",
"xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小缩放",
"xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴图层",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称",

View file

@ -76,7 +76,11 @@ export default function ({ getPageObjects, getService }) {
const { lat, lon, zoom } = await PageObjects.maps.getView();
expect(Math.round(lat)).to.equal(41);
expect(Math.round(lon)).to.equal(-102);
expect(Math.round(zoom)).to.equal(5);
// Centering is correct, but screen-size and dpi affect zoom level,
// causing this test to be brittle in different environments
// Expecting zoom-level to be between ]4,5]
expect(Math.ceil(zoom)).to.equal(5);
});
});