diff --git a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx
index c73963c0..c6866700 100644
--- a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx
+++ b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx
@@ -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');
diff --git a/maps_dashboards/public/components/map_container/map_container.tsx b/maps_dashboards/public/components/map_container/map_container.tsx
index 8bb88570..d066e01a 100644
--- a/maps_dashboards/public/components/map_container/map_container.tsx
+++ b/maps_dashboards/public/components/map_container/map_container.tsx
@@ -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;
@@ -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 (
diff --git a/maps_dashboards/public/components/tooltip/create_tooltip.tsx b/maps_dashboards/public/components/tooltip/create_tooltip.tsx
new file mode 100644
index 00000000..b1fb31f3
--- /dev/null
+++ b/maps_dashboards/public/components/tooltip/create_tooltip.tsx
@@ -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 {
+ return layer.type !== 'opensearch_vector_tile_map' && layer.source.showTooltips === true;
+}
+
+export function groupFeaturesByLayers(
+ features: MapGeoJSONFeature[],
+ layers: Exclude[]
+) {
+ 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(
+ ,
+ div
+ );
+
+ return popup.setDOMContent(div);
+}
diff --git a/maps_dashboards/public/components/tooltip/tooltipContainer.tsx b/maps_dashboards/public/components/tooltip/tooltipContainer.tsx
index 5d883ed2..d9898557 100644
--- a/maps_dashboards/public/components/tooltip/tooltipContainer.tsx
+++ b/maps_dashboards/public/components/tooltip/tooltipContainer.tsx
@@ -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) {
+ 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 (
-
+
-
+
-
+
diff --git a/maps_dashboards/public/components/tooltip/tooltipHeaderContent.tsx b/maps_dashboards/public/components/tooltip/tooltipHeaderContent.tsx
index 9d97993b..7373b38f 100644
--- a/maps_dashboards/public/components/tooltip/tooltipHeaderContent.tsx
+++ b/maps_dashboards/public/components/tooltip/tooltipHeaderContent.tsx
@@ -9,7 +9,7 @@ import React from 'react';
interface Props {
title: string;
- isClickEvent: boolean;
+ showCloseButton: boolean;
onClose: Function;
}
@@ -23,7 +23,7 @@ const TooltipHeaderContent = (props: Props) => {
- {props.isClickEvent && (
+ {props.showCloseButton && (
{
diff --git a/maps_dashboards/public/components/tooltip/tooltipTable.tsx b/maps_dashboards/public/components/tooltip/tooltipTable.tsx
index 1a82ae63..3a6a821c 100644
--- a/maps_dashboards/public/components/tooltip/tooltipTable.tsx
+++ b/maps_dashboards/public/components/tooltip/tooltipTable.tsx
@@ -3,16 +3,49 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiPagination, EuiText } from '@elastic/eui';
-import React, { useState, Fragment } from 'react';
+import {
+ EuiBasicTable,
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPagination,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import React, { useState, Fragment, useCallback } from 'react';
+
+export type RowData = {
+ key: string;
+ value: string;
+};
+export type PageData = RowData[];
+export type TableData = PageData[];
+
+export const ALL_LAYERS = -1;
interface Props {
- pages: any[];
- isClickEvent: boolean;
+ tables: TableData[];
+ onLayerChange?: (layer: number) => void;
+ showPagination?: boolean;
+ showLayerSelection?: boolean;
+}
+
+function getLayerLabel(layerIndex: number) {
+ if (layerIndex >= 0) {
+ return `layer-${layerIndex + 1}`;
+ }
+ return 'All layers';
}
-const TooltipTable = (props: Props) => {
- const [activePage, setActivePage] = useState(0);
+const TooltipTable = ({
+ tables,
+ onLayerChange,
+ showPagination = true,
+ showLayerSelection = true,
+}: Props) => {
+ const [selectedLayer, setSelectedLayer] = useState(0);
+ const [activePages, setActivePages] = useState>({});
const columns = [
{
field: 'key',
@@ -36,6 +69,37 @@ const TooltipTable = (props: Props) => {
};
};
+ const handleLayerChange = useCallback((data: EuiComboBoxOptionOption[]) => {
+ if (data.length > 0) {
+ const layer = data[0]?.value ?? 0;
+ setSelectedLayer(layer);
+ if (onLayerChange) {
+ onLayerChange(layer);
+ }
+ }
+ }, []);
+
+ const onSelectPage = useCallback(
+ (pageIndex) => {
+ setActivePages((state) => {
+ const newState = { ...state };
+ newState[selectedLayer] = pageIndex;
+ return newState;
+ });
+ },
+ [selectedLayer]
+ );
+
+ const options = [{ label: 'All layers', value: ALL_LAYERS }];
+ tables.forEach((_, i) => {
+ options.push({ label: `layer-${i + 1}`, value: i });
+ });
+
+ const selectedOptions = [{ label: getLayerLabel(selectedLayer), value: selectedLayer }];
+ const activePage = activePages[selectedLayer] ?? 0;
+ const tableItems = selectedLayer >= 0 ? tables[selectedLayer] : tables.flat();
+ const pageItems = tableItems[activePage];
+
const getCellProps = (item, column) => {
const { id } = item;
const { field } = column;
@@ -45,33 +109,14 @@ const TooltipTable = (props: Props) => {
textOnly: true,
};
};
- const buildMessage = (count: number) => {
- return (
-
- {1} of {count}
-
- );
- };
-
- const buildPagination = (count: number) => {
- return (
- setActivePage(pageIndex)}
- compressed
- />
- );
- };
return (
-
+
{
/>
-
+
+
+ {showLayerSelection && (
+
+
+
+ )}
- {props.isClickEvent
- ? buildPagination(props.pages.length)
- : buildMessage(props.pages.length)}
+ {showPagination ? (
+
+ ) : (
+
+ {1} of {tableItems.length}
+
+ )}
diff --git a/maps_dashboards/public/model/documentLayerFunctions.ts b/maps_dashboards/public/model/documentLayerFunctions.ts
index c7d74b66..46ef67e9 100644
--- a/maps_dashboards/public/model/documentLayerFunctions.ts
+++ b/maps_dashboards/public/model/documentLayerFunctions.ts
@@ -3,11 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Map as Maplibre, Popup, LngLatLike } from 'maplibre-gl';
-import ReactDOM from 'react-dom';
+import { Map as Maplibre } from 'maplibre-gl';
import { DocumentLayerSpecification } from './mapLayerType';
-import { TooltipContainer } from '../components/tooltip/tooltipContainer';
-import { LAYER_VISIBILITY } from '../../common';
interface MaplibreRef {
current: Maplibre | null;
@@ -341,53 +338,6 @@ const updateLayerConfig = (
}
};
-const clickPopup = new Popup({
- closeButton: false,
- closeOnClick: false,
- maxWidth: 'max-content',
-});
-const getClickPopup = () => {
- return clickPopup;
-};
-
-const hoverPopup = new Popup({
- closeButton: false,
- closeOnClick: false,
- maxWidth: 'max-content',
-});
-export const getHoverPopup = () => {
- return hoverPopup;
-};
-
-export const buildPopup = (
- popup: Popup,
- features: any[],
- title: string,
- coordinates: LngLatLike,
- isClickEvent: boolean
-) => {
- const div = document.createElement('div');
- ReactDOM.render(
- TooltipContainer(title, features, isClickEvent, () => {
- getClickPopup().remove();
- }),
- div
- );
- return popup.setDOMContent(div).setLngLat(coordinates);
-};
-
-const getCoordinates = (e: any): any => {
- const coordinates = e.features[0].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;
- }
- return coordinates;
-};
-
export const DocumentLayerFunctions = {
render: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification, data: any) => {
if (layerExistInMbSource(layerConfig.id, maplibreRef)) {
@@ -412,54 +362,4 @@ export const DocumentLayerFunctions = {
}
});
},
- addTooltip: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification) => {
- const layers = getCurrentStyleLayers(maplibreRef);
- layers.forEach((layer) => {
- if (layer.id.includes(layerConfig.id)) {
- maplibreRef.current?.on('click', layer.id, (e: any) => {
- if (
- maplibreRef.current &&
- layerConfig.visibility === LAYER_VISIBILITY.VISIBLE &&
- layerConfig.source.showTooltips === true
- ) {
- // remove click pop up if previously opened click popup is not closed by user.
- getClickPopup()?.remove();
- if (maplibreRef.current) {
- buildPopup(
- getClickPopup(),
- e.features,
- layerConfig.name,
- getCoordinates(e),
- true
- ).addTo(maplibreRef.current);
- }
- }
- });
- // on hover
- maplibreRef.current?.on('mouseenter', layer.id, (e: any) => {
- if (
- maplibreRef.current &&
- layerConfig.visibility === LAYER_VISIBILITY.VISIBLE &&
- layerConfig.source.showTooltips
- ) {
- maplibreRef.current.getCanvas().style.cursor = 'pointer';
- buildPopup(
- getHoverPopup(),
- e.features,
- layerConfig.name,
- getCoordinates(e),
- false
- ).addTo(maplibreRef.current);
- }
- });
- // on leave
- maplibreRef.current?.on('mouseleave', layer.id, function () {
- if (maplibreRef.current) {
- maplibreRef.current.getCanvas().style.cursor = '';
- getHoverPopup()?.remove();
- }
- });
- }
- });
- },
};