Skip to content

Commit 1c2037c

Browse files
committed
feat: protected feature API
- a new API, `/features/{protector-feature-id}/protected-by`, which returns list of adaptation options, by feature and layer, for all features protected by `protector-feature-id`. - a new recoil selector, which runs the query with the current selected feature ID. - new recoil selectors that filter the results by RCP and protection level.
1 parent 4f41593 commit 1c2037c

File tree

6 files changed

+150
-2
lines changed

6 files changed

+150
-2
lines changed

backend/backend/app/routers/features.py

+43
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,46 @@ def read_sorted_features(
8181
).order_by(desc("value"))
8282

8383
return paginate(q, page_params)
84+
85+
86+
@router.get(
87+
"/{protector_id}/protected-by",
88+
response_model=list[schemas.ProtectedFeatureListItem],
89+
)
90+
def read_protected_features(
91+
protector_id: int,
92+
db: Session = Depends(get_db),
93+
):
94+
adaptation_options = db.query(
95+
models.Feature.id.label("id"),
96+
models.Feature.string_id.label("string_id"),
97+
models.Feature.layer.label("layer"),
98+
models.AdaptationCostBenefit.adaptation_cost.label(
99+
"adaptation_cost"
100+
),
101+
models.AdaptationCostBenefit.adaptation_protection_level.label(
102+
"adaptation_protection_level"
103+
),
104+
models.AdaptationCostBenefit.adaptation_name.label(
105+
"adaptation_name"
106+
),
107+
models.AdaptationCostBenefit.avoided_ead_mean.label(
108+
"avoided_ead_mean"
109+
),
110+
models.AdaptationCostBenefit.avoided_eael_mean.label(
111+
"avoided_eael_mean"
112+
),
113+
models.AdaptationCostBenefit.rcp.label("rcp"),
114+
models.AdaptationCostBenefit.hazard.label("hazard"),
115+
).select_from(
116+
models.Feature
117+
).join(
118+
models.FeatureLayer
119+
).join(
120+
models.Feature.adaptation
121+
).filter(
122+
models.AdaptationCostBenefit.adaptation_name == "Flood defence around asset" # test query
123+
# models.AdaptationCostBenefit.protector_feature_id == protector_id
124+
)
125+
print(adaptation_options)
126+
return adaptation_options.all()

backend/backend/app/schemas.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ class ReturnPeriodDamagesVariables(DataVariables):
6969
loss_amax: float
7070

7171

72-
class ReturnPeriodDamage(ReturnPeriodDamagesDimensions, ReturnPeriodDamagesVariables):
72+
class ReturnPeriodDamage(
73+
ReturnPeriodDamagesDimensions,
74+
ReturnPeriodDamagesVariables
75+
):
7376
model_config = ConfigDict(from_attributes=True)
7477

7578

@@ -172,3 +175,20 @@ class FeatureListItemOut(BaseModel, Generic[SortFieldT]):
172175
AttributeT = TypeVar("AttributeT")
173176

174177
AttributeLookup = dict[int, AttributeT]
178+
179+
# Protected Features
180+
181+
182+
class ProtectedFeatureListItem(BaseModel):
183+
id: int
184+
string_id: str
185+
layer: str
186+
adaptation_name: str
187+
adaptation_protection_level: float
188+
adaptation_cost: float
189+
avoided_ead_mean: float
190+
avoided_eael_mean: float
191+
hazard: str
192+
rcp: float
193+
194+
model_config = ConfigDict(from_attributes=True)

backend/backend/db/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ class AdaptationCostBenefit(Base):
113113
primary_key=True,
114114
index=True
115115
)
116+
protector_feature_id = Column(
117+
Integer,
118+
primary_key=True,
119+
index=True
120+
)
116121

117122
hazard = Column(String(8), nullable=False, primary_key=True)
118123
rcp = Column(String(8), nullable=False, primary_key=True)

frontend/src/lib/api-client/services/FeaturesService.ts

+22
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,26 @@ export class FeaturesService {
8484
});
8585
}
8686

87+
/**
88+
* Read Protected Features
89+
* @returns any Successful Response
90+
* @throws ApiError
91+
*/
92+
public featuresReadProtectedFeatures({
93+
protectorId,
94+
}: {
95+
protectorId: number,
96+
}): CancelablePromise<any> {
97+
return this.httpRequest.request({
98+
method: 'GET',
99+
url: '/features/{protector_id}/protected-by',
100+
path: {
101+
'protector_id': protectorId,
102+
},
103+
errors: {
104+
422: `Validation Error`,
105+
},
106+
});
107+
}
108+
87109
}

frontend/src/lib/data-map/DataMap.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { useMap } from 'react-map-gl/maplibre';
33
import { FC, useRef } from 'react';
44
import { useRecoilValue } from 'recoil';
55

6+
import {
7+
protectedFeatureDetailsState,
8+
protectedFeatureProtectionLevelState,
9+
protectedFeatureRCPState,
10+
protectedFeatureDetailsQuery,
11+
} from 'lib/state/interactions/interaction-state';
612
import { useInteractions } from 'lib/state/interactions/use-interactions';
713
import { useDataLoadTrigger } from 'lib/data-map/use-data-load-trigger';
814
import { InteractionGroupConfig } from 'lib/data-map/types';
@@ -71,6 +77,19 @@ export const DataMap: FC<{
7177
const viewLayersParams = useRecoilValue(viewLayersParamsState);
7278
const saveViewLayers = useSaveViewLayers();
7379

80+
const protectedFeatures = useRecoilValue(protectedFeatureDetailsState);
81+
const protectedFeatureProtectionLevel = useRecoilValue(protectedFeatureProtectionLevelState);
82+
const protectedFeatureRCP = useRecoilValue(protectedFeatureRCPState);
83+
const protectedFeatureDetails = useRecoilValue(
84+
protectedFeatureDetailsQuery({ rcp: 2.6, protectionLevel: 1 }),
85+
);
86+
console.log({
87+
protectedFeatures,
88+
protectedFeatureProtectionLevel,
89+
protectedFeatureRCP,
90+
protectedFeatureDetails,
91+
});
92+
7493
useTrigger(viewLayers);
7594

7695
const { onHover, onClick, layerFilter, pickingRadius } = useInteractions(

frontend/src/lib/state/interactions/interaction-state.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import forEach from 'lodash/forEach';
22
import { atom, atomFamily, selector, selectorFamily } from 'recoil';
33

4-
import { InteractionLayer } from 'lib/data-map/types';
4+
import { InteractionLayer, VectorTarget } from 'lib/data-map/types';
55
import { isReset } from 'lib/recoil/is-reset';
66
import { ApiClient } from 'lib/api-client';
77

@@ -90,6 +90,45 @@ export const selectedAssetDetails = selectorFamily({
9090
},
9191
});
9292

93+
export const protectedFeatureDetailsState = selector({
94+
key: 'protectedFeatureDetails',
95+
get: async ({ get }) => {
96+
const selection = get(selectionState('assets'));
97+
const target = selection?.target as VectorTarget;
98+
if (!target?.feature?.id) {
99+
return null;
100+
}
101+
const featureDetails = await apiClient.features.featuresReadProtectedFeatures({
102+
protectorId: target.feature.id,
103+
});
104+
return featureDetails;
105+
},
106+
});
107+
108+
export const protectedFeatureRCPState = selector({
109+
key: 'protectedFeatureRCP',
110+
get: ({ get }) => new Set(get(protectedFeatureDetailsState)?.map((feature) => feature.rcp)),
111+
});
112+
113+
export const protectedFeatureProtectionLevelState = selector({
114+
key: 'protectedFeatureProtectionLevel',
115+
get: ({ get }) =>
116+
new Set(
117+
get(protectedFeatureDetailsState)?.map((feature) => feature.adaptation_protection_level),
118+
),
119+
});
120+
121+
type ProtectedFeatureDetailsQuery = { rcp: number; protectionLevel: number };
122+
export const protectedFeatureDetailsQuery = selectorFamily({
123+
key: 'protectedFeatureDetailsQuery',
124+
get:
125+
({ rcp, protectionLevel }: ProtectedFeatureDetailsQuery) =>
126+
({ get }) =>
127+
get(protectedFeatureDetailsState).filter(
128+
(item) => item.rcp === rcp && item.adaptation_protection_level === protectionLevel,
129+
),
130+
});
131+
93132
type AllowedGroupLayers = Record<string, string[]>;
94133

95134
const allowedGroupLayersImpl = atom<AllowedGroupLayers>({

0 commit comments

Comments
 (0)