From cbf016b879237d80d5ce00db515f454969760e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Thu, 14 Sep 2023 13:48:21 +0200 Subject: [PATCH] WIP --- .../features/selected-item/component.tsx | 27 +- app/components/map/legend/group/index.tsx | 7 +- app/components/map/legend/index.tsx | 33 +- app/components/map/legend/item/index.tsx | 5 +- app/components/map/legend/types.d.ts | 1 + app/hooks/features/index.ts | 11 +- app/hooks/map/constants.tsx | 71 ++-- app/hooks/map/index.ts | 36 +- app/hooks/map/types.ts | 5 +- app/hooks/wdpa/index.ts | 2 +- .../inventory-panel/features/index.tsx | 21 +- .../features/add/list/component.tsx | 25 +- .../grid-setup/planning-unit-status/index.tsx | 48 ++- .../protected-areas/categories/index.tsx | 40 +- .../scenario/solutions/gap-analysis/index.tsx | 28 +- .../scenario/solutions/overview/index.tsx | 26 +- app/layout/projects/show/map/index.tsx | 14 +- .../projects/show/map/legend/hooks/index.ts | 69 ++- app/layout/scenarios/edit/map/component.tsx | 396 +++++++++++------- .../scenarios/edit/map/legend/hooks/index.ts | 349 +++++++++++++++ app/layout/scenarios/new/map/component.tsx | 11 +- app/package.json | 1 + app/yarn.lock | 8 + 23 files changed, 910 insertions(+), 324 deletions(-) create mode 100644 app/components/map/legend/types.d.ts create mode 100644 app/layout/scenarios/edit/map/legend/hooks/index.ts diff --git a/app/components/features/selected-item/component.tsx b/app/components/features/selected-item/component.tsx index c9327727ef..13d0921e36 100644 --- a/app/components/features/selected-item/component.tsx +++ b/app/components/features/selected-item/component.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState, ReactNode } from 'react'; -import cx from 'classnames'; +import { HiEye, HiEyeOff } from 'react-icons/hi'; import { useFeatureFlags } from 'hooks/feature-flags'; @@ -10,16 +10,17 @@ import Select from 'components/forms/select'; import Icon from 'components/icon'; import InfoButton from 'components/info-button'; import Tooltip from 'components/tooltip'; +import { cn } from 'utils/cn'; import STRAT_1_IMG from 'images/info-buttons/img_strat_1.png'; import STRAT_2_IMG from 'images/info-buttons/img_strat_2.png'; import STRAT_3_IMG from 'images/info-buttons/img_strat_3.png'; -import HIDE_SVG from 'svgs/ui/hide.svg?sprite'; +// import HIDE_SVG from 'svgs/ui/hide.svg?sprite'; import INTERSECT_SVG from 'svgs/ui/intersect.svg?sprite'; import PLUS_SVG from 'svgs/ui/plus.svg?sprite'; import REMOVE_SVG from 'svgs/ui/remove.svg?sprite'; -import SHOW_SVG from 'svgs/ui/show.svg?sprite'; +// import SHOW_SVG from 'svgs/ui/show.svg?sprite'; import SPLIT_SVG from 'svgs/ui/split.svg?sprite'; export interface ItemProps { @@ -128,17 +129,12 @@ export const Item: React.FC = ({ // RENDER return (
-
+

{name}

@@ -153,7 +149,7 @@ export const Item: React.FC = ({ aria-label="manage-see-on-map" type="button" onClick={() => setSplitOpen(!splitOpen)} - className={cx({ + className={cn({ 'flex h-5 w-5 items-center justify-center ': true, 'text-white': !splitSelected, 'text-yellow-400': !!splitSelected, @@ -177,12 +173,17 @@ export const Item: React.FC = ({ aria-label="manage-see-on-map" type="button" onClick={onSeeOnMap} - className={cx({ + className={cn({ 'flex h-5 w-5 items-center justify-center text-white': true, 'text-gray-300': !isShown, })} > - + {isShown ? ( + + ) : ( + + )} + {/* */} diff --git a/app/components/map/legend/group/index.tsx b/app/components/map/legend/group/index.tsx index 3bf3ad38df..92dc73759e 100644 --- a/app/components/map/legend/group/index.tsx +++ b/app/components/map/legend/group/index.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, PropsWithChildren, useState } from 'react'; +import { ComponentProps, PropsWithChildren, useEffect, useState } from 'react'; import { HiChevronDown, HiChevronUp } from 'react-icons/hi'; @@ -22,6 +22,10 @@ const LegendGroup = ({ >): JSX.Element => { const [isOpen, setOpen] = useState(defaultOpen && !disabled); + useEffect(() => { + if (!disabled && defaultOpen) setOpen(true); + }, [disabled, defaultOpen]); + return ( diff --git a/app/components/map/legend/index.tsx b/app/components/map/legend/index.tsx index 5c9773e813..a9b56fdf90 100644 --- a/app/components/map/legend/index.tsx +++ b/app/components/map/legend/index.tsx @@ -1,15 +1,19 @@ -import React, { useMemo, Children, isValidElement } from 'react'; +import React, { useMemo, Children, isValidElement, PropsWithChildren } from 'react'; import { ScrollArea } from 'components/scroll-area'; import { cn } from 'utils/cn'; import SortableList from './sortable/list'; -export interface LegendProps { +export const Legend = ({ + open, + children, + className = '', + sortable, + onChangeOrder, +}: PropsWithChildren<{ open: boolean; className?: string; - children: React.ReactNode; - maxHeight: string | number; sortable?: { enabled: boolean; handle: boolean; @@ -17,16 +21,7 @@ export interface LegendProps { }; onChangeOrder?: (id: string[]) => void; onChangeOpen?: (open: boolean) => void; -} - -export const Legend: React.FC = ({ - open, - children, - className = '', - maxHeight, - sortable, - onChangeOrder, -}: LegendProps) => { +}>): JSX.Element => { const isChildren = useMemo(() => { return !!Children.count(Children.toArray(children).filter((c) => isValidElement(c))); }, [children]); @@ -34,17 +29,13 @@ export const Legend: React.FC = ({ return (
- {open && isChildren && ( - + {open && ( +
{!!sortable && ( diff --git a/app/components/map/legend/item/index.tsx b/app/components/map/legend/item/index.tsx index 42f2db4bbf..3fff85e4b6 100644 --- a/app/components/map/legend/item/index.tsx +++ b/app/components/map/legend/item/index.tsx @@ -10,8 +10,6 @@ import { cn } from 'utils/cn'; import OPACITY_SVG from 'svgs/map/opacity.svg?sprite'; import DRAG_SVG from 'svgs/ui/drag.svg?sprite'; -import HIDE_SVG from 'svgs/ui/hide.svg?sprite'; -import SHOW_SVG from 'svgs/ui/show.svg?sprite'; export interface LegendItemProps { id: string; @@ -64,8 +62,7 @@ export const LegendItem: React.FC = ({ }); return chldn && chldn.some((c) => !!c); }, [children]); - - const { opacity = 1, visibility = true } = settings || {}; + const { opacity = 1, visibility = false } = settings || {}; const { format } = useNumberFormatter({ style: 'percent', diff --git a/app/components/map/legend/types.d.ts b/app/components/map/legend/types.d.ts new file mode 100644 index 0000000000..9e031547fe --- /dev/null +++ b/app/components/map/legend/types.d.ts @@ -0,0 +1 @@ +export type LegendItemType = 'matrix' | 'basic' | 'choropleth' | 'gradient'; diff --git a/app/hooks/features/index.ts b/app/hooks/features/index.ts index b42b30017e..a91829a9b5 100644 --- a/app/hooks/features/index.ts +++ b/app/hooks/features/index.ts @@ -9,12 +9,15 @@ import { } from 'react-query'; import { AxiosRequestConfig } from 'axios'; +import chroma from 'chroma-js'; import Fuse from 'fuse.js'; import flatten from 'lodash/flatten'; import orderBy from 'lodash/orderBy'; import partition from 'lodash/partition'; import { useSession } from 'next-auth/react'; +import { COLORS } from 'hooks/map/constants'; + import { ItemProps as IntersectItemProps } from 'components/features/intersect-item/component'; import { ItemProps as RawItemProps } from 'components/features/raw-item/component'; import { Feature } from 'types/api/feature'; @@ -227,7 +230,7 @@ export function useSelectedFeatures( select: ({ data }) => { const { features = [] } = data; - let parsedData = features.map((d) => { + let parsedData = features.map((d, index) => { const { featureId, geoprocessingOperations, metadata } = d; const { @@ -293,12 +296,18 @@ export function useSelectedFeatures( ); } + const color = + features.length > COLORS['features-preview'].ramp.length + ? chroma.scale(COLORS['features-preview'].ramp).colors(features.length)[index] + : COLORS['features-preview'].ramp[index]; + return { ...d, id: featureId, name: alias || featureClassName, type: tag, description, + color, // SPLIT splitOptions, diff --git a/app/hooks/map/constants.tsx b/app/hooks/map/constants.tsx index 634d6da722..b1b2d2e050 100644 --- a/app/hooks/map/constants.tsx +++ b/app/hooks/map/constants.tsx @@ -4,6 +4,9 @@ import chroma from 'chroma-js'; import { FaSquare } from 'react-icons/fa'; import Icon from 'components/icon'; +import { LegendItemType } from 'components/map/legend/types'; +import { Feature } from 'types/api/feature'; +import { WDPA } from 'types/api/wdpa'; import HEXAGON_SVG from 'svgs/map/hexagon.svg?sprite'; import SQUARE_SVG from 'svgs/map/square.svg?sprite'; @@ -209,13 +212,16 @@ export const LEGEND_LAYERS = { visibility: true, }, }), - 'designated-areas': (options: { items: { name: string }[] }) => { - const { items } = options; + 'designated-areas': (options: { + items: { id: WDPA['id']; name: string }[]; + onChangeVisibility: (WDPAId: WDPA['id']) => void; + }) => { + const { items = [], onChangeVisibility } = options; - return items.map(({ name }) => ({ - id: `designated-areas-${name}`, + return items.map(({ id, name }) => ({ + id, name, - type: 'basic', + type: 'basic' as LegendItemType, icon: ( { + onChangeVisibility(id); + }, })); }, - 'features-preview-new': (options: { items: { id: string; name: string }[] }) => { - const { items } = options; - - return items.map(({ name, id }, index) => { - const COLOR = - items.length > COLORS['features-preview'].ramp.length - ? chroma.scale(COLORS['features-preview'].ramp).colors(items.length)[index] - : COLORS['features-preview'].ramp[index]; + 'features-preview-new': (options: { + items: { id: string; name: string; color: string }[]; + onChangeVisibility: (featureId: Feature['id']) => void; + }) => { + const { items, onChangeVisibility } = options; + return items.map(({ name, id, color }) => { return { id, name, - type: 'basic', - icon: ( - - ), + type: 'basic' as LegendItemType, + icon: , settingsManager: { opacity: true, visibility: true, }, items: [], + onChangeVisibility: () => { + onChangeVisibility?.(id); + }, }; }); }, @@ -261,7 +269,7 @@ export const LEGEND_LAYERS = { return { id: 'features-preview', name: 'Features preview', - type: 'basic', + type: 'basic' as LegendItemType, settingsManager: { opacity: true, visibility: true, @@ -317,7 +325,7 @@ export const LEGEND_LAYERS = { return items.map(({ name, min, max }, index) => ({ id: `features-abundance-${name}`, name, - type: 'gradient', + type: 'gradient' as LegendItemType, settingsManager: { opacity: true, visibility: true, @@ -361,7 +369,7 @@ export const LEGEND_LAYERS = { return items.map(({ name, min, max }) => ({ id: `cost-surface-${name}`, name, - type: 'gradient', + type: 'gradient' as LegendItemType, settingsManager: { opacity: true, visibility: true, @@ -409,7 +417,7 @@ export const LEGEND_LAYERS = { return { id: 'cost', name: options.cost.name, - type: 'gradient', + type: 'gradient' as LegendItemType, settingsManager: { opacity: true, visibility: true, @@ -427,7 +435,7 @@ export const LEGEND_LAYERS = { }; }, 'lock-available': (options) => { - const { puAvailableValue } = options; + const { puAvailableValue, onChangeVisibility } = options; return { id: 'lock-available', @@ -444,10 +452,11 @@ export const LEGEND_LAYERS = { visibility: true, }, description:
{puAvailableValue.length} PU
, + onChangeVisibility, }; }, 'lock-in': (options) => { - const { puIncludedValue } = options; + const { puIncludedValue, onChangeVisibility } = options; return { id: 'lock-in', @@ -464,10 +473,11 @@ export const LEGEND_LAYERS = { visibility: true, }, description:
{puIncludedValue.length} PU
, + onChangeVisibility, }; }, 'lock-out': (options) => { - const { puExcludedValue } = options; + const { puExcludedValue, onChangeVisibility } = options; return { id: 'lock-out', @@ -484,17 +494,18 @@ export const LEGEND_LAYERS = { visibility: true, }, description:
{puExcludedValue.length} PU
, + onChangeVisibility, }; }, // SOLUTIONS frequency: (options) => { - const { numberOfRuns } = options; + const { numberOfRuns, onChangeVisibility } = options; return { id: 'frequency', name: numberOfRuns ? `Frequency (${numberOfRuns} runs)` : 'Frequency', - type: 'gradient', + type: 'gradient' as LegendItemType, settingsManager: { opacity: true, visibility: true, @@ -517,11 +528,12 @@ export const LEGEND_LAYERS = { value: '100', }, ], + onChangeVisibility, }; }, - solution: () => ({ + solution: (options?: { onChangeVisibility?: () => void }) => ({ id: 'solution', - name: 'Solution selected', + name: 'Best solution', icon: ( { const { scenario1, scenario2 } = options; @@ -559,7 +572,7 @@ export const LEGEND_LAYERS = { return { id: 'compare', name: 'Solutions distribution', - type: 'matrix', + type: 'matrix' as LegendItemType, settingsManager: { opacity: true, visibility: true, diff --git a/app/hooks/map/index.ts b/app/hooks/map/index.ts index 8c2133e6d2..410818f24a 100644 --- a/app/hooks/map/index.ts +++ b/app/hooks/map/index.ts @@ -176,18 +176,20 @@ export function useWDPAPreviewLayer({ pid, active, bbox, - wdpaIucnCategories, + WDPACategories = [], cache = 0, options, }: UseWDPAPreviewLayer) { - const { opacity = 1, visibility = true } = options || {}; + const { layerSettings } = options || {}; + return useMemo(() => { if (!active || !bbox) return null; + const visibleCategories = WDPACategories.filter((id) => layerSettings[id]?.visibility); + return { id: `wdpa-preview-layer-${cache}`, type: 'vector', - opacity, source: { type: 'vector', tiles: [ @@ -200,14 +202,14 @@ export function useWDPAPreviewLayer({ type: 'fill', 'source-layer': 'layer0', layout: { - visibility: visibility ? 'visible' : 'none', + visibility: 'visible', }, // wdpaIucnCategories are filtered in two steps as they are custom and WDPA. // We have not way to separate them into two arrays but it would be ideal filter: [ 'any', - ['all', ['in', ['get', 'iucn_cat'], ['literal', wdpaIucnCategories]]], - ['all', ['in', ['get', 'id'], ['literal', wdpaIucnCategories]]], + ['all', ['in', ['get', 'iucn_cat'], ['literal', visibleCategories]]], + ['all', ['in', ['get', 'id'], ['literal', visibleCategories]]], ], paint: { 'fill-color': COLORS['wdpa-preview'], @@ -217,12 +219,12 @@ export function useWDPAPreviewLayer({ type: 'line', 'source-layer': 'layer0', layout: { - visibility: visibility ? 'visible' : 'none', + visibility: 'visible', }, filter: [ 'any', - ['all', ['in', ['get', 'iucn_cat'], ['literal', wdpaIucnCategories]]], - ['all', ['in', ['get', 'id'], ['literal', wdpaIucnCategories]]], + ['all', ['in', ['get', 'iucn_cat'], ['literal', visibleCategories]]], + ['all', ['in', ['get', 'id'], ['literal', visibleCategories]]], ], paint: { 'line-color': '#000', @@ -231,7 +233,7 @@ export function useWDPAPreviewLayer({ ], }, }; - }, [pid, active, bbox, wdpaIucnCategories, cache, opacity, visibility]); + }, [pid, active, bbox, WDPACategories, cache, layerSettings]); } // Feature preview layer @@ -301,18 +303,10 @@ export function useFeaturePreviewLayers({ return 'visible'; }; - return FEATURES.map((f, index) => { + return FEATURES.map((f) => { const { id } = f; - const F = featuresRecipe.find((fr) => fr.id === id) || f; - const settings = layerSettings[id] || {}; - - const COLOR = - selectedFeatures.length > COLORS['features-preview'].ramp.length - ? chroma.scale(COLORS['features-preview'].ramp).colors(selectedFeatures.length)[ - selectedFeatures.length - 1 - index - ] - : COLORS['features-preview'].ramp[selectedFeatures.length - 1 - index]; + const settings = layerSettings[id] || { visibility: true, opacity: 1, color: '#000' }; return { id: `feature-${id}-preview-layer-${cache}`, @@ -342,7 +336,7 @@ export function useFeaturePreviewLayers({ visibility: getLayerVisibility(settings?.visibility), }, paint: { - 'fill-color': COLOR, + 'fill-color': settings?.color, 'fill-opacity': settings?.opacity, }, }, diff --git a/app/hooks/map/types.ts b/app/hooks/map/types.ts index 6df168e01a..0aa58515d9 100644 --- a/app/hooks/map/types.ts +++ b/app/hooks/map/types.ts @@ -3,6 +3,7 @@ import { PUAction } from 'store/slices/scenarios/types'; import { TargetSPFItemProps } from 'components/features/target-spf-item/types'; import { Feature } from 'types/api/feature'; import { Scenario } from 'types/api/scenario'; +import { WDPA } from 'types/api/wdpa'; export interface UseGeoJSONLayer { cache?: number; @@ -64,7 +65,7 @@ export interface UseWDPAPreviewLayer { cache?: number; active?: boolean; bbox?: number[] | unknown; - wdpaIucnCategories?: string[]; + WDPACategories?: WDPA['id'][]; options?: Record; } @@ -89,7 +90,7 @@ export interface UseFeaturePreviewLayers { featuresRecipe?: Record[]; featureHoverId?: string; selectedFeatures?: Array; - layerSettings?: Record; + layerSettings?: Record; }; } export interface UseTargetedPreviewLayers { diff --git a/app/hooks/wdpa/index.ts b/app/hooks/wdpa/index.ts index 62ab528d1e..485f69243f 100644 --- a/app/hooks/wdpa/index.ts +++ b/app/hooks/wdpa/index.ts @@ -23,7 +23,7 @@ export function useWDPACategories({ const { data: session } = useSession(); return useQuery( - ['protected-areas', adminAreaId, customAreaId], + ['protected-areas', adminAreaId, customAreaId, scenarioId], async () => API.request({ method: 'GET', diff --git a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx index 615417b3f6..4be2fc53db 100644 --- a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx @@ -99,22 +99,23 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): const toggleSeeOnMap = useCallback( (featureId: Feature['id']) => { const newSelectedFeatures = [...visibleFeatures]; - if (!newSelectedFeatures.includes(featureId)) { + const isIncluded = newSelectedFeatures.includes(featureId); + if (!isIncluded) { newSelectedFeatures.push(featureId); - - dispatch( - setLayerSettings({ - id: featureId, - settings: { - visibility: true, - }, - }) - ); } else { const i = newSelectedFeatures.indexOf(featureId); newSelectedFeatures.splice(i, 1); } dispatch(setVisibleFeatures(newSelectedFeatures)); + + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + }, + }) + ); }, [dispatch, visibleFeatures] ); diff --git a/app/layout/project/sidebar/scenario/grid-setup/features/add/list/component.tsx b/app/layout/project/sidebar/scenario/grid-setup/features/add/list/component.tsx index 1d793145c8..1301dfb745 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/features/add/list/component.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/features/add/list/component.tsx @@ -18,6 +18,7 @@ import Item from 'components/features/selected-item'; import Loading from 'components/loading'; import Modal from 'components/modal'; import { ScrollArea } from 'components/scroll-area'; +import { Feature } from 'types/api/feature'; import { ScenarioSidebarTabs, ScenarioSidebarSubTabs } from 'utils/tabs'; import { mergeScenarioStatusMetaData } from 'utils/utils-scenarios'; @@ -30,11 +31,11 @@ export const ScenariosFeaturesList = ({ onContinue }): JSX.Element => { const [intersecting, setIntersecting] = useState(null); const [deleteFeature, setDeleteFeature] = useState(null); - const { push, query } = useRouter(); + const { query } = useRouter(); const { pid, sid } = query as { pid: string; sid: string }; const scenarioSlice = getScenarioEditSlice(sid); - const { setFeatures, setSubTab, setSelectedFeatures } = scenarioSlice.actions; + const { setFeatures, setLayerSettings, setSelectedFeatures } = scenarioSlice.actions; const { selectedFeatures } = useSelector((state) => state[`/scenarios/${sid}/edit`]); const dispatch = useDispatch(); @@ -239,17 +240,31 @@ export const ScenariosFeaturesList = ({ onContinue }): JSX.Element => { }, [onContinue]); const toggleSeeOnMap = useCallback( - (id) => { + (id: Feature['id']) => { const newSelectedFeatures = [...selectedFeatures]; - if (!newSelectedFeatures.includes(id)) { + const isIncluded = newSelectedFeatures.includes(id); + if (!isIncluded) { newSelectedFeatures.push(id); } else { const i = newSelectedFeatures.indexOf(id); newSelectedFeatures.splice(i, 1); } dispatch(setSelectedFeatures(newSelectedFeatures)); + + const selectedFeature = selectedFeaturesData.find(({ featureId }) => featureId === id); + const { color } = selectedFeature || {}; + + dispatch( + setLayerSettings({ + id, + settings: { + visibility: !isIncluded, + color, + }, + }) + ); }, - [dispatch, setSelectedFeatures, selectedFeatures] + [dispatch, setSelectedFeatures, setLayerSettings, selectedFeatures, selectedFeaturesData] ); const isShown = useCallback( diff --git a/app/layout/project/sidebar/scenario/grid-setup/planning-unit-status/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/planning-unit-status/index.tsx index 71e7a2861b..5aa48e28fb 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/planning-unit-status/index.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/planning-unit-status/index.tsx @@ -1,9 +1,8 @@ -import React, { useCallback } from 'react'; - -import { useDispatch, useSelector } from 'react-redux'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; +import { useAppDispatch, useAppSelector } from 'store/hooks'; import { getScenarioEditSlice } from 'store/slices/scenarios/edit'; import { PUAction } from 'store/slices/scenarios/types'; @@ -21,30 +20,20 @@ import PlanningUnitMethods from './actions'; import Tabs from './tabs'; import type { PlanningUnitTabsProps } from './tabs'; -export interface ScenariosSidebarAnalysisSectionsProps { - onChangeSection: (s: string) => void; -} - export const GridSetupPlanningUnits = (): JSX.Element => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const { query } = useRouter(); const { pid, sid } = query as { pid: string; sid: string }; - const scenarioSlice = getScenarioEditSlice(sid); + const scenarioSlice = useMemo(() => getScenarioEditSlice(sid), [sid]); + const { setLayerSettings } = scenarioSlice.actions; - const { setPUAction, setPuIncludedValue, setPuExcludedValue, setPuAvailableValue } = - scenarioSlice.actions; + const { setPUAction } = scenarioSlice.actions; - const { puAction } = useSelector((state) => state[`/scenarios/${sid}/edit`]); + const { puAction } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); const editable = useCanEditScenario(pid, sid); - const { data: PUData } = useScenarioPU(sid, { - onSuccess: ({ included, excluded, available }) => { - dispatch(setPuIncludedValue(included)); - dispatch(setPuExcludedValue(excluded)); - dispatch(setPuAvailableValue(available)); - }, - }); + const { data: PUData } = useScenarioPU(sid); const onChangeTab = useCallback( (t: Parameters[0]) => { @@ -53,6 +42,27 @@ export const GridSetupPlanningUnits = (): JSX.Element => { [dispatch, setPUAction] ); + useEffect(() => { + dispatch( + setLayerSettings({ + id: 'lock-available', + settings: { visibility: true }, + }) + ); + dispatch( + setLayerSettings({ + id: 'lock-in', + settings: { visibility: true }, + }) + ); + dispatch( + setLayerSettings({ + id: 'lock-out', + settings: { visibility: true }, + }) + ); + }, [dispatch, setLayerSettings]); + return ( { const [submitting, setSubmitting] = useState(false); const { addToast } = useToasts(); + const formRef = useRef['form']>(null); const { query } = useRouter(); const { pid, sid } = query as { pid: string; sid: string }; - const scenarioSlice = getScenarioEditSlice(sid); - const { setWDPACategories, setWDPAThreshold } = scenarioSlice.actions; + const scenarioSlice = useMemo(() => getScenarioEditSlice(sid), [sid]); + const { setWDPACategories, setWDPAThreshold, setLayerSettings } = scenarioSlice.actions; const dispatch = useAppDispatch(); const editable = useCanEditScenario(pid, sid); const { data: projectData } = useProject(pid); + const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); const { data: scenarioData, @@ -161,14 +164,18 @@ export const WDPACategories = ({ onContinue }): JSX.Element => { return PROJECT_PA_OPTIONS.concat(WDPA_OPTIONS); }, [wdpaData, WDPA_OPTIONS, PROJECT_PA_OPTIONS]); + const selectedAreas = useMemo( + () => wdpaData?.filter((pa) => pa.selected || layerSettings[pa.id]?.visibility) || [], + [wdpaData] + ); + const INITIAL_VALUES = useMemo(() => { - const selectedAreas = wdpaData?.filter((pa) => pa.selected) || []; const areas = selectedAreas.map((i) => i.id) || []; return { wdpaIucnCategories: areas, }; - }, [wdpaData]); + }, [selectedAreas]); useEffect(() => { if (scenarioData?.wdpaThreshold) { @@ -180,6 +187,19 @@ export const WDPACategories = ({ onContinue }): JSX.Element => { dispatch(setWDPACategories(INITIAL_VALUES)); }, [dispatch, setWDPACategories, INITIAL_VALUES]); + const { wdpaIucnCategories = [] } = formRef.current?.getState().values || {}; + + useEffect(() => { + wdpaData?.forEach(({ id }) => { + dispatch( + setLayerSettings({ + id, + settings: { visibility: wdpaIucnCategories.includes(id) }, + }) + ); + }); + }, [wdpaData, wdpaIucnCategories, dispatch, setLayerSettings]); + if ((scenarioIsFetching && !scenarioIsFetched) || (wdpaIsFetching && !wdpaIsFetched)) { return ( { initialValues={INITIAL_VALUES} > {({ form, values, handleSubmit }) => { + formRef.current = form; dispatch(setWDPACategories(values)); const plainWDPAOptions = WDPA_OPTIONS.map((o) => o.value); @@ -269,7 +290,10 @@ export const WDPACategories = ({ onContinue }): JSX.Element => {
  • III: Natural Monument or Feature.
  • IV: Habitat/Species Management Area.
  • V: Protected Landscape/Seascape.
  • -
  • VI: Protected area with sustainable use of natural resources.
  • {/* eslint-disable-line*/} +
  • + VI: Protected area with sustainable use of natural + resources. +
  • diff --git a/app/layout/project/sidebar/scenario/solutions/gap-analysis/index.tsx b/app/layout/project/sidebar/scenario/solutions/gap-analysis/index.tsx index b47abd9dd5..c739402967 100644 --- a/app/layout/project/sidebar/scenario/solutions/gap-analysis/index.tsx +++ b/app/layout/project/sidebar/scenario/solutions/gap-analysis/index.tsx @@ -1,4 +1,9 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; + +import { useRouter } from 'next/router'; + +import { useAppDispatch } from 'store/hooks'; +import { getScenarioEditSlice } from 'store/slices/scenarios/edit'; import { motion } from 'framer-motion'; @@ -11,11 +16,32 @@ import Toolbar from './toolbar'; export const SolutionsTargetAchievements = (): JSX.Element => { const [search, setSearch] = useState(null); + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const dispatch = useAppDispatch(); + const scenarioSlice = useMemo(() => getScenarioEditSlice(sid), [sid]); + const { setLayerSettings } = scenarioSlice.actions; const onSearch = useCallback((s: typeof search) => { setSearch(s); }, []); + useEffect(() => { + dispatch( + setLayerSettings({ + id: 'solution', + settings: { visibility: true }, + }) + ); + dispatch( + setLayerSettings({ + id: 'frequency', + settings: { visibility: true }, + }) + ); + }, [dispatch, setLayerSettings]); + return (
    { const [showTable, setShowTable] = useState(false); const { addToast } = useToasts(); - const scenarioSlice = getScenarioEditSlice(sid); + const scenarioSlice = useMemo(() => getScenarioEditSlice(sid), [sid]); const { setLayerSettings } = scenarioSlice.actions; - const { selectedSolution, layerSettings } = useSelector( + const { selectedSolution, layerSettings } = useAppSelector( (state) => state[`/scenarios/${sid}/edit`] ); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const { data: projectData } = useProject(pid); @@ -135,6 +134,21 @@ export const SolutionsOverview = (): JSX.Element => { [dispatch, setLayerSettings, layerSettings] ); + useEffect(() => { + dispatch( + setLayerSettings({ + id: 'solution', + settings: { visibility: true }, + }) + ); + dispatch( + setLayerSettings({ + id: 'frequency', + settings: { visibility: true }, + }) + ); + }, [dispatch, setLayerSettings]); + return ( { }); const WDPAsPreviewLayers = useWDPAPreviewLayer({ - wdpaIucnCategories: selectedWDPAsIds, + WDPACategories: selectedWDPAsIds, pid: `${pid}`, active: true, bbox, @@ -402,14 +403,12 @@ export const ProjectMap = (): JSX.Element => { ); }, [pid, sid1, sid2, projectName, downloadScenarioComparisonReportMutation, addToast]); - const legendMaxHeight = typeof window === 'undefined' ? 0 : window.innerHeight * 0.65; - const renderLegendItems = ({ type, intersections, items, }: { - type: 'matrix' | 'basic' | 'choropleth' | 'gradient'; + type: LegendItemType; intersections?: ComponentProps['intersections']; items: | ComponentProps['items'] @@ -555,8 +554,7 @@ export const ProjectMap = (): JSX.Element => { setOpen(!open)} > {legendConfig.map((c) => { @@ -587,8 +585,8 @@ export const ProjectMap = (): JSX.Element => { {subgroup.layers?.map((layer) => ( onChangeOpacity(opacity, layer.id)} - onChangeVisibility={() => onChangeVisibility(layer.id)} + // onChangeOpacity={(opacity) => onChangeOpacity(opacity, layer.id)} + // onChangeVisibility={() => onChangeVisibility(layer.id)} settings={layerSettings[layer.id]} {...layer} > diff --git a/app/layout/projects/show/map/legend/hooks/index.ts b/app/layout/projects/show/map/legend/hooks/index.ts index 7e452b5a3f..e97ac3f360 100644 --- a/app/layout/projects/show/map/legend/hooks/index.ts +++ b/app/layout/projects/show/map/legend/hooks/index.ts @@ -1,12 +1,17 @@ import { useRouter } from 'next/router'; -import { useAppSelector } from 'store/hooks'; +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { setSelectedFeatures, setLayerSettings } from 'store/slices/projects/[id]'; + +import chroma from 'chroma-js'; import { useProjectCostSurfaces } from 'hooks/cost-surface'; -import { useProjectFeatures } from 'hooks/features'; -import { LEGEND_LAYERS } from 'hooks/map/constants'; +import { useAllFeatures, useProjectFeatures } from 'hooks/features'; +import { COLORS, LEGEND_LAYERS } from 'hooks/map/constants'; import { useProjectWDPAs } from 'hooks/wdpa'; +import { Feature } from 'types/api/feature'; + export const useCostSurfaceLegend = () => { const { selectedCostSurfaces } = useAppSelector((state) => state['/projects/[id]']); const { query } = useRouter(); @@ -51,7 +56,12 @@ export const useConservationAreasLegend = () => { // }); return LEGEND_LAYERS['designated-areas']({ - items: [{ name: 'WDPA 1' }, { name: 'WDPA 2' }, { name: 'WDPA 3' }], + items: [ + { name: 'WDPA 1', id: '1' }, + { name: 'WDPA 2', id: '2' }, + { name: 'WDPA 3', id: '3' }, + ], + onChangeVisibility: () => {}, }); }; @@ -101,14 +111,53 @@ export const useFeatureLegend = () => { const { query } = useRouter(); const { pid } = query as { pid: string }; - const projectFeaturesQuery = useProjectFeatures(pid, selectedFeatures); + const dispatch = useAppDispatch(); + const projectFeaturesQuery = useAllFeatures( + pid, + {}, + { + select: ({ data }) => data, + } + ); - return LEGEND_LAYERS['features-preview-new']({ - items: - projectFeaturesQuery.data?.map(({ id, featureClassName: name }) => ({ + const totalItems = projectFeaturesQuery.data?.length || 0; + + const items = + projectFeaturesQuery.data.map(({ id, featureClassName }, index) => { + const color = + totalItems > COLORS['features-preview'].ramp.length + ? chroma.scale(COLORS['features-preview'].ramp).colors(totalItems)[index] + : COLORS['features-preview'].ramp[index]; + + return { id, - name, - })) || [], + name: featureClassName, + color, + }; + }) || []; + + return LEGEND_LAYERS['features-preview-new']({ + items, + onChangeVisibility: (featureId: Feature['id']) => { + const newSelectedFeatures = [...selectedFeatures]; + const isIncluded = newSelectedFeatures.includes(featureId); + if (!isIncluded) { + newSelectedFeatures.push(featureId); + } else { + const i = newSelectedFeatures.indexOf(featureId); + newSelectedFeatures.splice(i, 1); + } + dispatch(setSelectedFeatures(newSelectedFeatures)); + + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + }, + }) + ); + }, }); }; diff --git a/app/layout/scenarios/edit/map/component.tsx b/app/layout/scenarios/edit/map/component.tsx index d27fe03892..cb10c04a8a 100644 --- a/app/layout/scenarios/edit/map/component.tsx +++ b/app/layout/scenarios/edit/map/component.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import React, { ComponentProps, useCallback, useEffect, useState, useMemo } from 'react'; import { useRouter } from 'next/router'; @@ -7,6 +7,7 @@ import { getScenarioEditSlice } from 'store/slices/scenarios/edit'; import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; import { LayerManager, Layer } from '@vizzuality/layer-manager-react'; +import { FiLayers } from 'react-icons/fi'; import { useAccessToken } from 'hooks/auth'; import { useSelectedFeatures, useTargetedFeatures } from 'hooks/features'; @@ -17,12 +18,12 @@ import { useWDPAPreviewLayer, usePUGridLayer, useFeaturePreviewLayers, - useLegend, + // useLegend, useBBOX, useTargetedPreviewLayers, } from 'hooks/map'; import { useProject } from 'hooks/projects'; -import { useCostSurfaceRange, useScenario } from 'hooks/scenarios'; +import { useCostSurfaceRange, useScenario, useScenarioPU } from 'hooks/scenarios'; import { useBestSolution } from 'hooks/solutions'; import { useWDPACategories } from 'hooks/wdpa'; @@ -34,13 +35,18 @@ import FitBoundsControl from 'components/map/controls/fit-bounds'; import LoadingControl from 'components/map/controls/loading'; import ZoomControl from 'components/map/controls/zoom'; import Legend from 'components/map/legend'; +import LegendGroup from 'components/map/legend/group'; import LegendItem from 'components/map/legend/item'; +import { LegendItemType } from 'components/map/legend/types'; import LegendTypeBasic from 'components/map/legend/types/basic'; import LegendTypeChoropleth from 'components/map/legend/types/choropleth'; import LegendTypeGradient from 'components/map/legend/types/gradient'; import LegendTypeMatrix from 'components/map/legend/types/matrix'; import { TABS } from 'layout/project/navigation/constants'; import ScenariosDrawingManager from 'layout/scenarios/edit/map/drawing-manager'; +import { cn } from 'utils/cn'; + +import { useScenarioLegend } from './legend/hooks'; export const ScenariosEditMap = (): JSX.Element => { const [open, setOpen] = useState(true); @@ -67,11 +73,18 @@ export const ScenariosEditMap = (): JSX.Element => { const dispatch = useAppDispatch(); + useScenarioPU(sid, { + onSuccess: ({ included, excluded, available }) => { + dispatch(setPuIncludedValue(included)); + dispatch(setPuExcludedValue(excluded)); + dispatch(setPuAvailableValue(available)); + }, + }); + const { cache, // WDPA - wdpaCategories, wdpaThreshold, // Features @@ -97,6 +110,7 @@ export const ScenariosEditMap = (): JSX.Element => { // Settings layerSettings, } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + const legendConfig = useScenarioLegend(); const { data } = useProject(pid); const { bbox } = data; @@ -212,100 +226,112 @@ export const ScenariosEditMap = (): JSX.Element => { }, [tab]); const sublayers = useMemo(() => { - if (tab === TABS['scenario-protected-areas']) { - return ['wdpa-percentage']; - } - - if (tab === TABS['scenario-cost-surface']) { - return ['cost']; - } - - if (tab === TABS['scenario-planning-unit-status']) { - return ['wdpa-percentage', 'lock-available', 'lock-in', 'lock-out']; - } - - if ([TABS['scenario-features'], TABS['scenario-features-targets-spf']].includes(tab)) { - return ['wdpa-percentage', 'features', 'features-preview']; - } - - if (tab === TABS['scenario-gap-analysis']) { - return ['features']; - } - - if ([TABS['scenario-advanced-settings'], TABS['scenario-blm-calibration']].includes(tab)) { - return ['wdpa-percentage', 'features']; - } - - if ([TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab)) { - return ['frequency', 'solution']; - } - - return []; - }, [tab]); - - const layers = useMemo(() => { - const protectedCategories = protectedAreas || []; - - if (tab === TABS['scenario-cost-surface']) { - return ['cost', 'pugrid']; - } - if (tab === TABS['scenario-planning-unit-status']) { - return [ - ...(protectedCategories.length ? ['wdpa-percentage'] : []), - 'lock-in', - 'lock-out', - 'lock-available', - 'pugrid', - ]; - } - - if (tab === TABS['scenario-protected-areas'] && !!protectedCategories.length) { - return ['wdpa-percentage', 'wdpa-preview', 'pugrid']; - } - - if ([TABS['scenario-features'], TABS['scenario-features-targets-spf']].includes(tab)) { - return [ - ...(protectedCategories.length ? ['wdpa-percentage'] : []), - ...(preHighlightFeatures.length ? ['features-highlight'] : []), - !!previewFeatureIsSelected && 'features-preview', - 'pugrid', - ]; - } - - if (tab === TABS['scenario-gap-analysis']) { - return ['features', 'pugrid']; - } - - if ([TABS['scenario-advanced-settings'], TABS['scenario-blm-calibration']].includes(tab)) { - return ['wdpa-percentage', 'features']; - } - - if ([TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab)) { - return ['frequency', 'solution', 'pugrid']; - } - - if ( - [TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab) && - !postHighlightFeatures.length - ) { - return ['features']; - } - - if ( - [TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab) && - !!postHighlightFeatures.length - ) { - return ['features', 'features-highlight']; - } - - return ['pugrid']; - }, [ - tab, - protectedAreas, - previewFeatureIsSelected, - preHighlightFeatures.length, - postHighlightFeatures.length, - ]); + // if (tab === TABS['scenario-protected-areas']) { + // return ['wdpa-percentage']; + // } + + // if (tab === TABS['scenario-cost-surface']) { + // return ['cost']; + // } + + // if (tab === TABS['scenario-planning-unit-status']) { + // return ['wdpa-percentage', 'lock-available', 'lock-in', 'lock-out']; + // } + + // if ([TABS['scenario-features'], TABS['scenario-features-targets-spf']].includes(tab)) { + // return ['wdpa-percentage', 'features', 'features-preview']; + // } + + // if (tab === TABS['scenario-gap-analysis']) { + // return ['features']; + // } + + // if ([TABS['scenario-advanced-settings'], TABS['scenario-blm-calibration']].includes(tab)) { + // return ['wdpa-percentage', 'features']; + // } + + // if ( + // [TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab) + // ) { + // return ['frequency', 'solution']; + // } + + return [ + ...(layerSettings['wdpa-percentage']?.visibility ? ['wdpa-percentage'] : []), + ...(layerSettings['features']?.visibility ? ['features'] : []), + ...(layerSettings['features-highlight']?.visibility ? ['features-highlight'] : []), + ...(layerSettings['cost']?.visibility ? ['cost'] : []), + ...(layerSettings['lock-in']?.visibility ? ['lock-in'] : []), + ...(layerSettings['lock-out']?.visibility ? ['lock-out'] : []), + ...(layerSettings['lock-available']?.visibility ? ['lock-available'] : []), + ...(layerSettings['frequency']?.visibility ? ['frequency'] : []), + ...(layerSettings['solution']?.visibility ? ['solution'] : []), + ]; + }, [layerSettings]); + + // const layers = useMemo(() => { + // const protectedCategories = protectedAreas || []; + + // if (tab === TABS['scenario-cost-surface']) { + // return ['cost', 'pugrid']; + // } + // if (tab === TABS['scenario-planning-unit-status']) { + // return [ + // ...(protectedCategories.length ? ['wdpa-percentage'] : []), + // 'lock-in', + // 'lock-out', + // 'lock-available', + // 'pugrid', + // ]; + // } + + // if (tab === TABS['scenario-protected-areas'] && !!protectedCategories.length) { + // return ['wdpa-percentage', 'wdpa-preview', 'pugrid']; + // } + + // if ([TABS['scenario-features'], TABS['scenario-features-targets-spf']].includes(tab)) { + // return [ + // ...(protectedCategories.length ? ['wdpa-percentage'] : []), + // ...(preHighlightFeatures.length ? ['features-highlight'] : []), + // !!previewFeatureIsSelected && 'features-preview', + // 'pugrid', + // ]; + // } + + // if (tab === TABS['scenario-gap-analysis']) { + // return ['features', 'pugrid']; + // } + + // if ([TABS['scenario-advanced-settings'], TABS['scenario-blm-calibration']].includes(tab)) { + // return ['wdpa-percentage', 'features']; + // } + + // if ([TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab)) { + // return ['frequency', 'solution', 'pugrid']; + // } + + // if ( + // [TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab) && + // !postHighlightFeatures.length + // ) { + // return ['features']; + // } + + // if ( + // [TABS['scenario-solutions'], TABS['scenario-target-achievement']].includes(tab) && + // !!postHighlightFeatures.length + // ) { + // return ['features', 'features-highlight']; + // } + + // return ['pugrid']; + // }, [ + // tab, + // protectedAreas, + // previewFeatureIsSelected, + // preHighlightFeatures.length, + // postHighlightFeatures.length, + // ]); const featuresIds = useMemo(() => { if (allGapAnalysisData) { @@ -321,33 +347,33 @@ export const ScenariosEditMap = (): JSX.Element => { }, [postHighlightFeatures, selectedSolution, bestSolution]); const WDPApreviewLayer = useWDPAPreviewLayer({ - ...wdpaCategories, - pid: `${pid}`, + WDPACategories: protectedAreasData?.map(({ id }) => id), + pid, cache, - active: tab === TABS['scenario-protected-areas'], + active: true, bbox, options: { - ...layerSettings['wdpa-preview'], + layerSettings, }, }); const FeaturePreviewLayers = useFeaturePreviewLayers({ features: selectedFeaturesData, cache, - active: tab === TABS['scenario-features'], + active: selectedFeatures.length > 0, bbox, options: { featuresRecipe, featureHoverId, selectedFeatures, - ...layerSettings['features-preview'], + layerSettings, }, }); const TargetedPreviewLayers = useTargetedPreviewLayers({ features: targetedFeaturesData, cache, - active: tab === TABS['scenario-features-targets-spf'], + active: targetedFeaturesData.length > 0, bbox, options: { featuresRecipe, @@ -360,7 +386,7 @@ export const ScenariosEditMap = (): JSX.Element => { const PUGridLayer = usePUGridLayer({ cache, active: true, - sid: sid ? `${sid}` : null, + sid, include, sublayers, options: { @@ -402,23 +428,23 @@ export const ScenariosEditMap = (): JSX.Element => { ...TargetedPreviewLayers, ].filter((l) => !!l); - const LEGEND = useLegend({ - layers, - options: { - wdpaIucnCategories: protectedAreas, - wdpaThreshold: - tab === TABS['scenario-protected-areas'] ? wdpaThreshold : scenarioData?.wdpaThreshold, - cost: costSurfaceRangeData, - items: selectedPreviewFeatures, - puAction, - puIncludedValue: [...puIncludedValue, ...puTmpIncludedValue], - puExcludedValue: [...puExcludedValue, ...puTmpExcludedValue], - puAvailableValue: [...puAvailableValue, ...puTmpAvailableValue], - runId: selectedSolution?.runId || bestSolution?.runId, - numberOfRuns: scenarioData?.numberOfRuns || 0, - layerSettings, - }, - }); + // const LEGEND = useLegend({ + // layers, + // options: { + // wdpaIucnCategories: protectedAreas, + // wdpaThreshold: + // tab === TABS['scenario-protected-areas'] ? wdpaThreshold : scenarioData?.wdpaThreshold, + // cost: costSurfaceRangeData, + // items: selectedPreviewFeatures, + // puAction, + // puIncludedValue: [...puIncludedValue, ...puTmpIncludedValue], + // puExcludedValue: [...puExcludedValue, ...puTmpExcludedValue], + // puAvailableValue: [...puAvailableValue, ...puTmpAvailableValue], + // runId: selectedSolution?.runId || bestSolution?.runId, + // numberOfRuns: scenarioData?.numberOfRuns || 0, + // layerSettings, + // }, + // }); useEffect(() => { setBounds({ @@ -613,7 +639,38 @@ export const ScenariosEditMap = (): JSX.Element => { [setLayerSettings, dispatch, layerSettings] ); - console.log(LEGEND); + const renderLegendItems = ({ + type, + intersections, + items, + }: { + type?: LegendItemType; + intersections?: ComponentProps['intersections']; + items?: + | ComponentProps['items'] + | ComponentProps['items'] + | ComponentProps['items'] + | ComponentProps['items']; + }) => { + switch (type) { + case 'basic': + return ; + case 'choropleth': + return ; + case 'gradient': + return ; + case 'matrix': + return ( + + ); + default: + return null; + } + }; return (
    @@ -672,39 +729,68 @@ export const ScenariosEditMap = (): JSX.Element => { - {/* Legend */} - -
    - setOpen(!open)}> - {LEGEND.map((i) => { - const { type, items, intersections, id } = i; - +
    + + setOpen(!open)}> + {legendConfig.map((c) => { return ( - onChangeOpacity(opacity, id)} - onChangeVisibility={() => onChangeVisibility(id)} - {...i} + - {type === 'matrix' && ( - - )} - {type === 'basic' && ( - - )} - {type === 'choropleth' && ( - - )} - {type === 'gradient' && ( - - )} - + {c.layers?.map((layer) => { + if (layer) { + return ( + onChangeOpacity(opacity, layer.id)} + settings={layerSettings[layer.id]} + {...layer} + > + {renderLegendItems(layer)} + + ); + } + })} + {c.subgroups?.map((subgroup) => { + return ( + +
    + {subgroup.layers?.map((layer) => { + if (layer) { + return ( + onChangeOpacity(opacity, layer.id)} + settings={layerSettings[layer.id]} + {...layer} + > + {renderLegendItems(layer as any)} + + ); + } + })} +
    +
    + ); + })} + ); })}
    diff --git a/app/layout/scenarios/edit/map/legend/hooks/index.ts b/app/layout/scenarios/edit/map/legend/hooks/index.ts new file mode 100644 index 0000000000..42c218fe56 --- /dev/null +++ b/app/layout/scenarios/edit/map/legend/hooks/index.ts @@ -0,0 +1,349 @@ +import { useRouter } from 'next/router'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { getScenarioEditSlice } from 'store/slices/scenarios/edit'; + +import chroma from 'chroma-js'; + +import { useProjectCostSurfaces } from 'hooks/cost-surface'; +import { useProjectFeatures, useSelectedFeatures } from 'hooks/features'; +import { COLORS, LEGEND_LAYERS } from 'hooks/map/constants'; +import { useProject } from 'hooks/projects'; +import { useScenario } from 'hooks/scenarios'; +import { useWDPACategories } from 'hooks/wdpa'; + +import { Feature } from 'types/api/feature'; +import { WDPA } from 'types/api/wdpa'; + +export const useCostSurfaceLegend = () => { + const { query } = useRouter(); + const { pid, sid } = query as { pid: string; sid: string }; + const { selectedCostSurfaces } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + + const costSurfaceQuery = useProjectCostSurfaces( + pid, + {}, + { + select: (data) => data.filter((cs) => selectedCostSurfaces.includes(cs.id)), + } + ); + + // todo: uncomment when API is ready + // return LEGEND_LAYERS['cost-surface']({ + // items: costSurfaceQuery.data?.map(({ name, min = 1, max = 8 }) => ({ name, min, max })) || [], + // }); + + return LEGEND_LAYERS['cost-surface']({ + items: [{ name: 'Cost Surface 2', min: 1, max: 22 }], + }); +}; + +export const useConservationAreasLegend = () => { + const { query } = useRouter(); + const { pid, sid } = query as { pid: string; sid: string }; + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + + const { data: projectData } = useProject(pid); + const WDPACategoriesQuery = useWDPACategories({ + adminAreaId: + projectData?.adminAreaLevel2Id || projectData?.adminAreaLevel1I || projectData?.countryId, + customAreaId: + !projectData?.adminAreaLevel2Id && !projectData?.adminAreaLevel1I && !projectData?.countryId + ? projectData?.planningAreaId + : null, + scenarioId: sid, + }); + + const items = WDPACategoriesQuery.data?.map(({ id, name }) => ({ + id, + name, + })); + + return LEGEND_LAYERS['designated-areas']({ + items, + onChangeVisibility: (WDPAId: WDPA['id']) => { + dispatch( + setLayerSettings({ + id: WDPAId, + settings: { + visibility: !layerSettings[WDPAId]?.visibility, + }, + }) + ); + }, + }); +}; + +export const useFeatureAbundanceLegend = () => { + const { query } = useRouter(); + const { pid, sid } = query as { pid: string; sid: string }; + const { selectedFeatures } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + + const projectFeaturesQuery = useProjectFeatures(pid, selectedFeatures); + + // todo: uncomment when API is ready + // return projectFeaturesQuery.data?.map( + // ({ featureClassName: name }, index) => + // LEGEND_LAYERS['features-abundance']({ + // abundance: { + // min: 1, + // max: 8, + // name, + // index, + // }, + // }) || [] + // ); + + return LEGEND_LAYERS['features-abundance']({ + items: [ + { + min: 1, + max: 8, + name: 'feature abundance A', + }, + { + min: 2, + max: 5, + name: 'feature abundance B', + }, + { + min: 8, + max: 34, + name: 'feature abundance C', + }, + ], + }); +}; + +export const useFeatureLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const dispatch = useAppDispatch(); + const { data: features } = useSelectedFeatures(sid); + const scenarioSlice = getScenarioEditSlice(sid); + const { setSelectedFeatures, setLayerSettings } = scenarioSlice.actions; + const { selectedFeatures }: { selectedFeatures: Feature['id'][] } = useAppSelector( + (state) => state[`/scenarios/${sid}/edit`] + ); + + const totalItems = features.length || 0; + + const items = + features.map(({ id, name }, index) => { + const color = + totalItems > COLORS['features-preview'].ramp.length + ? chroma.scale(COLORS['features-preview'].ramp).colors(totalItems)[index] + : COLORS['features-preview'].ramp[index]; + + return { + id, + name, + color, + }; + }) || []; + + return LEGEND_LAYERS['features-preview-new']({ + items, + onChangeVisibility: (featureId: Feature['id']) => { + const newSelectedFeatures = [...selectedFeatures]; + const isIncluded = newSelectedFeatures.includes(featureId); + if (!isIncluded) { + newSelectedFeatures.push(featureId); + } else { + const i = newSelectedFeatures.indexOf(featureId); + newSelectedFeatures.splice(i, 1); + } + dispatch(setSelectedFeatures(newSelectedFeatures)); + + const { color } = items.find(({ id }) => id === featureId) || {}; + + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + color, + }, + }) + ); + }, + }); +}; + +export const useLockInLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + + const { puTmpIncludedValue, puIncludedValue, layerSettings } = useAppSelector( + (state) => state[`/scenarios/${sid}/edit`] + ); + return LEGEND_LAYERS['lock-in']({ + puIncludedValue: [...puIncludedValue, ...puTmpIncludedValue], + onChangeVisibility: () => { + dispatch( + setLayerSettings({ + id: 'lock-in', + settings: { + visibility: !layerSettings['lock-in']?.visibility, + }, + }) + ); + }, + }); +}; + +export const useLockOutLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + + const { puTmpExcludedValue, puExcludedValue, layerSettings } = useAppSelector( + (state) => state[`/scenarios/${sid}/edit`] + ); + + return LEGEND_LAYERS['lock-out']({ + puExcludedValue: [...puExcludedValue, ...puTmpExcludedValue], + onChangeVisibility: () => { + dispatch( + setLayerSettings({ + id: 'lock-out', + settings: { + visibility: !layerSettings['lock-out']?.visibility, + }, + }) + ); + }, + }); +}; + +export const useLockAvailableLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + const { puTmpAvailableValue, puAvailableValue, layerSettings } = useAppSelector( + (state) => state[`/scenarios/${sid}/edit`] + ); + + return LEGEND_LAYERS['lock-available']({ + puAvailableValue: [...puAvailableValue, ...puTmpAvailableValue], + onChangeVisibility: () => { + dispatch( + setLayerSettings({ + id: 'lock-available', + settings: { + visibility: !layerSettings['lock-available']?.visibility, + }, + }) + ); + }, + }); +}; + +export const useFrequencyLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + const scenarioQuery = useScenario(sid); + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + + if (!scenarioQuery.data?.ranAtLeastOnce) return null; + + return LEGEND_LAYERS['frequency']({ + numberOfRuns: scenarioQuery.data?.numberOfRuns, + onChangeVisibility: () => { + dispatch( + setLayerSettings({ + id: 'frequency', + settings: { + visibility: !layerSettings['frequency']?.visibility, + }, + }) + ); + }, + }); +}; + +export const useSolutionsLegend = () => { + const { query } = useRouter(); + const { sid } = query as { sid: string }; + + const scenarioQuery = useScenario(sid); + + const dispatch = useAppDispatch(); + const scenarioSlice = getScenarioEditSlice(sid); + const { setLayerSettings } = scenarioSlice.actions; + const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); + + if (!scenarioQuery.data?.ranAtLeastOnce) return null; + + return LEGEND_LAYERS['solution']({ + onChangeVisibility: () => { + dispatch( + setLayerSettings({ + id: 'solution', + settings: { + visibility: !layerSettings['solution']?.visibility, + }, + }) + ); + }, + }); +}; + +export const useScenarioLegend = () => { + return [ + { + name: 'Planning Grid', + subgroups: [ + { + name: 'Planning Grid', + layers: [ + LEGEND_LAYERS['pugrid'](), + useLockInLegend(), + useLockOutLegend(), + useLockAvailableLegend(), + ], + }, + { + name: 'Features', + layers: [LEGEND_LAYERS['wdpa-preview'](), useFrequencyLegend(), useSolutionsLegend()], + }, + ], + }, + { + name: 'Designated Areas', + subgroups: [ + { + name: 'Conservation Areas', + layers: useConservationAreasLegend(), + }, + ], + }, + { + name: 'Features (Continuous)', + layers: useFeatureAbundanceLegend(), + }, + { + name: 'Features (Binary)', + layers: useFeatureLegend(), + }, + ]; +}; diff --git a/app/layout/scenarios/new/map/component.tsx b/app/layout/scenarios/new/map/component.tsx index 39bc2d5fde..2a1d623429 100644 --- a/app/layout/scenarios/new/map/component.tsx +++ b/app/layout/scenarios/new/map/component.tsx @@ -169,19 +169,12 @@ export const ScenarioNewMap: React.FC = () => { {/* Legend */}
    - setOpen(!open)}> + setOpen(!open)}> {LEGEND.map((i) => { const { type, items, intersections } = i; return ( - onChangeOpacity(opacity, id)} - // onChangeVisibility={() => onChangeVisibility(id)} - {...i} - > + {type === 'matrix' && (