From 5eb44f1c6d855e0cdca240ead7f6c6b1b31697b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Wed, 13 Dec 2023 17:39:28 +0100 Subject: [PATCH] splitted features on map --- app/hooks/features/index.ts | 46 +++++-- app/hooks/map/index.ts | 38 ++---- app/hooks/map/types.ts | 1 + .../inventory-panel/features/index.tsx | 15 +-- .../bulk-action-menu/modals/delete/index.tsx | 48 +++++-- .../grid-setup/features/target-spf/index.tsx | 125 +++++++----------- .../targets-spf-table/actions-menu/index.tsx | 2 +- .../targets-spf-table/row-item/index.tsx | 8 +- .../target-spf/targets-spf-table/types.ts | 3 +- app/layout/projects/show/map/index.tsx | 8 +- .../projects/show/map/legend/hooks/index.ts | 15 +-- app/layout/scenarios/edit/map/component.tsx | 22 ++- .../scenarios/edit/map/legend/hooks/index.ts | 92 ++++++++++--- 13 files changed, 241 insertions(+), 182 deletions(-) diff --git a/app/hooks/features/index.ts b/app/hooks/features/index.ts index 537f2f24ae..5f5527cbcc 100644 --- a/app/hooks/features/index.ts +++ b/app/hooks/features/index.ts @@ -2,13 +2,18 @@ import { useMemo } from 'react'; import { useQuery, useMutation, useQueryClient, QueryObserverOptions } from 'react-query'; +import { useRouter } from 'next/router'; + 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'; @@ -171,13 +176,17 @@ export function useSelectedFeatures( filters: UseFeaturesFiltersProps = {}, queryOptions = {} ) { + const { query } = useRouter(); + const { pid } = query as { pid: string }; const { data: session } = useSession(); - const { search, sort, tag } = filters; + const { search } = filters; const queryClient = useQueryClient(); - const featureColorQueryState = - queryClient.getQueryState<{ id: Feature['id']; color: string }[]>('feature-colors'); + // const featureColorQueryState = + // queryClient.getQueryState<{ id: Feature['id']; color: string }[]>('feature-colors'); + + const featureColors = useColorFeatures(pid, sid); const fetchFeatures = () => SCENARIOS.request({ @@ -193,8 +202,7 @@ export function useSelectedFeatures( return useQuery(['selected-features', sid], fetchFeatures, { ...queryOptions, - enabled: - !!sid && ((featureColorQueryState && featureColorQueryState.status === 'success') || true), + enabled: (!!sid && featureColors?.length > 0) || true, select: ({ data }) => { const { features = [] } = data; @@ -271,16 +279,12 @@ export function useSelectedFeatures( id: featureId, name: alias || featureClassName, type: tag, - // type: Math.random() < 0.5 ? 'test' : 'andres', description, amountRange: { min: amountMin, max: amountMax, }, - color: featureColorQueryState - ? featureColorQueryState?.data?.find(({ id }) => featureId === id)?.color - : null, - + color: featureColors.find(({ id }) => featureId === id)?.color, // SPLIT splitOptions, splitSelected, @@ -749,3 +753,25 @@ export function useProjectFeatures( } ); } + +export function useColorFeatures(projectId: Project['id'], sid: Scenario['id']) { + const useAllFeaturesQuery = useAllFeatures(projectId, {}); + const targetedFeaturesQuery = useTargetedFeatures(sid); + + if (targetedFeaturesQuery.isSuccess && useAllFeaturesQuery.isSuccess) { + const data = [...(useAllFeaturesQuery.data?.data || []), ...targetedFeaturesQuery.data]; + return data.map(({ id }, index) => { + const color = + data.length > COLORS['features-preview'].ramp.length + ? chroma.scale(COLORS['features-preview'].ramp).colors(data.length)[index] + : COLORS['features-preview'].ramp[index]; + + return { + id, + color, + }; + }); + } + + return []; +} diff --git a/app/hooks/map/index.ts b/app/hooks/map/index.ts index 6f8732ec24..de8e51eb78 100644 --- a/app/hooks/map/index.ts +++ b/app/hooks/map/index.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react'; -import chroma from 'chroma-js'; import { Layer } from 'mapbox-gl'; import { CostSurface } from 'types/api/cost-surface'; @@ -484,36 +483,25 @@ export function useTargetedPreviewLayers({ return useMemo(() => { if (!active || !bbox || !features) return []; - const { selectedFeatures = [] } = options; + const { layerSettings } = options; - const FEATURES = [...features] - .filter((ft) => selectedFeatures.includes(ft.id as string)) - .sort((a, b) => { - const aIndex = selectedFeatures.indexOf(a.id as string); - const bIndex = selectedFeatures.indexOf(b.id as string); - return bIndex - aIndex; - }); - - const { opacity = 1, visibility = true } = options || {}; + const FEATURES = features.filter((ft) => Object.keys(layerSettings).includes(ft.id)); - const getLayerVisibility = () => { + const getLayerVisibility = ( + visibility: UseTargetedPreviewLayers['options']['layerSettings'][string]['visibility'] + ) => { if (!visibility) { return 'none'; } return 'visible'; }; - return FEATURES.map((f, index) => { - const { id, parentId, splitted, value } = f; + return FEATURES.map((f) => { + const { id, parentId, value, splitSelected } = f; - const ID = splitted ? parentId : id; + const { opacity = 1, color } = 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 _id = splitSelected ? parentId : id; return { id: `feature-${id}-targeted-preview-layer-${cache}`, @@ -521,7 +509,7 @@ export function useTargetedPreviewLayers({ source: { type: 'vector', tiles: [ - `${process.env.NEXT_PUBLIC_API_URL}/api/v1/geo-features/${ID}/preview/tiles/{z}/{x}/{y}.mvt?bbox=[${bbox}]`, + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/geo-features/${_id}/preview/tiles/{z}/{x}/{y}.mvt?bbox=[${bbox}]`, ], }, render: { @@ -536,10 +524,10 @@ export function useTargetedPreviewLayers({ ], }), layout: { - visibility: getLayerVisibility(), + visibility: getLayerVisibility(layerSettings[id].visibility), }, paint: { - 'fill-color': COLOR, + 'fill-color': color, 'fill-opacity': opacity, }, }, @@ -553,7 +541,7 @@ export function useTargetedPreviewLayers({ ], }), layout: { - visibility: getLayerVisibility(), + visibility: getLayerVisibility(layerSettings[id].visibility), }, paint: { 'line-color': '#000', diff --git a/app/hooks/map/types.ts b/app/hooks/map/types.ts index ed3a762857..900cbd0fc4 100644 --- a/app/hooks/map/types.ts +++ b/app/hooks/map/types.ts @@ -105,6 +105,7 @@ export interface UseTargetedPreviewLayers { selectedFeatures?: Array; opacity?: number; visibility?: boolean; + layerSettings?: Record; }; } 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 9c9278c850..b73dc1f8c8 100644 --- a/app/layout/project/sidebar/project/inventory-panel/features/index.tsx +++ b/app/layout/project/sidebar/project/inventory-panel/features/index.tsx @@ -1,7 +1,5 @@ import { useCallback, useState, ChangeEvent, useEffect } from 'react'; -import { useQueryClient } from 'react-query'; - import { useRouter } from 'next/router'; import { useAppDispatch, useAppSelector } from 'store/hooks'; @@ -11,7 +9,7 @@ import { setLayerSettings, } from 'store/slices/projects/[id]'; -import { useAllFeatures } from 'hooks/features'; +import { useAllFeatures, useColorFeatures } from 'hooks/features'; import ActionsMenu from 'layout/project/sidebar/project/inventory-panel/features/actions-menu'; import FeaturesBulkActionMenu from 'layout/project/sidebar/project/inventory-panel/features/bulk-action-menu'; @@ -46,12 +44,9 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): }); const [selectedFeaturesIds, setSelectedFeaturesIds] = useState([]); const { query } = useRouter(); - const { pid } = query as { pid: string }; - - const queryClient = useQueryClient(); + const { pid, sid } = query as { pid: string; sid: string }; - const featureColorQueryState = - queryClient.getQueryState<{ id: Feature['id']; color: string }[]>('feature-colors'); + const featureColors = useColorFeatures(pid, sid); const allFeaturesQuery = useAllFeatures( pid, @@ -68,7 +63,7 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): { select: ({ data }) => { return data?.map((feature) => { - const { color } = featureColorQueryState?.data?.find(({ id }) => feature.id === id) || {}; + const { color } = featureColors?.find(({ id }) => feature.id === id) || {}; return { id: feature.id, @@ -83,7 +78,7 @@ const InventoryPanelFeatures = ({ noData: noDataMessage }: { noData: string }): }, placeholderData: { data: [] }, keepPreviousData: true, - enabled: featureColorQueryState?.status === 'success', + enabled: featureColors.length > 0, } ); diff --git a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/bulk-action-menu/modals/delete/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/bulk-action-menu/modals/delete/index.tsx index d3b876af8e..b19737567f 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/bulk-action-menu/modals/delete/index.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/bulk-action-menu/modals/delete/index.tsx @@ -16,7 +16,7 @@ const DeleteModal = ({ onDismiss, onDone, }: { - features: (Feature & { name: string })[]; + features: any[]; selectedFeaturesIds: Feature['id'][]; onDismiss?: ModalProps['onDismiss']; onDone?: () => void; @@ -28,15 +28,15 @@ const DeleteModal = ({ const selectedFeaturesQuery = useSelectedFeatures(sid); - const selectedFeatures = useMemo( + const featuresToRemove = useMemo( () => features.filter(({ id }) => selectedFeaturesIds.includes(id)) ?? [], [features, selectedFeaturesIds] ); - const featureNames = selectedFeatures.map(({ name }) => name); + const featureNames = featuresToRemove.map(({ name }) => name); const handleBulkDelete = useCallback(() => { - const deletableFeatureIds = selectedFeatures.map(({ id }) => id); + const deletableFeatureIds = featuresToRemove.map(({ id }) => id); selectedFeaturesMutation.mutate( { @@ -44,7 +44,9 @@ const DeleteModal = ({ data: { status: 'draft', features: selectedFeaturesQuery.data - .filter(({ id: featureId }) => !deletableFeatureIds.includes(featureId)) + .filter(({ id: featureId }) => { + return !deletableFeatureIds.includes(featureId); + }) .map( ({ metadata, @@ -55,30 +57,54 @@ const DeleteModal = ({ color, splitOptions, splitFeaturesSelected, + geoprocessingOperations, splitFeaturesOptions, intersectFeaturesSelected, + splitSelected, ...sf - }) => ({ - ...sf, - }) + }) => { + if (splitSelected) { + const featureValues = features + .filter(({ id }) => deletableFeatureIds.includes(id)) + .map(({ value }) => value); + + return { + ...sf, + ...(geoprocessingOperations && { + geoprocessingOperations: geoprocessingOperations.map((go) => ({ + ...go, + splits: go.splits.filter((s) => { + return !featureValues.includes(s.value); + }), + })), + }), + }; + } + + return { + ...sf, + geoprocessingOperations, + }; + } ), }, }, { onSuccess: async () => { await queryClient.invalidateQueries(['selected-features', sid]); + await queryClient.invalidateQueries(['targeted-features', sid]); onDone?.(); - onDismiss(); }, } ); }, [ - selectedFeatures, - onDismiss, queryClient, sid, selectedFeaturesMutation, + features, selectedFeaturesQuery.data, + onDone, + featuresToRemove, ]); return ( diff --git a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/index.tsx index e7b0ec7081..a171b159f0 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/index.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/index.tsx @@ -68,11 +68,8 @@ const TargetAndSPFFeatures = (): JSX.Element => { const dispatch = useAppDispatch(); const scenarioSlice = getScenarioEditSlice(sid); - const { setSelectedFeatures, setSelectedContinuousFeatures, setLayerSettings } = - scenarioSlice.actions; - const { selectedFeatures, selectedContinuousFeatures } = useAppSelector( - (state) => state[`/scenarios/${sid}/edit`] - ); + const { setLayerSettings } = scenarioSlice.actions; + const { layerSettings } = useAppSelector((state) => state[`/scenarios/${sid}/edit`]); const allFeaturesQuery = useAllFeatures( pid, @@ -86,7 +83,7 @@ const TargetAndSPFFeatures = (): JSX.Element => { keepPreviousData: true, }); - const targedtedFeatures = useMemo(() => { + const targetedFeatures = useMemo(() => { let parsedData = []; selectedFeaturesQuery.data?.forEach((feature) => { if (feature.splitFeaturesSelected?.length > 0) { @@ -94,10 +91,10 @@ const TargetAndSPFFeatures = (): JSX.Element => { const splitFeatures = feature.splitFeaturesSelected.map((splitFeature) => ({ ...splitFeature, - id: feature.id, + id: `${feature.id}-${splitFeature.name}`, parentId: feature.id, name: `${feature.name} / ${splitFeature.name}`, - isVisibleOnMap: [...selectedFeatures, ...selectedContinuousFeatures].includes(feature.id), + isVisibleOnMap: layerSettings[`${feature.id}-${splitFeature.name}`]?.visibility ?? false, color: feature.color, amountRange: feature.amountRange, isCustom: feature.metadata?.isCustom, @@ -124,9 +121,7 @@ const TargetAndSPFFeatures = (): JSX.Element => { ...parsedData, { ...feature, - isVisibleOnMap: [...selectedFeatures, ...selectedContinuousFeatures].includes( - feature.id - ), + isVisibleOnMap: layerSettings[feature.id]?.visibility ?? false, isCustom: feature.metadata?.isCustom, scenarioUsageCount: featureMetadata?.scenarioUsageCount, type: featureMetadata?.tag, @@ -166,14 +161,7 @@ const TargetAndSPFFeatures = (): JSX.Element => { } return parsedData; - }, [ - selectedFeaturesQuery.data, - allFeaturesQuery.data, - filters, - featureValues, - selectedContinuousFeatures, - selectedFeatures, - ]); + }, [selectedFeaturesQuery.data, allFeaturesQuery.data, filters, featureValues, layerSettings]); const handleSearch = useDebouncedCallback( (value: Parameters['onChange']>[0]) => { @@ -219,42 +207,15 @@ const TargetAndSPFFeatures = (): JSX.Element => { const toggleSeeOnMap = useCallback( (id: Feature['id']) => { - const binaryFeatures = [...selectedFeatures]; - const continuousFeatures = [...selectedContinuousFeatures]; - - const selectedFeature = targedtedFeatures.find(({ id: featureId }) => featureId === id); + const selectedFeature = targetedFeatures.find(({ id: featureId }) => featureId === id); const isContinuous = selectedFeature.amountRange.min !== null && selectedFeature.amountRange.max !== null; - const _id = selectedFeature.splitted ? selectedFeature.parentId : selectedFeature.id; - - const isIncludedInBinary = binaryFeatures.includes(_id); - const isIncludedInContinuous = continuousFeatures.includes(_id); - - if (isContinuous) { - if (!isIncludedInContinuous) { - continuousFeatures.push(_id); - } else { - const i = continuousFeatures.indexOf(_id); - continuousFeatures.splice(i, 1); - } - - dispatch(setSelectedContinuousFeatures(continuousFeatures)); - } else { - if (!isIncludedInBinary) { - binaryFeatures.push(_id); - } else { - const i = binaryFeatures.indexOf(_id); - binaryFeatures.splice(i, 1); - } - dispatch(setSelectedFeatures(binaryFeatures)); - } - dispatch( setLayerSettings({ - id: _id, + id, settings: { - visibility: !(isIncludedInBinary || isIncludedInContinuous), + visibility: layerSettings[id] ? !layerSettings[id].visibility : true, color: selectedFeature?.color, ...(isContinuous && { amountRange: selectedFeature.amountRange, @@ -263,20 +224,12 @@ const TargetAndSPFFeatures = (): JSX.Element => { }) ); }, - [ - dispatch, - setSelectedFeatures, - setLayerSettings, - selectedFeatures, - selectedContinuousFeatures, - setSelectedContinuousFeatures, - targedtedFeatures, - ] + [dispatch, setLayerSettings, targetedFeatures, layerSettings] ); const onApplyAllTargets = useCallback(() => { setFeatureValues((prevValues) => { - const ids = targedtedFeatures.map(({ id }) => id); + const ids = targetedFeatures.map(({ id }) => id); return ids.reduce( (acc, featureId) => ({ @@ -290,11 +243,11 @@ const TargetAndSPFFeatures = (): JSX.Element => { ); }); setConfirmationTarget(null); - }, [confirmationTarget, targedtedFeatures]); + }, [confirmationTarget, targetedFeatures]); const onApplyAllSPF = useCallback(() => { setFeatureValues((prevValues) => { - const ids = targedtedFeatures.map(({ id }) => id); + const ids = targetedFeatures.map(({ id }) => id); return ids.reduce( (acc, featureId) => ({ @@ -308,17 +261,17 @@ const TargetAndSPFFeatures = (): JSX.Element => { ); }); setConfirmationFPF(null); - }, [confirmationFPF, targedtedFeatures]); + }, [confirmationFPF, targetedFeatures]); const handleSelectAllFeatures = useCallback( (evt: ChangeEvent) => { if (evt.target.checked) { - setSelectedFeatureIds(targedtedFeatures.map(({ id }) => id)); + setSelectedFeatureIds(targetedFeatures.map(({ id }) => id)); } else { setSelectedFeatureIds([]); } }, - [targedtedFeatures] + [targetedFeatures] ); const handleSelectFeature = useCallback((evt: ChangeEvent) => { @@ -348,14 +301,14 @@ const TargetAndSPFFeatures = (): JSX.Element => { ...go, splits: splits .filter((s) => { - return targedtedFeatures.find((f) => { + return targetedFeatures.find((f) => { return f.parentId === featureId && f.value === s.value; }); }) .map((s) => { const { marxanSettings: { prop, fpf }, - } = targedtedFeatures.find((f) => { + } = targetedFeatures.find((f) => { return f.parentId === featureId && f.value === s.value; }); @@ -401,7 +354,7 @@ const TargetAndSPFFeatures = (): JSX.Element => { selectedFeaturesMutation, featureValues, selectedFeaturesQuery.data, - targedtedFeatures, + targetedFeatures, ]); const handleRowValues = useCallback((id, values) => { @@ -415,14 +368,17 @@ const TargetAndSPFFeatures = (): JSX.Element => { }, []); const handleRowDeletion = useCallback( - (id) => { + (featureToRemove) => { selectedFeaturesMutation.mutate( { - id: `${sid}`, + id: sid, data: { status: 'draft', features: selectedFeaturesQuery.data - .filter(({ id: featureId }) => featureId !== id) + .filter(({ id: featureId }) => { + if (!featureToRemove.splitted) return featureId !== featureToRemove.id; + return true; + }) .map( ({ metadata, @@ -433,19 +389,38 @@ const TargetAndSPFFeatures = (): JSX.Element => { color, splitOptions, splitFeaturesSelected, + geoprocessingOperations, splitFeaturesOptions, intersectFeaturesSelected, splitSelected, ...sf - }) => ({ - ...sf, - }) + }) => { + if (featureToRemove.splitted) { + return { + ...sf, + ...(geoprocessingOperations && { + geoprocessingOperations: geoprocessingOperations.map((go) => ({ + ...go, + splits: go.splits.filter((s) => { + return s.value !== featureToRemove.value; + }), + })), + }), + }; + } + + return { + ...sf, + geoprocessingOperations, + }; + } ), }, }, { onSuccess: async () => { await queryClient.invalidateQueries(['selected-features', sid]); + await queryClient.invalidateQueries(['targeted-features', sid]); }, } ); @@ -513,7 +488,7 @@ const TargetAndSPFFeatures = (): JSX.Element => {
{ )} {displayBulkActions && ( { setSelectedFeatureIds([]); diff --git a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/targets-spf-table/actions-menu/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/targets-spf-table/actions-menu/index.tsx index 4738a13c2f..4e4d98a885 100644 --- a/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/targets-spf-table/actions-menu/index.tsx +++ b/app/layout/project/sidebar/scenario/grid-setup/features/target-spf/targets-spf-table/actions-menu/index.tsx @@ -122,7 +122,7 @@ const ActionsMenu = ({