Skip to content

Commit

Permalink
feat: added multi-layer support to map popup
Browse files Browse the repository at this point in the history
refactor map popup rendering logic, instead of adding event listener
to each layer for popup, this commit changed to add an event listener
globally and then query features of the current position

Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl committed Dec 22, 2022
1 parent 9c36083 commit 7f1c741
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const LayerControlPanel = memo(({ maplibreRef, setLayers, layers }: Props) => {
if (isCompleteResponse(response)) {
const dataSource = response.rawResponse.hits.hits;
layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource);
layersFunctionMap[layer.type].addTooltip(maplibreRef, layer);
// layersFunctionMap[layer.type].addTooltip(maplibreRef, layer);
search$.unsubscribe();
} else {
notifications.toasts.addWarning('An error has occurred when query dataSource');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@

import React, { useEffect, useRef, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import { Map as Maplibre, NavigationControl } from 'maplibre-gl';
import {
Map as Maplibre,
MapLayerMouseEvent,
MapMouseEvent,
NavigationControl,
Popup,
} from 'maplibre-gl';
import { LayerControlPanel } from '../layer_control_panel';
import './map_container.scss';
import { MAP_INITIAL_STATE, MAP_GLYPHS } from '../../../common';
import { MapLayerSpecification } from '../../model/mapLayerType';
import {
createPopup,
getPopupLngLat,
groupFeaturesByLayers,
isTooltipEnabledLayer,
} from '../tooltip/create_tooltip';

interface MapContainerProps {
mapIdFromUrl: string;
Expand Down Expand Up @@ -49,6 +61,73 @@ export const MapContainer = ({ mapIdFromUrl, setLayers, layers }: MapContainerPr
});
}, []);

useEffect(() => {
let clickPopup: Popup | null = null;

// We don't want to show layer information in the popup for the map tile layer
const tooltipEnabledLayers = layers.filter(isTooltipEnabledLayer);

function onClickMap(e: MapMouseEvent) {
// remove previous popup
clickPopup?.remove();

const features = maplibreRef.current?.queryRenderedFeatures(e.point);
if (features && maplibreRef.current) {
const featureGroup = groupFeaturesByLayers(features, tooltipEnabledLayers);
clickPopup = createPopup({ featureGroup });
clickPopup
?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat)
.addTo(maplibreRef.current);
}
}

let hoverPopup: Popup | null = null;

function onMouseEnter(e: MapLayerMouseEvent) {
hoverPopup?.remove();

if (maplibreRef.current) {
maplibreRef.current.getCanvas().style.cursor = 'pointer';
if (e.features) {
hoverPopup = createPopup({
featureGroup: [e.features],
showCloseButton: false,
showPagination: false,
showLayerSelection: false,
});
hoverPopup
?.setLngLat(getPopupLngLat(e.features[0].geometry) ?? e.lngLat)
.addTo(maplibreRef.current);
}
}
}

function onMouseLeave(e: MapLayerMouseEvent) {
hoverPopup?.remove();
if (maplibreRef.current) {
maplibreRef.current.getCanvas().style.cursor = '';
}
}

if (maplibreRef.current) {
maplibreRef.current.on('click', onClickMap);
tooltipEnabledLayers.forEach((l) => {
maplibreRef.current?.on('mouseenter', l.id, onMouseEnter);
maplibreRef.current?.on('mouseleave', l.id, onMouseLeave);
});
}

return () => {
if (maplibreRef.current) {
maplibreRef.current.off('click', onClickMap);
tooltipEnabledLayers.forEach((l) => {
maplibreRef.current?.off('mouseenter', l.id, onMouseEnter);
maplibreRef.current?.off('mouseleave', l.id, onMouseLeave);
});
}
};
}, [layers]);

return (
<div>
<EuiPanel hasShadow={false} hasBorder={false} color="transparent" className="zoombar">
Expand Down
80 changes: 80 additions & 0 deletions maps_dashboards/public/components/tooltip/create_tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Popup, MapGeoJSONFeature } from 'maplibre-gl';

import { MapLayerSpecification, OSMLayerSpecification } from '../../model/mapLayerType';
import { TooltipContainer } from './tooltipContainer';

type Options = {
featureGroup: GeoJSON.Feature[][];
showCloseButton?: boolean;
showPagination?: boolean;
showLayerSelection?: boolean;
};

export function isTooltipEnabledLayer(
layer: MapLayerSpecification
): layer is Exclude<MapLayerSpecification, OSMLayerSpecification> {
return layer.type !== 'opensearch_vector_tile_map' && layer.source.showTooltips === true;
}

export function groupFeaturesByLayers(
features: MapGeoJSONFeature[],
layers: Exclude<MapLayerSpecification, OSMLayerSpecification>[]
) {
const featureGroups: MapGeoJSONFeature[][] = [];
if (layers.length > 0) {
layers.forEach((l) => {
const layerFeatures = features.filter((f) => f.layer.source === l.id);
if (layerFeatures.length > 0) {
featureGroups.push(layerFeatures);
}
});
} else {
featureGroups.push(features);
}
return featureGroups;
}

export function getPopupLngLat(geometry: GeoJSON.Geometry) {
// geometry.coordinates is different for different geometry.type, here we use the geometry.coordinates
// of a Point as the position of the popup. For other types, such as Polygon, MultiPolygon, etc,
// use mouse position should be better
if (geometry.type === 'Point') {
return [geometry.coordinates[0], geometry.coordinates[1]] as [number, number];
} else {
return null;
}
}

export function createPopup({
featureGroup,
showCloseButton = true,
showPagination = true,
showLayerSelection = true,
}: Options) {
const popup = new Popup({
closeButton: false,
closeOnClick: false,
maxWidth: 'max-content',
});

// Don't show popup if no feature
if (featureGroup.length === 0) {
return null;
}

const div = document.createElement('div');
ReactDOM.render(
<TooltipContainer
featureGroup={featureGroup}
onClose={popup.remove}
showCloseButton={showCloseButton}
showPagination={showPagination}
showLayerSelection={showLayerSelection}
/>,
div
);

return popup.setDOMContent(div);
}
87 changes: 59 additions & 28 deletions maps_dashboards/public/components/tooltip/tooltipContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,76 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { useMemo, useState } from 'react';

import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiText, EuiHorizontalRule } from '@elastic/eui';
import { TooltipHeaderContent } from './tooltipHeaderContent';
import { TooltipTable } from './tooltipTable';

export function TooltipContainer(
title: string,
features: any[],
isClickEvent: boolean,
onClose: Function
) {
const toTableRows = () => {
const rows: any[] = [];
for (const feature of features) {
rows.push(featureToTableRow(feature?.properties));
}
return rows;
};
const featureToTableRow = (properties) => {
const rows: any[] = [];
for (const [k, v] of Object.entries(properties)) {
rows.push({
key: k,
value: `${v}`,
});
import { PageData, TableData, TooltipTable } from './tooltipTable';

interface TooltipProps {
featureGroup: GeoJSON.Feature[][];
onClose: () => void;
showCloseButton?: boolean;
showPagination?: boolean;
showLayerSelection?: boolean;
}

function featureToTableRow(properties: Record<string, any>) {
const row: PageData = [];
for (const [k, v] of Object.entries(properties)) {
row.push({
key: k,
value: `${v}`,
});
}
return row;
}

function toTable(features: GeoJSON.Feature[]) {
const table: TableData = [];
for (const feature of features) {
if (feature?.properties) {
table.push(featureToTableRow(feature.properties));
}
return rows;
};
}
return table;
}

function createTableData(featureGroups: GeoJSON.Feature[][]) {
return featureGroups.map(toTable);
}

export function TooltipContainer({
featureGroup,
onClose,
showCloseButton = true,
showPagination = true,
showLayerSelection = true,
}: TooltipProps) {
const [selectedLayer, setSelectedLayer] = useState(0);
const tables = useMemo(() => createTableData(featureGroup), [featureGroup]);

const title = selectedLayer >= 0 ? `layer-${selectedLayer + 1}` : 'All layers';

return (
<EuiPanel paddingSize={'s'} grow={true}>
<EuiPanel style={{ width: 350 }} paddingSize={'s'} grow={true}>
<EuiText className={'eui-textTruncate'} grow={true}>
<EuiFlexGroup responsive={false} direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<TooltipHeaderContent title={title} isClickEvent={isClickEvent} onClose={onClose} />
<TooltipHeaderContent
title={title}
onClose={onClose}
showCloseButton={showCloseButton}
/>
<EuiHorizontalRule margin="xs" />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<TooltipTable pages={toTableRows()} isClickEvent={isClickEvent} />
<TooltipTable
tables={tables}
onLayerChange={setSelectedLayer}
showPagination={showPagination}
showLayerSelection={showLayerSelection}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React from 'react';

interface Props {
title: string;
isClickEvent: boolean;
showCloseButton: boolean;
onClose: Function;
}

Expand All @@ -23,7 +23,7 @@ const TooltipHeaderContent = (props: Props) => {
</h4>
</EuiTextColor>
</EuiFlexItem>
{props.isClickEvent && (
{props.showCloseButton && (
<EuiFlexItem key="closeButton" grow={false}>
<EuiButtonIcon
onClick={() => {
Expand Down
Loading

0 comments on commit 7f1c741

Please sign in to comment.