[Maps] scale control (#88031)
* set width * cleanup * add show scale toggle in map settings * tslint * remove unmapped css comment * move scale for full screen mode so they do not collide * change default and add unit test for scale_control * simplify scale * add tilda to label * update jest expects * eslint * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
60f8b24529
commit
c272d35b65
|
@ -3,3 +3,4 @@
|
|||
@import 'widget_overlay/index';
|
||||
@import 'toolbar_overlay/index';
|
||||
@import 'mb_map/features_tooltip/index';
|
||||
@import 'mb_map/scale_control/index';
|
||||
|
|
|
@ -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 (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
|
@ -43,6 +47,17 @@ export function DisplayPanel({ settings, updateMapSetting }: Props) {
|
|||
onChange={onBackgroundColorChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.maps.mapSettingsPanel.showScaleLabel', {
|
||||
defaultMessage: 'Show scale',
|
||||
})}
|
||||
checked={settings.showScaleControl}
|
||||
onChange={onShowScale}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Props, State> {
|
|||
render() {
|
||||
let drawControl;
|
||||
let tooltipControl;
|
||||
let scaleControl;
|
||||
if (this.state.mbMap) {
|
||||
drawControl = <DrawControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />;
|
||||
tooltipControl = !this.props.settings.disableTooltipControl ? (
|
||||
|
@ -394,6 +397,9 @@ export class MBMap extends Component<Props, State> {
|
|||
renderTooltipContent={this.props.renderTooltipContent}
|
||||
/>
|
||||
) : null;
|
||||
scaleControl = this.props.settings.showScaleControl ? (
|
||||
<ScaleControl mbMap={this.state.mbMap} isFullScreen={this.props.isFullScreen} />
|
||||
) : null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
@ -403,6 +409,7 @@ export class MBMap extends Component<Props, State> {
|
|||
data-test-subj="mapContainer"
|
||||
>
|
||||
{drawControl}
|
||||
{scaleControl}
|
||||
{tooltipControl}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`isFullScreen 1`] = `
|
||||
<div
|
||||
className="mapScaleControl mapScaleControlFullScreen"
|
||||
style={
|
||||
Object {
|
||||
"width": "62.7474245034397px",
|
||||
}
|
||||
}
|
||||
>
|
||||
~50 km
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`render 1`] = `
|
||||
<div
|
||||
className="mapScaleControl"
|
||||
style={
|
||||
Object {
|
||||
"width": "62.7474245034397px",
|
||||
}
|
||||
}
|
||||
>
|
||||
~50 km
|
||||
</div>
|
||||
`;
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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(<ScaleControl mbMap={mockMBMap} isFullScreen={false} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('isFullScreen', () => {
|
||||
const component = shallow(<ScaleControl mbMap={mockMBMap} isFullScreen={true} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should un-register all map callbacks on unmount', () => {
|
||||
const component = mount(<ScaleControl mbMap={mockMBMap} isFullScreen={false} />);
|
||||
|
||||
expect(Object.keys(mockMbMapHandlers).length).toBe(1);
|
||||
|
||||
component.unmount();
|
||||
expect(Object.keys(mockMbMapHandlers).length).toBe(0);
|
||||
});
|
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<div
|
||||
className={classNames('mapScaleControl', {
|
||||
mapScaleControlFullScreen: this.props.isFullScreen,
|
||||
})}
|
||||
style={{ width: `${this.state.width}px` }}
|
||||
>
|
||||
{this.state.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
1
x-pack/plugins/maps/public/reducers/map.d.ts
vendored
1
x-pack/plugins/maps/public/reducers/map.d.ts
vendored
|
@ -55,6 +55,7 @@ export type MapSettings = {
|
|||
};
|
||||
maxZoom: number;
|
||||
minZoom: number;
|
||||
showScaleControl: boolean;
|
||||
showSpatialFilters: boolean;
|
||||
spatialFiltersAlpa: number;
|
||||
spatialFiltersFillColor: string;
|
||||
|
|
Loading…
Reference in a new issue