Skip to content
Closed
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
154 changes: 87 additions & 67 deletions src/components/BaselayerSections.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,101 @@
import { LayerSelectorProps } from './LayerSelector';
import { useState, useEffect, ReactNode, useRef, useCallback } from 'react';
import { LayerSelectorProps, NoMatches } from './LayerSelector';
import { CollapsibleSection } from './CollapsibleSection';
import { EXTERNAL_BASELAYERS } from '../configs/mapSettings';
import {
EXTERNAL_BASELAYERS,
EXTERNAL_DETAILS_ID,
} from '../configs/mapSettings';
import { getDefaultExpandedState, filterMapGroups } from '../utils/filterUtils';

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({
mapGroups,
activeBaselayerId,
isFlipped,
onBaselayerChange,
searchText,
markMatchingSearchText,
}: BaselayerSectionsProps) {
const [expandedState, setExpandedState] = useState<Set<string>>(
getDefaultExpandedState(mapGroups, activeBaselayerId)
);
const externalDetailsRef = useRef<HTMLDetailsElement>(null);

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]
);

useEffect(() => {
if (!externalDetailsRef.current) return;
if (searchText.length > 0) {
externalDetailsRef.current.open = true;
} else {
externalDetailsRef.current.open = expandedState.has(EXTERNAL_DETAILS_ID);
}
}, [searchText, externalDetailsRef, 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) => (
{filteredExternalLayers.length > 0 && (
<details ref={externalDetailsRef} onToggle={(e) => e.preventDefault()}>
<summary
title="External maps used for comparison"
onClick={() => handleToggle(EXTERNAL_DETAILS_ID)}
>
Comparison maps
</summary>
{filteredExternalLayers.map((bl) => (
<div
className={`input-container ${bl.disabledState(isFlipped) ? 'disabled' : ''}`}
key={bl.layer_id}
Expand All @@ -99,11 +114,16 @@ export function BaselayerSections({
onChange={() => onBaselayerChange(bl.layer_id, 'layerMenu')}
disabled={bl.disabledState(isFlipped)}
/>
<label htmlFor={bl.layer_id}>{bl.name}</label>
<label
htmlFor={bl.layer_id}
className="layer-selector-input-label"
>
{markMatchingSearchText(bl.name)}
</label>
</div>
))}
</CollapsibleSection>
}
</details>
)}
</>
);
}
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
119 changes: 106 additions & 13 deletions src/components/CollapsibleSection.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,116 @@
import { ReactNode } from 'react';
import { ReactNode, useRef, useEffect } from 'react';
import { LayerSelectorProps } from './LayerSelector';
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,
node,
nestedDepth,
onBaselayerChange,
activeBaselayerId,
searchText,
expandedState,
markMatchingSearchText,
matchedIds,
highlightMatch,
handleToggle,
}: Props) {
let children;
const detailsRef = useRef<HTMLDetailsElement>(null);

useEffect(() => {
if (!detailsRef.current) return;
if (searchText.length > 0) {
detailsRef.current.open = true;
} else {
detailsRef.current.open = expandedState.has(getNodeId(node));
}
}, [searchText, detailsRef, expandedState, node]);

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={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={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>
<details
ref={detailsRef}
style={{ marginLeft: nestedDepth * 5 }}
onToggle={(e) => e.preventDefault()}
>
<summary title={node.description} onClick={() => getNodeId(node)}>
{markMatchingSearchText(node.name, highlightMatch)}
</summary>
{children}
</details>
);
Expand Down
10 changes: 5 additions & 5 deletions src/components/CustomColorMapDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function CustomColorMapDialog({
handleUpdate();
}}
>
<label>
<label className="dialog-label">
<span>
Specify a{' '}
<a
Expand All @@ -146,7 +146,7 @@ export function CustomColorMapDialog({
onChange={(e) => setTempCmap(e.target.value)}
/>
</label>
<label>
<label className="dialog-label">
Minimum of {units}
<input
type="number"
Expand All @@ -159,7 +159,7 @@ export function CustomColorMapDialog({
}
/>
</label>
<label>
<label className="dialog-label">
Maximum of {units}
<input
type="number"
Expand All @@ -173,15 +173,15 @@ export function CustomColorMapDialog({
/>
</label>
<div className="cmap-toggles">
<label className="cmap-dialog-toggle">
<label className="dialog-label cmap-dialog-toggle">
<input
type="checkbox"
checked={tempIsLogScale}
onChange={(e) => setTempIsLogScale(e.target.checked)}
/>
Log Scale
</label>
<label className="cmap-dialog-toggle">
<label className="dialog-label cmap-dialog-toggle">
<input
type="checkbox"
checked={tempIsAbsValue}
Expand Down
Loading