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

[Maps] Lock tooltip in place with click #32733

Merged
merged 10 commits into from
Mar 12, 2019
2 changes: 2 additions & 0 deletions x-pack/plugins/maps/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export const APP_ID = 'maps';
export const APP_ICON = 'gisApp';

export const SOURCE_DATA_ID_ORIGIN = 'source';

export const FEATURE_ID_PROPERTY_NAME = '__kbn__feature_id__';
20 changes: 12 additions & 8 deletions x-pack/plugins/maps/public/actions/store_actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,18 +389,22 @@ export function updateSourceDataRequest(layerId, newData) {
}

export function endDataLoad(layerId, dataId, requestToken, data, meta) {
return ({
type: LAYER_DATA_LOAD_ENDED,
layerId,
dataId,
data,
meta,
requestToken
});
return (dispatch) => {
dispatch(clearTooltipStateForLayer(layerId));
dispatch({
type: LAYER_DATA_LOAD_ENDED,
layerId,
dataId,
data,
meta,
requestToken
});
};
}

export function onDataLoadError(layerId, dataId, requestToken, errorMessage) {
return async (dispatch) => {
dispatch(clearTooltipStateForLayer(layerId));
dispatch({
type: LAYER_DATA_LOAD_ERROR,
layerId,
Expand Down
110 changes: 79 additions & 31 deletions x-pack/plugins/maps/public/components/map/mb/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,29 @@ 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 { DECIMAL_DEGREES_PRECISION, FEATURE_ID_PROPERTY_NAME, ZOOM_PRECISION } from '../../../../common/constants';
import mapboxgl from 'mapbox-gl';
import { FeatureTooltip } from '../feature_tooltip';


const TOOLTIP_TYPE = {
Copy link
Contributor

@nreese nreese Mar 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are constants used by the store and actions. I think they should be defined in a more global location under public/store

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoretically yes, in practice no. the tooltip-state is only evaluated here to handle moves/clicks correctly. I would move these constants once they are actually being evaluated in the redux flow (e.g, tooltip type will be relevant when trying to preserve tooltips across data-refreshes, but it's not something addressed here).

HOVER: 'HOVER',
LOCKED: 'LOCKED'
};

export class MBMapContainer extends React.Component {

constructor() {
super();
this._mbMap = null;
this._tooltipContainer = document.createElement('div');
this._mbPopup = new mapboxgl.Popup({
closeButton: false,
closeButton: true,
closeOnClick: false,
});
this._mbPopup.on('close', () => {
this.props.setTooltipState(null);
});
}

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

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

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

const features = this._getFeaturesUnderPointer(e.point);
if (!features.length) {
this.props.setTooltipState(null);
return;
}

const targetFeature = features[0];
const layer = this._getLayer(targetFeature.layer.id);
const popupAnchorLocation = this._justifyAnchorLocation(e.lngLat, targetFeature);
this.props.setTooltipState({
type: TOOLTIP_TYPE.LOCKED,
layerId: layer.getId(),
featureId: targetFeature.properties[FEATURE_ID_PROPERTY_NAME],
location: popupAnchorLocation
});

};

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

if (this.props.tooltipState && this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED) {
//ignore hover events when tooltip is locked
return;
}

const features = this._getFeaturesUnderPointer(e.point);
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) {
if (targetFeature.properties[FEATURE_ID_PROPERTY_NAME] === this.props.tooltipState.featureId) {
return;
}
}

const layer = this._getLayer(targetFeature.layer.id);
const formattedProperties = await layer.getPropertiesForTooltip(targetFeature.properties);
const popupAnchorLocation = this._justifyAnchorLocation(e.lngLat, targetFeature);

this.props.setTooltipState({
type: TOOLTIP_TYPE.HOVER,
featureId: targetFeature.properties[FEATURE_ID_PROPERTY_NAME],
layerId: layer.getId(),
location: popupAnchorLocation
});

let popupAnchorLocation = [e.lngLat.lng, e.lngLat.lat]; // default popup location to mouse location
}, 100);

_justifyAnchorLocation(mbLngLat, targetFeature) {
let popupAnchorLocation = [mbLngLat.lng, mbLngLat.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;
while (Math.abs(mbLngLat.lng - coordinates[0]) > 180) {
coordinates[0] += mbLngLat.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);


return popupAnchorLocation;
}
_getMbLayerIdsForTooltips() {
return this.props.layerList.reduce((mbLayerIds, layer) => {
return layer.canShowTooltip() ? mbLayerIds.concat(layer.getMbLayerIds()) : mbLayerIds;
Expand All @@ -106,6 +136,11 @@ export class MBMapContainer extends React.Component {
};
}

_getFeaturesUnderPointer(mbLngLatPoint) {
const mbLayerIds = this._getMbLayerIdsForTooltips();
return this._mbMap.queryRenderedFeatures(mbLngLatPoint, { layers: mbLayerIds });
}

componentDidUpdate() {
// do not debounce syncing of map-state and tooltip
this._syncMbMapWithMapState();
Expand Down Expand Up @@ -162,7 +197,8 @@ export class MBMapContainer extends React.Component {
});


this._mbMap.on('mousemove', this._updateTooltipState);
this._mbMap.on('mousemove', this._updateHoverTooltipState);
this._mbMap.on('click', this._lockTooltip);

this.props.onMapReady(this._getMapState());
}
Expand All @@ -181,22 +217,34 @@ export class MBMapContainer extends React.Component {
}
}

_showTooltip() {
//todo: can still be optimized. No need to rerender if content remains identical
_renderContentToTooltip(content, location) {
if (!this._isMounted) {
return;
}
ReactDOM.render(
React.createElement(
FeatureTooltip, {
properties: this.props.tooltipState.formattedProperties,
properties: content,
}
),
this._tooltipContainer
);

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


async _showTooltip() {
const tooltipLayer = this.props.layerList.find(layer => {
return layer.getId() === this.props.tooltipState.layerId;
});
const targetFeature = tooltipLayer.getFeatureByFeatureById(this.props.tooltipState.featureId);
const formattedProperties = await tooltipLayer.getPropertiesForTooltip(targetFeature.properties);
this._renderContentToTooltip(formattedProperties, this.props.tooltipState.location);
}

_syncTooltipState() {
if (this.props.tooltipState) {
this._showTooltip();
Expand Down
28 changes: 24 additions & 4 deletions x-pack/plugins/maps/public/shared/layers/vector_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import turf from 'turf';
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 { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN } from '../../../common/constants';
import _ from 'lodash';

const EMPTY_FEATURE_COLLECTION = {
Expand Down Expand Up @@ -285,11 +285,12 @@ export class VectorLayer extends AbstractLayer {
try {
startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters);
const layerName = await this.getDisplayName();
const { data, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters);
stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, data, meta);
const { data: featureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters);
this._assignIdsToFeatures(featureCollection);
stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta);
return {
refreshed: true,
featureCollection: data
featureCollection: featureCollection
};
} catch (error) {
onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message);
Expand All @@ -299,6 +300,14 @@ export class VectorLayer extends AbstractLayer {
}
}


_assignIdsToFeatures(featureCollection) {
for (let i = 0; i < featureCollection.features.length; i++) {
const feature = featureCollection.features[i];
feature.properties[FEATURE_ID_PROPERTY_NAME] = (typeof feature.id === 'string' || typeof feature.id === 'number') ? feature.id : i;
}
}

_joinToFeatureCollection(sourceResult, joinState, updateSourceData) {
if (!sourceResult.refreshed && !joinState.shouldJoin) {
return false;
Expand Down Expand Up @@ -509,4 +518,15 @@ export class VectorLayer extends AbstractLayer {
return this._source.canFormatFeatureProperties();
}

getFeatureByFeatureById(id) {
const featureCollection = this._getSourceFeatureCollection(id);
if (!featureCollection) {
return;
}

return featureCollection.features.find((feature) => {
return feature.properties[FEATURE_ID_PROPERTY_NAME] === id;
});
}

}