Skip to content

Commit

Permalink
feat: multi-select layers + layer name as tooltip name
Browse files Browse the repository at this point in the history
Signed-off-by: Yulong Ruan <[email protected]>
  • Loading branch information
ruanyl committed Dec 23, 2022
1 parent 644aae2 commit b5f358a
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 109 deletions.
58 changes: 7 additions & 51 deletions maps_dashboards/public/components/map_container/map_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,15 @@

import React, { useEffect, useRef, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import {
Map as Maplibre,
MapLayerMouseEvent,
MapMouseEvent,
NavigationControl,
Popup,
} 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,
groupFeaturesByLayers,
isTooltipEnabledLayer,
} from '../tooltip/create_tooltip';
import { createPopup, getPopupLngLat, isTooltipEnabledLayer } from '../tooltip/create_tooltip';
import { DocumentLayerFunctions } from '../../model/documentLayerFunctions';

interface MapContainerProps {
setLayers: (layers: MapLayerSpecification[]) => void;
Expand Down Expand Up @@ -84,57 +74,23 @@ export const MapContainer = ({

const features = maplibreRef.current?.queryRenderedFeatures(e.point);
if (features && maplibreRef.current) {
const featureGroup = groupFeaturesByLayers(features, tooltipEnabledLayers);
clickPopup = createPopup({ featureGroup });
clickPopup = createPopup({ features, layers: tooltipEnabledLayers });
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);
});
for (const layer of tooltipEnabledLayers) {
DocumentLayerFunctions.addTooltip(maplibreRef.current, layer);
}
}

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]);
Expand Down
26 changes: 14 additions & 12 deletions maps_dashboards/public/components/tooltip/create_tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,35 @@ 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';
import { MapLayerSpecification, DocumentLayerSpecification } from '../../model/mapLayerType';
import { FeatureGroupItem, TooltipContainer } from './tooltipContainer';

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

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

export function groupFeaturesByLayers(
features: MapGeoJSONFeature[],
layers: Exclude<MapLayerSpecification, OSMLayerSpecification>[]
layers: DocumentLayerSpecification[]
) {
const featureGroups: MapGeoJSONFeature[][] = [];
const featureGroups: FeatureGroupItem[] = [];
if (layers.length > 0) {
layers.forEach((l) => {
const layerFeatures = features.filter((f) => f.layer.source === l.id);
layers.forEach((layer) => {
const layerFeatures = features.filter((f) => f.layer.source === layer.id);
if (layerFeatures.length > 0) {
featureGroups.push(layerFeatures);
featureGroups.push({ features: layerFeatures, layer });
}
});
} else {
featureGroups.push(features);
}
return featureGroups;
}
Expand All @@ -48,7 +47,8 @@ export function getPopupLngLat(geometry: GeoJSON.Geometry) {
}

export function createPopup({
featureGroup,
features,
layers,
showCloseButton = true,
showPagination = true,
showLayerSelection = true,
Expand All @@ -59,6 +59,8 @@ export function createPopup({
maxWidth: 'max-content',
});

const featureGroup = groupFeaturesByLayers(features, layers);

// Don't show popup if no feature
if (featureGroup.length === 0) {
return null;
Expand Down
36 changes: 27 additions & 9 deletions maps_dashboards/public/components/tooltip/tooltipContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import React, { useMemo, useState } from 'react';

import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiText, EuiHorizontalRule } from '@elastic/eui';
import { TooltipHeaderContent } from './tooltipHeaderContent';
import { PageData, TableData, TooltipTable } from './tooltipTable';
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: GeoJSON.Feature[][];
featureGroup: FeatureGroupItem[];
onClose: () => void;
showCloseButton?: boolean;
showPagination?: boolean;
Expand All @@ -28,17 +35,17 @@ function featureToTableRow(properties: Record<string, any>) {
return row;
}

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

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

Expand All @@ -49,10 +56,21 @@ export function TooltipContainer({
showPagination = true,
showLayerSelection = true,
}: TooltipProps) {
const [selectedLayer, setSelectedLayer] = useState(0);
const [selectedLayerIndexes, setSelectedLayerIndexes] = useState<number[]>([0]);
const tables = useMemo(() => createTableData(featureGroup), [featureGroup]);

const title = selectedLayer >= 0 ? `layer-${selectedLayer + 1}` : 'All layers';
const title = useMemo(() => {
if (selectedLayerIndexes.includes(ALL_LAYERS)) {
return 'All layers';
}
if (selectedLayerIndexes.length === 1) {
return tables[selectedLayerIndexes[0]].layer.name;
}
if (selectedLayerIndexes.length > 1) {
return `${tables[selectedLayerIndexes[0]].layer.name}, +${tables.length - 1}`;
}
return '';
}, [selectedLayerIndexes, tables]);

return (
<EuiPanel style={{ width: 350 }} paddingSize={'s'} grow={true}>
Expand All @@ -69,7 +87,7 @@ export function TooltipContainer({
<EuiFlexItem grow={true}>
<TooltipTable
tables={tables}
onLayerChange={setSelectedLayer}
onLayerChange={setSelectedLayerIndexes}
showPagination={showPagination}
showLayerSelection={showLayerSelection}
/>
Expand Down
98 changes: 62 additions & 36 deletions maps_dashboards/public/components/tooltip/tooltipTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,37 @@ import {
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React, { useState, Fragment, useCallback } from 'react';
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 {
tables: TableData[];
onLayerChange?: (layer: number) => void;
tables: Table[];
onLayerChange?: (layerIndexes: number[]) => void;
showPagination?: boolean;
showLayerSelection?: boolean;
}

function getLayerLabel(layerIndex: number) {
if (layerIndex >= 0) {
return `layer-${layerIndex + 1}`;
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 'All layers';

return merged;
}

const TooltipTable = ({
Expand All @@ -44,8 +52,14 @@ const TooltipTable = ({
showPagination = true,
showLayerSelection = true,
}: Props) => {
const [selectedLayer, setSelectedLayer] = useState(0);
const [activePages, setActivePages] = useState<Record<number, number | undefined>>({});
const [selectedLayers, setSelectedLayers] = useState<EuiComboBoxOptionOption<number>[]>([
{
label: tables[0]?.layer.name ?? '',
value: 0,
key: '0',
},
]);
const [activePage, setActivePage] = useState<number>(0);
const columns = [
{
field: 'key',
Expand All @@ -61,6 +75,11 @@ const TooltipTable = ({
},
];

useEffect(() => {
// When selected layer changed, reset the active page to the first page
setActivePage(0);
}, [selectedLayers]);

const getRowProps = (item) => {
const { id } = item;
return {
Expand All @@ -69,35 +88,43 @@ const TooltipTable = ({
};
};

const handleLayerChange = useCallback((data: EuiComboBoxOptionOption<number>[]) => {
if (data.length > 0) {
const layer = data[0]?.value ?? 0;
setSelectedLayer(layer);
const handleLayerChange = useCallback(
(layerSelections: EuiComboBoxOptionOption<number>[]) => {
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(layer);
onLayerChange(selections.map((s) => s.value ?? 0));
}
}
}, []);

const onSelectPage = useCallback(
(pageIndex) => {
setActivePages((state) => {
const newState = { ...state };
newState[selectedLayer] = pageIndex;
return newState;
});
},
[selectedLayer]
[tables]
);

const options = [{ label: 'All layers', value: ALL_LAYERS }];
tables.forEach((_, i) => {
options.push({ label: `layer-${i + 1}`, value: i });
});
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 selectedOptions = [{ label: getLayerLabel(selectedLayer), value: selectedLayer }];
const activePage = activePages[selectedLayer] ?? 0;
const tableItems = selectedLayer >= 0 ? tables[selectedLayer] : tables.flat();
const tableItems = useMemo(
() =>
mergeTables(
tables,
selectedLayers.map((l) => l.value ?? 0)
),
[tables, selectedLayers]
);
const pageItems = tableItems[activePage];

const getCellProps = (item, column) => {
Expand Down Expand Up @@ -128,10 +155,9 @@ const TooltipTable = ({
<EuiFlexGroup justifyContent="spaceAround" alignItems="center">
{showLayerSelection && (
<EuiFlexItem>
<EuiComboBox
<EuiComboBox<number>
placeholder="Select a layer"
singleSelection={{ asPlainText: true }}
selectedOptions={selectedOptions}
selectedOptions={selectedLayers}
options={options}
onChange={handleLayerChange}
/>
Expand All @@ -143,7 +169,7 @@ const TooltipTable = ({
aria-label="Compressed pagination"
pageCount={tableItems.length}
activePage={activePage}
onPageClick={onSelectPage}
onPageClick={setActivePage}
compressed
/>
) : (
Expand Down
Loading

0 comments on commit b5f358a

Please sign in to comment.