diff --git a/README.md b/README.md index aa5380725..9de24f852 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The configuration is split into three files that you can find in `src/config`: - 2. `layers.json` - 3. `tables.json` -Since many layers are common across multiple countries, we created shared configuration files that any deployment can access. These layers are generated by WFP globally and made available through the Humanitarian Data Cube. You can find the layers and their associated styles / legends in `src/config/shared`: +Since many layers are common across multiple countries, we created shared configuration files that any deployment can access. Many of these layers are generated by WFP globally and made available through the Humanitarian Data Cube. You can find the layers and their associated styles / legends in `src/config/shared`: - 1. `legends.json` - 2. `layers.json` diff --git a/api/app/googleflood.py b/api/app/googleflood.py new file mode 100644 index 000000000..c23e51b33 --- /dev/null +++ b/api/app/googleflood.py @@ -0,0 +1,195 @@ +"""Get data from Google Floods API""" + +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone +from os import getenv +from urllib.parse import urlencode + +from fastapi import HTTPException + +from .utils import make_request_with_retries + +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"], + "riverName": ( + data["river"] if "river" in data and len(data["river"]) > 1 else None + ), + "severity": data.get("severity", None), + "source": data.get("source", None), + "qualityVerified": data.get("qualityVerified", None), + "thresholds": data.get("thresholds", None), + "gaugeValueUnit": data.get("gaugeValueUnit", None), + }, + } + if "inundationMapSet" in data: + geojson["properties"]["inundationMapSet"] = data["inundationMapSet"] + return geojson + + +def fetch_flood_status(region_code): + """Fetch flood status for a region code""" + flood_status_url = f"https://floodforecasting.googleapis.com/v1/floodStatus:searchLatestFloodStatusByArea?key={GOOGLE_FLOODS_API_KEY}" + status_response = make_request_with_retries( + flood_status_url, method="post", data={"regionCode": region_code}, retries=3 + ) + return status_response + + +def fetch_flood_statuses_concurrently(region_codes: list[str]) -> list[dict]: + """Fetch flood statuses concurrently for a list of region codes.""" + flood_statuses = [] + with ThreadPoolExecutor() as executor: + future_to_region = { + executor.submit(fetch_flood_status, code): code for code in region_codes + } + for future in as_completed(future_to_region): + status_response = future.result() + 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", + ) + flood_statuses.extend(status_response.get("floodStatuses", [])) + return flood_statuses + + +def get_google_flood_dates(region_codes: list[str]): + """Fetch dates from the Google Floods API.""" + flood_statuses = fetch_flood_statuses_concurrently(region_codes) + + parsed_issued_times = [ + datetime.strptime(status["issuedTime"], "%Y-%m-%dT%H:%M:%S.%fZ") + for status in flood_statuses + if "issuedTime" in status + ] + parsed_issued_times.sort(reverse=True) # Sort in descending order + + # Format only the most recent date + most_recent_date = ( + { + "date": parsed_issued_times[0] + .replace(tzinfo=timezone.utc) + .strftime("%Y-%m-%d") + } + if parsed_issued_times + else {} + ) + + return [most_recent_date] if most_recent_date else [] + + +def get_google_floods_gauges( + region_codes: list[str], + as_geojson: bool = True, +): + """Get statistical charts data""" + initial_gauges = fetch_flood_statuses_concurrently(region_codes) + + gauge_details_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_details_params}" + + gauge_models_params = urlencode( + {"names": [f"gaugeModels/{gauge['gaugeId']}" for gauge in initial_gauges]}, + doseq=True, + ) + gauges_models_url = f"https://floodforecasting.googleapis.com/v1/gaugeModels:batchGet?key={GOOGLE_FLOODS_API_KEY}&{gauge_models_params}" + + # Run both requests + details_response = make_request_with_retries(gauges_details_url) + models_response = make_request_with_retries(gauges_models_url) + + # Create maps for quick lookup + gauge_details_map = { + item["gaugeId"]: item for item in details_response.get("gauges", []) + } + gauge_models_map = { + item["gaugeId"]: item for item in models_response.get("gaugeModels", []) + } + + gauges_details = [] + for gauge in initial_gauges: + gauge_id = gauge["gaugeId"] + detail = gauge_details_map.get(gauge_id, {}) + model = gauge_models_map.get(gauge_id, {}) + merged_gauge = {**gauge, **detail, **model} + gauges_details.append(merged_gauge) + + if as_geojson: + features = [] + for gauge in gauges_details: + try: + feature = format_gauge_to_geojson(gauge) + features.append(feature) + except Exception as e: + logger.error( + "Failed to format gauge %s: %s", gauge.get("gaugeId"), str(e) + ) + continue + + geojson_feature_collection = { + "type": "FeatureCollection", + "features": features, + } + return geojson_feature_collection + return gauges_details + + +def get_google_floods_gauge_forecast(gauge_ids: list[str]): + """Get forecast data for a gauge""" + + gauge_params = urlencode( + {"gaugeIds": [gauge_id for gauge_id in gauge_ids]}, + doseq=True, + ) + forecast_url = f"https://floodforecasting.googleapis.com/v1/gauges:queryGaugeForecasts?key={GOOGLE_FLOODS_API_KEY}&{gauge_params}" + forecast_response = make_request_with_retries(forecast_url) + + forecasts = forecast_response.get("forecasts", {}) + + forecast_data = {} + for gauge_id in gauge_ids: + forecast_map = {} + for forecast in forecasts.get(gauge_id, {}).get("forecasts", []): + issued_time = forecast.get("issuedTime") + for forecast_range in forecast.get("forecastRanges", []): + start_time = forecast_range.get("forecastStartTime") + value = round(forecast_range.get("value"), 2) + + # Deduplicate by forecastStartTime, keeping the most recent issuedTime + if ( + start_time not in forecast_map + or issued_time > forecast_map[start_time]["issuedTime"] + ): + forecast_map[start_time] = { + "issuedTime": issued_time, + "value": [start_time, value], + } + + forecast_data[gauge_id] = list(forecast_map.values()) + + return forecast_data diff --git a/api/app/main.py b/api/app/main.py index 5a14d4a1b..ac50703f6 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -14,6 +14,11 @@ 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_flood_dates, + get_google_floods_gauge_forecast, + 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 @@ -410,3 +415,60 @@ 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_codes: list[str] = Query(...)): + """ + Get the Google Floods gauges for a list of regions. + """ + if not region_codes: + raise HTTPException( + status_code=400, + detail="At least one region code must be provided.", + ) + for region_code in region_codes: + if len(region_code) != 2: + raise HTTPException( + status_code=400, + detail=f"Region code '{region_code}' must be exactly two characters (iso2).", + ) + + iso2_codes = [region_code.upper() for region_code in region_codes] + return get_google_floods_gauges(iso2_codes) + + +@app.get("/google-floods/dates/") +def get_google_floods_dates_api(region_codes: list[str] = Query(...)): + """ + Get the Google Floods dates for a list of regions. + """ + if not region_codes: + raise HTTPException( + status_code=400, + detail="At least one region code must be provided.", + ) + + for region_code in region_codes: + if len(region_code) != 2: + raise HTTPException( + status_code=400, + detail=f"Region code '{region_code}' must be exactly two characters (iso2).", + ) + + iso2_codes = [region_code.upper() for region_code in region_codes] + return get_google_flood_dates(iso2_codes) + + +@app.get("/google-floods/gauges/forecasts") +def get_google_floods_gauge_forecast_api( + gauge_ids: str = Query(..., description="Comma-separated list of gauge IDs") +): + """Get forecast data for a gauge or multiple gauges""" + gauge_id_list = [id.strip() for id in gauge_ids.split(",")] + if not gauge_id_list: + raise HTTPException( + status_code=400, + detail="gauge_ids must be provided and contain at least one value.", + ) + return get_google_floods_gauge_forecast(gauge_id_list) diff --git a/api/app/pytest.ini b/api/app/pytest.ini new file mode 100644 index 000000000..11c72fa2e --- /dev/null +++ b/api/app/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +log_cli = true +log_cli_level = INFO diff --git a/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast.yaml b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast.yaml new file mode 100644 index 000000000..d72a05ebb --- /dev/null +++ b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast.yaml @@ -0,0 +1,67 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gauges:queryGaugeForecasts?gaugeIds=hybas_1121465590 + response: + body: + string: !!binary | + H4sIAAAAAAAC/9Vay24bRxC86ysEnaVBvx+8+wcSnRIEgZIojoHEBiw5QGDo39OkY4GkL9zWEhMB + PJDLnd3isruqq3s+X1xeXv3+4eP9r3cPjw9Xm8vPdaAO/fHPL3cPPyMSiqkmPH9zdPqP/x28fP56 + d8q7h4dP97/dvvvrvs65IiC5gbghuUXeKG00hlEE2A9X1/vLvl75u7v3b+8PL398i935f9/9+Wl7 + B40cyS4caRLCrtfHp3699PePdx8fvwHGtwCb3esQ0cHSN++//UX7Cw/WPV2fCD1xACKIszEF0lLo + 0oUua0APYjYh9QZy7SLXFyOveBHX1ArvTrhYF7m9HLkNURcNSYNcDt270P3l0HmkkVuoisVi5NFF + Hi9HDqNiXNzAhXA59OxCzxdDdx3uFeTIELo8Xhia0A8WHkLf+/TT8/u9H3SKkuhWSYQ2IJUOmbme + kvgg8Ajt/dMT2bg4zRQsK1rhtdFx1QMVpZSY2XnqM/lYRgZVwLTSayod07AIr2gHXh4tE9nYS7wF + I0UbuOdSMSRVMc1MjUd+Dio+GXoJoBiIdNKTsQscz6ohtnMjudOQKgU5VtOQqtEEvJg4O9XlTCK2 + QQ4oaIBqi5NrLg+X9klX/GbyMFZx6RgK2pKQmUy89fFVzSejCSyPl5lkXLqNLE0HOJWLeSj2jMg5 + mHgB7MLMDgbL7V/eQA/60cL1RcR3IsIbqOobQsRXExGt3CqrT66cjdyaycVaHio1xckaKjK3RREu + xuzReeYzWxQ4oNsTmknDMbAAI/VsyFQe3nYKpCSkBXwmE1d5ysHaqebPQ8QLkCNzIuLyVnkBoC5y + OquExK6XpdupiJdr4PUkJAcRGoaVu2kYkZlEnIOZm3w2k4Z9WOk1g722grgMlKNls68yk4ir3BCm + svCI/rpq4ooWrYTvSMhcIg4bllRVfEb8T5h4QdszVaJkZLHvq/v3BsVHC9eXkLxF2QBtkAYZg+Rq + EgIjJcopN+chc4d29f9Gq8yZXhRrAjlkS/+mFsU5AJ072TWZimEEUKjJcvmbzMUwzJw620fmMnEZ + qCrkIQN7z/wMXHwydBruTkHeaB8Wgt6Y+Gjh6jJS+f9lfxb7SNzueFhLRpzLiQjJK+wT0xDndsN1 + JhVXjFZsFh13dmdNbVAU8vL56OG4WP8mdyh4VIinl5b4Yss9mY5haBkohWg0P+fSsdkgFG7sKzsP + F5+MWwYLRaouH1oWgN6g+Gjh+iryJYtwIzyUSuVX62eZV6XT7f/NJWOLEYbUGn7NJWPLwVuvT5av + rDB2HGAogGyL+0JzqbiiBcpEARMtn6BNpmIdgk7Z2s4+mY25ivoSvmZP6wxsfDJyHKUl6oqtHO2N + io8WnqIjF/tHtp+eLp4u/gULG2d/gzIAAA== + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:43 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=397 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast_multiple_gauges.yaml b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast_multiple_gauges.yaml new file mode 100644 index 000000000..8d889d28b --- /dev/null +++ b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauge_forecast_multiple_gauges.yaml @@ -0,0 +1,84 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gauges:queryGaugeForecasts?gaugeIds=hybas_1121465590&gaugeIds=hybas_1121499110 + response: + body: + string: !!binary | + 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 + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:44 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=383 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges.yaml b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges.yaml new file mode 100644 index 000000000..534a60a6f --- /dev/null +++ b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges.yaml @@ -0,0 +1,279 @@ +interactions: +- request: + body: '{"regionCode": "BD"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '20' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.0 + method: POST + uri: https://floodforecasting.googleapis.com/v1/floodStatus:searchLatestFloodStatusByArea + response: + body: + string: !!binary | + H4sIAAAAAAAC/+1d227bSBJ9n68I8pwQVdXdddGbc5mMd30ZJJkJdhaDgJKorAGPnfVlFtlB/n2b + sh1fSZNiUyspZIIgCduk2DpdPFV1qvqvH548eTo7PD6evjvLz85Pi9Onoyf/jP/55Mlf8z/j6U/5 + +adiexpPPH3x4dWLj+8+kMDTZ1enD05Pz4vp+4M/inIEAfnnoM8dvgcYgY7QMgavEn67/pHZ8Ukx + yU/Pyh96mx99Kn/y6nbx9OlZfnJ2fTF7DvFiNiqvB9dXiQOLo2nlsMtRX+/d9OW/7t/xz/zwvHjg + RDx1ePyf4uTF8fn8Vs8hQ7g40BsgMtKzm6PPP3++Hg3fzny98aFPillxUhxNioef/v7zx8mEB5// + 7gzcGXh998q5eH9yeYEft3Z2rr+fP/LP20dXn/LL5/kXu7v/6vWNIafFn8XJwdmX8tTWi/1fX3/c + 23+7u3VjxMFRnIX87OD4aDf//K44uz3jt85eg+7iuDkb5VcQ73VY3uin7Tc/3Xr++Qc5OcgPD/5b + TH8+Pvzy6fjoAqnmc59PTfKZqMcJ2GzqYFJI/B1mYPz0xlW+Pmtw793Xr7Z/2W149+l4NiU3gTEX + zusUDQRJZDId44TGWLS++87+h6YPXkxnE8ptPJkU3hfjcV4Ew3w8JbZpKPytW3/7++/Pqr6a5aG0 + 6iNcIfDnt/svtl5s72y//8f91X16fH4yKa5s1DUK58Zr53gyv9htCB7G/zs7n5Y/RD5jQ/b+xmc4 + PD76dHXeMHNqbHrvvv8+j9/B2Zdf45cxOyjKxzw7OS9+uDGozo6SNrSjOCKXBQAh2xA7Spd2lEyB + HJGvtqMPWd11Na037ebe/scfd/b3X23vvXmaDsjihECrgAyZdwicGMjaCsfxOwyI3x+O15YP9A9a + x6pUZX1VsxAcmyW2vkjtUIuG9NvyZsVlFD9isOp3kndKDtPOCkrbWUHhjmvZNVvLru+1HIZ30MWA + rstZhbyFKuBS5jWwp7TAdVvtuBSGm9O91u+g8lHKI1h0tSOL1Vocw+VoEB8gKPCA4yoDbIGNudoA + I5pPjeM4Se2ATF5hia8lH21+XLw1rpKIV+HEs+IbUkyEkdeMxNDrwst7fjGCx5b3nWHDa2mllzNF + OxlhUf1WAnZeEq9m9e3YlFqgbrB9nE3dGdYLbGEj3KAIw5c/be29eb1MlBorUqUDH80rOAs+MesP + f2uHU2ORDWFP9Ih9vUuu1ge+S6f9huykyou3rCSbqZFLzO2QS4665qJWwl8dLOyCOA0ZgPdSGVeh + jDigJY6rkGuBU45GSRkXj/VfXIyaMQEacFr14P9fnLJGa1VpTzVjjH5WavdTQ0t7ytqRsWJohNPr + YQNOVwmnnIETq4qSRJh6BjVJTFh9S5gawUaknAaYLkxPyww/1uBUvCefOMmE3Daa56Rr3Aqbxa2w + V5wOYem+gOwyBFSuihBEPwuid84uMZCxqeZPRugyLTM8m85fI2o9DNGBhqj1giA1cS00dYmtb7yq + vW1JZz0tbn9XiCcM0pTLAV1hK6zsqdrYiqimzhpQBG5oC1wObhOAW0dwMwrrA9S32++W6oUhBZJK + ehtZQakMTGxgXdtkNZgtrha6uFgzegv90tvBDVvcoKo3qJQPQOYch9RRrciYG+KUR95nGgDpO1QH + rRF0l00DgjOtzMqWua3o6YfEohfBVpo2bxqPjqh9VPRyZ1g/1rUVaDN3qYBBIzBSW6PQwXJJQoQx + E1dmvqI7q3EaE3NZCVnLWC364G/A+KFytO3drTevP77c2Xr3bvvH7Zdb77f39x6ex1WsTtOJTWis + PAu5eShm43E+GU9D/BcbTsz6rU7TmVObOD/LJd5dxkqzKSI74NmMfdFnddo0LyS42XhS4MRjPjPJ + RS26phMtpuBkqE77Nrm3l636UJUIjMuW59Vpidbtyw8vP0b683z31bvtnYYLl0bgMouGusP75+Ji + 0ozdS9+syXmbH75UW7kQaup8exZhEo1cI9heD1ycSG2erQ1Twmk+I5+HsZ+w2NRmEy6YbMoT0nG/ + tlZUbTYb52ObeT/jYgyzIi5WQyfsJxR6tLUTV1iIlhXcrPAM0YTojOOHmTGbYM+VwJ1Am8jWRkO2 + UJjaQfh21MjciW8cqSuDoaluOIxCWbwcf/mOlpebWV7ul/kvyTl94GEfpgJ3H3dV4ypl2cXt19Q9 + ebuY+MRZQWvK7HkUMGMf34+Li4VWykFtwQ8gSxq17he6y3ZIHUWu6Wo8UmfgUqdX2LWMqwAurhke + NG4Vj7tGUeuQYakbrywFjC4Yl9VFiXGK2Dh0wiVS1UvkLBsSt36sLmOoe2sqfAtmAWt0xGWxNqUu + 2JSWNpYC3ERu/7MCPtL7ynJAyKKTRpB6VkAazoofOc6ARPxGyFa/k85p/fMl9sFxVWuFuJbVk31T + SSQTW0u7EP68u8Km1AcOMtbe+giIaI2yCp1K6pQqN8WxjFxZER59u64O64DjDcexOiSrSqpGHDvD + gImzqt6aSgPmjgGXifFNIRJDwfbVgM5NL13QSjlARC6raUgd2pamoW0dRd7hzKRDaPsCko+Gtu8M + 6wW5D9jUupjhPfX2+sB42TFEQ2VfE5uRUhudFsUWXdamyXFfdiSK9tfzRjRvHYKIi8s4MDq+lX3x + yjXPzhIHu31r+RU42BTO+whVGEhu4/h38J6kKk8zpwqMqY1sgKxlcxcU9x1Wbw1IbmOEVaO7VtOl + SIQkdUGXNA37fjPBHarAVwjHA1dY2OAqQWV2orS3LMESx3nb9ipA4a6tn1dDzTGgdOEWRRK8VKYj + LAOm4FNHENhlTcu5rsoOAX1FcvGXvb/v7X/YSzgnIGUf0WqWr2rCiZeut3YvGAbblPzMEA68GtA5 + ISNglQrCkhmhOk3cyImohXfKmUdF25TKzGEPnL46PZELUAPksj9O8rg2tPBVJbMg2LXV8yAqXGv2 + VJbCC2iNMEtDPJ9czNG0kdMlVXDmFze4K1TU1U6BlFazvUn5Fs6ci++sSt9Uy1ihh8SwJW4VQgnl + l9zVOV0JnjCY18WFGfFwNRVc6s2H1OmWFjGUUpkLgZcpe3VlrVh87mrv1GOclMRN2tutXW+mt8L4 + S9h/LsQPUrPTmvqgqYtQXMtNEwk6vIdXyKANLWkuB3Q2bwRWrZ/UzKGKT62fbKrq17IjgJgE3Yi3 + 8KBfvxzQuY6q7AZek0d2Eo/UfZTa5jUQOhZTr04P5gG0CSytRHensv1KuQ0geE3dOHyBtklkXZPG + qyGU3AiMPuDxPNRcZXf/1esKLK9iN5WcJTeejYPl3hc8HjudqaLAmAEmU9dvN5UwdbNp/FXkYeJD + znkOYaqAJKTTYso9dlMZTzEELSyPlMrnpEaTHJkEhGU8Zuyrm0rlumy2Mm8vlKV3rvIZkzqosZ2s + yJx6c5C2tacmsBG9k4fNQhN6VwZSGeQ0ytAQMLHgkaAFcH1mASR0bfuzEtuFrlTDtY0CMkYn3NX0 + saKMIA5I7HGhtNSjxKXUVXP+aJLpzrCBu7bhrktIhjoSrekL7o1d6qwSYlthrnXYHmSFDO6A04X3 + Z1YQoZqskmB6eR817l9/pe+7TQyWoB0jBa3JoJiT0EMRXmhahHdBmDyJ34j+SMP67bAPVd3uE8Sl + IC/x6vUN3zIIZcmzKDtnS1y9pTPjtXpvrsgRkSl1Kgld8+5m89WLQqHDnsirFOEcpEiXAzqnkyzO + YM1rh8psTurQvIsLog1wib1f6oIut+SFULvXKRBr6tdxS2EhhO9Ryb1Gr+dlh9yAhatezWUtkXOp + 1VrELeIUmrEjlI3A7MAfO5R3IdepCj1ERydxuyf07Zs4uI4ti1dHwzA0Luurbw6C1flCLrKExFJQ + sqbtn75tyKuLVypeXOxRi3tnWG9AHsQ4CeyvRE+1jiZ4Jy5131500nIrPovA3QSiMDTiTmhwlaM1 + q9mi18r3VeK6RGubiVPeiNYjg729HNC9X45TqemXg0pqiUMsLclu6LL3+ZCVq3jcNfLKXESh3Gqo + eb+1nmnqRiS+rU+Gm9OdbGisdzmgK501AuaajaWFOXnmJbTkBIba1b5+V7vLbZx99Zkzkurd5TDz + CM6njs62bZuH0dp0DHpRs6AX9YvTzF+oIVVBDAShLqGQsd0anBLJm7XZHDms2/2cXZkGT8wROLMW + GpX5bnPds9yrQhPcpQOGVm5AoFbT8Xxo1dWGNUSLYJVV/CVriHOeOAfB2jYH0anD0ToDeY179y89 + uiCqoWb/T6DgLLFNVm0bzFVZvIr9gjA0iy9Q72UWg1IhBRuOLyeqRK1qBqyWOgfsmvaX86MQyn3i + IiHv2F+uGRnGfsnwQCP6oxEAJlLHiANp6iI3DC07eQFDRynDEN1d8+gDRKeIKjdOwSwudUxdGuRa + 1WLGF4LbjIZzw94T/eFYvQer6ZxojsCltreNo2jzrQIRRGkjNI6PpCnWyOYuXYjrI3esEYZxXOcu + dbC3sVAhjIKWm27iLWHYUjaOEavp6++T9wOIhLfNTgc+cz5Q12692Ewuh/0u3oEsLVzMFRgqRUbz + XpbsKXXHFU/tgFp6p9h1B4rVqOUagLqwkpMlUE0URRwqJ46iUGi5dQwKdqX1j3qfd4YNOF0tnIKK + VGuLouVDTU3aia1dtiVEZ802JW04CDdTsHjREI9qIuDE2hYqxT9//+HrD/8DfW/ahDfUAAA= + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:39 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=574 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gauges:batchGet?names=gauges%2FBWDB_SW101&names=gauges%2FBWDB_SW110&names=gauges%2FBWDB_SW117&names=gauges%2FBWDB_SW119.1&names=gauges%2FBWDB_SW131.5&names=gauges%2FBWDB_SW132.2&names=gauges%2FBWDB_SW133&names=gauges%2FBWDB_SW137A&names=gauges%2FBWDB_SW14.5&names=gauges%2FBWDB_SW140&names=gauges%2FBWDB_SW142.1&names=gauges%2FBWDB_SW145&names=gauges%2FBWDB_SW147.5&names=gauges%2FBWDB_SW157&names=gauges%2FBWDB_SW159&names=gauges%2FBWDB_SW15J&names=gauges%2FBWDB_SW162&names=gauges%2FBWDB_SW172&names=gauges%2FBWDB_SW173&names=gauges%2FBWDB_SW175.5&names=gauges%2FBWDB_SW177&names=gauges%2FBWDB_SW179&names=gauges%2FBWDB_SW201&names=gauges%2FBWDB_SW202&names=gauges%2FBWDB_SW203&names=gauges%2FBWDB_SW204&names=gauges%2FBWDB_SW206&names=gauges%2FBWDB_SW207&names=gauges%2FBWDB_SW211.5&names=gauges%2FBWDB_SW212&names=gauges%2FBWDB_SW225&names=gauges%2FBWDB_SW228&names=gauges%2FBWDB_SW236&names=gauges%2FBWDB_SW238&names=gauges%2FBWDB_SW247&names=gauges%2FBWDB_SW248&names=gauges%2FBWDB_SW251&names=gauges%2FBWDB_SW263&names=gauges%2FBWDB_SW263.1&names=gauges%2FBWDB_SW266&names=gauges%2FBWDB_SW267&names=gauges%2FBWDB_SW269&names=gauges%2FBWDB_SW269.5&names=gauges%2FBWDB_SW270&names=gauges%2FBWDB_SW273&names=gauges%2FBWDB_SW274&names=gauges%2FBWDB_SW275.5&names=gauges%2FBWDB_SW277&names=gauges%2FBWDB_SW285&names=gauges%2FBWDB_SW291.5R&names=gauges%2FBWDB_SW294&names=gauges%2FBWDB_SW299&names=gauges%2FBWDB_SW302&names=gauges%2FBWDB_SW36&names=gauges%2FBWDB_SW3A&names=gauges%2FBWDB_SW42&names=gauges%2FBWDB_SW45&names=gauges%2FBWDB_SW45.5&names=gauges%2FBWDB_SW46.9L&names=gauges%2FBWDB_SW49&names=gauges%2FBWDB_SW49A&names=gauges%2FBWDB_SW5&names=gauges%2FBWDB_SW50.6&names=gauges%2FBWDB_SW62&names=gauges%2FBWDB_SW63&names=gauges%2FBWDB_SW65&names=gauges%2FBWDB_SW67&names=gauges%2FBWDB_SW68.5&names=gauges%2FBWDB_SW71A&names=gauges%2FBWDB_SW72&names=gauges%2FBWDB_SW77&names=gauges%2FBWDB_SW84&names=gauges%2FBWDB_SW88&names=gauges%2FBWDB_SW88A&names=gauges%2FBWDB_SW90&names=gauges%2FBWDB_SW91.9R&names=gauges%2FBWDB_SW93.4L&names=gauges%2FBWDB_SW93.5L&names=gauges%2FBWDB_SW95&names=gauges%2FBWDB_SW99&names=gauges%2FCWC_012-MDSIL + response: + body: + string: !!binary | + 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 + 6aEmTTd+LkvDf//54o8X/wHapSYWR04AAA== + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:40 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=272 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gaugeModels:batchGet?names=gaugeModels%2FBWDB_SW101&names=gaugeModels%2FBWDB_SW110&names=gaugeModels%2FBWDB_SW117&names=gaugeModels%2FBWDB_SW119.1&names=gaugeModels%2FBWDB_SW131.5&names=gaugeModels%2FBWDB_SW132.2&names=gaugeModels%2FBWDB_SW133&names=gaugeModels%2FBWDB_SW137A&names=gaugeModels%2FBWDB_SW14.5&names=gaugeModels%2FBWDB_SW140&names=gaugeModels%2FBWDB_SW142.1&names=gaugeModels%2FBWDB_SW145&names=gaugeModels%2FBWDB_SW147.5&names=gaugeModels%2FBWDB_SW157&names=gaugeModels%2FBWDB_SW159&names=gaugeModels%2FBWDB_SW15J&names=gaugeModels%2FBWDB_SW162&names=gaugeModels%2FBWDB_SW172&names=gaugeModels%2FBWDB_SW173&names=gaugeModels%2FBWDB_SW175.5&names=gaugeModels%2FBWDB_SW177&names=gaugeModels%2FBWDB_SW179&names=gaugeModels%2FBWDB_SW201&names=gaugeModels%2FBWDB_SW202&names=gaugeModels%2FBWDB_SW203&names=gaugeModels%2FBWDB_SW204&names=gaugeModels%2FBWDB_SW206&names=gaugeModels%2FBWDB_SW207&names=gaugeModels%2FBWDB_SW211.5&names=gaugeModels%2FBWDB_SW212&names=gaugeModels%2FBWDB_SW225&names=gaugeModels%2FBWDB_SW228&names=gaugeModels%2FBWDB_SW236&names=gaugeModels%2FBWDB_SW238&names=gaugeModels%2FBWDB_SW247&names=gaugeModels%2FBWDB_SW248&names=gaugeModels%2FBWDB_SW251&names=gaugeModels%2FBWDB_SW263&names=gaugeModels%2FBWDB_SW263.1&names=gaugeModels%2FBWDB_SW266&names=gaugeModels%2FBWDB_SW267&names=gaugeModels%2FBWDB_SW269&names=gaugeModels%2FBWDB_SW269.5&names=gaugeModels%2FBWDB_SW270&names=gaugeModels%2FBWDB_SW273&names=gaugeModels%2FBWDB_SW274&names=gaugeModels%2FBWDB_SW275.5&names=gaugeModels%2FBWDB_SW277&names=gaugeModels%2FBWDB_SW285&names=gaugeModels%2FBWDB_SW291.5R&names=gaugeModels%2FBWDB_SW294&names=gaugeModels%2FBWDB_SW299&names=gaugeModels%2FBWDB_SW302&names=gaugeModels%2FBWDB_SW36&names=gaugeModels%2FBWDB_SW3A&names=gaugeModels%2FBWDB_SW42&names=gaugeModels%2FBWDB_SW45&names=gaugeModels%2FBWDB_SW45.5&names=gaugeModels%2FBWDB_SW46.9L&names=gaugeModels%2FBWDB_SW49&names=gaugeModels%2FBWDB_SW49A&names=gaugeModels%2FBWDB_SW5&names=gaugeModels%2FBWDB_SW50.6&names=gaugeModels%2FBWDB_SW62&names=gaugeModels%2FBWDB_SW63&names=gaugeModels%2FBWDB_SW65&names=gaugeModels%2FBWDB_SW67&names=gaugeModels%2FBWDB_SW68.5&names=gaugeModels%2FBWDB_SW71A&names=gaugeModels%2FBWDB_SW72&names=gaugeModels%2FBWDB_SW77&names=gaugeModels%2FBWDB_SW84&names=gaugeModels%2FBWDB_SW88&names=gaugeModels%2FBWDB_SW88A&names=gaugeModels%2FBWDB_SW90&names=gaugeModels%2FBWDB_SW91.9R&names=gaugeModels%2FBWDB_SW93.4L&names=gaugeModels%2FBWDB_SW93.5L&names=gaugeModels%2FBWDB_SW95&names=gaugeModels%2FBWDB_SW99&names=gaugeModels%2FCWC_012-MDSIL + response: + body: + string: !!binary | + H4sIAAAAAAAC/+1cO28cNxDu9SsE1QpBDt/pYsuFA6uxEqsIAkPAreUDLjJylvKA4f8e3smKI3KH + y5mCdA5ScYVuRhC/ncc3D+6no+Pjk+uru+vp/MNq2nw8+f74l/Sr4+NP+8+HL1+u0hcnzy7Pnr29 + uAQvT04fvr59v50+vv+wWe1UH5TS7/+82t6sb65fTX9Mm/SNE9bK9KOi9NoEp/XpV9nV1c31tH0Q + 9ULiotNft9vpt+nskUYoNL4ofD59dIo3V5u76eeb9e3uMOcvfnrx+uLrQX6/u9qsb/9+M23X79bT + 7ry327vp6D9/pgYJBDIkSuWYoJAoyE+4BInSuUZvSAIDES+siekHtJdOWuNxRNIzR0XnEYm5Rncj + UUCHpPbkCyPB7QkzEjvWSJRnQGJE2D3H6FSwWoKvQGKFRkXnIXG5Rm9I9A9kREx7cLXk4OrGB1fJ + MBIpgmz0GyU0KjpvJJBrdIfE0KNr+SBRSKrZehYSX2h0zzeGbiSFs+NG4ipBZ95IfK7RPbjaH+mQ + WOFtDQWwDQcH290hnOPkVqicFR4jgWVQ3/+smn5WDc3USmsqtdJmOLUKlp41ozD7mOUdRB2Nxb0/ + iUZUdBYSK3ON7t5v6JAk13WmMSAGoVBRjIBnGt0hcXQiYYTaP0cZrXbeaOVwKxEOF523kkKjOySK + XssfOLeKSsTXZFCS5+yzfZARnFXpcaKgJM/BRWdBiYVG9wCbULF0VKy6t3BlwQYfosS9x8K9p82K + znuPzjV6o6IZlQnOOKxY5BuJmHePEFIxnKFCrHwDrwoDaJVXnMq7clDTcFA7gj/6ZGmMozaXj9V+ + BALDsPLx+eXzt1LBd+dnFy9fsZq4ePbTyx6tjDD7hKelVd76oLqbg6RXzjZP0rWOU8BFkSCXa3Sn + AHT30BU7MIvm3/2ZO/3Uis6784oRFUFWiuXHoCRRvASfBQVUrtE/VdDt5MBjA0jPyRJq/2/roIyL + HiqDrcSYUVGsYso0ulNE4xmuszvpo6qmAgpkBdAyKDrX6A2KYyCSXMc2mklyHVQUm2xlGr0RMZFO + sBM9whsIq5xJ4W2JeUiK+NM/vtK5l6l16G1Dg94N6M9HLQydXSd/2Oe/KI3TYF0wlYMHXBRLJZlG + d4fg1GEAtecPusEAwAywACsFfWwRhDHNXbWIiyJdtVyjNyaeTiXANg9ywFEHOeBHD3IYHXqtBMi2 + OU4S9ajo/GgLco3uKcIlKvM07M+pBKd8bR72A3nYr0cP+wEYfLMWIIrJP7F4LZf5ukMiGTsBRR+i + xjepS5nF9lVvSJRhrO7Ge5b8b4MSUEi8FBoVnQ8lKtfoH18Z9bxsr0pqw9J5K1GjqxLDmGq155uy + 17/M1EfnG46NhEqFntlIrNT980RN5hrd3UbTNxBBtTdLi9bnIiTFhlN3I2H01GMzd02iRO6ajGQw + d2Vsdx84cVWs2Wt5waVSBlNBid8AKKxpVDNRK+7CLKbggtr1564MSGrLVsWUgcpKYDQrUZ5e9BUt + 4RotqYytEFqSa3SHRDF4yYF31BnrSqpoe+GQ1K6ozUMSRvOSmGpaTu+9efG1XGNddp3Ri69gnlwn + jyaaswFRcqoKN6FePovDL58pDYLOYiHilU1cXgrTsv+eZ2Qsgut2f9Bkfyg8qHuIcHTC4duXXzx5 + +SUMX35JD4UeIVz7FLecyS6XdKOnuBDpM3wIwsfGTkgUgIpi0SPT6B80PX3Vw7fPej151ltOh7vH + V8bIqlalFSMr6kWbInj3RoSzZFtZt9cN6/ZmwLo94x7egQ/5laX7QrWvtVogjYu+IIf3exirIA/3 + dKOFVEBBMPhgTn+xp1lRxFNGdwWNE5FxIaMot/GYWbwRZtF1im3v3qC4wCrRDpqUhkAnG9X5UhZh + gWwmxQ363pAw3hBQxkDccST5ekfxrqv+OYexa1cM0HAjqS2hYfPKTKM/S2fMGA68BcZIw6ZWsxYz + BvKScq7RnZkwCpcD7x2r3assnhhs3hoCBig6NL8gQEfqCwKMHP2CALCMaZRqXxwC6uLQruofvDjk + IquH+H9eC0mfvx59PvoHXlAKZG1VAAA= + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:40 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=145 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api.yaml b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api.yaml new file mode 100644 index 000000000..08776e427 --- /dev/null +++ b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api.yaml @@ -0,0 +1,279 @@ +interactions: +- request: + body: '{"regionCode": "BD"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '20' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.0 + method: POST + uri: https://floodforecasting.googleapis.com/v1/floodStatus:searchLatestFloodStatusByArea + response: + body: + string: !!binary | + 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 + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:40 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=548 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gauges:batchGet?names=gauges%2FBWDB_SW101&names=gauges%2FBWDB_SW110&names=gauges%2FBWDB_SW117&names=gauges%2FBWDB_SW119.1&names=gauges%2FBWDB_SW131.5&names=gauges%2FBWDB_SW132.2&names=gauges%2FBWDB_SW133&names=gauges%2FBWDB_SW137A&names=gauges%2FBWDB_SW14.5&names=gauges%2FBWDB_SW140&names=gauges%2FBWDB_SW142.1&names=gauges%2FBWDB_SW145&names=gauges%2FBWDB_SW147.5&names=gauges%2FBWDB_SW157&names=gauges%2FBWDB_SW159&names=gauges%2FBWDB_SW15J&names=gauges%2FBWDB_SW162&names=gauges%2FBWDB_SW172&names=gauges%2FBWDB_SW173&names=gauges%2FBWDB_SW175.5&names=gauges%2FBWDB_SW177&names=gauges%2FBWDB_SW179&names=gauges%2FBWDB_SW201&names=gauges%2FBWDB_SW202&names=gauges%2FBWDB_SW203&names=gauges%2FBWDB_SW204&names=gauges%2FBWDB_SW206&names=gauges%2FBWDB_SW207&names=gauges%2FBWDB_SW211.5&names=gauges%2FBWDB_SW212&names=gauges%2FBWDB_SW225&names=gauges%2FBWDB_SW228&names=gauges%2FBWDB_SW236&names=gauges%2FBWDB_SW238&names=gauges%2FBWDB_SW247&names=gauges%2FBWDB_SW248&names=gauges%2FBWDB_SW251&names=gauges%2FBWDB_SW263&names=gauges%2FBWDB_SW263.1&names=gauges%2FBWDB_SW266&names=gauges%2FBWDB_SW267&names=gauges%2FBWDB_SW269&names=gauges%2FBWDB_SW269.5&names=gauges%2FBWDB_SW270&names=gauges%2FBWDB_SW273&names=gauges%2FBWDB_SW274&names=gauges%2FBWDB_SW275.5&names=gauges%2FBWDB_SW277&names=gauges%2FBWDB_SW285&names=gauges%2FBWDB_SW291.5R&names=gauges%2FBWDB_SW294&names=gauges%2FBWDB_SW299&names=gauges%2FBWDB_SW302&names=gauges%2FBWDB_SW36&names=gauges%2FBWDB_SW3A&names=gauges%2FBWDB_SW42&names=gauges%2FBWDB_SW45&names=gauges%2FBWDB_SW45.5&names=gauges%2FBWDB_SW46.9L&names=gauges%2FBWDB_SW49&names=gauges%2FBWDB_SW49A&names=gauges%2FBWDB_SW5&names=gauges%2FBWDB_SW50.6&names=gauges%2FBWDB_SW62&names=gauges%2FBWDB_SW63&names=gauges%2FBWDB_SW65&names=gauges%2FBWDB_SW67&names=gauges%2FBWDB_SW68.5&names=gauges%2FBWDB_SW71A&names=gauges%2FBWDB_SW72&names=gauges%2FBWDB_SW77&names=gauges%2FBWDB_SW84&names=gauges%2FBWDB_SW88&names=gauges%2FBWDB_SW88A&names=gauges%2FBWDB_SW90&names=gauges%2FBWDB_SW91.9R&names=gauges%2FBWDB_SW93.4L&names=gauges%2FBWDB_SW93.5L&names=gauges%2FBWDB_SW95&names=gauges%2FBWDB_SW99&names=gauges%2FCWC_012-MDSIL + response: + body: + string: !!binary | + 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 + 6aEmTTd+LkvDf//54o8X/wHapSYWR04AAA== + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:41 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=274 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gaugeModels:batchGet?names=gaugeModels%2FBWDB_SW101&names=gaugeModels%2FBWDB_SW110&names=gaugeModels%2FBWDB_SW117&names=gaugeModels%2FBWDB_SW119.1&names=gaugeModels%2FBWDB_SW131.5&names=gaugeModels%2FBWDB_SW132.2&names=gaugeModels%2FBWDB_SW133&names=gaugeModels%2FBWDB_SW137A&names=gaugeModels%2FBWDB_SW14.5&names=gaugeModels%2FBWDB_SW140&names=gaugeModels%2FBWDB_SW142.1&names=gaugeModels%2FBWDB_SW145&names=gaugeModels%2FBWDB_SW147.5&names=gaugeModels%2FBWDB_SW157&names=gaugeModels%2FBWDB_SW159&names=gaugeModels%2FBWDB_SW15J&names=gaugeModels%2FBWDB_SW162&names=gaugeModels%2FBWDB_SW172&names=gaugeModels%2FBWDB_SW173&names=gaugeModels%2FBWDB_SW175.5&names=gaugeModels%2FBWDB_SW177&names=gaugeModels%2FBWDB_SW179&names=gaugeModels%2FBWDB_SW201&names=gaugeModels%2FBWDB_SW202&names=gaugeModels%2FBWDB_SW203&names=gaugeModels%2FBWDB_SW204&names=gaugeModels%2FBWDB_SW206&names=gaugeModels%2FBWDB_SW207&names=gaugeModels%2FBWDB_SW211.5&names=gaugeModels%2FBWDB_SW212&names=gaugeModels%2FBWDB_SW225&names=gaugeModels%2FBWDB_SW228&names=gaugeModels%2FBWDB_SW236&names=gaugeModels%2FBWDB_SW238&names=gaugeModels%2FBWDB_SW247&names=gaugeModels%2FBWDB_SW248&names=gaugeModels%2FBWDB_SW251&names=gaugeModels%2FBWDB_SW263&names=gaugeModels%2FBWDB_SW263.1&names=gaugeModels%2FBWDB_SW266&names=gaugeModels%2FBWDB_SW267&names=gaugeModels%2FBWDB_SW269&names=gaugeModels%2FBWDB_SW269.5&names=gaugeModels%2FBWDB_SW270&names=gaugeModels%2FBWDB_SW273&names=gaugeModels%2FBWDB_SW274&names=gaugeModels%2FBWDB_SW275.5&names=gaugeModels%2FBWDB_SW277&names=gaugeModels%2FBWDB_SW285&names=gaugeModels%2FBWDB_SW291.5R&names=gaugeModels%2FBWDB_SW294&names=gaugeModels%2FBWDB_SW299&names=gaugeModels%2FBWDB_SW302&names=gaugeModels%2FBWDB_SW36&names=gaugeModels%2FBWDB_SW3A&names=gaugeModels%2FBWDB_SW42&names=gaugeModels%2FBWDB_SW45&names=gaugeModels%2FBWDB_SW45.5&names=gaugeModels%2FBWDB_SW46.9L&names=gaugeModels%2FBWDB_SW49&names=gaugeModels%2FBWDB_SW49A&names=gaugeModels%2FBWDB_SW5&names=gaugeModels%2FBWDB_SW50.6&names=gaugeModels%2FBWDB_SW62&names=gaugeModels%2FBWDB_SW63&names=gaugeModels%2FBWDB_SW65&names=gaugeModels%2FBWDB_SW67&names=gaugeModels%2FBWDB_SW68.5&names=gaugeModels%2FBWDB_SW71A&names=gaugeModels%2FBWDB_SW72&names=gaugeModels%2FBWDB_SW77&names=gaugeModels%2FBWDB_SW84&names=gaugeModels%2FBWDB_SW88&names=gaugeModels%2FBWDB_SW88A&names=gaugeModels%2FBWDB_SW90&names=gaugeModels%2FBWDB_SW91.9R&names=gaugeModels%2FBWDB_SW93.4L&names=gaugeModels%2FBWDB_SW93.5L&names=gaugeModels%2FBWDB_SW95&names=gaugeModels%2FBWDB_SW99&names=gaugeModels%2FCWC_012-MDSIL + response: + body: + string: !!binary | + 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 + IquH+H9eC0mfvx59PvoHXlAKZG1VAAA= + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:41 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=388 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api_case_insensitive.yaml b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api_case_insensitive.yaml new file mode 100644 index 000000000..187959390 --- /dev/null +++ b/api/app/tests/cassettes/test_google_floods_api/test_get_google_floods_gauges_api_case_insensitive.yaml @@ -0,0 +1,279 @@ +interactions: +- request: + body: '{"regionCode": "BD"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '20' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.0 + method: POST + uri: https://floodforecasting.googleapis.com/v1/floodStatus:searchLatestFloodStatusByArea + response: + body: + string: !!binary | + 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 + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:42 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=517 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gauges:batchGet?names=gauges%2FBWDB_SW101&names=gauges%2FBWDB_SW110&names=gauges%2FBWDB_SW117&names=gauges%2FBWDB_SW119.1&names=gauges%2FBWDB_SW131.5&names=gauges%2FBWDB_SW132.2&names=gauges%2FBWDB_SW133&names=gauges%2FBWDB_SW137A&names=gauges%2FBWDB_SW14.5&names=gauges%2FBWDB_SW140&names=gauges%2FBWDB_SW142.1&names=gauges%2FBWDB_SW145&names=gauges%2FBWDB_SW147.5&names=gauges%2FBWDB_SW157&names=gauges%2FBWDB_SW159&names=gauges%2FBWDB_SW15J&names=gauges%2FBWDB_SW162&names=gauges%2FBWDB_SW172&names=gauges%2FBWDB_SW173&names=gauges%2FBWDB_SW175.5&names=gauges%2FBWDB_SW177&names=gauges%2FBWDB_SW179&names=gauges%2FBWDB_SW201&names=gauges%2FBWDB_SW202&names=gauges%2FBWDB_SW203&names=gauges%2FBWDB_SW204&names=gauges%2FBWDB_SW206&names=gauges%2FBWDB_SW207&names=gauges%2FBWDB_SW211.5&names=gauges%2FBWDB_SW212&names=gauges%2FBWDB_SW225&names=gauges%2FBWDB_SW228&names=gauges%2FBWDB_SW236&names=gauges%2FBWDB_SW238&names=gauges%2FBWDB_SW247&names=gauges%2FBWDB_SW248&names=gauges%2FBWDB_SW251&names=gauges%2FBWDB_SW263&names=gauges%2FBWDB_SW263.1&names=gauges%2FBWDB_SW266&names=gauges%2FBWDB_SW267&names=gauges%2FBWDB_SW269&names=gauges%2FBWDB_SW269.5&names=gauges%2FBWDB_SW270&names=gauges%2FBWDB_SW273&names=gauges%2FBWDB_SW274&names=gauges%2FBWDB_SW275.5&names=gauges%2FBWDB_SW277&names=gauges%2FBWDB_SW285&names=gauges%2FBWDB_SW291.5R&names=gauges%2FBWDB_SW294&names=gauges%2FBWDB_SW299&names=gauges%2FBWDB_SW302&names=gauges%2FBWDB_SW36&names=gauges%2FBWDB_SW3A&names=gauges%2FBWDB_SW42&names=gauges%2FBWDB_SW45&names=gauges%2FBWDB_SW45.5&names=gauges%2FBWDB_SW46.9L&names=gauges%2FBWDB_SW49&names=gauges%2FBWDB_SW49A&names=gauges%2FBWDB_SW5&names=gauges%2FBWDB_SW50.6&names=gauges%2FBWDB_SW62&names=gauges%2FBWDB_SW63&names=gauges%2FBWDB_SW65&names=gauges%2FBWDB_SW67&names=gauges%2FBWDB_SW68.5&names=gauges%2FBWDB_SW71A&names=gauges%2FBWDB_SW72&names=gauges%2FBWDB_SW77&names=gauges%2FBWDB_SW84&names=gauges%2FBWDB_SW88&names=gauges%2FBWDB_SW88A&names=gauges%2FBWDB_SW90&names=gauges%2FBWDB_SW91.9R&names=gauges%2FBWDB_SW93.4L&names=gauges%2FBWDB_SW93.5L&names=gauges%2FBWDB_SW95&names=gauges%2FBWDB_SW99&names=gauges%2FCWC_012-MDSIL + response: + body: + string: !!binary | + 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 + 6aEmTTd+LkvDf//54o8X/wHapSYWR04AAA== + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:42 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=268 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.0 + method: GET + uri: https://floodforecasting.googleapis.com/v1/gaugeModels:batchGet?names=gaugeModels%2FBWDB_SW101&names=gaugeModels%2FBWDB_SW110&names=gaugeModels%2FBWDB_SW117&names=gaugeModels%2FBWDB_SW119.1&names=gaugeModels%2FBWDB_SW131.5&names=gaugeModels%2FBWDB_SW132.2&names=gaugeModels%2FBWDB_SW133&names=gaugeModels%2FBWDB_SW137A&names=gaugeModels%2FBWDB_SW14.5&names=gaugeModels%2FBWDB_SW140&names=gaugeModels%2FBWDB_SW142.1&names=gaugeModels%2FBWDB_SW145&names=gaugeModels%2FBWDB_SW147.5&names=gaugeModels%2FBWDB_SW157&names=gaugeModels%2FBWDB_SW159&names=gaugeModels%2FBWDB_SW15J&names=gaugeModels%2FBWDB_SW162&names=gaugeModels%2FBWDB_SW172&names=gaugeModels%2FBWDB_SW173&names=gaugeModels%2FBWDB_SW175.5&names=gaugeModels%2FBWDB_SW177&names=gaugeModels%2FBWDB_SW179&names=gaugeModels%2FBWDB_SW201&names=gaugeModels%2FBWDB_SW202&names=gaugeModels%2FBWDB_SW203&names=gaugeModels%2FBWDB_SW204&names=gaugeModels%2FBWDB_SW206&names=gaugeModels%2FBWDB_SW207&names=gaugeModels%2FBWDB_SW211.5&names=gaugeModels%2FBWDB_SW212&names=gaugeModels%2FBWDB_SW225&names=gaugeModels%2FBWDB_SW228&names=gaugeModels%2FBWDB_SW236&names=gaugeModels%2FBWDB_SW238&names=gaugeModels%2FBWDB_SW247&names=gaugeModels%2FBWDB_SW248&names=gaugeModels%2FBWDB_SW251&names=gaugeModels%2FBWDB_SW263&names=gaugeModels%2FBWDB_SW263.1&names=gaugeModels%2FBWDB_SW266&names=gaugeModels%2FBWDB_SW267&names=gaugeModels%2FBWDB_SW269&names=gaugeModels%2FBWDB_SW269.5&names=gaugeModels%2FBWDB_SW270&names=gaugeModels%2FBWDB_SW273&names=gaugeModels%2FBWDB_SW274&names=gaugeModels%2FBWDB_SW275.5&names=gaugeModels%2FBWDB_SW277&names=gaugeModels%2FBWDB_SW285&names=gaugeModels%2FBWDB_SW291.5R&names=gaugeModels%2FBWDB_SW294&names=gaugeModels%2FBWDB_SW299&names=gaugeModels%2FBWDB_SW302&names=gaugeModels%2FBWDB_SW36&names=gaugeModels%2FBWDB_SW3A&names=gaugeModels%2FBWDB_SW42&names=gaugeModels%2FBWDB_SW45&names=gaugeModels%2FBWDB_SW45.5&names=gaugeModels%2FBWDB_SW46.9L&names=gaugeModels%2FBWDB_SW49&names=gaugeModels%2FBWDB_SW49A&names=gaugeModels%2FBWDB_SW5&names=gaugeModels%2FBWDB_SW50.6&names=gaugeModels%2FBWDB_SW62&names=gaugeModels%2FBWDB_SW63&names=gaugeModels%2FBWDB_SW65&names=gaugeModels%2FBWDB_SW67&names=gaugeModels%2FBWDB_SW68.5&names=gaugeModels%2FBWDB_SW71A&names=gaugeModels%2FBWDB_SW72&names=gaugeModels%2FBWDB_SW77&names=gaugeModels%2FBWDB_SW84&names=gaugeModels%2FBWDB_SW88&names=gaugeModels%2FBWDB_SW88A&names=gaugeModels%2FBWDB_SW90&names=gaugeModels%2FBWDB_SW91.9R&names=gaugeModels%2FBWDB_SW93.4L&names=gaugeModels%2FBWDB_SW93.5L&names=gaugeModels%2FBWDB_SW95&names=gaugeModels%2FBWDB_SW99&names=gaugeModels%2FCWC_012-MDSIL + response: + body: + string: !!binary | + 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 + IquH+H9eC0mfvx59PvoHXlAKZG1VAAA= + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sat, 31 Aug 2024 00:23:43 GMT + Server: + - ESF + Server-Timing: + - gfet4t7; dur=406 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/api/app/tests/test_google_floods_api.py b/api/app/tests/test_google_floods_api.py new file mode 100644 index 000000000..3d662f405 --- /dev/null +++ b/api/app/tests/test_google_floods_api.py @@ -0,0 +1,126 @@ +import pytest +from app.googleflood import get_google_floods_gauges +from app.main import app +from fastapi.testclient import TestClient + +client = TestClient(app) + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], +) +def test_get_google_floods_gauges(): + """ + Test get_google_floods_gauges returns valid GeoJSON. + """ + gauges = get_google_floods_gauges(["BD"]) + assert gauges["type"] == "FeatureCollection" + assert len(gauges["features"]) > 0 + 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"] + assert "siteName" in feature["properties"] + assert "thresholds" in feature["properties"] + assert "gaugeValueUnit" in feature["properties"] + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], + ignore_hosts=["testserver"], +) +def test_get_google_floods_gauges_api(): + """ + Test API endpoint for Google Floods gauges. + """ + response = client.get("/google-floods/gauges/?region_codes=BD") + assert response.status_code == 200 + + response_geojson = response.json() + assert response_geojson["type"] == "FeatureCollection" + assert len(response_geojson["features"]) > 0 + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], + ignore_hosts=["testserver"], +) +def test_get_google_floods_gauges_api_case_insensitive(): + """ + Test API with case insensitive region code. + """ + response = client.get("/google-floods/gauges/?region_codes=bd") + assert response.status_code == 200 + + response_geojson = response.json() + assert response_geojson["type"] == "FeatureCollection" + assert len(response_geojson["features"]) > 0 + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], + ignore_hosts=["testserver"], +) +def test_get_google_floods_gauges_api_requires_valid_region_code(): + """ + Test API with invalid region code. + """ + response = client.get("/google-floods/gauges/?region_codes=usa") + assert response.status_code == 400 + assert ( + response.json()["detail"] + == "Region code 'usa' must be exactly two characters (iso2)." + ) + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], + ignore_hosts=["testserver"], +) +def test_get_google_floods_gauge_forecast(): + """ + Test API for Google Floods gauge forecast. + """ + response = client.get("/google-floods/gauges/forecasts/?gauge_ids=hybas_1121465590") + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert len(response_json["hybas_1121465590"]) > 0 + assert isinstance(response_json["hybas_1121465590"][0]["value"][0], str) + assert isinstance(response_json["hybas_1121465590"][0]["value"][1], float) + + +@pytest.mark.vcr( + match_on=["uri", "method"], + filter_headers=["Authorization"], + filter_query_parameters=["key"], + ignore_hosts=["testserver"], +) +def test_get_google_floods_gauge_forecast_multiple_gauges(): + """ + Test API for multiple gauge forecasts. + """ + gauge_ids = ["hybas_1121465590", "hybas_1121499110"] + response = client.get( + "/google-floods/gauges/forecasts/?gauge_ids=" + ",".join(gauge_ids) + ) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 2 + for gauge_id, gauge in response_json.items(): + assert gauge_id in gauge_ids + assert len(gauge) > 0 + assert isinstance(gauge[0]["value"][0], str) + assert isinstance(gauge[0]["value"][1], float) diff --git a/api/app/utils.py b/api/app/utils.py index f3ee8136c..7e7de6929 100644 --- a/api/app/utils.py +++ b/api/app/utils.py @@ -2,6 +2,9 @@ import requests from fastapi import HTTPException +from requests.adapters import Retry + +logger = logging.getLogger(__name__) def forward_http_error(resp: requests.Response, excluded_codes: list[int]) -> None: @@ -38,3 +41,45 @@ def filter(self, record): self.warning_count += 1 return False + + +def make_request_with_retries( + url: str, + method: str = "get", + data: dict = None, + retries: int = 1, + timeout: int = 10, +) -> dict: + """Make a request with retries and error handling.""" + retry_strategy = Retry( + total=retries, + backoff_factor=0.25, + status_forcelist=[429, 500, 502, 503, 504], + ) + + with requests.Session() as session: + adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy) + session.mount("https://", adapter) + + try: + if method.lower() == "post": + response = session.post(url, json=data, timeout=timeout) + else: + response = session.get(url, timeout=timeout) + + response.raise_for_status() + response_data = response.json() + + except requests.exceptions.RequestException as e: + logger.warning("Request failed at url %s: %s", url, e) + raise HTTPException( + status_code=500, detail=f"Error fetching data from {url}" + ) + + if "error" in response_data: + logger.error("Error in response: %s", response_data["error"]) + raise HTTPException( + status_code=500, detail=f"Error fetching data from {url}" + ) + + return response_data diff --git a/api/docker-compose.deploy.yml b/api/docker-compose.deploy.yml index b1427ec32..eebedd784 100644 --- a/api/docker-compose.deploy.yml +++ b/api/docker-compose.deploy.yml @@ -52,6 +52,7 @@ services: - HDC_TOKEN=${HDC_TOKEN:?"Provide a token to access HDC chart data."} - STAC_AWS_ACCESS_KEY_ID=${STAC_AWS_ACCESS_KEY_ID:?"Provide the AWS access key for the stac api."} - STAC_AWS_SECRET_ACCESS_KEY=${STAC_AWS_SECRET_ACCESS_KEY:?"Provide the AWS secret key for the stac api."} + - GOOGLE_FLOODS_API_KEY=${GOOGLE_FLOODS_API_KEY:?"Provide the Google Floods API key."} command: uvicorn app.main:app --host 0.0.0.0 --port 80 restart: always # adding a DNS to allow self lookup of the api diff --git a/api/docker-compose.develop.yml b/api/docker-compose.develop.yml index b74eff683..50c729cc2 100644 --- a/api/docker-compose.develop.yml +++ b/api/docker-compose.develop.yml @@ -23,6 +23,7 @@ services: - KOBO_USERNAME=${KOBO_USERNAME:?"Add a user to access KOBO data."} - STAC_AWS_ACCESS_KEY_ID=${STAC_AWS_ACCESS_KEY_ID} - STAC_AWS_SECRET_ACCESS_KEY=${STAC_AWS_SECRET_ACCESS_KEY} + - GOOGLE_FLOODS_API_KEY=${GOOGLE_FLOODS_API_KEY} command: uvicorn app.main:app --host 0.0.0.0 --port 80 --reload # Infinite loop, to keep it alive, for debugging # command: bash -c "while true; do echo 'sleeping...' && sleep 10; done" diff --git a/api/poetry.lock b/api/poetry.lock index 341d641ad..eaa030ce2 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "affine" @@ -1750,6 +1750,25 @@ pytest = ">=6.2.4,<8.0.0" pytest-base-url = ">=1.0.0,<3.0.0" python-slugify = ">=6.0.0,<9.0.0" +[[package]] +name = "pytest-recording" +version = "0.13.2" +description = "A pytest plugin that allows you recording of network interactions via VCR.py" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_recording-0.13.2-py3-none-any.whl", hash = "sha256:3820fe5743d1ac46e807989e11d073cb776a60bdc544cf43ebca454051b22d13"}, + {file = "pytest_recording-0.13.2.tar.gz", hash = "sha256:000c3babbb466681457fd65b723427c1779a0c6c17d9e381c3142a701e124877"}, +] + +[package.dependencies] +pytest = ">=3.5.0" +vcrpy = ">=2.0.1" + +[package.extras] +dev = ["pytest-httpbin", "pytest-mock", "requests", "werkzeug (==3.0.3)"] +tests = ["pytest-httpbin", "pytest-mock", "requests", "werkzeug (==3.0.3)"] + [[package]] name = "pytest-subtests" version = "0.7.0" @@ -2689,6 +2708,26 @@ files = [ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "vcrpy" +version = "6.0.1" +description = "Automatically mock your HTTP interactions to simplify and speed up testing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "vcrpy-6.0.1-py2.py3-none-any.whl", hash = "sha256:621c3fb2d6bd8aa9f87532c688e4575bcbbde0c0afeb5ebdb7e14cac409edfdd"}, + {file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"}, +] + +[package.dependencies] +PyYAML = "*" +urllib3 = {version = "<2", markers = "platform_python_implementation == \"PyPy\""} +wrapt = "*" +yarl = "*" + +[package.extras] +tests = ["Werkzeug (==2.0.3)", "aiohttp", "boto3", "httplib2", "httpx", "pytest", "pytest-aiohttp", "pytest-asyncio", "pytest-cov", "pytest-httpbin", "requests (>=2.22.0)", "tornado", "urllib3"] + [[package]] name = "watchfiles" version = "0.21.0" @@ -2872,6 +2911,85 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "xarray" version = "2023.1.0" @@ -3001,4 +3119,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "d72c32d0bb241fe01323ba3758b9002d66ba348d2633d1b2b36d0de6a277e714" +content-hash = "df68cc98d18f738f802ed3de16c1c29dcdecf6808da9e5cc001fc987dfc640d8" diff --git a/api/pyproject.toml b/api/pyproject.toml index d11968ec2..ec556935c 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -29,6 +29,7 @@ pytest-asyncio = "^0.21.1" pytest-playwright = "^0.4.3" rasterio = "^1.3.9" rioxarray = "^0.17.0" +pytest-recording = "^0.13.2" [tool.poetry.group.dev.dependencies] black = "^24.3.0" diff --git a/api/set_envs.sh b/api/set_envs.sh index 842c30652..b9268e743 100644 --- a/api/set_envs.sh +++ b/api/set_envs.sh @@ -19,5 +19,8 @@ export HDC_TOKEN=$(aws secretsmanager get-secret-value --secret-id HDC_TOKEN export ACLED_API_KEY=$(aws secretsmanager get-secret-value --secret-id ACLED_CREDENTIALS | jq .SecretString | jq fromjson | jq -r .ACLED_API_KEY) export ACLED_API_EMAIL=$(aws secretsmanager get-secret-value --secret-id ACLED_CREDENTIALS | jq .SecretString | jq fromjson | jq -r .ACLED_API_EMAIL) +# Google Flood +export GOOGLE_FLOODS_API_KEY=$(aws secretsmanager get-secret-value --secret-id GOOGLE_FLOODS_API_KEY | jq .SecretString | jq fromjson | jq -r .GOOGLE_FLOODS_API_KEY) + export HOSTNAME=prism-api.ovio.org export INFO_EMAIL=info@ovio.org diff --git a/frontend/package.json b/frontend/package.json index 80d911e7f..6253869a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -68,6 +68,7 @@ "babel-eslint": "10.0.3", "canvas": "link:./node_modules/.cache/null", "chart.js": "^2.9.3", + "chartjs-plugin-annotation": "0.5.7", "chartjs-plugin-datalabels": "^1.0.0", "colormap": "^2.3.1", "comlink": "^4.4.1", diff --git a/frontend/src/components/Common/Chart/index.tsx b/frontend/src/components/Common/Chart/index.tsx index bc01ccd32..7174a3ce5 100644 --- a/frontend/src/components/Common/Chart/index.tsx +++ b/frontend/src/components/Common/Chart/index.tsx @@ -1,6 +1,7 @@ -import React, { memo, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import colormap from 'colormap'; import { ChartOptions } from 'chart.js'; +import 'chartjs-plugin-annotation'; import { Bar, Line } from 'react-chartjs-2'; import { ChartConfig, DatasetField } from 'config/types'; import { TableData } from 'context/tableStateSlice'; @@ -31,14 +32,14 @@ function downloadChartPng(ref: React.RefObject, filename: string) { const useStyles = makeStyles(() => ({ firstIcon: { position: 'absolute', - top: 0, - right: 0, + top: '8px', + right: '0rem', padding: '0.25rem', }, secondIcon: { position: 'absolute', - top: 0, - right: '2rem', + top: '8px', + right: '1.75rem', padding: '0.25rem', }, })); @@ -56,6 +57,8 @@ export type ChartProps = { showDownloadIcons?: boolean; iconStyles?: React.CSSProperties; downloadFilenamePrefix?: string[]; + units?: string; + yAxisLabel?: string; }; const Chart = memo( @@ -72,11 +75,18 @@ const Chart = memo( showDownloadIcons = false, iconStyles, downloadFilenamePrefix = [], + units, + yAxisLabel, }: ChartProps) => { const { t } = useSafeTranslation(); const classes = useStyles(); const chartRef = React.useRef(null); + // This isChartReady state allows us to trigger a render after the chart is ready to update the saved ref + const [_isChartReady, setIsChartReady] = useState(false); + const isEWSChart = !!data.EWSConfig; + const isGoogleFloodChart = !!data.GoogleFloodConfig; + const isFloodChart = isEWSChart || isGoogleFloodChart; const downloadFilename = buildCsvFileName([ ...downloadFilenamePrefix, @@ -149,11 +159,19 @@ const Chart = memo( backgroundColor: colors[i], borderColor: colors[i], borderWidth: 2, - pointRadius: isEWSChart ? 0 : 1, // Disable point rendering for EWS only. + pointRadius: isFloodChart ? 0 : 1, // Disable point rendering for flood charts only. data: indices.map(index => (row[index] as number) || null), pointHitRadius: 10, })), - [colors, config.category, config.fill, indices, isEWSChart, t, tableRows], + [ + colors, + config.category, + config.fill, + indices, + isFloodChart, + t, + tableRows, + ], ); const configureIndicePointRadius = useCallback( @@ -165,9 +183,9 @@ const Chart = memo( if (foundDataSetFieldPointRadius !== undefined) { return foundDataSetFieldPointRadius; } - return isEWSChart ? 0 : 1; // Disable point rendering for EWS only. + return isFloodChart ? 0 : 1; // Disable point rendering for flood charts only. }, - [isEWSChart, datasetFields, header], + [isFloodChart, datasetFields, header], ); // The indicesDataSet @@ -178,7 +196,7 @@ const Chart = memo( fill: config.fill || false, backgroundColor: colors[i], borderColor: colors[i], - borderWidth: 2, + borderWidth: 3, data: tableRows.map(row => (row[indiceKey] as number) || null), pointRadius: configureIndicePointRadius(indiceKey), pointHitRadius: 10, @@ -194,7 +212,7 @@ const Chart = memo( ], ); - const EWSthresholds = useMemo(() => { + const floodThresholds = useMemo(() => { if (data.EWSConfig) { return Object.values(data.EWSConfig).map(obj => ({ label: obj.label, @@ -208,8 +226,21 @@ const Chart = memo( fill: false, })); } + if (data.GoogleFloodConfig) { + return Object.values(data.GoogleFloodConfig).map(obj => ({ + label: obj.label, + backgroundColor: obj.color, + borderColor: obj.color, + borderWidth: 2, + pointRadius: 0, + pointHitRadius: 10, + // Deep copy is needed: https://github.com/reactchartjs/react-chartjs-2/issues/524#issuecomment-722814079 + data: [...obj.values], + fill: false, + })); + } return []; - }, [data.EWSConfig]); + }, [data.EWSConfig, data.GoogleFloodConfig]); /** * The following value assumes that the data is formatted as follows: @@ -237,21 +268,73 @@ const Chart = memo( * using config.transpose = true. * - fill */ + + const today = useMemo(() => { + const date = new Date(); + date.setHours(0, 0, 0, 0); + return date; + }, []); + + const parseDateString = (dateString: string) => { + const [year, month, day] = dateString.split('-').map(Number); + const date = new Date(year, month - 1, day); + date.setHours(0, 0, 0, 0); + return date; + }; + + const isFutureDate = useCallback( + (dateString: string) => parseDateString(dateString) >= today, + [today], + ); + const isPastDate = useCallback( + (dateString: string) => parseDateString(dateString) <= today, + [today], + ); + const datasets = !transpose ? tableRowsDataSet : indicesDataSet; - const datasetsWithThresholds = [...datasets, ...EWSthresholds]; + const datasetsWithThresholds = [...datasets, ...floodThresholds]; const datasetsTrimmed = datasetsWithThresholds.map(set => ({ ...set, data: set.data.slice(chartRange[0], chartRange[1]), })); - const chartData = React.useMemo( - () => ({ + const chartData = React.useMemo(() => { + if (isGoogleFloodChart) { + const pastDatasets = datasets.map(dataset => ({ + ...dataset, + data: dataset.data.map((point, index) => + isPastDate(labels[index] as string) ? point : null, + ), + borderDash: 0, + })); + const futureDatasets = datasets.map(dataset => ({ + ...dataset, + label: t(`${dataset.label} (Future)`), + data: dataset.data.map((point, index) => + isFutureDate(labels[index] as string) ? point : null, + ), + borderDash: [5, 5], + })); + return { + labels, + datasets: [...pastDatasets, ...futureDatasets, ...floodThresholds], + }; + } + return { labels, datasets: datasetsTrimmed, - }), - [datasetsTrimmed, labels], - ); + }; + }, [ + isGoogleFloodChart, + labels, + floodThresholds, + datasets, + datasetsTrimmed, + isPastDate, + t, + isFutureDate, + ]); const chartConfig = useMemo( () => @@ -281,6 +364,8 @@ const Chart = memo( scaleLabel: { labelString: xAxisLabel, display: true, + lineHeight: 1.5, + fontColor: '#AAA', }, } : {}), @@ -292,32 +377,127 @@ const Chart = memo( fontColor: '#CCC', ...(config?.minValue && { suggestedMin: config?.minValue }), ...(config?.maxValue && { suggestedMax: config?.maxValue }), + maxTicksLimit: 8, + callback: (value: string) => + `${value}${units ? ` ${units}` : ''}`, }, stacked: config?.stacked ?? false, gridLines: { display: false, }, + afterDataLimits: axis => { + // Increase y-axis by 20% for Google Flood charts to make space for the annotation label + if (isGoogleFloodChart) { + const range = axis.max - axis.min; + axis.max += range * 0.25; // eslint-disable-line no-param-reassign, fp/no-mutation + } + }, + ...(yAxisLabel + ? { + scaleLabel: { + display: true, + labelString: yAxisLabel, + lineHeight: 1.5, + fontColor: '#AAA', + }, + } + : {}), }, ], }, - // display values for all datasets in the tooltip tooltips: { mode: 'index', + callbacks: { + label: (tooltipItem, labelData) => { + const datasetLabel = + labelData.datasets?.[tooltipItem.datasetIndex as number] + ?.label || ''; + const value = tooltipItem.yLabel; + const unitLabel = units ? ` ${units}` : ''; + + // Get the data point for the current tooltip item + const dataPoint = + labelData.datasets?.[tooltipItem.datasetIndex as number] + ?.data?.[tooltipItem.index as number]; + + // Check if any label is present in the tooltip + const labelPresent = labelData.datasets?.some(dataset => { + const { label } = dataset; + if (tooltipItem.index !== undefined) { + const indexData = dataset.data?.[tooltipItem.index]; + return ( + label === datasetLabel.replace(' (Future)', '') && + indexData !== null + ); + } + return false; + }); + + // Hide "{label} (Future)" if "{label}" is present + if (labelPresent && datasetLabel.includes(' (Future)')) { + return null; + } + + // Only show labels with non-null data points + if (dataPoint !== null) { + return `${datasetLabel}: ${value}${unitLabel}`; + } + + return null; + }, + }, }, legend: { display: config.displayLegend, position: legendAtBottom ? 'bottom' : 'right', labels: { boxWidth: 12, boxHeight: 12 }, }, + animation: { + onComplete: () => { + setIsChartReady(true); + }, + }, + ...(isGoogleFloodChart + ? { + annotation: { + annotations: [ + { + type: 'line', + mode: 'vertical', + scaleID: 'x-axis-0', + value: today.toISOString().split('T')[0], + borderColor: 'rgba(255, 255, 255, 0.8)', + borderWidth: 2, + label: { + content: t('Today'), + enabled: true, + position: 'top', + yAdjust: -6, + fontColor: '#CCC', + fontSize: 10, + }, + }, + ], + }, + } + : {}), }) as ChartOptions, [ - config, - isEWSChart, - legendAtBottom, notMaintainAspectRatio, - title, subtitle, + title, + config?.stacked, + config?.minValue, + config?.maxValue, + config.displayLegend, xAxisLabel, + legendAtBottom, + isGoogleFloodChart, + today, + t, + isEWSChart, + units, + yAxisLabel, ], ); diff --git a/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx b/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx index d7274c47d..12f96b406 100644 --- a/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/ImpactLayer/index.tsx @@ -74,6 +74,7 @@ const onClick = dispatch( addPopupData( getFeatureInfoPropsData( + layer.featureInfoTitle || {}, layer.featureInfoProps || {}, coordinates, feature, diff --git a/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx b/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx index 1fcfb1ccd..b0ab8b458 100644 --- a/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx +++ b/frontend/src/components/MapView/Layers/PointDataLayer/index.tsx @@ -27,7 +27,11 @@ import { fillPaintCategorical, fillPaintData, } from 'components/MapView/Layers/styles'; -import { setEWSParams, clearDataset } from 'context/datasetStateSlice'; +import { + setEWSParams, + setGoogleFloodParams, + clearDataset, +} from 'context/datasetStateSlice'; import { createEWSDatasetParams } from 'utils/ews-utils'; import { addPopupParams } from 'components/MapView/Layers/layer-utils'; import { @@ -38,6 +42,7 @@ import { import { findFeature, getLayerMapId, useMapCallback } from 'utils/map-utils'; import { getFormattedDate } from 'utils/date-utils'; import { geoToH3, h3ToGeoBoundary } from 'h3-js'; +import { createGoogleFloodDatasetParams } from 'utils/google-flood-utils'; const onClick = ({ layer, dispatch, t }: MapEventWrapFunctionProps) => @@ -57,6 +62,18 @@ const onClick = ); dispatch(setEWSParams(ewsDatasetParams)); } + if (layer.loader === PointDataLoader.GOOGLE_FLOOD && layer.detailUrl) { + dispatch(clearDataset()); + if (!feature?.properties) { + return; + } + const googleFloodDatasetParams = createGoogleFloodDatasetParams( + feature?.properties, + layer.detailUrl, + layer.featureInfoTitle, + ); + dispatch(setGoogleFloodParams(googleFloodDatasetParams)); + } }; // Point Data, takes any GeoJSON of points and shows it. diff --git a/frontend/src/components/MapView/Layers/layer-utils.tsx b/frontend/src/components/MapView/Layers/layer-utils.tsx index 99f08eca8..be7a6674f 100644 --- a/frontend/src/components/MapView/Layers/layer-utils.tsx +++ b/frontend/src/components/MapView/Layers/layer-utils.tsx @@ -86,6 +86,7 @@ export const addPopupParams = ( const { dataField, + featureInfoTitle, featureInfoProps, title, dataLabel, @@ -153,6 +154,7 @@ export const addPopupParams = ( : {}; const featureInfoPropsData = getFeatureInfoPropsData( + featureInfoTitle, featureInfoPropsWithFallback || {}, coordinates, feature, diff --git a/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx b/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx index be44e7acd..5ed35a3f4 100644 --- a/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx +++ b/frontend/src/components/MapView/MapTooltip/PointDataChart/PopupPointDataChart.tsx @@ -1,5 +1,5 @@ import Chart from 'components/Common/Chart'; -import { isAdminBoundary } from 'components/MapView/utils'; + import { ChartConfig } from 'config/types'; import { CHART_DATA_PREFIXES, @@ -9,6 +9,8 @@ import { t } from 'i18next'; import { memo } from 'react'; import { useSelector } from 'react-redux'; import { createStyles, makeStyles } from '@material-ui/core'; +import { isAdminBoundary } from 'utils/admin-utils'; +import { GoogleFloodParams } from 'utils/google-flood-utils'; const useStyles = makeStyles(() => createStyles({ @@ -16,7 +18,7 @@ const useStyles = makeStyles(() => display: 'flex', flexDirection: 'column', gap: '8px', - paddingTop: '20px', // leave room for the close icon + paddingTop: '8px', // leave room for the close icon }, chartSection: { paddingTop: '16px', // leave room for the download icons @@ -47,20 +49,25 @@ const PopupPointDataChart = memo(() => { return null; } + const xAxisLabel = isAdminBoundary(datasetParams) + ? undefined + : t('Timestamps reflect local time in region'); + const yAxisLabel = (datasetParams as GoogleFloodParams).yAxisLabel + ? t((datasetParams as GoogleFloodParams).yAxisLabel) + : undefined; + return (
diff --git a/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx b/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx index 55ccf3328..6ff9930ea 100644 --- a/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx +++ b/frontend/src/components/MapView/MapTooltip/PointDataChart/usePointDataChart.tsx @@ -7,7 +7,11 @@ import { dateRangeSelector } from 'context/mapStateSlice/selectors'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { appConfig } from 'config'; -import { isAdminBoundary } from '../../utils'; +import { + GoogleFloodParams, + isGoogleFloodDatasetParams, +} from 'utils/google-flood-utils'; +import { isAdminBoundary } from 'utils/admin-utils'; const usePointDataChart = () => { const dispatch = useDispatch(); @@ -19,31 +23,34 @@ const usePointDataChart = () => { const { startDate: selectedDate } = useSelector(dateRangeSelector); useEffect(() => { - if (!datasetParams || !selectedDate) { - return; - } + if (datasetParams && selectedDate) { + if (isAdminBoundary(datasetParams)) { + const { code: adminCode, level } = + datasetParams.boundaryProps[datasetParams.id]; + const requestParams: DatasetRequestParams = { + id: datasetParams.id, + level, + adminCode: adminCode || appConfig.countryAdmin0Id, + boundaryProps: datasetParams.boundaryProps, + url: datasetParams.url, + serverLayerName: datasetParams.serverLayerName, + datasetFields: datasetParams.datasetFields, + }; + dispatch(loadDataset(requestParams)); + } else { + if (isGoogleFloodDatasetParams(datasetParams as GoogleFloodParams)) { + const requestParams = datasetParams as GoogleFloodParams; + dispatch(loadDataset(requestParams)); + return; + } - if (isAdminBoundary(datasetParams)) { - const { code: adminCode, level } = - datasetParams.boundaryProps[datasetParams.id]; - const requestParams: DatasetRequestParams = { - id: datasetParams.id, - level, - adminCode: adminCode || appConfig.countryAdmin0Id, - boundaryProps: datasetParams.boundaryProps, - url: datasetParams.url, - serverLayerName: datasetParams.serverLayerName, - datasetFields: datasetParams.datasetFields, - }; - dispatch(loadDataset(requestParams)); - } else { - const requestParams: DatasetRequestParams = { - date: selectedDate, - externalId: datasetParams.externalId, - triggerLevels: datasetParams.triggerLevels, - baseUrl: datasetParams.baseUrl, - }; - dispatch(loadDataset(requestParams)); + // Assumes EWSDataset + const requestParams: DatasetRequestParams = { + date: selectedDate, + ...datasetParams, + }; + dispatch(loadDataset(requestParams)); + } } }, [datasetParams, dispatch, selectedDate]); diff --git a/frontend/src/components/MapView/MapTooltip/index.tsx b/frontend/src/components/MapView/MapTooltip/index.tsx index 351b9230d..686a3f4f7 100644 --- a/frontend/src/components/MapView/MapTooltip/index.tsx +++ b/frontend/src/components/MapView/MapTooltip/index.tsx @@ -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 { @@ -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'; @@ -42,11 +49,13 @@ const useStyles = makeStyles(() => marginBottom: '4px', }, popup: { + // Overrides the default maxWidth of 240px set by react-map-gl + maxWidth: 'none !important', + zIndex: 5, '& div.maplibregl-popup-content': { background: 'black', color: 'white', padding: '5px 5px 5px 5px', - maxWidth: '40em', maxHeight: '400px', overflow: 'auto', }, @@ -73,7 +82,7 @@ const MapTooltip = memo(() => { const classes = useStyles(); const dispatch = useDispatch(); const popup = useSelector(tooltipSelector); - const { i18n } = useSafeTranslation(); + const { t, i18n } = useSafeTranslation(); const [popupTitle, setPopupTitle] = useState(''); const [adminLevel, setAdminLevel] = useState( undefined, @@ -81,14 +90,26 @@ 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) { + // Title can be a template requiring interpolation + return t(providedPopupTitle.data as string, providedPopupTitle.context); + } if (isEnglishLanguageSelected(i18n)) { return popup.locationName; } return popup.locationLocalName; - }, [i18n, popup.locationLocalName, popup.locationName]); - - const popupData = popup.data; + }, [ + i18n, + popup.locationLocalName, + popup.locationName, + providedPopupTitle, + t, + ]); // TODO - simplify logic once we revamp admin levels object const adminLevelsNames = useCallback(() => { @@ -119,7 +140,6 @@ const MapTooltip = memo(() => { latitude={popup.coordinates?.[1]} longitude={popup.coordinates?.[0]} className={classes.popup} - style={{ zIndex: 5, maxWidth: 'none' }} closeButton={false} > 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; + } + + 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, @@ -162,6 +173,7 @@ export function getFeatureInfoPropsData( const { properties } = feature; return { + ...getTitle(featureInfoTitle, properties), ...getMetaData(featureInfoProps, metaDataKeys, properties), ...getData(featureInfoProps, keys, properties, coordinates), }; @@ -305,8 +317,3 @@ export const getExposureAnalysisTableData = ( sortColumn: Column['id'], sortOrder: 'asc' | 'desc', ) => orderBy(tableData, sortColumn, sortOrder); - -export const isAdminBoundary = ( - params: AdminBoundaryParams | EWSParams, -): params is AdminBoundaryParams => - (params as AdminBoundaryParams).id !== undefined; diff --git a/frontend/src/config/cambodia/layers.json b/frontend/src/config/cambodia/layers.json index 77481b8f7..a64b6c82d 100644 --- a/frontend/src/config/cambodia/layers.json +++ b/frontend/src/config/cambodia/layers.json @@ -1988,5 +1988,77 @@ } ], "legend_text": "Index meausring a household’s social and economic capacities and resilience to cope with, adapt to and recover from the floods and droughts. Aggregated at Commune level" + }, + "forecast_river_discharge_at_gauges": { + "title": "River discharge forecast", + "loader": "google_flood", + "type": "point_data", + "hex_display": false, + "data": "https://prism-api.ovio.org/google-floods/gauges/?region_codes=KH", + "detail_url": "https://prism-api.ovio.org/google-floods/gauges/forecasts", + "data_field": "severity", + "data_field_type": "text", + "date_url": "https://prism-api.ovio.org/google-floods/dates/?region_codes=KH", + "opacity": 0.9, + "legend_text": "River discharge forecast at verified gauges. Visit [Google Research](https://sites.research.google/floodforecasting/) to learn more about Google's AI forecasting models.", + "legend": [ + { "label": "Extreme", "value": "EXTREME", "color": "#a70606" }, + { "label": "Danger", "value": "SEVERE", "color": "#ea250a" }, + { "label": "Warning", "value": "ABOVE_NORMAL", "color": "#fba705" }, + { "label": "Normal", "value": "NO_FLOODING", "color": "#089180" }, + { "label": "No data", "value": "UNKNOWN", "color": "#858585" } + ], + "feature_info_title": { + "siteName": { + "type": "text", + "template": "Site: {{siteName}}", + "visibility": "if-defined" + }, + "riverName": { + "type": "text", + "template": "River: {{riverName}}", + "visibility": "if-defined" + }, + "gaugeId": { + "type": "text", + "template": "Gauge ID: {{gaugeId}}", + "visibility": "if-defined" + } + }, + "feature_info_props": { + "siteName": { + "type": "text", + "dataTitle": "Site", + "visibility": "if-defined" + }, + "river": { + "type": "text", + "dataTitle": "River", + "visibility": "if-defined" + }, + "severity": { + "type": "labelMapping", + "dataTitle": "Flood Status", + "labelMap": { + "EXTREME": "Extreme", + "SEVERE": "Danger", + "ABOVE_NORMAL": "Warning", + "NO_FLOODING": "Normal", + "UNKNOWN": "No data" + } + }, + "gaugeId": { + "type": "text", + "dataTitle": "Gauge ID" + }, + "source": { + "type": "text", + "dataTitle": "Data source" + }, + "issuedTime": { + "type": "date", + "dataTitle": "Status issued" + } + } } } diff --git a/frontend/src/config/cambodia/prism.json b/frontend/src/config/cambodia/prism.json index 5fa399a30..e6cd0b91e 100644 --- a/frontend/src/config/cambodia/prism.json +++ b/frontend/src/config/cambodia/prism.json @@ -33,7 +33,8 @@ "flood": { "flood_monitoring": ["flood_extent"], "flood_risk": ["flood_hazard"], - "early_warning": ["ews_remote"] + "early_warning": ["ews_remote"], + "river_discharge_forecast": ["forecast_river_discharge_at_gauges"] }, "rainfall": { "forecasts": [ diff --git a/frontend/src/config/cambodia/translation.json b/frontend/src/config/cambodia/translation.json index 713ade12a..4e443ef1a 100644 --- a/frontend/src/config/cambodia/translation.json +++ b/frontend/src/config/cambodia/translation.json @@ -108,7 +108,7 @@ "3 - commune": "3 - ឃុំ", "Commune": "ឃុំ", "Number of people affected": "ចំនួនមនុស្ស", - "Timestamps reflect local time in Cambodia": "ត្រាពេលវេលាឆ្លុះបញ្ចាំងម៉ោងក្នុងស្រុកនៅកម្ពុជា", + "Timestamps reflect local time in region": "ត្រាពេលវេលាឆ្លុះបញ្ចាំងម៉ោងក្នុងស្រុកនៅកម្ពុជា", "EWS 1294 river level data": "EWS 1294 ទិន្នន័យកម្រិតទន្លេ", "Mean water level": "កម្រិតទឹកជាមធ្យម", "Max water level": "កម្រិតទឹកអតិបរមា", diff --git a/frontend/src/config/mozambique/layers.json b/frontend/src/config/mozambique/layers.json index c3d4d701d..f0874e535 100644 --- a/frontend/src/config/mozambique/layers.json +++ b/frontend/src/config/mozambique/layers.json @@ -36,7 +36,7 @@ "line-opacity": 0.8 } } - }, + }, "admin_boundaries": { "type": "boundary", "path": "data/mozambique/moz_bnd_adm3_WFP.json", @@ -2529,7 +2529,7 @@ "type": "admin_level_data", "path": "data/mozambique/ipc/ipc-2022.json", "data_field": "ph3ps_p", - "boundary": "admin2_boundaries", + "boundary": "admin2_boundaries", "admin_level": 2, "admin_code": "adm2_source_id", "opacity": 0.9, @@ -3032,28 +3032,19 @@ { "id": "spi_1m", "importance": 0.5, - "key": [ - "CHIRPS", - "R1S_DEKAD" - ], + "key": ["CHIRPS", "R1S_DEKAD"], "aggregation": "last_dekad" }, { "id": "lst_anomaly", "importance": 0.25, - "key": [ - "MODIS", - "MYD11C2_TDD_DEKAD" - ], + "key": ["MODIS", "MYD11C2_TDD_DEKAD"], "aggregation": "last_dekad" }, { "id": "ndvi_dekad", "importance": 0.25, - "key": [ - "MODIS", - "NDVI_smoothed_5KM" - ], + "key": ["MODIS", "NDVI_smoothed_5KM"], "aggregation": "average", "invert": "True" } diff --git a/frontend/src/config/rbd/layers.json b/frontend/src/config/rbd/layers.json index e4de325a5..40203570f 100644 --- a/frontend/src/config/rbd/layers.json +++ b/frontend/src/config/rbd/layers.json @@ -6405,7 +6405,7 @@ "color": "#e51a19" } ], - "legend_text": "ACLED Conflict analysis 2021 produced by IFPRI. For more details, see [Political violence in the G5 Sahel Countries](https://hdl.handle.net/10568/139670)" + "legend_text": "ACLED Conflict analysis 2021 produced by IFPRI. For more details, see [Political violence in the G5 Sahel Countries](https://hdl.handle.net/10568/139670)" }, "fs_hotspot_2024": { "title": "Food security hotspot analysis", diff --git a/frontend/src/config/shared/translation/english.json b/frontend/src/config/shared/translation/english.json index 8f043550d..e17f67185 100644 --- a/frontend/src/config/shared/translation/english.json +++ b/frontend/src/config/shared/translation/english.json @@ -114,7 +114,7 @@ "The following layer requires authentication": "The following layer requires authentication", "Threshold": "Threshold", "Timeline": "Timeline", - "Timestamps reflect local time in Cambodia": "Timestamps reflect local time in Cambodia", + "Timestamps reflect local time in region": "Timestamps reflect local time in region", "Title": "Title", "Today": "Today", "trig.": "trig.", diff --git a/frontend/src/config/shared/translation/khmer.json b/frontend/src/config/shared/translation/khmer.json index 713ade12a..4e443ef1a 100644 --- a/frontend/src/config/shared/translation/khmer.json +++ b/frontend/src/config/shared/translation/khmer.json @@ -108,7 +108,7 @@ "3 - commune": "3 - ឃុំ", "Commune": "ឃុំ", "Number of people affected": "ចំនួនមនុស្ស", - "Timestamps reflect local time in Cambodia": "ត្រាពេលវេលាឆ្លុះបញ្ចាំងម៉ោងក្នុងស្រុកនៅកម្ពុជា", + "Timestamps reflect local time in region": "ត្រាពេលវេលាឆ្លុះបញ្ចាំងម៉ោងក្នុងស្រុកនៅកម្ពុជា", "EWS 1294 river level data": "EWS 1294 ទិន្នន័យកម្រិតទន្លេ", "Mean water level": "កម្រិតទឹកជាមធ្យម", "Max water level": "កម្រិតទឹកអតិបរមា", diff --git a/frontend/src/config/types.ts b/frontend/src/config/types.ts index f50acfc68..78e4cd669 100644 --- a/frontend/src/config/types.ts +++ b/frontend/src/config/types.ts @@ -267,7 +267,13 @@ export enum Interval { ONE_YEAR = '1-year', } -export type FeatureInfoObject = { [key: string]: FeatureInfoProps }; +export type FeatureTitleObject = { + [key: string]: FeatureInfoTitle; +}; + +export type FeatureInfoObject = { + [key: string]: FeatureInfoProps; +}; export class CommonLayerProps { id: LayerKey; @@ -297,7 +303,9 @@ export class CommonLayerProps { contentPath?: string; @optional - featureInfoProps?: { [key: string]: FeatureInfoProps }; + featureInfoTitle?: FeatureTitleObject; + @optional + featureInfoProps?: FeatureInfoObject; /* * only for layer that has grouped menu and always assigned to main layer of group (../components/Navbar/utils.ts) @@ -396,13 +404,26 @@ export enum DataType { LabelMapping = 'labelMapping', } +export enum FeatureInfoVisibility { + Always = 'always', // Default + IfDefined = 'if-defined', +} + type PopupMetaDataKeys = keyof PopupMetaData; -interface FeatureInfoProps { +export interface FeatureInfoTitle { + type: DataType; + template: string; + labelMap?: { [key: string]: string }; + visibility?: FeatureInfoVisibility; +} + +export interface FeatureInfoProps { type: DataType; dataTitle: string; labelMap?: { [key: string]: string }; metadata?: PopupMetaDataKeys; + visibility?: FeatureInfoVisibility; } export enum DatesPropagation { @@ -668,6 +689,7 @@ export class ImpactLayerProps extends CommonLayerProps { export enum PointDataLoader { EWS = 'ews', ACLED = 'acled', + GOOGLE_FLOOD = 'google_flood', } export class PointDataLayerProps extends CommonLayerProps { @@ -722,6 +744,9 @@ export class PointDataLayerProps extends CommonLayerProps { @optional dataFieldType?: DataFieldType = DataFieldType.NUMBER; + + @optional + detailUrl?: string; } export type RequiredKeys = { @@ -869,7 +894,7 @@ type AdminLevelDisplayType = { export type PointData = { lat: number; lon: number; - date: number; // in unix time. + date?: number; // in unix time. [key: string]: any; }; diff --git a/frontend/src/context/datasetStateSlice.ts b/frontend/src/context/datasetStateSlice.ts index a440b5ea5..c2fdcff44 100644 --- a/frontend/src/context/datasetStateSlice.ts +++ b/frontend/src/context/datasetStateSlice.ts @@ -10,6 +10,11 @@ import { } from 'utils/ews-utils'; import { fetchWithTimeout } from 'utils/fetch-with-timeout'; import { getFormattedDate, getTimeInMilliseconds } from 'utils/date-utils'; +import { + FloodSensorData, + GoogleFloodParams, + GoogleFloodTriggersConfig, +} from 'utils/google-flood-utils'; import type { CreateAsyncThunkTypes, RootState } from './store'; import { TableData } from './tableStateSlice'; @@ -32,7 +37,7 @@ type EWSDataPointsRequestParams = EWSParams & { type DatasetState = { data?: TableData; isLoading: boolean; - datasetParams?: AdminBoundaryParams | EWSParams; + datasetParams?: AdminBoundaryParams | EWSParams | GoogleFloodParams; chartType: ChartType; title: string; }; @@ -69,7 +74,8 @@ export type AdminBoundaryRequestParams = AdminBoundaryParams & { export type DatasetRequestParams = | AdminBoundaryRequestParams - | EWSDataPointsRequestParams; + | EWSDataPointsRequestParams + | GoogleFloodParams; type DataItem = { date: number; @@ -169,6 +175,63 @@ export const loadEWSDataset = async ( }); }; +export const loadGoogleFloodDataset = async ( + params: GoogleFloodParams, + dispatch: Dispatch, +): Promise => { + const { gaugeId, triggerLevels, detailUrl } = params; + + const url = `${detailUrl}?gauge_ids=${gaugeId}`; + + let dataPoints: { [key: string]: FloodSensorData[] } = {}; + try { + const resp = await fetchWithTimeout( + url, + dispatch, + {}, + `Request failed for fetching Google Flood data points by location at ${url}`, + ); + // eslint-disable-next-line fp/no-mutation + dataPoints = await resp.json(); + } catch (error) { + console.error(error); + } + + const results: DataItem[] = dataPoints[gaugeId].map(item => { + const [measureDate, value] = item.value; + // offset back from UTC to local time so that the date is displayed correctly + // i.e. in Cambodia Time as it is received. + const offset = new Date().getTimezoneOffset(); + return { + date: getTimeInMilliseconds(measureDate) - offset * 60 * 1000, + values: { Measure: value.toString() }, + }; + }); + + const tableData = createTableData(results, TableDataFormat.DATE); + + const GoogleFloodConfig = Object.entries(triggerLevels).reduce( + (acc, [key, value]) => { + const obj = { + ...GoogleFloodTriggersConfig[key], + values: tableData.rows.map(() => value), + }; + + return { ...acc, [key]: obj }; + }, + {}, + ); + + const tableDataWithGoogleFloodConfig: TableData = { + ...tableData, + GoogleFloodConfig, + }; + + return new Promise(resolve => { + resolve(tableDataWithGoogleFloodConfig); + }); +}; + type HDCResponse = { data: { [key: string]: number[] }; date: string[]; @@ -282,10 +345,18 @@ export const loadDataset = createAsyncThunk< CreateAsyncThunkTypes >( 'datasetState/loadDataset', - async (params: DatasetRequestParams, { dispatch }) => - (params as AdminBoundaryRequestParams).id - ? loadAdminBoundaryDataset(params as AdminBoundaryRequestParams, dispatch) - : loadEWSDataset(params as EWSDataPointsRequestParams, dispatch), + async (params: DatasetRequestParams, { dispatch }) => { + if ((params as AdminBoundaryRequestParams).id) { + return loadAdminBoundaryDataset( + params as AdminBoundaryRequestParams, + dispatch, + ); + } + if ((params as EWSDataPointsRequestParams).date) { + return loadEWSDataset(params as EWSDataPointsRequestParams, dispatch); + } + return loadGoogleFloodDataset(params as GoogleFloodParams, dispatch); + }, ); export const datasetResultStateSlice = createSlice({ @@ -332,6 +403,32 @@ export const datasetResultStateSlice = createSlice({ title: chartTitle, }; }, + setGoogleFloodParams: ( + state, + { payload }: PayloadAction, + ): DatasetState => { + const { + gaugeId, + triggerLevels, + detailUrl, + chartTitle, + unit, + yAxisLabel, + } = payload; + + return { + ...state, + datasetParams: { + gaugeId, + triggerLevels, + chartTitle, + detailUrl, + unit, + yAxisLabel, + }, + title: chartTitle, + }; + }, }, extraReducers: builder => { builder.addCase( @@ -369,6 +466,7 @@ export const { setDatasetTitle, setDatasetChartType, setEWSParams, + setGoogleFloodParams, } = datasetResultStateSlice.actions; export default datasetResultStateSlice.reducer; diff --git a/frontend/src/context/tableStateSlice.ts b/frontend/src/context/tableStateSlice.ts index e2d2ddb81..3690dca72 100644 --- a/frontend/src/context/tableStateSlice.ts +++ b/frontend/src/context/tableStateSlice.ts @@ -2,14 +2,30 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import * as Papa from 'papaparse'; import { TableType } from 'config/types'; import { TableDefinitions } from 'config/utils'; -import { EWSChartItemsObject } from 'utils/ews-utils'; import type { CreateAsyncThunkTypes, RootState } from './store'; export type TableRowType = { [key: string]: string | number }; + +type FloodChartConfig = { + label: string; + color: string; +}; + +type FloodChartItem = FloodChartConfig & { values: number[] }; + +export type FloodChartConfigObject = { + [key: string]: FloodChartConfig; +}; + +type FloodChartItemsObject = { + [key: string]: FloodChartItem; +}; + export type TableData = { columns: string[]; rows: TableRowType[]; - EWSConfig?: EWSChartItemsObject; + EWSConfig?: FloodChartItemsObject; + GoogleFloodConfig?: FloodChartItemsObject; }; type TableState = { diff --git a/frontend/src/context/tooltipStateSlice.ts b/frontend/src/context/tooltipStateSlice.ts index 471b697bc..265b7da8e 100644 --- a/frontend/src/context/tooltipStateSlice.ts +++ b/frontend/src/context/tooltipStateSlice.ts @@ -3,6 +3,16 @@ import { merge } from 'lodash'; import { AdminCodeString } from 'config/types'; import type { RootState } from './store'; +export interface PopupTitleData { + title: { + prop: string; + data: number | string | null; + context?: { + [key: string]: string; + }; + }; +} + export interface PopupData { [key: string]: { data: number | string | null; @@ -22,7 +32,7 @@ export interface MapTooltipState { locationSelectorKey: string; locationName: string; locationLocalName: string; - data: PopupData & PopupMetaData; + data: (PopupData & PopupMetaData) | PopupTitleData; showing: boolean; wmsGetFeatureInfoLoading: boolean; } diff --git a/frontend/src/utils/admin-utils.ts b/frontend/src/utils/admin-utils.ts index 3310d2950..c978a99d1 100644 --- a/frontend/src/utils/admin-utils.ts +++ b/frontend/src/utils/admin-utils.ts @@ -10,8 +10,9 @@ import { getBoundaryLayersByAdminLevel, getDisplayBoundaryLayers, } from 'config/utils'; -import { AdminBoundaryParams } from 'context/datasetStateSlice'; +import { AdminBoundaryParams, EWSParams } from 'context/datasetStateSlice'; import { CHART_API_URL } from 'utils/constants'; +import { GoogleFloodParams } from './google-flood-utils'; const { multiCountry } = appConfig; const MAX_ADMIN_LEVEL = multiCountry ? 3 : 2; @@ -89,3 +90,8 @@ export const getChartAdminBoundaryParams = ( return adminBoundaryParams; }; + +export const isAdminBoundary = ( + params: AdminBoundaryParams | EWSParams | GoogleFloodParams, +): params is AdminBoundaryParams => + (params as AdminBoundaryParams).id !== undefined; diff --git a/frontend/src/utils/ews-utils.ts b/frontend/src/utils/ews-utils.ts index 53a90b4ad..d166d6527 100644 --- a/frontend/src/utils/ews-utils.ts +++ b/frontend/src/utils/ews-utils.ts @@ -2,35 +2,26 @@ import GeoJSON, { FeatureCollection, Point } from 'geojson'; import { Dispatch } from 'redux'; import { PointData, PointLayerData } from 'config/types'; import { oneDayInMs } from 'components/MapView/LeftPanel/utils'; +import { FloodChartConfigObject } from 'context/tableStateSlice'; import { fetchWithTimeout } from './fetch-with-timeout'; import { getFormattedDate } from './date-utils'; import { DateFormat } from './name-utils'; -type EWSChartConfig = { - label: string; - color: string; -}; - -type EWSChartItem = EWSChartConfig & { values: number[] }; - -export type EWSChartConfigObject = { [key: string]: EWSChartConfig }; -export type EWSChartItemsObject = { [key: string]: EWSChartItem }; - -export const EWSTriggersConfig: EWSChartConfigObject = { +export const EWSTriggersConfig: FloodChartConfigObject = { normal: { - label: 'normal', + label: 'Normal', color: '#1a9641', }, watchLevel: { - label: 'watch level', + label: 'Watch level', color: '#f9d84e', }, warning: { - label: 'warning', + label: 'Warning', color: '#fdae61', }, severeWarning: { - label: 'severe warning', + label: 'Severe warning', color: '#e34a33', }, }; diff --git a/frontend/src/utils/fetch-with-timeout.ts b/frontend/src/utils/fetch-with-timeout.ts index f5172e155..ae8718ce0 100644 --- a/frontend/src/utils/fetch-with-timeout.ts +++ b/frontend/src/utils/fetch-with-timeout.ts @@ -8,7 +8,7 @@ interface FetchWithTimeoutOptions extends RequestInit { export const ANALYSIS_REQUEST_TIMEOUT = 60000; -const DEFAULT_REQUEST_TIMEOUT = 15000; +const DEFAULT_REQUEST_TIMEOUT = 30000; export const fetchWithTimeout = async ( resource: RequestInfo, diff --git a/frontend/src/utils/google-flood-utils.ts b/frontend/src/utils/google-flood-utils.ts new file mode 100644 index 000000000..2845cb233 --- /dev/null +++ b/frontend/src/utils/google-flood-utils.ts @@ -0,0 +1,123 @@ +import { oneDayInMs } from 'components/MapView/LeftPanel/utils'; +import { getTitle } from 'utils/title-utils'; +import { FeatureTitleObject } from 'config/types'; +import { PopupData } from 'context/tooltipStateSlice'; +import { FloodChartConfigObject } from 'context/tableStateSlice'; +import { t } from 'i18next'; + +export type GoogleFloodParams = { + gaugeId: string; + triggerLevels: GoogleFloodTriggerLevels; + detailUrl: string; + chartTitle: string; + unit: string; + yAxisLabel: string; +}; + +const GOOGLE_FLOOD_UNITS = { + GAUGE_VALUE_UNIT_UNSPECIFIED: '', + METERS: 'm', + CUBIC_METERS_PER_SECOND: 'm³/s', +}; + +const GOOGLE_FLOOD_Y_AXIS_LABEL = { + GAUGE_VALUE_UNIT_UNSPECIFIED: '', + METERS: 'Unit = water depth in m', + CUBIC_METERS_PER_SECOND: 'Unit = discharge in m³/s', +}; + +export const GoogleFloodTriggersConfig: FloodChartConfigObject = { + normal: { + label: 'Normal', + color: '#1a9641', + }, + warning: { + label: 'Warning', + color: '#f9d84e', + }, + danger: { + label: 'Danger', + color: '#fdae61', + }, + extremeDanger: { + label: 'Extreme danger', + color: '#e34a33', + }, +}; + +/* eslint-disable camelcase */ +export type FloodSensorData = { + location_id: number; + value: [string, number]; +}; + +type GoogleFloodTriggerLevels = { + warning: number; + danger: number; + extremeDanger: number; +}; +/* eslint-enable camelcase */ + +// input parameter is used here only for testing +export const createGoogleFloodDatesArray = (testEndDate?: number): number[] => { + const datesArray = []; + + const now = new Date(); + + const endDate = testEndDate + ? new Date(testEndDate).setUTCHours(12, 0, 0, 0) + : now.setUTCHours(12, 0, 0, 0); + + const tempDate = new Date('2021-01-01'); + tempDate.setUTCHours(12, 0, 0, 0); + + while (tempDate.getTime() <= endDate) { + // eslint-disable-next-line fp/no-mutating-methods + datesArray.push(tempDate.getTime()); + + tempDate.setTime(tempDate.getTime() + oneDayInMs); + } + + return datesArray; +}; + +export const createGoogleFloodDatasetParams = ( + featureProperties: any, + detailUrl: string, + featureInfoTitle?: FeatureTitleObject, +): GoogleFloodParams => { + const { gaugeId, thresholds, gaugeValueUnit } = featureProperties; + const chartTitle = + t( + (getTitle(featureInfoTitle, featureProperties) as PopupData)?.title + .data as string, + featureProperties, + ) || featureProperties.gaugeId; + + const parsedLevels = JSON.parse(thresholds); + const triggerLevels = { + warning: parsedLevels.warningLevel.toFixed(2), + danger: parsedLevels.dangerLevel.toFixed(2), + extremeDanger: parsedLevels.extremeDangerLevel.toFixed(2), + }; + const unit = + GOOGLE_FLOOD_UNITS[gaugeValueUnit as keyof typeof GOOGLE_FLOOD_UNITS]; + const yAxisLabel = t( + GOOGLE_FLOOD_Y_AXIS_LABEL[ + gaugeValueUnit as keyof typeof GOOGLE_FLOOD_Y_AXIS_LABEL + ], + ); + + return { + gaugeId, + triggerLevels, + chartTitle, + detailUrl, + unit: t(unit), + yAxisLabel: t(yAxisLabel), + }; +}; + +export const isGoogleFloodDatasetParams = ( + params: GoogleFloodParams, +): params is GoogleFloodParams => params.gaugeId !== undefined; diff --git a/frontend/src/utils/title-utils.ts b/frontend/src/utils/title-utils.ts new file mode 100644 index 000000000..c79145c93 --- /dev/null +++ b/frontend/src/utils/title-utils.ts @@ -0,0 +1,27 @@ +import { FeatureInfoVisibility, FeatureTitleObject } from 'config/types'; +import { PopupData } from 'context/tooltipStateSlice'; + +export 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: featureInfoTitle[titleField].template, + context: { + [titleField]: properties[titleField], + }, + }, + } + : {}; +}; diff --git a/frontend/test/setupTests.ts b/frontend/test/setupTests.ts index 4d4f0ae88..94f9758db 100644 --- a/frontend/test/setupTests.ts +++ b/frontend/test/setupTests.ts @@ -148,3 +148,13 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({ unobserve: jest.fn(), disconnect: jest.fn(), })); + +jest.mock('chartjs-plugin-annotation', () => ({ + // Mock the necessary parts of the module + default: { + id: 'annotation', + beforeInit: jest.fn(), + afterDraw: jest.fn(), + destroy: jest.fn(), + }, +})); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 351788c2b..ad002b279 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6240,9 +6240,11 @@ caniuse-lite@^1.0.30001587: canvas@^2.11.2: version "0.0.0" + uid "" "canvas@link:./node_modules/.cache/null": version "0.0.0" + uid "" canvg@^3.0.6: version "3.0.10" @@ -6316,7 +6318,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chart.js@^2.9.3: +chart.js@^2.4.0, chart.js@^2.9.3: version "2.9.4" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684" integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A== @@ -6339,6 +6341,13 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" +chartjs-plugin-annotation@0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/chartjs-plugin-annotation/-/chartjs-plugin-annotation-0.5.7.tgz#1bf0e30199a6a9ff9889ce0f37a1e755a80d10bf" + integrity sha512-tKN5KLc69unyZGTvSdhVQEyAOhVNnSkFCCgefZhO4UaqFfABZGFU/d9p6WM2KB0eXFs/rR3Jayh7dvyASC7K0A== + dependencies: + chart.js "^2.4.0" + chartjs-plugin-datalabels@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-1.0.0.tgz#5a3d21dde1ed12602dd48c634985a39f431165f1"