Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Maps] Move tooltips to store (#32333) #32608

Merged
merged 1 commit into from
Mar 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions x-pack/plugins/maps/public/actions/store_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getMapReady,
getWaitingForMapReadyLayerListRaw,
getTransientLayerId,
getTooltipState
} from '../selectors/map_selectors';
import { updateFlyout, FLYOUT_STATE } from '../store/ui';
import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants';
Expand Down Expand Up @@ -49,6 +50,7 @@ export const CLEAR_GOTO = 'CLEAR_GOTO';
export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE';
export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE';
export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE';
export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE';

function getLayerLoadingCallbacks(dispatch, layerId) {
return {
Expand Down Expand Up @@ -146,6 +148,15 @@ export function setLayerErrorStatus(layerId, errorMessage) {
};
}

export function clearTooltipStateForLayer(layerId) {
return (dispatch, getState) => {
const tooltipState = getTooltipState(getState());
if (tooltipState && tooltipState.layerId === layerId) {
dispatch(setTooltipState(null));
}
};
}

export function toggleLayerVisible(layerId) {
return async (dispatch, getState) => {
//if the current-state is invisible, we also want to sync data
Expand All @@ -157,6 +168,11 @@ export function toggleLayerVisible(layerId) {
return;
}
const makeVisible = !layer.isVisible();

if (!makeVisible) {
dispatch(clearTooltipStateForLayer(layerId));
}

await dispatch({
type: TOGGLE_LAYER_VISIBLE,
layerId
Expand Down Expand Up @@ -195,7 +211,7 @@ export function removeTransientLayer() {
}

export function setTransientLayer(layerId) {
return {
return {
type: SET_TRANSIENT_LAYER,
transientLayerId: layerId,
};
Expand Down Expand Up @@ -284,11 +300,18 @@ export function mapExtentChanged(newMapConstants) {
...newMapConstants
}
});
const newDataFilters = { ...dataFilters, ...newMapConstants };
const newDataFilters = { ...dataFilters, ...newMapConstants };
await syncDataForAllLayers(getState, dispatch, newDataFilters);
};
}

export function setTooltipState(tooltipState) {
return {
type: 'SET_TOOLTIP_STATE',
tooltipState: tooltipState
};
}

export function setMouseCoordinates({ lat, lon }) {
let safeLon = lon;
if (lon > 180) {
Expand Down Expand Up @@ -463,18 +486,19 @@ export function removeSelectedLayer() {
};
}

export function removeLayer(id) {
export function removeLayer(layerId) {
return (dispatch, getState) => {
const layerGettingRemoved = getLayerList(getState()).find(layer => {
return id === layer.getId();
return layerId === layer.getId();
});
if (layerGettingRemoved) {
layerGettingRemoved.destroy();
if (!layerGettingRemoved) {
return;
}

dispatch(clearTooltipStateForLayer(layerId));
layerGettingRemoved.destroy();
dispatch({
type: REMOVE_LAYER,
id
id: layerId
});
};
}
Expand Down
11 changes: 7 additions & 4 deletions x-pack/plugins/maps/public/components/map/mb/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
setMouseCoordinates,
clearMouseCoordinates,
clearGoto,
setLayerErrorStatus,
setTooltipState
} from '../../../actions/store_actions';
import { getLayerList, getMapReady, getGoto } from '../../../selectors/map_selectors';
import { getTooltipState, getLayerList, getMapReady, getGoto } from '../../../selectors/map_selectors';
import { getInspectorAdapters } from '../../../store/non_serializable_instances';

function mapStateToProps(state = {}) {
Expand All @@ -24,6 +24,7 @@ function mapStateToProps(state = {}) {
layerList: getLayerList(state),
goto: getGoto(state),
inspectorAdapters: getInspectorAdapters(state),
tooltipState: getTooltipState(state)
};
}

Expand All @@ -49,8 +50,10 @@ function mapDispatchToProps(dispatch) {
clearGoto: () => {
dispatch(clearGoto());
},
setLayerErrorStatus: (id, msg) =>
dispatch(setLayerErrorStatus(id, msg))
setTooltipState(tooltipState) {
dispatch(setTooltipState(tooltipState));
}

};
}

Expand Down
181 changes: 115 additions & 66 deletions x-pack/plugins/maps/public/components/map/mb/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@

import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import { ResizeChecker } from 'ui/resize_checker';
import { syncLayerOrder, removeOrphanedSourcesAndLayers, createMbMapInstance } from './utils';
import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants';
import mapboxgl from 'mapbox-gl';
import { FeatureTooltip } from '../feature_tooltip';

export class MBMapContainer extends React.Component {

constructor() {
super();
this._mbMap = null;
this._listeners = new Map(); // key is mbLayerId, value eventHandlers map
this._tooltipContainer = document.createElement('div');
this._mbPopup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
});
}

_debouncedSync = _.debounce(() => {
Expand All @@ -26,6 +32,61 @@ export class MBMapContainer extends React.Component {
}
}, 256);

_updateTooltipState = _.debounce(async (e) => {

const mbLayerIds = this._getMbLayerIdsForTooltips();
const features = this._mbMap.queryRenderedFeatures(e.point, { layers: mbLayerIds });

if (!features.length) {
this.props.setTooltipState(null);
return;
}

const targetFeature = features[0];
if (this.props.tooltipState) {
const propertiesUnchanged = _.isEqual(this.props.tooltipState.activeFeature.properties, targetFeature.properties);
const geometryUnchanged = _.isEqual(this.props.tooltipState.activeFeature.geometry, targetFeature.geometry);
if(propertiesUnchanged && geometryUnchanged) {
return;
}
}

const layer = this._getLayer(targetFeature.layer.id);
const formattedProperties = await layer.getPropertiesForTooltip(targetFeature.properties);

let popupAnchorLocation = [e.lngLat.lng, e.lngLat.lat]; // default popup location to mouse location
if (targetFeature.geometry.type === 'Point') {
const coordinates = targetFeature.geometry.coordinates.slice();

// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
// over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}

popupAnchorLocation = coordinates;
}

this.props.setTooltipState({
activeFeature: {
properties: targetFeature.properties,
geometry: targetFeature.geometry
},
formattedProperties: formattedProperties,
layerId: layer.getId(),
location: popupAnchorLocation
});

}, 100);


_getMbLayerIdsForTooltips() {
return this.props.layerList.reduce((mbLayerIds, layer) => {
return layer.canShowTooltip() ? mbLayerIds.concat(layer.getMbLayerIds()) : mbLayerIds;
}, []);
}

_getMapState() {
const zoom = this._mbMap.getZoom();
const mbCenter = this._mbMap.getCenter();
Expand All @@ -45,6 +106,13 @@ export class MBMapContainer extends React.Component {
};
}

componentDidUpdate() {
// do not debounce syncing of map-state and tooltip
this._syncMbMapWithMapState();
this._syncTooltipState();
this._debouncedSync();
}

componentDidMount() {
this._initializeMap();
this._isMounted = true;
Expand All @@ -58,6 +126,7 @@ export class MBMapContainer extends React.Component {
if (this._mbMap) {
this._mbMap.remove();
this._mbMap = null;
this._tooltipContainer = null;
}
this.props.onMapDestroyed();
}
Expand All @@ -70,29 +139,6 @@ export class MBMapContainer extends React.Component {
return;
}

// Override mapboxgl.Map "on" and "removeLayer" methods so we can track layer listeners
// Tracked layer listerners are used to clean up event handlers
const originalMbBoxOnFunc = this._mbMap.on;
const originalMbBoxRemoveLayerFunc = this._mbMap.removeLayer;
this._mbMap.on = (...args) => {
// args do not identify layer so there is nothing to track
if (args.length <= 2) {
originalMbBoxOnFunc.apply(this._mbMap, args);
return;
}

const eventType = args[0];
const mbLayerId = args[1];
const handler = args[2];
this._addListener(eventType, mbLayerId, handler);

originalMbBoxOnFunc.apply(this._mbMap, args);
};
this._mbMap.removeLayer = (id) => {
this._removeListeners(id);
originalMbBoxRemoveLayerFunc.apply(this._mbMap, [id]);
};

this._initResizerChecker();

// moveend callback is debounced to avoid updating map extent state while map extent is still changing
Expand All @@ -115,44 +161,48 @@ export class MBMapContainer extends React.Component {
this.props.clearMouseCoordinates();
});


this._mbMap.on('mousemove', this._updateTooltipState);

this.props.onMapReady(this._getMapState());
}

_addListener(eventType, mbLayerId, handler) {
this._removeListener(eventType, mbLayerId);

const eventHandlers = !this._listeners.has(mbLayerId)
? new Map()
: this._listeners.get(mbLayerId);
eventHandlers.set(eventType, handler);
this._listeners.set(mbLayerId, eventHandlers);
_initResizerChecker() {
this._checker = new ResizeChecker(this.refs.mapContainer);
this._checker.on('resize', () => {
this._mbMap.resize();
});
}

_removeListeners(mbLayerId) {
if (this._listeners.has(mbLayerId)) {
const eventHandlers = this._listeners.get(mbLayerId);
eventHandlers.forEach((value, eventType) => {
this._removeListener(eventType, mbLayerId);
});
this._listeners.delete(mbLayerId);
_hideTooltip() {
if (this._mbPopup.isOpen()) {
this._mbPopup.remove();
ReactDOM.unmountComponentAtNode(this._tooltipContainer);
}
}

_removeListener(eventType, mbLayerId) {
if (this._listeners.has(mbLayerId)) {
const eventHandlers = this._listeners.get(mbLayerId);
if (eventHandlers.has(eventType)) {
this._mbMap.off(eventType, mbLayerId, eventHandlers.get(eventType));
eventHandlers.delete(eventType);
}
}
_showTooltip() {
//todo: can still be optimized. No need to rerender if content remains identical
ReactDOM.render(
React.createElement(
FeatureTooltip, {
properties: this.props.tooltipState.formattedProperties,
}
),
this._tooltipContainer
);

this._mbPopup.setLngLat(this.props.tooltipState.location)
.setDOMContent(this._tooltipContainer)
.addTo(this._mbMap);
}

_initResizerChecker() {
this._checker = new ResizeChecker(this.refs.mapContainer);
this._checker.on('resize', () => {
this._mbMap.resize();
});
_syncTooltipState() {
if (this.props.tooltipState) {
this._showTooltip();
} else {
this._hideTooltip();
}
}

_syncMbMapWithMapState = () => {
Expand Down Expand Up @@ -188,21 +238,25 @@ export class MBMapContainer extends React.Component {

};

_getLayer(mbLayerId) {
return this.props.layerList.find((layer) => {
const mbLayerIds = layer.getMbLayerIds();
return mbLayerIds.indexOf(mbLayerId) > -1;
});
}

_syncMbMapWithLayerList = () => {
const {
isMapReady,
layerList,
} = this.props;

if (!isMapReady) {
if (!this.props.isMapReady) {
return;
}

removeOrphanedSourcesAndLayers(this._mbMap, layerList);
layerList.forEach(layer => {
removeOrphanedSourcesAndLayers(this._mbMap, this.props.layerList);
this.props.layerList.forEach(layer => {
layer.syncLayerWithMB(this._mbMap);
});
syncLayerOrder(this._mbMap, layerList);

syncLayerOrder(this._mbMap, this.props.layerList);
};

_syncMbMapWithInspector = () => {
Expand All @@ -222,12 +276,7 @@ export class MBMapContainer extends React.Component {
};

render() {
// do not debounce syncing zoom and center
this._syncMbMapWithMapState();
this._debouncedSync();
return (
<div id={'mapContainer'} className="mapContainer" ref="mapContainer"/>
);
return (<div id={'mapContainer'} className="mapContainer" ref="mapContainer"/>);
}
}

Expand Down
Loading