Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Floods Part 1: Adding backend + creating Gauge Points; COUNTRY=cambodia #1328

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c5c1215
Adding Google Floods Backend
Aug 8, 2024
b9722d3
Viewing the gauges on the frontend, adding error handling to backend
gislawill Aug 12, 2024
bc92d38
Update comment
gislawill Aug 12, 2024
08a4499
Updates to support tooltip content
gislawill Aug 12, 2024
0653f1e
copy update
gislawill Aug 12, 2024
7cd866d
Refactor title state management
gislawill Aug 12, 2024
94e1588
Fixing and adding server tests
gislawill Aug 12, 2024
6ac3aad
update set_env
ericboucher Aug 13, 2024
eb29b6e
Merge branch 'master' into feature/google-floods/1317
ericboucher Aug 13, 2024
6be26d7
update snapshots
ericboucher Aug 13, 2024
2fe481a
Merge branch 'master' into feature/google-floods/1317
gislawill Aug 15, 2024
8e84cbb
Adding support for multi country deployments and adding google flood …
gislawill Aug 21, 2024
efdf2c9
Use interpolation for popup titles
gislawill Aug 21, 2024
8fa327c
Adding pytest-recording
gislawill Aug 21, 2024
f5ffbf6
Merge branch 'master' into feature/google-floods/1317
ericboucher Aug 21, 2024
438ee04
Update test_google_floods_api.py
ericboucher Aug 21, 2024
e369704
Merge branch 'feature/google-floods/1317' of https://github.com/WFP-V…
ericboucher Aug 21, 2024
6547f3b
Fixing river template
gislawill Aug 22, 2024
ac58dca
More selective with pytest recording
gislawill Aug 22, 2024
65af7e7
Merge branch 'master' into feature/google-floods/1317
ericboucher Aug 22, 2024
0adb49d
Update API URL
ericboucher Aug 22, 2024
dc94f60
Merge branch 'master' into feature/google-floods/1317
ericboucher Sep 25, 2024
b0780fa
Merge branch 'master' into feature/google-floods/1317
gislawill Oct 7, 2024
7f401bb
Adding support for Cambodia
gislawill Oct 7, 2024
341a3a7
Merge branch 'master' into feature/google-floods/1317
gislawill Oct 7, 2024
1e51479
Merge branch 'master' into feature/google-floods/1317
gislawill Oct 8, 2024
2a890cf
Adding date support
gislawill Oct 9, 2024
6d33543
Google Floods Part 2: Adding tooltip graph; COUNTRY=cambodia (#1330)
gislawill Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions api/app/googleflood.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Get data from Google Floods API"""

import logging
from os import getenv
from urllib.parse import urlencode

import requests
from fastapi import HTTPException

logger = logging.getLogger(__name__)

GOOGLE_FLOODS_API_KEY = getenv("GOOGLE_FLOODS_API_KEY", "")
if GOOGLE_FLOODS_API_KEY == "":
logger.warning("Missing backend parameter: GOOGLE_FLOODS_API_KEY")


def format_gauge_to_geojson(data):
"""Format Gauge data to GeoJSON"""
geojson = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
data["gaugeLocation"]["longitude"],
data["gaugeLocation"]["latitude"],
],
},
"properties": {
"gaugeId": data["gaugeId"],
"issuedTime": data["issuedTime"],
"siteName": data["siteName"],
"river": (
gislawill marked this conversation as resolved.
Show resolved Hide resolved
data["river"] if "river" in data and len(data["river"]) > 1 else None
),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"river" can come in as a string with just 1 space (" "). This ensures there's a real value

"severity": data["severity"],
"source": data["source"],
"qualityVerified": data["qualityVerified"],
},
}
if "inundationMapSet" in data:
geojson["properties"]["inundationMapSet"] = data["inundationMapSet"]
return geojson


def get_google_floods_gauges(
iso2: str,
as_geojson: bool = True,
):
"""Get statistical charts data"""
# Retry 3 times due to intermittent API errors
flood_status_url = f"https://floodforecasting.googleapis.com/v1/floodStatus:searchLatestFloodStatusByArea?key={GOOGLE_FLOODS_API_KEY}"
for _ in range(3):
try:
status_response = requests.post(
flood_status_url, json={"regionCode": iso2}, timeout=2
).json()
break
except requests.RequestException as e:
logger.warning("Request failed at url %s: %s", flood_status_url, e)
status_response = {}

if "error" in status_response:
logger.error("Error in response: %s", status_response["error"])
raise HTTPException(
status_code=500, detail="Error fetching flood status data from Google API"
)

initial_gauges = status_response.get("floodStatuses", [])

gauge_params = urlencode(
{"names": [f"gauges/{gauge['gaugeId']}" for gauge in initial_gauges]},
doseq=True,
)
gauges_details_url = f"https://floodforecasting.googleapis.com/v1/gauges:batchGet?key={GOOGLE_FLOODS_API_KEY}&{gauge_params}"

try:
details_response = requests.get(gauges_details_url, timeout=2).json()
except requests.RequestException as e:
logger.warning("Request failed at url %s: %s", gauges_details_url, e)
details_response = {}

if "error" in details_response:
logger.error("Error in response: %s", details_response["error"])
raise HTTPException(
status_code=500, detail="Error fetching gauges details from Google API"
)

# Create a map for quick lookup
gauge_details_map = {
item["gaugeId"]: item for item in details_response.get("gauges", [])
}

gauges_details = []
for gauge in initial_gauges:
gauge_id = gauge["gaugeId"]
detail = gauge_details_map.get(gauge_id, {})
merged_gauge = {**gauge, **detail}
gauges_details.append(merged_gauge)

if as_geojson:
geojson_feature_collection = {
"type": "FeatureCollection",
"features": [format_gauge_to_geojson(gauge) for gauge in gauges_details],
}
return geojson_feature_collection
return gauges_details
12 changes: 12 additions & 0 deletions api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from app.database.alert_model import AlchemyEncoder, AlertModel
from app.database.database import AlertsDataBase
from app.database.user_info_model import UserInfoModel
from app.googleflood import get_google_floods_gauges
from app.hdc import get_hdc_stats
from app.kobo import get_form_dates, get_form_responses, parse_datetime_params
from app.models import AcledRequest, RasterGeotiffModel
Expand Down Expand Up @@ -412,3 +413,14 @@ def post_raster_geotiff(raster_geotiff: RasterGeotiffModel):
return JSONResponse(
content={"download_url": presigned_download_url}, status_code=200
)


@app.get("/google-floods/gauges/")
def get_google_floods_gauges_api(region_code: str):
if len(region_code) != 2:
raise HTTPException(
status_code=400,
detail="region code must be provided and exactly two characters (iso2).",
)
iso2 = region_code.upper()
return get_google_floods_gauges(iso2)
3 changes: 3 additions & 0 deletions api/app/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
log_cli = true
log_cli_level = INFO
56 changes: 56 additions & 0 deletions api/app/tests/test_google_floods_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from app.googleflood import get_google_floods_gauges
from app.main import app
from fastapi.testclient import TestClient

client = TestClient(app)


def test_get_google_floods_gauges():
"""
This test is not used in the API, but is used to test the get_google_floods_gauges function
It is used to ensure that the get_google_floods_gauges function returns a valid GeoJSON object
"""
gauges = get_google_floods_gauges("BD")
assert gauges["type"] == "FeatureCollection"
for feature in gauges["features"]:
assert feature["geometry"]["type"] == "Point"
assert len(feature["geometry"]["coordinates"]) == 2
assert "gaugeId" in feature["properties"]
assert "issuedTime" in feature["properties"]
assert "severity" in feature["properties"]


def test_get_google_floods_gauges_api():
"""
This test is used to test the API endpoint for getting Google Floods gauges
"""
response = client.get("/google-floods/gauges/?region_code=BD")
assert response.status_code == 200

response_geojson = response.json()
assert response_geojson["type"] == "FeatureCollection"
assert len(response_geojson["features"]) > 0


def test_get_google_floods_gauges_api_case_insensitive():
"""
This test is used to test the API endpoint for getting Google Floods gauges with a case insensitive region code
"""
response = client.get("/google-floods/gauges/?region_code=bd")
assert response.status_code == 200

response_geojson = response.json()
assert response_geojson["type"] == "FeatureCollection"
assert len(response_geojson["features"]) > 0


def test_get_google_floods_gauges_api_requires_valid_region_code():
"""
This test is used to test the API endpoint for getting Google Floods gauges with an invalid region code
"""
response = client.get("/google-floods/gauges/?region_code=usa")
assert response.status_code == 400
assert (
response.json()["detail"]
== "region code must be provided and exactly two characters (iso2)."
)
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const onClick =
dispatch(
addPopupData(
getFeatureInfoPropsData(
layer.featureInfoTitle || {},
layer.featureInfoProps || {},
coordinates,
feature,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/MapView/Layers/layer-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const addPopupParams = (

const {
dataField,
featureInfoTitle,
featureInfoProps,
title,
dataLabel,
Expand Down Expand Up @@ -153,6 +154,7 @@ export const addPopupParams = (
: {};

const featureInfoPropsData = getFeatureInfoPropsData(
featureInfoTitle,
featureInfoPropsWithFallback || {},
coordinates,
feature,
Expand Down
21 changes: 16 additions & 5 deletions frontend/src/components/MapView/MapTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { omit } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { Popup } from 'react-map-gl/maplibre';
import {
Expand All @@ -7,7 +8,13 @@ import {
IconButton,
makeStyles,
} from '@material-ui/core';
import { hidePopup, tooltipSelector } from 'context/tooltipStateSlice';
import {
hidePopup,
PopupData,
PopupMetaData,
PopupTitleData,
tooltipSelector,
} from 'context/tooltipStateSlice';
import { isEnglishLanguageSelected, useSafeTranslation } from 'i18n';
import { AdminLevelType } from 'config/types';
import { appConfig } from 'config';
Expand Down Expand Up @@ -80,15 +87,19 @@ const MapTooltip = memo(() => {
);

const { dataset, isLoading } = usePointDataChart();

const providedPopupTitle = (popup.data as PopupTitleData).title;
const popupData: PopupData & PopupMetaData = providedPopupTitle
? omit(popup.data, 'title', providedPopupTitle.prop)
: popup.data;
const defaultPopupTitle = useMemo(() => {
if (providedPopupTitle) {
return providedPopupTitle.data;
}
if (isEnglishLanguageSelected(i18n)) {
return popup.locationName;
}
return popup.locationLocalName;
}, [i18n, popup.locationLocalName, popup.locationName]);

const popupData = popup.data;
}, [i18n, popup.locationLocalName, popup.locationName, providedPopupTitle]);

// TODO - simplify logic once we revamp admin levels object
const adminLevelsNames = useCallback(() => {
Expand Down
59 changes: 49 additions & 10 deletions frontend/src/components/MapView/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { orderBy, snakeCase, values } from 'lodash';
import { TFunction } from 'i18next';
import { Dispatch } from 'redux';
import { LayerDefinitions } from 'config/utils';
import { formatFeatureInfo } from 'utils/server-utils';
import { formatFeatureInfo, formatFeatureTitle } from 'utils/server-utils';
import {
AvailableDates,
FeatureInfoObject,
FeatureInfoProps,
FeatureInfoType,
FeatureInfoVisibility,
FeatureTitleObject,
LayerType,
LegendDefinitionItem,
WMSLayerProps,
Expand Down Expand Up @@ -115,6 +118,33 @@ const sortKeys = (featureInfoProps: FeatureInfoObject): string[][] => {
return [dataKeys, metaDataKeys];
};

const getTitle = (
featureInfoTitle: FeatureTitleObject | undefined,
properties: any,
): PopupData | {} => {
if (!featureInfoTitle) {
return {};
}
const titleField = Object.keys(featureInfoTitle).find(
(field: string) =>
featureInfoTitle[field].visibility !== FeatureInfoVisibility.IfDefined ||
!!properties[field],
);
return titleField
? {
title: {
prop: titleField,
data: formatFeatureTitle(
properties[titleField],
featureInfoTitle[titleField].type,
featureInfoTitle[titleField].template,
featureInfoTitle[titleField].labelMap,
),
},
}
: {};
};

const getMetaData = (
featureInfoProps: FeatureInfoObject,
metaDataKeys: string[],
Expand All @@ -136,24 +166,32 @@ const getData = (
coordinates: any,
) =>
Object.keys(properties)
.filter(prop => keys.includes(prop))
.reduce(
(obj, item) => ({
.filter(prop => keys.includes(prop) && prop !== 'title')
.reduce((obj, item) => {
const itemProps = featureInfoProps[item] as FeatureInfoProps;
if (
itemProps.visibility === FeatureInfoVisibility.IfDefined &&
!properties[item]
) {
return obj;
}
Comment on lines +145 to +150
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some feature properties that may or may not be defined by the google api (for example: site name, river name). This allows us to hide the property from the pop up if it's not defined


return {
...obj,
[featureInfoProps[item].dataTitle]: {
[itemProps.dataTitle]: {
data: formatFeatureInfo(
properties[item],
featureInfoProps[item].type,
featureInfoProps[item].labelMap,
itemProps.type,
itemProps.labelMap,
),
coordinates,
},
}),
{},
);
};
}, {});

// TODO: maplibre: fix feature
export function getFeatureInfoPropsData(
featureInfoTitle: FeatureTitleObject | undefined,
featureInfoProps: FeatureInfoObject,
coordinates: number[],
feature: any,
Expand All @@ -162,6 +200,7 @@ export function getFeatureInfoPropsData(
const { properties } = feature;

return {
...getTitle(featureInfoTitle, properties),
...getMetaData(featureInfoProps, metaDataKeys, properties),
...getData(featureInfoProps, keys, properties, coordinates),
};
Expand Down
Loading
Loading