Skip to content

Commit

Permalink
Finalise legend component
Browse files Browse the repository at this point in the history
  • Loading branch information
jolevesq committed Dec 20, 2024
1 parent 623d973 commit a1781be
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,85 @@ 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';
import { logger } from '@/core/utils/logger';

// Define component types and interfaces
type LegendLayerType = React.FC<{ layer: TypeLegendLayer }>;

interface CollapsibleContentProps {
layer: TypeLegendLayer;
legendExpanded: boolean;
legendExpanded: boolean; // Expanded come from store ordered layer info array
initLightBox: (imgSrc: string, title: string, index: number, total: number) => void;
childLayers: TypeLegendLayer[];
items: TypeLegendLayer['items'];
LegendLayerComponent: LegendLayerType;
}

// CollapsibleContent component moved after LegendLayer
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,
childLayers,
items,
LegendLayerComponent,
}: CollapsibleContentProps) {
logger.logDebug('legend1 collapsible', layer, childLayers, items);
}: CollapsibleContentProps): JSX.Element | null {
logger.logTraceRender('components/legend/legend-layer-container');

// Hooks
const theme = useTheme();
const sxClasses = getSxClasses(theme);

// If it is a WMS and there is a legend image, add it with the lightbox handlers
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;
// 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 (
<Collapse in={legendExpanded} sx={sxClasses.collapsibleContainer} timeout="auto">
<Box
component="img"
tabIndex={0}
src={imgSrc}
sx={{ maxWidth: '100%', cursor: 'pointer' }}
onClick={() => initLightBox(imgSrc, '', 0, 2)}
onKeyDown={(e) => (e.code === 'Space' || e.code === 'Enter' ? initLightBox(imgSrc, '', 0, 2) : null)}
/>
</Collapse>
<WMSLegendImage
imgSrc={layer.icons[0].iconImage || ''}
initLightBox={initLightBox}
legendExpanded={legendExpanded}
sxClasses={sxClasses}
/>
);
}

// If there is only one item or no childlayer, do not create the component
if (childLayers?.length === 0 && items?.length === 1) {
return null;
}

if (layer.children?.length > 0) layer.children.every((item) => logger.logDebug('TTT item', item.layerPath));

// Render list of items (layer class) or there is a child layer so render a new legend-layer component
return (
<Collapse in={legendExpanded} sx={sxClasses.collapsibleContainer} timeout="auto">
{layer.children?.length > 0 && (
<List sx={{ width: '100%', padding: '20px', margin: '20px 0px' }}>
{layer.children
.filter((d) => !['error', 'processing'].includes(d.layerStatus ?? ''))
.map((item) => (
<LegendLayerComponent layer={item} key={item.layerPath} />
))}
</List>
)}
<List>
{layer.children
.filter((d) => !['error', 'processing'].includes(d.layerStatus ?? ''))
.map((item) => (
<LegendLayerComponent layer={item} key={item.layerPath} />
))}
</List>
<ItemsList items={items} />
</Collapse>
);
Expand Down
113 changes: 65 additions & 48 deletions packages/geoview-core/src/core/components/legend/legend-layer-ctrl.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useTheme } from '@mui/material';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Expand All @@ -13,74 +13,91 @@ import {
HighlightIcon,
} from '@/ui';
import { useLayerHighlightedLayer, useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state';
import { TypeLegendLayer } from '@/core/components/layers/types';
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;
layerStatus: string;
itemsLength: number;
childLayers: TypeLegendLayer[];
visibility: boolean;
visibility: boolean; // Visibility come from store ordered layer info array
}

// SecondaryControls component
export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers, visibility }: SecondaryControlsProps): JSX.Element {
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();
const { setOrToggleLayerVisibility } = useMapStoreActions();
const { setHighlightLayer, zoomToLayerExtent } = useLayerStoreActions();

// Is button disabled?
const isLayerVisible = layer.controls?.visibility ?? false;

// #region Handlers Callbacks
const handleToggleVisibility = useCallback(
(e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
setOrToggleLayerVisibility(layer.layerPath);
},
[layer.layerPath, setOrToggleLayerVisibility]
);

const handleHighlightLayer = useCallback(
(e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
setHighlightLayer(layer.layerPath);
},
[layer.layerPath, setHighlightLayer]
);
// Extract constant from layer prop
const { layerStatus, items, children } = layer;

const handleZoomTo = useCallback(
(e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
zoomToLayerExtent(layer.layerPath).catch((error) => {
logger.logPromiseFailed('in zoomToLayerExtent in legend-layer.handleZoomTo', error);
});
},
[layer.layerPath, zoomToLayerExtent]
);
// #endregion Handlers
// Component helper
const controls = useControlActions(layer.layerPath);
const subTitle = useSubtitle(children, items);

if (!['processed', 'loaded'].includes(layerStatus)) {
if (!['processed', 'loaded'].includes(layerStatus || 'error')) {
return <Box />;
}

// Calculate subtitle after the condition
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 (
<Stack direction="row" alignItems="center" sx={sxClasses.layerStackIcons}>
{!!subTitle.length && <Typography fontSize={14}>{subTitle}</Typography>}
Expand All @@ -89,7 +106,7 @@ export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers
edge="end"
tooltip="layers.toggleVisibility"
className="buttonOutline"
onClick={handleToggleVisibility}
onClick={controls.handleToggleVisibility}
disabled={!isLayerVisible}
>
{visibility ? <VisibilityOutlinedIcon /> : <VisibilityOffOutlinedIcon />}
Expand All @@ -98,11 +115,11 @@ export function SecondaryControls({ layer, layerStatus, itemsLength, childLayers
tooltip="legend.highlightLayer"
sx={{ marginTop: '-0.3125rem' }}
className="buttonOutline"
onClick={handleHighlightLayer}
onClick={controls.handleHighlightLayer}
>
{highlightedLayer === layer.layerPath ? <HighlightIcon /> : <HighlightOutlinedIcon />}
</IconButton>
<IconButton tooltip="legend.zoomTo" className="buttonOutline" onClick={handleZoomTo}>
<IconButton tooltip="legend.zoomTo" className="buttonOutline" onClick={controls.handleZoomTo}>
<ZoomInSearchIcon />
</IconButton>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { useTheme } from '@mui/material';
import { memo, useMemo } from 'react';
import { Box, ListItem, Tooltip, ListItemText, ListItemIcon, List, BrowserNotSupportedIcon } from '@/ui';
import { TypeLegendLayer } from '@/core/components/layers/types';
import { TypeLegendItem } from '@/core/components/layers/types';
import { getSxClasses } from './legend-styles';
import { logger } from '@/core/utils/logger';

interface ItemsListProps {
items: TypeLegendLayer['items'];
items: TypeLegendItem[];
}

// ItemsList component
export const ItemsList = memo(function ItemsList({ items }: ItemsListProps) {
logger.logDebug('legend1 item list', 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
const listItems = items.map((item) => (
// 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';

return <List sx={sxClasses.subList}>{listItems}</List>;
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

0 comments on commit a1781be

Please sign in to comment.