diff --git a/apps/src/components/fields/skill.tsx b/apps/src/components/fields/skill.tsx index 99eee296b..5d423a198 100644 --- a/apps/src/components/fields/skill.tsx +++ b/apps/src/components/fields/skill.tsx @@ -8,16 +8,27 @@ import { __ } from '@/context/locale-provider'; import TooltipWidget from '@/components/ui/tooltip-widget'; import React from 'react'; +import { useClimateVariable } from '@/hooks/use-climate-variable'; +import { ForecastDisplays } from '@/types/climate-variable-interface'; + export interface S2DForecastDisplaySkillFieldCheckboxProps { tooltip?: React.ReactNode; } +/** + * @see {@link selectLowSkillVisibility} — "skill" is an S2D-specific concept + */ export const MaskLowSkillField = ( props: S2DForecastDisplaySkillFieldCheckboxProps, ) => { + const { climateVariable } = useClimateVariable(); + const dispatch = useAppDispatch(); const checked = useAppSelector(selectLowSkillVisibility()); + const forecastDisplay = climateVariable?.getForecastDisplay(); + const isForecast = forecastDisplay === ForecastDisplays.FORECAST; + const onCheckedChange = (checked: boolean) => { dispatch(setLowSkillVisibility({visible: checked})); }; @@ -29,6 +40,7 @@ export const MaskLowSkillField = ( const fieldProps = { checked, onCheckedChange, + disabled: !isForecast, ...propsRest, }; @@ -36,12 +48,12 @@ export const MaskLowSkillField = (
diff --git a/apps/src/components/map-layers/low-skill-layer.tsx b/apps/src/components/map-layers/low-skill-layer.tsx index 5fe60ec87..37a0798ca 100644 --- a/apps/src/components/map-layers/low-skill-layer.tsx +++ b/apps/src/components/map-layers/low-skill-layer.tsx @@ -6,6 +6,7 @@ import { useS2D } from '@/hooks/use-s2d'; import { useAppSelector } from '@/app/hooks'; import { selectLowSkillVisibility } from '@/features/map/map-slice'; import { buildSkillLayerName, buildSkillLayerTime } from '@/lib/s2d'; +import { ForecastDisplays } from '@/types/climate-variable-interface'; import L from 'leaflet'; interface LowSkillLayerProps { @@ -14,13 +15,17 @@ interface LowSkillLayerProps { /** * Leaflet layer for the "low skill". + * + * @see {@link selectLowSkillVisibility} — "skill" is an S2D-specific concept */ const LowSkillLayer = ({ pane, }: LowSkillLayerProps): React.ReactElement | null => { const { climateVariable } = useClimateVariable(); + const forecastDisplay = climateVariable?.getForecastDisplay(); + const isForecast = forecastDisplay === ForecastDisplays.FORECAST; const { releaseDate } = useS2D(); - const isLowSkillMasked = !useAppSelector(selectLowSkillVisibility()); + const isLowSkillVisible = useAppSelector(selectLowSkillVisibility()); const { opacity: { mapData }, } = useAppSelector((state) => state.map); @@ -33,6 +38,9 @@ const LowSkillLayer = ({ timeValue = buildSkillLayerTime(climateVariable, releaseDate); } + const hasLayerData = layerName && timeValue; + const shouldHideLayer = !isForecast || !isLowSkillVisible; + // Update the opacity on the *existing* layer if it exists. We do it like // that because we don't want a change in opacity to recreate the layer. // Instead, we want to update the existing layer (else it creates flashing @@ -49,7 +57,7 @@ const LowSkillLayer = ({ // attributes change. return useMemo( () => { - if (isLowSkillMasked || !layerName || !timeValue) { + if (shouldHideLayer || !hasLayerData) { return null; } @@ -78,7 +86,12 @@ const LowSkillLayer = ({ // above takes care of that). But we still use it as an initial value. // // eslint-disable-next-line react-hooks/exhaustive-deps - [pane, isLowSkillMasked, layerName, timeValue] + [ + layerName, + pane, + shouldHideLayer, + timeValue, + ] ); }; diff --git a/apps/src/features/map/map-slice.ts b/apps/src/features/map/map-slice.ts index 81ee9c5ad..ecdd63368 100644 --- a/apps/src/features/map/map-slice.ts +++ b/apps/src/features/map/map-slice.ts @@ -201,7 +201,9 @@ export const { } = mapSlice.actions; /** - * Selector that returns the visibility of the "low skill" layer. + * Selector that returns the visibility of the low-skill vector mask overlay. + * + * @see {@link MapState.isLowSkillVisible} */ export const selectLowSkillVisibility = () => (state: RootState) => diff --git a/apps/src/lib/s2d.ts b/apps/src/lib/s2d.ts index ed9fc1be0..9c7ecbd20 100644 --- a/apps/src/lib/s2d.ts +++ b/apps/src/lib/s2d.ts @@ -152,6 +152,10 @@ export function formatPeriodRange(periodRange: PeriodRange): [string, string] { /** * Create and return the GeoServer layer name for the Skill layer. * + * "Skill" in meteorology refers to forecast accuracy relative to a baseline + * (e.g., climatology). The skill layer is a vector mask overlay showing + * diagonal lines over regions where forecast confidence is low. + * * The name is `CDC:s2d-skill---` * * Where: diff --git a/apps/src/types/types.ts b/apps/src/types/types.ts index ea3447eb1..9f69dc390 100644 --- a/apps/src/types/types.ts +++ b/apps/src/types/types.ts @@ -216,8 +216,14 @@ export interface MapState { */ messageDisplayStates: {[key: string]: boolean}; /** - * True if the "low skill" layer is visible (i.e. true if "mask low skill" - * is checked) + * Whether low-skill areas are masked on the map. + * + * "Mask" is the verb — to visually cover low-confidence forecast areas + * with diagonal lines. When `true`, those areas are masked. + * + * Only meaningful for S2D variables when Forecast Display = "Forecast". + * + * Controlled by `MaskLowSkillField` in `components/fields/skill.tsx`. */ isLowSkillVisible: boolean; /**