[maps] remove tile_map, region_map, and maps_legacy plugins (#105326)

* [maps] remove tile_map plugin

* initial bounds

* update embeddable query context

* start editor

* remove tile_map from tsconfig and i18n cleanup

* implement view in maps button

* tslint

* remove empty lines

* remove tileMap from limits.yml

* remove region_map and maps_legacy plugins

* region_map vis with Map embeddable

* make MapComponent

* lint

* clean up

* shorten text

* lint

* remove region_map from interpreter functional tests

* update docs

* add migration for removing ui_settings

* remove tile_map and region_map functional tests

* tslint

* call handlers.done when layers are loaded

* fix visualize create menu test

* eslint

* add owner comment to ui_settings/saved_objects/migrations.ts

* remove deleted plugins from codeowners

* review feedback

* use correct value for TILE_MAP_RENDER

* down select mapModules for getLayerDescriptors callback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2021-08-18 12:51:31 -06:00 committed by GitHub
parent 9fa41d1aef
commit dd9dd52718
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
153 changed files with 1032 additions and 7160 deletions

4
.github/CODEOWNERS vendored
View file

@ -178,12 +178,8 @@
/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis
/x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis
/x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis
#CC# /src/plugins/maps_legacy/ @elastic/kibana-gis
/src/plugins/maps_legacy/ @elastic/kibana-gis
#CC# /x-pack/plugins/file_upload @elastic/kibana-gis
/x-pack/plugins/file_upload @elastic/kibana-gis
/src/plugins/tile_map/ @elastic/kibana-gis
/src/plugins/region_map/ @elastic/kibana-gis
/packages/kbn-mapbox-gl @elastic/kibana-gis
# Operations

View file

@ -33,7 +33,6 @@
"kbnDocViews": "src/legacy/core_plugins/kbn_doc_views",
"lists": "packages/kbn-securitysolution-list-utils/src",
"management": ["src/legacy/core_plugins/management", "src/plugins/management"],
"maps_legacy": "src/plugins/maps_legacy",
"monaco": "packages/kbn-monaco/src",
"esQuery": "packages/kbn-es-query/src",
"presentationUtil": "src/plugins/presentation_util",
@ -49,14 +48,12 @@
"kibana_utils": "src/plugins/kibana_utils",
"navigation": "src/plugins/navigation",
"newsfeed": "src/plugins/newsfeed",
"regionMap": "src/plugins/region_map",
"savedObjects": "src/plugins/saved_objects",
"savedObjectsManagement": "src/plugins/saved_objects_management",
"security": "src/plugins/security_oss",
"server": "src/legacy/server",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],
"tileMap": "src/plugins/tile_map",
"timelion": ["src/plugins/timelion", "src/plugins/vis_type_timelion"],
"uiActions": "src/plugins/ui_actions",
"visDefaultEditor": "src/plugins/vis_default_editor",

View file

@ -182,10 +182,6 @@ management section itself.
|Configuration of kibana-wide EMS settings and some higher level utilities.
|{kib-repo}blob/{branch}/src/plugins/maps_legacy/README.md[mapsLegacy]
|Internal objects used by the Coordinate, Region, and Vega visualizations.
|{kib-repo}blob/{branch}/src/plugins/navigation/README.md[navigation]
|The navigation plugins exports the TopNavMenu component.
It also provides a stateful version of it on the start contract.
@ -200,10 +196,6 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s
|The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas).
|{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap]
|Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states.
|{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects]
|NOTE: This plugin is deprecated and will be removed in 8.0. See https://github.com/elastic/kibana/issues/46435 for more information.
@ -247,10 +239,6 @@ generating deep links to other apps, and creating short URLs.
|This plugin adds the Advanced Settings section for the Usage and Security Data collection (aka Telemetry).
|{kib-repo}blob/{branch}/src/plugins/tile_map/README.md[tileMap]
|Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs.
|{kib-repo}blob/{branch}/src/plugins/timelion/README.md[timelion]
|Contains the deprecated timelion application. For the timelion visualization,
which also contains the timelion APIs and backend, look at the vis_type_timelion plugin.

View file

@ -534,17 +534,6 @@ of the chart. Use numbers between 0 and 1. The lower the number, the more the hi
[[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`::
The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance.
[[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`::
Shows a warning in a region map when terms cannot be joined to a shape.
[[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`::
The default properties for the WMS map server supported in the coordinate map.
[[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`::
The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high,
and 12 is the maximum. For more information, refer to
{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator].
[[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`::
**The legacy XY charts are deprecated and will not be supported as of 7.16.**
The visualize editor uses a new XY charts library with improved performance, color palettes, fill capacity, and more. Enable this option if you prefer to use the legacy charts library.
@ -563,4 +552,4 @@ only production-ready visualizations are available to users.
[horizontal]
[[telemetry-enabled-advanced-setting]]`telemetry:enabled`::
When enabled, helps improve the Elastic Stack by providing usage statistics for
basic features. This data will not be shared outside of Elastic.
basic features. This data will not be shared outside of Elastic.

View file

@ -280,10 +280,6 @@
"jsonwebtoken": "^8.5.1",
"jsts": "^1.6.2",
"kea": "^2.4.2",
"leaflet": "1.5.1",
"leaflet-draw": "0.4.14",
"leaflet-responsive-popup": "0.6.4",
"leaflet.heat": "0.2.0",
"less": "npm:@elastic/less@2.7.3-kibana",
"load-json-file": "^6.2.0",
"loader-utils": "^1.2.3",

View file

@ -50,15 +50,13 @@ pageLoadAssetSize:
lists: 22900
logstash: 53548
management: 46112
maps: 80000
mapsLegacy: 87859
maps: 90000
ml: 82187
monitoring: 80000
navigation: 37269
newsfeed: 42228
observability: 89709
painlessLab: 179748
regionMap: 66098
remoteClusters: 51327
reporting: 183418
rollup: 97204
@ -75,7 +73,6 @@ pageLoadAssetSize:
spaces: 57868
telemetry: 51957
telemetryManagementSection: 38586
tileMap: 65337
timelion: 29920
transform: 41007
triggersActionsUi: 100000

View file

@ -128,3 +128,38 @@ describe('ui_settings 7.13.0 migrations', () => {
});
});
});
describe('ui_settings 8.0.0 migrations', () => {
const migration = migrations['8.0.0'];
test('returns doc on empty object', () => {
expect(migration({} as SavedObjectUnsanitizedDoc)).toEqual({
references: [],
});
});
test('removes ui_settings from deleted region_map and tile_map plugins', () => {
const doc = {
type: 'config',
id: '8.0.0',
attributes: {
buildNum: 9007199254740991,
'visualization:regionmap:showWarnings': false,
'visualization:tileMap:WMSdefaults': '{}',
'visualization:tileMap:maxPrecision': 10,
},
references: [],
updated_at: '2020-06-09T20:18:20.349Z',
migrationVersion: {},
};
expect(migration(doc)).toEqual({
type: 'config',
id: '8.0.0',
attributes: {
buildNum: 9007199254740991,
},
references: [],
updated_at: '2020-06-09T20:18:20.349Z',
migrationVersion: {},
});
});
});

View file

@ -75,4 +75,27 @@ export const migrations = {
}),
references: doc.references || [],
}),
'8.0.0': (doc: SavedObjectUnsanitizedDoc<any>): SavedObjectSanitizedDoc<any> => ({
...doc,
...(doc.attributes && {
// owner: Team:Geo
attributes: Object.keys(doc.attributes).reduce(
(acc, key) =>
[
'visualization:regionmap:showWarnings',
'visualization:tileMap:WMSdefaults',
'visualization:tileMap:maxPrecision',
].includes(key)
? {
...acc,
}
: {
...acc,
[key]: doc.attributes[key],
},
{}
),
}),
references: doc.references || [],
}),
};

View file

@ -1,7 +0,0 @@
# Maps legacy
Internal objects used by the Coordinate, Region, and Vega visualizations.
It exports the default Leaflet-based map and exposes the connection to the Elastic Maps service.
This plugin is targeted for removal in 8.0.

View file

@ -1,13 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({});
export type MapsLegacyConfig = TypeOf<typeof configSchema>;

View file

@ -1,13 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/maps_legacy'],
};

View file

@ -1,13 +0,0 @@
{
"id": "mapsLegacy",
"owner": {
"name": "GIS",
"githubTeam": "kibana-gis"
},
"version": "8.0.0",
"kibanaVersion": "kibana",
"ui": true,
"server": true,
"requiredPlugins": ["mapsEms"],
"requiredBundles": ["visDefaultEditor", "mapsEms"]
}

View file

@ -1,23 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TmsLayer } from '../../../maps_ems/public';
export interface WMSOptions {
selectedTmsLayer?: TmsLayer;
enabled: boolean;
url?: string;
options: {
version?: string;
layers?: string;
format: string;
transparent: boolean;
attribution?: string;
styles?: string;
};
}

View file

@ -1,68 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
interface Props {
isMapsAvailable: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
visualizationLabel: string;
}
export function LegacyMapDeprecationMessage(props: Props) {
const getMapsMessage = !props.isMapsAvailable ? (
<FormattedMessage
id="maps_legacy.defaultDistributionMessage"
defaultMessage="To get Maps, upgrade to the {defaultDistribution} of Elasticsearch and Kibana."
values={{
defaultDistribution: (
<EuiLink
color="accent"
external
href="https://www.elastic.co/downloads/kibana"
target="_blank"
>
default distribution
</EuiLink>
),
}}
/>
) : null;
const button = props.isMapsAvailable ? (
<div>
<EuiButton onClick={props.onClick} size="s">
<FormattedMessage id="maps_legacy.openInMapsButtonLabel" defaultMessage="View in Maps" />
</EuiButton>
</div>
) : null;
return (
<EuiCallOut
className="hide-for-sharing"
data-test-subj="deprecatedVisInfo"
size="s"
title={i18n.translate('maps_legacy.legacyMapDeprecationTitle', {
defaultMessage: '{label} will migrate to Maps in 8.0.',
values: { label: props.visualizationLabel },
})}
>
<p>
<FormattedMessage
id="maps_legacy.legacyMapDeprecationMessage"
defaultMessage="With Maps, you can add multiple layers and indices, plot individual documents, symbolize features from data values, add heatmaps, grids, and clusters, and more. {getMapsMessage}"
values={{ getMapsMessage }}
/>
</p>
{button}
</EuiCallOut>
);
}

View file

@ -1,205 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TextInputOption } from '../../../vis_default_editor/public';
import { WMSOptions } from '../common/types';
interface WmsInternalOptions {
wms: WMSOptions;
setValue: <T extends keyof WMSOptions>(paramName: T, value: WMSOptions[T]) => void;
}
function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) {
const wmsLink = (
<EuiLink href="http://www.opengeospatial.org/standards/wms" target="_blank">
<FormattedMessage id="maps_legacy.wmsOptions.wmsLinkText" defaultMessage="OGC standard" />
</EuiLink>
);
const footnoteText = (
<>
<span aria-hidden="true">*</span>
<FormattedMessage
id="maps_legacy.wmsOptions.mapLoadFailDescription"
defaultMessage="If this parameter is incorrect, maps will fail to load."
/>
</>
);
const footnote = (
<EuiScreenReaderOnly>
<p>{footnoteText}</p>
</EuiScreenReaderOnly>
);
const setOptions = <T extends keyof WMSOptions['options']>(
paramName: T,
value: WMSOptions['options'][T]
) =>
setValue('options', {
...wms.options,
[paramName]: value,
});
return (
<>
<EuiSpacer size="xs" />
<EuiText size="xs">
<FormattedMessage
id="maps_legacy.wmsOptions.wmsDescription"
defaultMessage="WMS is an {wmsLink} for map image services."
values={{ wmsLink }}
/>
</EuiText>
<EuiSpacer size="m" />
<TextInputOption
label={
<>
<FormattedMessage id="maps_legacy.wmsOptions.wmsUrlLabel" defaultMessage="WMS url" />
<span aria-hidden="true">*</span>
</>
}
helpText={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.urlOfWMSWebServiceTip"
defaultMessage="The URL of the WMS web service."
/>
{footnote}
</>
}
paramName="url"
value={wms.url}
setValue={setValue}
/>
<TextInputOption
label={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.wmsLayersLabel"
defaultMessage="WMS layers"
/>
<span aria-hidden="true">*</span>
</>
}
helpText={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.listOfLayersToUseTip"
defaultMessage="A comma separated list of layers to use."
/>
{footnote}
</>
}
paramName="layers"
value={wms.options.layers}
setValue={setOptions}
/>
<TextInputOption
label={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.wmsVersionLabel"
defaultMessage="WMS version"
/>
<span aria-hidden="true">*</span>
</>
}
helpText={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.versionOfWMSserverSupportsTip"
defaultMessage="The version of WMS the server supports."
/>
{footnote}
</>
}
paramName="version"
value={wms.options.version}
setValue={setOptions}
/>
<TextInputOption
label={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.wmsFormatLabel"
defaultMessage="WMS format"
/>
<span aria-hidden="true">*</span>
</>
}
helpText={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.imageFormatToUseTip"
defaultMessage="Usually image/png or image/jpeg. Use png if the server will return transparent layers."
/>
{footnote}
</>
}
paramName="format"
value={wms.options.format}
setValue={setOptions}
/>
<TextInputOption
label={
<FormattedMessage
id="maps_legacy.wmsOptions.wmsAttributionLabel"
defaultMessage="WMS attribution"
/>
}
helpText={
<FormattedMessage
id="maps_legacy.wmsOptions.attributionStringTip"
defaultMessage="Attribution string for the lower right corner."
/>
}
paramName="attribution"
value={wms.options.attribution}
setValue={setOptions}
/>
<TextInputOption
label={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.wmsStylesLabel"
defaultMessage="WMS styles"
/>
<span aria-hidden="true">*</span>
</>
}
helpText={
<>
<FormattedMessage
id="maps_legacy.wmsOptions.wmsServerSupportedStylesListTip"
defaultMessage="A comma separated list of WMS server supported styles to use. Blank in most cases."
/>
{footnote}
</>
}
paramName="styles"
value={wms.options.styles}
setValue={setOptions}
/>
<EuiText size="xs">
<p aria-hidden="true">{footnoteText}</p>
</EuiText>
</>
);
}
export { WmsInternalOptions };

View file

@ -1,88 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useMemo } from 'react';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { TmsLayer } from '../../../maps_ems/public';
import { SelectOption, SwitchOption } from '../../../vis_default_editor/public';
import { WmsInternalOptions } from './wms_internal_options';
import { WMSOptions } from '../common/types';
interface Props<K> {
stateParams: K;
setValue: (title: 'wms', options: WMSOptions) => void;
tmsLayers: TmsLayer[];
}
const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id });
function WmsOptions<K extends { wms: WMSOptions }>({ stateParams, setValue, tmsLayers }: Props<K>) {
const { wms } = stateParams;
const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]);
const setWmsOption = <T extends keyof WMSOptions>(paramName: T, value: WMSOptions[T]) =>
setValue('wms', {
...wms,
[paramName]: value,
});
const selectTmsLayer = (id: string) => {
const layer = tmsLayers.find((l: TmsLayer) => l.id === id);
if (layer) {
setWmsOption('selectedTmsLayer', layer);
}
};
return (
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="maps_legacy.wmsOptions.baseLayerSettingsTitle"
defaultMessage="Base layer settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<SwitchOption
label={i18n.translate('maps_legacy.wmsOptions.wmsMapServerLabel', {
defaultMessage: 'WMS map server',
})}
tooltip={i18n.translate('maps_legacy.wmsOptions.useWMSCompliantMapTileServerTip', {
defaultMessage: 'Use WMS compliant map tile server. For advanced users only.',
})}
paramName="enabled"
value={wms.enabled}
setValue={setWmsOption}
/>
{!wms.enabled && (
<>
<EuiSpacer size="s" />
<SelectOption
id="wmsOptionsSelectTmsLayer"
label={i18n.translate('maps_legacy.wmsOptions.layersLabel', {
defaultMessage: 'Layers',
})}
options={tmsLayerOptions}
paramName="selectedTmsLayer"
value={wms.selectedTmsLayer && wms.selectedTmsLayer.id}
setValue={(param, value) => selectTmsLayer(value)}
/>
</>
)}
{wms.enabled && <WmsInternalOptions wms={wms} setValue={setWmsOption} />}
</EuiPanel>
);
}
export { WmsOptions };

View file

@ -1,31 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginInitializerContext } from 'kibana/public';
import { MapsLegacyPlugin } from './plugin';
import * as colorUtil from './map/color_util';
import { KibanaMapLayer } from './map/kibana_map_layer';
import { mapTooltipProvider } from './tooltip_provider';
import './map/index.scss';
export function plugin(initializerContext: PluginInitializerContext) {
return new MapsLegacyPlugin(initializerContext);
}
/** @public */
export { colorUtil, KibanaMapLayer, mapTooltipProvider };
export { WMSOptions } from './common/types';
export { WmsOptions } from './components/wms_options';
export { LegacyMapDeprecationMessage } from './components/legacy_map_deprecation_message';
export { lazyLoadMapsLegacyModules } from './lazy_load_bundle';
export type MapsLegacyPluginSetup = ReturnType<MapsLegacyPlugin['setup']>;
export type MapsLegacyPluginStart = ReturnType<MapsLegacyPlugin['start']>;

View file

@ -1,29 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IUiSettingsClient, ToastsSetup } from 'kibana/public';
import type { MapsEmsConfig, IServiceSettings } from '../../maps_ems/public';
let toast: ToastsSetup;
export const setToasts = (notificationToast: ToastsSetup) => (toast = notificationToast);
export const getToasts = () => toast;
let uiSettings: IUiSettingsClient;
export const setUiSettings = (coreUiSettings: IUiSettingsClient) => (uiSettings = coreUiSettings);
export const getUiSettings = () => uiSettings;
let mapsEmsConfig: MapsEmsConfig;
export const setMapsEmsConfig = (config: MapsEmsConfig) => (mapsEmsConfig = config);
export const getEmsTileLayerId = () => mapsEmsConfig.emsTileLayerId;
let getServiceSettingsFunction: () => Promise<IServiceSettings>;
export const setGetServiceSettings = (getSS: () => Promise<IServiceSettings>) =>
(getServiceSettingsFunction = getSS);
export const getServiceSettings = async (): Promise<IServiceSettings> => {
return await getServiceSettingsFunction();
};

View file

@ -1,30 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
let loadModulesPromise: Promise<LazyLoadedMapsLegacyModules>;
interface LazyLoadedMapsLegacyModules {
KibanaMap: unknown;
L: unknown;
}
export async function lazyLoadMapsLegacyModules(): Promise<LazyLoadedMapsLegacyModules> {
if (typeof loadModulesPromise !== 'undefined') {
return loadModulesPromise;
}
loadModulesPromise = new Promise(async (resolve) => {
const { KibanaMap, L } = await import('./lazy');
resolve({
KibanaMap,
L,
});
});
return loadModulesPromise;
}

View file

@ -1,12 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// @ts-expect-error
export { KibanaMap } from '../../map/kibana_map';
// @ts-expect-error
export { L } from '../../leaflet';

View file

@ -1,22 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
if (!window.hasOwnProperty('L')) {
require('leaflet/dist/leaflet.css');
window.L = require('leaflet/dist/leaflet.js');
window.L.Browser.touch = false;
window.L.Browser.pointer = false;
require('leaflet.heat/dist/leaflet-heat.js');
require('leaflet-draw/dist/leaflet.draw.css');
require('leaflet-draw/dist/leaflet.draw.js');
require('leaflet-responsive-popup/leaflet.responsive.popup.css');
require('leaflet-responsive-popup/leaflet.responsive.popup.js');
}
export const L = window.L;

View file

@ -1,158 +0,0 @@
// stylelint-disable selector-no-qualifying-type
// SASSTODO: Create these tooltip variables in EUI
// And/Or create a tooltip mixin
$tempEUITooltipBackground: tintOrShade($euiColorFullShade, 25%, 90%);
$tempEUITooltipText: $euiColorGhost;
// Converted leaflet icon sprite into background svg for custom coloring (dark mode)
$visMapLeafletSprite: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 60' height='60' width='600'%3E%3Cg fill='#{hexToRGB($euiTextColor)}'%3E%3Cg%3E%3Cpath d='M18 36v6h6v-6h-6zm4 4h-2v-2h2v2z'/%3E%3Cpath d='M36 18v6h6v-6h-6zm4 4h-2v-2h2v2z'/%3E%3Cpath d='M23.142 39.145l-2.285-2.29 16-15.998 2.285 2.285z'/%3E%3C/g%3E%3Cpath d='M100 24.565l-2.096 14.83L83.07 42 76 28.773 86.463 18z'/%3E%3Cpath d='M140 20h20v20h-20z'/%3E%3Cpath d='M221 30c0 6.078-4.926 11-11 11s-11-4.922-11-11c0-6.074 4.926-11 11-11s11 4.926 11 11z'/%3E%3Cpath d='M270,19c-4.971,0-9,4.029-9,9c0,4.971,5.001,12,9,14c4.001-2,9-9.029,9-14C279,23.029,274.971,19,270,19z M270,31.5c-2.484,0-4.5-2.014-4.5-4.5c0-2.484,2.016-4.5,4.5-4.5c2.485,0,4.5,2.016,4.5,4.5C274.5,29.486,272.485,31.5,270,31.5z'/%3E%3Cg%3E%3Cpath d='M337,30.156v0.407v5.604c0,1.658-1.344,3-3,3h-10c-1.655,0-3-1.342-3-3v-10c0-1.657,1.345-3,3-3h6.345 l3.19-3.17H324c-3.313,0-6,2.687-6,6v10c0,3.313,2.687,6,6,6h10c3.314,0,6-2.687,6-6v-8.809L337,30.156'/%3E%3Cpath d='M338.72 24.637l-8.892 8.892H327V30.7l8.89-8.89z'/%3E%3Cpath d='M338.697 17.826h4v4h-4z' transform='rotate(-134.99 340.703 19.817)'/%3E%3C/g%3E%3Cg%3E%3Cpath d='M381 42h18V24h-18v18zm14-16h2v14h-2V26zm-4 0h2v14h-2V26zm-4 0h2v14h-2V26zm-4 0h2v14h-2V26z'/%3E%3Cpath d='M395 20v-4h-10v4h-6v2h22v-2h-6zm-2 0h-6v-2h6v2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A";
.leaflet-touch .leaflet-bar,
.leaflet-draw-actions {
@include euiBottomShadowMedium($color: $euiShadowColorLarge, $opacity: .2);
border: none;
}
.leaflet-container {
background: $euiColorEmptyShade;
//the heatmap layer plugin logs an error to the console when the map is in a 0-sized container
min-width: 1px !important;
min-height: 1px !important;
}
.leaflet-clickable {
&:hover {
stroke-width: $euiSizeS;
stroke-opacity: .8;
}
}
/**
* 1. Since Leaflet is an external library, we also have to provide EUI variables
* to non-override colors for darkmode.
*/
.leaflet-draw-actions,
.leaflet-control {
a {
background-color: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightShade); /* 1 */
border-color: lightOrDarkTheme($euiColorLightShade, $euiColorMediumShade) !important; /* 1 */
color: $euiTextColor !important; /* 1 */
&:hover {
background-color: $euiColorLightestShade;
}
}
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: $euiBorderRadius;
border-top-right-radius: $euiBorderRadius;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: $euiBorderRadius;
border-bottom-right-radius: $euiBorderRadius;
}
.leaflet-retina .leaflet-draw-toolbar a {
background-image: url($visMapLeafletSprite); /* 1 */
}
.leaflet-control-layers-expanded {
padding: 0;
margin: 0;
@include fontSize(11px);
font-family: $euiFontFamily;
font-weight: $euiFontWeightMedium;
line-height: $euiLineHeight;
label {
font-weight: $euiFontWeightMedium;
margin: 0;
padding: 0;
}
}
/* over-rides leaflet popup styles to look like kibana tooltip */
.leaflet-popup-content-wrapper {
margin: 0;
padding: 0;
background: $tempEUITooltipBackground;
color: $tempEUITooltipText;
border-radius: $euiBorderRadius !important; // Override all positions the popup might be at
}
.leaflet-popup {
pointer-events: none;
}
.leaflet-popup-content {
margin: 0;
@include euiFontSizeS;
font-weight: $euiFontWeightRegular;
word-wrap: break-word;
overflow: hidden;
pointer-events: none;
> * {
margin: $euiSizeS $euiSizeS 0;
}
> :last-child {
margin-bottom: $euiSizeS;
}
table {
td,th {
padding: $euiSizeXS;
}
}
}
.leaflet-popup-tip-container,
.leaflet-popup-close-button,
.leaflet-draw-tooltip {
display: none !important;
}
.leaflet-container .leaflet-control-attribution {
background-color: transparentize($euiColorEmptyShade, .7);
color: $euiColorDarkShade;
// attributions are appended in blocks of <p> tags, this will allow them to display in one line
p {
display: inline;
}
}
.leaflet-touch .leaflet-control-zoom-in,
.leaflet-touch .leaflet-control-zoom-out {
text-indent: -10000px;
background-repeat: no-repeat;
background-position: center;
}
// Custom SVG as background for zoom controls based off of EUI glyphs plusInCircleFilled and minusInCircleFilled
.leaflet-touch .leaflet-control-zoom-in {
background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M8,7 L8,3.5 C8,3.22385763 7.77614237,3 7.5,3 C7.22385763,3 7,3.22385763 7,3.5 L7,7 L3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L7,8 L7,11.5 C7,11.7761424 7.22385763,12 7.5,12 C7.77614237,12 8,11.7761424 8,11.5 L8,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L8,7 Z M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z' /%3E%3C/svg%3E");
}
.leaflet-touch .leaflet-control-zoom-out {
background-image: url("data:image/svg+xml,%0A%3Csvg width='15px' height='15px' viewBox='0 0 15 15' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='#{hexToRGB($euiTextColor)}' d='M7.5,0 C11.6355882,0 15,3.36441176 15,7.5 C15,11.6355882 11.6355882,15 7.5,15 C3.36441176,15 0,11.6355882 0,7.5 C0,3.36441176 3.36441176,0 7.5,0 Z M3.5,7 C3.22385763,7 3,7.22385763 3,7.5 C3,7.77614237 3.22385763,8 3.5,8 L11.5,8 C11.7761424,8 12,7.77614237 12,7.5 C12,7.22385763 11.7761424,7 11.5,7 L3.5,7 Z' /%3E%3C/svg%3E");
}
// Filter to desaturate mapquest tiles
img.leaflet-tile {
@if (lightness($euiTextColor) < 50) {
filter: brightness(1.03) grayscale(.73);
} @else {
filter: invert(1) brightness(1.75) grayscale(1);
}
}
img.leaflet-tile.filters-off {
filter: none;
}

View file

@ -1,33 +0,0 @@
.visMapLegend {
@include fontSize(11px);
@include euiBottomShadowMedium($color: $euiShadowColorLarge);
font-family: $euiFontFamily;
font-weight: $euiFontWeightMedium;
line-height: $euiLineHeight;
color: $euiColorDarkShade;
padding: $euiSizeS;
background: transparentize($euiColorEmptyShade, .2);
border-radius: $euiBorderRadius;
i {
@include size($euiSizeS + 2px);
display: inline-block;
margin: 3px $euiSizeXS 0 0;
border-radius: 50%;
border: 1px solid $euiColorDarkShade;
background: $euiColorDarkShade;
}
}
.visMapLegend__title {
font-weight: $euiFontWeightBold;
}
// Wrapper/Position
// top left needs some more styles
.leaflet-top.leaflet-left .visMapLegend__wrapper {
position: absolute;
left: $euiSizeXXL;
white-space: nowrap;
}

View file

@ -1,244 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { filter, first } from 'rxjs/operators';
import {
getEmsTileLayerId,
getUiSettings,
getToasts,
getServiceSettings,
} from '../kibana_services';
import { lazyLoadMapsLegacyModules } from '../lazy_load_bundle';
const WMS_MINZOOM = 0;
const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS
export function BaseMapsVisualizationProvider() {
/**
* Abstract base class for a visualization consisting of a map with a single baselayer.
* @class BaseMapsVisualization
* @constructor
*/
return class BaseMapsVisualization {
constructor(element, handlers, initialVisParams) {
this.handlers = handlers;
this._params = initialVisParams;
this._container = element;
this._kibanaMap = null;
this._chartData = null; //reference to data currently on the map.
this._baseLayerDirty = true;
this._mapIsLoaded = this._makeKibanaMap();
}
isLoaded() {
return this._mapIsLoaded;
}
destroy() {
if (this._kibanaMap) {
this._kibanaMap.destroy();
this._kibanaMap = null;
}
}
/**
* Implementation of Visualization#render.
* Child-classes can extend this method if the render-complete function requires more time until rendering has completed.
* @param esResponse
* @param status
* @return {Promise}
*/
async render(esResponse = this._esResponse, visParams = this._params) {
await this._mapIsLoaded;
if (!this._kibanaMap) {
//the visualization has been destroyed;
return;
}
this.resize();
this._params = visParams;
await this._updateParams();
if (this._hasESResponseChanged(esResponse)) {
this._esResponse = esResponse;
await this._updateData(esResponse);
}
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
await this._whenBaseLayerIsLoaded();
}
resize() {
this._kibanaMap?.resize();
}
/**
* Creates an instance of a kibana-map with a single baselayer and assigns it to the this._kibanaMap property.
* Clients can override this method to customize the initialization.
* @private
*/
async _makeKibanaMap() {
const options = {};
const zoomFromUiState = parseInt(this.handlers.uiState?.get('mapZoom'));
const centerFromUIState = this.handlers.uiState?.get('mapCenter');
const { mapZoom, mapCenter } = this._getMapsParams();
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : mapZoom;
options.center = centerFromUIState ? centerFromUIState : mapCenter;
const modules = await lazyLoadMapsLegacyModules();
this._kibanaMap = new modules.KibanaMap(this._container, options);
this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default
this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default
this._kibanaMap.addLegendControl();
this._kibanaMap.addFitControl();
this._kibanaMap.persistUiStateForVisualization(this.handlers.uiState);
this._kibanaMap.on('baseLayer:loaded', () => {
this._baseLayerDirty = false;
});
this._kibanaMap.on('baseLayer:loading', () => {
this._baseLayerDirty = true;
});
await this._updateBaseLayer();
}
_tmsConfigured() {
const { wms } = this._getMapsParams();
const hasTmsBaseLayer = wms && !!wms.selectedTmsLayer;
return hasTmsBaseLayer;
}
_wmsConfigured() {
const { wms } = this._getMapsParams();
const hasWmsBaseLayer = wms && !!wms.enabled;
return hasWmsBaseLayer;
}
async _updateBaseLayer() {
const emsTileLayerId = getEmsTileLayerId();
if (!this._kibanaMap) {
return;
}
const mapParams = this._getMapsParams();
if (!this._tmsConfigured()) {
try {
const serviceSettings = await getServiceSettings();
const tmsServices = await serviceSettings.getTMSServices();
const userConfiguredTmsLayer = tmsServices[0];
const initBasemapLayer = userConfiguredTmsLayer
? userConfiguredTmsLayer
: tmsServices.find((s) => s.id === emsTileLayerId.bright);
if (initBasemapLayer) {
this._setTmsLayer(initBasemapLayer);
}
} catch (e) {
getToasts().addWarning(e.message);
return;
}
return;
}
try {
if (this._wmsConfigured()) {
if (WMS_MINZOOM > this._kibanaMap.getMaxZoomLevel()) {
this._kibanaMap.setMinZoom(WMS_MINZOOM);
this._kibanaMap.setMaxZoom(WMS_MAXZOOM);
}
this._kibanaMap.setBaseLayer({
baseLayerType: 'wms',
options: {
minZoom: WMS_MINZOOM,
maxZoom: WMS_MAXZOOM,
url: mapParams.wms.url,
...mapParams.wms.options,
},
});
} else if (this._tmsConfigured()) {
const selectedTmsLayer = mapParams.wms.selectedTmsLayer;
this._setTmsLayer(selectedTmsLayer);
}
} catch (tmsLoadingError) {
getToasts().addWarning(tmsLoadingError.message);
}
}
async _setTmsLayer(tmsLayer) {
this._kibanaMap.setMinZoom(tmsLayer.minZoom);
this._kibanaMap.setMaxZoom(tmsLayer.maxZoom);
if (this._kibanaMap.getZoomLevel() > tmsLayer.maxZoom) {
this._kibanaMap.setZoomLevel(tmsLayer.maxZoom);
}
let isDesaturated = this._getMapsParams().isDesaturated;
if (typeof isDesaturated !== 'boolean') {
isDesaturated = true;
}
const isDarkMode = getUiSettings().get('theme:darkMode');
const serviceSettings = await getServiceSettings();
const meta = await serviceSettings.getAttributesForTMSLayer(
tmsLayer,
isDesaturated,
isDarkMode
);
const options = { ...tmsLayer };
delete options.id;
delete options.subdomains;
this._kibanaMap.setBaseLayer({
baseLayerType: 'tms',
options: { ...options, ...meta },
});
}
async _updateData() {
throw new Error(
i18n.translate('maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage', {
defaultMessage: 'Child should implement this method to respond to data-update',
})
);
}
_hasESResponseChanged(data) {
return this._esResponse !== data;
}
/**
* called on options change (vis.params change)
*/
async _updateParams() {
const mapParams = this._getMapsParams();
await this._updateBaseLayer();
this._kibanaMap.setLegendPosition(mapParams.legendPosition);
this._kibanaMap.setShowTooltip(mapParams.addTooltip);
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
}
_getMapsParams() {
return this._params;
}
_whenBaseLayerIsLoaded() {
if (!this._tmsConfigured()) {
return true;
}
const maxTimeForBaseLayer = 10000;
const interval$ = Rx.interval(10).pipe(filter(() => !this._baseLayerDirty));
const timer$ = Rx.timer(maxTimeForBaseLayer);
return Rx.race(interval$, timer$).pipe(first()).toPromise();
}
};
}

View file

@ -1,11 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function getLegendColors(colorRamp: unknown, numLegendColors?: number): string[];
export function getColor(colorRamp: unknown, i: number): string;

View file

@ -1,25 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function getLegendColors(colorRamp, numLegendColors = 4) {
const colors = [];
colors[0] = getColor(colorRamp, 0);
for (let i = 1; i < numLegendColors - 1; i++) {
colors[i] = getColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors));
}
colors[numLegendColors - 1] = getColor(colorRamp, colorRamp.length - 1);
return colors;
}
export function getColor(colorRamp, i) {
const color = colorRamp[i][1];
const red = Math.floor(color[0] * 255);
const green = Math.floor(color[1] * 255);
const blue = Math.floor(color[2] * 255);
return `rgb(${red},${green},${blue})`;
}

View file

@ -1,16 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { geohashColumns } from './geohash_columns';
test('geohashColumns', () => {
expect(geohashColumns(1)).toBe(8);
expect(geohashColumns(2)).toBe(8 * 4);
expect(geohashColumns(3)).toBe(8 * 4 * 8);
expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4);
});

View file

@ -1,27 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function geohashColumns(precision: number): number {
return geohashCells(precision, 0);
}
/**
* Get the number of geohash cells for a given precision
*
* @param {number} precision the geohash precision (1<=precision<=12).
* @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude).
* @returns {number} Number of geohash cells (rows or columns) at that precision
*/
function geohashCells(precision: number, axis: number) {
let cells = 1;
for (let i = 1; i <= precision; i += 1) {
/* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */
cells *= i % 2 === axis ? 4 : 8;
}
return cells;
}

View file

@ -1,2 +0,0 @@
@import './leaflet_overrides';
@import './legend';

View file

@ -1,683 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventEmitter } from 'events';
import $ from 'jquery';
import { get, isEqual, escape } from 'lodash';
import { zoomToPrecision } from './zoom_to_precision';
import { i18n } from '@kbn/i18n';
import { ORIGIN } from '../../../maps_ems/common';
import { L } from '../leaflet';
function makeFitControl(fitContainer, kibanaMap) {
// eslint-disable-next-line no-undef
const FitControl = L.Control.extend({
options: {
position: 'topleft',
},
initialize: function (fitContainer, kibanaMap) {
this._fitContainer = fitContainer;
this._kibanaMap = kibanaMap;
this._leafletMap = null;
},
onAdd: function (leafletMap) {
this._leafletMap = leafletMap;
const fitDatBoundsLabel = i18n.translate(
'maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel',
{ defaultMessage: 'Fit Data Bounds' }
);
$(this._fitContainer)
.html(
`<a class="kuiIcon fa-crop" href="#" title="${fitDatBoundsLabel}" aria-label="${fitDatBoundsLabel}"></a>`
)
.on('click', (e) => {
e.preventDefault();
this._kibanaMap.fitToData();
});
return this._fitContainer;
},
onRemove: function () {
$(this._fitContainer).off('click');
},
});
return new FitControl(fitContainer, kibanaMap);
}
function makeLegendControl(container, kibanaMap, position) {
// eslint-disable-next-line no-undef
const LegendControl = L.Control.extend({
options: {
position: 'topright',
},
initialize: function (container, kibanaMap, position) {
this._legendContainer = container;
this._kibanaMap = kibanaMap;
this.options.position = position;
},
updateContents() {
this._legendContainer.empty();
const $div = $('<div>').addClass('visMapLegend');
this._legendContainer.append($div);
const layers = this._kibanaMap.getLayers();
layers.forEach((layer) => layer.appendLegendContents($div));
},
onAdd: function () {
this._layerUpdateHandle = () => this.updateContents();
this._kibanaMap.on('layers:update', this._layerUpdateHandle);
this.updateContents();
return this._legendContainer.get(0);
},
onRemove: function () {
this._kibanaMap.removeListener('layers:update', this._layerUpdateHandle);
this._legendContainer.empty();
},
});
return new LegendControl(container, kibanaMap, position);
}
/**
* Collects map functionality required for Kibana.
* Serves as simple abstraction for leaflet as well.
*/
export class KibanaMap extends EventEmitter {
constructor(containerNode, options) {
super();
this._containerNode = containerNode;
this._leafletBaseLayer = null;
this._baseLayerSettings = null;
this._baseLayerIsDesaturated = true;
this._leafletDrawControl = null;
this._leafletFitControl = null;
this._leafletLegendControl = null;
this._legendPosition = 'topright';
this._layers = [];
this._listeners = [];
this._showTooltip = false;
const leafletOptions = {
minZoom: options.minZoom,
maxZoom: options.maxZoom,
center: options.center ? options.center : [0, 0],
zoom: options.zoom ? options.zoom : 2,
// eslint-disable-next-line no-undef
renderer: L.canvas(),
zoomAnimation: false, // Desaturate map tiles causes animation rendering artifacts
zoomControl: options.zoomControl === undefined ? true : options.zoomControl,
};
// eslint-disable-next-line no-undef
this._leafletMap = L.map(containerNode, leafletOptions);
this._leafletMap.attributionControl.setPrefix('');
if (!options.scrollWheelZoom) {
this._leafletMap.scrollWheelZoom.disable();
}
let previousZoom = this._leafletMap.getZoom();
this._leafletMap.on('zoomend', () => {
if (previousZoom !== this._leafletMap.getZoom()) {
previousZoom = this._leafletMap.getZoom();
this.emit('zoomchange');
}
});
this._leafletMap.on('zoomend', () => this.emit('zoomend'));
this._leafletMap.on('dragend', () => this.emit('dragend'));
this._leafletMap.on('zoomend', () => this._updateExtent());
this._leafletMap.on('dragend', () => this._updateExtent());
this._leafletMap.on('mousemove', (e) =>
this._layers.forEach((layer) => layer.movePointer('mousemove', e))
);
this._leafletMap.on('mouseout', (e) =>
this._layers.forEach((layer) => layer.movePointer('mouseout', e))
);
this._leafletMap.on('mousedown', (e) =>
this._layers.forEach((layer) => layer.movePointer('mousedown', e))
);
this._leafletMap.on('mouseup', (e) =>
this._layers.forEach((layer) => layer.movePointer('mouseup', e))
);
this._leafletMap.on('draw:created', (event) => {
const drawType = event.layerType;
if (drawType === 'rectangle') {
const bounds = event.layer.getBounds();
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
let southEastLng = southEast.lng;
if (southEastLng > 180) {
southEastLng -= 360;
}
let northWestLng = northWest.lng;
if (northWestLng < -180) {
northWestLng += 360;
}
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
//Bounds cannot be created unless they form a box with larger than 0 dimensions
//Invalid areas are rejected by ES.
if (southEastLat === northWestLat || southEastLng === northWestLng) {
return;
}
this.emit('drawCreated:rectangle', {
bounds: {
bottom_right: {
lat: southEastLat,
lon: southEastLng,
},
top_left: {
lat: northWestLat,
lon: northWestLng,
},
},
});
} else if (drawType === 'polygon') {
const latLongs = event.layer.getLatLngs()[0];
this.emit('drawCreated:polygon', {
points: latLongs.map((leafletLatLng) => {
return {
lat: leafletLatLng.lat,
lon: leafletLatLng.lng,
};
}),
});
}
});
this.resize();
}
setShowTooltip(showTooltip) {
this._showTooltip = showTooltip;
}
getLayers() {
return this._layers.slice();
}
addLayer(kibanaLayer) {
const onshowTooltip = (event) => {
if (!this._showTooltip) {
return;
}
if (!this._popup) {
// eslint-disable-next-line no-undef
this._popup = new L.ResponsivePopup({ autoPan: false });
this._popup.setLatLng(event.position);
this._popup.setContent(event.content);
this._leafletMap.openPopup(this._popup);
} else {
if (!this._popup.getLatLng().equals(event.position)) {
this._popup.setLatLng(event.position);
}
if (this._popup.getContent() !== event.content) {
this._popup.setContent(event.content);
}
}
};
kibanaLayer.on('showTooltip', onshowTooltip);
this._listeners.push({ name: 'showTooltip', handle: onshowTooltip, layer: kibanaLayer });
const onHideTooltip = () => {
this._leafletMap.closePopup();
this._popup = null;
};
kibanaLayer.on('hideTooltip', onHideTooltip);
this._listeners.push({ name: 'hideTooltip', handle: onHideTooltip, layer: kibanaLayer });
const onStyleChanged = () => {
if (this._leafletLegendControl) {
this._leafletLegendControl.updateContents();
}
};
kibanaLayer.on('styleChanged', onStyleChanged);
this._listeners.push({ name: 'styleChanged', handle: onStyleChanged, layer: kibanaLayer });
this._layers.push(kibanaLayer);
kibanaLayer.addToLeafletMap(this._leafletMap);
this.emit('layers:update');
this._addAttributions(kibanaLayer.getAttributions());
}
removeLayer(kibanaLayer) {
if (!kibanaLayer) {
return;
}
this._removeAttributions(kibanaLayer.getAttributions());
const index = this._layers.indexOf(kibanaLayer);
if (index >= 0) {
this._layers.splice(index, 1);
kibanaLayer.removeFromLeafletMap(this._leafletMap);
}
this._listeners.forEach((listener) => {
if (listener.layer === kibanaLayer) {
listener.layer.removeListener(listener.name, listener.handle);
}
});
//must readd all attributions, because we might have removed dupes
this._layers.forEach((layer) => this._addAttributions(layer.getAttributions()));
if (this._baseLayerSettings) {
this._addAttributions(this._baseLayerSettings.options.attribution);
}
}
_addAttributions(attribution) {
const attributions = getAttributionArray(attribution);
attributions.forEach((attribution) => {
this._leafletMap.attributionControl.removeAttribution(attribution); //this ensures we do not add duplicates
this._leafletMap.attributionControl.addAttribution(attribution);
});
}
_removeAttributions(attribution) {
const attributions = getAttributionArray(attribution);
attributions.forEach((attribution) => {
this._leafletMap.attributionControl.removeAttribution(attribution); //this ensures we do not add duplicates
});
}
destroy() {
if (this._leafletFitControl) {
this._leafletMap.removeControl(this._leafletFitControl);
}
if (this._leafletDrawControl) {
this._leafletMap.removeControl(this._leafletDrawControl);
}
if (this._leafletLegendControl) {
this._leafletMap.removeControl(this._leafletLegendControl);
}
this.setBaseLayer(null);
let layer;
while (this._layers.length) {
layer = this._layers.pop();
layer.removeFromLeafletMap(this._leafletMap);
}
this._leafletMap.remove();
this._containerNode.innerHTML = '';
this._listeners.forEach((listener) =>
listener.layer.removeListener(listener.name, listener.handle)
);
}
getCenter() {
const center = this._leafletMap.getCenter();
return { lon: center.lng, lat: center.lat };
}
setCenter(latitude, longitude) {
// eslint-disable-next-line no-undef
const latLong = L.latLng(latitude, longitude);
if (latLong.equals && !latLong.equals(this._leafletMap.getCenter())) {
this._leafletMap.setView(latLong);
}
}
setZoomLevel(zoomLevel) {
if (this._leafletMap.getZoom() !== zoomLevel) {
this._leafletMap.setZoom(zoomLevel);
}
}
getZoomLevel = () => {
return this._leafletMap.getZoom();
};
getMaxZoomLevel = () => {
return this._leafletMap.getMaxZoom();
};
getGeohashPrecision() {
return zoomToPrecision(this._leafletMap.getZoom(), 12, this._leafletMap.getMaxZoom());
}
getLeafletBounds() {
return this._leafletMap.getBounds();
}
getMetersPerPixel() {
const pointC = this._leafletMap.latLngToContainerPoint(this._leafletMap.getCenter()); // center (pixels)
const pointX = [pointC.x + 1, pointC.y]; // add one pixel to x
const pointY = [pointC.x, pointC.y + 1]; // add one pixel to y
const latLngC = this._leafletMap.containerPointToLatLng(pointC);
const latLngX = this._leafletMap.containerPointToLatLng(pointX);
const latLngY = this._leafletMap.containerPointToLatLng(pointY);
const distanceX = latLngC.distanceTo(latLngX); // calculate distance between c and x (latitude)
const distanceY = latLngC.distanceTo(latLngY); // calculate distance between c and y (longitude)
return Math.min(distanceX, distanceY);
}
_getLeafletBounds(resizeOnFail) {
const boundsRaw = this._leafletMap.getBounds();
const bounds = this._leafletMap.wrapLatLngBounds(boundsRaw);
if (!bounds) {
return null;
}
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
if (southEast.lng === northWest.lng || southEast.lat === northWest.lat) {
if (resizeOnFail) {
this._leafletMap.invalidateSize();
return this._getLeafletBounds(false);
} else {
return null;
}
} else {
return bounds;
}
}
getBounds() {
const bounds = this._getLeafletBounds(true);
if (!bounds) {
return null;
}
const southEast = bounds.getSouthEast();
const northWest = bounds.getNorthWest();
const southEastLng = southEast.lng;
const northWestLng = northWest.lng;
const southEastLat = southEast.lat;
const northWestLat = northWest.lat;
// When map has not width or height, the map has no dimensions.
// These dimensions are enforced due to CSS style rules that enforce min-width/height of 0
// that enforcement also resolves errors with the heatmap layer plugin.
return {
bottom_right: {
lat: southEastLat,
lon: southEastLng,
},
top_left: {
lat: northWestLat,
lon: northWestLng,
},
};
}
setDesaturateBaseLayer(isDesaturated) {
if (isDesaturated === this._baseLayerIsDesaturated) {
return;
}
this._baseLayerIsDesaturated = isDesaturated;
this._updateDesaturation();
if (this._leafletBaseLayer) {
this._leafletBaseLayer.redraw();
}
}
addDrawControl() {
const drawColor = '#000';
const drawOptions = {
draw: {
polyline: false,
marker: false,
circle: false,
rectangle: {
shapeOptions: {
stroke: false,
color: drawColor,
},
},
polygon: {
shapeOptions: {
color: drawColor,
},
},
circlemarker: false,
},
};
// eslint-disable-next-line no-undef
this._leafletDrawControl = new L.Control.Draw(drawOptions);
this._leafletMap.addControl(this._leafletDrawControl);
}
addFitControl() {
if (this._leafletFitControl || !this._leafletMap) {
return;
}
// eslint-disable-next-line no-undef
const fitContainer = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-control-fit');
this._leafletFitControl = makeFitControl(fitContainer, this);
this._leafletMap.addControl(this._leafletFitControl);
}
addLegendControl() {
if (this._leafletLegendControl || !this._leafletMap) {
return;
}
this._updateLegend();
}
setLegendPosition(position) {
if (this._legendPosition === position) {
if (!this._leafletLegendControl) {
this._updateLegend();
}
} else {
this._legendPosition = position;
this._updateLegend();
}
}
_updateLegend() {
if (this._leafletLegendControl) {
this._leafletMap.removeControl(this._leafletLegendControl);
}
const $wrapper = $('<div>').addClass('visMapLegend__wrapper');
this._leafletLegendControl = makeLegendControl($wrapper, this, this._legendPosition);
this._leafletMap.addControl(this._leafletLegendControl);
}
resize() {
this._leafletMap.invalidateSize();
this._updateExtent();
}
setMinZoom(zoom) {
this._leafletMap.setMinZoom(zoom);
}
setMaxZoom(zoom) {
this._leafletMap.setMaxZoom(zoom);
}
getLeafletBaseLayer() {
return this._leafletBaseLayer;
}
setBaseLayer(settings) {
if (isEqual(settings, this._baseLayerSettings)) {
return;
}
if (settings === null) {
if (this._leafletBaseLayer && this._leafletMap) {
this._removeAttributions(this._baseLayerSettings.options.attribution);
this._leafletMap.removeLayer(this._leafletBaseLayer);
this._leafletBaseLayer = null;
this._baseLayerSettings = null;
}
return;
}
this._baseLayerSettings = settings;
if (this._leafletBaseLayer) {
this._leafletMap.removeLayer(this._leafletBaseLayer);
this._leafletBaseLayer = null;
}
let baseLayer;
if (settings.baseLayerType === 'wms') {
//This is user-input that is rendered with the Leaflet attribution control. Needs to be sanitized.
this._baseLayerSettings.options.attribution = escape(settings.options.attribution);
baseLayer = this._getWMSBaseLayer(settings.options);
} else if (settings.baseLayerType === 'tms') {
baseLayer = this._getTMSBaseLayer(settings.options);
}
if (baseLayer) {
baseLayer.on('tileload', () => this._updateDesaturation());
baseLayer.on('load', () => {
this.emit('baseLayer:loaded');
});
baseLayer.on('loading', () => {
this.emit('baseLayer:loading');
});
this._leafletBaseLayer = baseLayer;
this._leafletBaseLayer.addTo(this._leafletMap);
this._leafletBaseLayer.bringToBack();
if (settings.options.minZoom > this._leafletMap.getZoom()) {
this._leafletMap.setZoom(settings.options.minZoom);
}
this._addAttributions(settings.options.attribution);
this.resize();
}
}
isInside(bucketRectBounds) {
const mapBounds = this._leafletMap.getBounds();
return mapBounds.intersects(bucketRectBounds);
}
async fitToData() {
if (!this._leafletMap) {
return;
}
const boundsArray = await Promise.all(
this._layers.map(async (layer) => {
return await layer.getBounds();
})
);
let bounds = null;
boundsArray.forEach(async (b) => {
if (bounds) {
bounds.extend(b);
} else {
bounds = b;
}
});
if (bounds && bounds.isValid()) {
this._leafletMap.fitBounds(bounds);
}
}
_getTMSBaseLayer(options) {
// eslint-disable-next-line no-undef
return L.tileLayer(options.url, {
minZoom: options.minZoom,
maxZoom: options.maxZoom,
subdomains: options.subdomains || [],
});
}
_getWMSBaseLayer(options) {
const wmsOptions = {
format: options.format || '',
layers: options.layers || '',
minZoom: options.minZoom,
maxZoom: options.maxZoom,
styles: options.styles || '',
transparent: options.transparent,
version: options.version || '1.3.0',
};
return typeof options.url === 'string' && options.url.length
? // eslint-disable-next-line no-undef
L.tileLayer.wms(options.url, wmsOptions)
: null;
}
_updateExtent() {
this._layers.forEach((layer) => layer.updateExtent());
}
_updateDesaturation() {
const tiles = $('img.leaflet-tile-loaded');
// Don't apply client-side styling to EMS basemaps
if (get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) {
tiles.addClass('filters-off');
} else {
if (this._baseLayerIsDesaturated) {
tiles.removeClass('filters-off');
} else if (!this._baseLayerIsDesaturated) {
tiles.addClass('filters-off');
}
}
}
persistUiStateForVisualization(uiState) {
function persistMapStateInUiState() {
const centerFromUIState = uiState.get('mapCenter');
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) {
uiState.set('mapZoom', this.getZoomLevel());
}
const centerFromMap = this.getCenter();
if (
!centerFromUIState ||
centerFromMap.lon !== centerFromUIState[1] ||
centerFromMap.lat !== centerFromUIState[0]
) {
uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]);
}
}
this.on('dragend', persistMapStateInUiState);
this.on('zoomend', persistMapStateInUiState);
}
useUiStateFromVisualization(uiState) {
const zoomFromUiState = parseInt(uiState?.get('mapZoom'));
const centerFromUIState = uiState?.get('mapCenter');
if (!isNaN(zoomFromUiState)) {
this.setZoomLevel(zoomFromUiState);
}
if (centerFromUIState) {
this.setCenter(centerFromUIState[0], centerFromUIState[1]);
}
}
}
function getAttributionArray(attribution) {
const attributionString = attribution || '';
let attributions = attributionString.split(/\s*\|\s*/);
if (attributions.length === 1) {
//temp work-around due to inconsistency in manifests of how attributions are delimited
attributions = attributions[0].split(',');
}
return attributions;
}

View file

@ -1,25 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export class KibanaMapLayer {
constructor();
getBounds(): Promise<unknown>;
addToLeafletMap(leafletMap: unknown): void;
removeFromLeafletMap(leafletMap: unknown): void;
appendLegendContents(): void;
updateExtent(): void;
movePointer(): void;
getAttributions(): unknown;
}

View file

@ -1,38 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventEmitter } from 'events';
export class KibanaMapLayer extends EventEmitter {
constructor() {
super();
this._leafletLayer = null;
}
async getBounds() {
return this._leafletLayer.getBounds();
}
addToLeafletMap(leafletMap) {
this._leafletLayer.addTo(leafletMap);
}
removeFromLeafletMap(leafletMap) {
leafletMap.removeLayer(this._leafletLayer);
}
appendLegendContents() {}
updateExtent() {}
movePointer() {}
getAttributions() {
return this._attribution;
}
}

View file

@ -1,63 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// @ts-ignore
import { getUiSettings } from '../kibana_services';
import { geohashColumns } from './geohash_columns';
/**
* Get the number of geohash columns (world-wide) for a given precision
* @param precision the geohash precision
* @returns {number} the number of columns
*/
const DEFAULT_PRECISION = 2;
function getMaxPrecision() {
const config = getUiSettings();
return parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12;
}
export function getZoomPrecision() {
/**
* Map Leaflet zoom levels to geohash precision levels.
* The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide.
*/
const zoomPrecision: any = {};
const minGeohashPixels = 16;
const maxPrecision = getMaxPrecision();
for (let zoom = 0; zoom <= 21; zoom += 1) {
const worldPixels = 256 * Math.pow(2, zoom);
zoomPrecision[zoom] = 1;
for (let precision = 2; precision <= maxPrecision; precision += 1) {
const columns = geohashColumns(precision);
if (worldPixels / columns >= minGeohashPixels) {
zoomPrecision[zoom] = precision;
} else {
break;
}
}
}
return zoomPrecision;
}
export function getPrecision(val: string) {
let precision = parseInt(val, 10);
const maxPrecision = getMaxPrecision();
if (Number.isNaN(precision)) {
precision = DEFAULT_PRECISION;
}
if (precision > maxPrecision) {
return maxPrecision;
}
return precision;
}

View file

@ -1,49 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { geohashColumns } from './geohash_columns';
const defaultMaxPrecision = 12;
const minGeoHashPixels = 16;
const calculateZoomToPrecisionMap = (maxZoom: number): Map<number, number> => {
/**
* Map Leaflet zoom levels to geohash precision levels.
* The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide.
*/
const zoomPrecisionMap = new Map();
for (let zoom = 0; zoom <= maxZoom; zoom += 1) {
if (typeof zoomPrecisionMap.get(zoom) === 'number') {
continue;
}
const worldPixels = 256 * Math.pow(2, zoom);
zoomPrecisionMap.set(zoom, 1);
for (let precision = 2; precision <= defaultMaxPrecision; precision += 1) {
const columns = geohashColumns(precision);
if (worldPixels / columns >= minGeoHashPixels) {
zoomPrecisionMap.set(zoom, precision);
} else {
break;
}
}
}
return zoomPrecisionMap;
};
export function zoomToPrecision(mapZoom: number, maxPrecision: number, maxZoom: number) {
const zoomPrecisionMap = calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21);
const precision = zoomPrecisionMap.get(mapZoom);
return precision ? Math.min(precision, maxPrecision) : maxPrecision;
}

View file

@ -1,61 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// @ts-ignore
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public';
// @ts-ignore
import {
setToasts,
setUiSettings,
setMapsEmsConfig,
setGetServiceSettings,
} from './kibana_services';
// @ts-ignore
import { getPrecision, getZoomPrecision } from './map/precision';
import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index';
import { MapsLegacyConfig } from '../config';
// @ts-ignore
import { BaseMapsVisualizationProvider } from './map/base_maps_visualization';
import type { MapsEmsPluginSetup } from '../../maps_ems/public';
/**
* These are the interfaces with your public contracts. You should export these
* for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces.
* @public
*/
export interface MapsLegacySetupDependencies {
mapsEms: MapsEmsPluginSetup;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MapsLegacyStartDependencies {}
export class MapsLegacyPlugin implements Plugin<MapsLegacyPluginSetup, MapsLegacyPluginStart> {
readonly _initializerContext: PluginInitializerContext<MapsLegacyConfig>;
constructor(initializerContext: PluginInitializerContext<MapsLegacyConfig>) {
this._initializerContext = initializerContext;
}
public setup(core: CoreSetup, plugins: MapsLegacySetupDependencies) {
setToasts(core.notifications.toasts);
setUiSettings(core.uiSettings);
setMapsEmsConfig(plugins.mapsEms.config);
setGetServiceSettings(plugins.mapsEms.getServiceSettings);
const getBaseMapsVis = () => new BaseMapsVisualizationProvider();
return {
getZoomPrecision,
getPrecision,
getBaseMapsVis,
};
}
public start(core: CoreStart, plugins: MapsLegacyStartDependencies) {}
}

View file

@ -1,9 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function mapTooltipProvider(element: unknown, formatter: unknown): () => unknown;

View file

@ -1,32 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import ReactDOMServer from 'react-dom/server';
function getToolTipContent(details) {
return ReactDOMServer.renderToStaticMarkup(
<table>
<tbody>
{details.map((detail, i) => (
<tr key={i}>
<td className="visTooltip__label">{detail.label}</td>
<td className="visTooltip__value">{detail.value}</td>
</tr>
))}
</tbody>
</table>
);
}
export function mapTooltipProvider(element, formatter) {
return (...args) => {
const details = formatter(...args);
return details && getToolTipContent(details);
};
}

View file

@ -1,43 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Plugin, PluginConfigDescriptor } from 'kibana/server';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { configSchema, MapsLegacyConfig } from '../config';
import { getUiSettings } from './ui_settings';
export const config: PluginConfigDescriptor<MapsLegacyConfig> = {
exposeToBrowser: {},
schema: configSchema,
};
export interface MapsLegacyPluginSetup {
config: MapsLegacyConfig;
}
export class MapsLegacyPlugin implements Plugin<MapsLegacyPluginSetup> {
readonly _initializerContext: PluginInitializerContext<MapsLegacyConfig>;
constructor(initializerContext: PluginInitializerContext<MapsLegacyConfig>) {
this._initializerContext = initializerContext;
}
public setup(core: CoreSetup) {
core.uiSettings.register(getUiSettings());
const pluginConfig = this._initializerContext.config.get();
return {
config: pluginConfig,
};
}
public start() {}
}
export const plugin = (initializerContext: PluginInitializerContext) =>
new MapsLegacyPlugin(initializerContext);

View file

@ -1,102 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from 'kibana/server';
import { schema } from '@kbn/config-schema';
export function getUiSettings(): Record<string, UiSettingsParams<unknown>> {
return {
'visualization:tileMap:maxPrecision': {
name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionTitle', {
defaultMessage: 'Maximum tile map precision',
}),
value: 7,
description: i18n.translate(
'maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText',
{
defaultMessage:
'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, 12 is the max. {cellDimensionsLink}',
description:
'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.maxPrecisionText + ' +
'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText',
values: {
cellDimensionsLink:
`<a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator"
target="_blank" rel="noopener">` +
i18n.translate(
'maps_legacy.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText',
{
defaultMessage: 'Explanation of cell dimensions',
}
) +
'</a>',
},
}
),
schema: schema.number(),
category: ['visualization'],
},
'visualization:tileMap:WMSdefaults': {
name: i18n.translate('maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsTitle', {
defaultMessage: 'Default WMS properties',
}),
value: JSON.stringify(
{
enabled: false,
url: '',
options: {
version: '',
layers: '',
format: 'image/png',
transparent: true,
attribution: '',
styles: '',
},
},
null,
2
),
type: 'json',
description: i18n.translate(
'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText',
{
defaultMessage:
'Default {propertiesLink} for the WMS map server support in the coordinate map',
description:
'Part of composite text: maps_legacy.advancedSettings.visualization.tileMap.wmsDefaultsText + ' +
'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText',
values: {
propertiesLink:
'<a href="http://leafletjs.com/reference.html#tilelayer-wms" target="_blank" rel="noopener noreferrer">' +
i18n.translate(
'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText',
{
defaultMessage: 'properties',
}
) +
'</a>',
},
}
),
schema: schema.object({
enabled: schema.boolean(),
url: schema.string(),
options: schema.object({
version: schema.string(),
layers: schema.string(),
format: schema.string(),
transparent: schema.boolean(),
attribution: schema.string(),
styles: schema.string(),
}),
}),
category: ['visualization'],
},
};
}

View file

@ -1,14 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": ["public/**/*", "server/**/*", "config.ts"],
"references": [
{ "path": "../vis_default_editor/tsconfig.json" },
{ "path": "../maps_ems/tsconfig.json" }
]
}

View file

@ -1,5 +0,0 @@
# Region map visualization
Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states.
This plugin is targeted for removal in 8.0.

View file

@ -1,13 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/region_map'],
};

View file

@ -1,21 +0,0 @@
{
"id": "regionMap",
"owner": {
"name": "GIS",
"githubTeam": "kibana-gis"
},
"version": "8.0.0",
"kibanaVersion": "kibana",
"ui": true,
"server": true,
"requiredPlugins": [
"visualizations",
"expressions",
"mapsLegacy",
"mapsEms",
"kibanaLegacy",
"data",
"share"
],
"requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"]
}

View file

@ -1,4 +0,0 @@
{
"name": "region_map",
"version": "kibana"
}

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#regionmap returns an object with the correct structure 1`] = `
Object {
"as": "region_map_vis",
"type": "render",
"value": Object {
"visConfig": Object {
"addTooltip": true,
"colorSchema": "Yellow to Red",
"emsHotLink": "",
"isDisplayWarning": true,
"legendPosition": "bottomright",
"mapCenter": Array [
0,
0,
],
"mapZoom": 2,
"metric": Object {
"accessor": 0,
"aggType": "count",
"format": Object {
"id": "number",
},
"params": Object {},
},
"outlineWeight": 1,
"selectedJoinField": null,
"showAllShapes": true,
"wms": Object {
"enabled": false,
"options": Object {
"format": "image/png",
"transparent": true,
},
},
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
},
"visType": "region_map",
},
}
`;

View file

@ -1,500 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import $ from 'jquery';
import _ from 'lodash';
import d3 from 'd3';
import { i18n } from '@kbn/i18n';
import * as topojson from 'topojson-client';
import { getNotifications } from './kibana_services';
import { colorUtil, KibanaMapLayer } from '../../maps_legacy/public';
import { truncatedColorMaps } from '../../charts/public';
const EMPTY_STYLE = {
weight: 1,
opacity: 0.6,
color: 'rgb(200,200,200)',
fillOpacity: 0,
};
export class ChoroplethLayer extends KibanaMapLayer {
static _doInnerJoin(sortedMetrics, sortedGeojsonFeatures, joinField) {
let j = 0;
for (let i = 0; i < sortedGeojsonFeatures.length; i++) {
const property = sortedGeojsonFeatures[i].properties[joinField];
sortedGeojsonFeatures[i].__kbnJoinedMetric = null;
const position = sortedMetrics.length
? compareLexicographically(property, sortedMetrics[j].term)
: -1;
if (position === -1) {
//just need to cycle on
} else if (position === 0) {
sortedGeojsonFeatures[i].__kbnJoinedMetric = sortedMetrics[j];
} else if (position === 1) {
//needs to catch up
while (j < sortedMetrics.length) {
const newTerm = sortedMetrics[j].term;
const newPosition = compareLexicographically(newTerm, property);
if (newPosition === -1) {
//not far enough
} else if (newPosition === 0) {
sortedGeojsonFeatures[i].__kbnJoinedMetric = sortedMetrics[j];
break;
} else if (newPosition === 1) {
//too far!
break;
}
if (j === sortedMetrics.length - 1) {
//always keep a reference to the last metric
break;
} else {
j++;
}
}
}
}
}
constructor(
name,
attribution,
format,
showAllShapes,
meta,
layerConfig,
serviceSettings,
leaflet
) {
super();
this._serviceSettings = serviceSettings;
this._metrics = null;
this._joinField = null;
this._colorRamp = truncatedColorMaps[Object.keys(truncatedColorMaps)[0]].value;
this._lineWeight = 1;
this._tooltipFormatter = () => '';
this._attribution = attribution;
this._boundsOfData = null;
this._showAllShapes = showAllShapes;
this._layerName = name;
this._layerConfig = layerConfig;
this._leaflet = leaflet;
// eslint-disable-next-line no-undef
this._leafletLayer = this._leaflet.geoJson(null, {
onEachFeature: (feature, layer) => {
layer.on('click', () => {
this.emit('select', feature.properties[this._joinField]);
});
let location = null;
layer.on({
mouseover: () => {
const tooltipContents = this._tooltipFormatter(feature);
if (!location) {
// eslint-disable-next-line no-undef
const leafletGeojson = this._leaflet.geoJson(feature);
location = leafletGeojson.getBounds().getCenter();
}
this.emit('showTooltip', {
content: tooltipContents,
position: location,
});
},
mouseout: () => {
this.emit('hideTooltip');
},
});
},
style: this._makeEmptyStyleFunction(),
});
this._loaded = false;
this._error = false;
this._isJoinValid = false;
this._whenDataLoaded = new Promise(async (resolve) => {
try {
const data = await this._makeJsonAjaxCall();
let featureCollection;
let formatType;
if (typeof format === 'string') {
formatType = format;
} else if (format && format.type) {
formatType = format.type;
} else {
formatType = 'geojson';
}
if (formatType === 'geojson') {
featureCollection = data;
} else if (formatType === 'topojson') {
const features = _.get(data, 'objects.' + meta.feature_collection_path);
featureCollection = topojson.feature(data, features); //conversion to geojson
} else {
//should never happen
throw new Error(
i18n.translate('regionMap.choroplethLayer.unrecognizedFormatErrorMessage', {
defaultMessage: 'Unrecognized format {formatType}',
values: { formatType },
})
);
}
this._sortedFeatures = featureCollection.features.slice();
this._sortFeatures();
if (showAllShapes) {
this._leafletLayer.addData(featureCollection);
} else {
//we need to delay adding the data until we have performed the join and know which features
//should be displayed
}
this._loaded = true;
this._setStyle();
resolve();
} catch (e) {
this._loaded = true;
this._error = true;
let errorMessage;
if (e.status === 404) {
errorMessage = i18n.translate(
'regionMap.choroplethLayer.downloadingVectorData404ErrorMessage',
{
defaultMessage:
"Server responding with '404' when attempting to fetch {name}. \
Make sure the file exists at that location.",
values: { name: name },
}
);
} else {
errorMessage = i18n.translate(
'regionMap.choroplethLayer.downloadingVectorDataErrorMessage',
{
defaultMessage:
'Cannot download {name} file. Please ensure the \
CORS configuration of the server permits requests from the Kibana application on this host.',
values: { name: name },
}
);
}
getNotifications().toasts.addDanger({
title: i18n.translate(
'regionMap.choroplethLayer.downloadingVectorDataErrorMessageTitle',
{
defaultMessage: 'Error downloading vector data',
}
),
text: errorMessage,
});
resolve();
}
});
}
//This method is stubbed in the tests to avoid network request during unit tests.
async _makeJsonAjaxCall() {
return this._serviceSettings.getJsonForRegionLayer(this._layerConfig);
}
_invalidateJoin() {
this._isJoinValid = false;
}
_doInnerJoin() {
ChoroplethLayer._doInnerJoin(this._metrics, this._sortedFeatures, this._joinField);
this._isJoinValid = true;
}
_setStyle() {
if (this._error || !this._loaded || !this._metrics || !this._joinField) {
return;
}
if (!this._isJoinValid) {
this._doInnerJoin();
if (!this._showAllShapes) {
const featureCollection = {
type: 'FeatureCollection',
features: this._sortedFeatures.filter((feature) => feature.__kbnJoinedMetric),
};
this._leafletLayer.addData(featureCollection);
}
}
const styler = this._makeChoroplethStyler();
this._leafletLayer.setStyle(styler.leafletStyleFunction);
if (this._metrics && this._metrics.length > 0) {
const { min, max } = getMinMax(this._metrics);
this._legendColors = colorUtil.getLegendColors(this._colorRamp);
const quantizeDomain = min !== max ? [min, max] : d3.scale.quantize().domain();
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
}
this._boundsOfData = styler.getLeafletBounds();
this.emit('styleChanged', {
mismatches: styler.getMismatches(),
});
}
getUrl() {
return this._layerName;
}
setTooltipFormatter(tooltipFormatter, fieldFormatter, fieldName, metricLabel) {
this._tooltipFormatter = (geojsonFeature) => {
if (!this._metrics) {
return '';
}
const match = this._metrics.find((bucket) => {
return (
compareLexicographically(bucket.term, geojsonFeature.properties[this._joinField]) === 0
);
});
return tooltipFormatter(match, fieldFormatter, fieldName, metricLabel);
};
}
setJoinField(joinfield) {
if (joinfield === this._joinField) {
return;
}
this._joinField = joinfield;
this._sortFeatures();
this._setStyle();
}
cloneChoroplethLayerForNewData(
name,
attribution,
format,
showAllData,
meta,
layerConfig,
serviceSettings,
leaflet
) {
const clonedLayer = new ChoroplethLayer(
name,
attribution,
format,
showAllData,
meta,
layerConfig,
serviceSettings,
leaflet
);
clonedLayer.setJoinField(this._joinField);
clonedLayer.setColorRamp(this._colorRamp);
clonedLayer.setLineWeight(this._lineWeight);
clonedLayer.setTooltipFormatter(this._tooltipFormatter);
if (this._metrics) {
clonedLayer.setMetrics(this._metrics, this._valueFormatter, this._metricTitle);
}
return clonedLayer;
}
_sortFeatures() {
if (this._sortedFeatures && this._joinField) {
this._sortedFeatures.sort((a, b) => {
const termA = a.properties[this._joinField];
const termB = b.properties[this._joinField];
return compareLexicographically(termA, termB);
});
this._invalidateJoin();
}
}
whenDataLoaded() {
return this._whenDataLoaded;
}
setMetrics(metrics, fieldFormatter, metricTitle) {
this._metrics = metrics.slice();
this._valueFormatter = fieldFormatter;
this._metricTitle = metricTitle;
this._metrics.sort((a, b) => compareLexicographically(a.term, b.term));
this._invalidateJoin();
this._setStyle();
}
setColorRamp(colorRamp) {
if (_.isEqual(colorRamp, this._colorRamp)) {
return;
}
this._colorRamp = colorRamp;
this._setStyle();
}
setLineWeight(lineWeight) {
if (this._lineWeight === lineWeight) {
return;
}
this._lineWeight = lineWeight;
this._setStyle();
}
canReuseInstance(name, showAllShapes) {
return this._layerName === name && this._showAllShapes === showAllShapes;
}
canReuseInstanceForNewMetrics(name, showAllShapes, newMetrics) {
if (this._layerName !== name) {
return false;
}
if (showAllShapes) {
return this._showAllShapes === showAllShapes;
}
if (!this._metrics) {
return;
}
const currentKeys = Object.keys(this._metrics);
const newKeys = Object.keys(newMetrics);
return _.isEqual(currentKeys, newKeys);
}
getBounds() {
const bounds = super.getBounds();
return this._boundsOfData ? this._boundsOfData : bounds;
}
appendLegendContents(jqueryDiv) {
if (!this._legendColors || !this._legendQuantizer) {
return;
}
const titleText = this._metricTitle;
const $title = $('<div>').addClass('visMapLegend__title').text(titleText);
jqueryDiv.append($title);
this._legendColors.forEach((color) => {
const labelText = this._legendQuantizer
.invertExtent(color)
.map((val) => {
return this._valueFormatter.convert(val);
})
.join('  ');
const label = $('<div>');
const icon = $('<i>').css({
background: color,
'border-color': makeColorDarker(color),
});
const text = $('<span>').text(labelText);
label.append(icon);
label.append(text);
jqueryDiv.append(label);
});
}
_makeEmptyStyleFunction() {
const emptyStyle = _.assign({}, EMPTY_STYLE, {
weight: this._lineWeight,
});
return () => {
return emptyStyle;
};
}
_makeChoroplethStyler() {
const emptyStyle = this._makeEmptyStyleFunction();
if (this._metrics.length === 0) {
return {
leafletStyleFunction: () => {
return emptyStyle();
},
getMismatches: () => {
return [];
},
getLeafletBounds: () => {
return null;
},
};
}
const { min, max } = getMinMax(this._metrics);
// eslint-disable-next-line no-undef
const boundsOfAllFeatures = new this._leaflet.LatLngBounds();
return {
leafletStyleFunction: (geojsonFeature) => {
const match = geojsonFeature.__kbnJoinedMetric;
if (!match) {
return emptyStyle();
}
// eslint-disable-next-line no-undef
const boundsOfFeature = this._leaflet.geoJson(geojsonFeature).getBounds();
boundsOfAllFeatures.extend(boundsOfFeature);
return {
fillColor: getChoroplethColor(match.value, min, max, this._colorRamp),
weight: this._lineWeight,
opacity: 1,
color: 'white',
fillOpacity: 0.7,
};
},
/**
* should not be called until getLeafletStyleFunction has been called
* @return {Array}
*/
getMismatches: () => {
const mismatches = this._metrics.slice();
this._sortedFeatures.forEach((feature) => {
const index = mismatches.indexOf(feature.__kbnJoinedMetric);
if (index >= 0) {
mismatches.splice(index, 1);
}
});
return mismatches.map((b) => b.term);
},
getLeafletBounds: function () {
return boundsOfAllFeatures.isValid() ? boundsOfAllFeatures : null;
},
};
}
}
//lexicographic compare
function compareLexicographically(termA, termB) {
termA = typeof termA === 'string' ? termA : termA.toString();
termB = typeof termB === 'string' ? termB : termB.toString();
return termA.localeCompare(termB);
}
function makeColorDarker(color) {
const amount = 1.3; //magic number, carry over from earlier
return d3.hcl(color).darker(amount).toString();
}
function getMinMax(data) {
let min = data[0].value;
let max = data[0].value;
for (let i = 1; i < data.length; i += 1) {
min = Math.min(data[i].value, min);
max = Math.max(data[i].value, max);
}
return { min, max };
}
function getChoroplethColor(value, min, max, colorRamp) {
if (min === max) {
return colorUtil.getColor(colorRamp, colorRamp.length - 1);
}
const fraction = (value - min) / (max - min);
const index = Math.round(colorRamp.length * fraction) - 1;
const i = Math.max(Math.min(colorRamp.length - 1, index), 0);
return colorUtil.getColor(colorRamp, i);
}

View file

@ -1,18 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { IServiceSettings } from 'src/plugins/maps_ems/public';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
import { RegionMapVisParams } from '../region_map_types';
const RegionMapOptions = lazy(() => import('./region_map_options'));
export const createRegionMapOptions = (getServiceSettings: () => Promise<IServiceSettings>) => (
props: VisEditorOptionsProps<RegionMapVisParams>
) => <RegionMapOptions {...props} getServiceSettings={getServiceSettings} />;

View file

@ -1,211 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback, useMemo } from 'react';
import { EuiIcon, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
import { truncatedColorSchemas } from '../../../charts/public';
import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_ems/public';
import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public';
import { WmsOptions } from '../../../maps_legacy/public';
import { RegionMapVisParams } from '../region_map_types';
import { getTmsLayers, getVectorLayers } from '../kibana_services';
const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({
text: name,
value: layerId,
});
const mapFieldForOption = ({ description, name }: FileLayerField) => ({
text: description,
value: name,
});
const tmsLayers = getTmsLayers();
const vectorLayers = getVectorLayers();
const vectorLayerOptions = vectorLayers.map(mapLayerForOption);
export type RegionMapOptionsProps = {
getServiceSettings: () => Promise<IServiceSettings>;
} & VisEditorOptionsProps<RegionMapVisParams>;
function RegionMapOptions(props: RegionMapOptionsProps) {
const { getServiceSettings, stateParams, setValue } = props;
const fieldOptions = useMemo(
() =>
((stateParams.selectedLayer && stateParams.selectedLayer.fields) || []).map(
mapFieldForOption
),
[stateParams.selectedLayer]
);
const setEmsHotLink = useCallback(
async (layer: VectorLayer) => {
const serviceSettings = await getServiceSettings();
const emsHotLink = await serviceSettings.getEMSHotLink(layer);
setValue('emsHotLink', emsHotLink);
},
[setValue, getServiceSettings]
);
const setLayer = useCallback(
async (paramName: 'selectedLayer', value: VectorLayer['layerId']) => {
const newLayer = vectorLayers.find(({ layerId }: VectorLayer) => layerId === value);
if (newLayer) {
setValue(paramName, newLayer);
setValue('selectedJoinField', newLayer.fields[0]);
setEmsHotLink(newLayer);
}
},
[setEmsHotLink, setValue]
);
const setField = useCallback(
(paramName: 'selectedJoinField', value: FileLayerField['name']) => {
if (stateParams.selectedLayer) {
setValue(
paramName,
stateParams.selectedLayer.fields.find((f) => f.name === value)
);
}
},
[setValue, stateParams.selectedLayer]
);
return (
<>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.layerSettingsTitle"
defaultMessage="Layer settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<SelectOption
id="regionMapOptionsSelectLayer"
label={i18n.translate('regionMap.visParams.vectorMapLabel', {
defaultMessage: 'Vector map',
})}
labelAppend={
stateParams.emsHotLink && (
<EuiText size="xs">
<EuiLink
href={stateParams.emsHotLink}
target="_blank"
title={i18n.translate('regionMap.visParams.previewOnEMSLinkTitle', {
defaultMessage: 'Preview {selectedLayerName} on the Elastic Maps Service',
values: {
selectedLayerName:
stateParams.selectedLayer && stateParams.selectedLayer.name,
},
})}
>
<FormattedMessage
id="regionMap.visParams.previewOnEMSLinkText"
defaultMessage="Preview on EMS"
/>{' '}
<EuiIcon type="popout" size="s" />
</EuiLink>
</EuiText>
)
}
options={vectorLayerOptions}
paramName="selectedLayer"
value={stateParams.selectedLayer && stateParams.selectedLayer.layerId}
setValue={setLayer}
/>
<SelectOption
id="regionMapOptionsSelectJoinField"
label={i18n.translate('regionMap.visParams.joinFieldLabel', {
defaultMessage: 'Join field',
})}
options={fieldOptions}
paramName="selectedJoinField"
value={stateParams.selectedJoinField && stateParams.selectedJoinField.name}
setValue={setField}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.displayWarningsLabel', {
defaultMessage: 'Display warnings',
})}
tooltip={i18n.translate('regionMap.visParams.switchWarningsTipText', {
defaultMessage:
'Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.',
})}
paramName="isDisplayWarning"
value={stateParams.isDisplayWarning}
setValue={setValue}
/>
<SwitchOption
label={i18n.translate('regionMap.visParams.showAllShapesLabel', {
defaultMessage: 'Show all shapes',
})}
tooltip={i18n.translate('regionMap.visParams.turnOffShowingAllShapesTipText', {
defaultMessage:
'Turning this off only shows the shapes that were matched with a corresponding term.',
})}
paramName="showAllShapes"
value={stateParams.showAllShapes}
setValue={setValue}
/>
</EuiPanel>
<EuiSpacer size="s" />
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h2>
<FormattedMessage
id="regionMap.visParams.styleSettingsLabel"
defaultMessage="Style settings"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<SelectOption
label={i18n.translate('regionMap.visParams.colorSchemaLabel', {
defaultMessage: 'Color schema',
})}
options={truncatedColorSchemas}
paramName="colorSchema"
value={stateParams.colorSchema}
setValue={setValue}
/>
<NumberInputOption
label={i18n.translate('regionMap.visParams.outlineWeightLabel', {
defaultMessage: 'Border thickness',
})}
min={0}
paramName="outlineWeight"
value={stateParams.outlineWeight}
setValue={setValue}
/>
</EuiPanel>
<EuiSpacer size="s" />
<WmsOptions setValue={setValue} stateParams={stateParams} tmsLayers={tmsLayers} />
</>
);
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { RegionMapOptions as default };

View file

@ -1,73 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { getQueryService, getShareService } from './kibana_services';
import { Vis } from '../../visualizations/public';
import { LegacyMapDeprecationMessage } from '../../maps_legacy/public';
function getEmsLayerId(id: string | number, layerId: string) {
if (typeof id === 'string') {
return id;
}
// Region maps from 6.x will have numerical EMS id refering to S3 bucket id.
// In this case, use layerId with contains the EMS layer name.
const split = layerId.split('.');
return split.length === 2 ? split[1] : undefined;
}
export function getDeprecationMessage(vis: Vis) {
const title = i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' });
async function onClick(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
const locator = getShareService().url.locators.get('MAPS_APP_REGION_MAP_LOCATOR');
if (!locator) return;
const query = getQueryService();
const params: { [key: string]: any } = {
label: vis.title ? vis.title : title,
emsLayerId: vis.params.selectedLayer.isEMS
? getEmsLayerId(vis.params.selectedLayer.id, vis.params.selectedLayer.layerId)
: undefined,
leftFieldName: vis.params.selectedLayer.isEMS ? vis.params.selectedJoinField.name : undefined,
colorSchema: vis.params.colorSchema,
indexPatternId: vis.data.indexPattern?.id,
indexPatternTitle: vis.data.indexPattern?.title,
metricAgg: 'count',
filters: query.filterManager.getFilters(),
query: query.queryString.getQuery(),
timeRange: query.timefilter.timefilter.getTime(),
};
const bucketAggs = vis.data?.aggs?.byType('buckets');
if (bucketAggs?.length && bucketAggs[0].type.dslName === 'terms') {
params.termsFieldName = bucketAggs[0].getField()?.name;
params.termsSize = bucketAggs[0].getParam('size');
}
const metricAggs = vis.data?.aggs?.byType('metrics');
if (metricAggs?.length) {
params.metricAgg = metricAggs[0].type.dslName;
params.metricFieldName = metricAggs[0].getField()?.name;
}
locator.navigate(params);
}
return (
<LegacyMapDeprecationMessage
isMapsAvailable={!!getShareService().url.locators.get('MAPS_APP_REGION_MAP_LOCATOR')}
onClick={onClick}
visualizationLabel={title}
/>
);
}

View file

@ -1,19 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginInitializerContext } from 'kibana/public';
import { RegionMapPlugin as Plugin } from './plugin';
export interface RegionMapsConfigType {
includeElasticMapsService: boolean;
layers: any[];
}
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
}

View file

@ -1,39 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreStart } from 'kibana/public';
import { NotificationsStart } from 'kibana/public';
import { createGetterSetter } from '../../kibana_utils/public';
import { DataPublicPluginStart } from '../../data/public';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { VectorLayer, TmsLayer } from '../../maps_ems/public';
export const [getCoreService, setCoreService] = createGetterSetter<CoreStart>('Core');
export const [getFormatService, setFormatService] = createGetterSetter<
DataPublicPluginStart['fieldFormats']
>('data.fieldFormats');
export const [getNotifications, setNotifications] = createGetterSetter<NotificationsStart>(
'Notifications'
);
export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
export const [getShareService, setShareService] = createGetterSetter<SharePluginStart>('Share');
export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter<KibanaLegacyStart>(
'KibanaLegacy'
);
export const [getTmsLayers, setTmsLayers] = createGetterSetter<TmsLayer[]>('TmsLayers');
export const [getVectorLayers, setVectorLayers] = createGetterSetter<VectorLayer[]>('VectorLayers');

View file

@ -1,120 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
IUiSettingsClient,
NotificationsStart,
} from 'kibana/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { VisualizationsSetup } from '../../visualizations/public';
// @ts-ignore
import { createRegionMapFn } from './region_map_fn';
// @ts-ignore
import { createRegionMapTypeDefinition } from './region_map_type';
import { MapsLegacyPluginSetup } from '../../maps_legacy/public';
import { IServiceSettings, MapsEmsPluginSetup } from '../../maps_ems/public';
import {
setCoreService,
setFormatService,
setNotifications,
setKibanaLegacy,
setQueryService,
setShareService,
} from './kibana_services';
import { DataPublicPluginStart } from '../../data/public';
import { RegionMapsConfigType } from './index';
import { MapsLegacyConfig } from '../../maps_legacy/config';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { getRegionMapRenderer } from './region_map_renderer';
/** @private */
export interface RegionMapVisualizationDependencies {
uiSettings: IUiSettingsClient;
regionmapsConfig: RegionMapsConfig;
getServiceSettings: () => Promise<IServiceSettings>;
BaseMapsVisualization: any;
}
/** @internal */
export interface RegionMapPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
mapsLegacy: MapsLegacyPluginSetup;
mapsEms: MapsEmsPluginSetup;
}
/** @internal */
export interface RegionMapPluginStartDependencies {
data: DataPublicPluginStart;
notifications: NotificationsStart;
kibanaLegacy: KibanaLegacyStart;
share: SharePluginStart;
}
/** @internal */
export interface RegionMapsConfig {
includeElasticMapsService: boolean;
layers: any[];
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RegionMapPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RegionMapPluginStart {}
/** @internal */
export class RegionMapPlugin implements Plugin<RegionMapPluginSetup, RegionMapPluginStart> {
readonly _initializerContext: PluginInitializerContext<MapsLegacyConfig>;
constructor(initializerContext: PluginInitializerContext) {
this._initializerContext = initializerContext;
}
public setup(
core: CoreSetup,
{ expressions, visualizations, mapsLegacy, mapsEms }: RegionMapPluginSetupDependencies
) {
const config = {
...this._initializerContext.config.get<RegionMapsConfigType>(),
// The maps legacy plugin updates the regionmap config directly in service_settings,
// future work on how configurations across the different plugins are organized would
// ideally constrain regionmap config updates to occur only from this plugin
...mapsEms.config.regionmap,
};
const visualizationDependencies: Readonly<RegionMapVisualizationDependencies> = {
uiSettings: core.uiSettings,
regionmapsConfig: config as RegionMapsConfig,
getServiceSettings: mapsEms.getServiceSettings,
BaseMapsVisualization: mapsLegacy.getBaseMapsVis(),
};
expressions.registerFunction(createRegionMapFn);
expressions.registerRenderer(getRegionMapRenderer(visualizationDependencies));
visualizations.createBaseVisualization(
createRegionMapTypeDefinition(visualizationDependencies)
);
return {};
}
public start(core: CoreStart, plugins: RegionMapPluginStartDependencies) {
setCoreService(core);
setFormatService(plugins.data.fieldFormats);
setQueryService(plugins.data.query);
setNotifications(core.notifications);
setKibanaLegacy(plugins.kibanaLegacy);
setShareService(plugins.share);
return {};
}
}

View file

@ -1,51 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createRegionMapFn } from './region_map_fn';
describe('interpreter/functions#regionmap', () => {
const fn = functionWrapper(createRegionMapFn());
const context = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
const visConfig = {
legendPosition: 'bottomright',
addTooltip: true,
colorSchema: 'Yellow to Red',
emsHotLink: '',
selectedJoinField: null,
isDisplayWarning: true,
wms: {
enabled: false,
options: {
format: 'image/png',
transparent: true,
},
},
mapZoom: 2,
mapCenter: [0, 0],
outlineWeight: 1,
showAllShapes: true,
metric: {
accessor: 0,
format: {
id: 'number',
},
params: {},
aggType: 'count',
},
};
it('returns an object with the correct structure', () => {
const actual = fn(context, { visConfig: JSON.stringify(visConfig) });
expect(actual).toMatchSnapshot();
});
});

View file

@ -1,63 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { RegionMapVisConfig } from './region_map_types';
interface Arguments {
visConfig: string | null;
}
export interface RegionMapVisRenderValue {
visData: Datatable;
visType: 'region_map';
visConfig: RegionMapVisConfig;
}
export type RegionMapExpressionFunctionDefinition = ExpressionFunctionDefinition<
'regionmap',
Datatable,
Arguments,
Render<RegionMapVisRenderValue>
>;
export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({
name: 'regionmap',
type: 'render',
context: {
types: ['datatable'],
},
help: i18n.translate('regionMap.function.help', {
defaultMessage: 'Regionmap visualization',
}),
args: {
visConfig: {
types: ['string', 'null'],
default: '"{}"',
help: '',
},
},
fn(context, args, handlers) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'region_map_vis',
value: {
visData: context,
visType: 'region_map',
visConfig,
},
};
},
});

View file

@ -1,41 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { ExpressionRenderDefinition } from 'src/plugins/expressions';
import { VisualizationContainer } from '../../visualizations/public';
import { RegionMapVisualizationDependencies } from './plugin';
import { RegionMapVisRenderValue } from './region_map_fn';
const RegionMapVisualization = lazy(() => import('./region_map_visualization_component'));
export const getRegionMapRenderer: (
deps: RegionMapVisualizationDependencies
) => ExpressionRenderDefinition<RegionMapVisRenderValue> = (deps) => ({
name: 'region_map_vis',
reuseDomNode: true,
render: async (domNode, { visConfig, visData }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<VisualizationContainer handlers={handlers}>
<RegionMapVisualization
deps={deps}
handlers={handlers}
visConfig={visConfig}
visData={visData}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -1,143 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { VisTypeDefinition } from '../../visualizations/public';
import { ORIGIN, VectorLayer } from '../../maps_ems/public';
import { getDeprecationMessage } from './get_deprecation_message';
import { RegionMapVisualizationDependencies } from './plugin';
import { createRegionMapOptions } from './components';
import { toExpressionAst } from './to_ast';
import { RegionMapVisParams } from './region_map_types';
import { mapToLayerWithId } from './util';
import { setTmsLayers, setVectorLayers } from './kibana_services';
export function createRegionMapTypeDefinition({
uiSettings,
regionmapsConfig,
getServiceSettings,
}: RegionMapVisualizationDependencies): VisTypeDefinition<RegionMapVisParams> {
return {
name: 'region_map',
getInfoMessage: getDeprecationMessage,
title: i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }),
description: i18n.translate('regionMap.mapVis.regionMapDescription', {
defaultMessage:
'Show metrics on a thematic map. Use one of the \
provided base maps, or add your own. Darker colors represent higher values.',
}),
icon: 'visMapRegion',
visConfig: {
defaults: {
legendPosition: 'bottomright',
addTooltip: true,
colorSchema: 'Yellow to Red',
emsHotLink: '',
isDisplayWarning: true,
wms: uiSettings.get('visualization:tileMap:WMSdefaults'),
mapZoom: 2,
mapCenter: [0, 0],
outlineWeight: 1,
showAllShapes: true, // still under consideration
},
},
editorConfig: {
optionsTemplate: createRegionMapOptions(getServiceSettings),
schemas: [
{
group: 'metrics',
name: 'metric',
title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', {
defaultMessage: 'Value',
}),
min: 1,
max: 1,
aggFilter: [
'count',
'avg',
'sum',
'min',
'max',
'cardinality',
'top_hits',
'sum_bucket',
'min_bucket',
'max_bucket',
'avg_bucket',
],
defaults: [{ schema: 'metric', type: 'count' }],
},
{
group: 'buckets',
name: 'segment',
title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', {
defaultMessage: 'Shape field',
}),
min: 1,
max: 1,
aggFilter: ['terms'],
},
],
},
toExpressionAst,
setup: async (vis) => {
const serviceSettings = await getServiceSettings();
const tmsLayers = await serviceSettings.getTMSServices();
setTmsLayers(tmsLayers);
setVectorLayers([]);
if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) {
vis.params.wms.selectedTmsLayer = tmsLayers[0];
}
const vectorLayers = regionmapsConfig.layers.map(
mapToLayerWithId.bind(null, ORIGIN.KIBANA_YML)
);
let selectedLayer = vectorLayers[0];
let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined;
if (regionmapsConfig.includeElasticMapsService) {
const layers = await serviceSettings.getFileLayers();
const newLayers = layers
.map(mapToLayerWithId.bind(null, ORIGIN.EMS))
.filter(
(layer: VectorLayer) =>
!vectorLayers.some((vectorLayer) => vectorLayer.layerId === layer.layerId)
);
// backfill v1 manifest for now
newLayers.forEach((layer: VectorLayer) => {
if (layer.format === 'geojson') {
layer.format = {
type: 'geojson',
};
}
});
const allVectorLayers = [...vectorLayers, ...newLayers];
setVectorLayers(allVectorLayers);
[selectedLayer] = allVectorLayers;
selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined;
if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) {
vis.params.emsHotLink = await serviceSettings.getEMSHotLink(selectedLayer);
}
}
if (!vis.params.selectedLayer) {
vis.params.selectedLayer = selectedLayer;
vis.params.selectedJoinField = selectedJoinField;
}
return vis;
},
requiresSearch: true,
};
}

View file

@ -1,31 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SchemaConfig } from 'src/plugins/visualizations/public';
import { VectorLayer, FileLayerField } from '../../maps_ems/public';
import { WMSOptions } from '../../maps_legacy/public';
export interface RegionMapVisParams {
readonly addTooltip: true;
readonly legendPosition: 'bottomright';
colorSchema: string;
emsHotLink?: string | null;
mapCenter: [number, number];
mapZoom: number;
outlineWeight: number | '';
isDisplayWarning: boolean;
showAllShapes: boolean;
selectedLayer?: VectorLayer;
selectedJoinField?: FileLayerField;
wms: WMSOptions;
}
export interface RegionMapVisConfig extends RegionMapVisParams {
metric: SchemaConfig;
bucket?: SchemaConfig;
}

View file

@ -1,239 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services';
import { truncatedColorMaps } from '../../charts/public';
import { tooltipFormatter } from './tooltip_formatter';
import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public';
import { ORIGIN } from '../../maps_ems/public';
export function createRegionMapVisualization({
regionmapsConfig,
uiSettings,
BaseMapsVisualization,
getServiceSettings,
}) {
return class RegionMapsVisualization extends BaseMapsVisualization {
constructor(container, handlers, initialVisParams) {
super(container, handlers, initialVisParams);
this._choroplethLayer = null;
this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter);
}
async render(esResponse, visParams) {
getKibanaLegacy().loadFontAwesome();
await super.render(esResponse, visParams);
if (this._choroplethLayer) {
await this._choroplethLayer.whenDataLoaded();
}
}
async _updateData(table) {
this._chartData = table;
const termColumn = this._params.bucket ? table.columns[this._params.bucket.accessor] : null;
const valueColumn = table.columns[this._params.metric.accessor];
let results;
if (!this._hasColumns() || !table.rows.length) {
results = [];
} else {
results = table.rows.map((row) => {
const term = row[termColumn.id];
const value = row[valueColumn.id];
return { term: term, value: value };
});
}
const selectedLayer = await this._loadConfig(this._params.selectedLayer);
if (!this._params.selectedJoinField && selectedLayer) {
this._params.selectedJoinField = selectedLayer.fields[0];
}
if (!selectedLayer) {
return;
}
await this._updateChoroplethLayerForNewMetrics(
selectedLayer.name,
selectedLayer.attribution,
this._params.showAllShapes,
results
);
const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format);
this._choroplethLayer.setMetrics(results, metricFieldFormatter, valueColumn.name);
if (termColumn && valueColumn) {
this._choroplethLayer.setTooltipFormatter(
this._tooltipFormatter,
metricFieldFormatter,
termColumn.name,
valueColumn.name
);
}
this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState);
}
async _loadConfig(fileLayerConfig) {
// Load the selected layer from the metadata-service.
// Do not use the selectedLayer from the visState.
// These settings are stored in the URL and can be used to inject dirty display content.
const { escape } = await import('lodash');
if (
fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS
(fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects
) {
const serviceSettings = await getServiceSettings();
return await serviceSettings.loadFileLayerConfig(fileLayerConfig);
}
//Configured in the kibana.yml. Needs to be resolved through the settings.
const configuredLayer = regionmapsConfig.layers.find(
(layer) => layer.name === fileLayerConfig.name
);
if (configuredLayer) {
return {
...configuredLayer,
attribution: escape(configuredLayer.attribution ? configuredLayer.attribution : ''),
};
}
return null;
}
async _updateParams() {
await super._updateParams();
const selectedLayer = await this._loadConfig(this._params.selectedLayer);
if (!this._params.selectedJoinField && selectedLayer) {
this._params.selectedJoinField = selectedLayer.fields[0];
}
if (!this._params.selectedJoinField || !selectedLayer) {
return;
}
await this._updateChoroplethLayerForNewProperties(
selectedLayer.name,
selectedLayer.attribution,
this._params.showAllShapes
);
const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format);
this._choroplethLayer.setJoinField(this._params.selectedJoinField.name);
this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value);
this._choroplethLayer.setLineWeight(this._params.outlineWeight);
this._choroplethLayer.setTooltipFormatter(
this._tooltipFormatter,
metricFieldFormatter,
this._metricLabel
);
}
async _updateChoroplethLayerForNewMetrics(name, attribution, showAllData, newMetrics) {
if (
this._choroplethLayer &&
this._choroplethLayer.canReuseInstanceForNewMetrics(name, showAllData, newMetrics)
) {
return;
}
await this._recreateChoroplethLayer(name, attribution, showAllData);
}
async _updateChoroplethLayerForNewProperties(name, attribution, showAllData) {
if (this._choroplethLayer && this._choroplethLayer.canReuseInstance(name, showAllData)) {
return;
}
await this._recreateChoroplethLayer(name, attribution, showAllData);
}
async _recreateChoroplethLayer(name, attribution, showAllData) {
const selectedLayer = await this._loadConfig(this._params.selectedLayer);
this._kibanaMap.removeLayer(this._choroplethLayer);
if (this._choroplethLayer) {
this._choroplethLayer = this._choroplethLayer.cloneChoroplethLayerForNewData(
name,
attribution,
selectedLayer.format,
showAllData,
selectedLayer.meta,
selectedLayer,
await getServiceSettings(),
(await lazyLoadMapsLegacyModules()).L
);
} else {
const { ChoroplethLayer } = await import('./choropleth_layer');
this._choroplethLayer = new ChoroplethLayer(
name,
attribution,
selectedLayer.format,
showAllData,
selectedLayer.meta,
selectedLayer,
await getServiceSettings(),
(await lazyLoadMapsLegacyModules()).L
);
}
this._choroplethLayer.on('select', (event) => {
const { rows, columns } = this._chartData;
const rowIndex = rows.findIndex((row) => row[columns[0].id] === event);
this.handlers.event({
name: 'filterBucket',
data: {
data: [
{
table: this._chartData,
column: 0,
row: rowIndex,
value: event,
},
],
},
});
});
this._choroplethLayer.on('styleChanged', (event) => {
const shouldShowWarning =
this._params.isDisplayWarning && uiSettings.get('visualization:regionmap:showWarnings');
if (event.mismatches.length > 0 && shouldShowWarning) {
getNotifications().toasts.addWarning({
title: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningTitle', {
defaultMessage:
'Unable to show {mismatchesLength} {oneMismatch, plural, one {result} other {results}} on map',
values: {
mismatchesLength: event.mismatches.length,
oneMismatch: event.mismatches.length > 1 ? 0 : 1,
},
}),
text: i18n.translate('regionMap.visualization.unableToShowMismatchesWarningText', {
defaultMessage:
"Ensure that each of these term matches a shape on that shape's join field: {mismatches}",
values: {
mismatches: event.mismatches ? event.mismatches.join(', ') : '',
},
}),
});
}
});
this._kibanaMap.addLayer(this._choroplethLayer);
}
_hasColumns() {
return this._chartData && this._chartData.columns.length === 2;
}
};
}

View file

@ -1,4 +0,0 @@
.rgmChart__wrapper, .rgmChart {
flex: 1 1 0;
display: flex;
}

View file

@ -1,92 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect, useMemo, useRef } from 'react';
import { EuiResizeObserver } from '@elastic/eui';
import { throttle } from 'lodash';
import { IInterpreterRenderHandlers, Datatable } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
import { RegionMapVisualizationDependencies } from './plugin';
import { RegionMapVisConfig } from './region_map_types';
// @ts-expect-error
import { createRegionMapVisualization } from './region_map_visualization';
import './region_map_visualization.scss';
interface RegionMapVisController {
render(visData?: Datatable, visConfig?: RegionMapVisConfig): Promise<void>;
resize(): void;
destroy(): void;
}
interface TileMapVisualizationProps {
deps: RegionMapVisualizationDependencies;
handlers: IInterpreterRenderHandlers;
visData: Datatable;
visConfig: RegionMapVisConfig;
}
const RegionMapVisualization = ({
deps,
handlers,
visData,
visConfig,
}: TileMapVisualizationProps) => {
const chartDiv = useRef<HTMLDivElement>(null);
const visController = useRef<RegionMapVisController | null>(null);
const isFirstRender = useRef(true);
const uiState = handlers.uiState as PersistedState | undefined;
useEffect(() => {
if (chartDiv.current && isFirstRender.current) {
isFirstRender.current = false;
const Controller = createRegionMapVisualization(deps);
visController.current = new Controller(chartDiv.current, handlers, visConfig);
}
}, [deps, handlers, visConfig, visData]);
useEffect(() => {
visController.current?.render(visData, visConfig).then(handlers.done);
}, [visData, visConfig, handlers.done]);
useEffect(() => {
const onUiStateChange = () => {
visController.current?.render().then(handlers.done);
};
uiState?.on('change', onUiStateChange);
return () => {
uiState?.off('change', onUiStateChange);
};
}, [uiState, handlers.done]);
useEffect(() => {
return () => {
visController.current?.destroy();
visController.current = null;
};
}, []);
const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []);
return (
<EuiResizeObserver onResize={updateChartSize}>
{(resizeRef) => (
<div className="rgmChart__wrapper" ref={resizeRef}>
<div className="rgmChart" ref={chartDiv} />
</div>
)}
</EuiResizeObserver>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { RegionMapVisualization as default };

View file

@ -1,48 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public';
import { RegionMapExpressionFunctionDefinition } from './region_map_fn';
import { RegionMapVisConfig, RegionMapVisParams } from './region_map_types';
export const toExpressionAst: VisToExpressionAst<RegionMapVisParams> = (vis, params) => {
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('esaggs', {
index: buildExpression([
buildExpressionFunction<IndexPatternLoadExpressionFunctionDefinition>('indexPatternLoad', {
id: vis.data.indexPattern!.id!,
}),
]),
metricsAtAllLevels: false,
partialRows: false,
aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
});
const schemas = getVisSchemas(vis, params);
const visConfig: RegionMapVisConfig = {
...vis.params,
metric: schemas.metric[0],
};
if (schemas.segment) {
visConfig.bucket = schemas.segment[0];
}
const regionmap = buildExpressionFunction<RegionMapExpressionFunctionDefinition>('regionmap', {
visConfig: JSON.stringify(visConfig),
});
const ast = buildExpression([esaggs, regionmap]);
return ast.toAst();
};

View file

@ -1,29 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) {
if (!metric) {
return '';
}
const details = [];
if (fieldName && metric) {
details.push({
label: fieldName,
value: metric.term,
});
}
if (metric) {
details.push({
label: metricName,
value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value,
});
}
return details;
}

View file

@ -1,15 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FileLayer, VectorLayer, ORIGIN } from '../../maps_ems/public';
export const mapToLayerWithId = (prefix: string, layer: FileLayer): VectorLayer => ({
...layer,
layerId: `${prefix}.${layer.name}`,
isEMS: ORIGIN.EMS === prefix,
});

View file

@ -1,18 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreSetup } from 'src/core/server';
import { getUiSettings } from './ui_settings';
export const plugin = () => ({
setup(core: CoreSetup) {
core.uiSettings.register(getUiSettings());
},
start() {},
});

View file

@ -1,31 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from 'kibana/server';
import { schema } from '@kbn/config-schema';
export function getUiSettings(): Record<string, UiSettingsParams<unknown>> {
return {
'visualization:regionmap:showWarnings': {
name: i18n.translate('regionMap.advancedSettings.visualization.showRegionMapWarningsTitle', {
defaultMessage: 'Show region map warning',
}),
value: true,
description: i18n.translate(
'regionMap.advancedSettings.visualization.showRegionMapWarningsText',
{
defaultMessage:
'Whether the region map shows a warning when terms cannot be joined to a shape on the map.',
}
),
schema: schema.boolean(),
category: ['visualization'],
},
};
}

View file

@ -1,15 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": ["public/**/*", "server/**/*"],
"references": [
{ "path": "../maps_legacy/tsconfig.json" },
{ "path": "../maps_ems/tsconfig.json" },
{ "path": "../vis_default_editor/tsconfig.json" },
]
}

View file

@ -1,5 +0,0 @@
# Coordinate map visualization
Create a coordinate map. Display the results of a geohash_tile aggregation as bubbles, rectangles, or heatmap color blobs.
This plugin is targeted for removal in 8.0.

View file

@ -1,13 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/tile_map'],
};

View file

@ -1,21 +0,0 @@
{
"id": "tileMap",
"owner": {
"name": "GIS",
"githubTeam": "kibana-gis"
},
"version": "8.0.0",
"kibanaVersion": "kibana",
"ui": true,
"server": true,
"requiredPlugins": [
"visualizations",
"expressions",
"mapsLegacy",
"mapsEms",
"kibanaLegacy",
"data",
"share"
],
"requiredBundles": ["kibanaUtils", "charts", "visDefaultEditor"]
}

View file

@ -1,4 +0,0 @@
{
"name": "tile_map",
"version": "kibana"
}

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#tilemap returns an object with the correct structure 1`] = `
Object {
"as": "tile_map_vis",
"type": "render",
"value": Object {
"visConfig": Object {
"addTooltip": true,
"colorSchema": "Yellow to Red",
"dimensions": Object {
"geocentroid": null,
"geohash": null,
"metric": Object {
"accessor": 0,
"aggType": "count",
"format": Object {
"id": "number",
},
"params": Object {},
},
},
"heatClusterSize": 1.5,
"isDesaturated": true,
"legendPosition": "bottomright",
"mapCenter": Array [
0,
0,
],
"mapType": "Scaled Circle Markers",
"mapZoom": 2,
"wms": Object {
"enabled": false,
"options": Object {
"format": "image/png",
"transparent": true,
},
},
},
"visData": Object {
"featureCollection": Object {
"features": Array [],
"type": "FeatureCollection",
},
"meta": Object {
"geohashGridDimensionsAtEquator": null,
"geohashPrecision": null,
"max": null,
"min": null,
},
},
"visType": "tile_map",
},
}
`;

View file

@ -1,65 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { MapTypes } from '../utils/map_types';
export const collections = {
mapTypes: [
{
value: MapTypes.ScaledCircleMarkers,
text: i18n.translate('tileMap.mapTypes.scaledCircleMarkersText', {
defaultMessage: 'Scaled circle markers',
}),
},
{
value: MapTypes.ShadedCircleMarkers,
text: i18n.translate('tileMap.mapTypes.shadedCircleMarkersText', {
defaultMessage: 'Shaded circle markers',
}),
},
{
value: MapTypes.ShadedGeohashGrid,
text: i18n.translate('tileMap.mapTypes.shadedGeohashGridText', {
defaultMessage: 'Shaded geohash grid',
}),
},
{
value: MapTypes.Heatmap,
text: i18n.translate('tileMap.mapTypes.heatmapText', {
defaultMessage: 'Heatmap',
}),
},
],
legendPositions: [
{
value: 'bottomleft',
text: i18n.translate('tileMap.legendPositions.bottomLeftText', {
defaultMessage: 'Bottom left',
}),
},
{
value: 'bottomright',
text: i18n.translate('tileMap.legendPositions.bottomRightText', {
defaultMessage: 'Bottom right',
}),
},
{
value: 'topleft',
text: i18n.translate('tileMap.legendPositions.topLeftText', {
defaultMessage: 'Top left',
}),
},
{
value: 'topright',
text: i18n.translate('tileMap.legendPositions.topRightText', {
defaultMessage: 'Top right',
}),
},
],
};

View file

@ -1,14 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import type { TileMapOptionsProps } from './tile_map_options';
const TileMapOptions = lazy(() => import('./tile_map_options'));
export const TileMapOptionsLazy = (props: TileMapOptionsProps) => <TileMapOptions {...props} />;

View file

@ -1,103 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
import {
BasicOptions,
SelectOption,
SwitchOption,
RangeOption,
} from '../../../vis_default_editor/public';
import { truncatedColorSchemas } from '../../../charts/public';
import { WmsOptions } from '../../../maps_legacy/public';
import { TileMapVisParams } from '../types';
import { MapTypes } from '../utils/map_types';
import { getTmsLayers } from '../services';
import { collections } from './collections';
export type TileMapOptionsProps = VisEditorOptionsProps<TileMapVisParams>;
const tmsLayers = getTmsLayers();
function TileMapOptions(props: TileMapOptionsProps) {
const { stateParams, setValue, vis } = props;
useEffect(() => {
if (!stateParams.mapType) {
setValue('mapType', collections.mapTypes[0].value);
}
}, [setValue, stateParams.mapType]);
return (
<>
<EuiPanel paddingSize="s">
<SelectOption
label={i18n.translate('tileMap.visParams.mapTypeLabel', {
defaultMessage: 'Map type',
})}
options={collections.mapTypes}
paramName="mapType"
value={stateParams.mapType}
setValue={setValue}
/>
{stateParams.mapType === MapTypes.Heatmap ? (
<RangeOption
label={i18n.translate('tileMap.visParams.clusterSizeLabel', {
defaultMessage: 'Cluster size',
})}
max={3}
min={1}
paramName="heatClusterSize"
step={0.1}
value={stateParams.heatClusterSize}
setValue={setValue}
/>
) : (
<SelectOption
label={i18n.translate('tileMap.visParams.colorSchemaLabel', {
defaultMessage: 'Color schema',
})}
options={truncatedColorSchemas}
paramName="colorSchema"
value={stateParams.colorSchema}
setValue={setValue}
/>
)}
<BasicOptions {...props} legendPositions={collections.legendPositions} />
<SwitchOption
disabled={!vis.type.visConfig?.canDesaturate}
label={i18n.translate('tileMap.visParams.desaturateTilesLabel', {
defaultMessage: 'Desaturate tiles',
})}
tooltip={i18n.translate('tileMap.visParams.reduceVibrancyOfTileColorsTip', {
defaultMessage:
'Reduce the vibrancy of tile colors. This does not work in any version of Internet Explorer.',
})}
paramName="isDesaturated"
value={stateParams.isDesaturated}
setValue={setValue}
/>
</EuiPanel>
<EuiSpacer size="s" />
<WmsOptions setValue={setValue} stateParams={stateParams} tmsLayers={tmsLayers} />
</>
);
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TileMapOptions as default };

View file

@ -1,29 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
/**
* just a place to put feature detection checks
*/
export const supportsCssFilters = (function () {
const e = document.createElement('img');
const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter'];
const test = 'grayscale(1)';
rules.forEach(function (rule) {
e.style[rule] = test;
});
document.body.appendChild(e);
const styles = window.getComputedStyle(e);
const can = _(styles).pick(rules).includes(test);
document.body.removeChild(e);
return can;
})();

View file

@ -1,172 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { min, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { KibanaMapLayer } from '../../maps_legacy/public';
import { HeatmapMarkers } from './markers/heatmap';
import { ScaledCirclesMarkers } from './markers/scaled_circles';
import { ShadedCirclesMarkers } from './markers/shaded_circles';
import { GeohashGridMarkers } from './markers/geohash_grid';
import { MapTypes } from './utils/map_types';
export class GeohashLayer extends KibanaMapLayer {
constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap, leaflet) {
super();
this._featureCollection = featureCollection;
this._featureCollectionMetaData = featureCollectionMetaData;
this._geohashOptions = options;
this._zoom = zoom;
this._kibanaMap = kibanaMap;
this._leaflet = leaflet;
const geojson = this._leaflet.geoJson(this._featureCollection);
this._bounds = geojson.getBounds();
this._createGeohashMarkers();
this._lastBounds = null;
}
_createGeohashMarkers() {
const markerOptions = {
isFilteredByCollar: this._geohashOptions.isFilteredByCollar,
valueFormatter: this._geohashOptions.valueFormatter,
tooltipFormatter: this._geohashOptions.tooltipFormatter,
label: this._geohashOptions.label,
colorRamp: this._geohashOptions.colorRamp,
};
switch (this._geohashOptions.mapType) {
case MapTypes.ScaledCircleMarkers:
this._geohashMarkers = new ScaledCirclesMarkers(
this._featureCollection,
this._featureCollectionMetaData,
markerOptions,
this._zoom,
this._kibanaMap,
this._leaflet
);
break;
case MapTypes.ShadedCircleMarkers:
this._geohashMarkers = new ShadedCirclesMarkers(
this._featureCollection,
this._featureCollectionMetaData,
markerOptions,
this._zoom,
this._kibanaMap,
this._leaflet
);
break;
case MapTypes.ShadedGeohashGrid:
this._geohashMarkers = new GeohashGridMarkers(
this._featureCollection,
this._featureCollectionMetaData,
markerOptions,
this._zoom,
this._kibanaMap,
this._leaflet
);
break;
case MapTypes.Heatmap:
let radius = 15;
if (this._featureCollectionMetaData.geohashGridDimensionsAtEquator) {
const minGridLength = min(this._featureCollectionMetaData.geohashGridDimensionsAtEquator);
const metersPerPixel = this._kibanaMap.getMetersPerPixel();
radius = minGridLength / metersPerPixel / 2;
}
radius = radius * parseFloat(this._geohashOptions.heatmap.heatClusterSize);
this._geohashMarkers = new HeatmapMarkers(
this._featureCollection,
{
radius: radius,
blur: radius,
maxZoom: this._kibanaMap.getZoomLevel(),
minOpacity: 0.1,
tooltipFormatter: this._geohashOptions.tooltipFormatter,
},
this._zoom,
this._featureCollectionMetaData.max,
this._leaflet
);
break;
default:
throw new Error(
i18n.translate('tileMap.geohashLayer.mapTitle', {
defaultMessage: '{mapType} mapType not recognized',
values: {
mapType: this._geohashOptions.mapType,
},
})
);
}
this._geohashMarkers.on('showTooltip', (event) => this.emit('showTooltip', event));
this._geohashMarkers.on('hideTooltip', (event) => this.emit('hideTooltip', event));
this._leafletLayer = this._geohashMarkers.getLeafletLayer();
}
appendLegendContents(jqueryDiv) {
return this._geohashMarkers.appendLegendContents(jqueryDiv);
}
movePointer(...args) {
this._geohashMarkers.movePointer(...args);
}
async getBounds() {
if (this._geohashOptions.fetchBounds) {
const geoHashBounds = await this._geohashOptions.fetchBounds();
if (geoHashBounds) {
const northEast = this._leaflet.latLng(
geoHashBounds.top_left.lat,
geoHashBounds.bottom_right.lon
);
const southWest = this._leaflet.latLng(
geoHashBounds.bottom_right.lat,
geoHashBounds.top_left.lon
);
return this._leaflet.latLngBounds(southWest, northEast);
}
}
return this._bounds;
}
updateExtent() {
// Client-side filtering is only enabled when server-side filter is not used
if (!this._geohashOptions.isFilteredByCollar) {
const bounds = this._kibanaMap.getLeafletBounds();
if (!this._lastBounds || !this._lastBounds.equals(bounds)) {
//this removal is required to trigger the bounds filter again
this._kibanaMap.removeLayer(this);
this._createGeohashMarkers();
this._kibanaMap.addLayer(this);
}
this._lastBounds = bounds;
}
}
isReusable(options) {
if (isEqual(this._geohashOptions, options)) {
return true;
}
//check if any impacts leaflet styler function
if (this._geohashOptions.colorRamp !== options.colorRamp) {
return false;
} else if (this._geohashOptions.mapType !== options.mapType) {
return false;
} else if (
this._geohashOptions.mapType === 'Heatmap' &&
!isEqual(this._geohashOptions.heatmap, options)
) {
return false;
} else {
return true;
}
}
}

View file

@ -1,70 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { getQueryService, getShareService } from './services';
import { indexPatterns } from '../../data/public';
import { Vis } from '../../visualizations/public';
import { LegacyMapDeprecationMessage } from '../../maps_legacy/public';
export function getDeprecationMessage(vis: Vis) {
const title = i18n.translate('tileMap.vis.mapTitle', {
defaultMessage: 'Coordinate Map',
});
async function onClick(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
const locator = getShareService().url.locators.get('MAPS_APP_TILE_MAP_LOCATOR');
if (!locator) return;
const query = getQueryService();
const params: { [key: string]: any } = {
label: vis.title ? vis.title : title,
mapType: vis.params.mapType,
colorSchema: vis.params.colorSchema,
indexPatternId: vis.data.indexPattern?.id,
metricAgg: 'count',
filters: query.filterManager.getFilters(),
query: query.queryString.getQuery(),
timeRange: query.timefilter.timefilter.getTime(),
};
const bucketAggs = vis.data?.aggs?.byType('buckets');
if (bucketAggs?.length && bucketAggs[0].type.dslName === 'geohash_grid') {
params.geoFieldName = bucketAggs[0].getField()?.name;
} else if (vis.data.indexPattern) {
// attempt to default to first geo point field when geohash is not configured yet
const geoField = vis.data.indexPattern.fields.find((field) => {
return (
!indexPatterns.isNestedField(field) && field.aggregatable && field.type === 'geo_point'
);
});
if (geoField) {
params.geoFieldName = geoField.name;
}
}
const metricAggs = vis.data?.aggs?.byType('metrics');
if (metricAggs?.length) {
params.metricAgg = metricAggs[0].type.dslName;
params.metricFieldName = metricAggs[0].getField()?.name;
}
locator.navigate(params);
}
return (
<LegacyMapDeprecationMessage
isMapsAvailable={!!getShareService().url.locators.get('MAPS_APP_TILE_MAP_LOCATOR')}
onClick={onClick}
visualizationLabel={title}
/>
);
}

View file

@ -1,14 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginInitializerContext } from 'kibana/public';
import { TileMapPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
}

View file

@ -1,24 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ScaledCirclesMarkers } from './scaled_circles';
export class GeohashGridMarkers extends ScaledCirclesMarkers {
getMarkerFunction() {
return (feature) => {
const geohashRect = feature.properties.geohash_meta.rectangle;
// get bounds from northEast[3] and southWest[1]
// corners in geohash rectangle
const corners = [
[geohashRect[3][0], geohashRect[3][1]],
[geohashRect[1][0], geohashRect[1][1]],
];
return this._leaflet.rectangle(corners);
};
}
}

View file

@ -1,183 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import d3 from 'd3';
import { EventEmitter } from 'events';
/**
* Map overlay: canvas layer with leaflet.heat plugin
*
* @param map {Leaflet Object}
* @param geoJson {geoJson Object}
* @param params {Object}
*/
export class HeatmapMarkers extends EventEmitter {
constructor(featureCollection, options, zoom, max, leaflet) {
super();
this._geojsonFeatureCollection = featureCollection;
const points = dataToHeatArray(featureCollection, max);
this._leafletLayer = new leaflet.HeatLayer(points, options);
this._tooltipFormatter = options.tooltipFormatter;
this._zoom = zoom;
this._disableTooltips = false;
this._getLatLng = _.memoize(
function (feature) {
return leaflet.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]);
},
function (feature) {
// turn coords into a string for the memoize cache
return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(',');
}
);
this._addTooltips();
}
getBounds() {
return this._leafletLayer.getBounds();
}
getLeafletLayer() {
return this._leafletLayer;
}
appendLegendContents() {}
movePointer(type, event) {
if (type === 'mousemove') {
this._debounceMoveMoveLocation(event);
} else if (type === 'mouseout') {
this.emit('hideTooltip');
} else if (type === 'mousedown') {
this._disableTooltips = true;
this.emit('hideTooltip');
} else if (type === 'mouseup') {
this._disableTooltips = false;
}
}
_addTooltips() {
const mouseMoveLocation = (e) => {
if (!this._geojsonFeatureCollection.features.length || this._disableTooltips) {
this.emit('hideTooltip');
return;
}
const feature = this._nearestFeature(e.latlng);
if (this._tooltipProximity(e.latlng, feature)) {
const content = this._tooltipFormatter(feature);
if (!content) {
return;
}
this.emit('showTooltip', {
content: content,
position: e.latlng,
});
} else {
this.emit('hideTooltip');
}
};
this._debounceMoveMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, {
leading: true,
trailing: false,
});
}
/**
* Finds nearest feature in mapData to event latlng
*
* @method _nearestFeature
* @param latLng {Leaflet latLng}
* @return nearestPoint {Leaflet latLng}
*/
_nearestFeature(latLng) {
const self = this;
let nearest;
if (latLng.lng < -180 || latLng.lng > 180) {
return;
}
_.reduce(
this._geojsonFeatureCollection.features,
function (distance, feature) {
const featureLatLng = self._getLatLng(feature);
const dist = latLng.distanceTo(featureLatLng);
if (dist < distance) {
nearest = feature;
return dist;
}
return distance;
},
Infinity
);
return nearest;
}
/**
* display tooltip if feature is close enough to event latlng
*
* @method _tooltipProximity
* @param latlng {Leaflet latLng Object}
* @param feature {geoJson Object}
* @return {Boolean}
*/
_tooltipProximity(latlng, feature) {
if (!feature) return;
let showTip = false;
const featureLatLng = this._getLatLng(feature);
// zoomScale takes map zoom and returns proximity value for tooltip display
// domain (input values) is map zoom (min 1 and max 18)
// range (output values) is distance in meters
// used to compare proximity of event latlng to feature latlng
const zoomScale = d3.scale
.linear()
.domain([1, 4, 7, 10, 13, 16, 18])
.range([1000000, 300000, 100000, 15000, 2000, 150, 50]);
const proximity = zoomScale(this._zoom);
const distance = latlng.distanceTo(featureLatLng);
// maxLngDif is max difference in longitudes
// to prevent feature tooltip from appearing 360°
// away from event latlng
const maxLngDif = 40;
const lngDif = Math.abs(latlng.lng - featureLatLng.lng);
if (distance < proximity && lngDif < maxLngDif) {
showTip = true;
}
d3.scale.pow().exponent(0.2).domain([1, 18]).range([1500000, 50]);
return showTip;
}
}
/**
* returns normalized data for heat map intensity
*
* @method dataToHeatArray
* @param featureCollection {Array}
* @return {Array}
*/
function dataToHeatArray(featureCollection, max) {
return featureCollection.features.map((feature) => {
const lat = feature.geometry.coordinates[1];
const lng = feature.geometry.coordinates[0];
// show bucket value normalized to max value
const heatIntensity = feature.properties.value / max;
return [lat, lng, heatIntensity];
});
}

View file

@ -1,236 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
import { EventEmitter } from 'events';
import { colorUtil } from '../../../maps_legacy/public';
import { truncatedColorMaps } from '../../../charts/public';
export class ScaledCirclesMarkers extends EventEmitter {
constructor(
featureCollection,
featureCollectionMetaData,
options,
targetZoom,
kibanaMap,
leaflet
) {
super();
this._featureCollection = featureCollection;
this._featureCollectionMetaData = featureCollectionMetaData;
this._zoom = targetZoom;
this._valueFormatter =
options.valueFormatter ||
((x) => {
x;
});
this._tooltipFormatter =
options.tooltipFormatter ||
((x) => {
x;
});
this._label = options.label;
this._colorRamp = options.colorRamp;
this._legendColors = null;
this._legendQuantizer = null;
this._leaflet = leaflet;
this._popups = [];
const layerOptions = {
pointToLayer: this.getMarkerFunction(),
style: this.getStyleFunction(),
onEachFeature: (feature, layer) => {
this._bindPopup(feature, layer);
},
};
// Filter leafletlayer on client when results are not filtered on the server
if (!options.isFilteredByCollar) {
layerOptions.filter = (feature) => {
const bucketRectBounds = feature.properties.geohash_meta.rectangle;
return kibanaMap.isInside(bucketRectBounds);
};
}
this._leafletLayer = this._leaflet.geoJson(null, layerOptions);
this._leafletLayer.addData(this._featureCollection);
}
getLeafletLayer() {
return this._leafletLayer;
}
getStyleFunction() {
const min = _.get(this._featureCollectionMetaData, 'min', 0);
const max = _.get(this._featureCollectionMetaData, 'max', 1);
const quantizeDomain = min !== max ? [min, max] : d3.scale.quantize().domain();
this._legendColors = this.getLegendColors();
this._legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(this._legendColors);
return makeStyleFunction(this._legendColors, quantizeDomain);
}
movePointer() {}
getLabel() {
if (this._popups.length) {
return this._label;
}
return '';
}
appendLegendContents(jqueryDiv) {
if (!this._legendColors || !this._legendQuantizer) {
return;
}
const titleText = this.getLabel();
const $title = $('<div>').addClass('visMapLegend__title').text(titleText);
jqueryDiv.append($title);
this._legendColors.forEach((color) => {
const labelText = this._legendQuantizer
.invertExtent(color)
.map(this._valueFormatter)
.join('  ');
const label = $('<div>');
const icon = $('<i>').css({
background: color,
'border-color': makeColorDarker(color),
});
const text = $('<span>').text(labelText);
label.append(icon);
label.append(text);
jqueryDiv.append(label);
});
}
/**
* Binds popup and events to each feature on map
*
* @method bindPopup
* @param feature {Object}
* @param layer {Object}
* return {undefined}
*/
_bindPopup(feature, layer) {
const popup = layer.on({
mouseover: (e) => {
const layer = e.target;
// bring layer to front if not older browser
if (!this._leaflet.Browser.ie && !this._leaflet.Browser.opera) {
layer.bringToFront();
}
this._showTooltip(feature);
},
mouseout: () => {
this.emit('hideTooltip');
},
});
this._popups.push(popup);
}
/**
* Checks if event latlng is within bounds of mapData
* features and shows tooltip for that feature
*
* @method _showTooltip
* @param feature {LeafletFeature}
* @return undefined
*/
_showTooltip(feature) {
const content = this._tooltipFormatter(feature);
if (!content) {
return;
}
const latLng = this._leaflet.latLng(
feature.geometry.coordinates[1],
feature.geometry.coordinates[0]
);
this.emit('showTooltip', {
content: content,
position: latLng,
});
}
getMarkerFunction() {
const scaleFactor = 0.6;
return (feature, latlng) => {
const value = feature.properties.value;
const scaledRadius = this._radiusScale(value) * scaleFactor;
return this._leaflet.circleMarker(latlng).setRadius(scaledRadius);
};
}
/**
* radiusScale returns a number for scaled circle markers
* for relative sizing of markers
*
* @method _radiusScale
* @param value {Number}
* @return {Number}
*/
_radiusScale(value) {
//magic numbers
const precisionBiasBase = 5;
const precisionBiasNumerator = 200;
const precision = _.max(
this._featureCollection.features.map((feature) => {
return String(feature.properties.geohash).length;
})
);
const pct = Math.abs(value) / Math.abs(this._featureCollectionMetaData.max);
const zoomRadius = 0.5 * Math.pow(2, this._zoom);
const precisionScale = precisionBiasNumerator / Math.pow(precisionBiasBase, precision);
// square root value percentage
return Math.pow(pct, 0.5) * zoomRadius * precisionScale;
}
getBounds() {
return this._leafletLayer.getBounds();
}
getLegendColors() {
const colorRamp = _.get(truncatedColorMaps[this._colorRamp], 'value');
return colorUtil.getLegendColors(colorRamp);
}
}
function makeColorDarker(color) {
const amount = 1.3; //magic number, carry over from earlier
return d3.hcl(color).darker(amount).toString();
}
function makeStyleFunction(legendColors, quantizeDomain) {
const legendQuantizer = d3.scale.quantize().domain(quantizeDomain).range(legendColors);
return (feature) => {
const value = _.get(feature, 'properties.value');
const color = legendQuantizer(value);
return {
fillColor: color,
color: makeColorDarker(color),
weight: 1.5,
opacity: 1,
fillOpacity: 0.75,
};
};
}

View file

@ -1,52 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import _ from 'lodash';
import { ScaledCirclesMarkers } from './scaled_circles';
export class ShadedCirclesMarkers extends ScaledCirclesMarkers {
getMarkerFunction() {
// multiplier to reduce size of all circles
const scaleFactor = 0.8;
return (feature, latlng) => {
const radius = this._geohashMinDistance(feature) * scaleFactor;
return this._leaflet.circle(latlng, radius);
};
}
/**
* _geohashMinDistance returns a min distance in meters for sizing
* circle markers to fit within geohash grid rectangle
*
* @method _geohashMinDistance
* @param feature {Object}
* @return {Number}
*/
_geohashMinDistance(feature) {
const centerPoint = feature.properties.geohash_meta.center;
const geohashRect = feature.properties.geohash_meta.rectangle;
// centerPoint is an array of [lat, lng]
// geohashRect is the 4 corners of the geoHash rectangle
// an array that starts at the southwest corner and proceeds
// clockwise, each value being an array of [lat, lng]
// center lat and southeast lng
const east = this._leaflet.latLng([centerPoint[0], geohashRect[2][1]]);
// southwest lat and center lng
const north = this._leaflet.latLng([geohashRect[3][0], centerPoint[1]]);
// get latLng of geohash center point
const center = this._leaflet.latLng([centerPoint[0], centerPoint[1]]);
// get smallest radius at center of geohash grid rectangle
const eastRadius = Math.floor(center.distanceTo(east));
const northRadius = Math.floor(center.distanceTo(north));
return _.min([eastRadius, northRadius]);
}
}

View file

@ -1,102 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
IUiSettingsClient,
} from 'kibana/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { MapsLegacyPluginSetup } from '../../maps_legacy/public';
import { MapsEmsPluginSetup } from '../../maps_ems/public';
import { IServiceSettings } from '../../maps_ems/public';
import { DataPublicPluginStart } from '../../data/public';
import {
setCoreService,
setFormatService,
setQueryService,
setKibanaLegacy,
setShareService,
} from './services';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { createTileMapFn } from './tile_map_fn';
import { createTileMapTypeDefinition } from './tile_map_type';
import { getTileMapRenderer } from './tile_map_renderer';
/** @private */
export interface TileMapVisualizationDependencies {
uiSettings: IUiSettingsClient;
getZoomPrecision: any;
getPrecision: any;
BaseMapsVisualization: any;
getServiceSettings: () => Promise<IServiceSettings>;
}
/** @internal */
export interface TileMapPluginSetupDependencies {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
visualizations: VisualizationsSetup;
mapsLegacy: MapsLegacyPluginSetup;
mapsEms: MapsEmsPluginSetup;
}
/** @internal */
export interface TileMapPluginStartDependencies {
data: DataPublicPluginStart;
kibanaLegacy: KibanaLegacyStart;
share: SharePluginStart;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TileMapPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TileMapPluginStart {}
/** @internal */
export class TileMapPlugin implements Plugin<TileMapPluginSetup, TileMapPluginStart> {
initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public setup(
core: CoreSetup,
{ expressions, visualizations, mapsLegacy, mapsEms }: TileMapPluginSetupDependencies
) {
const { getZoomPrecision, getPrecision } = mapsLegacy;
const visualizationDependencies: Readonly<TileMapVisualizationDependencies> = {
getZoomPrecision,
getPrecision,
BaseMapsVisualization: mapsLegacy.getBaseMapsVis(),
uiSettings: core.uiSettings,
getServiceSettings: mapsEms.getServiceSettings,
};
expressions.registerFunction(createTileMapFn);
expressions.registerRenderer(getTileMapRenderer(visualizationDependencies));
visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies));
return {};
}
public start(core: CoreStart, plugins: TileMapPluginStartDependencies) {
setFormatService(plugins.data.fieldFormats);
setQueryService(plugins.data.query);
setKibanaLegacy(plugins.kibanaLegacy);
setShareService(plugins.share);
setCoreService(core);
return {};
}
}

View file

@ -1,32 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreStart } from 'kibana/public';
import { createGetterSetter } from '../../kibana_utils/public';
import { DataPublicPluginStart } from '../../data/public';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { SharePluginStart } from '../../share/public';
import { TmsLayer } from '../../maps_ems/public';
export const [getCoreService, setCoreService] = createGetterSetter<CoreStart>('Core');
export const [getFormatService, setFormatService] = createGetterSetter<
DataPublicPluginStart['fieldFormats']
>('vislib data.fieldFormats');
export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
export const [getShareService, setShareService] = createGetterSetter<SharePluginStart>('Share');
export const [getKibanaLegacy, setKibanaLegacy] = createGetterSetter<KibanaLegacyStart>(
'KibanaLegacy'
);
export const [getTmsLayers, setTmsLayers] = createGetterSetter<TmsLayer[]>('TmsLayers');

View file

@ -1,85 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createTileMapFn } from './tile_map_fn';
jest.mock('./utils', () => ({
convertToGeoJson: jest.fn().mockReturnValue({
featureCollection: {
type: 'FeatureCollection',
features: [],
},
meta: {
min: null,
max: null,
geohashPrecision: null,
geohashGridDimensionsAtEquator: null,
},
}),
}));
import { convertToGeoJson } from './utils';
describe('interpreter/functions#tilemap', () => {
const fn = functionWrapper(createTileMapFn());
const context = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
const visConfig = {
colorSchema: 'Yellow to Red',
mapType: 'Scaled Circle Markers',
isDesaturated: true,
addTooltip: true,
heatClusterSize: 1.5,
legendPosition: 'bottomright',
mapZoom: 2,
mapCenter: [0, 0],
wms: {
enabled: false,
options: {
format: 'image/png',
transparent: true,
},
},
dimensions: {
metric: {
accessor: 0,
format: {
id: 'number',
},
params: {},
aggType: 'count',
},
geohash: null,
geocentroid: null,
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, { visConfig: JSON.stringify(visConfig) });
expect(actual).toMatchSnapshot();
});
it('calls response handler with correct values', async () => {
const { geohash, metric, geocentroid } = visConfig.dimensions;
await fn(context, { visConfig: JSON.stringify(visConfig) });
expect(convertToGeoJson).toHaveBeenCalledTimes(1);
expect(convertToGeoJson).toHaveBeenCalledWith(context, {
geohash,
metric,
geocentroid,
});
});
});

View file

@ -1,71 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { TileMapVisConfig, TileMapVisData } from './types';
interface Arguments {
visConfig: string | null;
}
export interface TileMapVisRenderValue {
visData: TileMapVisData;
visType: 'tile_map';
visConfig: TileMapVisConfig;
}
export type TileMapExpressionFunctionDefinition = ExpressionFunctionDefinition<
'tilemap',
Datatable,
Arguments,
Promise<Render<TileMapVisRenderValue>>
>;
export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({
name: 'tilemap',
type: 'render',
context: {
types: ['datatable'],
},
help: i18n.translate('tileMap.function.help', {
defaultMessage: 'Tilemap visualization',
}),
args: {
visConfig: {
types: ['string', 'null'],
default: '"{}"',
help: '',
},
},
async fn(context, args, handlers) {
const visConfig = args.visConfig && JSON.parse(args.visConfig);
const { geohash, metric, geocentroid } = visConfig.dimensions;
const { convertToGeoJson } = await import('./utils');
const convertedData = convertToGeoJson(context, {
geohash,
metric,
geocentroid,
});
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: 'tile_map_vis',
value: {
visData: convertedData,
visType: 'tile_map',
visConfig,
},
};
},
});

View file

@ -1,41 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { ExpressionRenderDefinition } from 'src/plugins/expressions';
import { VisualizationContainer } from '../../visualizations/public';
import { TileMapVisualizationDependencies } from './plugin';
import { TileMapVisRenderValue } from './tile_map_fn';
const TileMapVisualization = lazy(() => import('./tile_map_visualization_component'));
export const getTileMapRenderer: (
deps: TileMapVisualizationDependencies
) => ExpressionRenderDefinition<TileMapVisRenderValue> = (deps) => ({
name: 'tile_map_vis',
reuseDomNode: true,
render: async (domNode, { visConfig, visData }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<VisualizationContainer handlers={handlers}>
<TileMapVisualization
deps={deps}
handlers={handlers}
visData={visData}
visConfig={visConfig}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -1,95 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { VisTypeDefinition } from 'src/plugins/visualizations/public';
// @ts-expect-error
import { supportsCssFilters } from './css_filters';
import { TileMapOptionsLazy } from './components';
import { getDeprecationMessage } from './get_deprecation_message';
import { TileMapVisualizationDependencies } from './plugin';
import { toExpressionAst } from './to_ast';
import { TileMapVisParams } from './types';
import { setTmsLayers } from './services';
export function createTileMapTypeDefinition(
dependencies: TileMapVisualizationDependencies
): VisTypeDefinition<TileMapVisParams> {
const { uiSettings, getServiceSettings } = dependencies;
return {
name: 'tile_map',
getInfoMessage: getDeprecationMessage,
title: i18n.translate('tileMap.vis.mapTitle', {
defaultMessage: 'Coordinate Map',
}),
icon: 'visMapCoordinate',
description: i18n.translate('tileMap.vis.mapDescription', {
defaultMessage: 'Plot latitude and longitude coordinates on a map',
}),
visConfig: {
canDesaturate: Boolean(supportsCssFilters),
defaults: {
colorSchema: 'Yellow to Red',
mapType: 'Scaled Circle Markers',
isDesaturated: true,
addTooltip: true,
heatClusterSize: 1.5,
legendPosition: 'bottomright',
mapZoom: 2,
mapCenter: [0, 0],
wms: uiSettings.get('visualization:tileMap:WMSdefaults'),
},
},
toExpressionAst,
editorConfig: {
optionsTemplate: TileMapOptionsLazy,
schemas: [
{
group: 'metrics',
name: 'metric',
title: i18n.translate('tileMap.vis.map.editorConfig.schemas.metricTitle', {
defaultMessage: 'Value',
}),
min: 1,
max: 1,
aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{
group: 'buckets',
name: 'segment',
title: i18n.translate('tileMap.vis.map.editorConfig.schemas.geoCoordinatesTitle', {
defaultMessage: 'Geo coordinates',
}),
aggFilter: ['geohash_grid'],
min: 1,
max: 1,
},
],
},
setup: async (vis) => {
let tmsLayers;
try {
const serviceSettings = await getServiceSettings();
tmsLayers = await serviceSettings.getTMSServices();
} catch (e) {
return vis;
}
setTmsLayers(tmsLayers);
if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) {
vis.params.wms.selectedTmsLayer = tmsLayers[0];
}
return vis;
},
requiresSearch: true,
};
}

View file

@ -1,256 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { get, round } from 'lodash';
import { getFormatService, getQueryService, getKibanaLegacy } from './services';
import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public';
import { tooltipFormatter } from './tooltip_formatter';
import { geoContains } from './utils';
function scaleBounds(bounds) {
const scale = 0.5; // scale bounds by 50%
const topLeft = bounds.top_left;
const bottomRight = bounds.bottom_right;
let latDiff = round(Math.abs(topLeft.lat - bottomRight.lat), 5);
const lonDiff = round(Math.abs(bottomRight.lon - topLeft.lon), 5);
// map height can be zero when vis is first created
if (latDiff === 0) latDiff = lonDiff;
const latDelta = latDiff * scale;
let topLeftLat = round(topLeft.lat, 5) + latDelta;
if (topLeftLat > 90) topLeftLat = 90;
let bottomRightLat = round(bottomRight.lat, 5) - latDelta;
if (bottomRightLat < -90) bottomRightLat = -90;
const lonDelta = lonDiff * scale;
let topLeftLon = round(topLeft.lon, 5) - lonDelta;
if (topLeftLon < -180) topLeftLon = -180;
let bottomRightLon = round(bottomRight.lon, 5) + lonDelta;
if (bottomRightLon > 180) bottomRightLon = 180;
return {
top_left: { lat: topLeftLat, lon: topLeftLon },
bottom_right: { lat: bottomRightLat, lon: bottomRightLon },
};
}
export const createTileMapVisualization = (dependencies) => {
const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies;
return class CoordinateMapsVisualization extends BaseMapsVisualization {
constructor(element, handlers, initialVisParams) {
super(element, handlers, initialVisParams);
this._geohashLayer = null;
this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter);
}
updateGeohashAgg = () => {
const geohashAgg = this._getGeoHashAgg();
if (!geohashAgg) return;
const updateVarsObject = {
name: 'bounds',
data: {},
};
const bounds = this._kibanaMap.getBounds();
const mapCollar = scaleBounds(bounds);
if (!geoContains(geohashAgg.sourceParams.params.boundingBox, mapCollar)) {
updateVarsObject.data.boundingBox = {
top_left: mapCollar.top_left,
bottom_right: mapCollar.bottom_right,
};
} else {
updateVarsObject.data.boundingBox = geohashAgg.sourceParams.params.boundingBox;
}
// todo: autoPrecision should be vis parameter, not aggConfig one
const zoomPrecision = getZoomPrecision();
updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision
? zoomPrecision[this.handlers.uiState.get('mapZoom')]
: getPrecision(geohashAgg.sourceParams.params.precision);
this.handlers.event(updateVarsObject);
};
async render(esResponse, visParams) {
getKibanaLegacy().loadFontAwesome();
await super.render(esResponse, visParams);
}
async _makeKibanaMap() {
await super._makeKibanaMap(this._params);
let previousPrecision = this._kibanaMap.getGeohashPrecision();
let precisionChange = false;
this.handlers.uiState.on('change', (prop) => {
if (prop === 'mapZoom' || prop === 'mapCenter') {
this.updateGeohashAgg();
}
});
this._kibanaMap.on('zoomchange', () => {
precisionChange = previousPrecision !== this._kibanaMap.getGeohashPrecision();
previousPrecision = this._kibanaMap.getGeohashPrecision();
});
this._kibanaMap.on('zoomend', () => {
const geohashAgg = this._getGeoHashAgg();
if (!geohashAgg) {
return;
}
const isAutoPrecision =
typeof geohashAgg.sourceParams.params.autoPrecision === 'boolean'
? geohashAgg.sourceParams.params.autoPrecision
: true;
if (!isAutoPrecision) {
return;
}
if (precisionChange) {
this.updateGeohashAgg();
} else {
//when we filter queries by collar
this._updateData(this._geoJsonFeatureCollectionAndMeta);
}
});
this._kibanaMap.addDrawControl();
this._kibanaMap.on('drawCreated:rectangle', (event) => {
const geohashAgg = this._getGeoHashAgg();
this.addSpatialFilter(geohashAgg, 'geo_bounding_box', event.bounds);
});
this._kibanaMap.on('drawCreated:polygon', (event) => {
const geohashAgg = this._getGeoHashAgg();
this.addSpatialFilter(geohashAgg, 'geo_polygon', { points: event.points });
});
}
async _updateData(geojsonFeatureCollectionAndMeta) {
// Only recreate geohash layer when there is new aggregation data
// Exception is Heatmap: which needs to be redrawn every zoom level because the clustering is based on meters per pixel
if (
this._getMapsParams().mapType !== 'Heatmap' &&
geojsonFeatureCollectionAndMeta === this._geoJsonFeatureCollectionAndMeta
) {
return;
}
if (this._geohashLayer) {
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
}
if (!geojsonFeatureCollectionAndMeta) {
this._geoJsonFeatureCollectionAndMeta = null;
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
return;
}
if (
!this._geoJsonFeatureCollectionAndMeta ||
!geojsonFeatureCollectionAndMeta.featureCollection.features.length
) {
this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta;
this.updateGeohashAgg();
}
this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta;
this._recreateGeohashLayer();
}
async _recreateGeohashLayer() {
const { GeohashLayer } = await import('./geohash_layer');
if (this._geohashLayer) {
this._kibanaMap.removeLayer(this._geohashLayer);
this._geohashLayer = null;
}
const geohashOptions = this._getGeohashOptions();
this._geohashLayer = new GeohashLayer(
this._geoJsonFeatureCollectionAndMeta.featureCollection,
this._geoJsonFeatureCollectionAndMeta.meta,
geohashOptions,
this._kibanaMap.getZoomLevel(),
this._kibanaMap,
(await lazyLoadMapsLegacyModules()).L
);
this._kibanaMap.addLayer(this._geohashLayer);
}
async _updateParams() {
await super._updateParams();
this._kibanaMap.setDesaturateBaseLayer(this._params.isDesaturated);
//avoid recreating the leaflet layer when there are option-changes that do not effect the representation
//e.g. tooltip-visibility, legend position, basemap-desaturation, ...
const geohashOptions = this._getGeohashOptions();
if (!this._geohashLayer || !this._geohashLayer.isReusable(geohashOptions)) {
if (this._geoJsonFeatureCollectionAndMeta) {
this._recreateGeohashLayer();
}
this._updateData(this._geoJsonFeatureCollectionAndMeta);
}
}
_getGeohashOptions() {
const newParams = this._getMapsParams();
const metricDimension = this._params.dimensions.metric;
const metricLabel = metricDimension ? metricDimension.label : '';
const metricFormat = getFormatService().deserialize(
metricDimension && metricDimension.format
);
return {
label: metricLabel,
valueFormatter: this._geoJsonFeatureCollectionAndMeta
? metricFormat.getConverterFor('text')
: null,
tooltipFormatter: this._geoJsonFeatureCollectionAndMeta
? this._tooltipFormatter.bind(null, metricLabel, metricFormat.getConverterFor('text'))
: null,
mapType: newParams.mapType,
isFilteredByCollar: this._isFilteredByCollar(),
colorRamp: newParams.colorSchema,
heatmap: {
heatClusterSize: newParams.heatClusterSize,
},
};
}
addSpatialFilter(agg, filterName, filterData) {
if (!agg) {
return;
}
const indexPatternName = agg.indexPatternId;
const field = agg.field;
const filter = { meta: { negate: false, index: indexPatternName } };
filter[filterName] = { ignore_unmapped: true };
filter[filterName][field] = filterData;
const { filterManager } = getQueryService();
filterManager.addFilters([filter]);
}
_getGeoHashAgg() {
return (
this._geoJsonFeatureCollectionAndMeta && this._geoJsonFeatureCollectionAndMeta.meta.geohash
);
}
_isFilteredByCollar() {
const DEFAULT = false;
const agg = this._getGeoHashAgg();
if (agg) {
return get(agg, 'sourceParams.params.isFilteredByCollar', DEFAULT);
} else {
return DEFAULT;
}
}
};
};

View file

@ -1,4 +0,0 @@
.tlmChart__wrapper, .tlmChart {
flex: 1 1 0;
display: flex;
}

View file

@ -1,92 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect, useMemo, useRef } from 'react';
import { EuiResizeObserver } from '@elastic/eui';
import { throttle } from 'lodash';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
import { TileMapVisualizationDependencies } from './plugin';
import { TileMapVisConfig, TileMapVisData } from './types';
// @ts-expect-error
import { createTileMapVisualization } from './tile_map_visualization';
import './tile_map_visualization.scss';
interface TileMapVisController {
render(visData?: TileMapVisData, visConfig?: TileMapVisConfig): Promise<void>;
resize(): void;
destroy(): void;
}
interface TileMapVisualizationProps {
deps: TileMapVisualizationDependencies;
handlers: IInterpreterRenderHandlers;
visData: TileMapVisData;
visConfig: TileMapVisConfig;
}
const TileMapVisualization = ({
deps,
handlers,
visData,
visConfig,
}: TileMapVisualizationProps) => {
const chartDiv = useRef<HTMLDivElement>(null);
const visController = useRef<TileMapVisController | null>(null);
const isFirstRender = useRef(true);
const uiState = handlers.uiState as PersistedState;
useEffect(() => {
if (chartDiv.current && isFirstRender.current) {
isFirstRender.current = false;
const Controller = createTileMapVisualization(deps);
visController.current = new Controller(chartDiv.current, handlers, visConfig);
}
}, [deps, handlers, visConfig, visData]);
useEffect(() => {
visController.current?.render(visData, visConfig).then(handlers.done);
}, [visData, visConfig, handlers.done]);
useEffect(() => {
const onUiStateChange = () => {
visController.current?.render().then(handlers.done);
};
uiState.on('change', onUiStateChange);
return () => {
uiState.off('change', onUiStateChange);
};
}, [uiState, handlers.done]);
useEffect(() => {
return () => {
visController.current?.destroy();
visController.current = null;
};
}, []);
const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []);
return (
<EuiResizeObserver onResize={updateChartSize}>
{(resizeRef) => (
<div className="tlmChart__wrapper" ref={resizeRef}>
<div className="tlmChart" ref={chartDiv} />
</div>
)}
</EuiResizeObserver>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TileMapVisualization as default };

View file

@ -1,48 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../data/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { getVisSchemas, VisToExpressionAst } from '../../visualizations/public';
import { TileMapExpressionFunctionDefinition } from './tile_map_fn';
import { TileMapVisConfig, TileMapVisParams } from './types';
export const toExpressionAst: VisToExpressionAst<TileMapVisParams> = (vis, params) => {
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('esaggs', {
index: buildExpression([
buildExpressionFunction<IndexPatternLoadExpressionFunctionDefinition>('indexPatternLoad', {
id: vis.data.indexPattern!.id!,
}),
]),
metricsAtAllLevels: false,
partialRows: false,
aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
});
const schemas = getVisSchemas(vis, params);
const visConfig: TileMapVisConfig = {
...vis.params,
dimensions: {
metric: schemas.metric[0],
geohash: schemas.segment ? schemas.segment[0] : null,
geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null,
},
};
const tilemap = buildExpressionFunction<TileMapExpressionFunctionDefinition>('tilemap', {
visConfig: JSON.stringify(visConfig),
});
const ast = buildExpression([esaggs, tilemap]);
return ast.toAst();
};

View file

@ -1,34 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
export function tooltipFormatter(metricTitle, metricFormat, feature) {
if (!feature) {
return '';
}
return [
{
label: metricTitle,
value: metricFormat(feature.properties.value),
},
{
label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', {
defaultMessage: 'Latitude',
}),
value: feature.geometry.coordinates[1],
},
{
label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', {
defaultMessage: 'Longitude',
}),
value: feature.geometry.coordinates[0],
},
];
}

View file

@ -1,46 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FeatureCollection } from 'geojson';
import type { SchemaConfig } from 'src/plugins/visualizations/public';
import type { DatatableColumnMeta } from 'src/plugins/expressions';
import type { WMSOptions } from 'src/plugins/maps_legacy/public';
import type { MapTypes } from './utils/map_types';
export interface TileMapVisData {
featureCollection: FeatureCollection;
meta: {
min: number;
max: number;
geohash?: DatatableColumnMeta;
geohashPrecision: number | undefined;
geohashGridDimensionsAtEquator: [number, number] | undefined;
};
}
export interface TileMapVisDimensions {
metric: SchemaConfig;
geohash: SchemaConfig | null;
geocentroid: SchemaConfig | null;
}
export interface TileMapVisParams {
colorSchema: string;
mapType: MapTypes;
isDesaturated: boolean;
addTooltip: boolean;
heatClusterSize: number;
legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft';
mapZoom: number;
mapCenter: [number, number];
wms: WMSOptions;
}
export interface TileMapVisConfig extends TileMapVisParams {
dimensions: TileMapVisDimensions;
}

View file

@ -1,122 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Feature } from 'geojson';
import type { Datatable } from '../../../expressions/public';
import type { TileMapVisDimensions, TileMapVisData } from '../types';
import { decodeGeoHash } from './decode_geo_hash';
import { gridDimensions } from './grid_dimensions';
export function convertToGeoJson(
tabifiedResponse: Datatable,
{ geohash, geocentroid, metric }: TileMapVisDimensions
): TileMapVisData {
let features: Feature[];
let min = Infinity;
let max = -Infinity;
if (tabifiedResponse && tabifiedResponse.rows) {
const table = tabifiedResponse;
const geohashColumn = geohash ? table.columns[geohash.accessor] : null;
if (!geohashColumn) {
features = [];
} else {
const metricColumn = table.columns[metric.accessor];
const geocentroidColumn = geocentroid ? table.columns[geocentroid.accessor] : null;
features = table.rows
.map((row) => {
const geohashValue = row[geohashColumn.id];
if (!geohashValue) return false;
const geohashLocation = decodeGeoHash(geohashValue);
let pointCoordinates: number[];
if (geocentroidColumn) {
const location = row[geocentroidColumn.id];
pointCoordinates = [location.lon, location.lat];
} else {
pointCoordinates = [geohashLocation.longitude[2], geohashLocation.latitude[2]];
}
const rectangle = [
[geohashLocation.latitude[0], geohashLocation.longitude[0]],
[geohashLocation.latitude[0], geohashLocation.longitude[1]],
[geohashLocation.latitude[1], geohashLocation.longitude[1]],
[geohashLocation.latitude[1], geohashLocation.longitude[0]],
];
const centerLatLng = [geohashLocation.latitude[2], geohashLocation.longitude[2]];
if (geohash?.params.useGeocentroid) {
// see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used
pointCoordinates[0] = clampGrid(
pointCoordinates[0],
geohashLocation.longitude[0],
geohashLocation.longitude[1]
);
pointCoordinates[1] = clampGrid(
pointCoordinates[1],
geohashLocation.latitude[0],
geohashLocation.latitude[1]
);
}
const value = row[metricColumn.id];
min = Math.min(min, value);
max = Math.max(max, value);
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: pointCoordinates,
},
properties: {
geohash: geohashValue,
geohash_meta: {
center: centerLatLng,
rectangle,
},
value,
},
} as Feature;
})
.filter((row): row is Feature => !!row);
}
} else {
features = [];
}
const convertedData: TileMapVisData = {
featureCollection: {
type: 'FeatureCollection',
features,
},
meta: {
min,
max,
geohashPrecision: geohash?.params.precision,
geohashGridDimensionsAtEquator: geohash?.params.precision
? gridDimensions(geohash.params.precision)
: undefined,
},
};
if (geohash && geohash.accessor) {
convertedData.meta.geohash = tabifiedResponse.columns[geohash.accessor].meta;
}
return convertedData;
}
function clampGrid(val: number, min: number, max: number) {
if (val > max) val = max;
else if (val < min) val = min;
return val;
}

View file

@ -1,16 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { decodeGeoHash } from './decode_geo_hash';
test('decodeGeoHash', () => {
expect(decodeGeoHash('drm3btev3e86')).toEqual({
latitude: [41.119999922811985, 41.12000009045005, 41.12000000663102],
longitude: [-71.34000029414892, -71.3399999588728, -71.34000012651086],
});
});

View file

@ -1,89 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
interface DecodedGeoHash {
latitude: number[];
longitude: number[];
}
/**
* Decodes geohash to object containing
* top-left and bottom-right corners of
* rectangle and center point.
*/
export function decodeGeoHash(geohash: string): DecodedGeoHash {
const BITS: number[] = [16, 8, 4, 2, 1];
const BASE32: string = '0123456789bcdefghjkmnpqrstuvwxyz';
let isEven: boolean = true;
const lat: number[] = [];
const lon: number[] = [];
lat[0] = -90.0;
lat[1] = 90.0;
lon[0] = -180.0;
lon[1] = 180.0;
let latErr: number = 90.0;
let lonErr: number = 180.0;
[...geohash].forEach((nextChar: string) => {
const cd: number = BASE32.indexOf(nextChar);
for (let j = 0; j < 5; j++) {
const mask: number = BITS[j];
if (isEven) {
lonErr = lonErr /= 2;
refineInterval(lon, cd, mask);
} else {
latErr = latErr /= 2;
refineInterval(lat, cd, mask);
}
isEven = !isEven;
}
});
lat[2] = (lat[0] + lat[1]) / 2;
lon[2] = (lon[0] + lon[1]) / 2;
return {
latitude: lat,
longitude: lon,
};
}
function refineInterval(interval: number[], cd: number, mask: number) {
if (cd & mask) { /* eslint-disable-line */
interval[0] = (interval[0] + interval[1]) / 2;
} else {
interval[1] = (interval[0] + interval[1]) / 2;
}
}
interface GeoBoundingBoxCoordinate {
lat: number;
lon: number;
}
interface GeoBoundingBox {
top_left: GeoBoundingBoxCoordinate;
bottom_right: GeoBoundingBoxCoordinate;
}
export function geoContains(collar?: GeoBoundingBox, bounds?: GeoBoundingBox) {
if (!bounds || !collar) return false;
// test if bounds top_left is outside collar
if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) {
return false;
}
// test if bounds bottom_right is outside collar
if (
bounds.bottom_right.lat < collar.bottom_right.lat ||
bounds.bottom_right.lon > collar.bottom_right.lon
) {
return false;
}
// both corners are inside collar so collar contains bounds
return true;
}

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// geohash precision mapping of geohash grid cell dimensions (width x height, in meters) at equator.
// https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator
const gridAtEquator: { [key: number]: [number, number] } = {
1: [5009400, 4992600],
2: [1252300, 624100],
3: [156500, 156000],
4: [39100, 19500],
5: [4900, 4900],
6: [1200, 609.4],
7: [152.9, 152.4],
8: [38.2, 19],
9: [4.8, 4.8],
10: [1.2, 0.595],
11: [0.149, 0.149],
12: [0.037, 0.019],
};
export function gridDimensions(precision: number) {
return gridAtEquator[precision];
}

View file

@ -1,10 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { convertToGeoJson } from './convert_to_geojson';
export { geoContains } from './decode_geo_hash';

View file

@ -1,14 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export enum MapTypes {
ScaledCircleMarkers = 'Scaled Circle Markers',
ShadedCircleMarkers = 'Shaded Circle Markers',
ShadedGeohashGrid = 'Shaded Geohash Grid',
Heatmap = 'Heatmap',
}

Some files were not shown because too many files have changed in this diff Show more