diff --git a/app/hooks/map/index.ts b/app/hooks/map/index.ts index 2b01985dc3..cd483bf043 100644 --- a/app/hooks/map/index.ts +++ b/app/hooks/map/index.ts @@ -658,6 +658,7 @@ export function usePUGridLayer({ puIncludedValue, puExcludedValue, puAvailableValue, + selectedFeatures = [], preHighlightFeatures = [], postHighlightFeatures = [], runId, @@ -764,6 +765,55 @@ export function usePUGridLayer({ ], }, })), + // features abundance + ...selectedFeatures.map((featureId) => { + const { + visibility = true, + opacity = 1, + amountRange = { min: 50000, max: 1000000 }, + } = restLayerSettings[featureId] || {}; + + return { + type: 'fill', + 'source-layer': 'layer0', + layout: { + visibility: getLayerVisibility(visibility), + }, + filter: ['all', ['in', featureId, ['get', 'featureList']]], + paint: { + 'fill-outline-color': 'yellow', + 'fill-color': [ + 'let', + 'amount', + [ + 'to-number', + [ + 'let', + 'idx', + ['index-of', featureId, ['get', 'featureList']], + [ + 'slice', + ['get', 'featureList'], + ['+', ['index-of', ':', ['get', 'featureList'], ['var', 'idx']], 1], + ['index-of', ';', ['get', 'featureList'], ['var', 'idx']], + ], + ], + ], + [ + 'interpolate', + ['linear'], + ['var', 'amount'], + amountRange.min, + 'white', // ! use COLORS.abundance.default instead when is available + amountRange.max, + 'green', + // color, // ! enable the color variable when we receive it + ], + ], + 'fill-opacity': opacity, + }, + }; + }), ] : []), diff --git a/app/hooks/map/types.ts b/app/hooks/map/types.ts index be11d396be..dd73357bb3 100644 --- a/app/hooks/map/types.ts +++ b/app/hooks/map/types.ts @@ -123,7 +123,8 @@ export interface UsePUGridLayer { puExcludedValue?: string[]; puAvailableValue?: string[]; runId?: number; - features?: string[]; + features?: Feature['id'][]; + selectedFeatures?: Feature['id'][]; preHighlightFeatures?: Array; postHighlightFeatures?: Array; cost?: { @@ -172,6 +173,10 @@ export interface UsePUGridLayer { [key: string]: { opacity?: number; visibility?: boolean; + amountRange?: { + min: number; + max: number; + }; }; }; }; diff --git a/app/layout/projects/show/map/legend/hooks/index.ts b/app/layout/projects/show/map/legend/hooks/index.ts index 98b8d0dabf..01ca16e2db 100644 --- a/app/layout/projects/show/map/legend/hooks/index.ts +++ b/app/layout/projects/show/map/legend/hooks/index.ts @@ -116,7 +116,8 @@ export const useConservationAreasLegend = () => { }); }; -export const useFeatureAbundanceLegend = () => { +export const useFeaturesLegend = () => { + const { selectedFeatures } = useAppSelector((state) => state['/projects/[id]']); const { query } = useRouter(); const { pid } = query as { pid: string }; @@ -125,18 +126,39 @@ export const useFeatureAbundanceLegend = () => { pid, { sort: 'feature_class_name' }, { - select: ({ data }) => data, + select: ({ data }) => ({ + binaryFeatures: + data?.filter( + (feature) => !Object.hasOwn(feature, 'min') && !Object.hasOwn(feature, 'max') + ) || [], + continuousFeatures: + data?.filter( + (feature) => Object.hasOwn(feature, 'min') && Object.hasOwn(feature, 'max') + ) || [], + }), } ); - const { layerSettings, selectedFeatures: visibleFeatures } = useAppSelector( - (state) => state['/projects/[id]'] - ); + const totalItems = + projectFeaturesQuery.data?.binaryFeatures.length + + projectFeaturesQuery.data?.continuousFeatures.length || 0; - const totalItems = projectFeaturesQuery.data?.length || 0; + const binaryFeaturesItems = + projectFeaturesQuery.data?.binaryFeatures?.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]; - const items = - projectFeaturesQuery.data?.map( + return { + id, + name: featureClassName, + color, + }; + }) || []; + + const continuousFeaturesItems = + projectFeaturesQuery.data?.continuousFeatures.map( ({ id, featureClassName, amountRange = { min: 5000, max: 100000 } }, index) => { const color = totalItems > COLORS['features-preview'].ramp.length @@ -152,91 +174,62 @@ export const useFeatureAbundanceLegend = () => { } ) || []; - return LEGEND_LAYERS['features-abundance']({ - items, - onChangeVisibility: (featureId: Feature['id']) => { - const { color, amountRange } = items.find(({ id }) => id === featureId) || {}; - - const newSelectedFeatures = [...visibleFeatures]; - 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: `feature-abundance-${featureId}`, - settings: { - visibility: !layerSettings[featureId]?.visibility, - amountRange, - color, - }, - }) - ); - }, - }); -}; - -export const useFeaturesLegend = () => { - const { selectedFeatures } = useAppSelector((state) => state['/projects/[id]']); - const { query } = useRouter(); - const { pid } = query as { pid: string }; - - const dispatch = useAppDispatch(); - const projectFeaturesQuery = useAllFeatures( - pid, - { sort: 'feature_class_name' }, - { - select: ({ data }) => data, - } - ); - - 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: 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)); + return [ + ...LEGEND_LAYERS['features-preview-new']({ + items: binaryFeaturesItems, + 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 } = binaryFeaturesItems.find(({ id }) => id === featureId) || {}; - const { color } = items.find(({ id }) => id === featureId) || {}; + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + color, + }, + }) + ); + }, + }), + ...LEGEND_LAYERS['features-abundance']({ + items: continuousFeaturesItems, + onChangeVisibility: (featureId: Feature['id']) => { + const { color, amountRange } = + continuousFeaturesItems.find(({ id }) => id === featureId) || {}; + + 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, - color, - }, - }) - ); - }, - }); + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + amountRange, + color, + }, + }) + ); + }, + }), + ]; }; export const useComparisonScenariosLegend = ({ @@ -306,11 +299,7 @@ export const useInventoryLegend = ({ layers: useConservationAreasLegend(), }, { - name: 'Features (Continuous)', - layers: useFeatureAbundanceLegend(), - }, - { - name: 'Features (Binary)', + name: 'Features', layers: useFeaturesLegend(), }, ]; diff --git a/app/layout/scenarios/edit/map/component.tsx b/app/layout/scenarios/edit/map/component.tsx index 973a015f42..111e3a2c05 100644 --- a/app/layout/scenarios/edit/map/component.tsx +++ b/app/layout/scenarios/edit/map/component.tsx @@ -7,24 +7,20 @@ import { getScenarioEditSlice } from 'store/slices/scenarios/edit'; import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; import { LayerManager, Layer } from '@vizzuality/layer-manager-react'; -import { sortBy } from 'lodash'; import { FiLayers } from 'react-icons/fi'; import { useAccessToken } from 'hooks/auth'; import { useSelectedFeatures, useTargetedFeatures } from 'hooks/features'; import { useAllGapAnalysis } from 'hooks/gap-analysis'; import { - // usePUGridPreviewLayer, - // useAdminPreviewLayer, useWDPAPreviewLayer, usePUGridLayer, useFeaturePreviewLayers, - // useLegend, useBBOX, useTargetedPreviewLayers, } from 'hooks/map'; import { useProject } from 'hooks/projects'; -import { useCostSurfaceRange, useScenario, useScenarioPU } from 'hooks/scenarios'; +import { useScenario, useScenarioPU } from 'hooks/scenarios'; import { useBestSolution } from 'hooks/solutions'; import { useWDPACategories } from 'hooks/wdpa'; @@ -130,59 +126,11 @@ export const ScenariosEditMap = (): JSX.Element => { const { data: scenarioData } = useScenario(sid); const { data: selectedFeaturesData } = useSelectedFeatures(sid, {}); - const { data: targetedFeaturesData } = useTargetedFeatures(sid, {}); - - const previewFeatureIsSelected = useMemo(() => { - if (tab === TABS['scenario-features']) { - return ( - (selectedFeaturesData || []).filter(({ id }) => selectedFeatures.includes(id)).length > 0 - ); - } - - if (tab === TABS['scenario-features-targets-spf']) { - return ( - (targetedFeaturesData || []).filter(({ id }) => selectedFeatures.includes(id)).length > 0 - ); - } - - return false; - }, [tab, selectedFeaturesData, targetedFeaturesData, selectedFeatures]); - - const selectedPreviewFeatures = useMemo(() => { - if (tab === TABS['scenario-features']) { - return (selectedFeaturesData || []) - .filter(({ id }) => selectedFeatures.includes(id)) - .map(({ name, id }) => ({ name, id })) - .sort((a, b) => { - const aIndex = selectedFeatures.indexOf(a.id as string); - const bIndex = selectedFeatures.indexOf(b.id as string); - return aIndex - bIndex; - }); - } - - if (tab === TABS['scenario-features-targets-spf']) { - return (targetedFeaturesData || []) - .filter(({ id }) => selectedFeatures.includes(id)) - .map(({ name, id }) => ({ name, id })) - .sort((a, b) => { - const aIndex = selectedFeatures.indexOf(a.id as string); - const bIndex = selectedFeatures.indexOf(b.id as string); - return aIndex - bIndex; - }); - } - - return []; - }, [tab, selectedFeaturesData, targetedFeaturesData, selectedFeatures]); - - const { data: costSurfaceRangeData } = useCostSurfaceRange(sid); - const { data: allGapAnalysisData } = useAllGapAnalysis(sid, { enabled: !!sid, }); - const { data: projectData } = useProject(pid); - const { data: protectedAreasData } = useWDPACategories({ adminAreaId: projectData?.adminAreaLevel2Id || projectData?.adminAreaLevel1I || projectData?.countryId, @@ -285,55 +233,35 @@ export const ScenariosEditMap = (): JSX.Element => { puIncludedValue: [...puIncludedValue, ...puTmpIncludedValue], puExcludedValue: [...puExcludedValue, ...puTmpExcludedValue], puAvailableValue: [...puAvailableValue, ...puTmpAvailableValue], - // features: [TABS['scenario-target-achievement'], TABS['scenario-gap-analysis']].includes(tab) - // ? [] - // : featuresIds, + features: [TABS['scenario-features'], TABS['scenario-features-targets-spf']].includes(tab) + ? [] + : featuresIds, + selectedFeatures, preHighlightFeatures, postHighlightFeatures: postHighlightedFeaturesIds, - // cost: costSurfaceRangeData, runId: selectedSolution?.runId || bestSolution?.runId, settings: { - // pugrid: layerSettings.pugrid, - // 'wdpa-percentage': layerSettings['wdpa-percentage'], - // features: layerSettings.features, - // 'cost-surface': layerSettings[selectedCostSurface], - // 'lock-in': layerSettings['lock-in'], - // 'lock-out': layerSettings['lock-out'], - // 'lock-available': layerSettings['lock-available'], - // frequency: layerSettings.frequency, - // solution: layerSettings.solution, + pugrid: layerSettings.pugrid, + 'wdpa-percentage': layerSettings['wdpa-percentage'], + 'features-highlight': layerSettings['features-highlight'], + cost: layerSettings.cost, + 'lock-in': layerSettings['lock-in'], + 'lock-out': layerSettings['lock-out'], + 'lock-available': layerSettings['lock-available'], + frequency: layerSettings.frequency, + solution: layerSettings.solution, ...layerSettings, }, }, }); const LAYERS = [ - // PUGridPreviewLayer, - // AdminPreviewLayer, PUGridLayer, WDPApreviewLayer, ...FeaturePreviewLayers, ...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, - // }, - // }); - useEffect(() => { setBounds({ bbox: BBOX, @@ -491,7 +419,7 @@ export const ScenariosEditMap = (): JSX.Element => { ); const handleTransformRequest = useCallback( - (url) => { + (url: string) => { if (url.startsWith(process.env.NEXT_PUBLIC_API_URL)) { return { url, @@ -520,19 +448,6 @@ export const ScenariosEditMap = (): JSX.Element => { [setLayerSettings, dispatch] ); - const onChangeVisibility = useCallback( - (id) => { - const { visibility = true } = layerSettings[id] || {}; - dispatch( - setLayerSettings({ - id, - settings: { visibility: !visibility }, - }) - ); - }, - [setLayerSettings, dispatch, layerSettings] - ); - const renderLegendItems = ({ type, intersections, diff --git a/app/layout/scenarios/edit/map/legend/hooks/index.ts b/app/layout/scenarios/edit/map/legend/hooks/index.ts index de400b6003..d04d7cc93b 100644 --- a/app/layout/scenarios/edit/map/legend/hooks/index.ts +++ b/app/layout/scenarios/edit/map/legend/hooks/index.ts @@ -7,7 +7,7 @@ import chroma from 'chroma-js'; import { orderBy, sortBy } from 'lodash'; import { useProjectCostSurfaces } from 'hooks/cost-surface'; -import { useSelectedFeatures } from 'hooks/features'; +import { useAllFeatures } from 'hooks/features'; import { useAllGapAnalysis } from 'hooks/gap-analysis'; import { COLORS, LEGEND_LAYERS } from 'hooks/map/constants'; import { useProject } from 'hooks/projects'; @@ -119,69 +119,42 @@ export const useConservationAreasLegend = () => { }); }; -export const useFeatureAbundanceLegend = () => { - const { query } = useRouter(); - const { sid } = query as { sid: string }; - - const dispatch = useAppDispatch(); - const { data: features } = useSelectedFeatures(sid); - const scenarioSlice = getScenarioEditSlice(sid); - const { setLayerSettings } = scenarioSlice.actions; - - const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); - - const totalItems = features?.length || 0; - - const items = - features?.map(({ id, name, amountRange = { min: 5000, max: 100000 } }, 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, - amountRange, - color, - }; - }) || []; - - return LEGEND_LAYERS['features-abundance']({ - items, - onChangeVisibility: (featureId: Feature['id']) => { - const { color, amountRange } = items.find(({ id }) => id === featureId) || {}; - - dispatch( - setLayerSettings({ - id: `feature-abundance-${featureId}`, - settings: { - visibility: !layerSettings[featureId]?.visibility, - amountRange, - color, - }, - }) - ); - }, - }); -}; - export const useFeaturesLegend = () => { const { query } = useRouter(); - const { sid } = query as { sid: string }; - - const dispatch = useAppDispatch(); - const { data: features } = useSelectedFeatures(sid); + const { pid, sid } = query as { pid: string; sid: string }; 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 dispatch = useAppDispatch(); + const projectFeaturesQuery = useAllFeatures( + pid, + { sort: 'feature_class_name' }, + { + select: ({ data }) => ({ + binaryFeatures: + data?.filter( + (feature) => + !Object.hasOwn(feature.amountRange, 'min') && + !Object.hasOwn(feature.amountRange, 'max') + ) || [], + continuousFeatures: + data?.filter( + (feature) => + Object.hasOwn(feature.amountRange, 'min') && Object.hasOwn(feature.amountRange, 'max') + ) || [], + }), + } + ); - const items = - features?.map(({ id, name }, index) => { + const totalItems = + projectFeaturesQuery.data?.binaryFeatures.length + + projectFeaturesQuery.data?.continuousFeatures.length || 0; + + const binaryFeaturesItems = + projectFeaturesQuery.data?.binaryFeatures?.map(({ id, featureClassName }, index) => { const color = totalItems > COLORS['features-preview'].ramp.length ? chroma.scale(COLORS['features-preview'].ramp).colors(totalItems)[index] @@ -189,37 +162,84 @@ export const useFeaturesLegend = () => { 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)); + const continuousFeaturesItems = + projectFeaturesQuery.data?.continuousFeatures.map( + ({ id, featureClassName, amountRange = { min: 5000, max: 100000 } }, index) => { + const color = + totalItems > COLORS['features-preview'].ramp.length + ? chroma.scale(COLORS['features-preview'].ramp).colors(totalItems)[index] + : COLORS['features-preview'].ramp[index]; - const { color } = items.find(({ id }) => id === featureId) || {}; + return { + id, + name: featureClassName, + amountRange, + color, + }; + } + ) || []; - dispatch( - setLayerSettings({ - id: featureId, - settings: { - visibility: !isIncluded, - color, - }, - }) - ); - }, - }); + return [ + ...LEGEND_LAYERS['features-preview-new']({ + items: binaryFeaturesItems, + 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 } = binaryFeaturesItems.find(({ id }) => id === featureId) || {}; + + dispatch( + setLayerSettings({ + id: featureId, + settings: { + visibility: !isIncluded, + color, + }, + }) + ); + }, + }), + ...LEGEND_LAYERS['features-abundance']({ + items: continuousFeaturesItems, + onChangeVisibility: (featureId: Feature['id']) => { + const { color, amountRange } = + continuousFeaturesItems.find(({ id }) => id === featureId) || {}; + + 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, + amountRange, + color, + }, + }) + ); + }, + }), + ]; }; export const useLockInLegend = () => { @@ -463,11 +483,7 @@ export const useScenarioLegend = () => { layers: useConservationAreasLegend(), }, { - name: 'Features (Continuous)', - layers: useFeatureAbundanceLegend(), - }, - { - name: 'Features (Binary)', + name: 'Features', layers: useFeaturesLegend(), }, ];