[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:
parent
3f808688e1
commit
9c76f19186
62 changed files with 2407 additions and 380 deletions
|
@ -17,7 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React, { Component } from 'react';
|
import React, { Component, ReactNode } from 'react';
|
||||||
import { EuiFormRow, EuiDualRange } from '@elastic/eui';
|
import { EuiFormRow, EuiDualRange } from '@elastic/eui';
|
||||||
import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row';
|
import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row';
|
||||||
import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range';
|
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'> {
|
interface Props extends Omit<EuiDualRangeProps, 'value' | 'onChange' | 'min' | 'max'> {
|
||||||
value?: Value;
|
value?: Value;
|
||||||
allowEmptyRange?: boolean;
|
allowEmptyRange?: boolean;
|
||||||
label?: string;
|
label?: string | ReactNode;
|
||||||
formRowDisplay?: EuiFormRowDisplayKeys;
|
formRowDisplay?: EuiFormRowDisplayKeys;
|
||||||
onChange?: (val: [string, string]) => void;
|
onChange?: (val: [string, string]) => void;
|
||||||
min?: number;
|
min?: number;
|
||||||
|
|
|
@ -223,6 +223,11 @@ export enum SCALING_TYPES {
|
||||||
|
|
||||||
export const RGBA_0000 = 'rgba(0,0,0,0)';
|
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 const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID';
|
||||||
|
|
||||||
export enum INITIAL_LOCATION {
|
export enum INITIAL_LOCATION {
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './data_request_descriptor_types';
|
export * from './data_request_descriptor_types';
|
||||||
export * from './descriptor_types';
|
export * from './sources';
|
||||||
export * from './map_descriptor';
|
export * from './map_descriptor';
|
||||||
export * from './style_property_descriptor_types';
|
export * from './style_property_descriptor_types';
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||||
|
|
||||||
|
import { GeoJsonProperties } from 'geojson';
|
||||||
import { Query } from '../../../../../src/plugins/data/common';
|
import { Query } from '../../../../../src/plugins/data/common';
|
||||||
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
|
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
|
||||||
|
|
||||||
|
@ -39,8 +40,9 @@ export type Goto = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TooltipFeature = {
|
export type TooltipFeature = {
|
||||||
id: number;
|
id?: number | string;
|
||||||
layerId: string;
|
layerId: string;
|
||||||
|
mbProperties: GeoJsonProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TooltipState = {
|
export type TooltipState = {
|
||||||
|
|
|
@ -7,7 +7,14 @@
|
||||||
|
|
||||||
import { FeatureCollection } from 'geojson';
|
import { FeatureCollection } from 'geojson';
|
||||||
import { Query } from 'src/plugins/data/public';
|
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 { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types';
|
||||||
import { DataRequestDescriptor } from './data_request_descriptor_types';
|
import { DataRequestDescriptor } from './data_request_descriptor_types';
|
||||||
|
|
||||||
|
@ -96,18 +103,34 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor &
|
||||||
urlTemplate: string;
|
urlTemplate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & {
|
export type MVTFieldDescriptor = {
|
||||||
|
name: string;
|
||||||
|
type: MVT_FIELD_TYPE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TiledSingleLayerVectorSourceSettings = {
|
||||||
urlTemplate: string;
|
urlTemplate: string;
|
||||||
layerName: 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 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.
|
// 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.
|
// These are important so mapbox does not issue invalid requests based on the zoom level.
|
||||||
// e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels
|
|
||||||
|
// 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;
|
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;
|
maxSourceZoom: number;
|
||||||
|
|
||||||
|
fields: MVTFieldDescriptor[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor &
|
||||||
|
TiledSingleLayerVectorSourceSettings & {
|
||||||
|
tooltipProperties: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type GeojsonFileSourceDescriptor = {
|
export type GeojsonFileSourceDescriptor = {
|
||||||
__featureCollection: FeatureCollection;
|
__featureCollection: FeatureCollection;
|
||||||
name: string;
|
name: string;
|
|
@ -128,6 +128,10 @@ export class ESAggField implements IESAggField {
|
||||||
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
|
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
|
||||||
return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null;
|
return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutoDomain(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function esAggFieldsFactory(
|
export function esAggFieldsFactory(
|
||||||
|
|
|
@ -20,6 +20,12 @@ export interface IField {
|
||||||
isValid(): boolean;
|
isValid(): boolean;
|
||||||
getOrdinalFieldMetaRequest(): Promise<unknown>;
|
getOrdinalFieldMetaRequest(): Promise<unknown>;
|
||||||
getCategoricalFieldMetaRequest(size: number): 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;
|
supportsFieldMeta(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,4 +86,8 @@ export class AbstractField implements IField {
|
||||||
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
|
async getCategoricalFieldMetaRequest(size: number): Promise<unknown> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutoDomain(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
59
x-pack/plugins/maps/public/classes/fields/mvt_field.ts
Normal file
59
x-pack/plugins/maps/public/classes/fields/mvt_field.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,10 @@ export class TopTermPercentageField implements IESAggField {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportsAutoDomain(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
supportsFieldMeta(): boolean {
|
supportsFieldMeta(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { getFileUploadComponent } from '../../../kibana_services';
|
import { getFileUploadComponent } from '../../../kibana_services';
|
||||||
import { GeojsonFileSource } from '../../sources/geojson_file_source';
|
import { GeojsonFileSource } from '../../sources/geojson_file_source';
|
||||||
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
|
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
import { createDefaultLayerDescriptor } from '../../sources/es_search_source';
|
import { createDefaultLayerDescriptor } from '../../sources/es_search_source';
|
||||||
import { RenderWizardArguments } from '../../layers/layer_wizard_registry';
|
import { RenderWizardArguments } from '../../layers/layer_wizard_registry';
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ export class HeatmapLayer extends VectorLayer {
|
||||||
resolution: this.getSource().getGridResolution(),
|
resolution: this.getSource().getGridResolution(),
|
||||||
});
|
});
|
||||||
mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
|
mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
|
||||||
mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
getLayerTypeIconName() {
|
getLayerTypeIconName() {
|
||||||
|
|
|
@ -325,27 +325,28 @@ export class AbstractLayer implements ILayer {
|
||||||
return this._source.getMinZoom();
|
return this._source.getMinZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getMbSourceId() {
|
||||||
|
return this.getId();
|
||||||
|
}
|
||||||
|
|
||||||
_requiresPrevSourceCleanup(mbMap: unknown) {
|
_requiresPrevSourceCleanup(mbMap: unknown) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeStaleMbSourcesAndLayers(mbMap: unknown) {
|
_removeStaleMbSourcesAndLayers(mbMap: unknown) {
|
||||||
if (this._requiresPrevSourceCleanup(mbMap)) {
|
if (this._requiresPrevSourceCleanup(mbMap)) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const mbStyle = mbMap.getStyle();
|
const mbStyle = mbMap.getStyle();
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
mbStyle.layers.forEach((mbLayer) => {
|
mbStyle.layers.forEach((mbLayer) => {
|
||||||
// @ts-ignore
|
|
||||||
if (this.ownsMbLayerId(mbLayer.id)) {
|
if (this.ownsMbLayerId(mbLayer.id)) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
mbMap.removeLayer(mbLayer.id);
|
mbMap.removeLayer(mbLayer.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
Object.keys(mbStyle.sources).some((mbSourceId) => {
|
Object.keys(mbStyle.sources).some((mbSourceId) => {
|
||||||
// @ts-ignore
|
|
||||||
if (this.ownsMbSourceId(mbSourceId)) {
|
if (this.ownsMbSourceId(mbSourceId)) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
mbMap.removeSource(mbSourceId);
|
mbMap.removeSource(mbSourceId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -429,7 +430,7 @@ export class AbstractLayer implements ILayer {
|
||||||
throw new Error('Should implement AbstractLayer#ownsMbLayerId');
|
throw new Error('Should implement AbstractLayer#ownsMbLayerId');
|
||||||
}
|
}
|
||||||
|
|
||||||
ownsMbSourceId(sourceId: string): boolean {
|
ownsMbSourceId(mbSourceId: string): boolean {
|
||||||
throw new Error('Should implement AbstractLayer#ownsMbSourceId');
|
throw new Error('Should implement AbstractLayer#ownsMbSourceId');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,8 +74,8 @@ export class TileLayer extends AbstractLayer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceId = this.getId();
|
const mbSourceId = this._getMbSourceId();
|
||||||
mbMap.addSource(sourceId, {
|
mbMap.addSource(mbSourceId, {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: [tmsSourceData.url],
|
tiles: [tmsSourceData.url],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
|
@ -85,7 +85,7 @@ export class TileLayer extends AbstractLayer {
|
||||||
mbMap.addLayer({
|
mbMap.addLayer({
|
||||||
id: mbLayerId,
|
id: mbLayerId,
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: sourceId,
|
source: mbSourceId,
|
||||||
minzoom: this._descriptor.minZoom,
|
minzoom: this._descriptor.minZoom,
|
||||||
maxzoom: this._descriptor.maxZoom,
|
maxzoom: this._descriptor.maxZoom,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
`;
|
|
@ -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' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,31 +6,30 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EuiIcon } from '@elastic/eui';
|
import { EuiIcon } from '@elastic/eui';
|
||||||
|
import { Feature } from 'geojson';
|
||||||
import { VectorStyle } from '../../styles/vector/vector_style';
|
import { VectorStyle } from '../../styles/vector/vector_style';
|
||||||
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants';
|
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants';
|
||||||
import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer';
|
import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer';
|
||||||
import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
|
|
||||||
import { ITiledSingleLayerVectorSource } from '../../sources/vector_source';
|
import { ITiledSingleLayerVectorSource } from '../../sources/vector_source';
|
||||||
import { DataRequestContext } from '../../../actions';
|
import { DataRequestContext } from '../../../actions';
|
||||||
import { ISource } from '../../sources/source';
|
|
||||||
import {
|
import {
|
||||||
VectorLayerDescriptor,
|
VectorLayerDescriptor,
|
||||||
VectorSourceRequestMeta,
|
VectorSourceRequestMeta,
|
||||||
} from '../../../../common/descriptor_types';
|
} 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 {
|
export class TiledVectorLayer extends VectorLayer {
|
||||||
static type = LAYER_TYPE.TILED_VECTOR;
|
static type = LAYER_TYPE.TILED_VECTOR;
|
||||||
|
|
||||||
static createDescriptor(
|
static createDescriptor(
|
||||||
descriptor: Partial<VectorLayerDescriptor>,
|
descriptor: Partial<VectorLayerDescriptor>,
|
||||||
mapColors: string[]
|
mapColors?: string[]
|
||||||
): VectorLayerDescriptor {
|
): VectorLayerDescriptor {
|
||||||
const layerDescriptor = super.createDescriptor(descriptor, mapColors);
|
const layerDescriptor = super.createDescriptor(descriptor, mapColors);
|
||||||
layerDescriptor.type = TiledVectorLayer.type;
|
layerDescriptor.type = TiledVectorLayer.type;
|
||||||
|
|
||||||
if (!layerDescriptor.style) {
|
if (!layerDescriptor.style) {
|
||||||
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors);
|
const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []);
|
||||||
layerDescriptor.style = VectorStyle.createDescriptor(styleProperties);
|
layerDescriptor.style = VectorStyle.createDescriptor(styleProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,13 +63,16 @@ export class TiledVectorLayer extends VectorLayer {
|
||||||
);
|
);
|
||||||
const prevDataRequest = this.getSourceDataRequest();
|
const prevDataRequest = this.getSourceDataRequest();
|
||||||
|
|
||||||
const canSkip = await canSkipSourceUpdate({
|
if (prevDataRequest) {
|
||||||
source: this._source as ISource,
|
const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
|
||||||
prevDataRequest,
|
const canSkipBecauseNoChanges =
|
||||||
nextMeta: searchFilters,
|
data.layerName === this._source.getLayerName() &&
|
||||||
});
|
data.minSourceZoom === this._source.getMinZoom() &&
|
||||||
if (canSkip) {
|
data.maxSourceZoom === this._source.getMaxZoom();
|
||||||
return null;
|
|
||||||
|
if (canSkipBecauseNoChanges) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
|
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
|
||||||
|
@ -89,37 +91,41 @@ export class TiledVectorLayer extends VectorLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncSourceBindingWithMb(mbMap: unknown) {
|
_syncSourceBindingWithMb(mbMap: unknown) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const mbSource = mbMap.getSource(this.getId());
|
const mbSource = mbMap.getSource(this._getMbSourceId());
|
||||||
if (!mbSource) {
|
if (mbSource) {
|
||||||
const sourceDataRequest = this.getSourceDataRequest();
|
return;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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) {
|
_syncStylePropertiesWithMb(mbMap: unknown) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const mbSource = mbMap.getSource(this.getId());
|
const mbSource = mbMap.getSource(this._getMbSourceId());
|
||||||
if (!mbSource) {
|
if (!mbSource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -129,32 +135,52 @@ export class TiledVectorLayer extends VectorLayer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
|
const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
|
||||||
|
if (sourceMeta.layerName === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._setMbPointsProperties(mbMap, sourceMeta.layerName);
|
this._setMbPointsProperties(mbMap, sourceMeta.layerName);
|
||||||
this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName);
|
this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
_requiresPrevSourceCleanup(mbMap: unknown): boolean {
|
_requiresPrevSourceCleanup(mbMap: unknown): boolean {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const mbTileSource = mbMap.getSource(this.getId());
|
const mbTileSource = mbMap.getSource(this._getMbSourceId());
|
||||||
if (!mbTileSource) {
|
if (!mbTileSource) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataRequest = this.getSourceDataRequest();
|
const dataRequest = this.getSourceDataRequest();
|
||||||
if (!dataRequest) {
|
if (!dataRequest) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig;
|
const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig;
|
||||||
if (
|
|
||||||
mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate &&
|
if (!tiledSourceMeta) {
|
||||||
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.
|
|
||||||
return false;
|
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) {
|
syncLayerWithMB(mbMap: unknown) {
|
||||||
|
@ -171,4 +197,8 @@ export class TiledVectorLayer extends VectorLayer {
|
||||||
// higher resolution vector tiles cannot be displayed at lower-res
|
// higher resolution vector tiles cannot be displayed at lower-res
|
||||||
return Math.max(this._source.getMinZoom(), super.getMinZoom());
|
return Math.max(this._source.getMinZoom(), super.getMinZoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeatureById(id: string | number): Feature | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||||
|
|
||||||
|
import { Feature, GeoJsonProperties } from 'geojson';
|
||||||
import { AbstractLayer } from '../layer';
|
import { AbstractLayer } from '../layer';
|
||||||
import { IVectorSource } from '../../sources/vector_source';
|
import { IVectorSource } from '../../sources/vector_source';
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +18,7 @@ import { IJoin } from '../../joins/join';
|
||||||
import { IVectorStyle } from '../../styles/vector/vector_style';
|
import { IVectorStyle } from '../../styles/vector/vector_style';
|
||||||
import { IField } from '../../fields/field';
|
import { IField } from '../../fields/field';
|
||||||
import { DataRequestContext } from '../../../actions';
|
import { DataRequestContext } from '../../../actions';
|
||||||
|
import { ITooltipProperty } from '../../tooltips/tooltip_property';
|
||||||
|
|
||||||
export type VectorLayerArguments = {
|
export type VectorLayerArguments = {
|
||||||
source: IVectorSource;
|
source: IVectorSource;
|
||||||
|
@ -31,6 +33,8 @@ export interface IVectorLayer extends ILayer {
|
||||||
getValidJoins(): IJoin[];
|
getValidJoins(): IJoin[];
|
||||||
getSource(): IVectorSource;
|
getSource(): IVectorSource;
|
||||||
getStyle(): IVectorStyle;
|
getStyle(): IVectorStyle;
|
||||||
|
getFeatureById(id: string | number): Feature | null;
|
||||||
|
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
||||||
|
@ -75,4 +79,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
|
||||||
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
|
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
|
||||||
getSource(): IVectorSource;
|
getSource(): IVectorSource;
|
||||||
getStyle(): IVectorStyle;
|
getStyle(): IVectorStyle;
|
||||||
|
getFeatureById(id: string | number): Feature | null;
|
||||||
|
getPropertiesForTooltip(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -672,10 +672,10 @@ export class VectorLayer extends AbstractLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncVisibilityWithMb(mbMap, markerLayerId);
|
this.syncVisibilityWithMb(mbMap, markerLayerId);
|
||||||
mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
mbMap.setLayerZoomRange(markerLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||||
if (markerLayerId !== textLayerId) {
|
if (markerLayerId !== textLayerId) {
|
||||||
this.syncVisibilityWithMb(mbMap, 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);
|
this.syncVisibilityWithMb(mbMap, fillLayerId);
|
||||||
mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||||
const fillFilterExpr = getFillFilterExpression(hasJoins);
|
const fillFilterExpr = getFillFilterExpression(hasJoins);
|
||||||
if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
|
if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
|
||||||
mbMap.setFilter(fillLayerId, fillFilterExpr);
|
mbMap.setFilter(fillLayerId, fillFilterExpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncVisibilityWithMb(mbMap, lineLayerId);
|
this.syncVisibilityWithMb(mbMap, lineLayerId);
|
||||||
mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
|
||||||
const lineFilterExpr = getLineFilterExpression(hasJoins);
|
const lineFilterExpr = getLineFilterExpression(hasJoins);
|
||||||
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
|
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
|
||||||
mbMap.setFilter(lineLayerId, lineFilterExpr);
|
mbMap.setFilter(lineLayerId, lineFilterExpr);
|
||||||
|
@ -822,9 +822,9 @@ export class VectorLayer extends AbstractLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncSourceBindingWithMb(mbMap) {
|
_syncSourceBindingWithMb(mbMap) {
|
||||||
const mbSource = mbMap.getSource(this.getId());
|
const mbSource = mbMap.getSource(this._getMbSourceId());
|
||||||
if (!mbSource) {
|
if (!mbSource) {
|
||||||
mbMap.addSource(this.getId(), {
|
mbMap.addSource(this._getMbSourceId(), {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: EMPTY_FEATURE_COLLECTION,
|
data: EMPTY_FEATURE_COLLECTION,
|
||||||
});
|
});
|
||||||
|
@ -891,16 +891,17 @@ export class VectorLayer extends AbstractLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPropertiesForTooltip(properties) {
|
async getPropertiesForTooltip(properties) {
|
||||||
let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties);
|
const vectorSource = this.getSource();
|
||||||
this._addJoinsToSourceTooltips(allTooltips);
|
let allProperties = await vectorSource.filterAndFormatPropertiesToHtml(properties);
|
||||||
|
this._addJoinsToSourceTooltips(allProperties);
|
||||||
|
|
||||||
for (let i = 0; i < this.getJoins().length; i++) {
|
for (let i = 0; i < this.getJoins().length; i++) {
|
||||||
const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip(
|
const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip(
|
||||||
properties
|
properties
|
||||||
);
|
);
|
||||||
allTooltips = [...allTooltips, ...propsFromJoin];
|
allProperties = [...allProperties, ...propsFromJoin];
|
||||||
}
|
}
|
||||||
return allTooltips;
|
return allProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
canShowTooltip() {
|
canShowTooltip() {
|
||||||
|
@ -912,7 +913,7 @@ export class VectorLayer extends AbstractLayer {
|
||||||
getFeatureById(id) {
|
getFeatureById(id) {
|
||||||
const featureCollection = this._getSourceFeatureCollection();
|
const featureCollection = this._getSourceFeatureCollection();
|
||||||
if (!featureCollection) {
|
if (!featureCollection) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return featureCollection.features.find((feature) => {
|
return featureCollection.features.find((feature) => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/vi
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
onChange: (args: OnSourceChangeArgs) => void;
|
onChange: (...args: OnSourceChangeArgs[]) => void;
|
||||||
source: IEmsFileSource;
|
source: IEmsFileSource;
|
||||||
tooltipFields: IField[];
|
tooltipFields: IField[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -6,23 +6,21 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor';
|
||||||
MVTSingleLayerVectorSourceEditor,
|
|
||||||
MVTSingleLayerVectorSourceConfig,
|
|
||||||
} from './mvt_single_layer_vector_source_editor';
|
|
||||||
import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source';
|
import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source';
|
||||||
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
|
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
|
||||||
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
|
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
|
||||||
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
|
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
|
||||||
|
import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types';
|
||||||
|
|
||||||
export const mvtVectorSourceWizardConfig: LayerWizard = {
|
export const mvtVectorSourceWizardConfig: LayerWizard = {
|
||||||
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
|
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
|
||||||
description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', {
|
description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', {
|
||||||
defaultMessage: 'Vector source wizard',
|
defaultMessage: 'Data service implementing the Mapbox vector tile specification',
|
||||||
}),
|
}),
|
||||||
icon: 'grid',
|
icon: 'grid',
|
||||||
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
|
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
|
||||||
const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => {
|
const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => {
|
||||||
const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig);
|
const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig);
|
||||||
const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
|
const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
|
||||||
previewLayers([layerDescriptor]);
|
previewLayers([layerDescriptor]);
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
|
||||||
});
|
|
|
@ -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,
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
|
@ -10,17 +10,14 @@ import _ from 'lodash';
|
||||||
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
|
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
|
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 = {
|
interface Props {
|
||||||
urlTemplate: string;
|
onSourceConfigChange: (sourceConfig: TiledSingleLayerVectorSourceSettings) => void;
|
||||||
layerName: string;
|
|
||||||
minSourceZoom: number;
|
|
||||||
maxSourceZoom: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -28,6 +25,7 @@ interface State {
|
||||||
layerName: string;
|
layerName: string;
|
||||||
minSourceZoom: number;
|
minSourceZoom: number;
|
||||||
maxSourceZoom: number;
|
maxSourceZoom: number;
|
||||||
|
fields?: MVTFieldDescriptor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
||||||
|
@ -36,6 +34,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
||||||
layerName: '',
|
layerName: '',
|
||||||
minSourceZoom: MIN_ZOOM,
|
minSourceZoom: MIN_ZOOM,
|
||||||
maxSourceZoom: MAX_ZOOM,
|
maxSourceZoom: MAX_ZOOM,
|
||||||
|
fields: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
_sourceConfigChange = _.debounce(() => {
|
_sourceConfigChange = _.debounce(() => {
|
||||||
|
@ -50,6 +49,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
||||||
layerName: this.state.layerName,
|
layerName: this.state.layerName,
|
||||||
minSourceZoom: this.state.minSourceZoom,
|
minSourceZoom: this.state.minSourceZoom,
|
||||||
maxSourceZoom: this.state.maxSourceZoom,
|
maxSourceZoom: this.state.maxSourceZoom,
|
||||||
|
fields: this.state.fields,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -64,23 +64,13 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleLayerNameInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
_handleChange = (state: {
|
||||||
const layerName = e.target.value;
|
layerName: string;
|
||||||
this.setState(
|
fields: MVTFieldDescriptor[];
|
||||||
{
|
minSourceZoom: number;
|
||||||
layerName,
|
maxSourceZoom: number;
|
||||||
},
|
}) => {
|
||||||
() => this._sourceConfigChange()
|
this.setState(state, () => 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());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -90,37 +80,30 @@ export class MVTSingleLayerVectorSourceEditor extends Component<Props, State> {
|
||||||
label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', {
|
label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', {
|
||||||
defaultMessage: 'Url',
|
defaultMessage: 'Url',
|
||||||
})}
|
})}
|
||||||
>
|
helpText={i18n.translate(
|
||||||
<EuiFieldText value={this.state.urlTemplate} onChange={this._handleUrlTemplateChange} />
|
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage',
|
||||||
</EuiFormRow>
|
|
||||||
<EuiFormRow
|
|
||||||
label={i18n.translate(
|
|
||||||
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage',
|
|
||||||
{
|
{
|
||||||
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>
|
</EuiFormRow>
|
||||||
<ValidatedDualRange
|
|
||||||
label=""
|
<MVTSingleLayerSourceSettings
|
||||||
formRowDisplay="columnCompressed"
|
handleChange={this._handleChange}
|
||||||
min={MIN_ZOOM}
|
layerName={this.state.layerName}
|
||||||
max={MAX_ZOOM}
|
fields={this.state.fields}
|
||||||
value={[this.state.minSourceZoom, this.state.maxSourceZoom]}
|
minSourceZoom={this.state.minSourceZoom}
|
||||||
showInput="inputWithPopover"
|
maxSourceZoom={this.state.maxSourceZoom}
|
||||||
showRange
|
showFields={false}
|
||||||
showLabels
|
|
||||||
onChange={this._handleZoomRangeChange}
|
|
||||||
allowEmptyRange={false}
|
|
||||||
compressed
|
|
||||||
prepend={i18n.translate(
|
|
||||||
'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Zoom levels',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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[];
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
|
||||||
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
|
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
|
||||||
|
|
||||||
export type SourceEditorArgs = {
|
export type SourceEditorArgs = {
|
||||||
onChange: (args: OnSourceChangeArgs) => void;
|
onChange: (...args: OnSourceChangeArgs[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImmutableSourceProperty = {
|
export type ImmutableSourceProperty = {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
/* 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 { Filter, TimeRange } from 'src/plugins/data/public';
|
||||||
import { AbstractSource, ISource } from '../source';
|
import { AbstractSource, ISource } from '../source';
|
||||||
import { IField } from '../../fields/field';
|
import { IField } from '../../fields/field';
|
||||||
|
@ -35,7 +35,7 @@ export type BoundsFilters = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVectorSource extends ISource {
|
export interface IVectorSource extends ISource {
|
||||||
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
|
filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||||
getBoundsForFilters(
|
getBoundsForFilters(
|
||||||
boundsFilters: BoundsFilters,
|
boundsFilters: BoundsFilters,
|
||||||
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
|
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
|
||||||
|
@ -51,10 +51,12 @@ export interface IVectorSource extends ISource {
|
||||||
getSyncMeta(): VectorSourceSyncMeta;
|
getSyncMeta(): VectorSourceSyncMeta;
|
||||||
getFieldNames(): string[];
|
getFieldNames(): string[];
|
||||||
getApplyGlobalQuery(): boolean;
|
getApplyGlobalQuery(): boolean;
|
||||||
|
createField({ fieldName }: { fieldName: string }): IField;
|
||||||
|
canFormatFeatureProperties(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
|
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
|
||||||
filterAndFormatPropertiesToHtml(properties: unknown): Promise<ITooltipProperty[]>;
|
filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise<ITooltipProperty[]>;
|
||||||
getBoundsForFilters(
|
getBoundsForFilters(
|
||||||
boundsFilters: BoundsFilters,
|
boundsFilters: BoundsFilters,
|
||||||
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
|
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
|
||||||
|
@ -72,6 +74,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
|
||||||
canFormatFeatureProperties(): boolean;
|
canFormatFeatureProperties(): boolean;
|
||||||
getApplyGlobalQuery(): boolean;
|
getApplyGlobalQuery(): boolean;
|
||||||
getFieldNames(): string[];
|
getFieldNames(): string[];
|
||||||
|
createField({ fieldName }: { fieldName: string }): IField;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledSingleLayerVectorSource extends IVectorSource {
|
export interface ITiledSingleLayerVectorSource extends IVectorSource {
|
||||||
|
@ -83,4 +86,5 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource {
|
||||||
}>;
|
}>;
|
||||||
getMinZoom(): number;
|
getMinZoom(): number;
|
||||||
getMaxZoom(): number;
|
getMaxZoom(): number;
|
||||||
|
getLayerName(): string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ export class ColorMapSelect extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderColorStopsInput() {
|
_renderColorStopsInput() {
|
||||||
if (!this.props.useCustomColorMap) {
|
if (!this.props.isCustomOnly && !this.props.useCustomColorMap) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ export class ColorMapSelect extends Component {
|
||||||
swatches={this.props.swatches}
|
swatches={this.props.swatches}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else
|
} else {
|
||||||
colorStopEditor = (
|
colorStopEditor = (
|
||||||
<ColorStopsCategorical
|
<ColorStopsCategorical
|
||||||
colorStops={this.state.customColorMap}
|
colorStops={this.state.customColorMap}
|
||||||
|
@ -112,6 +112,7 @@ export class ColorMapSelect extends Component {
|
||||||
swatches={this.props.swatches}
|
swatches={this.props.swatches}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup>
|
<EuiFlexGroup>
|
||||||
|
@ -121,6 +122,10 @@ export class ColorMapSelect extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderColorMapSelections() {
|
_renderColorMapSelections() {
|
||||||
|
if (this.props.isCustomOnly) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const colorMapOptionsWithCustom = [
|
const colorMapOptionsWithCustom = [
|
||||||
{
|
{
|
||||||
value: CUSTOM_COLOR_MAP,
|
value: CUSTOM_COLOR_MAP,
|
||||||
|
@ -146,19 +151,22 @@ export class ColorMapSelect extends Component {
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup gutterSize={'none'}>
|
<Fragment>
|
||||||
{toggle}
|
<EuiFlexGroup gutterSize={'none'}>
|
||||||
<EuiFlexItem>
|
{toggle}
|
||||||
<EuiSuperSelect
|
<EuiFlexItem>
|
||||||
compressed
|
<EuiSuperSelect
|
||||||
options={colorMapOptionsWithCustom}
|
compressed
|
||||||
onChange={this._onColorMapSelect}
|
options={colorMapOptionsWithCustom}
|
||||||
valueOfSelected={valueOfSelected}
|
onChange={this._onColorMapSelect}
|
||||||
hasDividers={true}
|
valueOfSelected={valueOfSelected}
|
||||||
data-test-subj={`colorMapSelect_${this.props.styleProperty.getStyleName()}`}
|
hasDividers={true}
|
||||||
/>
|
data-test-subj={`colorMapSelect_${this.props.styleProperty.getStyleName()}`}
|
||||||
</EuiFlexItem>
|
/>
|
||||||
</EuiFlexGroup>
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +174,6 @@ export class ColorMapSelect extends Component {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{this._renderColorMapSelections()}
|
{this._renderColorMapSelections()}
|
||||||
<EuiSpacer size="s" />
|
|
||||||
{this._renderColorStopsInput()}
|
{this._renderColorStopsInput()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,6 +90,7 @@ export function DynamicColorForm({
|
||||||
if (styleProperty.isOrdinal()) {
|
if (styleProperty.isOrdinal()) {
|
||||||
return (
|
return (
|
||||||
<ColorMapSelect
|
<ColorMapSelect
|
||||||
|
isCustomOnly={!field.supportsAutoDomain}
|
||||||
colorMapOptions={COLOR_GRADIENTS}
|
colorMapOptions={COLOR_GRADIENTS}
|
||||||
customOptionLabel={i18n.translate('xpack.maps.style.customColorRampLabel', {
|
customOptionLabel={i18n.translate('xpack.maps.style.customColorRampLabel', {
|
||||||
defaultMessage: 'Custom color ramp',
|
defaultMessage: 'Custom color ramp',
|
||||||
|
@ -108,6 +109,7 @@ export function DynamicColorForm({
|
||||||
} else if (styleProperty.isCategorical()) {
|
} else if (styleProperty.isCategorical()) {
|
||||||
return (
|
return (
|
||||||
<ColorMapSelect
|
<ColorMapSelect
|
||||||
|
isCustomOnly={!field.supportsAutoDomain}
|
||||||
colorMapOptions={COLOR_PALETTES}
|
colorMapOptions={COLOR_PALETTES}
|
||||||
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
customOptionLabel={i18n.translate('xpack.maps.style.customColorPaletteLabel', {
|
||||||
defaultMessage: 'Custom color palette',
|
defaultMessage: 'Custom color palette',
|
||||||
|
|
|
@ -81,9 +81,16 @@ export function FieldSelect({ fields, selectedFieldName, onChange, styleName, ..
|
||||||
|
|
||||||
let selectedOption;
|
let selectedOption;
|
||||||
if (selectedFieldName) {
|
if (selectedFieldName) {
|
||||||
selectedOption = fields.find((field) => {
|
const field = fields.find((field) => {
|
||||||
return field.name === selectedFieldName;
|
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 (
|
return (
|
||||||
|
|
|
@ -46,19 +46,16 @@ export class StyleMapSelect extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderCustomStopsInput() {
|
_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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
{this.props.renderCustomStopsInput(this._onCustomMapChange)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const mapOptionsWithCustom = [
|
const mapOptionsWithCustom = [
|
||||||
{
|
{
|
||||||
value: CUSTOM_MAP,
|
value: CUSTOM_MAP,
|
||||||
|
@ -87,6 +84,15 @@ export class StyleMapSelect extends Component {
|
||||||
hasDividers={true}
|
hasDividers={true}
|
||||||
compressed
|
compressed
|
||||||
/>
|
/>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{this._renderMapSelect()}
|
||||||
{this._renderCustomStopsInput()}
|
{this._renderCustomStopsInput()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -36,17 +36,20 @@ export function DynamicIconForm({
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderIconMapSelect() {
|
function renderIconMapSelect() {
|
||||||
if (!styleOptions.field || !styleOptions.field.name) {
|
const field = styleProperty.getField();
|
||||||
|
if (!field) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconMapSelect
|
<IconMapSelect
|
||||||
{...styleOptions}
|
{...styleOptions}
|
||||||
|
useCustomIconMap={_.get(styleOptions, 'useCustomColorRamp', false)}
|
||||||
styleProperty={styleProperty}
|
styleProperty={styleProperty}
|
||||||
onChange={onIconMapChange}
|
onChange={onIconMapChange}
|
||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
symbolOptions={symbolOptions}
|
symbolOptions={symbolOptions}
|
||||||
|
isCustomOnly={!field.supportsAutoDomain()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import React from 'react';
|
||||||
|
|
||||||
import { StyleMapSelect } from '../style_map_select';
|
import { StyleMapSelect } from '../style_map_select';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { getIconPaletteOptions } from '../../symbol_utils';
|
|
||||||
import { IconStops } from './icon_stops';
|
import { IconStops } from './icon_stops';
|
||||||
|
import { getIconPaletteOptions } from '../../symbol_utils';
|
||||||
|
|
||||||
export function IconMapSelect({
|
export function IconMapSelect({
|
||||||
customIconStops,
|
customIconStops,
|
||||||
|
@ -19,6 +19,7 @@ export function IconMapSelect({
|
||||||
styleProperty,
|
styleProperty,
|
||||||
symbolOptions,
|
symbolOptions,
|
||||||
useCustomIconMap,
|
useCustomIconMap,
|
||||||
|
isCustomOnly,
|
||||||
}) {
|
}) {
|
||||||
function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) {
|
function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) {
|
||||||
onChange({
|
onChange({
|
||||||
|
@ -52,6 +53,7 @@ export function IconMapSelect({
|
||||||
useCustomMap={useCustomIconMap}
|
useCustomMap={useCustomIconMap}
|
||||||
selectedMapId={iconPaletteId}
|
selectedMapId={iconPaletteId}
|
||||||
renderCustomStopsInput={renderCustomIconStopsInput}
|
renderCustomStopsInput={renderCustomIconStopsInput}
|
||||||
|
isCustomOnly={isCustomOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ export class VectorStyleEditor extends Component {
|
||||||
name: field.getName(),
|
name: field.getName(),
|
||||||
origin: field.getOrigin(),
|
origin: field.getOrigin(),
|
||||||
type: await field.getDataType(),
|
type: await field.getDataType(),
|
||||||
|
supportsAutoDomain: field.supportsAutoDomain(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,7 +110,9 @@ export class VectorStyleEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getOrdinalFields() {
|
_getOrdinalFields() {
|
||||||
return [...this.state.dateFields, ...this.state.numberFields];
|
return [...this.state.dateFields, ...this.state.numberFields].filter((field) => {
|
||||||
|
return field.supportsAutoDomain;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleSelectedFeatureChange = (selectedFeature) => {
|
_handleSelectedFeatureChange = (selectedFeature) => {
|
||||||
|
|
|
@ -10,11 +10,10 @@ import { VECTOR_STYLES } from '../../../../../common/constants';
|
||||||
|
|
||||||
export class DynamicOrientationProperty extends DynamicStyleProperty {
|
export class DynamicOrientationProperty extends DynamicStyleProperty {
|
||||||
syncIconRotationWithMb(symbolLayerId, mbMap) {
|
syncIconRotationWithMb(symbolLayerId, mbMap) {
|
||||||
if (this._options.field && this._options.field.name) {
|
if (this._field && this._field.isValid()) {
|
||||||
const targetName = getComputedFieldName(
|
const targetName = this._field.supportsAutoDomain()
|
||||||
VECTOR_STYLES.ICON_ORIENTATION,
|
? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName())
|
||||||
this._options.field.name
|
: this._field.getName();
|
||||||
);
|
|
||||||
// Using property state instead of feature-state because layout properties do not support feature-state
|
// Using property state instead of feature-state because layout properties do not support feature-state
|
||||||
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]);
|
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,7 +10,11 @@ import { getComputedFieldName } from '../style_util';
|
||||||
export class DynamicTextProperty extends DynamicStyleProperty {
|
export class DynamicTextProperty extends DynamicStyleProperty {
|
||||||
syncTextFieldWithMb(mbLayerId, mbMap) {
|
syncTextFieldWithMb(mbLayerId, mbMap) {
|
||||||
if (this._field && this._field.isValid()) {
|
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], '']);
|
mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']);
|
||||||
} else {
|
} else {
|
||||||
mbMap.setLayoutProperty(mbLayerId, 'text-field', null);
|
mbMap.setLayoutProperty(mbLayerId, 'text-field', null);
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface ITooltipProperty {
|
||||||
|
|
||||||
export interface LoadFeatureProps {
|
export interface LoadFeatureProps {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
featureId: number;
|
featureId?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeatureGeometry {
|
export interface FeatureGeometry {
|
||||||
|
|
|
@ -43,16 +43,16 @@ export class LayerPanel extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
this.loadDisplayName();
|
this._loadDisplayName();
|
||||||
this.loadImmutableSourceProperties();
|
this._loadImmutableSourceProperties();
|
||||||
this.loadLeftJoinFields();
|
this._loadLeftJoinFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDisplayName = async () => {
|
_loadDisplayName = async () => {
|
||||||
if (!this.props.selectedLayer) {
|
if (!this.props.selectedLayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ export class LayerPanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadImmutableSourceProperties = async () => {
|
_loadImmutableSourceProperties = async () => {
|
||||||
if (!this.props.selectedLayer) {
|
if (!this.props.selectedLayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ export class LayerPanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async loadLeftJoinFields() {
|
async _loadLeftJoinFields() {
|
||||||
if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
|
if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -97,8 +97,11 @@ export class LayerPanel extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSourceChange = ({ propName, value, newLayerType }) => {
|
_onSourceChange = (...args) => {
|
||||||
this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType);
|
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() {
|
_renderFilterSection() {
|
||||||
|
|
|
@ -31,14 +31,15 @@ export class FeatureProperties extends React.Component {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_loadProperties = () => {
|
_loadProperties = async () => {
|
||||||
this._fetchProperties({
|
this._fetchProperties({
|
||||||
nextFeatureId: this.props.featureId,
|
nextFeatureId: this.props.featureId,
|
||||||
nextLayerId: this.props.layerId,
|
nextLayerId: this.props.layerId,
|
||||||
|
mbProperties: this.props.mbProperties,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_fetchProperties = async ({ nextLayerId, nextFeatureId }) => {
|
_fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => {
|
||||||
if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) {
|
if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) {
|
||||||
// do not reload same feature properties
|
// do not reload same feature properties
|
||||||
return;
|
return;
|
||||||
|
@ -64,6 +65,7 @@ export class FeatureProperties extends React.Component {
|
||||||
properties = await this.props.loadFeatureProperties({
|
properties = await this.props.loadFeatureProperties({
|
||||||
layerId: nextLayerId,
|
layerId: nextLayerId,
|
||||||
featureId: nextFeatureId,
|
featureId: nextFeatureId,
|
||||||
|
mbProperties: mbProperties,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
|
|
|
@ -132,6 +132,7 @@ export class FeaturesTooltip extends React.Component {
|
||||||
<FeatureProperties
|
<FeatureProperties
|
||||||
featureId={this.state.currentFeature.id}
|
featureId={this.state.currentFeature.id}
|
||||||
layerId={this.state.currentFeature.layerId}
|
layerId={this.state.currentFeature.layerId}
|
||||||
|
mbProperties={this.state.currentFeature.mbProperties}
|
||||||
loadFeatureProperties={this.props.loadFeatureProperties}
|
loadFeatureProperties={this.props.loadFeatureProperties}
|
||||||
showFilterButtons={!!this.props.addFilters && this.props.isLocked}
|
showFilterButtons={!!this.props.addFilters && this.props.isLocked}
|
||||||
onCloseTooltip={this.props.closeTooltip}
|
onCloseTooltip={this.props.closeTooltip}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class TooltipControl extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getIdsForFeatures(mbFeatures) {
|
_getTooltipFeatures(mbFeatures) {
|
||||||
const uniqueFeatures = [];
|
const uniqueFeatures = [];
|
||||||
//there may be duplicates in the results from mapbox
|
//there may be duplicates in the results from mapbox
|
||||||
//this is because mapbox returns the results per tile
|
//this is because mapbox returns the results per tile
|
||||||
|
@ -72,9 +72,18 @@ export class TooltipControl extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!match) {
|
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({
|
uniqueFeatures.push({
|
||||||
id: featureId,
|
id: featureId,
|
||||||
layerId: layerId,
|
layerId: layerId,
|
||||||
|
mbProperties,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +98,7 @@ export class TooltipControl extends React.Component {
|
||||||
|
|
||||||
this._updateHoverTooltipState.cancel(); //ignore any possible moves
|
this._updateHoverTooltipState.cancel(); //ignore any possible moves
|
||||||
|
|
||||||
const mbFeatures = this._getFeaturesUnderPointer(e.point);
|
const mbFeatures = this._getMbFeaturesUnderPointer(e.point);
|
||||||
if (!mbFeatures.length) {
|
if (!mbFeatures.length) {
|
||||||
// No features at click location so there is no tooltip to open
|
// No features at click location so there is no tooltip to open
|
||||||
return;
|
return;
|
||||||
|
@ -98,9 +107,9 @@ export class TooltipControl extends React.Component {
|
||||||
const targetMbFeataure = mbFeatures[0];
|
const targetMbFeataure = mbFeatures[0];
|
||||||
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure);
|
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure);
|
||||||
|
|
||||||
const features = this._getIdsForFeatures(mbFeatures);
|
const features = this._getTooltipFeatures(mbFeatures);
|
||||||
this.props.openOnClickTooltip({
|
this.props.openOnClickTooltip({
|
||||||
features: features,
|
features,
|
||||||
location: popupAnchorLocation,
|
location: popupAnchorLocation,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -111,7 +120,7 @@ export class TooltipControl extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mbFeatures = this._getFeaturesUnderPointer(e.point);
|
const mbFeatures = this._getMbFeaturesUnderPointer(e.point);
|
||||||
if (!mbFeatures.length) {
|
if (!mbFeatures.length) {
|
||||||
this.props.closeOnHoverTooltip();
|
this.props.closeOnHoverTooltip();
|
||||||
return;
|
return;
|
||||||
|
@ -127,7 +136,7 @@ export class TooltipControl extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature);
|
const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature);
|
||||||
const features = this._getIdsForFeatures(mbFeatures);
|
const features = this._getTooltipFeatures(mbFeatures);
|
||||||
this.props.openOnHoverTooltip({
|
this.props.openOnHoverTooltip({
|
||||||
features: features,
|
features: features,
|
||||||
location: popupAnchorLocation,
|
location: popupAnchorLocation,
|
||||||
|
@ -149,7 +158,7 @@ export class TooltipControl extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getFeaturesUnderPointer(mbLngLatPoint) {
|
_getMbFeaturesUnderPointer(mbLngLatPoint) {
|
||||||
if (!this.props.mbMap) {
|
if (!this.props.mbMap) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,7 +236,7 @@ describe('TooltipControl', () => {
|
||||||
|
|
||||||
sinon.assert.notCalled(closeOnClickTooltipStub);
|
sinon.assert.notCalled(closeOnClickTooltipStub);
|
||||||
sinon.assert.calledWith(openOnClickTooltipStub, {
|
sinon.assert.calledWith(openOnClickTooltipStub, {
|
||||||
features: [{ id: 1, layerId: 'tfi3f' }],
|
features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }],
|
||||||
location: [100, 30],
|
location: [100, 30],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
// Mapbox feature geometry is from vector tile and is not the same as the original geometry.
|
||||||
_loadFeatureGeometry = ({ layerId, featureId }) => {
|
_loadFeatureGeometry = ({ layerId, featureId }) => {
|
||||||
const tooltipLayer = this._findLayerById(layerId);
|
const tooltipLayer = this._findLayerById(layerId);
|
||||||
if (!tooltipLayer) {
|
if (!tooltipLayer || typeof featureId === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,22 +70,24 @@ export class TooltipPopover extends Component {
|
||||||
return targetFeature.geometry;
|
return targetFeature.geometry;
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadFeatureProperties = async ({ layerId, featureId }) => {
|
_loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => {
|
||||||
const tooltipLayer = this._findLayerById(layerId);
|
const tooltipLayer = this._findLayerById(layerId);
|
||||||
if (!tooltipLayer) {
|
if (!tooltipLayer) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetFeature = tooltipLayer.getFeatureById(featureId);
|
let targetFeature;
|
||||||
if (!targetFeature) {
|
if (typeof featureId !== 'undefined') {
|
||||||
return [];
|
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 }) => {
|
_loadPreIndexedShape = async ({ layerId, featureId }) => {
|
||||||
const tooltipLayer = this._findLayerById(layerId);
|
const tooltipLayer = this._findLayerById(layerId);
|
||||||
if (!tooltipLayer) {
|
if (!tooltipLayer || typeof featureId === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MapToolTipComponent } from './map_tool_tip';
|
import { MapToolTipComponent } from './map_tool_tip';
|
||||||
import { MapFeature } from '../types';
|
import { TooltipFeature } from '../../../../../../maps/common/descriptor_types';
|
||||||
|
|
||||||
describe('MapToolTip', () => {
|
describe('MapToolTip', () => {
|
||||||
test('placeholder component renders correctly against snapshot', () => {
|
test('placeholder component renders correctly against snapshot', () => {
|
||||||
|
@ -18,10 +18,11 @@ describe('MapToolTip', () => {
|
||||||
test('full component renders correctly against snapshot', () => {
|
test('full component renders correctly against snapshot', () => {
|
||||||
const addFilters = jest.fn();
|
const addFilters = jest.fn();
|
||||||
const closeTooltip = jest.fn();
|
const closeTooltip = jest.fn();
|
||||||
const features: MapFeature[] = [
|
const features: TooltipFeature[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
layerId: 'layerId',
|
layerId: 'layerId',
|
||||||
|
mbProperties: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const getLayerName = jest.fn();
|
const getLayerName = jest.fn();
|
||||||
|
|
|
@ -36,11 +36,6 @@ export type SetQuery = (params: {
|
||||||
refetch: inputsModel.Refetch;
|
refetch: inputsModel.Refetch;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
export interface MapFeature {
|
|
||||||
id: number;
|
|
||||||
layerId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FeatureGeometry {
|
export interface FeatureGeometry {
|
||||||
coordinates: [number];
|
coordinates: [number];
|
||||||
type: string;
|
type: string;
|
||||||
|
|
|
@ -9214,9 +9214,6 @@
|
||||||
"xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです",
|
"xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです",
|
||||||
"xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス",
|
"xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス",
|
||||||
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報",
|
"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.MVTSingleLayerVectorSource.sourceTitle": "ベトルタイルレイヤー",
|
||||||
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル",
|
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル",
|
||||||
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名",
|
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名",
|
||||||
|
|
|
@ -9218,9 +9218,6 @@
|
||||||
"xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴",
|
"xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴",
|
||||||
"xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务",
|
"xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务",
|
||||||
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置",
|
"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.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴图层",
|
||||||
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别",
|
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别",
|
||||||
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称",
|
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称",
|
||||||
|
|
|
@ -76,7 +76,11 @@ export default function ({ getPageObjects, getService }) {
|
||||||
const { lat, lon, zoom } = await PageObjects.maps.getView();
|
const { lat, lon, zoom } = await PageObjects.maps.getView();
|
||||||
expect(Math.round(lat)).to.equal(41);
|
expect(Math.round(lat)).to.equal(41);
|
||||||
expect(Math.round(lon)).to.equal(-102);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue