diff --git a/maps_dashboards/public/components/map_container/map_container.tsx b/maps_dashboards/public/components/map_container/map_container.tsx
index 22f43a09..b43e300f 100644
--- a/maps_dashboards/public/components/map_container/map_container.tsx
+++ b/maps_dashboards/public/components/map_container/map_container.tsx
@@ -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;
@@ -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 (
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..e77bd0ca
--- /dev/null
+++ b/maps_dashboards/public/components/tooltip/create_tooltip.tsx
@@ -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(
+ ,
+ 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..d985f051 100644
--- a/maps_dashboards/public/components/tooltip/tooltipContainer.tsx
+++ b/maps_dashboards/public/components/tooltip/tooltipContainer.tsx
@@ -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) {
+ 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([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 (
-
+
-
+
-
+
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..06ed6f2a 100644
--- a/maps_dashboards/public/components/tooltip/tooltipTable.tsx
+++ b/maps_dashboards/public/components/tooltip/tooltipTable.tsx
@@ -3,16 +3,63 @@
* 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, useEffect, useMemo } from 'react';
+import { DocumentLayerSpecification } from '../../model/mapLayerType';
+
+export type RowData = {
+ key: string;
+ value: string;
+};
+export type PageData = RowData[];
+export type TableData = PageData[];
+type Table = { table: TableData; layer: DocumentLayerSpecification };
+
+export const ALL_LAYERS = -1;
interface Props {
- pages: any[];
- isClickEvent: boolean;
+ tables: Table[];
+ onLayerChange?: (layerIndexes: number[]) => void;
+ showPagination?: boolean;
+ showLayerSelection?: boolean;
+}
+
+function mergeTables(tables: Table[], selectedIndex: number[]) {
+ const merged: TableData = [];
+ const allSelected = selectedIndex.includes(ALL_LAYERS);
+
+ for (let i = 0; i < tables.length; i++) {
+ if (allSelected || selectedIndex.includes(i)) {
+ merged.push(...tables[i].table);
+ }
+ }
+
+ return merged;
}
-const TooltipTable = (props: Props) => {
- const [activePage, setActivePage] = useState(0);
+const TooltipTable = ({
+ tables,
+ onLayerChange,
+ showPagination = true,
+ showLayerSelection = true,
+}: Props) => {
+ const [selectedLayers, setSelectedLayers] = useState[]>([
+ {
+ label: tables[0]?.layer.name ?? '',
+ value: 0,
+ key: '0',
+ },
+ ]);
+ const [activePage, setActivePage] = useState(0);
const columns = [
{
field: 'key',
@@ -28,6 +75,11 @@ const TooltipTable = (props: Props) => {
},
];
+ useEffect(() => {
+ // When selected layer changed, reset the active page to the first page
+ setActivePage(0);
+ }, [selectedLayers]);
+
const getRowProps = (item) => {
const { id } = item;
return {
@@ -36,6 +88,45 @@ const TooltipTable = (props: Props) => {
};
};
+ const handleLayerChange = useCallback(
+ (layerSelections: EuiComboBoxOptionOption[]) => {
+ if (tables.length === 0) {
+ return;
+ }
+
+ let selections = layerSelections;
+
+ // when cleared selections, automatically select the first layer: value = 0
+ if (layerSelections.length === 0) {
+ selections = [{ label: tables[0]?.layer.name, value: 0, key: '0' }];
+ }
+
+ setSelectedLayers(selections);
+ if (onLayerChange) {
+ onLayerChange(selections.map((s) => s.value ?? 0));
+ }
+ },
+ [tables]
+ );
+
+ const options = useMemo(() => {
+ const layerOptions = [{ label: 'All layers', value: ALL_LAYERS, key: '-1' }];
+ tables.forEach(({ layer }, i) => {
+ layerOptions.push({ label: layer.name, value: i, key: `${i}` });
+ });
+ return layerOptions;
+ }, [tables]);
+
+ const tableItems = useMemo(
+ () =>
+ mergeTables(
+ tables,
+ selectedLayers.map((l) => l.value ?? 0)
+ ),
+ [tables, selectedLayers]
+ );
+ const pageItems = tableItems[activePage];
+
const getCellProps = (item, column) => {
const { id } = item;
const { field } = column;
@@ -45,33 +136,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 && (
+
+
+ placeholder="Select a layer"
+ selectedOptions={selectedLayers}
+ options={options}
+ onChange={handleLayerChange}
+ />
+
+ )}
- {props.isClickEvent
- ? buildPagination(props.pages.length)
- : buildMessage(props.pages.length)}
+ {showPagination ? (
+
+ ) : (
+
+ {1} of {tableItems.length}
+
+ )}
diff --git a/maps_dashboards/public/model/DataLayerController.ts b/maps_dashboards/public/model/DataLayerController.ts
index 34aeec84..0a511f70 100644
--- a/maps_dashboards/public/model/DataLayerController.ts
+++ b/maps_dashboards/public/model/DataLayerController.ts
@@ -63,7 +63,7 @@ export const doDataLayerRender = async (
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/model/documentLayerFunctions.ts b/maps_dashboards/public/model/documentLayerFunctions.ts
index c7d74b66..548c6209 100644
--- a/maps_dashboards/public/model/documentLayerFunctions.ts
+++ b/maps_dashboards/public/model/documentLayerFunctions.ts
@@ -3,11 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Map as Maplibre, Popup, LngLatLike } from 'maplibre-gl';
-import ReactDOM from 'react-dom';
+import { Map as Maplibre, Popup, MapGeoJSONFeature } from 'maplibre-gl';
+import { createPopup, getPopupLngLat } from '../components/tooltip/create_tooltip';
import { DocumentLayerSpecification } from './mapLayerType';
-import { TooltipContainer } from '../components/tooltip/tooltipContainer';
-import { LAYER_VISIBILITY } from '../../common';
interface MaplibreRef {
current: Maplibre | null;
@@ -341,52 +339,11 @@ 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();
+let layerPopup: Popup | null = null;
- // 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;
-};
+function getPopup() {
+ return layerPopup;
+}
export const DocumentLayerFunctions = {
render: (maplibreRef: MaplibreRef, layerConfig: DocumentLayerSpecification, data: any) => {
@@ -412,54 +369,24 @@ 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();
- }
+ addTooltip: (map: Maplibre, layerConfig: DocumentLayerSpecification) => {
+ map.on('mouseenter', layerConfig.id, (e) => {
+ getPopup()?.remove();
+ map.getCanvas().style.cursor = 'pointer';
+ if (e.features) {
+ layerPopup = createPopup({
+ features: (e.features ?? []) as MapGeoJSONFeature[],
+ layers: [layerConfig],
+ showCloseButton: false,
+ showPagination: false,
+ showLayerSelection: false,
});
+ layerPopup?.setLngLat(getPopupLngLat(e.features[0].geometry) ?? e.lngLat).addTo(map);
}
});
+ map.on('mouseleave', layerConfig.id, () => {
+ getPopup()?.remove();
+ map.getCanvas().style.cursor = '';
+ });
},
};