mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Maps] Move tooltips to store (#32333)
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
This commit is contained in:
parent
89a89e3ad4
commit
6b6e670586
9 changed files with 221 additions and 190 deletions
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -195,7 +211,7 @@ export function removeTransientLayer() {
|
|||
}
|
||||
|
||||
export function setTransientLayer(layerId) {
|
||||
return {
|
||||
return {
|
||||
type: SET_TRANSIENT_LAYER,
|
||||
transientLayerId: layerId,
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 = {}) {
|
||||
|
@ -24,6 +24,7 @@ function mapStateToProps(state = {}) {
|
|||
layerList: getLayerList(state),
|
||||
goto: getGoto(state),
|
||||
inspectorAdapters: getInspectorAdapters(state),
|
||||
tooltipState: getTooltipState(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -49,8 +50,10 @@ function mapDispatchToProps(dispatch) {
|
|||
clearGoto: () => {
|
||||
dispatch(clearGoto());
|
||||
},
|
||||
setLayerErrorStatus: (id, msg) =>
|
||||
dispatch(setLayerErrorStatus(id, msg))
|
||||
setTooltipState(tooltipState) {
|
||||
dispatch(setTooltipState(tooltipState));
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -115,39 +161,12 @@ 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);
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_initResizerChecker() {
|
||||
this._checker = new ResizeChecker(this.refs.mapContainer);
|
||||
this._checker.on('resize', () => {
|
||||
|
@ -155,6 +174,37 @@ export class MBMapContainer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_hideTooltip() {
|
||||
if (this._mbPopup.isOpen()) {
|
||||
this._mbPopup.remove();
|
||||
ReactDOM.unmountComponentAtNode(this._tooltipContainer);
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
_syncTooltipState() {
|
||||
if (this.props.tooltipState) {
|
||||
this._showTooltip();
|
||||
} else {
|
||||
this._hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
_syncMbMapWithMapState = () => {
|
||||
const {
|
||||
isMapReady,
|
||||
|
@ -188,21 +238,25 @@ export class MBMapContainer extends React.Component {
|
|||
|
||||
};
|
||||
|
||||
_syncMbMapWithLayerList = () => {
|
||||
const {
|
||||
isMapReady,
|
||||
layerList,
|
||||
} = this.props;
|
||||
_getLayer(mbLayerId) {
|
||||
return this.props.layerList.find((layer) => {
|
||||
const mbLayerIds = layer.getMbLayerIds();
|
||||
return mbLayerIds.indexOf(mbLayerId) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMapReady) {
|
||||
_syncMbMapWithLayerList = () => {
|
||||
|
||||
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 = () => {
|
||||
|
@ -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"/>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,10 @@ function createStyleInstance(styleDescriptor) {
|
|||
}
|
||||
}
|
||||
|
||||
export const getTooltipState = ({ map }) => {
|
||||
return map.tooltipState;
|
||||
};
|
||||
|
||||
export const getMapReady = ({ map }) => map && map.ready;
|
||||
|
||||
export const getGoto = ({ map }) => map && map.goto;
|
||||
|
|
|
@ -45,10 +45,19 @@ export class HeatmapLayer extends AbstractLayer {
|
|||
return metricfields[0].propertyKey;
|
||||
}
|
||||
|
||||
|
||||
_getMbLayerId() {
|
||||
return this.getId() + '_heatmap';
|
||||
}
|
||||
|
||||
getMbLayerIds() {
|
||||
return [this._getMbLayerId()];
|
||||
}
|
||||
|
||||
syncLayerWithMB(mbMap) {
|
||||
|
||||
const mbSource = mbMap.getSource(this.getId());
|
||||
const mbLayerId = this.getId() + '_heatmap';
|
||||
const mbLayerId = this._getMbLayerId();
|
||||
|
||||
if (!mbSource) {
|
||||
mbMap.addSource(this.getId(), {
|
||||
|
|
|
@ -165,6 +165,14 @@ export class AbstractLayer {
|
|||
//no-op by default
|
||||
}
|
||||
|
||||
getMbLayerIds() {
|
||||
throw new Error('Should implement AbstractLayer#getMbLayerIds');
|
||||
}
|
||||
|
||||
canShowTooltip() {
|
||||
return false;
|
||||
}
|
||||
|
||||
syncLayerWithMb() {
|
||||
//no-op by default
|
||||
}
|
||||
|
|
|
@ -49,10 +49,18 @@ export class TileLayer extends AbstractLayer {
|
|||
}
|
||||
}
|
||||
|
||||
_getMbLayerId() {
|
||||
return this.getId() + '_raster';
|
||||
}
|
||||
|
||||
getMbLayerIds() {
|
||||
return [this._getMbLayerId()];
|
||||
}
|
||||
|
||||
syncLayerWithMB(mbMap) {
|
||||
|
||||
const source = mbMap.getSource(this.getId());
|
||||
const mbLayerId = this.getId() + '_raster';
|
||||
const mbLayerId = this._getMbLayerId();
|
||||
|
||||
if (!source) {
|
||||
const sourceDataRequest = this.getSourceDataRequest();
|
||||
|
|
|
@ -4,16 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import turf from 'turf';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { AbstractLayer } from './layer';
|
||||
import { VectorStyle } from './styles/vector_style';
|
||||
import { LeftInnerJoin } from './joins/left_inner_join';
|
||||
import { SOURCE_DATA_ID_ORIGIN } from '../../../common/constants';
|
||||
import { FeatureTooltip } from '../../components/map/feature_tooltip';
|
||||
import _ from 'lodash';
|
||||
|
||||
const EMPTY_FEATURE_COLLECTION = {
|
||||
|
@ -25,13 +20,6 @@ export class VectorLayer extends AbstractLayer {
|
|||
|
||||
static type = 'VECTOR';
|
||||
|
||||
static popup = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
});
|
||||
|
||||
static tooltipContainer = document.createElement('div');
|
||||
|
||||
static createDescriptor(options, mapColors) {
|
||||
const layerDescriptor = super.createDescriptor(options);
|
||||
layerDescriptor.type = VectorLayer.type;
|
||||
|
@ -171,10 +159,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
!isGeoGridPrecisionAware
|
||||
) {
|
||||
const sourceDataRequest = this._findDataRequestForSource(sourceDataId);
|
||||
if (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress());
|
||||
}
|
||||
|
||||
const sourceDataRequest = this._findDataRequestForSource(sourceDataId);
|
||||
|
@ -397,7 +382,7 @@ export class VectorLayer extends AbstractLayer {
|
|||
|
||||
_setMbPointsProperties(mbMap) {
|
||||
const sourceId = this.getId();
|
||||
const pointLayerId = this.getId() + '_circle';
|
||||
const pointLayerId = this._getMbPointLayerId();
|
||||
const pointLayer = mbMap.getLayer(pointLayerId);
|
||||
if (!pointLayer) {
|
||||
mbMap.addLayer({
|
||||
|
@ -415,13 +400,12 @@ export class VectorLayer extends AbstractLayer {
|
|||
});
|
||||
mbMap.setLayoutProperty(pointLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
|
||||
mbMap.setLayerZoomRange(pointLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
||||
this._addTooltipListeners(mbMap, pointLayerId);
|
||||
}
|
||||
|
||||
_setMbLinePolygonProperties(mbMap) {
|
||||
const sourceId = this.getId();
|
||||
const fillLayerId = this.getId() + '_fill';
|
||||
const lineLayerId = this.getId() + '_line';
|
||||
const fillLayerId = this._getMbPolygonLayerId();
|
||||
const lineLayerId = this._getMbLineLayerId();
|
||||
if (!mbMap.getLayer(fillLayerId)) {
|
||||
mbMap.addLayer({
|
||||
id: fillLayerId,
|
||||
|
@ -462,7 +446,6 @@ export class VectorLayer extends AbstractLayer {
|
|||
mbMap.setLayoutProperty(lineLayerId, 'visibility', this.isVisible() ? 'visible' : 'none');
|
||||
mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
||||
mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
|
||||
this._addTooltipListeners(mbMap, fillLayerId);
|
||||
}
|
||||
|
||||
_syncStylePropertiesWithMb(mbMap) {
|
||||
|
@ -493,101 +476,37 @@ export class VectorLayer extends AbstractLayer {
|
|||
});
|
||||
}
|
||||
|
||||
_canShowTooltips() {
|
||||
return this._source.canFormatFeatureProperties();
|
||||
_getMbPointLayerId() {
|
||||
return this.getId() + '_circle';
|
||||
}
|
||||
|
||||
async _getPropertiesForTooltip(feature) {
|
||||
const tooltipsFromSource = await this._source.filterAndFormatProperties(feature.properties);
|
||||
_getMbLineLayerId() {
|
||||
return this.getId() + '_line';
|
||||
}
|
||||
|
||||
_getMbPolygonLayerId() {
|
||||
return this.getId() + '_fill';
|
||||
}
|
||||
|
||||
getMbLayerIds() {
|
||||
return [this._getMbPointLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId()];
|
||||
}
|
||||
|
||||
async getPropertiesForTooltip(properties) {
|
||||
const tooltipsFromSource = await this._source.filterAndFormatProperties(properties);
|
||||
|
||||
//add tooltips from joins
|
||||
const allProps = this._joins.reduce((acc, join) => {
|
||||
const propsFromJoin = join.filterAndFormatPropertiesForTooltip(feature.properties);
|
||||
return this._joins.reduce((acc, join) => {
|
||||
const propsFromJoin = join.filterAndFormatPropertiesForTooltip(properties);
|
||||
return {
|
||||
...propsFromJoin,
|
||||
...acc,
|
||||
};
|
||||
}, { ...tooltipsFromSource });
|
||||
|
||||
return allProps;
|
||||
}
|
||||
|
||||
_addTooltipListeners(mbMap, mbLayerId) {
|
||||
|
||||
if (!this._canShowTooltips()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showTooltip = async (feature, eventLngLat) => {
|
||||
let popupAnchorLocation = eventLngLat; // default popup location to mouse location
|
||||
if (feature.geometry.type === 'Point') {
|
||||
const coordinates = feature.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(eventLngLat.lng - coordinates[0]) > 180) {
|
||||
coordinates[0] += eventLngLat.lng > coordinates[0] ? 360 : -360;
|
||||
}
|
||||
|
||||
popupAnchorLocation = coordinates;
|
||||
}
|
||||
|
||||
const properties = await this._getPropertiesForTooltip(feature);
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
FeatureTooltip, {
|
||||
properties: properties,
|
||||
}
|
||||
),
|
||||
VectorLayer.tooltipContainer
|
||||
);
|
||||
|
||||
VectorLayer.popup.setLngLat(popupAnchorLocation)
|
||||
.setDOMContent(VectorLayer.tooltipContainer)
|
||||
.addTo(mbMap);
|
||||
};
|
||||
|
||||
let activeFeature;
|
||||
let isTooltipOpen = false;
|
||||
mbMap.on('mousemove', mbLayerId, _.debounce((e) => {
|
||||
if (!isTooltipOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const features = mbMap.queryRenderedFeatures(e.point)
|
||||
.filter(feature => {
|
||||
return feature.layer.source === this.getId();
|
||||
});
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertiesUnchanged = _.isEqual(activeFeature.properties, features[0].properties);
|
||||
const geometryUnchanged = _.isEqual(activeFeature.geometry, features[0].geometry);
|
||||
if(propertiesUnchanged && geometryUnchanged) {
|
||||
// mouse over same feature, no need to update tooltip
|
||||
return;
|
||||
}
|
||||
|
||||
activeFeature = features[0];
|
||||
showTooltip(activeFeature, e.lngLat);
|
||||
}, 100));
|
||||
|
||||
mbMap.on('mouseenter', mbLayerId, (e) => {
|
||||
isTooltipOpen = true;
|
||||
mbMap.getCanvas().style.cursor = 'pointer';
|
||||
|
||||
activeFeature = e.features[0];
|
||||
showTooltip(activeFeature, e.lngLat);
|
||||
});
|
||||
|
||||
mbMap.on('mouseleave', mbLayerId, () => {
|
||||
isTooltipOpen = false;
|
||||
mbMap.getCanvas().style.cursor = '';
|
||||
VectorLayer.popup.remove();
|
||||
ReactDOM.unmountComponentAtNode(VectorLayer.tooltipContainer);
|
||||
});
|
||||
canShowTooltip() {
|
||||
return this._source.canFormatFeatureProperties();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ import {
|
|||
TRACK_CURRENT_LAYER_STATE,
|
||||
ROLLBACK_TO_TRACKED_LAYER_STATE,
|
||||
REMOVE_TRACKED_LAYER_STATE,
|
||||
UPDATE_SOURCE_DATA_REQUEST
|
||||
UPDATE_SOURCE_DATA_REQUEST,
|
||||
SET_TOOLTIP_STATE
|
||||
} from '../actions/store_actions';
|
||||
|
||||
import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util';
|
||||
|
@ -84,6 +85,7 @@ const updateLayerSourceDescriptorProp = (state, layerId, propName, value) => {
|
|||
const INITIAL_STATE = {
|
||||
ready: false,
|
||||
goto: null,
|
||||
tooltipState: null,
|
||||
mapState: {
|
||||
zoom: 4,
|
||||
center: {
|
||||
|
@ -114,6 +116,11 @@ export function map(state = INITIAL_STATE, action) {
|
|||
return trackCurrentLayerState(state, action.layerId);
|
||||
case ROLLBACK_TO_TRACKED_LAYER_STATE:
|
||||
return rollbackTrackedLayerState(state, action.layerId);
|
||||
case SET_TOOLTIP_STATE:
|
||||
return {
|
||||
...state,
|
||||
tooltipState: action.tooltipState
|
||||
};
|
||||
case SET_MOUSE_COORDINATES:
|
||||
return {
|
||||
...state,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue