Skip to content

Commit

Permalink
[Maps] Move tooltips to store (#32333)
Browse files Browse the repository at this point in the history
This is an internal refactor:
- move tooltip management out of layers, and to the mapbox-component
- use global handler iso of multiple handlers on individual layers (this did remove the cursor-pointer change, since we no longer are explicitly handling on-enter/leave events).
- put tooltip state in store

Fixes bugs:
- when layer is removed, any corresponding tooltip should be removed as well
- when layer is made invisible, any corresponding tooltip should be removed as well
  • Loading branch information
thomasneirynck authored Mar 7, 2019
1 parent 89a89e3 commit 6b6e670
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 187 deletions.
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

0 comments on commit 6b6e670

Please sign in to comment.