From 65f23d265589fdda5d352a26530aa0051dbc86b0 Mon Sep 17 00:00:00 2001 From: jolevesq Date: Fri, 13 Dec 2024 15:24:34 -0500 Subject: [PATCH] refactor(legend): Simplify the code and tweak performance Closes #2660 --- .../public/configs/navigator/28-geocore.json | 12 - .../legend/legend-layer-container.tsx | 70 +++++ .../components/legend/legend-layer-ctrl.tsx | 111 ++++++++ .../components/legend/legend-layer-items.tsx | 36 +++ .../core/components/legend/legend-layer.tsx | 254 ++++-------------- 5 files changed, 270 insertions(+), 213 deletions(-) create mode 100644 packages/geoview-core/src/core/components/legend/legend-layer-container.tsx create mode 100644 packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx create mode 100644 packages/geoview-core/src/core/components/legend/legend-layer-items.tsx diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index 139ba497d62..d0dfec36113 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -17,18 +17,6 @@ { "geoviewLayerType": "geoCore", "geoviewLayerId": "ccc75c12-5acc-4a6a-959f-ef6f621147b9" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "0fca08b5-e9d0-414b-a3c4-092ff9c5e326" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "03ccfb5c-a06e-43e3-80fd-09d4f8f69703" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "6433173f-bca8-44e6-be8e-3e8a19d3c299" } ] }, diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx new file mode 100644 index 00000000000..980d204d369 --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx @@ -0,0 +1,70 @@ +import { useTheme } from '@mui/material'; +import { memo } from 'react'; +import { Box, Collapse, List } from '@/ui'; +import { TypeLegendLayer } from '@/core/components/layers/types'; +import { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; +import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; +import { ItemsList } from './legend-layer-items'; + +// Define component types and interfaces +type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; + +interface CollapsibleContentProps { + layer: TypeLegendLayer; + legendExpanded: boolean; + initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; + childLayers: TypeLegendLayer[]; + items: TypeLegendLayer['items']; + LegendLayerComponent: LegendLayerType; +} + +// CollapsibleContent component moved after LegendLayer +export const CollapsibleContent = memo(function CollapsibleContent({ + layer, + legendExpanded, + initLightBox, + childLayers, + items, + LegendLayerComponent, +}: CollapsibleContentProps) { + logger.logDebug('legend1 collapsible', layer, childLayers, items); + const theme = useTheme(); + const sxClasses = getSxClasses(theme); + + if (layer.type === CV_CONST_LAYER_TYPES.WMS && layer.icons.length && layer.icons[0].iconImage && layer.icons[0].iconImage !== 'no data') { + const imgSrc = layer.icons[0].iconImage; + return ( + + initLightBox(imgSrc, '', 0, 2)} + onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} + /> + + ); + } + + // if (!(childLayers?.length > 1 || items?.length > 1)) { + // return null; + // } + + // TODO: childslayers always empty... seems to be use for items + return ( + + {layer.children?.length > 0 && ( + + {layer.children + .filter((d) => !['error', 'processing'].includes(d.layerStatus ?? '')) + .map((item) => ( + + ))} + + )} + + + ); +}); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx new file mode 100644 index 00000000000..bd993ee435c --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx @@ -0,0 +1,111 @@ +import { useTheme } from '@mui/material'; +import { memo, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Box, + IconButton, + Stack, + VisibilityOutlinedIcon, + HighlightOutlinedIcon, + ZoomInSearchIcon, + Typography, + VisibilityOffOutlinedIcon, + HighlightIcon, +} from '@/ui'; +import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; +import { TypeLegendLayer } from '@/core/components/layers/types'; +import { useMapStoreActions } from '@/core/stores/'; +import { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; + +interface SecondaryControlsProps { + layer: TypeLegendLayer; + layerStatus: string; + itemsLength: number; + childLayers: TypeLegendLayer[]; +} + +// SecondaryControls component +export const SecondaryControls = memo(function SecondaryControls({ layer, layerStatus, itemsLength, childLayers }: SecondaryControlsProps) { + logger.logDebug('legend1 - ctrl', layer, layerStatus, itemsLength, childLayers); + // Hooks + const { t } = useTranslation(); + const theme = useTheme(); + const sxClasses = getSxClasses(theme); + + // Stores + const highlightedLayer = useLayerHighlightedLayer(); + const { getVisibilityFromOrderedLayerInfo, setOrToggleLayerVisibility } = useMapStoreActions(); + const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); + + const [visibility, setVisibility] = useState(getVisibilityFromOrderedLayerInfo(layer.layerPath)); + const isLayerVisible = layer.controls?.visibility ?? false; + + // #region Handlers + const handleToggleVisibility = useCallback( + (e: React.MouseEvent): void => { + e.stopPropagation(); + setOrToggleLayerVisibility(layer.layerPath); + setVisibility(getVisibilityFromOrderedLayerInfo(layer.layerPath)); + }, + [layer.layerPath, setOrToggleLayerVisibility] + ); + + const handleHighlightLayer = useCallback( + (e: React.MouseEvent): void => { + e.stopPropagation(); + setHighlightLayer(layer.layerPath); + }, + [layer.layerPath, setHighlightLayer] + ); + + const handleZoomTo = useCallback( + (e: React.MouseEvent): void => { + e.stopPropagation(); + zoomToLayerExtent(layer.layerPath).catch((error) => { + logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error); + }); + }, + [layer.layerPath, zoomToLayerExtent] + ); + // #endregion Handlers + + if (!['processed', 'loaded'].includes(layerStatus)) { + return ; + } + + let subTitle = ''; + if (childLayers.length) { + subTitle = t('legend.subLayersCount').replace('{count}', childLayers.length.toString()); + } else if (itemsLength > 1) { + subTitle = t('legend.itemsCount').replace('{count}', itemsLength.toString()).replace('{totalCount}', itemsLength.toString()); + } + + return ( + + {!!subTitle.length && {subTitle}} + + + {visibility ? : } + + + {highlightedLayer === layer.layerPath ? : } + + + + + + + ); +}); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx new file mode 100644 index 00000000000..8b3ee083497 --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx @@ -0,0 +1,36 @@ +import { useTheme } from '@mui/material'; +import { memo } from 'react'; +import { Box, ListItem, Tooltip, ListItemText, ListItemIcon, List, BrowserNotSupportedIcon } from '@/ui'; +import { TypeLegendLayer } from '@/core/components/layers/types'; +import { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; + +interface ItemsListProps { + items: TypeLegendLayer['items']; +} + +// ItemsList component +export const ItemsList = memo(function ItemsList({ items }: ItemsListProps) { + logger.logDebug('legend1 item list', items); + + // Hooks + const theme = useTheme(); + const sxClasses = getSxClasses(theme); + + if (!items?.length) { + return null; + } + + return ( + + {items.map((item, index) => ( + + {item.icon ? : } + + + + + ))} + + ); +}); diff --git a/packages/geoview-core/src/core/components/legend/legend-layer.tsx b/packages/geoview-core/src/core/components/legend/legend-layer.tsx index 70d07b043e7..d01e3adbb21 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer.tsx @@ -1,215 +1,52 @@ -import { useTheme } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { - Box, - ListItem, - Tooltip, - ListItemText, - ListItemIcon, - Collapse, - List, - BrowserNotSupportedIcon, - IconButton, - KeyboardArrowDownIcon, - KeyboardArrowUpIcon, - Stack, - VisibilityOutlinedIcon, - HighlightOutlinedIcon, - ZoomInSearchIcon, - Typography, - VisibilityOffOutlinedIcon, - HighlightIcon, -} from '@/ui'; -import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; +import { memo, useCallback, useMemo, useState } from 'react'; +import { ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; import { useMapStoreActions } from '@/core/stores/'; -import { getSxClasses } from './legend-styles'; -import { LayerIcon } from '@/core/components/common/layer-icon'; import { logger } from '@/core/utils/logger'; -import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; import { useLightBox } from '@/core/components/common'; +import { LayerIcon } from '../common/layer-icon'; +import { SecondaryControls } from './legend-layer-ctrl'; +import { CollapsibleContent } from './legend-layer-container'; + +// Define component types and interfaces +type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; interface LegendLayerProps { layer: TypeLegendLayer; } -export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { - // Log - logger.logTraceRender('components/legend/legend-layer'); +// Main LegendLayer component +export const LegendLayer: LegendLayerType = memo(function LegendLayer({ layer }: LegendLayerProps) { + // Hooks + logger.logDebug('legend1 LegendLayerType', layer); - const { t } = useTranslation(); - const theme = useTheme(); - const sxClasses = getSxClasses(theme); + // const { t } = useTranslation(); + // const theme = useTheme(); + // const sxClasses = getSxClasses(theme); + // Stores const { initLightBox, LightBoxComponent } = useLightBox(); + const { getLegendCollapsedFromOrderedLayerInfo, setLegendCollapsed } = useMapStoreActions(); - // Get store actions - const highlightedLayer = useLayerHighlightedLayer(); - const { getVisibilityFromOrderedLayerInfo, setOrToggleLayerVisibility, getLegendCollapsedFromOrderedLayerInfo, setLegendCollapsed } = - useMapStoreActions(); - const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); - - const getLayerChildren = (): TypeLegendLayer[] => { - return layer.children?.filter((c) => ['processed', 'loaded'].includes(c.layerStatus ?? '')); - }; - - /** - * Handle expand/shrink of layer groups. - */ - const handleExpandGroupClick = (): void => { - setLegendCollapsed(layer.layerPath); - }; - - /** - * Set the layer visivbility on the map - * @param {React.MouseEvent} e Mouse event - */ - const handleToggleVisibility = (e: React.MouseEvent): void => { - e.stopPropagation(); - setOrToggleLayerVisibility(layer.layerPath); - }; - - /** - * Set the highlight feature on the map for a layer - * @param {React.MouseEvent} e Mouse event - */ - const handleHighlightLayer = (e: React.MouseEvent): void => { - e.stopPropagation(); - setHighlightLayer(layer.layerPath); - }; - - /** - * Set the zoom on the map based on the layer path - * @param {React.MouseEvent} e Mouse event - */ - const handleZoomTo = (e: React.MouseEvent): void => { - e.stopPropagation(); - zoomToLayerExtent(layer.layerPath).catch((error) => { - // Log - logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error); - }); - }; - - const legendExpanded = !getLegendCollapsedFromOrderedLayerInfo(layer.layerPath); - - const visibility = !getVisibilityFromOrderedLayerInfo(layer.layerPath); - const isLayerVisible = layer.controls?.visibility ?? false; - - const getSecondaryText = (): JSX.Element => { - // dnt show icons when layer status is not loaded - if (!['processed', 'loaded'].includes(layer.layerStatus ?? '')) { - return ; - } - let subTitle = ''; - if (getLayerChildren().length) { - subTitle = t('legend.subLayersCount').replace('{count}', getLayerChildren().length.toString()); - } else if (layer.items.length > 1) { - subTitle = t('legend.itemsCount') - .replace('{count}', layer.items.length.toString()) - .replace('{totalCount}', layer.items.length.toString()); - } - return ( - - {!!subTitle.length && {subTitle}} - - handleToggleVisibility(e)} - disabled={!isLayerVisible} - > - {visibility ? : } - - handleHighlightLayer(e)} - > - {highlightedLayer === layer.layerPath ? : } - - handleZoomTo(e)}> - - - - - ); - }; - - // renders the layers children, if any - function renderChildren(): JSX.Element | null { - if (!layer.children?.length) { - return null; - } - - return ( - - {layer.children - .filter((d) => !['error', 'processing'].includes(d.layerStatus ?? '')) - .map((item) => ( - - ))} - - ); - } - - // renders the layers items if any - function renderItems(): JSX.Element | null { - if (!layer.items?.length) { - return null; - } - return ( - - {layer.items.map((item) => ( - - {item.icon ? : } - - - - - ))} - - ); - } - - function renderCollapsible(): JSX.Element | null { - if ( - layer.type === CV_CONST_LAYER_TYPES.WMS && - layer.icons.length && - layer.icons[0].iconImage && - layer.icons[0].iconImage !== 'no data' - ) { - const imgSrc = layer.icons[0].iconImage; - return ( - - initLightBox(imgSrc, '', 0, 2)} - onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} - /> - - ); - } + // Memoized values + const layerChildren = useMemo( + () => layer.children?.filter((c) => ['processed', 'loaded'].includes(c.layerStatus ?? '')) ?? [], + [layer.children] + ); - // show sub items only when number of items are more than 1. - if (!(layer.children?.length > 1 || layer.items?.length > 1)) { - return null; - } + const [legendExpanded, setLegendExpanded] = useState(getLegendCollapsedFromOrderedLayerInfo(layer.layerPath)); - return ( - - {renderChildren()} - {renderItems()} - - ); - } + const handleExpandGroupClick = useCallback( + (e: React.MouseEvent): void => { + e.stopPropagation(); + setLegendCollapsed(layer.layerPath); + setLegendExpanded(getLegendCollapsedFromOrderedLayerInfo(layer.layerPath)); + }, + [layer.layerPath, setLegendCollapsed] + ); return ( - + <> <> @@ -223,7 +60,14 @@ export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { primary={layer.layerName} className="layerTitle" disableTypography - secondary={getSecondaryText()} + secondary={ + + } /> {!!(layer.children?.length > 1 || layer.items?.length > 1) && ( @@ -233,9 +77,17 @@ export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { )} - - {renderCollapsible()} - - + + {LightBoxComponent} + ); -} +}); + +export default LegendLayer;