{
renderTooltipContent={this.props.renderTooltipContent}
/>
) : null;
+ scaleControl = this.props.settings.showScaleControl ? (
+
+ ) : null;
}
return (
{
data-test-subj="mapContainer"
>
{drawControl}
+ {scaleControl}
{tooltipControl}
);
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/__snapshots__/scale_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/__snapshots__/scale_control.test.tsx.snap
new file mode 100644
index 000000000000..39d51b74bb7c
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/__snapshots__/scale_control.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`isFullScreen 1`] = `
+
+ ~50 km
+
+`;
+
+exports[`render 1`] = `
+
+ ~50 km
+
+`;
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/_index.scss b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/_index.scss
new file mode 100644
index 000000000000..264dfe5e2484
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/_index.scss
@@ -0,0 +1,15 @@
+.mapScaleControl {
+ position: absolute;
+ z-index: $euiZLevel1;
+ left: $euiSizeM;
+ bottom: $euiSizeM;
+ pointer-events: none;
+ color: $euiTextColor;
+ border-left: 2px solid $euiTextColor;
+ border-bottom: 2px solid $euiTextColor;
+ text-align: right;
+}
+
+.mapScaleControlFullScreen {
+ bottom: $euiSizeL * 2;
+}
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/index.ts
new file mode 100644
index 000000000000..c156421908b6
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { ScaleControl } from './scale_control';
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx
new file mode 100644
index 000000000000..d1f1fed77aae
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import { ScaleControl } from './scale_control';
+import { LngLat, LngLatBounds, Map as MapboxMap, PointLike } from 'mapbox-gl';
+
+const CLIENT_HEIGHT_PIXELS = 1200;
+const DISTANCE_METERS = 87653;
+
+const mockMbMapHandlers: { [key: string]: () => void } = {};
+const mockMBMap = ({
+ on: (eventName: string, callback: () => void) => {
+ mockMbMapHandlers[eventName] = callback;
+ },
+ off: (eventName: string) => {
+ delete mockMbMapHandlers[eventName];
+ },
+ getContainer: () => {
+ return {
+ clientHeight: CLIENT_HEIGHT_PIXELS,
+ };
+ },
+ getZoom: () => {
+ return 4;
+ },
+ getBounds: () => {
+ return ({
+ getNorth: () => {
+ return 75;
+ },
+ getSouth: () => {
+ return -60;
+ },
+ } as unknown) as LngLatBounds;
+ },
+ unproject: (point: PointLike) => {
+ return ({
+ distanceTo: (lngLat: LngLat) => {
+ return DISTANCE_METERS;
+ },
+ } as unknown) as LngLat;
+ },
+} as unknown) as MapboxMap;
+
+test('render', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('isFullScreen', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('should un-register all map callbacks on unmount', () => {
+ const component = mount();
+
+ expect(Object.keys(mockMbMapHandlers).length).toBe(1);
+
+ component.unmount();
+ expect(Object.keys(mockMbMapHandlers).length).toBe(0);
+});
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx
new file mode 100644
index 000000000000..74270baa1ce6
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import classNames from 'classnames';
+import React, { Component } from 'react';
+import { Map as MapboxMap } from 'mapbox-gl';
+const MAX_WIDTH = 110;
+
+interface Props {
+ isFullScreen: boolean;
+ mbMap: MapboxMap;
+}
+
+interface State {
+ label: string;
+ width: number;
+}
+
+function getScaleDistance(value: number) {
+ const orderOfMagnitude = Math.floor(Math.log10(value));
+ const pow10 = Math.pow(10, orderOfMagnitude);
+
+ // reduce value to single order of magnitude to making rounding simple regardless of order of magnitude
+ const distance = value / pow10;
+
+ if (distance < 1) {
+ return pow10 * (Math.round(distance * 10) / 10);
+ }
+
+ // provide easy to multiple round numbers for scale distance so its easy to measure distances longer then the scale
+ if (distance >= 10) {
+ return 10 * pow10;
+ }
+
+ if (distance >= 5) {
+ return 5 * pow10;
+ }
+
+ if (distance >= 3) {
+ return 3 * pow10;
+ }
+
+ return Math.floor(distance) * pow10;
+}
+
+export class ScaleControl extends Component {
+ private _isMounted: boolean = false;
+
+ state: State = { label: '', width: 0 };
+
+ componentDidMount() {
+ this._isMounted = true;
+ this.props.mbMap.on('move', this._onUpdate);
+ this._onUpdate();
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ this.props.mbMap.off('move', this._onUpdate);
+ }
+
+ _onUpdate = () => {
+ if (!this._isMounted) {
+ return;
+ }
+ const centerHeight = this.props.mbMap.getContainer().clientHeight / 2;
+ const leftLatLon = this.props.mbMap.unproject([0, centerHeight]);
+ const rightLatLon = this.props.mbMap.unproject([MAX_WIDTH, centerHeight]);
+ const maxDistanceMeters = leftLatLon.distanceTo(rightLatLon);
+ if (maxDistanceMeters >= 1000) {
+ this._setScale(
+ maxDistanceMeters / 1000,
+ i18n.translate('xpack.maps.kilometersAbbr', {
+ defaultMessage: 'km',
+ })
+ );
+ } else {
+ this._setScale(
+ maxDistanceMeters,
+ i18n.translate('xpack.maps.metersAbbr', {
+ defaultMessage: 'm',
+ })
+ );
+ }
+ };
+
+ _setScale(maxDistance: number, unit: string) {
+ const scaleDistance = getScaleDistance(maxDistance);
+ const zoom = this.props.mbMap.getZoom();
+ const bounds = this.props.mbMap.getBounds();
+ let label = `${scaleDistance} ${unit}`;
+ if (
+ zoom <= 4 ||
+ (zoom <= 6 && (bounds.getNorth() > 23.5 || bounds.getSouth() < -23.5)) ||
+ (zoom <= 8 && (bounds.getNorth() > 45 || bounds.getSouth() < -45))
+ ) {
+ label = '~' + label;
+ }
+ this.setState({
+ width: MAX_WIDTH * (scaleDistance / maxDistance),
+ label,
+ });
+ }
+
+ render() {
+ return (
+
+ {this.state.label}
+
+ );
+ }
+}
diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts
index 37fd19ea5a47..5e508b37fdde 100644
--- a/x-pack/plugins/maps/public/reducers/default_map_settings.ts
+++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts
@@ -22,6 +22,7 @@ export function getDefaultMapSettings(): MapSettings {
browserLocation: { zoom: 2 },
maxZoom: MAX_ZOOM,
minZoom: MIN_ZOOM,
+ showScaleControl: false,
showSpatialFilters: true,
spatialFiltersAlpa: 0.3,
spatialFiltersFillColor: '#DA8B45',
diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts
index da185a190e6d..273d1de6fddf 100644
--- a/x-pack/plugins/maps/public/reducers/map.d.ts
+++ b/x-pack/plugins/maps/public/reducers/map.d.ts
@@ -55,6 +55,7 @@ export type MapSettings = {
};
maxZoom: number;
minZoom: number;
+ showScaleControl: boolean;
showSpatialFilters: boolean;
spatialFiltersAlpa: number;
spatialFiltersFillColor: string;