mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Vega] Use mapbox instead of leaflet (#88605)
* [WIP][Vega] Use mapbox instead of leaflet #78395 add MapServiceSettings class some work add tms_raster_layer add LayerParameters type clenup view.ts some cleeanup fix grammar some refactoring and add attribution control Some refactoring Add some validation for zoom settings and destroy handler Some refactoring some work fix bundle size Move getZoomSettings to the separate file update licence some work move logger to createViewConfig add throttling for updating vega layer * move EMSClient to a separate bundle * [unit testing] add tests for validation_helper.ts * [Bundle optimization] lazy loading of '@elastic/ems-client' only if user open map layer * [Map] fix cursor: crosshair -> auto * [unit testing] add tests for tms_raster_layer.test * [unit testing] add tests for vega_layer.ts * VSI related code was moved into a separate file / unit tests were added * Add functional test for vega map * [unit testing] add tests for map_service_setting.ts * Add unload in function test and delete some unneeded code from test * road_map -> road_map_desaturated * [unit testing] add more tests for map_service_settings.test.ts * Add unit tests for view.ts * Fix some remarks * Fix unit tests * remove tms_tile_layers enum * [unit testing] fix map_service_settings.test.ts * Fix unit test for view.ts * Fix some comments * Fix type check * Fix CI Co-authored-by: Alexey Antonov <alexwizp@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
da1a4e947a
commit
e8e8f78b39
33 changed files with 1265 additions and 249 deletions
|
@ -1,7 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"<div class=\\"vgaVis__view leaflet-container leaflet-grab leaflet-touch-drag\\" style=\\"height: 100%; position: relative;\\" tabindex=\\"0\\"><div class=\\"leaflet-pane leaflet-map-pane\\" style=\\"left: 0px; top: 0px;\\"><div class=\\"leaflet-pane leaflet-tile-pane\\"></div><div class=\\"leaflet-pane leaflet-shadow-pane\\"></div><div class=\\"leaflet-pane leaflet-overlay-pane\\"><div class=\\"leaflet-vega-container\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\" style=\\"left: 0px; top: 0px; cursor: default;\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-rect role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"rect mark container\\"><path d=\\"M0,0h0v0h0Z\\" fill=\\"#0f0\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div></div><div class=\\"leaflet-pane leaflet-marker-pane\\"></div><div class=\\"leaflet-pane leaflet-tooltip-pane\\"></div><div class=\\"leaflet-pane leaflet-popup-pane\\"></div></div><div class=\\"leaflet-control-container\\"><div class=\\"leaflet-top leaflet-left\\"><div class=\\"leaflet-control-zoom leaflet-bar leaflet-control\\"><a class=\\"leaflet-control-zoom-in\\" href=\\"#\\" title=\\"Zoom in\\" role=\\"button\\" aria-label=\\"Zoom in\\">+</a><a class=\\"leaflet-control-zoom-out\\" href=\\"#\\" title=\\"Zoom out\\" role=\\"button\\" aria-label=\\"Zoom out\\">−</a></div></div><div class=\\"leaflet-top leaflet-right\\"></div><div class=\\"leaflet-bottom leaflet-left\\"></div><div class=\\"leaflet-bottom leaflet-right\\"><div class=\\"leaflet-control-attribution leaflet-control\\"></div></div></div></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`;
|
||||
|
||||
exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"<div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"512\\" height=\\"512\\" viewBox=\\"0 0 512 512\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(0,0)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h512v512h-512Z\\"></path><g><g class=\\"mark-group role-scope\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,512C18.962962962962962,512,37.925925925925924,512,56.888888888888886,512C75.85185185185185,512,94.81481481481481,512,113.77777777777777,512C132.74074074074073,512,151.7037037037037,512,170.66666666666666,512C189.62962962962962,512,208.59259259259258,512,227.55555555555554,512C246.5185185185185,512,265.48148148148147,512,284.44444444444446,512C303.4074074074074,512,322.3703703703704,512,341.3333333333333,512C360.29629629629625,512,379.25925925925924,512,398.2222222222222,512C417.18518518518516,512,436.1481481481481,512,455.1111111111111,512C474.0740740740741,512,493.037037037037,512,512,512L512,355.2C493.037037037037,324.79999999999995,474.0740740740741,294.4,455.1111111111111,294.4C436.1481481481481,294.4,417.18518518518516,457.6,398.2222222222222,457.6C379.25925925925924,457.6,360.29629629629625,233.60000000000002,341.3333333333333,233.60000000000002C322.3703703703704,233.60000000000002,303.4074074074074,435.2,284.44444444444446,435.2C265.48148148148147,435.2,246.5185185185185,345.6,227.55555555555554,345.6C208.59259259259258,345.6,189.62962962962962,451.2,170.66666666666666,451.2C151.7037037037037,451.2,132.74074074074073,252.8,113.77777777777777,252.8C94.81481481481481,252.8,75.85185185185185,346.1333333333333,56.888888888888886,374.4C37.925925925925924,402.66666666666663,18.962962962962962,412.5333333333333,0,422.4Z\\" fill=\\"#54B399\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0,0h0v0h0Z\\"></path><g><g class=\\"mark-area role-mark\\" role=\\"graphics-symbol\\" aria-roledescription=\\"area mark container\\"><path d=\\"M0,422.4C18.962962962962962,412.5333333333333,37.925925925925924,402.66666666666663,56.888888888888886,374.4C75.85185185185185,346.1333333333333,94.81481481481481,252.8,113.77777777777777,252.8C132.74074074074073,252.8,151.7037037037037,451.2,170.66666666666666,451.2C189.62962962962962,451.2,208.59259259259258,345.6,227.55555555555554,345.6C246.5185185185185,345.6,265.48148148148147,435.2,284.44444444444446,435.2C303.4074074074074,435.2,322.3703703703704,233.60000000000002,341.3333333333333,233.60000000000002C360.29629629629625,233.60000000000002,379.25925925925924,457.6,398.2222222222222,457.6C417.18518518518516,457.6,436.1481481481481,294.4,455.1111111111111,294.4C474.0740740740741,294.4,493.037037037037,324.79999999999995,512,355.2L512,307.2C493.037037037037,275.2,474.0740740740741,243.2,455.1111111111111,243.2C436.1481481481481,243.2,417.18518518518516,371.2,398.2222222222222,371.2C379.25925925925924,371.2,360.29629629629625,22.399999999999977,341.3333333333333,22.399999999999977C322.3703703703704,22.399999999999977,303.4074074074074,278.4,284.44444444444446,278.4C265.48148148148147,278.4,246.5185185185185,204.8,227.55555555555554,192C208.59259259259258,179.20000000000002,189.62962962962962,185.6,170.66666666666666,172.8C151.7037037037037,160.00000000000003,132.74074074074073,83.19999999999999,113.77777777777777,83.19999999999999C94.81481481481481,83.19999999999999,75.85185185185185,83.19999999999999,56.888888888888886,83.19999999999999C37.925925925925924,83.19999999999999,18.962962962962962,164.79999999999998,0,246.39999999999998Z\\" fill=\\"#6092C0\\" fill-opacity=\\"1\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`;
|
||||
|
||||
exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"<ul class=\\"vgaVis__messages\\"><li class=\\"vgaVis__message vgaVis__message--warn\\"><pre class=\\"vgaVis__messageCode\\">\\"width\\" and \\"height\\" params are ignored because \\"autosize\\" is enabled. Set \\"autosize\\": \\"none\\" to disable</pre></li></ul><div class=\\"vgaVis__view\\" style=\\"height: 100%; cursor: default;\\" role=\\"graphics-document\\" aria-roledescription=\\"visualization\\" aria-label=\\"Vega visualization\\"><svg xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\" version=\\"1.1\\" class=\\"marks\\" width=\\"0\\" height=\\"0\\" viewBox=\\"0 0 0 0\\" style=\\"background-color: transparent;\\"><g fill=\\"none\\" stroke-miterlimit=\\"10\\" transform=\\"translate(7,7)\\"><g class=\\"mark-group role-frame root\\" role=\\"graphics-object\\" aria-roledescription=\\"group mark container\\"><g transform=\\"translate(0,0)\\"><path class=\\"background\\" aria-hidden=\\"true\\" d=\\"M0.5,0.5h0v0h0Z\\" fill=\\"transparent\\" stroke=\\"#ddd\\"></path><g><g class=\\"mark-line role-mark marks\\" role=\\"graphics-object\\" aria-roledescription=\\"line mark container\\"><path aria-label=\\"key: Dec 11, 2017; doc_count: 0\\" role=\\"graphics-symbol\\" aria-roledescription=\\"line mark\\" d=\\"M0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0L0,0\\" stroke=\\"#54B399\\" stroke-width=\\"2\\"></path></g></g><path class=\\"foreground\\" aria-hidden=\\"true\\" d=\\"\\" display=\\"none\\"></path></g></g></g></svg></div><div class=\\"vgaVis__controls vgaVis__controls--column\\"></div>"`;
|
||||
|
|
|
@ -17,17 +17,18 @@ import {
|
|||
setData,
|
||||
setInjectedVars,
|
||||
setUISettings,
|
||||
setMapsLegacyConfig,
|
||||
setInjectedMetadata,
|
||||
setMapServiceSettings,
|
||||
} from './services';
|
||||
|
||||
import { createVegaFn } from './vega_fn';
|
||||
import { createVegaTypeDefinition } from './vega_type';
|
||||
import { IServiceSettings } from '../../maps_legacy/public';
|
||||
import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public';
|
||||
import { ConfigSchema } from '../config';
|
||||
|
||||
import { getVegaInspectorView } from './vega_inspector';
|
||||
import { getVegaVisRenderer } from './vega_vis_renderer';
|
||||
import { MapServiceSettings } from './vega_view/vega_map_view/map_service_settings';
|
||||
|
||||
/** @internal */
|
||||
export interface VegaVisualizationDependencies {
|
||||
|
@ -44,7 +45,7 @@ export interface VegaPluginSetupDependencies {
|
|||
visualizations: VisualizationsSetup;
|
||||
inspector: InspectorSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
mapsLegacy: any;
|
||||
mapsLegacy: MapsLegacyPluginSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -68,8 +69,12 @@ export class VegaPlugin implements Plugin<Promise<void>, void> {
|
|||
enableExternalUrls: this.initializerContext.config.get().enableExternalUrls,
|
||||
emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true),
|
||||
});
|
||||
|
||||
setUISettings(core.uiSettings);
|
||||
setMapsLegacyConfig(mapsLegacy.config);
|
||||
|
||||
setMapServiceSettings(
|
||||
new MapServiceSettings(mapsLegacy.config, this.initializerContext.env.packageInfo.version)
|
||||
);
|
||||
|
||||
const visualizationDependencies: Readonly<VegaVisualizationDependencies> = {
|
||||
core,
|
||||
|
|
|
@ -10,7 +10,7 @@ import { CoreStart, NotificationsStart, IUiSettingsClient } from 'src/core/publi
|
|||
|
||||
import { DataPublicPluginStart } from '../../data/public';
|
||||
import { createGetterSetter } from '../../kibana_utils/public';
|
||||
import { MapsLegacyConfig } from '../../maps_legacy/config';
|
||||
import { MapServiceSettings } from './vega_view/vega_map_view/map_service_settings';
|
||||
|
||||
export const [getData, setData] = createGetterSetter<DataPublicPluginStart>('Data');
|
||||
|
||||
|
@ -24,13 +24,14 @@ export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter<
|
|||
CoreStart['injectedMetadata']
|
||||
>('InjectedMetadata');
|
||||
|
||||
export const [
|
||||
getMapServiceSettings,
|
||||
setMapServiceSettings,
|
||||
] = createGetterSetter<MapServiceSettings>('MapServiceSettings');
|
||||
|
||||
export const [getInjectedVars, setInjectedVars] = createGetterSetter<{
|
||||
enableExternalUrls: boolean;
|
||||
emsTileLayerId: unknown;
|
||||
}>('InjectedVars');
|
||||
|
||||
export const [getMapsLegacyConfig, setMapsLegacyConfig] = createGetterSetter<MapsLegacyConfig>(
|
||||
'MapsLegacyConfig'
|
||||
);
|
||||
|
||||
export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||
"config": {
|
||||
"kibana": { "renderer": "svg", "type": "map", "mapStyle": false}
|
||||
"kibana": { "type": "map", "mapStyle": "default", "latitude": 25, "longitude": -70, "zoom": 3}
|
||||
},
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
|
|
|
@ -18,12 +18,21 @@ interface VegaViewParams {
|
|||
serviceSettings: IServiceSettings;
|
||||
filterManager: DataPublicPluginStart['query']['filterManager'];
|
||||
timefilter: DataPublicPluginStart['query']['timefilter']['timefilter'];
|
||||
// findIndex: (index: string) => Promise<...>;
|
||||
}
|
||||
|
||||
export class VegaBaseView {
|
||||
constructor(params: VegaViewParams);
|
||||
init(): Promise<void>;
|
||||
onError(error: any): void;
|
||||
onWarn(error: any): void;
|
||||
setView(map: any): void;
|
||||
setDebugValues(view: any, spec: any, vlspec: any): void;
|
||||
_addDestroyHandler(handler: Function): void;
|
||||
|
||||
destroy(): Promise<void>;
|
||||
|
||||
_$container: any;
|
||||
_parser: any;
|
||||
_vegaViewConfig: any;
|
||||
_serviceSettings: any;
|
||||
}
|
||||
|
|
|
@ -160,8 +160,6 @@ export class VegaBaseView {
|
|||
|
||||
createViewConfig() {
|
||||
const config = {
|
||||
// eslint-disable-next-line import/namespace
|
||||
logLevel: vega.Warn, // note: eslint has a false positive here
|
||||
renderer: this._parser.renderer,
|
||||
};
|
||||
|
||||
|
@ -189,6 +187,13 @@ export class VegaBaseView {
|
|||
};
|
||||
config.loader = loader;
|
||||
|
||||
const logger = vega.logger(vega.Warn);
|
||||
|
||||
logger.warn = this.onWarn.bind(this);
|
||||
logger.error = this.onError.bind(this);
|
||||
|
||||
config.logger = logger;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +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
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KibanaMapLayer } from '../../../maps_legacy/public';
|
||||
|
||||
export class VegaMapLayer extends KibanaMapLayer {
|
||||
constructor(spec, options, leaflet) {
|
||||
super();
|
||||
|
||||
// Used by super.getAttributions()
|
||||
this._attribution = options.attribution;
|
||||
delete options.attribution;
|
||||
this._leafletLayer = leaflet.vega(spec, options);
|
||||
}
|
||||
|
||||
getVegaView() {
|
||||
return this._leafletLayer._view;
|
||||
}
|
||||
|
||||
getVegaSpec() {
|
||||
return this._leafletLayer._spec;
|
||||
}
|
||||
}
|
|
@ -1,168 +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
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { vega } from '../lib/vega';
|
||||
import { VegaBaseView } from './vega_base_view';
|
||||
import { VegaMapLayer } from './vega_map_layer';
|
||||
import { getMapsLegacyConfig, getUISettings } from '../services';
|
||||
import { lazyLoadMapsLegacyModules, TMS_IN_YML_ID } from '../../../maps_legacy/public';
|
||||
|
||||
const isUserConfiguredTmsLayer = ({ tilemap }) => Boolean(tilemap.url);
|
||||
|
||||
export class VegaMapView extends VegaBaseView {
|
||||
constructor(opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
async getMapStyleOptions() {
|
||||
const isDarkMode = getUISettings().get('theme:darkMode');
|
||||
const mapsLegacyConfig = getMapsLegacyConfig();
|
||||
const tmsServices = await this._serviceSettings.getTMSServices();
|
||||
const mapConfig = this._parser.mapConfig;
|
||||
|
||||
let mapStyle;
|
||||
|
||||
if (mapConfig.mapStyle !== 'default') {
|
||||
mapStyle = mapConfig.mapStyle;
|
||||
} else {
|
||||
if (isUserConfiguredTmsLayer(mapsLegacyConfig)) {
|
||||
mapStyle = TMS_IN_YML_ID;
|
||||
} else {
|
||||
mapStyle = mapsLegacyConfig.emsTileLayerId.bright;
|
||||
}
|
||||
}
|
||||
|
||||
const mapOptions = tmsServices.find((s) => s.id === mapStyle);
|
||||
|
||||
if (!mapOptions) {
|
||||
this.onWarn(
|
||||
i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', {
|
||||
defaultMessage: '{mapStyleParam} was not found',
|
||||
values: { mapStyleParam: `"mapStyle":${mapStyle}` },
|
||||
})
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...mapOptions,
|
||||
...(await this._serviceSettings.getAttributesForTMSLayer(mapOptions, true, isDarkMode)),
|
||||
};
|
||||
}
|
||||
|
||||
async _initViewCustomizations() {
|
||||
const mapConfig = this._parser.mapConfig;
|
||||
let baseMapOpts;
|
||||
let limitMinZ = 0;
|
||||
let limitMaxZ = 25;
|
||||
|
||||
// In some cases, Vega may be initialized twice, e.g. after awaiting...
|
||||
if (!this._$container) return;
|
||||
|
||||
if (mapConfig.mapStyle !== false) {
|
||||
baseMapOpts = await this.getMapStyleOptions();
|
||||
|
||||
if (baseMapOpts) {
|
||||
limitMinZ = baseMapOpts.minZoom;
|
||||
limitMaxZ = baseMapOpts.maxZoom;
|
||||
}
|
||||
}
|
||||
|
||||
const validate = (name, value, dflt, min, max) => {
|
||||
if (value === undefined) {
|
||||
value = dflt;
|
||||
} else if (value < min) {
|
||||
this.onWarn(
|
||||
i18n.translate('visTypeVega.mapView.resettingPropertyToMinValueWarningMessage', {
|
||||
defaultMessage: 'Resetting {name} to {min}',
|
||||
values: { name: `"${name}"`, min },
|
||||
})
|
||||
);
|
||||
value = min;
|
||||
} else if (value > max) {
|
||||
this.onWarn(
|
||||
i18n.translate('visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage', {
|
||||
defaultMessage: 'Resetting {name} to {max}',
|
||||
values: { name: `"${name}"`, max },
|
||||
})
|
||||
);
|
||||
value = max;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
let minZoom = validate('minZoom', mapConfig.minZoom, limitMinZ, limitMinZ, limitMaxZ);
|
||||
let maxZoom = validate('maxZoom', mapConfig.maxZoom, limitMaxZ, limitMinZ, limitMaxZ);
|
||||
if (minZoom > maxZoom) {
|
||||
this.onWarn(
|
||||
i18n.translate('visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage', {
|
||||
defaultMessage: '{minZoomPropertyName} and {maxZoomPropertyName} have been swapped',
|
||||
values: {
|
||||
minZoomPropertyName: '"minZoom"',
|
||||
maxZoomPropertyName: '"maxZoom"',
|
||||
},
|
||||
})
|
||||
);
|
||||
[minZoom, maxZoom] = [maxZoom, minZoom];
|
||||
}
|
||||
const zoom = validate('zoom', mapConfig.zoom, 2, minZoom, maxZoom);
|
||||
|
||||
// let maxBounds = null;
|
||||
// if (mapConfig.maxBounds) {
|
||||
// const b = mapConfig.maxBounds;
|
||||
// eslint-disable-next-line no-undef
|
||||
// maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2]));
|
||||
// }
|
||||
|
||||
const modules = await lazyLoadMapsLegacyModules();
|
||||
|
||||
this._kibanaMap = new modules.KibanaMap(this._$container.get(0), {
|
||||
zoom,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
center: [mapConfig.latitude, mapConfig.longitude],
|
||||
zoomControl: mapConfig.zoomControl,
|
||||
scrollWheelZoom: mapConfig.scrollWheelZoom,
|
||||
});
|
||||
|
||||
if (baseMapOpts) {
|
||||
this._kibanaMap.setBaseLayer({
|
||||
baseLayerType: 'tms',
|
||||
options: baseMapOpts,
|
||||
});
|
||||
}
|
||||
|
||||
const vegaMapLayer = new VegaMapLayer(
|
||||
this._parser.spec,
|
||||
{
|
||||
vega,
|
||||
bindingsContainer: this._$controls.get(0),
|
||||
delayRepaint: mapConfig.delayRepaint,
|
||||
viewConfig: this._vegaViewConfig,
|
||||
onWarning: this.onWarn.bind(this),
|
||||
onError: this.onError.bind(this),
|
||||
},
|
||||
modules.L
|
||||
);
|
||||
|
||||
this._kibanaMap.addLayer(vegaMapLayer);
|
||||
|
||||
this._addDestroyHandler(() => {
|
||||
this._kibanaMap.removeLayer(vegaMapLayer);
|
||||
if (baseMapOpts) {
|
||||
this._kibanaMap.setBaseLayer(null);
|
||||
}
|
||||
this._kibanaMap.destroy();
|
||||
});
|
||||
|
||||
const vegaView = vegaMapLayer.getVegaView();
|
||||
await this.setView(vegaView);
|
||||
this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { TMS_IN_YML_ID } from '../../../../maps_legacy/public';
|
||||
|
||||
export const vegaLayerId = 'vega';
|
||||
export const userConfiguredLayerId = TMS_IN_YML_ID;
|
||||
export const defaultMapConfig = {
|
||||
maxZoom: 20,
|
||||
minZoom: 0,
|
||||
tileSize: 256,
|
||||
};
|
||||
|
||||
export const defaultMabBoxStyle = {
|
||||
/**
|
||||
* according to the MapBox documentation that value should be '8'
|
||||
* @see (https://docs.mapbox.com/mapbox-gl-js/style-spec/root/#version)
|
||||
*/
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
};
|
||||
|
||||
export const defaultProjection = {
|
||||
name: 'projection',
|
||||
type: 'mercator',
|
||||
scale: { signal: '512*pow(2,zoom)/2/PI' },
|
||||
rotate: [{ signal: '-longitude' }, 0, 0],
|
||||
center: [0, { signal: 'latitude' }],
|
||||
translate: [{ signal: 'width/2' }, { signal: 'height/2' }],
|
||||
fit: false,
|
||||
};
|
|
@ -6,6 +6,5 @@
|
|||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { VegaBaseView } from './vega_base_view';
|
||||
|
||||
export class VegaMapView extends VegaBaseView {}
|
||||
export { initTmsRasterLayer } from './tms_raster_layer';
|
||||
export { initVegaLayer } from './vega_layer';
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { initTmsRasterLayer } from './tms_raster_layer';
|
||||
|
||||
type InitTmsRasterLayerParams = Parameters<typeof initTmsRasterLayer>[0];
|
||||
|
||||
type IdType = InitTmsRasterLayerParams['id'];
|
||||
type MapType = InitTmsRasterLayerParams['map'];
|
||||
type ContextType = InitTmsRasterLayerParams['context'];
|
||||
|
||||
describe('vega_map_view/tms_raster_layer', () => {
|
||||
let id: IdType;
|
||||
let map: MapType;
|
||||
let context: ContextType;
|
||||
|
||||
beforeEach(() => {
|
||||
id = 'foo_tms_layer_id';
|
||||
map = ({
|
||||
addSource: jest.fn(),
|
||||
addLayer: jest.fn(),
|
||||
} as unknown) as MapType;
|
||||
context = {
|
||||
tiles: ['http://some.tile.com/map/{z}/{x}/{y}.jpg'],
|
||||
maxZoom: 10,
|
||||
minZoom: 2,
|
||||
tileSize: 512,
|
||||
};
|
||||
});
|
||||
|
||||
test('should register a new layer', () => {
|
||||
initTmsRasterLayer({ id, map, context });
|
||||
|
||||
expect(map.addLayer).toHaveBeenCalledWith({
|
||||
id: 'foo_tms_layer_id',
|
||||
maxzoom: 10,
|
||||
minzoom: 2,
|
||||
source: 'foo_tms_layer_id',
|
||||
type: 'raster',
|
||||
});
|
||||
|
||||
expect(map.addSource).toHaveBeenCalledWith('foo_tms_layer_id', {
|
||||
scheme: 'xyz',
|
||||
tileSize: 512,
|
||||
tiles: ['http://some.tile.com/map/{z}/{x}/{y}.jpg'],
|
||||
type: 'raster',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { LayerParameters } from './types';
|
||||
|
||||
interface TMSRasterLayerContext {
|
||||
tiles: string[];
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
tileSize: number;
|
||||
}
|
||||
|
||||
export const initTmsRasterLayer = ({
|
||||
id,
|
||||
map,
|
||||
context: { tiles, maxZoom, minZoom, tileSize },
|
||||
}: LayerParameters<TMSRasterLayerContext>) => {
|
||||
map.addSource(id, {
|
||||
type: 'raster',
|
||||
tiles,
|
||||
tileSize,
|
||||
scheme: 'xyz',
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id,
|
||||
type: 'raster',
|
||||
source: id,
|
||||
maxzoom: maxZoom,
|
||||
minzoom: minZoom,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Map } from 'mapbox-gl';
|
||||
|
||||
export interface LayerParameters<TContext extends Record<string, any> = {}> {
|
||||
id: string;
|
||||
map: Map;
|
||||
context: TContext;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { initVegaLayer } from './vega_layer';
|
||||
|
||||
type InitVegaLayerParams = Parameters<typeof initVegaLayer>[0];
|
||||
|
||||
type IdType = InitVegaLayerParams['id'];
|
||||
type MapType = InitVegaLayerParams['map'];
|
||||
type ContextType = InitVegaLayerParams['context'];
|
||||
|
||||
describe('vega_map_view/tms_raster_layer', () => {
|
||||
let id: IdType;
|
||||
let map: MapType;
|
||||
let context: ContextType;
|
||||
|
||||
beforeEach(() => {
|
||||
id = 'foo_vega_layer_id';
|
||||
map = ({
|
||||
getCanvasContainer: () => document.createElement('div'),
|
||||
getCanvas: () => ({
|
||||
style: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
}),
|
||||
addLayer: jest.fn(),
|
||||
} as unknown) as MapType;
|
||||
context = {
|
||||
vegaView: {
|
||||
initialize: jest.fn(),
|
||||
},
|
||||
updateVegaView: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
test('should register a new custom layer', () => {
|
||||
initVegaLayer({ id, map, context });
|
||||
|
||||
const calledWith = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0];
|
||||
expect(calledWith).toHaveProperty('id', 'foo_vega_layer_id');
|
||||
expect(calledWith).toHaveProperty('type', 'custom');
|
||||
});
|
||||
|
||||
test('should initialize vega container on "onAdd" hook', () => {
|
||||
initVegaLayer({ id, map, context });
|
||||
const { onAdd } = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0];
|
||||
|
||||
onAdd(map);
|
||||
expect(context.vegaView.initialize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should update vega view on "render" hook', () => {
|
||||
initVegaLayer({ id, map, context });
|
||||
const { render } = (map.addLayer as jest.MockedFunction<any>).mock.calls[0][0];
|
||||
|
||||
expect(context.updateVegaView).not.toHaveBeenCalled();
|
||||
render();
|
||||
expect(context.updateVegaView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Map, CustomLayerInterface } from 'mapbox-gl';
|
||||
import type { LayerParameters } from './types';
|
||||
|
||||
// @ts-ignore
|
||||
import { vega } from '../../lib/vega';
|
||||
|
||||
export interface VegaLayerContext {
|
||||
vegaView: vega.View;
|
||||
updateVegaView: (map: Map, view: vega.View) => void;
|
||||
}
|
||||
|
||||
export function initVegaLayer({
|
||||
id,
|
||||
map: mapInstance,
|
||||
context: { vegaView, updateVegaView },
|
||||
}: LayerParameters<VegaLayerContext>) {
|
||||
const vegaLayer: CustomLayerInterface = {
|
||||
id,
|
||||
type: 'custom',
|
||||
onAdd(map: Map) {
|
||||
const mapContainer = map.getCanvasContainer();
|
||||
const mapCanvas = map.getCanvas();
|
||||
const vegaContainer = document.createElement('div');
|
||||
|
||||
vegaContainer.style.position = 'absolute';
|
||||
vegaContainer.style.top = '0px';
|
||||
vegaContainer.style.width = mapCanvas.style.width;
|
||||
vegaContainer.style.height = mapCanvas.style.height;
|
||||
|
||||
mapContainer.appendChild(vegaContainer);
|
||||
vegaView.initialize(vegaContainer);
|
||||
},
|
||||
render() {
|
||||
updateVegaView(mapInstance, vegaView);
|
||||
},
|
||||
};
|
||||
|
||||
mapInstance.addLayer(vegaLayer);
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { uiSettingsServiceMock } from 'src/core/public/mocks';
|
||||
|
||||
import { MapServiceSettings, getAttributionsForTmsService } from './map_service_settings';
|
||||
import { MapsLegacyConfig } from '../../../../maps_legacy/config';
|
||||
import { EMSClient, TMSService } from '@elastic/ems-client';
|
||||
import { setUISettings } from '../../services';
|
||||
|
||||
const getPrivateField = <T>(mapServiceSettings: MapServiceSettings, privateField: string) =>
|
||||
get(mapServiceSettings, privateField) as T;
|
||||
|
||||
describe('vega_map_view/map_service_settings', () => {
|
||||
describe('MapServiceSettings', () => {
|
||||
const appVersion = '99';
|
||||
let config: MapsLegacyConfig;
|
||||
let getUiSettingsMockedValue: any;
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
emsTileLayerId: {
|
||||
desaturated: 'road_map_desaturated',
|
||||
dark: 'dark_map',
|
||||
},
|
||||
} as MapsLegacyConfig;
|
||||
setUISettings({
|
||||
...uiSettingsServiceMock.createSetupContract(),
|
||||
get: () => getUiSettingsMockedValue,
|
||||
});
|
||||
});
|
||||
|
||||
test('should be able to create instance of MapServiceSettings', () => {
|
||||
const mapServiceSettings = new MapServiceSettings(config, appVersion);
|
||||
|
||||
expect(mapServiceSettings instanceof MapServiceSettings).toBeTruthy();
|
||||
expect(mapServiceSettings.hasUserConfiguredTmsLayer()).toBeFalsy();
|
||||
expect(mapServiceSettings.defaultTmsLayer()).toBe('road_map_desaturated');
|
||||
});
|
||||
|
||||
test('should be able to set user configured base layer through config', () => {
|
||||
const mapServiceSettings = new MapServiceSettings(
|
||||
{
|
||||
...config,
|
||||
tilemap: {
|
||||
url: 'http://some.tile.com/map/{z}/{x}/{y}.jpg',
|
||||
options: {
|
||||
attribution: 'attribution',
|
||||
minZoom: 0,
|
||||
maxZoom: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
appVersion
|
||||
);
|
||||
|
||||
expect(mapServiceSettings.defaultTmsLayer()).toBe('TMS in config/kibana.yml');
|
||||
expect(mapServiceSettings.hasUserConfiguredTmsLayer()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should load ems client only on executing getTmsService method', async () => {
|
||||
const mapServiceSettings = new MapServiceSettings(config, appVersion);
|
||||
|
||||
expect(getPrivateField<EMSClient>(mapServiceSettings, 'emsClient')).toBeUndefined();
|
||||
|
||||
await mapServiceSettings.getTmsService('road_map');
|
||||
|
||||
expect(
|
||||
getPrivateField<EMSClient>(mapServiceSettings, 'emsClient') instanceof EMSClient
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should set isDarkMode value on executing getTmsService method', async () => {
|
||||
const mapServiceSettings = new MapServiceSettings(config, appVersion);
|
||||
getUiSettingsMockedValue = true;
|
||||
|
||||
expect(getPrivateField<EMSClient>(mapServiceSettings, 'isDarkMode')).toBeFalsy();
|
||||
|
||||
await mapServiceSettings.getTmsService('road_map');
|
||||
|
||||
expect(getPrivateField<EMSClient>(mapServiceSettings, 'isDarkMode')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('getAttributionsForTmsService method should return attributes in a correct form', () => {
|
||||
const tmsService = ({
|
||||
getAttributions: jest.fn(() => [
|
||||
{ url: 'https://fist_attr.com', label: 'fist_attr' },
|
||||
{ url: 'https://second_attr.com', label: 'second_attr' },
|
||||
]),
|
||||
} as unknown) as TMSService;
|
||||
|
||||
expect(getAttributionsForTmsService(tmsService)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"<a rel=\\"noreferrer noopener\\" href=\\"https://fist_attr.com\\">fist_attr</a>",
|
||||
"<a rel=\\"noreferrer noopener\\" href=\\"https://second_attr.com\\">second_attr</a>",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EMSClient, TMSService } from '@elastic/ems-client';
|
||||
import { getUISettings } from '../../services';
|
||||
import { userConfiguredLayerId } from './constants';
|
||||
import type { MapsLegacyConfig } from '../../../../maps_legacy/config';
|
||||
|
||||
type EmsClientConfig = ConstructorParameters<typeof EMSClient>[0];
|
||||
|
||||
const hasUserConfiguredTmsService = (config: MapsLegacyConfig) => Boolean(config.tilemap?.url);
|
||||
|
||||
const initEmsClientAsync = async (config: Partial<EmsClientConfig>) => {
|
||||
/**
|
||||
* Build optimization: '@elastic/ems-client' should be loaded from a separate chunk
|
||||
*/
|
||||
const emsClientModule = await import('@elastic/ems-client');
|
||||
|
||||
return new emsClientModule.EMSClient({
|
||||
language: i18n.getLocale(),
|
||||
appName: 'kibana',
|
||||
// Wrap to avoid errors passing window fetch
|
||||
fetchFunction(input: RequestInfo, init?: RequestInit) {
|
||||
return fetch(input, init);
|
||||
},
|
||||
...config,
|
||||
} as EmsClientConfig);
|
||||
};
|
||||
|
||||
export class MapServiceSettings {
|
||||
private emsClient?: EMSClient;
|
||||
private isDarkMode: boolean = false;
|
||||
|
||||
constructor(public config: MapsLegacyConfig, private appVersion: string) {}
|
||||
|
||||
private isInitialized() {
|
||||
return Boolean(this.emsClient);
|
||||
}
|
||||
|
||||
public hasUserConfiguredTmsLayer() {
|
||||
return hasUserConfiguredTmsService(this.config);
|
||||
}
|
||||
|
||||
public defaultTmsLayer() {
|
||||
const { dark, desaturated } = this.config.emsTileLayerId;
|
||||
|
||||
if (this.hasUserConfiguredTmsLayer()) {
|
||||
return userConfiguredLayerId;
|
||||
}
|
||||
|
||||
return this.isDarkMode ? dark : desaturated;
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
this.isDarkMode = getUISettings().get('theme:darkMode');
|
||||
|
||||
this.emsClient = await initEmsClientAsync({
|
||||
appVersion: this.appVersion,
|
||||
fileApiUrl: this.config.emsFileApiUrl,
|
||||
tileApiUrl: this.config.emsTileApiUrl,
|
||||
landingPageUrl: this.config.emsLandingPageUrl,
|
||||
});
|
||||
}
|
||||
|
||||
public async getTmsService(tmsTileLayer: string) {
|
||||
if (!this.isInitialized()) {
|
||||
await this.initialize();
|
||||
}
|
||||
return this.emsClient?.findTMSServiceById(tmsTileLayer);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAttributionsForTmsService(tmsService: TMSService) {
|
||||
return tmsService.getAttributions().map(({ label, url }) => {
|
||||
const anchorTag = document.createElement('a');
|
||||
|
||||
anchorTag.textContent = label;
|
||||
anchorTag.setAttribute('rel', 'noreferrer noopener');
|
||||
anchorTag.setAttribute('href', url);
|
||||
|
||||
return anchorTag.outerHTML;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
export { validateZoomSettings } from './validation_helper';
|
||||
export { injectMapPropsIntoSpec } from './vsi_helper';
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
import { validateZoomSettings } from './validation_helper';
|
||||
|
||||
type ValidateZoomSettingsParams = Parameters<typeof validateZoomSettings>;
|
||||
|
||||
type MapConfigType = ValidateZoomSettingsParams[0];
|
||||
type LimitsType = ValidateZoomSettingsParams[1];
|
||||
type OnWarnType = ValidateZoomSettingsParams[2];
|
||||
|
||||
describe('vega_map_view/validation_helper', () => {
|
||||
describe('validateZoomSettings', () => {
|
||||
let mapConfig: MapConfigType;
|
||||
let limits: LimitsType;
|
||||
let onWarn: OnWarnType;
|
||||
|
||||
beforeEach(() => {
|
||||
onWarn = jest.fn();
|
||||
mapConfig = {
|
||||
maxZoom: 10,
|
||||
minZoom: 5,
|
||||
zoom: 5,
|
||||
};
|
||||
limits = {
|
||||
maxZoom: 15,
|
||||
minZoom: 2,
|
||||
};
|
||||
});
|
||||
|
||||
test('should return validated interval', () => {
|
||||
expect(validateZoomSettings(mapConfig, limits, onWarn)).toEqual({
|
||||
maxZoom: 10,
|
||||
minZoom: 5,
|
||||
zoom: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return default interval in case if mapConfig not provided', () => {
|
||||
mapConfig = {} as MapConfigType;
|
||||
expect(validateZoomSettings(mapConfig, limits, onWarn)).toEqual({
|
||||
maxZoom: 15,
|
||||
minZoom: 2,
|
||||
zoom: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('should reset MaxZoom if the passed value is greater than the limit', () => {
|
||||
mapConfig = {
|
||||
...mapConfig,
|
||||
maxZoom: 20,
|
||||
};
|
||||
|
||||
const result = validateZoomSettings(mapConfig, limits, onWarn);
|
||||
|
||||
expect(onWarn).toBeCalledWith('Resetting "maxZoom" to 15');
|
||||
expect(result.maxZoom).toEqual(15);
|
||||
});
|
||||
|
||||
test('should reset MinZoom if the passed value is greater than the limit', () => {
|
||||
mapConfig = {
|
||||
...mapConfig,
|
||||
minZoom: 0,
|
||||
};
|
||||
|
||||
const result = validateZoomSettings(mapConfig, limits, onWarn);
|
||||
|
||||
expect(onWarn).toBeCalledWith('Resetting "minZoom" to 2');
|
||||
expect(result.minZoom).toEqual(2);
|
||||
});
|
||||
|
||||
test('should reset Zoom if the passed value is greater than the max limit', () => {
|
||||
mapConfig = {
|
||||
...mapConfig,
|
||||
zoom: 45,
|
||||
};
|
||||
|
||||
const result = validateZoomSettings(mapConfig, limits, onWarn);
|
||||
|
||||
expect(onWarn).toBeCalledWith('Resetting "zoom" to 10');
|
||||
expect(result.zoom).toEqual(10);
|
||||
});
|
||||
|
||||
test('should reset Zoom if the passed value is greater than the min limit', () => {
|
||||
mapConfig = {
|
||||
...mapConfig,
|
||||
zoom: 0,
|
||||
};
|
||||
|
||||
const result = validateZoomSettings(mapConfig, limits, onWarn);
|
||||
|
||||
expect(onWarn).toBeCalledWith('Resetting "zoom" to 5');
|
||||
expect(result.zoom).toEqual(5);
|
||||
});
|
||||
|
||||
test('should swap min <--> max values', () => {
|
||||
mapConfig = {
|
||||
maxZoom: 10,
|
||||
minZoom: 15,
|
||||
};
|
||||
|
||||
const result = validateZoomSettings(mapConfig, limits, onWarn);
|
||||
|
||||
expect(onWarn).toBeCalledWith('"minZoom" and "maxZoom" have been swapped');
|
||||
expect(result).toEqual({ maxZoom: 15, minZoom: 10, zoom: 10 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
function validate(
|
||||
name: string,
|
||||
value: number,
|
||||
defaultValue: number,
|
||||
min: number,
|
||||
max: number,
|
||||
onWarn: (message: string) => void
|
||||
) {
|
||||
if (value === undefined) {
|
||||
value = defaultValue;
|
||||
} else if (value < min) {
|
||||
onWarn(
|
||||
i18n.translate('visTypeVega.mapView.resettingPropertyToMinValueWarningMessage', {
|
||||
defaultMessage: 'Resetting {name} to {min}',
|
||||
values: { name: `"${name}"`, min },
|
||||
})
|
||||
);
|
||||
value = min;
|
||||
} else if (value > max) {
|
||||
onWarn(
|
||||
i18n.translate('visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage', {
|
||||
defaultMessage: 'Resetting {name} to {max}',
|
||||
values: { name: `"${name}"`, max },
|
||||
})
|
||||
);
|
||||
value = max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function validateZoomSettings(
|
||||
mapConfig: {
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
zoom?: number;
|
||||
},
|
||||
limits: {
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
},
|
||||
onWarn: (message: any) => void
|
||||
) {
|
||||
const DEFAULT_ZOOM = 3;
|
||||
|
||||
let { maxZoom, minZoom, zoom = DEFAULT_ZOOM } = mapConfig;
|
||||
|
||||
minZoom = validate('minZoom', minZoom, limits.minZoom, limits.minZoom, limits.maxZoom, onWarn);
|
||||
maxZoom = validate('maxZoom', maxZoom, limits.maxZoom, limits.minZoom, limits.maxZoom, onWarn);
|
||||
|
||||
if (minZoom > maxZoom) {
|
||||
onWarn(
|
||||
i18n.translate('visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage', {
|
||||
defaultMessage: '{minZoomPropertyName} and {maxZoomPropertyName} have been swapped',
|
||||
values: {
|
||||
minZoomPropertyName: '"minZoom"',
|
||||
maxZoomPropertyName: '"maxZoom"',
|
||||
},
|
||||
})
|
||||
);
|
||||
[minZoom, maxZoom] = [maxZoom, minZoom];
|
||||
}
|
||||
|
||||
zoom = validate('zoom', zoom, DEFAULT_ZOOM, minZoom, maxZoom, onWarn);
|
||||
|
||||
return {
|
||||
zoom,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { injectMapPropsIntoSpec } from './vsi_helper';
|
||||
import { VegaSpec } from '../../../data_model/types';
|
||||
|
||||
describe('vega_map_view/vsi_helper', () => {
|
||||
describe('injectMapPropsIntoSpec', () => {
|
||||
test('should inject map properties into vega spec', () => {
|
||||
const spec = ({
|
||||
$schema: 'https://vega.github.io/schema/vega/v5.json',
|
||||
config: {
|
||||
kibana: { type: 'map', latitude: 25, longitude: -70, zoom: 3 },
|
||||
},
|
||||
} as unknown) as VegaSpec;
|
||||
|
||||
expect(injectMapPropsIntoSpec(spec)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"$schema": "https://vega.github.io/schema/vega/v5.json",
|
||||
"autosize": "none",
|
||||
"config": Object {
|
||||
"kibana": Object {
|
||||
"latitude": 25,
|
||||
"longitude": -70,
|
||||
"type": "map",
|
||||
"zoom": 3,
|
||||
},
|
||||
},
|
||||
"projections": Array [
|
||||
Object {
|
||||
"center": Array [
|
||||
0,
|
||||
Object {
|
||||
"signal": "latitude",
|
||||
},
|
||||
],
|
||||
"fit": false,
|
||||
"name": "projection",
|
||||
"rotate": Array [
|
||||
Object {
|
||||
"signal": "-longitude",
|
||||
},
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"scale": Object {
|
||||
"signal": "512*pow(2,zoom)/2/PI",
|
||||
},
|
||||
"translate": Array [
|
||||
Object {
|
||||
"signal": "width/2",
|
||||
},
|
||||
Object {
|
||||
"signal": "height/2",
|
||||
},
|
||||
],
|
||||
"type": "mercator",
|
||||
},
|
||||
],
|
||||
"signals": Array [
|
||||
Object {
|
||||
"name": "zoom",
|
||||
},
|
||||
Object {
|
||||
"name": "latitude",
|
||||
},
|
||||
Object {
|
||||
"name": "longitude",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import Vsi from 'vega-spec-injector';
|
||||
|
||||
import { VegaSpec } from '../../../data_model/types';
|
||||
import { defaultProjection } from '../constants';
|
||||
|
||||
export const injectMapPropsIntoSpec = (spec: VegaSpec) => {
|
||||
const vsi = new Vsi();
|
||||
|
||||
vsi.overrideField(spec, 'autosize', 'none');
|
||||
vsi.addToList(spec, 'signals', ['zoom', 'latitude', 'longitude']);
|
||||
vsi.addToList(spec, 'projections', [defaultProjection]);
|
||||
|
||||
return spec;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
@import '~mapbox-gl/dist/mapbox-gl.css';
|
||||
|
||||
.vgaVis {
|
||||
.mapboxgl-canvas-container {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import 'jest-canvas-mock';
|
||||
|
||||
import type { TMSService } from '@elastic/ems-client';
|
||||
import { VegaMapView } from './view';
|
||||
import { VegaViewParams } from '../vega_base_view';
|
||||
import { VegaParser } from '../../data_model/vega_parser';
|
||||
import { TimeCache } from '../../data_model/time_cache';
|
||||
import { SearchAPI } from '../../data_model/search_api';
|
||||
import vegaMap from '../../test_utils/vega_map_test.json';
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
import { dataPluginMock } from '../../../../data/public/mocks';
|
||||
import { IServiceSettings } from '../../../../maps_legacy/public';
|
||||
import type { MapsLegacyConfig } from '../../../../maps_legacy/config';
|
||||
import { MapServiceSettings } from './map_service_settings';
|
||||
import { userConfiguredLayerId } from './constants';
|
||||
import {
|
||||
setInjectedVars,
|
||||
setData,
|
||||
setNotifications,
|
||||
setMapServiceSettings,
|
||||
setUISettings,
|
||||
} from '../../services';
|
||||
|
||||
jest.mock('../../lib/vega', () => ({
|
||||
vega: jest.requireActual('vega'),
|
||||
vegaLite: jest.requireActual('vega-lite'),
|
||||
}));
|
||||
|
||||
jest.mock('mapbox-gl', () => ({
|
||||
Map: jest.fn().mockImplementation(() => ({
|
||||
getLayer: () => '',
|
||||
removeLayer: jest.fn(),
|
||||
once: (eventName: string, handler: Function) => handler(),
|
||||
remove: () => jest.fn(),
|
||||
getCanvas: () => ({ clientWidth: 512, clientHeight: 512 }),
|
||||
getCenter: () => ({ lat: 20, lng: 20 }),
|
||||
getZoom: () => 3,
|
||||
addControl: jest.fn(),
|
||||
addLayer: jest.fn(),
|
||||
})),
|
||||
MapboxOptions: jest.fn(),
|
||||
NavigationControl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./layers', () => ({
|
||||
initVegaLayer: jest.fn(),
|
||||
initTmsRasterLayer: jest.fn(),
|
||||
}));
|
||||
|
||||
import { initVegaLayer, initTmsRasterLayer } from './layers';
|
||||
import { Map, NavigationControl } from 'mapbox-gl';
|
||||
|
||||
describe('vega_map_view/view', () => {
|
||||
describe('VegaMapView', () => {
|
||||
const coreStart = coreMock.createStart();
|
||||
const dataPluginStart = dataPluginMock.createStartContract();
|
||||
const mockGetServiceSettings = async () => {
|
||||
return {} as IServiceSettings;
|
||||
};
|
||||
let vegaParser: VegaParser;
|
||||
|
||||
setInjectedVars({
|
||||
emsTileLayerId: {},
|
||||
enableExternalUrls: true,
|
||||
});
|
||||
setData(dataPluginStart);
|
||||
setNotifications(coreStart.notifications);
|
||||
setUISettings(coreStart.uiSettings);
|
||||
|
||||
const getTmsService = jest.fn().mockReturnValue(({
|
||||
getVectorStyleSheet: () => ({
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
}),
|
||||
getMaxZoom: async () => 20,
|
||||
getMinZoom: async () => 0,
|
||||
getAttributions: () => [{ url: 'tms_attributions' }],
|
||||
} as unknown) as TMSService);
|
||||
const config = {
|
||||
tilemap: {
|
||||
url: 'test',
|
||||
options: {
|
||||
attribution: 'tilemap-attribution',
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
},
|
||||
},
|
||||
} as MapsLegacyConfig;
|
||||
|
||||
function setMapService(defaultTmsLayer: string) {
|
||||
setMapServiceSettings(({
|
||||
getTmsService,
|
||||
defaultTmsLayer: () => defaultTmsLayer,
|
||||
config,
|
||||
} as unknown) as MapServiceSettings);
|
||||
}
|
||||
|
||||
async function createVegaMapView() {
|
||||
await vegaParser.parseAsync();
|
||||
return new VegaMapView({
|
||||
vegaParser,
|
||||
filterManager: dataPluginStart.query.filterManager,
|
||||
timefilter: dataPluginStart.query.timefilter.timefilter,
|
||||
fireEvent: (event: any) => {},
|
||||
parentEl: document.createElement('div'),
|
||||
} as VegaViewParams);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vegaParser = new VegaParser(
|
||||
JSON.stringify(vegaMap),
|
||||
new SearchAPI({
|
||||
search: dataPluginStart.search,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
injectedMetadata: coreStart.injectedMetadata,
|
||||
}),
|
||||
new TimeCache(dataPluginStart.query.timefilter.timefilter, 0),
|
||||
{},
|
||||
mockGetServiceSettings
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should be added TmsRasterLayer and do not use tmsService if mapStyle is "user_configured"', async () => {
|
||||
setMapService(userConfiguredLayerId);
|
||||
const vegaMapView = await createVegaMapView();
|
||||
|
||||
await vegaMapView.init();
|
||||
|
||||
const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig;
|
||||
expect(Map).toHaveBeenCalledWith({
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
},
|
||||
customAttribution: 'tilemap-attribution',
|
||||
container: vegaMapView._$container.get(0),
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
zoom: 3,
|
||||
scrollZoom: scrollWheelZoom,
|
||||
center: [longitude, latitude],
|
||||
});
|
||||
expect(getTmsService).not.toHaveBeenCalled();
|
||||
expect(initTmsRasterLayer).toHaveBeenCalled();
|
||||
expect(initVegaLayer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not be added TmsRasterLayer and use tmsService if mapStyle is not "user_configured"', async () => {
|
||||
setMapService('road_map_desaturated');
|
||||
const vegaMapView = await createVegaMapView();
|
||||
|
||||
await vegaMapView.init();
|
||||
|
||||
const { longitude, latitude, scrollWheelZoom } = vegaMapView._parser.mapConfig;
|
||||
expect(Map).toHaveBeenCalledWith({
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
},
|
||||
customAttribution: ['<a rel="noreferrer noopener" href="tms_attributions"></a>'],
|
||||
container: vegaMapView._$container.get(0),
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
zoom: 3,
|
||||
scrollZoom: scrollWheelZoom,
|
||||
center: [longitude, latitude],
|
||||
});
|
||||
expect(getTmsService).toHaveBeenCalled();
|
||||
expect(initTmsRasterLayer).not.toHaveBeenCalled();
|
||||
expect(initVegaLayer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should be added NavigationControl', async () => {
|
||||
setMapService('road_map_desaturated');
|
||||
const vegaMapView = await createVegaMapView();
|
||||
|
||||
await vegaMapView.init();
|
||||
|
||||
expect(NavigationControl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
181
src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts
Normal file
181
src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Map, Style, NavigationControl, MapboxOptions } from 'mapbox-gl';
|
||||
|
||||
import { initTmsRasterLayer, initVegaLayer } from './layers';
|
||||
import { VegaBaseView } from '../vega_base_view';
|
||||
import { getMapServiceSettings } from '../../services';
|
||||
import { getAttributionsForTmsService } from './map_service_settings';
|
||||
import type { MapServiceSettings } from './map_service_settings';
|
||||
|
||||
import {
|
||||
defaultMapConfig,
|
||||
defaultMabBoxStyle,
|
||||
userConfiguredLayerId,
|
||||
vegaLayerId,
|
||||
} from './constants';
|
||||
|
||||
import { validateZoomSettings, injectMapPropsIntoSpec } from './utils';
|
||||
|
||||
// @ts-expect-error
|
||||
import { vega } from '../../lib/vega';
|
||||
|
||||
import './vega_map_view.scss';
|
||||
|
||||
async function updateVegaView(mapBoxInstance: Map, vegaView: vega.View) {
|
||||
const mapCanvas = mapBoxInstance.getCanvas();
|
||||
const { lat, lng } = mapBoxInstance.getCenter();
|
||||
let shouldRender = false;
|
||||
|
||||
const sendSignal = (sig: string, value: any) => {
|
||||
if (vegaView.signal(sig) !== value) {
|
||||
vegaView.signal(sig, value);
|
||||
shouldRender = true;
|
||||
}
|
||||
};
|
||||
|
||||
sendSignal('width', mapCanvas.clientWidth);
|
||||
sendSignal('height', mapCanvas.clientHeight);
|
||||
sendSignal('latitude', lat);
|
||||
sendSignal('longitude', lng);
|
||||
sendSignal('zoom', mapBoxInstance.getZoom());
|
||||
|
||||
if (shouldRender) {
|
||||
await vegaView.runAsync();
|
||||
}
|
||||
}
|
||||
|
||||
export class VegaMapView extends VegaBaseView {
|
||||
private mapServiceSettings: MapServiceSettings = getMapServiceSettings();
|
||||
private mapStyle = this.getMapStyle();
|
||||
|
||||
private getMapStyle() {
|
||||
const { mapStyle } = this._parser.mapConfig;
|
||||
|
||||
return mapStyle === 'default' ? this.mapServiceSettings.defaultTmsLayer() : mapStyle;
|
||||
}
|
||||
|
||||
private get shouldShowZoomControl() {
|
||||
return Boolean(this._parser.mapConfig.zoomControl);
|
||||
}
|
||||
|
||||
private getMapParams(defaults: { maxZoom: number; minZoom: number }): Partial<MapboxOptions> {
|
||||
const { longitude, latitude, scrollWheelZoom } = this._parser.mapConfig;
|
||||
const zoomSettings = validateZoomSettings(this._parser.mapConfig, defaults, this.onWarn);
|
||||
|
||||
return {
|
||||
...zoomSettings,
|
||||
center: [longitude, latitude],
|
||||
scrollZoom: scrollWheelZoom,
|
||||
};
|
||||
}
|
||||
|
||||
private async initMapContainer(vegaView: vega.View) {
|
||||
let style: Style = defaultMabBoxStyle;
|
||||
let customAttribution: MapboxOptions['customAttribution'] = [];
|
||||
const zoomSettings = {
|
||||
minZoom: defaultMapConfig.minZoom,
|
||||
maxZoom: defaultMapConfig.maxZoom,
|
||||
};
|
||||
|
||||
if (this.mapStyle && this.mapStyle !== userConfiguredLayerId) {
|
||||
const tmsService = await this.mapServiceSettings.getTmsService(this.mapStyle);
|
||||
|
||||
if (!tmsService) {
|
||||
this.onWarn(
|
||||
i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', {
|
||||
defaultMessage: '{mapStyleParam} was not found',
|
||||
values: { mapStyleParam: `"mapStyle":${this.mapStyle}` },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
zoomSettings.maxZoom = (await tmsService.getMaxZoom()) ?? defaultMapConfig.maxZoom;
|
||||
zoomSettings.minZoom = (await tmsService.getMinZoom()) ?? defaultMapConfig.minZoom;
|
||||
customAttribution = getAttributionsForTmsService(tmsService);
|
||||
style = (await tmsService.getVectorStyleSheet()) as Style;
|
||||
} else {
|
||||
customAttribution = this.mapServiceSettings.config.tilemap.options.attribution;
|
||||
}
|
||||
|
||||
// In some cases, Vega may be initialized twice, e.g. after awaiting...
|
||||
if (!this._$container) return;
|
||||
|
||||
const mapBoxInstance = new Map({
|
||||
style,
|
||||
customAttribution,
|
||||
container: this._$container.get(0),
|
||||
...this.getMapParams({ ...zoomSettings }),
|
||||
});
|
||||
|
||||
const initMapComponents = () => {
|
||||
this.initControls(mapBoxInstance);
|
||||
this.initLayers(mapBoxInstance, vegaView);
|
||||
|
||||
this._addDestroyHandler(() => {
|
||||
if (mapBoxInstance.getLayer(vegaLayerId)) {
|
||||
mapBoxInstance.removeLayer(vegaLayerId);
|
||||
}
|
||||
if (mapBoxInstance.getLayer(userConfiguredLayerId)) {
|
||||
mapBoxInstance.removeLayer(userConfiguredLayerId);
|
||||
}
|
||||
mapBoxInstance.remove();
|
||||
});
|
||||
};
|
||||
|
||||
mapBoxInstance.once('load', initMapComponents);
|
||||
}
|
||||
|
||||
private initControls(mapBoxInstance: Map) {
|
||||
if (this.shouldShowZoomControl) {
|
||||
mapBoxInstance.addControl(new NavigationControl({ showCompass: false }), 'top-left');
|
||||
}
|
||||
}
|
||||
|
||||
private initLayers(mapBoxInstance: Map, vegaView: vega.View) {
|
||||
const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId;
|
||||
|
||||
if (shouldShowUserConfiguredLayer) {
|
||||
const { url, options } = this.mapServiceSettings.config.tilemap;
|
||||
|
||||
initTmsRasterLayer({
|
||||
id: userConfiguredLayerId,
|
||||
map: mapBoxInstance,
|
||||
context: {
|
||||
tiles: [url!],
|
||||
maxZoom: options.maxZoom ?? defaultMapConfig.maxZoom,
|
||||
minZoom: options.minZoom ?? defaultMapConfig.minZoom,
|
||||
tileSize: options.tileSize ?? defaultMapConfig.tileSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
initVegaLayer({
|
||||
id: vegaLayerId,
|
||||
map: mapBoxInstance,
|
||||
context: {
|
||||
vegaView,
|
||||
updateVegaView,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async _initViewCustomizations() {
|
||||
const vegaView = new vega.View(
|
||||
vega.parse(injectMapPropsIntoSpec(this._parser.spec)),
|
||||
this._vegaViewConfig
|
||||
);
|
||||
|
||||
this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec);
|
||||
this.setView(vegaView);
|
||||
|
||||
await this.initMapContainer(vegaView);
|
||||
}
|
||||
}
|
|
@ -16,8 +16,6 @@ export class VegaView extends VegaBaseView {
|
|||
|
||||
const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig);
|
||||
|
||||
view.warn = this.onWarn.bind(this);
|
||||
view.error = this.onError.bind(this);
|
||||
if (this._parser.useResize) this.updateVegaSize(view);
|
||||
view.initialize(this._$container.get(0), this._$controls.get(0));
|
||||
|
||||
|
|
|
@ -10,13 +10,10 @@ import 'jest-canvas-mock';
|
|||
|
||||
import $ from 'jquery';
|
||||
|
||||
import 'leaflet/dist/leaflet.js';
|
||||
import 'leaflet-vega';
|
||||
import { createVegaVisualization } from './vega_visualization';
|
||||
|
||||
import vegaliteGraph from './test_utils/vegalite_graph.json';
|
||||
import vegaGraph from './test_utils/vega_graph.json';
|
||||
import vegaMapGraph from './test_utils/vega_map_test.json';
|
||||
|
||||
import { VegaParser } from './data_model/vega_parser';
|
||||
import { SearchAPI } from './data_model/search_api';
|
||||
|
@ -146,32 +143,5 @@ describe('VegaVisualizations', () => {
|
|||
vegaVis.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show vega blank rectangle on top of a map (vegamap)', async () => {
|
||||
let vegaVis;
|
||||
try {
|
||||
vegaVis = new VegaVisualization(domNode, jest.fn());
|
||||
const vegaParser = new VegaParser(
|
||||
JSON.stringify(vegaMapGraph),
|
||||
new SearchAPI({
|
||||
search: dataPluginStart.search,
|
||||
uiSettings: coreStart.uiSettings,
|
||||
injectedMetadata: coreStart.injectedMetadata,
|
||||
}),
|
||||
0,
|
||||
0,
|
||||
mockGetServiceSettings
|
||||
);
|
||||
await vegaParser.parseAsync();
|
||||
|
||||
mockedWidthValue = 256;
|
||||
mockedHeightValue = 256;
|
||||
|
||||
await vegaVis.render(vegaParser);
|
||||
expect(domNode.innerHTML).toMatchSnapshot();
|
||||
} finally {
|
||||
vegaVis.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -78,7 +78,7 @@ export const createVegaVisualization = ({
|
|||
};
|
||||
|
||||
if (vegaParser.useMap) {
|
||||
const { VegaMapView } = await import('./vega_view/vega_map_view');
|
||||
const { VegaMapView } = await import('./vega_view/vega_map_view/view');
|
||||
this.vegaView = new VegaMapView(vegaViewParams);
|
||||
} else {
|
||||
const { VegaView: VegaViewClass } = await import('./vega_view/vega_view');
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"include": [
|
||||
"server/**/*",
|
||||
"public/**/*",
|
||||
"*.ts"
|
||||
"*.ts",
|
||||
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
|
||||
"public/test_utils/vega_map_test.json"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../core/tsconfig.json" },
|
||||
|
|
|
@ -269,3 +269,24 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "visualization:VegaMap",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "visualization",
|
||||
"visualization": {
|
||||
"description": "VegaMap",
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
|
||||
},
|
||||
"title": "VegaMap",
|
||||
"uiStateJSON": "{}",
|
||||
"version": 1,
|
||||
"visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
return {
|
||||
...functionalConfig.getAll(),
|
||||
|
||||
testFiles: [require.resolve('./tests/console_app'), require.resolve('./tests/discover')],
|
||||
testFiles: [
|
||||
require.resolve('./tests/console_app'),
|
||||
require.resolve('./tests/discover'),
|
||||
require.resolve('./tests/vega'),
|
||||
],
|
||||
|
||||
services,
|
||||
|
||||
|
|
27
test/visual_regression/tests/vega/index.ts
Normal file
27
test/visual_regression/tests/vega/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DEFAULT_OPTIONS } from '../../services/visual_testing/visual_testing';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
// Width must be the same as visual_testing or canvas image widths will get skewed
|
||||
const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || [];
|
||||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const browser = getService('browser');
|
||||
|
||||
describe('vega app', function () {
|
||||
this.tags('ciGroup6');
|
||||
|
||||
before(function () {
|
||||
return browser.setWindowSize(SCREEN_WIDTH, 1000);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./vega_map_visualization'));
|
||||
});
|
||||
}
|
34
test/visual_regression/tests/vega/vega_map_visualization.ts
Normal file
34
test/visual_regression/tests/vega/vega_map_visualization.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* and the Server Side Public License, v 1; you may not use this file except in
|
||||
* compliance with, at your election, the Elastic License or the Server Side
|
||||
* Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']);
|
||||
const visualTesting = getService('visualTesting');
|
||||
|
||||
describe('vega chart in visualize app', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('kibana_sample_data_flights');
|
||||
await esArchiver.loadIfNeeded('visualize');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('kibana_sample_data_flights');
|
||||
await esArchiver.unload('visualize');
|
||||
});
|
||||
|
||||
it('should show map with vega layer', async function () {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await PageObjects.visualize.openSavedVisualization('VegaMap');
|
||||
await PageObjects.visChart.waitForVisualizationRenderingStabilized();
|
||||
await visualTesting.snapshot();
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue