diff --git a/app/hooks/cost-surface/index.ts b/app/hooks/cost-surface/index.ts index 0f59195201..7fac58c35b 100644 --- a/app/hooks/cost-surface/index.ts +++ b/app/hooks/cost-surface/index.ts @@ -5,52 +5,50 @@ import { useSession } from 'next-auth/react'; import { CostSurface } from 'types/api/cost-surface'; import { Project } from 'types/api/project'; -import { API } from 'services/api'; +import { API, JSONAPI } from 'services/api'; import UPLOADS from 'services/uploads'; export function useProjectCostSurfaces( pid: Project['id'], - params: { search?: string; sort?: string } = {}, + params: { sort?: string } = {}, queryOptions: QueryObserverOptions = {} ) { const { data: session } = useSession(); - const mockData: CostSurface[] = [ - { - id: 'b7454579-c48e-4e2f-8438-833280cb65d8', - name: 'Brazil 15 k Cost Surface', - isCustom: true, - scenarioUsageCount: 3, - }, - { - id: 'rfjghhrtersdtbkjshfw', - name: 'Cost Surface Rwanda B', - isCustom: true, - scenarioUsageCount: 0, - }, - { - id: '23275455HGVVCMSJHDFk', - name: 'Cost Surface Rwanda C', - isCustom: true, - scenarioUsageCount: 0, - }, - ]; - return useQuery({ queryKey: ['cost-surfaces', pid], queryFn: async () => - API.request({ + JSONAPI.request<{ data: CostSurface[] }>({ method: 'GET', - // !TODO: change this to the correct endpoint - url: `/projects/${pid}/protected-areas`, + url: `/projects/${pid}/cost-surfaces`, headers: { Authorization: `Bearer ${session.accessToken}`, }, params, - }).then(({ data }) => mockData), - // TODO: enable this when the endpoint is ready - enabled: true, - // enabled: Boolean(pid), + }).then(({ data }) => data?.data), + enabled: Boolean(pid), + ...queryOptions, + }); +} + +export function useProjectCostSurface( + pid: Project['id'], + csid: CostSurface['id'], + queryOptions: QueryObserverOptions = {} +) { + const { data: session } = useSession(); + + return useQuery({ + queryKey: ['cost-surface', csid], + queryFn: async () => + JSONAPI.request<{ data: CostSurface }>({ + method: 'GET', + url: `/projects/${pid}/cost-surfaces/${csid}`, + headers: { + Authorization: `Bearer ${session.accessToken}`, + }, + }).then(({ data }) => data?.data), + enabled: Boolean(csid), ...queryOptions, }); } @@ -67,7 +65,6 @@ export function useEditProjectCostSurface() { projectId: Project['id']; body: Record; }) => { - // TODO: change this to the correct endpoint return API.patch( `projects/${projectId}/cost-surfaces/${costSurfaceId}`, { @@ -90,8 +87,7 @@ export function useUploadProjectCostSurface() { const uploadProjectCostSurface = ({ id, data }: { id: CostSurface['id']; data: FormData }) => { return UPLOADS.request({ method: 'POST', - // TODO: change this to the correct endpoint - url: `/projects/${id}/cost-surface/shapefile`, + url: `/projects/${id}/cost-surfaces/shapefile`, data, headers: { Authorization: `Bearer ${session.accessToken}`, diff --git a/app/hooks/map/constants.tsx b/app/hooks/map/constants.tsx index a2b5382519..e264d7869c 100644 --- a/app/hooks/map/constants.tsx +++ b/app/hooks/map/constants.tsx @@ -313,22 +313,6 @@ export const LEGEND_LAYERS = { visibility: true, }, }), - // 'features-highlight': () => ({ - // id: 'features-highlight', - // name: 'Selected Features', - // icon: ( - // - // ), - // settingsManager: { - // opacity: true, - // visibility: true, - // }, - // }), - // ANALYSIS ['features-abundance']: (options: { items: { @@ -366,7 +350,7 @@ export const LEGEND_LAYERS = { }, 'cost-surface': (options: { items: { id: CostSurface['id']; name: CostSurface['name']; min?: number; max?: number }[]; - onChangeVisibility: () => void; + onChangeVisibility: (id: CostSurface['id']) => void; }) => { const { items, onChangeVisibility } = options; @@ -388,7 +372,9 @@ export const LEGEND_LAYERS = { value: `${max}`, }, ], - onChangeVisibility, + onChangeVisibility: () => { + onChangeVisibility?.(id); + }, })); }, 'lock-available': (options) => { diff --git a/app/hooks/map/index.ts b/app/hooks/map/index.ts index d3955ad9df..2b01985dc3 100644 --- a/app/hooks/map/index.ts +++ b/app/hooks/map/index.ts @@ -1,6 +1,10 @@ import { useMemo } from 'react'; import chroma from 'chroma-js'; +import { Layer } from 'mapbox-gl'; + +import { CostSurface } from 'types/api/cost-surface'; +import { Project } from 'types/api/project'; import { COLORS, LEGEND_LAYERS } from './constants'; import { @@ -171,6 +175,56 @@ export function useGridPreviewLayer({ active, gridId, cache = 0 }: UseGridPrevie }, [active, gridId, cache]); } +export function useCostSurfaceLayer({ + active, + pid, + costSurfaceId, + layerSettings, +}: { + active: boolean; + pid: Project['id']; + costSurfaceId: CostSurface['id']; + layerSettings: UsePUGridLayer['options']['settings']['cost-surface']; +}): Layer { + return useMemo(() => { + if (!active) return null; + + return { + id: `cost-surface-layer-${pid}-${costSurfaceId}`, + type: 'vector', + source: { + type: 'vector', + tiles: [ + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/projects/${pid}/cost-surfaces/${costSurfaceId}/preview/tiles/{z}/{x}/{y}.mvt`, + ], + }, + render: { + layers: [ + { + type: 'fill', + 'source-layer': 'layer0', + layout: { + visibility: layerSettings.visibility ? 'visible' : 'none', + }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], + ['get', 'cost'], + layerSettings.min === layerSettings.max ? 0 : layerSettings.min, + COLORS.cost[0], + layerSettings.max, + COLORS.cost[1], + ], + 'fill-opacity': 0.75 * (layerSettings.opacity || 1), + }, + }, + ], + }, + }; + }, [active, pid, costSurfaceId, layerSettings]); +} + // WDPA preview layer export function useWDPAPreviewLayer({ pid, @@ -601,14 +655,9 @@ export function usePUGridLayer({ const { wdpaIucnCategories = [], wdpaThreshold = 0, - cost = { - min: 0, - max: 100, - }, puIncludedValue, puExcludedValue, puAvailableValue, - // features = [], preHighlightFeatures = [], postHighlightFeatures = [], runId, @@ -617,8 +666,6 @@ export function usePUGridLayer({ const { pugrid: PUgridSettings = {}, 'wdpa-percentage': WdpaPercentageSettings = {}, - // features: FeaturesSettings = {}, - 'cost-surface': CostSettings = {}, 'lock-in': LockInSettings = {}, 'lock-out': LockOutSettings = {}, 'lock-available': LockAvailableSettings = {}, @@ -630,9 +677,6 @@ export function usePUGridLayer({ const { opacity: PUgridOpacity = 1, visibility: PUgridVisibility = true } = PUgridSettings; const { opacity: WdpaPercentageOpacity = 1, visibility: WdpaPercentageVisibility = true } = WdpaPercentageSettings; - // const { opacity: FeaturesOpacity = 1, visibility: FeaturesVisibility = true } = - // FeaturesSettings; - const { opacity: CostOpacity = 1, visibility: CostVisibility = true } = CostSettings; const { opacity: LockInOpacity = 1, visibility: LockInVisibility = true } = LockInSettings; const { opacity: LockOutOpacity = 1, visibility: LockOutVisibility = true } = LockOutSettings; const { opacity: LockAvailableOpacity = 1, visibility: LockAvailableVisibility = true } = @@ -723,31 +767,6 @@ export function usePUGridLayer({ ] : []), - // ANALYSIS - COST SURFACE - ...(sublayers.includes('cost') - ? [ - { - type: 'fill', - 'source-layer': 'layer0', - layout: { - visibility: getLayerVisibility(CostVisibility), - }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - ['get', 'costValue'], - cost.min === cost.max ? 0 : cost.min, - COLORS.cost[0], - cost.max, - COLORS.cost[1], - ], - 'fill-opacity': 0.75 * CostOpacity, - }, - }, - ] - : []), - // PROTECTED AREAS ...(sublayers.includes('wdpa-percentage') && wdpaThreshold !== null && diff --git a/app/hooks/map/types.ts b/app/hooks/map/types.ts index 3216d50036..be11d396be 100644 --- a/app/hooks/map/types.ts +++ b/app/hooks/map/types.ts @@ -1,6 +1,7 @@ import { PUAction } from 'store/slices/scenarios/types'; import { TargetSPFItemProps } from 'components/features/target-spf-item/types'; +import { CostSurface } from 'types/api/cost-surface'; import { Feature } from 'types/api/feature'; import { Scenario } from 'types/api/scenario'; import { WDPA } from 'types/api/wdpa'; @@ -145,6 +146,8 @@ export interface UsePUGridLayer { 'cost-surface'?: { opacity?: number; visibility?: boolean; + min: CostSurface['min']; + max: CostSurface['max']; }; 'lock-in'?: { opacity?: number; diff --git a/app/hooks/scenarios/index.ts b/app/hooks/scenarios/index.ts index e84166e168..5e7576ba44 100644 --- a/app/hooks/scenarios/index.ts +++ b/app/hooks/scenarios/index.ts @@ -34,8 +34,6 @@ import { UseScenariosOptionsProps, UseDeleteScenarioProps, DeleteScenarioProps, - UseUploadScenarioCostSurfaceProps, - UploadScenarioCostSurfaceProps, UseUploadScenarioPUProps, UploadScenarioPUProps, UseSaveScenarioPUProps, @@ -650,36 +648,6 @@ export function useCostSurfaceRange(id: Scenario['id']) { }, [query, data]); } -export function useUploadCostSurface({ - requestConfig = { - method: 'GET', - }, -}: UseUploadScenarioCostSurfaceProps) { - const { data: session } = useSession(); - - const uploadScenarioCostSurface = ({ id, data }: UploadScenarioCostSurfaceProps) => { - return UPLOADS.request({ - url: `/scenarios/${id}/cost-surface/shapefile`, - data, - headers: { - Authorization: `Bearer ${session.accessToken}`, - 'Content-Type': 'multipart/form-data', - }, - ...requestConfig, - }); - }; - - return useMutation(uploadScenarioCostSurface, { - onSuccess: (data, variables, context) => { - console.info('Success', data, variables, context); - }, - onError: (error, variables, context) => { - // An error happened! - console.info('Error', error, variables, context); - }, - }); -} - // PLANNING UNITS export function useScenarioPU( sid: string, diff --git a/app/hooks/scenarios/types.ts b/app/hooks/scenarios/types.ts index b2de799f45..6ae5791803 100644 --- a/app/hooks/scenarios/types.ts +++ b/app/hooks/scenarios/types.ts @@ -39,15 +39,6 @@ export interface UploadScenarioPUProps { data: FormData; } -export interface UseUploadScenarioCostSurfaceProps { - requestConfig?: AxiosRequestConfig; -} - -export interface UploadScenarioCostSurfaceProps { - id?: string; - data: any; -} - export interface UseSaveScenarioPUProps { requestConfig?: AxiosRequestConfig; } diff --git a/app/layout/project/navigation/hooks.tsx b/app/layout/project/navigation/hooks.tsx index 8a04747c2c..9502f3e3fd 100644 --- a/app/layout/project/navigation/hooks.tsx +++ b/app/layout/project/navigation/hooks.tsx @@ -19,7 +19,7 @@ export const useInventoryItems = (): SubMenuItem[] => { const { showCS } = useFeatureFlags(); const { query, route } = useRouter(); const { pid, tab } = query as { pid: string; tab: string }; - const isProjectRoute = route.startsWith('/projects/[pid]'); + const isProjectRoute = route.startsWith('/projects/[pid]') && !route.includes('/scenarios'); return [ { diff --git a/app/layout/project/navigation/index.tsx b/app/layout/project/navigation/index.tsx index b101c19ddf..32f3a7cbcf 100644 --- a/app/layout/project/navigation/index.tsx +++ b/app/layout/project/navigation/index.tsx @@ -63,7 +63,7 @@ export const Navigation = (): JSX.Element => { const { query, route } = useRouter(); const { pid, sid, tab } = query as { pid: string; sid: string; tab: string }; - const isProjectRoute = route.startsWith('/projects/[pid]'); + const isProjectRoute = route.startsWith('/projects/[pid]') && !route.includes('/scenarios'); const isScenarioRoute = route.startsWith('/projects/[pid]/scenarios/') && !route.endsWith('/new'); const { addToast } = useToasts(); diff --git a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx index 8fa57bbd25..dc04d6b4cb 100644 --- a/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/components/inventory-table/row-item/index.tsx @@ -40,10 +40,8 @@ const RowItem = ({ > {name}
- Currently in use in - - {scenarios} - {' '} + Currently in use in{' '} + {scenarios}{' '} scenarios.
diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts index d458ead34d..15dc04b69c 100644 --- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts +++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts @@ -17,7 +17,7 @@ export function bulkDeleteCostSurfaceFromProject( pid: Project['id']; csid: CostSurface['id']; }) => { - return PROJECTS.delete(`/${pid}/cost-surfaces/${csid}`, { + return PROJECTS.delete(`/${pid}/cost-surface/${csid}`, { headers: { Authorization: `Bearer ${session.accessToken}`, }, diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/index.tsx b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/index.tsx index 68acce6c71..60f2d46bbe 100644 --- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/index.tsx @@ -1,9 +1,14 @@ -import { useState, useCallback, useEffect, ChangeEvent } from 'react'; +import { useState, useCallback, useEffect, ChangeEvent, useMemo } from 'react'; import { useRouter } from 'next/router'; import { useAppDispatch, useAppSelector } from 'store/hooks'; -import { setSelectedCostSurfaces as setVisibleCostSurface } from 'store/slices/projects/[id]'; +import { + setLayerSettings, + setSelectedCostSurfaces as setVisibleCostSurface, +} from 'store/slices/projects/[id]'; + +import { orderBy } from 'lodash'; import { useProjectCostSurfaces } from 'hooks/cost-surface'; @@ -22,9 +27,11 @@ const COST_SURFACE_TABLE_COLUMNS = [ const InventoryPanelCostSurface = ({ noData: noDataMessage }: { noData: string }): JSX.Element => { const dispatch = useAppDispatch(); - const { selectedCostSurface: visibleCostSurface, search } = useAppSelector( - (state) => state['/projects/[id]'] - ); + const { + selectedCostSurface: visibleCostSurface, + search, + layerSettings, + } = useAppSelector((state) => state['/projects/[id]']); const { query } = useRouter(); const { pid } = query as { pid: string }; @@ -34,26 +41,44 @@ const InventoryPanelCostSurface = ({ noData: noDataMessage }: { noData: string } sort: COST_SURFACE_TABLE_COLUMNS[0].name, }); - const allProjectCostSurfacesQuery = useProjectCostSurfaces( + const allProjectCostSurfacesQuery = useProjectCostSurfaces< + Omit, 'isDefault'>[] + >( pid, { ...filters, - search, }, { select: (data) => - data?.map((cs) => ({ - id: cs.id, - name: cs.name, - isCustom: cs.isCustom, - scenarioUsageCount: cs.scenarioUsageCount, - })), + data + .filter(({ isDefault }) => !isDefault) + .map((cs) => ({ + ...cs, + isCustom: !cs.isDefault, + })), keepPreviousData: true, placeholderData: [], } ); - const costSurfaceIds = allProjectCostSurfacesQuery.data?.map((cs) => cs.id); + const filteredData = useMemo(() => { + let sortedData = allProjectCostSurfacesQuery.data; + + if (filters.sort === '-name') { + sortedData = orderBy(allProjectCostSurfacesQuery.data, 'name', 'desc'); + } + + if (search) { + sortedData = sortedData.filter((cs) => + cs.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()) + ); + } + + // the API assumes the default sort is ascending + return sortedData; + }, [filters, allProjectCostSurfacesQuery.data, search]); + + const costSurfaceIds = filteredData?.map((cs) => cs.id); const handleSelectAll = useCallback( (evt: ChangeEvent) => { @@ -81,13 +106,24 @@ const InventoryPanelCostSurface = ({ noData: noDataMessage }: { noData: string } const toggleSeeOnMap = useCallback( (costSurfaceId: CostSurface['id']) => { + costSurfaceIds.forEach((id) => { + dispatch( + setLayerSettings({ + id, + settings: { + visibility: id !== costSurfaceId ? false : !layerSettings[costSurfaceId]?.visibility, + }, + }) + ); + }); + if (costSurfaceId === visibleCostSurface) { dispatch(setVisibleCostSurface(null)); } else { dispatch(setVisibleCostSurface(costSurfaceId)); } }, - [dispatch, visibleCostSurface] + [dispatch, visibleCostSurface, layerSettings, costSurfaceIds] ); const handleSort = useCallback( @@ -104,7 +140,7 @@ const InventoryPanelCostSurface = ({ noData: noDataMessage }: { noData: string } const displayBulkActions = selectedCostSurfaceIds.length > 0; - const data: DataItem[] = allProjectCostSurfacesQuery.data?.map((cs) => ({ + const data: DataItem[] = filteredData?.map((cs) => ({ ...cs, name: cs.name, scenarios: cs.scenarioUsageCount, diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/delete/index.tsx b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/delete/index.tsx index c4f233af67..17d822be94 100644 --- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/delete/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/delete/index.tsx @@ -68,7 +68,7 @@ const DeleteModal = ({ addToast( 'delete-bulk-project-cost-surfaces', <> -

Error!

+

Error

Something went wrong deleting the cost surfaces.

, { diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/edit/index.tsx b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/edit/index.tsx index c6135de79b..b3170e0b06 100644 --- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/edit/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/edit/index.tsx @@ -30,7 +30,13 @@ const EditModal = ({ const formRef = useRef['form']>(null); - const allProjectCostSurfacesQuery = useProjectCostSurfaces(pid, {}); + const allProjectCostSurfacesQuery = useProjectCostSurfaces( + pid, + {}, + { + select: (data) => data.find(({ id }) => id === costSurfaceId), + } + ); const editProjectCostSurfaceMutation = useEditProjectCostSurface(); @@ -53,7 +59,7 @@ const EditModal = ({ addToast( 'success-edit-cost-surfaces', <> -

Success!

+

Success

Cost surface edited

, { @@ -82,7 +88,7 @@ const EditModal = ({ return ( initialValues={{ - name: allProjectCostSurfacesQuery.data?.[0]?.name, + name: allProjectCostSurfacesQuery.data?.name, }} ref={formRef} onSubmit={onEditSubmit} diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/upload/index.tsx b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/upload/index.tsx index d9ef0803be..92401deff0 100644 --- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/upload/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/modals/upload/index.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDropzone, DropzoneProps } from 'react-dropzone'; import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form'; +import { useQueryClient } from 'react-query'; import { useRouter } from 'next/router'; @@ -42,6 +43,7 @@ export const CostSurfaceUploadModal = ({ onDismiss: () => void; }): JSX.Element => { const formRef = useRef['form']>(null); + const queryClient = useQueryClient(); const [loading, setLoading] = useState(false); const [successFile, setSuccessFile] = useState<{ name: FormValues['name'] }>(null); @@ -114,9 +116,11 @@ export const CostSurfaceUploadModal = ({ data.append('name', name); uploadProjectCostSurfaceMutation.mutate( - { data, id: `${pid}` }, + { data, id: pid }, { - onSuccess: () => { + onSuccess: async () => { + await queryClient.invalidateQueries(['cost-surfaces', pid]); + setSuccessFile({ ...successFile }); onClose(); addToast( @@ -163,7 +167,7 @@ export const CostSurfaceUploadModal = ({ } ); }, - [pid, addToast, onClose, uploadProjectCostSurfaceMutation, successFile] + [pid, addToast, onClose, uploadProjectCostSurfaceMutation, successFile, queryClient] ); const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ @@ -214,7 +218,7 @@ export const CostSurfaceUploadModal = ({ -

+

Please download and fill in the{' '} - - - - )} - -

- - - -
- - - ); - }} - /> - + Upload cost surface + + )} {successFile && ( @@ -408,6 +159,8 @@ export const GridSetupCostSurface = (): JSX.Element => { )} + + setOpened(false)} isOpen={opened} /> )} diff --git a/app/layout/projects/show/map/index.tsx b/app/layout/projects/show/map/index.tsx index 8db9c9dab6..24a369e10c 100644 --- a/app/layout/projects/show/map/index.tsx +++ b/app/layout/projects/show/map/index.tsx @@ -13,6 +13,7 @@ import { FiLayers } from 'react-icons/fi'; import { HiOutlinePrinter } from 'react-icons/hi'; import { useAccessToken } from 'hooks/auth'; +import { useProjectCostSurface } from 'hooks/cost-surface'; import { useAllFeatures } from 'hooks/features'; import { usePUCompareLayer, @@ -21,6 +22,7 @@ import { useBBOX, useFeaturePreviewLayers, useWDPAPreviewLayer, + useCostSurfaceLayer, } from 'hooks/map'; import { useDownloadScenarioComparisonReport, useProject } from 'hooks/projects'; import { useScenarios } from 'hooks/scenarios'; @@ -105,6 +107,8 @@ export const ProjectMap = (): JSX.Element => { } ); + const costSurfaceQuery = useProjectCostSurface(pid, selectedCostSurface); + const selectedFeaturesData = useMemo(() => { return allFeaturesQuery.data?.data.filter((f) => selectedFeaturesIds?.includes(f.id)); }, [selectedFeaturesIds, allFeaturesQuery.data?.data]); @@ -134,7 +138,7 @@ export const ProjectMap = (): JSX.Element => { const PUGridLayer = usePUGridLayer({ active: rawScenariosIsFetched && rawScenariosData && !!rawScenariosData.length && !sid2, sid: sid ? `${sid}` : null, - include: 'results,cost', + include: 'results', sublayers: [ ...(sid1 && !sid2 ? ['frequency'] : []), ...(!!selectedCostSurface ? ['cost'] : []), @@ -145,12 +149,22 @@ export const ProjectMap = (): JSX.Element => { pugrid: layerSettings.pugrid, 'wdpa-percentage': layerSettings['wdpa-percentage'], features: layerSettings.features, - 'cost-surface': layerSettings[selectedCostSurface], frequency: layerSettings.frequency, }, }, }); + const costSurfaceLayer = useCostSurfaceLayer({ + active: Boolean(selectedCostSurface) && costSurfaceQuery.isSuccess, + pid, + costSurfaceId: selectedCostSurface, + layerSettings: { + ...layerSettings[selectedCostSurface], + min: costSurfaceQuery.data?.min, + max: costSurfaceQuery.data?.max, + } as Parameters[0]['layerSettings'], + }); + const PUCompareLayer = usePUCompareLayer({ active: isComparisonEnabled, sid: sid1, @@ -192,6 +206,7 @@ export const ProjectMap = (): JSX.Element => { const LAYERS = [ PUGridLayer, + costSurfaceLayer, PUCompareLayer, PlanningAreaLayer, WDPAsPreviewLayers, diff --git a/app/layout/projects/show/map/legend/hooks/index.ts b/app/layout/projects/show/map/legend/hooks/index.ts index 39b91235c6..98b8d0dabf 100644 --- a/app/layout/projects/show/map/legend/hooks/index.ts +++ b/app/layout/projects/show/map/legend/hooks/index.ts @@ -5,6 +5,7 @@ import { setSelectedFeatures, setLayerSettings, setSelectedWDPAs as setVisibleWDPAs, + setSelectedCostSurfaces as setVisibleCostSurface, } from 'store/slices/projects/[id]'; import chroma from 'chroma-js'; @@ -15,6 +16,7 @@ import { COLORS, LEGEND_LAYERS } from 'hooks/map/constants'; import { useScenario } from 'hooks/scenarios'; import { useProjectWDPAs } from 'hooks/wdpa'; +import { CostSurface } from 'types/api/cost-surface'; import { Feature } from 'types/api/feature'; import { Scenario } from 'types/api/scenario'; import { WDPA } from 'types/api/wdpa'; @@ -44,27 +46,31 @@ export const useCostSurfaceLegend = () => { const dispatch = useAppDispatch(); const { layerSettings, selectedCostSurface } = useAppSelector((state) => state['/projects/[id]']); - const costSurfaceQuery = useProjectCostSurfaces( - pid, - {}, - { - select: (data) => data.map(({ id, name }) => ({ id, name })), - } - ); + const costSurfaceQuery = useProjectCostSurfaces(pid, {}); + + const costSurfaceIds = costSurfaceQuery.data?.map((cs) => cs.id); if (!costSurfaceQuery.data?.length) return []; return LEGEND_LAYERS['cost-surface']({ items: costSurfaceQuery.data, - onChangeVisibility: () => { - dispatch( - setLayerSettings({ - id: selectedCostSurface, - settings: { - visibility: !layerSettings[selectedCostSurface]?.visibility, - }, - }) - ); + onChangeVisibility: (costSurfaceId: CostSurface['id']) => { + costSurfaceIds.forEach((id) => { + dispatch( + setLayerSettings({ + id, + settings: { + visibility: id !== costSurfaceId ? false : !layerSettings[costSurfaceId]?.visibility, + }, + }) + ); + }); + + if (costSurfaceId === selectedCostSurface) { + dispatch(setVisibleCostSurface(null)); + } else { + dispatch(setVisibleCostSurface(costSurfaceId)); + } }, }); }; diff --git a/app/layout/scenarios/edit/map/legend/hooks/index.ts b/app/layout/scenarios/edit/map/legend/hooks/index.ts index a8f25064b0..de400b6003 100644 --- a/app/layout/scenarios/edit/map/legend/hooks/index.ts +++ b/app/layout/scenarios/edit/map/legend/hooks/index.ts @@ -14,6 +14,7 @@ import { useProject } from 'hooks/projects'; import { useScenario } from 'hooks/scenarios'; import { useWDPACategories } from 'hooks/wdpa'; +import { CostSurface } from 'types/api/cost-surface'; import { Feature } from 'types/api/feature'; import { WDPA } from 'types/api/wdpa'; @@ -55,8 +56,7 @@ export const useCostSurfaceLegend = () => { pid, {}, { - select: (data) => - data.filter(({ id }) => selectedCostSurface === id).map(({ id, name }) => ({ id, name })), + select: (data) => data.filter(({ id }) => selectedCostSurface === id), } ); @@ -64,12 +64,12 @@ export const useCostSurfaceLegend = () => { return LEGEND_LAYERS['cost-surface']({ items: costSurfaceQuery.data, - onChangeVisibility: () => { + onChangeVisibility: (costSurfaceId: CostSurface['id']) => { dispatch( setLayerSettings({ - id: selectedCostSurface, + id: costSurfaceId, settings: { - visibility: !layerSettings[selectedCostSurface]?.visibility, + visibility: !layerSettings[costSurfaceId]?.visibility, }, }) ); diff --git a/app/package.json b/app/package.json index bf3d6e0d12..5e2e7acc67 100644 --- a/app/package.json +++ b/app/package.json @@ -97,6 +97,7 @@ "@types/chroma-js": "2.4.1", "@types/d3": "^6.3.0", "@types/lodash": "4.14.192", + "@types/mapbox-gl": "2.7.15", "@types/node": "18.15.11", "@types/react": "18.0.32", "@types/react-map-gl": "6.1.3", diff --git a/app/types/api/cost-surface.ts b/app/types/api/cost-surface.ts index 7390ea12ba..30a5db772e 100644 --- a/app/types/api/cost-surface.ts +++ b/app/types/api/cost-surface.ts @@ -1,6 +1,8 @@ export interface CostSurface { id: string; name: string; - isCustom: boolean; + isDefault: boolean; + min: number; + max: number; scenarioUsageCount: number; } diff --git a/app/yarn.lock b/app/yarn.lock index 77e0f6cc6d..b638d528ef 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -3541,6 +3541,15 @@ __metadata: languageName: node linkType: hard +"@types/mapbox-gl@npm:2.7.15": + version: 2.7.15 + resolution: "@types/mapbox-gl@npm:2.7.15" + dependencies: + "@types/geojson": "*" + checksum: 39d5ef3341d2cbc6bf254c08e14ac2a09c7e6192cf18c838a0b7ce7ca6db195376f5c0357edd1b7f2afe132a0ba5bd262842b8274c4cd091a413057221e4322b + languageName: node + linkType: hard + "@types/mapbox-gl@npm:^2.0.3": version: 2.1.2 resolution: "@types/mapbox-gl@npm:2.1.2" @@ -4126,6 +4135,7 @@ __metadata: "@types/chroma-js": 2.4.1 "@types/d3": ^6.3.0 "@types/lodash": 4.14.192 + "@types/mapbox-gl": 2.7.15 "@types/node": 18.15.11 "@types/react": 18.0.32 "@types/react-map-gl": 6.1.3 @@ -4720,9 +4730,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001157, caniuse-lite@npm:^1.0.30001406": - version: 1.0.30001473 - resolution: "caniuse-lite@npm:1.0.30001473" - checksum: 007ad17463612d38080fc59b5fa115ccb1016a1aff8daab92199a7cf8eb91cf987e85e7015cb0bca830ee2ef45f252a016c29a98a6497b334cceb038526b73f1 + version: 1.0.30001546 + resolution: "caniuse-lite@npm:1.0.30001546" + checksum: d3ef82f5ee94743002c5b2dd61c84342debcc94b2d5907b64ade3514ecfc4f20bbe86a6bc453fd6436d5fbcf6582e07405d7c2077565675a71c83adc238a11fa languageName: node linkType: hard