diff --git a/backend/backend/app/routers/features.py b/backend/backend/app/routers/features.py index 0f1835de..b8b6cede 100644 --- a/backend/backend/app/routers/features.py +++ b/backend/backend/app/routers/features.py @@ -78,3 +78,52 @@ def read_sorted_features( ).order_by(desc("value")) return paginate(session, q, page_params) + + +@router.get( + "/{protector_id}/protected-by", + response_model=list[schemas.ProtectedFeatureListItem], +) +def read_protected_features( + protector_id: int, + session: SessionDep, +): + """ + Get all adaptation options, by feature ID and layer, for features + protected by a given protector feature. + """ + + adaptation_options = select( + models.Feature.id.label("id"), + models.Feature.string_id.label("string_id"), + models.Feature.layer.label("layer"), + models.AdaptationCostBenefit.adaptation_cost.label( + "adaptation_cost" + ), + models.AdaptationCostBenefit.adaptation_protection_level.label( + "adaptation_protection_level" + ), + models.AdaptationCostBenefit.adaptation_name.label( + "adaptation_name" + ), + models.AdaptationCostBenefit.avoided_ead_mean.label( + "avoided_ead_mean" + ), + models.AdaptationCostBenefit.avoided_eael_mean.label( + "avoided_eael_mean" + ), + models.AdaptationCostBenefit.rcp.label("rcp"), + models.AdaptationCostBenefit.hazard.label("hazard"), + ).select_from( + models.Feature + ).join( + models.FeatureLayer + ).join( + models.Feature.adaptation + ).filter( + models.AdaptationCostBenefit.adaptation_name == + "Flood defence around asset" # test query + # models.AdaptationCostBenefit.protector_feature_id == protector_id + ) + print(adaptation_options) + return session.execute(adaptation_options) diff --git a/backend/backend/app/schemas.py b/backend/backend/app/schemas.py index 01290503..81f72804 100644 --- a/backend/backend/app/schemas.py +++ b/backend/backend/app/schemas.py @@ -175,3 +175,20 @@ class FeatureListItemOut(BaseModel, Generic[SortFieldT]): AttributeT = TypeVar("AttributeT") AttributeLookup = dict[int, AttributeT] + +# Protected Features + + +class ProtectedFeatureListItem(BaseModel): + id: int + string_id: str + layer: str + adaptation_name: str + adaptation_protection_level: float + adaptation_cost: float + avoided_ead_mean: float + avoided_eael_mean: float + hazard: str + rcp: float + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/backend/db/models.py b/backend/backend/db/models.py index 4833c59f..c7e06bbe 100644 --- a/backend/backend/db/models.py +++ b/backend/backend/db/models.py @@ -113,6 +113,11 @@ class AdaptationCostBenefit(Base): primary_key=True, index=True ) + protector_feature_id = Column( + Integer, + primary_key=True, + index=True + ) hazard = Column(String(8), nullable=False, primary_key=True) rcp = Column(String(8), nullable=False, primary_key=True) diff --git a/frontend/src/lib/api-client/services/FeaturesService.ts b/frontend/src/lib/api-client/services/FeaturesService.ts index 0c9040df..bc3341d9 100644 --- a/frontend/src/lib/api-client/services/FeaturesService.ts +++ b/frontend/src/lib/api-client/services/FeaturesService.ts @@ -84,4 +84,26 @@ export class FeaturesService { }); } + /** + * Read Protected Features + * @returns any Successful Response + * @throws ApiError + */ + public featuresReadProtectedFeatures({ + protectorId, + }: { + protectorId: number, + }): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/features/{protector_id}/protected-by', + path: { + 'protector_id': protectorId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx index bce90058..4478d4de 100644 --- a/frontend/src/lib/data-map/DataMap.tsx +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -3,6 +3,12 @@ import { useMap } from 'react-map-gl/maplibre'; import { FC, useRef } from 'react'; import { useRecoilValue } from 'recoil'; +import { + protectedFeatureDetailsState, + protectedFeatureProtectionLevelState, + protectedFeatureRCPState, + protectedFeatureDetailsQuery, +} from 'lib/state/interactions/interaction-state'; import { useInteractions } from 'lib/state/interactions/use-interactions'; import { useDataLoadTrigger } from 'lib/data-map/use-data-load-trigger'; import { InteractionGroupConfig } from 'lib/data-map/types'; @@ -74,6 +80,19 @@ export const DataMap: FC<{ const viewLayersParams = useRecoilValue(viewLayersParamsState); const saveViewLayers = useSaveViewLayers(); + const protectedFeatures = useRecoilValue(protectedFeatureDetailsState); + const protectedFeatureProtectionLevel = useRecoilValue(protectedFeatureProtectionLevelState); + const protectedFeatureRCP = useRecoilValue(protectedFeatureRCPState); + const protectedFeatureDetails = useRecoilValue( + protectedFeatureDetailsQuery({ rcp: 2.6, protectionLevel: 1 }), + ); + console.log({ + protectedFeatures, + protectedFeatureProtectionLevel, + protectedFeatureRCP, + protectedFeatureDetails, + }); + useTrigger(viewLayers); const { onHover, onClick, layerFilter, pickingRadius } = useInteractions( diff --git a/frontend/src/lib/state/interactions/interaction-state.ts b/frontend/src/lib/state/interactions/interaction-state.ts index 6a3c77bb..dcd7f4f3 100644 --- a/frontend/src/lib/state/interactions/interaction-state.ts +++ b/frontend/src/lib/state/interactions/interaction-state.ts @@ -1,7 +1,7 @@ import forEach from 'lodash/forEach'; import { atom, atomFamily, selector, selectorFamily } from 'recoil'; -import { InteractionLayer } from 'lib/data-map/types'; +import { InteractionLayer, VectorTarget } from 'lib/data-map/types'; import { isReset } from 'lib/recoil/is-reset'; import { ApiClient } from 'lib/api-client'; @@ -90,6 +90,59 @@ export const selectedAssetDetails = selectorFamily({ }, }); +/** + * Fetch a list of adaptation options, by feature ID and layer, + * for features protected by the current selected feature. + */ +export const protectedFeatureDetailsState = selector({ + key: 'protectedFeatureDetails', + get: async ({ get }) => { + const selection = get(selectionState('assets')); + const target = selection?.target as VectorTarget; + if (!target?.feature?.id) { + return null; + } + const featureDetails = await apiClient.features.featuresReadProtectedFeatures({ + protectorId: target.feature.id, + }); + return featureDetails; + }, +}); + +/** + * A set of unique RCP values for the protected feature list. + */ +export const protectedFeatureRCPState = selector({ + key: 'protectedFeatureRCP', + get: ({ get }) => new Set(get(protectedFeatureDetailsState)?.map((feature) => feature.rcp)), +}); + +/** + * A set of unique protection levels for the protected feature list. + */ +export const protectedFeatureProtectionLevelState = selector({ + key: 'protectedFeatureProtectionLevel', + get: ({ get }) => + new Set( + get(protectedFeatureDetailsState)?.map((feature) => feature.adaptation_protection_level), + ), +}); + +type ProtectedFeatureDetailsQuery = { rcp: number; protectionLevel: number }; +/** + * A list of adaptation options, by feature ID and layer, + * for a specific RCP and protection level. + */ +export const protectedFeatureDetailsQuery = selectorFamily({ + key: 'protectedFeatureDetailsQuery', + get: + ({ rcp = 2.6, protectionLevel = 1 }: ProtectedFeatureDetailsQuery) => + ({ get }) => + get(protectedFeatureDetailsState)?.filter( + (item) => item.rcp === rcp && item.adaptation_protection_level === protectionLevel, + ), +}); + type AllowedGroupLayers = Record; const allowedGroupLayersImpl = atom({