diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss index 19c11d3fde66..a1a65796dc94 100644 --- a/x-pack/plugins/maps/public/connected_components/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/_index.scss @@ -3,3 +3,4 @@ @import 'widget_overlay/index'; @import 'toolbar_overlay/index'; @import 'mb_map/features_tooltip/index'; +@import 'mb_map/scale_control/index'; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/display_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/display_panel.tsx index f0b9fae04c0a..02aa366bb15f 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/display_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/display_panel.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiFormRow, EuiPanel, EuiSwitch, EuiSwitchEvent, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; @@ -21,6 +21,10 @@ export function DisplayPanel({ settings, updateMapSetting }: Props) { updateMapSetting('backgroundColor', color); }; + const onShowScale = (event: EuiSwitchEvent) => { + updateMapSetting('showScaleControl', event.target.checked); + }; + return ( @@ -43,6 +47,17 @@ export function DisplayPanel({ settings, updateMapSetting }: Props) { onChange={onBackgroundColorChange} /> + + + + ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index a624381781e5..fd26abdc57f3 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -26,6 +26,7 @@ import { getSpatialFiltersLayer, getMapSettings, } from '../../selectors/map_selectors'; +import { getIsFullScreen } from '../../selectors/ui_selectors'; import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; import { MapStoreState } from '../../reducers/store'; @@ -38,6 +39,7 @@ function mapStateToProps(state: MapStoreState) { goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), scrollZoom: getScrollZoom(state), + isFullScreen: getIsFullScreen(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 5f6c7369fd23..4dc765f1704a 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -18,6 +18,7 @@ import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; // @ts-expect-error import { DrawControl } from './draw_control'; +import { ScaleControl } from './scale_control'; // @ts-expect-error import { TooltipControl } from './tooltip_control'; import { clampToLatBounds, clampToLonBounds } from '../../../common/elasticsearch_util'; @@ -55,6 +56,7 @@ interface Props { spatialFiltersLayer: ILayer; goto?: Goto | null; inspectorAdapters: Adapters; + isFullScreen: boolean; scrollZoom: boolean; extentChanged: (mapExtentState: MapExtentState) => void; onMapReady: (mapExtentState: MapExtentState) => void; @@ -381,6 +383,7 @@ export class MBMap extends Component { render() { let drawControl; let tooltipControl; + let scaleControl; if (this.state.mbMap) { drawControl = ; tooltipControl = !this.props.settings.disableTooltipControl ? ( @@ -394,6 +397,9 @@ export class MBMap extends Component { 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;