diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts index 37bc8f8ed07..9b06ddd5588 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts @@ -357,7 +357,15 @@ export class LegendEventProcessor extends AbstractEventProcessor { createNewLegendEntries(2, layers); // Update the legend layers with the updated array, triggering the subscribe - this.getLayerState(mapId).setterActions.setLegendLayers(layers); + // Reorder the array so legend tab is in synch + const sortedLayers = layers.sort((a, b) => + MapEventProcessor.getMapIndexFromOrderedLayerInfo(mapId, a.layerPath) > + MapEventProcessor.getMapIndexFromOrderedLayerInfo(mapId, b.layerPath) + ? 1 + : -1 + ); + + this.getLayerState(mapId).setterActions.setLegendLayers(sortedLayers); } // #endregion 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..d8b227fc069 --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-container.tsx @@ -0,0 +1,96 @@ +import { useTheme } from '@mui/material'; +import { memo, useMemo } from 'react'; +import { Box, Collapse, List } from '@/ui'; +import { TypeLegendLayer } from '@/core/components/layers/types'; +import { getSxClasses } from './legend-styles'; +import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; +import { ItemsList } from './legend-layer-items'; +import { logger } from '@/core/utils/logger'; + +// Define component types and interfaces +type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>; + +interface CollapsibleContentProps { + layer: TypeLegendLayer; + legendExpanded: boolean; // Expanded come from store ordered layer info array + initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; + LegendLayerComponent: LegendLayerType; +} + +interface WMSLegendImageProps { + imgSrc: string; + initLightBox: (imgSrc: string, title: string, index: number, total: number) => void; + legendExpanded: boolean; + sxClasses: Record; +} + +// Constant style outside of render +const styles = { + wmsImage: { + maxWidth: '90%', + cursor: 'pointer', + }, +} as const; + +// Extracted WMS Legend Component +const WMSLegendImage = memo( + ({ imgSrc, initLightBox, legendExpanded, sxClasses }: WMSLegendImageProps): JSX.Element => ( + + initLightBox(imgSrc, '', 0, 2)} + onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)} + /> + + ) +); +WMSLegendImage.displayName = 'WMSLegendImage'; + +export const CollapsibleContent = memo(function CollapsibleContent({ + layer, + legendExpanded, + initLightBox, + LegendLayerComponent, +}: CollapsibleContentProps): JSX.Element | null { + logger.logTraceRender('components/legend/legend-layer-container'); + + // Hooks + const theme = useTheme(); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + + // Props extraction + const { children, items } = layer; + + // Early returns + if (children?.length === 0 && items?.length === 1) return null; + + const isWMSWithLegend = layer.type === CV_CONST_LAYER_TYPES.WMS && layer.icons?.[0]?.iconImage && layer.icons[0].iconImage !== 'no data'; + + // If it is a WMS legend, create a specific component + if (isWMSWithLegend) { + return ( + + ); + } + + return ( + + + {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..bea5dcea6fc --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx @@ -0,0 +1,128 @@ +import { useTheme } from '@mui/material'; +import { useMemo } 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 { TypeLegendItem, 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; + visibility: boolean; // Visibility come from store ordered layer info array +} + +type ControlActions = { + handleToggleVisibility: (e: React.MouseEvent) => void; + handleHighlightLayer: (e: React.MouseEvent) => void; + handleZoomTo: (e: React.MouseEvent) => void; +}; + +// Constant style outside of render +const styles = { + btnMargin: { marginTop: '-0.3125rem' }, +} as const; + +// Custom hook for control actions +const useControlActions = (layerPath: string): ControlActions => { + const { setOrToggleLayerVisibility } = useMapStoreActions(); + const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions(); + + return useMemo( + () => ({ + handleToggleVisibility: (e: React.MouseEvent): void => { + e.stopPropagation(); + setOrToggleLayerVisibility(layerPath); + }, + handleHighlightLayer: (e: React.MouseEvent): void => { + e.stopPropagation(); + setHighlightLayer(layerPath); + }, + handleZoomTo: (e: React.MouseEvent): void => { + e.stopPropagation(); + zoomToLayerExtent(layerPath).catch((error) => { + logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error); + }); + }, + }), + [layerPath, setHighlightLayer, setOrToggleLayerVisibility, zoomToLayerExtent] + ); +}; + +// Create subtitle +const useSubtitle = (children: TypeLegendLayer[], items: TypeLegendItem[]): string => { + // Hooks + const { t } = useTranslation(); + + return useMemo(() => { + if (children.length) { + return t('legend.subLayersCount').replace('{count}', children.length.toString()); + } + if (items.length > 1) { + return t('legend.itemsCount') + .replace('{count}', items.filter((item) => item.isVisible).length.toString()) + .replace('{totalCount}', items.length.toString()); + } + return ''; + }, [children.length, items, t]); +}; + +// SecondaryControls component (no memo to force re render from layers panel modifications) +export function SecondaryControls({ layer, visibility }: SecondaryControlsProps): JSX.Element { + logger.logTraceRender('components/legend/legend-layer-ctrl'); + + // Hooks + const theme = useTheme(); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + + // Stores + const highlightedLayer = useLayerHighlightedLayer(); + + // Is button disabled? + const isLayerVisible = layer.controls?.visibility ?? false; + + // Extract constant from layer prop + const { layerStatus, items, children } = layer; + + // Component helper + const controls = useControlActions(layer.layerPath); + const subTitle = useSubtitle(children, items); + + if (!['processed', 'loaded'].includes(layerStatus || 'error')) { + return ; + } + + 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..440f39bbd5f --- /dev/null +++ b/packages/geoview-core/src/core/components/legend/legend-layer-items.tsx @@ -0,0 +1,43 @@ +import { useTheme } from '@mui/material'; +import { memo, useMemo } from 'react'; +import { Box, ListItem, Tooltip, ListItemText, ListItemIcon, List, BrowserNotSupportedIcon } from '@/ui'; +import { TypeLegendItem } from '@/core/components/layers/types'; +import { getSxClasses } from './legend-styles'; +import { logger } from '@/core/utils/logger'; + +interface ItemsListProps { + items: TypeLegendItem[]; +} + +// Extracted ListItem Component +const LegendListItem = memo( + ({ item }: { item: TypeLegendItem }): JSX.Element => ( + + {item.icon ? : } + + + + + ) +); +LegendListItem.displayName = 'LegendListItem'; + +// Item list component (no memo to force re render from layers panel modifications) +export const ItemsList = memo(function ItemsList({ items }: ItemsListProps): JSX.Element | null { + logger.logTraceRender('components/legend/legend-layer-items'); + + // Hooks + const theme = useTheme(); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + + if (!items?.length) return null; + + // Direct mapping since we only reach this code if items has content + return ( + + {items.map((item) => ( + + ))} + + ); +}); 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..78a11f4adf7 100644 --- a/packages/geoview-core/src/core/components/legend/legend-layer.tsx +++ b/packages/geoview-core/src/core/components/legend/legend-layer.tsx @@ -1,241 +1,102 @@ +import { memo, useCallback, useMemo } from 'react'; 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 { Box, ListItem, Tooltip, ListItemText, IconButton, KeyboardArrowDownIcon, KeyboardArrowUpIcon } from '@/ui'; import { TypeLegendLayer } from '@/core/components/layers/types'; -import { useMapStoreActions } from '@/core/stores/'; +import { useLayerStoreActions, useMapStoreActions } from '@/core/stores/'; +import { useLightBox } from '@/core/components/common'; +import { LayerIcon } from '../common/layer-icon'; +import { SecondaryControls } from './legend-layer-ctrl'; +import { CollapsibleContent } from './legend-layer-container'; 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'; interface LegendLayerProps { layer: TypeLegendLayer; } +interface LegendLayerHeaderProps { + layer: TypeLegendLayer; + isCollapsed: boolean; + isVisible: boolean; + onExpandClick: (e: React.MouseEvent) => void; +} + +// Constant style outside of render +const styles = { + listItemText: { + '&:hover': { cursor: 'pointer' }, + }, +} as const; + +// Extracted Header Component +const LegendLayerHeader = memo( + ({ layer, isCollapsed, isVisible, onExpandClick }: LegendLayerHeaderProps): JSX.Element => ( + + + + } + /> + + {(layer.children?.length > 1 || layer.items?.length > 1) && ( + + {!isCollapsed ? : } + + )} + + ) +); +LegendLayerHeader.displayName = 'LegendLayerHeader'; + +// Main LegendLayer component export function LegendLayer({ layer }: LegendLayerProps): JSX.Element { - // Log logger.logTraceRender('components/legend/legend-layer'); - const { t } = useTranslation(); + // Hooks const theme = useTheme(); - const sxClasses = getSxClasses(theme); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + // Stores const { initLightBox, LightBoxComponent } = useLightBox(); - - // 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); + const { getLegendCollapsedFromOrderedLayerInfo, getVisibilityFromOrderedLayerInfo, setLegendCollapsed } = useMapStoreActions(); + const { getLayerStatus } = useLayerStoreActions(); + const isCollapsed = getLegendCollapsedFromOrderedLayerInfo(layer.layerPath); + const isVisible = getVisibilityFromOrderedLayerInfo(layer.layerPath); + const layerStatus = getLayerStatus(layer.layerPath); + + // Create a new layer object with updated status (no useMemo to ensure updates) + const currentLayer = { + ...layer, + layerStatus, + items: layer.items?.map((item) => ({ + ...item, + })), }; - /** - * 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)} - /> - - ); - } - - // show sub items only when number of items are more than 1. - if (!(layer.children?.length > 1 || layer.items?.length > 1)) { - return null; - } - - return ( - - {renderChildren()} - {renderItems()} - - ); - } + const handleExpandGroupClick = useCallback( + (e: React.MouseEvent): void => { + e.stopPropagation(); + setLegendCollapsed(layer.layerPath); // store value + }, + [layer.layerPath, setLegendCollapsed] + ); return ( - - - - <> - - - - {!!(layer.children?.length > 1 || layer.items?.length > 1) && ( - - {legendExpanded ? : } - - )} - - - - {renderCollapsible()} + <> + + + + - + ); } diff --git a/packages/geoview-core/src/core/components/legend/legend-styles.ts b/packages/geoview-core/src/core/components/legend/legend-styles.ts index 4d776e271b8..65d0a4dcfbb 100644 --- a/packages/geoview-core/src/core/components/legend/legend-styles.ts +++ b/packages/geoview-core/src/core/components/legend/legend-styles.ts @@ -24,7 +24,6 @@ export const getSxClasses = (theme: Theme): SxClasses => ({ fontWeight: 'normal', fontSize: theme.palette.geoViewFontSize.md, textAlign: 'left', - marginBottom: '15px', }, layersListContainer: { padding: '20px', diff --git a/packages/geoview-core/src/core/components/legend/legend.tsx b/packages/geoview-core/src/core/components/legend/legend.tsx index fa7ac2b2f11..dc82e84f48e 100644 --- a/packages/geoview-core/src/core/components/legend/legend.tsx +++ b/packages/geoview-core/src/core/components/legend/legend.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@mui/material'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Typography } from '@/ui'; import { useGeoViewMapId } from '@/core/stores/'; @@ -18,45 +18,78 @@ interface LegendType { containerType?: 'appBar' | 'footerBar'; } +// Constant style outside of render (styles) +const styles = { + noLayersContainer: { + padding: '2rem', + margin: '2rem', + width: '100%', + textAlign: 'center', + }, + layerBox: { + paddingRight: '0.65rem', + }, + flexContainer: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + }, +} as const; + +// Constant style outside of render (responsive widths) +const responsiveWidths = { + full: { xs: '100%' }, + responsive: { + xs: '100%', + sm: '50%', + md: '33.33%', + lg: '25%', + xl: '25%', + }, +} as const; + export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): JSX.Element { - // Log logger.logTraceRender('components/legend/legend'); - const mapId = useGeoViewMapId(); + // Hooks const { t } = useTranslation(); - const theme = useTheme(); const sxClasses = getSxClasses(theme); - // internal state + // State const [legendLayers, setLegendLayers] = useState([]); const [formattedLegendLayerList, setFormattedLegendLayersList] = useState([]); - // store state + // Store + const mapId = useGeoViewMapId(); const orderedLayerInfo = useMapOrderedLayerInfo(); const layersList = useLayerLegendLayers(); // Custom hook for calculating the height of footer panel const { leftPanelRef } = useFooterPanelHeight({ footerPanelTab: 'legend' }); + // Memoize breakpoint values + const breakpoints = useMemo( + () => ({ + sm: theme.breakpoints.values.sm, + md: theme.breakpoints.values.md, + lg: theme.breakpoints.values.lg, + }), + [theme.breakpoints.values.sm, theme.breakpoints.values.md, theme.breakpoints.values.lg] + ); + /** * Get the size of list based on window size. */ - const getLegendLayerListSize = useMemo(() => { - return () => { - let size = 4; - // when legend is loaded in appbar size will always be 1. - if (containerType === CONTAINER_TYPE.APP_BAR) return 1; - if (window.innerWidth < theme.breakpoints.values.sm) { - size = 1; - } else if (window.innerWidth < theme.breakpoints.values.md) { - size = 2; - } else if (window.innerWidth < theme.breakpoints.values.lg) { - size = 3; - } - return size; - }; - }, [theme.breakpoints.values.lg, theme.breakpoints.values.md, theme.breakpoints.values.sm, containerType]); + const getLegendLayerListSize = useCallback(() => { + if (containerType === CONTAINER_TYPE.APP_BAR) return 1; + + const { innerWidth } = window; + if (innerWidth < breakpoints.sm) return 1; + if (innerWidth < breakpoints.md) return 2; + if (innerWidth < breakpoints.lg) return 3; + return 4; + }, [breakpoints, containerType]); /** * Transform the list of the legends into subsets of lists. @@ -65,74 +98,69 @@ export function Legend({ fullWidth, containerType = 'footerBar' }: LegendType): * @param {TypeLegendLayer} layers array of layers. * @returns List of array of layers */ - const updateLegendLayerListByWindowSize = (layers: TypeLegendLayer[]): void => { - const arrSize = getLegendLayerListSize(); - - // create list of arrays based on size of the window. - const list = Array.from({ length: arrSize }, () => []) as Array; - layers.forEach((layer, index) => { - const idx = index % arrSize; - list[idx].push(layer); - }); - setFormattedLegendLayersList(list); - }; + const updateLegendLayerListByWindowSize = useCallback( + (layers: TypeLegendLayer[]): void => { + const arrSize = getLegendLayerListSize(); + const list = Array.from({ length: arrSize }, () => []) as Array; + + layers.forEach((layer, index) => { + list[index % arrSize].push(layer); + }); + + setFormattedLegendLayersList(list); + }, + [getLegendLayerListSize] + ); + // Handle initial layer setup useEffect(() => { - // Log - logger.logTraceUseEffect('LEGEND - visibleLayers', orderedLayerInfo.length, orderedLayerInfo); - + logger.logTraceUseEffect('LEGEND - layer setup', orderedLayerInfo.length, orderedLayerInfo, layersList); setLegendLayers(layersList); updateLegendLayerListByWindowSize(layersList); + }, [orderedLayerInfo, layersList, updateLegendLayerListByWindowSize]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderedLayerInfo, layersList]); - + // Handle window resize useEffect(() => { - // Log - logger.logTraceUseEffect('LEGEND - legendLayers', legendLayers); + logger.logTraceUseEffect('LEGEND - window resize', legendLayers); // update subsets of list when window size updated. const formatLegendLayerList = (): void => { - // Log logger.logTraceCore('LEGEND - window resize event'); updateLegendLayerListByWindowSize(legendLayers); }; window.addEventListener('resize', formatLegendLayerList); + return () => window.removeEventListener('resize', formatLegendLayerList); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [legendLayers]); + }, [legendLayers, updateLegendLayerListByWindowSize]); + + // Memoize the rendered content based on whether there are legend layers + const content = useMemo(() => { + if (!legendLayers.length) { + return ( + + + {t('legend.noLayersAdded')} + + + {t('legend.noLayersAddedDescription')} + + + ); + } + + return formattedLegendLayerList.map((layers, idx) => ( + + {layers.map((layer) => ( + + ))} + + )); + }, [legendLayers, formattedLegendLayerList, fullWidth, sxClasses, t]); return ( - - {!!legendLayers.length && - formattedLegendLayerList.map((layers, idx) => { - return ( - - {layers.map((layer) => { - return ; - })} - - ); - })} - - {/* Show legend Instructions when no layer found. */} - {!legendLayers.length && ( - - - {t('legend.noLayersAdded')} - - - {t('legend.noLayersAddedDescription')} - - - )} - + {content} ); } diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts index d6447c9571d..f186826bc57 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts @@ -11,6 +11,7 @@ import { TypeGetStore, TypeSetStore } from '@/core/stores/geoview-store'; import { layerEntryIsEsriDynamic, TypeFeatureInfoEntryPartial, + TypeLayerStatus, TypeLayerStyleConfig, TypeResultSet, TypeResultSetEntry, @@ -21,6 +22,7 @@ import { MapEventProcessor } from '@/api/event-processors/event-processor-childr import { TypeGeoviewLayerType, TypeVectorLayerStyles } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; import { esriQueryRecordsByUrlObjectIds } from '@/geo/layer/gv-layers/utils'; +import { CV_CONST_LAYER_TYPES } from '@/api/config/types/config-constants'; // #region INTERFACES & TYPES @@ -42,6 +44,7 @@ export interface ILayerState { getLayer: (layerPath: string) => TypeLegendLayer | undefined; getLayerBounds: (layerPath: string) => number[] | undefined; getLayerDeleteInProgress: () => boolean; + getLayerStatus: (layerPath: string) => TypeLayerStatus; refreshLayer: (layerPath: string) => void; setAllItemsVisibility: (layerPath: string, visibility: boolean) => void; setDisplayState: (newDisplayState: TypeLayersViewDisplayState) => void; @@ -154,6 +157,16 @@ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILay */ getLayerDeleteInProgress: () => get().layerState.layerDeleteInProgress, + /** + * Gets the layer status in the store which correspond to the layer path + * @param {string} layerPath - The layer path of the bounds to get + * @returns {TypeLayerStatus | undefined} The status or undefined + */ + getLayerStatus: (layerPath: string): TypeLayerStatus | undefined => { + const curLayers = get().layerState.legendLayers; + return LegendEventProcessor.findLayerByPath(curLayers, layerPath)!.layerStatus; + }, + /** * Refresh layer and set states to original values. * @param {string} layerPath - The layer path of the layer to change. @@ -381,8 +394,11 @@ export const useSelectedLayer = (): TypeLegendLayer | undefined => { export const useIconLayerSet = (layerPath: string): string[] => { const layers = useStore(useGeoViewStore(), (state) => state.layerState.legendLayers); const layer = LegendEventProcessor.findLayerByPath(layers, layerPath); - if (layer) { + if (layer && layer.type !== CV_CONST_LAYER_TYPES.WMS) { return layer.items.map((item) => item.icon).filter((d) => d !== null) as string[]; } + if (layer && layer.type === CV_CONST_LAYER_TYPES.WMS) { + return layer.icons.map((item) => item.iconImage).filter((d) => d !== null) as string[]; + } return []; }; diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts index ff9095f68bf..23681f117f9 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts @@ -812,13 +812,19 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt */ setLegendCollapsed: (layerPath: string, collapsed?: boolean): void => { const curLayerInfo = get().mapState.orderedLayerInfo; - const layerInfo = curLayerInfo.find((info) => info.layerPath === layerPath); - if (layerInfo) { - const newCollapsed = collapsed || !layerInfo.legendCollapsed; - layerInfo.legendCollapsed = newCollapsed; + const layerIndex = curLayerInfo.findIndex((info) => info.layerPath === layerPath); + + if (layerIndex !== -1) { + // Create shallow copy of array + const newLayerInfo = curLayerInfo.slice(); + // Only create new object for the changed layer + newLayerInfo[layerIndex] = { + ...curLayerInfo[layerIndex], + legendCollapsed: collapsed ?? !curLayerInfo[layerIndex].legendCollapsed, + }; // Redirect - get().mapState.setterActions.setOrderedLayerInfo(curLayerInfo); + get().mapState.setterActions.setOrderedLayerInfo(newLayerInfo); } },