Skip to content

Commit

Permalink
[Maps] Lock tooltip in place with click (#32733) (#33000)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasneirynck authored Mar 13, 2019
1 parent b5fd3da commit f8c36f0
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 59 deletions.
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 @@ -23,3 +23,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
50 changes: 41 additions & 9 deletions x-pack/plugins/maps/public/components/map/feature_tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,50 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import React, { Fragment } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

export function FeatureTooltip({ properties }) {

return Object.keys(properties).map(propertyName => {
export class FeatureTooltip extends React.Component {


_renderProperties() {
return Object.keys(this.props.properties).map(propertyName => {
return (
<div key={propertyName}>
<strong>{propertyName}</strong>
{' '}
{this.props.properties[propertyName]}
</div>
);
});
}

render() {
return (
<div key={propertyName}>
<strong>{propertyName}</strong>
{' '}
{properties[propertyName]}
</div>
<Fragment>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={true}>
<EuiFlexGroup alignItems="flexEnd" direction="row" justifyContent="spaceBetween">
<EuiFlexItem>&nbsp;</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
onClick={this.props.onCloseClick}
iconType="cross"
aria-label={i18n.translate('xpack.maps.tooltip.closeAreaLabel', {
defaultMessage: 'Close tooltip'
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
{this._renderProperties()}
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
});
}
}

120 changes: 82 additions & 38 deletions x-pack/plugins/maps/public/components/map/mb/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ 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 = {
HOVER: 'HOVER',
LOCKED: 'LOCKED'
};

export class MBMapContainer extends React.Component {

constructor() {
Expand All @@ -25,62 +31,89 @@ export class MBMapContainer extends React.Component {
});
}

_onTooltipClose = () => {
this.props.setTooltipState(null);
};

_debouncedSync = _.debounce(() => {
if (this._isMounted) {
this._syncMbMapWithLayerList();
this._syncMbMapWithInspector();
}
}, 256);

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

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

this._updateHoverTooltipState.cancel();//ignore any possible moves

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((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 +139,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 +200,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 +220,27 @@ export class MBMapContainer extends React.Component {
}
}

_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)
_renderContentToTooltip(content, location) {
if (!this._isMounted) {
return;
}
ReactDOM.render((<FeatureTooltip properties={content} onCloseClick={this._onTooltipClose}/>), this._tooltipContainer);

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;
});
}

}

0 comments on commit f8c36f0

Please sign in to comment.