diff --git a/x-pack/plugins/maps/common/constants.js b/x-pack/plugins/maps/common/constants.js
index 0af229569e5d1..98de5ad588863 100644
--- a/x-pack/plugins/maps/common/constants.js
+++ b/x-pack/plugins/maps/common/constants.js
@@ -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__';
diff --git a/x-pack/plugins/maps/public/actions/store_actions.js b/x-pack/plugins/maps/public/actions/store_actions.js
index e38c5e25c086a..7f042d409ee27 100644
--- a/x-pack/plugins/maps/public/actions/store_actions.js
+++ b/x-pack/plugins/maps/public/actions/store_actions.js
@@ -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,
diff --git a/x-pack/plugins/maps/public/components/map/feature_tooltip.js b/x-pack/plugins/maps/public/components/map/feature_tooltip.js
index 426d7009a7c59..4c89e15eced25 100644
--- a/x-pack/plugins/maps/public/components/map/feature_tooltip.js
+++ b/x-pack/plugins/maps/public/components/map/feature_tooltip.js
@@ -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 (
+
+ {propertyName}
+ {' '}
+ {this.props.properties[propertyName]}
+
+ );
+ });
+ }
+
+ render() {
return (
-
- {propertyName}
- {' '}
- {properties[propertyName]}
-
+
+
+
+
+
+
+
+
+
+
+
+ {this._renderProperties()}
+
+
+
);
- });
+ }
}
diff --git a/x-pack/plugins/maps/public/components/map/mb/view.js b/x-pack/plugins/maps/public/components/map/mb/view.js
index 6421ca54ee66c..1f11e7d807ab3 100644
--- a/x-pack/plugins/maps/public/components/map/mb/view.js
+++ b/x-pack/plugins/maps/public/components/map/mb/view.js
@@ -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() {
@@ -25,6 +31,10 @@ export class MBMapContainer extends React.Component {
});
}
+ _onTooltipClose = () => {
+ this.props.setTooltipState(null);
+ };
+
_debouncedSync = _.debounce(() => {
if (this._isMounted) {
this._syncMbMapWithLayerList();
@@ -32,55 +42,78 @@ 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 = (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;
@@ -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();
@@ -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());
}
@@ -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((), 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();
diff --git a/x-pack/plugins/maps/public/shared/layers/vector_layer.js b/x-pack/plugins/maps/public/shared/layers/vector_layer.js
index d3076de4852f3..49e4180974c0c 100644
--- a/x-pack/plugins/maps/public/shared/layers/vector_layer.js
+++ b/x-pack/plugins/maps/public/shared/layers/vector_layer.js
@@ -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 = {
@@ -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);
@@ -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;
@@ -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;
+ });
+ }
+
}