Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/crud/geostore.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,27 @@ async def build_gadm_geostore(
)


async def get_wdpa_geostore_id(dataset, version, wdpa_id):
src_table: Table = db.table(version)
src_table.schema = dataset
columns_etc: List[Column | Label] = [
db.column("gfw_geostore_id"),
]

sql: Select = (
db.select(columns_etc)
.select_from(src_table)
.where(db.text("wdpa_pid=:wdpa_id").bindparams(wdpa_id=wdpa_id))
)
row = await db.first(sql)

if row is None:
raise RecordNotFoundError(
f"WDPA area with id {wdpa_id} not found in {dataset} version {version}"
)
return row.gfw_geostore_id


async def _find_first_geostore(
adm_level,
admin_provider,
Expand Down
30 changes: 25 additions & 5 deletions app/models/pydantic/datamart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

from app.models.pydantic.responses import Response

from ...crud.geostore import get_gadm_geostore_id
from ...crud.geostore import get_gadm_geostore_id, get_wdpa_geostore_id
from ...crud.versions import get_latest_version
from ...errors import RecordNotFoundError
from .base import StrictBaseModel


Expand Down Expand Up @@ -78,6 +80,21 @@ def set_version_default(cls, v):
return v or "4.1"


class WdpaAreaOfInterest(AreaOfInterest):
type: Literal["protected_area"] = "protected_area"
wdpa_id: str = Field(..., title="World Database on Protected Areas (WDPA) ID")

async def get_geostore_id(self) -> UUID:
dataset = "wdpa_protected_areas"
try:
latest_version = await get_latest_version(dataset)
except RecordNotFoundError:
raise HTTPException(
status_code=404, detail="WDPA dataset does not have latest version."
)
return await get_wdpa_geostore_id(dataset, latest_version, self.wdpa_id)


class Global(AreaOfInterest):
type: Literal["global"] = Field(
"global",
Expand All @@ -97,7 +114,7 @@ class DataMartSource(StrictBaseModel):


class DataMartMetadata(StrictBaseModel):
aoi: Union[GeostoreAreaOfInterest, AdminAreaOfInterest, Global]
aoi: Union[GeostoreAreaOfInterest, AdminAreaOfInterest, Global, WdpaAreaOfInterest]
sources: list[DataMartSource]


Expand All @@ -119,9 +136,9 @@ class DataMartResourceLinkResponse(Response):


class TreeCoverLossByDriverIn(StrictBaseModel):
aoi: Union[GeostoreAreaOfInterest, AdminAreaOfInterest, Global] = Field(
..., discriminator="type"
)
aoi: Union[
GeostoreAreaOfInterest, AdminAreaOfInterest, Global, WdpaAreaOfInterest
] = Field(..., discriminator="type")
canopy_cover: int = 30
dataset_version: Dict[str, str] = {}

Expand Down Expand Up @@ -250,6 +267,9 @@ def parse_area_of_interest(request: Request) -> AreaOfInterest:
if aoi_type == "global":
return Global()

if aoi_type == "protected_area":
return WdpaAreaOfInterest(wdpa_id=params.get("aoi[wdpa_id]"))

# If neither type is provided, raise an error
raise HTTPException(
status_code=422, detail="Invalid Area of Interest parameters"
Expand Down
25 changes: 11 additions & 14 deletions batch/python/tiles_geojson.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def to_4326(crs: CRS, x: float, y: float) -> Tuple[float, float]:

def extract_metadata_from_gdalinfo(gdalinfo_json: Dict[str, Any]) -> Dict[str, Any]:
"""Extract necessary metadata from the gdalinfo JSON output."""
corner_coordinates = gdalinfo_json["cornerCoordinates"]
wgs84Extent = gdalinfo_json["wgs84Extent"]["coordinates"][0]
geo_transform = gdalinfo_json["geoTransform"]

bands = [
Expand Down Expand Up @@ -52,16 +52,13 @@ def extract_metadata_from_gdalinfo(gdalinfo_json: Dict[str, Any]) -> Dict[str, A
for band in gdalinfo_json.get("bands", [])
]

crs: CRS = CRS.from_string(gdalinfo_json["coordinateSystem"]["wkt"])
metadata = {
# NOTE: pixetl seems to always write features in tiles.geojson in
# degrees (when the tiles themselves are epsg:3857 I think
# the units should be meters). Reproduce that behavior for
# backwards compatibility. If it ever changes, remove the call to
# to_4326 here.
# wgs84Extent is in decimal degrees, not meters.
"extent": [
*to_4326(crs, *corner_coordinates["lowerLeft"]),
*to_4326(crs, *corner_coordinates["upperRight"]),
wgs84Extent[0][0], # left of upperleft
wgs84Extent[2][1], # bottom of lowerRight
wgs84Extent[2][0], # right of lowerRight
wgs84Extent[0][1] # top of upperLeft
],
"width": gdalinfo_json["size"][0],
"height": gdalinfo_json["size"][1],
Expand Down Expand Up @@ -108,11 +105,11 @@ def generate_geojsons(
extent = metadata["extent"]
# Create a Polygon from the extent
polygon_coords = [
[extent[0], extent[1]],
[extent[0], extent[3]],
[extent[2], extent[3]],
[extent[2], extent[1]],
[extent[0], extent[1]],
[extent[0], extent[3]], # left/top
[extent[2], extent[3]], # right/top
[extent[2], extent[1]], # right/bottom
[extent[0], extent[1]], # left/bottom
[extent[0], extent[3]], # left/top
]
polygon = Polygon(polygon_coords)

Expand Down
106 changes: 105 additions & 1 deletion tests_v2/unit/app/routes/datamart/test_land.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,111 @@ async def test_post_tree_cover_loss_by_drivers(
mock_get_resources.assert_awaited_with(resource_id)


class TestWdpaAreaOfInterest:
@pytest.mark.asyncio
async def test_get_tree_cover_loss_by_drivers_found(
self,
apikey,
async_client: AsyncClient,
):
with (
patch(
"app.routes.datamart.land._check_resource_exists", return_value=True
) as mock_get_resources,
):
api_key, payload = apikey
origin = payload["domains"][0]

headers = {"origin": origin}
params = {
"x-api-key": api_key,
"aoi[type]": "protected_area",
"canopy_cover": 30,
"aoi[wdpa_id]": "123",
}
aoi = {"type": "protected_area", "wdpa_id": "123"}
resource_id = _get_resource_id(
"tree_cover_loss_by_driver", aoi, 30, DEFAULT_LAND_DATASET_VERSIONS
)

response = await async_client.get(
"/v0/land/tree_cover_loss_by_driver", headers=headers, params=params
)

assert response.status_code == 200
assert (
f"/v0/land/tree_cover_loss_by_driver/{resource_id}"
in response.json()["data"]["link"]
)
mock_get_resources.assert_awaited_with(resource_id)

@pytest.mark.asyncio
async def test_post_tree_cover_loss_by_drivers(
self,
geostore,
apikey,
async_client: AsyncClient,
):
api_key, payload = apikey
origin = payload["domains"][0]

wdpa_id = "123"
headers = {"origin": origin, "x-api-key": api_key}
dataset_version = {"umd_tree_cover_loss": "v1.8"}
aoi = {"type": "protected_area", "wdpa_id": wdpa_id}
payload = {
"aoi": aoi,
"canopy_cover": 30,
"dataset_version": dataset_version,
}
with (
patch(
"app.routes.datamart.land._check_resource_exists", return_value=False
) as mock_get_resources,
patch(
"app.models.pydantic.datamart.get_wdpa_geostore_id",
return_value=geostore,
) as mock_get_geostore,
patch(
"app.models.pydantic.datamart.get_latest_version",
return_value="v1.7",
),
):
dataset_versions = DEFAULT_LAND_DATASET_VERSIONS | dataset_version
resource_id = _get_resource_id(
"tree_cover_loss_by_driver",
aoi,
30,
dataset_versions,
)

response = await async_client.post(
"/v0/land/tree_cover_loss_by_driver", headers=headers, json=payload
)

assert response.status_code == 202

body = response.json()

assert body["status"] == "success"
assert (
f"/v0/land/tree_cover_loss_by_driver/{resource_id}"
in body["data"]["link"]
)

resource_id = body["data"]["link"].split("/")[-1]
try:
resource_id = uuid.UUID(resource_id)
assert True
except ValueError:
assert False

mock_get_resources.assert_awaited_with(resource_id)
mock_get_geostore.assert_awaited_with(
"wdpa_protected_areas", "v1.7", wdpa_id
)


MOCK_RESULT = [
{
"umd_tree_cover_loss__year": 2001,
Expand Down Expand Up @@ -634,7 +739,6 @@ async def test_post_tree_cover_loss_by_drivers(
},
]


MOCK_RESOURCE = {
"status": "saved",
"message": None,
Expand Down