Skip to content

Commit

Permalink
add /{lon},{lat}/values endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
hrodmn committed Jan 2, 2024
1 parent 7070461 commit 60ec292
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 6 deletions.
42 changes: 42 additions & 0 deletions tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,45 @@ def test_bbox_collection(rio, app):
assert response.headers["content-type"] == "image/tiff; application=geotiff"
meta = parse_img(response.content)
assert meta["crs"] == "epsg:3857"


def test_query_point_collections(app):
"""Get values for a Point."""
response = app.get(
f"/collections/{collection_id}/-85.5,36.1624/values", params={"assets": "cog"}
)

assert response.status_code == 200
resp = response.json()

values = resp["values"]
assert len(values) == 2
assert values[0][0] == [
"https://noaa-eri-pds.s3.us-east-1.amazonaws.com/2020_Nashville_Tornado/20200307a_RGB/20200307aC0853130w361030n.tif"
]
assert values[0][2] == ["cog_b1", "cog_b2", "cog_b3"]

# with coord-crs
response = app.get(
f"/collections/{collection_id}/-9517816.46282489,4322990.432036275/values",
params={"assets": "cog", "coord_crs": "epsg:3857"},
)
assert response.status_code == 200
resp = response.json()
assert len(resp["values"]) == 2

# CollectionId not found
response = app.get(
"/collections/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/-85.5,36.1624/values",
params={"assets": "cog"},
)
assert response.status_code == 404
resp = response.json()
assert resp["detail"] == "CollectionId `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` not found"

# at a point with no assets
response = app.get(
f"/collections/{collection_id}/-86.0,-35.0/values", params={"assets": "cog"}
)

assert response.status_code == 204 # (no content)
42 changes: 42 additions & 0 deletions tests/test_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -969,3 +969,45 @@ def test_bbox(rio, app, search_no_bbox):
assert response.headers["content-type"] == "image/tiff; application=geotiff"
meta = parse_img(response.content)
assert meta["crs"] == "epsg:3857"


def test_query_point_searches(app, search_no_bbox, search_bbox):
"""Test getting values for a Point."""
response = app.get(
f"/searches/{search_no_bbox}/-85.5,36.1624/values", params={"assets": "cog"}
)

assert response.status_code == 200
resp = response.json()

values = resp["values"]
assert len(values) == 2
assert values[0][0] == [
"https://noaa-eri-pds.s3.us-east-1.amazonaws.com/2020_Nashville_Tornado/20200307a_RGB/20200307aC0853130w361030n.tif"
]
assert values[0][2] == ["cog_b1", "cog_b2", "cog_b3"]

# with coord-crs
response = app.get(
f"/searches/{search_no_bbox}/-9517816.46282489,4322990.432036275/values",
params={"assets": "cog", "coord_crs": "epsg:3857"},
)
assert response.status_code == 200
resp = response.json()
assert len(resp["values"]) == 2

# SearchId not found
response = app.get(
"/searches/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/-85.5,36.1624/values",
params={"assets": "cog"},
)
assert response.status_code == 404
resp = response.json()
assert resp["detail"] == "SearchId `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` not found"

# outside of searchid bbox
response = app.get(
f"/searches/{search_bbox}/-86.0,35.0/values", params={"assets": "cog"}
)

assert response.status_code == 204 # (no content)
52 changes: 50 additions & 2 deletions titiler/pgstac/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@
from titiler.core.models.mapbox import TileJSON
from titiler.core.models.responses import MultiBaseStatisticsGeoJSON
from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader
from titiler.core.resources.responses import GeoJSONResponse, XMLResponse
from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse
from titiler.core.utils import render_image
from titiler.mosaic.factory import PixelSelectionParams
from titiler.mosaic.models.responses import Point
from titiler.pgstac import model
from titiler.pgstac.dependencies import (
BackendParams,
Expand Down Expand Up @@ -150,6 +151,7 @@ def register_routes(self) -> None:
self._tiles_routes()
self._tilejson_routes()
self._wmts_routes()
self._point_routes()

if self.add_part:
self._part_routes()
Expand Down Expand Up @@ -220,7 +222,6 @@ def tile(
reader_options={**reader_params},
**backend_params,
) as src_dst:

if MOSAIC_STRICT_ZOOM and (
tile.z < src_dst.minzoom or tile.z > src_dst.maxzoom
):
Expand Down Expand Up @@ -901,6 +902,53 @@ def feature_image(

return Response(content, media_type=media_type, headers=headers)

def _point_routes(self):
"""Register point values endpoint."""

@self.router.get(
"/{lon},{lat}/values",
response_model=model.Point,
response_class=JSONResponse,
responses={200: {"description": "Return a value for a point"}},
)
def point(
lon: Annotated[float, Path(description="Longitude")],
lat: Annotated[float, Path(description="Latitude")],
search_id=Depends(self.path_dependency),
coord_crs=Depends(CoordCRSParams),
layer_params=Depends(self.layer_dependency),
dataset_params=Depends(self.dataset_dependency),
pgstac_params=Depends(self.pgstac_dependency),
backend_params=Depends(self.backend_dependency),
reader_params=Depends(self.reader_dependency),
env=Depends(self.environment_dependency),
):
"""Get Point value for a Mosaic."""
threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS))

with rasterio.Env(**env):
with self.reader(
search_id,
reader_options={**reader_params},
**backend_params,
) as src_dst:
values = src_dst.point(
lon,
lat,
coord_crs=coord_crs or WGS84_CRS,
threads=threads,
**layer_params,
**dataset_params,
**pgstac_params,
)

return {
"coordinates": [lon, lat],
"values": [
(val.assets, val.data.tolist(), val.band_names) for val in values
],
}


def add_search_register_route(
app: FastAPI,
Expand Down
14 changes: 13 additions & 1 deletion titiler/pgstac/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""

from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from typing import Any, Dict, List, Literal, Optional, Tuple

from geojson_pydantic.geometries import Geometry
from geojson_pydantic.types import BBox
Expand Down Expand Up @@ -210,3 +210,15 @@ class Infos(BaseModel):
searches: List[Info]
links: Optional[List[Link]] = None
context: Context


class Point(BaseModel):
"""
Point model.
response model for `/{lon}/{lat}/values` endpoint
"""

coordinates: List[float]
values: List[Tuple[List[str], List[float], List[str]]]
8 changes: 5 additions & 3 deletions titiler/pgstac/mosaic.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from rio_tiler.io.base import BaseReader, MultiBaseReader
from rio_tiler.models import ImageData
from rio_tiler.mosaic import mosaic_reader
from rio_tiler.tasks import multi_values
from rio_tiler.tasks import create_tasks, filter_tasks
from rio_tiler.types import AssetInfo, BBox

from titiler.pgstac.settings import CacheSettings, RetrySettings
Expand Down Expand Up @@ -355,7 +355,7 @@ def _reader(
item: Dict[str, Any],
lon: float,
lat: float,
coord_crs=coord_crs,
coord_crs: CRS = coord_crs,
**kwargs: Any,
) -> Dict:
with self.reader(item, **self.reader_options) as src_dst:
Expand All @@ -364,7 +364,9 @@ def _reader(
if "allowed_exceptions" not in kwargs:
kwargs.update({"allowed_exceptions": (PointOutsideBounds,)})

return list(multi_values(mosaic_assets, _reader, lon, lat, **kwargs).items())
tasks = create_tasks(_reader, mosaic_assets, lon=lon, lat=lat, coord_crs=coord_crs, **kwargs)

return [val for val, _ in filter_tasks(tasks)]

def part(
self,
Expand Down

0 comments on commit 60ec292

Please sign in to comment.