Skip to content

Commit

Permalink
feat: added multi-layer support to map popup (#140)
Browse files Browse the repository at this point in the history
* feat: added multi-layer support to map popup

1. Abstract a reusable popup rendering logic.
2. For click-popups which need to display multi-layers
  information, added an event listener globally and then
  query features from all layers at the current position.
3. For hover-popups which show information of the current
  layer features, moved the popup rendering logic out from
  the layer render logic(in the layer control panel component).

Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl authored Dec 23, 2022
1 parent 04759d1 commit 254a252
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import React, { useEffect, useRef, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import { Map as Maplibre, NavigationControl } from 'maplibre-gl';
import { Map as Maplibre, 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 { IndexPattern } from '../../../../../src/plugins/data/public';
import { MapState } from '../../model/mapState';
import { createPopup, getPopupLngLat, isTooltipEnabledLayer } from '../tooltip/create_tooltip';
import { DocumentLayerFunctions } from '../../model/documentLayerFunctions';

interface MapContainerProps {
setLayers: (layers: MapLayerSpecification[]) => void;
Expand Down Expand Up @@ -60,6 +62,39 @@ export const MapContainer = ({
});
}, []);

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) {
clickPopup = createPopup({ features, layers: tooltipEnabledLayers });
clickPopup
?.setLngLat(getPopupLngLat(features[0].geometry) ?? e.lngLat)
.addTo(maplibreRef.current);
}
}

if (maplibreRef.current) {
maplibreRef.current.on('click', onClickMap);
for (const layer of tooltipEnabledLayers) {
DocumentLayerFunctions.addTooltip(maplibreRef.current, layer);
}
}

return () => {
if (maplibreRef.current) {
maplibreRef.current.off('click', onClickMap);
}
};
}, [layers]);

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

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

type Options = {
features: MapGeoJSONFeature[];
layers: DocumentLayerSpecification[];
showCloseButton?: boolean;
showPagination?: boolean;
showLayerSelection?: boolean;
};

export function isTooltipEnabledLayer(
layer: MapLayerSpecification
): layer is DocumentLayerSpecification {
return layer.type !== 'opensearch_vector_tile_map' && layer.source.showTooltips === true;
}

export function groupFeaturesByLayers(
features: MapGeoJSONFeature[],
layers: DocumentLayerSpecification[]
) {
const featureGroups: FeatureGroupItem[] = [];
if (layers.length > 0) {
layers.forEach((layer) => {
const layerFeatures = features.filter((f) => f.layer.source === layer.id);
if (layerFeatures.length > 0) {
featureGroups.push({ features: layerFeatures, layer });
}
});
}
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({
features,
layers,
showCloseButton = true,
showPagination = true,
showLayerSelection = true,
}: Options) {
const popup = new Popup({
closeButton: false,
closeOnClick: false,
maxWidth: 'max-content',
});

const featureGroup = groupFeaturesByLayers(features, layers);

// 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);
}
103 changes: 76 additions & 27 deletions maps_dashboards/public/components/tooltip/tooltipContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,94 @@
* 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));
import { ALL_LAYERS, PageData, TableData, TooltipTable } from './tooltipTable';
import { MapGeoJSONFeature } from 'maplibre-gl';
import { DocumentLayerSpecification } from '../../model/mapLayerType';

export type FeatureGroupItem = {
features: MapGeoJSONFeature[];
layer: DocumentLayerSpecification;
};

interface TooltipProps {
featureGroup: FeatureGroupItem[];
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(featureGroupItem: FeatureGroupItem) {
const table: TableData = [];
for (const feature of featureGroupItem.features) {
if (feature?.properties) {
table.push(featureToTableRow(feature.properties));
}
}
return { table, layer: featureGroupItem.layer };
}

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

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

const title = useMemo(() => {
if (selectedLayerIndexes.includes(ALL_LAYERS)) {
return 'All layers';
}
if (selectedLayerIndexes.length === 1) {
return tables[selectedLayerIndexes[0]].layer.name;
}
return rows;
};
const featureToTableRow = (properties) => {
const rows: any[] = [];
for (const [k, v] of Object.entries(properties)) {
rows.push({
key: k,
value: `${v}`,
});
if (selectedLayerIndexes.length > 1) {
return `${tables[selectedLayerIndexes[0]].layer.name}, +${tables.length - 1}`;
}
return rows;
};
return '';
}, [selectedLayerIndexes, tables]);

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={setSelectedLayerIndexes}
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 254a252

Please sign in to comment.