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

feat: added multi-layer support to map popup #140

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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