Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(legend): Simplify the code and tweak performance #2663

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 { 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<string, object>;
}

// Extracted WMS Legend Component
const WMSLegendImage = memo(
({ imgSrc, initLightBox, legendExpanded, sxClasses }: WMSLegendImageProps): JSX.Element => (
<Collapse in={legendExpanded} sx={sxClasses!.collapsibleContainer} timeout="auto">
<Box
component="img"
tabIndex={0}
src={imgSrc}
sx={{ maxWidth: '90%', cursor: 'pointer' }}
onClick={() => initLightBox(imgSrc, '', 0, 2)}
onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)}
/>
</Collapse>
)
);
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 = getSxClasses(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 (
<WMSLegendImage
imgSrc={layer.icons[0].iconImage || ''}
initLightBox={initLightBox}
legendExpanded={legendExpanded}
sxClasses={sxClasses}
/>
);
}

return (
<Collapse in={legendExpanded} sx={sxClasses.collapsibleContainer} timeout="auto">
<List>
{layer.children
.filter((d) => !['error', 'processing'].includes(d.layerStatus ?? ''))
.map((item) => (
<LegendLayerComponent layer={item} key={item.layerPath} />
))}
</List>
<ItemsList items={items} />
</Collapse>
);
});
128 changes: 128 additions & 0 deletions packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx
Original file line number Diff line number Diff line change
@@ -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;
};

// 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
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 <Box />;
}

return (
<Stack direction="row" alignItems="center" sx={sxClasses.layerStackIcons}>
{!!subTitle.length && <Typography fontSize={14}>{subTitle}</Typography>}
<Box sx={sxClasses.subtitle}>
<IconButton
edge="end"
tooltip="layers.toggleVisibility"
className="buttonOutline"
onClick={controls.handleToggleVisibility}
disabled={!isLayerVisible}
>
{visibility ? <VisibilityOutlinedIcon /> : <VisibilityOffOutlinedIcon />}
</IconButton>
<IconButton
tooltip="legend.highlightLayer"
sx={{ marginTop: '-0.3125rem' }}
className="buttonOutline"
onClick={controls.handleHighlightLayer}
>
{highlightedLayer === layer.layerPath ? <HighlightIcon /> : <HighlightOutlinedIcon />}
</IconButton>
<IconButton tooltip="legend.zoomTo" className="buttonOutline" onClick={controls.handleZoomTo}>
<ZoomInSearchIcon />
</IconButton>
</Box>
</Stack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 => (
<ListItem key={`${item.icon}-${item.name}`} className={!item.isVisible ? 'unchecked' : 'checked'}>
<ListItemIcon>{item.icon ? <Box component="img" alt={item.name} src={item.icon} /> : <BrowserNotSupportedIcon />}</ListItemIcon>
<Tooltip title={item.name} placement="top" enterDelay={1000}>
<ListItemText primary={item.name} />
</Tooltip>
</ListItem>
)
);
LegendListItem.displayName = 'LegendListItem';

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 (
<List sx={sxClasses.subList}>
{items.map((item) => (
<LegendListItem item={item} key={`${item.icon}-${item.name}`} />
))}
</List>
);
});
Loading
Loading