diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index 413b440279d77b..3bda29964a9a1e 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -13,6 +13,7 @@ import { MapFilters, MapCenterAndZoom, MapRefreshConfig, + MapExtent, } from '../../common/descriptor_types'; import { MapSettings } from '../reducers/map'; @@ -34,6 +35,9 @@ export function updateSourceProp( ): void; export function setGotoWithCenter(config: MapCenterAndZoom): AnyAction; +export function setGotoWithBounds(config: MapExtent): AnyAction; + +export function fitToDataBounds(): AnyAction; export function replaceLayerList(layerList: unknown[]): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js index da6ba6b481054b..ea2602397702b6 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.js +++ b/x-pack/plugins/maps/public/actions/map_actions.js @@ -19,6 +19,7 @@ import { getOpenTooltips, getQuery, getDataRequestDescriptor, + getFittableLayers, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; @@ -567,6 +568,55 @@ export function fitToLayerExtent(layerId) { }; } +export function fitToDataBounds() { + return async function(dispatch, getState) { + const layerList = getFittableLayers(getState()); + + if (!layerList.length) { + return; + } + + const dataFilters = getDataFilters(getState()); + const boundsPromises = layerList.map(async layer => { + return layer.getBounds(dataFilters); + }); + + const bounds = await Promise.all(boundsPromises); + const corners = []; + for (let i = 0; i < bounds.length; i++) { + const b = bounds[i]; + + //filter out undefined bounds (uses Infinity due to turf responses) + + if ( + b.minLon === Infinity || + b.maxLon === Infinity || + b.minLat === -Infinity || + b.maxLat === -Infinity + ) { + continue; + } + + corners.push([b.minLon, b.minLat]); + corners.push([b.maxLon, b.maxLat]); + } + + if (!corners.length) { + return; + } + + const turfUnionBbox = turf.bbox(turf.multiPoint(corners)); + const dataBounds = { + minLon: turfUnionBbox[0], + minLat: turfUnionBbox[1], + maxLon: turfUnionBbox[2], + maxLat: turfUnionBbox[3], + }; + + dispatch(setGotoWithBounds(dataBounds)); + }; +} + export function setGotoWithBounds(bounds) { return { type: SET_GOTO, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap new file mode 100644 index 00000000000000..3407bcfd4f845f --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Must render zoom tools 1`] = ` + + + + + + + + +`; + +exports[`Must zoom tools and draw filter tools 1`] = ` + + + + + + + + + + + +`; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss index 2754a3e204263f..e92e89b1703709 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss @@ -12,6 +12,7 @@ // sass-lint:disable-block no-important background-color: $euiColorEmptyShade !important; pointer-events: all; + position: relative; &:enabled, &:enabled:hover, diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx new file mode 100644 index 00000000000000..0b168badb2f3ff --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ILayer } from '../../../layers/layer'; + +interface Props { + layerList: ILayer[]; + fitToBounds: () => void; +} + +export const FitToData: React.FunctionComponent = ({ layerList, fitToBounds }: Props) => { + if (layerList.length === 0) { + return null; + } + + return ( + + ); +}; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts new file mode 100644 index 00000000000000..d6ded62f2f480c --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../../reducers/store'; +import { fitToDataBounds } from '../../../actions/map_actions'; +import { getFittableLayers } from '../../../selectors/map_selectors'; +import { FitToData } from './fit_to_data'; + +function mapStateToProps(state: MapStoreState) { + return { + layerList: getFittableLayers(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + fitToBounds: () => { + dispatch(fitToDataBounds()); + }, + }; +} + +const connectedFitToData = connect(mapStateToProps, mapDispatchToProps)(FitToData); +export { connectedFitToData as FitToData }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js index 32668be8f8f676..a4f85163512f77 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SetViewControl } from './set_view_control'; import { ToolsControl } from './tools_control'; +import { FitToData } from './fit_to_data'; export class ToolbarOverlay extends React.Component { _renderToolsControl() { @@ -36,6 +37,10 @@ export class ToolbarOverlay extends React.Component { + + + + {this._renderToolsControl()} ); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx new file mode 100644 index 00000000000000..c03aa1e0985405 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -0,0 +1,21 @@ +/* + * 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 { shallow } from 'enzyme'; + +// @ts-ignore +import { ToolbarOverlay } from './toolbar_overlay'; + +test('Must render zoom tools', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); +}); + +test('Must zoom tools and draw filter tools', async () => { + const component = shallow( {}} geoFields={['coordinates']} />); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap index b8c652909408ac..388712e1ebccad 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -72,7 +72,7 @@ exports[`TOCEntryActionsPopover is rendered 1`] = ` "disabled": false, "icon": , "name": "Fit to data", "onClick": [Function], @@ -200,7 +200,7 @@ exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBou "disabled": true, "icon": , "name": "Fit to data", "onClick": [Function], @@ -328,7 +328,7 @@ exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1 "disabled": false, "icon": , "name": "Fit to data", "onClick": [Function], diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index e0fdff62829fb9..dfc93c29263ee8 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -137,7 +137,7 @@ export class TOCEntryActionsPopover extends Component { name: i18n.translate('xpack.maps.layerTocActions.fitToDataTitle', { defaultMessage: 'Fit to data', }), - icon: , + icon: , 'data-test-subj': 'fitToBoundsButton', toolTipContent: this.state.supportsFitToBounds ? null diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index bc881d06f62ced..9caa151db6d5a4 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnyAction } from 'redux'; import { MapCenter } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; import { MapSettings } from '../reducers/map'; import { IVectorLayer } from '../layers/vector_layer'; +import { ILayer } from '../layers/layer'; export function getHiddenLayerIds(state: MapStoreState): string[]; @@ -25,3 +25,6 @@ export function hasMapSettingsChanges(state: MapStoreState): boolean; export function isUsingSearch(state: MapStoreState): boolean; export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; + +export function getLayerList(state: MapStoreState): ILayer[]; +export function getFittableLayers(state: MapStoreState): ILayer[]; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index f43c92d4c99457..38a862973623a2 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -251,6 +251,19 @@ export const getLayerList = createSelector( } ); +export const getFittableLayers = createSelector(getLayerList, layerList => { + return layerList.filter(layer => { + //These are the only layer-types that implement bounding-box retrieval reliably + //This will _not_ work if Maps will allow register custom layer types + const isFittable = + layer.getType() === LAYER_TYPE.VECTOR || + layer.getType() === LAYER_TYPE.BLENDED_VECTOR || + layer.getType() === LAYER_TYPE.HEATMAP; + + return isFittable && layer.isVisible(); + }); +}); + export const getHiddenLayerIds = createSelector(getLayerListRaw, layers => layers.filter(layer => !layer.visible).map(layer => layer.id) );