Skip to content

Commit

Permalink
[Maps] Add global fit to data (#64702) (#65261)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasneirynck committed May 5, 2020
1 parent 4b4bab7 commit 1f55d7f
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 5 deletions.
4 changes: 4 additions & 0 deletions x-pack/plugins/maps/public/actions/map_actions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MapFilters,
MapCenterAndZoom,
MapRefreshConfig,
MapExtent,
} from '../../common/descriptor_types';
import { MapSettings } from '../reducers/map';

Expand All @@ -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;

Expand Down
50 changes: 50 additions & 0 deletions x-pack/plugins/maps/public/actions/map_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getOpenTooltips,
getQuery,
getDataRequestDescriptor,
getFittableLayers,
} from '../selectors/map_selectors';

import { FLYOUT_STATE } from '../reducers/ui';
Expand Down Expand Up @@ -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,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// sass-lint:disable-block no-important
background-color: $euiColorEmptyShade !important;
pointer-events: all;
position: relative;

&:enabled,
&:enabled:hover,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ layerList, fitToBounds }: Props) => {
if (layerList.length === 0) {
return null;
}

return (
<EuiButtonIcon
className="mapToolbarOverlay__button"
onClick={fitToBounds}
data-test-subj="fitToData"
iconType="expand"
color="text"
aria-label={i18n.translate('xpack.maps.fitToData.fitButtonLabel', {
defaultMessage: 'Fit to data bounds',
})}
title={i18n.translate('xpack.maps.fitToData.fitAriaLabel', {
defaultMessage: 'Fit to data bounds',
})}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<AnyAction>) {
return {
fitToBounds: () => {
dispatch(fitToDataBounds());
},
};
}

const connectedFitToData = connect(mapStateToProps, mapDispatchToProps)(FitToData);
export { connectedFitToData as FitToData };
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -36,6 +37,10 @@ export class ToolbarOverlay extends React.Component {
<SetViewControl />
</EuiFlexItem>

<EuiFlexItem>
<FitToData />
</EuiFlexItem>

{this._renderToolsControl()}
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<ToolbarOverlay />);
expect(component).toMatchSnapshot();
});

test('Must zoom tools and draw filter tools', async () => {
const component = shallow(<ToolbarOverlay addFilters={() => {}} geoFields={['coordinates']} />);
expect(component).toMatchSnapshot();
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class TOCEntryActionsPopover extends Component<Props, State> {
name: i18n.translate('xpack.maps.layerTocActions.fitToDataTitle', {
defaultMessage: 'Fit to data',
}),
icon: <EuiIcon type="search" size="m" />,
icon: <EuiIcon type="expand" size="m" />,
'data-test-subj': 'fitToBoundsButton',
toolTipContent: this.state.supportsFitToBounds
? null
Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/maps/public/selectors/map_selectors.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];

Expand All @@ -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[];
13 changes: 13 additions & 0 deletions x-pack/plugins/maps/public/selectors/map_selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Expand Down

0 comments on commit 1f55d7f

Please sign in to comment.