mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
9fa41d1aef
commit
dd9dd52718
153 changed files with 1032 additions and 7160 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 || [],
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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.
|
|
@ -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>;
|
|
@ -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'],
|
||||
};
|
|
@ -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"]
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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']>;
|
|
@ -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();
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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})`;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
@import './leaflet_overrides';
|
||||
@import './legend';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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'],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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" }
|
||||
]
|
||||
}
|
|
@ -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.
|
|
@ -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'],
|
||||
};
|
|
@ -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"]
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "region_map",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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);
|
||||
}
|
|
@ -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} />;
|
|
@ -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 };
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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');
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.rgmChart__wrapper, .rgmChart {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
}
|
|
@ -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 };
|
|
@ -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();
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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() {},
|
||||
});
|
|
@ -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'],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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" },
|
||||
]
|
||||
}
|
|
@ -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.
|
|
@ -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'],
|
||||
};
|
|
@ -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"]
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "tile_map",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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} />;
|
|
@ -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 };
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -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');
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
.tlmChart__wrapper, .tlmChart {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
}
|
|
@ -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 };
|
|
@ -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();
|
||||
};
|
|
@ -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],
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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],
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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';
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue