Skip to content
Merged
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
198 changes: 109 additions & 89 deletions src/components/BaselayerSections.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,129 @@
import { LayerSelectorProps } from './LayerSelector';
import { CollapsibleSection } from './CollapsibleSection';
import { EXTERNAL_BASELAYERS } from '../configs/mapSettings';
import { useState, ReactNode, useCallback, memo } from 'react';
import { LayerSelectorProps, NoMatches } from './LayerSelector';
import CollapsibleSection from './CollapsibleSection';
import {
EXTERNAL_BASELAYERS,
EXTERNAL_DETAILS_ID,
} from '../configs/mapSettings';
import { getDefaultExpandedState, filterMapGroups } from '../utils/filterUtils';
import { ChevronRightIcon } from './icons/ChevronRightIcon';
import { ChevronDownIcon } from './icons/ChevronDownIcon';

type BaselayerSectionsProps = {
mapGroups: LayerSelectorProps['mapGroups'];
activeBaselayerId: LayerSelectorProps['activeBaselayerId'];
isFlipped: LayerSelectorProps['isFlipped'];
onBaselayerChange: LayerSelectorProps['onBaselayerChange'];
searchText: string;
markMatchingSearchText: (
label: string,
shouldHighlight?: boolean
) => string | ReactNode;
};

export function BaselayerSections({
function BaselayerSections({
mapGroups,
activeBaselayerId,
isFlipped,
onBaselayerChange,
searchText,
markMatchingSearchText,
}: BaselayerSectionsProps) {
const [expandedState, setExpandedState] = useState<Set<string>>(
getDefaultExpandedState(mapGroups, activeBaselayerId)
);
const { filteredMapGroups, matchedIds } = filterMapGroups(
mapGroups,
searchText
);
const filteredExternalLayers = EXTERNAL_BASELAYERS.filter((bl) =>
bl.name.toLowerCase().includes(searchText.toLowerCase())
);
const isEmpty =
filteredMapGroups.length + filteredExternalLayers.length === 0;

const handleToggle = useCallback(
(id: string) => {
if (expandedState.has(id)) {
setExpandedState((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
} else {
setExpandedState((prev) => new Set(prev).add(id));
}
},
[expandedState]
);

if (isEmpty) {
return <NoMatches />;
}

return (
<>
{mapGroups.map((group, groupIndex) => (
{filteredMapGroups.map((group) => (
<CollapsibleSection
key={'section-internal-mapGroup-' + groupIndex}
summary={group.name}
defaultOpen={groupIndex === 0}
tooltip={group.description}
>
{group.maps.map((map, mapIndex) => (
<CollapsibleSection
key={
'section-internal-mapGroup-' + groupIndex + '-map-' + map.map_id
}
summary={map.name}
defaultOpen={mapIndex === 0}
tooltip={map.description}
nestedDepth={1}
>
{map.bands.map((band, bandIndex) => (
<CollapsibleSection
key={
'section-internal-mapGroup-' +
groupIndex +
'-map-' +
map.map_id +
'-band-' +
band.band_id
}
summary={band.name}
defaultOpen={bandIndex === 0}
tooltip={band.description}
nestedDepth={2}
>
{band.layers.map((layer) => (
<label
key={'baselayer-label-' + layer.layer_id}
className="layer-selecter-input-label"
>
<input
key={'baselayer-input-' + layer.layer_id}
type="radio"
id={String(layer.layer_id)}
value={layer.layer_id}
name="baselayer"
checked={layer.layer_id === activeBaselayerId}
onChange={() =>
onBaselayerChange(String(layer.layer_id), 'layerMenu')
}
/>
{layer.name}
</label>
))}
</CollapsibleSection>
))}
</CollapsibleSection>
))}
</CollapsibleSection>
key={group.name}
node={group}
nestedDepth={0}
onBaselayerChange={onBaselayerChange}
activeBaselayerId={activeBaselayerId}
searchText={searchText}
expandedState={expandedState}
markMatchingSearchText={markMatchingSearchText}
matchedIds={matchedIds}
highlightMatch={matchedIds.has(group.name)}
handleToggle={handleToggle}
/>
))}
{
<CollapsibleSection
key="section-comparison-maps"
summary="Comparison maps"
defaultOpen={true}
>
{EXTERNAL_BASELAYERS.map((bl) => (
<div
className={`input-container ${bl.disabledState(isFlipped) ? 'disabled' : ''}`}
key={bl.layer_id}
title={
bl.disabledState(isFlipped)
? 'The current RA range is incompatible with this baselayer.'
: undefined
}
>
<input
type="radio"
id={bl.layer_id}
value={bl.layer_id}
name="baselayer"
checked={bl.layer_id === activeBaselayerId}
onChange={() => onBaselayerChange(bl.layer_id, 'layerMenu')}
disabled={bl.disabledState(isFlipped)}
/>
<label htmlFor={bl.layer_id}>{bl.name}</label>
</div>
))}
</CollapsibleSection>
}
{filteredExternalLayers.length > 0 && (
<div>
<div
title="External maps used for comparison"
onClick={() => handleToggle(EXTERNAL_DETAILS_ID)}
className="layer-title-container"
>
{expandedState.has(EXTERNAL_DETAILS_ID) ? (
<ChevronDownIcon />
) : (
<ChevronRightIcon />
)}
Comparison maps
</div>
{expandedState.has(EXTERNAL_DETAILS_ID) &&
filteredExternalLayers.map((bl) => (
<div
className={`input-container ${bl.disabledState(isFlipped) ? 'disabled' : ''}`}
key={bl.layer_id}
title={
bl.disabledState(isFlipped)
? 'The current RA range is incompatible with this baselayer.'
: undefined
}
>
<input
type="radio"
id={bl.layer_id}
value={bl.layer_id}
name="baselayer"
checked={bl.layer_id === activeBaselayerId}
onChange={() => onBaselayerChange(bl.layer_id, 'layerMenu')}
disabled={bl.disabledState(isFlipped)}
/>
<label
htmlFor={bl.layer_id}
className="external-layer-selector-input-label"
>
{markMatchingSearchText(bl.name)}
</label>
</div>
))}
</div>
)}
</>
);
}

export default memo(BaselayerSections);
2 changes: 1 addition & 1 deletion src/components/CenterMapFeature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function CenterMapFeature({
className="center-feature-form generic-form"
onSubmit={onSubmit}
>
<label>
<label className="dialog-label">
Feature Name:
<input
name="feature_name"
Expand Down
121 changes: 106 additions & 15 deletions src/components/CollapsibleSection.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,115 @@
import { ReactNode } from 'react';
import { ReactNode, memo } from 'react';
import { LayerSelectorProps } from './LayerSelector';
import { ChevronRightIcon } from './icons/ChevronRightIcon';
import { ChevronDownIcon } from './icons/ChevronDownIcon';
import {
BandResponse,
LayerResponse,
MapGroupResponse,
MapResponse,
} from '../types/maps';
import { getNodeId } from '../utils/filterUtils';

type Props = {
summary: ReactNode;
children: ReactNode;
defaultOpen: boolean;
tooltip?: string;
nestedDepth?: number;
node: MapGroupResponse | MapResponse | BandResponse | LayerResponse;
nestedDepth: number;
onBaselayerChange: LayerSelectorProps['onBaselayerChange'];
activeBaselayerId: LayerSelectorProps['activeBaselayerId'];
searchText: string;
expandedState: Set<string>;
markMatchingSearchText: (
label: string,
shouldHighlight?: boolean
) => string | ReactNode;
matchedIds: Set<string>;
highlightMatch: boolean;
handleToggle: (id: string) => void;
};

export function CollapsibleSection({
summary,
defaultOpen,
children,
tooltip,
nestedDepth = 0,
function CollapsibleSection({
node,
nestedDepth,
onBaselayerChange,
activeBaselayerId,
searchText,
expandedState,
markMatchingSearchText,
matchedIds,
highlightMatch,
handleToggle,
}: Props) {
let children;
const nodeId = getNodeId(node);

if (expandedState.has(nodeId) || searchText.length > 0) {
if ('band_id' in node) {
children = (node as BandResponse).layers.map((layer) => (
<label
key={'baselayer-label-' + layer.layer_id}
className="layer-selector-input-label"
>
<input
key={'baselayer-input-' + layer.layer_id}
type="radio"
id={String(layer.layer_id)}
value={layer.layer_id}
name="baselayer"
checked={layer.layer_id === activeBaselayerId}
onChange={() =>
onBaselayerChange(String(layer.layer_id), 'layerMenu')
}
/>
{markMatchingSearchText(layer.name, matchedIds.has(layer.layer_id))}
</label>
));
} else if ('map_id' in node) {
children = (node as MapResponse).bands.map((band) => (
<CollapsibleSection
key={band.band_id}
node={band}
nestedDepth={1}
onBaselayerChange={onBaselayerChange}
activeBaselayerId={activeBaselayerId}
searchText={searchText}
expandedState={expandedState}
markMatchingSearchText={markMatchingSearchText}
matchedIds={matchedIds}
highlightMatch={matchedIds.has(band.band_id)}
handleToggle={handleToggle}
/>
));
} else {
children = (node as MapGroupResponse).maps.map((map) => (
<CollapsibleSection
key={map.map_id}
node={map}
nestedDepth={1}
onBaselayerChange={onBaselayerChange}
activeBaselayerId={activeBaselayerId}
searchText={searchText}
expandedState={expandedState}
markMatchingSearchText={markMatchingSearchText}
matchedIds={matchedIds}
highlightMatch={matchedIds.has(map.map_id)}
handleToggle={handleToggle}
/>
));
}
}

return (
<details style={{ marginLeft: nestedDepth * 5 }} open={defaultOpen}>
<summary title={tooltip}>{summary}</summary>
<div style={{ marginLeft: nestedDepth * 10 }}>
<div
title={node.description}
onClick={() => handleToggle(nodeId)}
className="layer-title-container"
>
{expandedState.has(nodeId) ? <ChevronDownIcon /> : <ChevronRightIcon />}
{markMatchingSearchText(node.name, highlightMatch)}
</div>
{children}
</details>
</div>
);
}

export default memo(CollapsibleSection);
Loading