Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add render extension support #163

Merged
merged 2 commits into from
May 16, 2024
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
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

* update `titiler.pgstac.model.Link` to match the OGC specification
* use `{tileMatrixSetId}` in templated URL links
* add support for `render` and `item-assets` STAC Collection extensions
* add `/info` endpoint to the `Collections` endpoints
* add `/collections` and `/collections/{collection_id}` endpoints when `TITILER_PGSTAC_API_DEBUG=TRUE`

## 1.2.3 (2024-03-25)

Expand Down
6 changes: 3 additions & 3 deletions docs/src/advanced/custom_tilejson.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,16 @@ class MosaicTilerFactory(TitilerPgSTACFactory.MosaicTilerFactory):
]

if layer:
config = search_info.defaults.get(layer)
config = search_info.metadata.defaults_params.get(layer)
if not config:
raise HTTPException(status_code=404, detail=f"Invalid {layer} configuration.")

# This assume the default configuration follows the endpoint expected format
# as `"true_color": [("assets", "B4"), ("assets", "B3"), ("assets", "B2")]`
# as `"true_color": {"assets": ["B4", "B3", "B2"]}`
qs = QueryParams(config)

if qs:
tiles_url += f"?{urlencode(qs)}"
tiles_url += f"?{urlencode(qs, doseq=True)}"

minzoom = _first_value([minzoom, search_info.metadata.minzoom], tms.minzoom)
maxzoom = _first_value([maxzoom, search_info.metadata.maxzoom], tms.maxzoom)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/advanced/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
},
"ndvi": {
"expression": "(B4-B3)/(B4+B3)",
"rescale": "-1,1",
"rescale": [[-1, 1]],
"colormap_name": "viridis"
}
}
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

DATA_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
collection = os.path.join(DATA_DIR, "noaa-emergency-response.json")
collection_maxar = os.path.join(DATA_DIR, "maxar_BayOfBengal.json")
items = os.path.join(DATA_DIR, "noaa-eri-nashville2020.json")

test_db = pytest_pgsql.TransactedPostgreSQLTestDB.create_fixture(
Expand Down Expand Up @@ -67,14 +68,15 @@ def database_url(test_db):
print("Load items and collection into PgSTAC")
loader = Loader(db=db)
loader.load_collections(collection)
loader.load_collections(collection_maxar)
loader.load_items(items)

# Make sure we have 1 collection and 163 items in pgstac
with psycopg.connect(str(test_db.connection.engine.url)) as conn:
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM pgstac.collections")
val = cur.fetchone()[0]
assert val == 1
assert val == 2

cur.execute("SELECT COUNT(*) FROM pgstac.items")
val = cur.fetchone()[0]
Expand Down
133 changes: 133 additions & 0 deletions tests/fixtures/maxar_BayOfBengal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"id": "MAXAR_BayofBengal_Cyclone_Mocha_May_23",
"type": "Collection",
"links": [],
"title": "Bay of Bengal Cyclone Mocha 2023",
"extent": {
"spatial": {
"bbox": [
[
91.831615,
19.982078842323997,
92.97426268500965,
21.666101
],
[
92.567815,
20.18811887678192,
92.74417544237298,
20.62968532404085
],
[
92.72278776887262,
20.104801,
92.893524,
20.630214
],
[
92.75855246040959,
19.982078842323997,
92.89682495377032,
20.514473160464657
],
[
92.84253515935835,
19.984656587012033,
92.97426268500965,
20.514418665444474
],
[
91.831615,
21.518411,
91.957078,
21.666101
]
]
},
"temporal": {
"interval": [
[
"2023-01-03T04:30:17Z",
"2023-05-22T04:35:25Z"
]
]
}
},
"license": "CC-BY-NC-4.0",
"renders": {
"visual": {
"title": "Visual Image",
"assets": [
"visual"
],
"asset_bidx": ["visual|1,2,3"],
"minmax_zoom": [
8,
22
],
"tilematrixsets": {
"WebMercatorQuad": [
8,
22
]
}
},
"color": {
"title": "Colored Image",
"assets": [
"visual"
],
"asset_bidx": ["visual|1"],
"colormap": {
"1": [0, 0, 0, 255],
"1000": [255, 255, 255, 255]
}
},
"visualr": {
"title": "Rescaled Image",
"assets": [
"visual"
],
"asset_bidx": ["visual|1"],
"rescale": [
[0, 100]
]
}
},
"description": "Maxar OpenData | Cyclone Mocha, a category five cyclone with 130 mph winds and torrential rain, hit parts of Myanmar and Bangladesh, forcing mass evacuations ahead of the storm. The cyclone, one of the most powerful to hit the region in the last decade, made landfall on Sunday, May 14, 2023, near Sittwe in Myanmar's Rakhine state. Rain and a storm surge caused widespread flooding in low-lying areas. The United National Office Coordination of Humanitarian Affairs stated that there had been extensive damage among already vulnerable communities and that communications with the affected areas have been difficult.",
"item_assets": {
"visual": {
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"roles": [
"visual"
],
"title": "Visual Image"
},
"data-mask": {
"type": "application/geopackage+sqlite3",
"roles": [
"data-mask"
],
"title": "Data Mask"
},
"ms_analytic": {
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"roles": [
"data"
],
"title": "Multispectral Image"
},
"pan_analytic": {
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"roles": [
"data"
],
"title": "Panchromatic Image"
}
},
"stac_version": "1.0.0",
"stac_extensions": [
"https://stac-extensions.github.io/item-assets/v1.0.0/schema.json",
"https://stac-extensions.github.io/render/v1.0.0/schema.json"
]
}
31 changes: 30 additions & 1 deletion tests/fixtures/noaa-emergency-response.json
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
{"id":"noaa-emergency-response", "title": "NOAA Emergency Response Imagery", "description":"NOAA Emergency Response Imagery hosted on AWS Public Dataset.","stac_version":"1.0.0","license":"public-domain","links":[],"extent":{"spatial":{"bbox":[[-180,-90,180,90]]},"temporal":{"interval":[["2005-01-01T00:00:00Z",null]]}}}
{
"id": "noaa-emergency-response",
"type": "Collection",
"title": "NOAA Emergency Response Imagery",
"description": "NOAA Emergency Response Imagery hosted on AWS Public Dataset.",
"stac_version": "1.0.0",
"license": "public-domain",
"links": [],
"extent": {
"spatial": {
"bbox": [
[
-180,
-90,
180,
90
]
]
},
"temporal": {
"interval": [
[
"2005-01-01T00:00:00Z",
null
]
]
}
},
"stac_extensions": []
}
29 changes: 29 additions & 0 deletions tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,32 @@ def test_query_point_collections(app):
)

assert response.status_code == 204 # (no content)


def test_collections_render(app, tmp_path):
"""Create wmts document."""
response = app.get(
"/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/WebMercatorQuad/WMTSCapabilities.xml"
)
assert response.status_code == 200
assert response.headers["content-type"] == "application/xml"

wmts = tmp_path / "WMTSCapabilities.xml"
with open(wmts, "wb") as f:
f.write(response.content)

# Validate it's a good WMTS
with rasterio.open(wmts) as src:
assert not src.crs
assert src.profile["driver"] == "WMTS"
assert len(src.subdatasets) == 3
assert ["color", "visual", "visualr"] == [
s.split(",layer=")[1] for s in src.subdatasets
]
with rasterio.open(src.subdatasets[0]) as sub:
assert sub.crs == CRS.from_epsg(3857)
assert sub.profile["driver"] == "WMTS"

response = app.get("/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/info")
assert response.status_code == 200
assert len(response.json()["links"]) == 10 # self, tilejson (4), map (4), wmts (1)
42 changes: 23 additions & 19 deletions tests/test_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,16 +552,28 @@ def test_query_with_metadata(app):
"maxzoom": 2,
"defaults": {
"one_band": {
"assets": "cog",
"asset_bidx": "cog|1",
"assets": ["cog"],
"asset_bidx": ["cog|1"],
},
"three_bands": {
"assets": "cog",
"asset_bidx": "cog|1,2,3",
"assets": ["cog"],
"asset_bidx": ["cog|1,2,3"],
},
"rescale": {
"assets": ["cog"],
"asset_bidx": ["cog|1"],
"rescale": [
[-1, 1],
],
},
"colormap": {
"assets": ["cog"],
"asset_bidx": ["cog|1"],
"colormap": {"1": [0, 0, 0, 255], "1000": [255, 255, 255, 255]},
},
# missing `assets`
"bad_layer": {
"asset_bidx": "cog|1,2,3",
"asset_bidx": ["cog|1,2,3"],
},
},
},
Expand All @@ -572,9 +584,7 @@ def test_query_with_metadata(app):
assert response.status_code == 200
resp = response.json()
assert resp["id"]
assert (
len(resp["links"]) == 6
) # info, tilejson, map, wmts tilejson for one_band, tilejson for three_bands
assert len(resp["links"]) == 8 # info, tilejson, map, wmts, tilejson layers
link = resp["links"][-2]

mosaic_id_metadata = resp["id"]
Expand Down Expand Up @@ -616,9 +626,7 @@ def test_query_with_metadata(app):

with rasterio.open(io.BytesIO(response.content)) as src:
assert src.profile["driver"] == "WMTS"
assert len(src.subdatasets) == 2
assert src.subdatasets[0].endswith(",layer=one_band")
assert src.subdatasets[1].endswith(",layer=three_bands")
assert len(src.subdatasets) == 4

# 4. assets and metadata layers
with pytest.warns(UserWarning):
Expand All @@ -631,26 +639,22 @@ def test_query_with_metadata(app):

with rasterio.open(io.BytesIO(response.content)) as src:
assert src.profile["driver"] == "WMTS"
assert len(src.subdatasets) == 3
assert src.subdatasets[0].endswith(",layer=one_band")
assert src.subdatasets[1].endswith(",layer=three_bands")
assert src.subdatasets[2].endswith(",layer=default")
assert len(src.subdatasets) == 5

with pytest.warns(UserWarning):
response = app.get(f"/searches/{mosaic_id_metadata}/info")
assert response.status_code == 200
resp = response.json()
assert resp["search"]["hash"] == mosaic_id_metadata
assert len(resp["links"]) == 10 # self, tilejson (3), map (3), wmts (3)
assert len(resp["links"]) == 12 # self, tilejson (5), map (5), wmts (1)

assert resp["links"][1]["title"] == "TileJSON link (Template URL)."
assert (
resp["links"][2]["title"]
== "TileJSON link for `one_band` layer (Template URL)."
resp["links"][2]["title"] == "TileJSON link for `rescale` layer (Template URL)."
)
assert (
resp["links"][3]["title"]
== "TileJSON link for `three_bands` layer (Template URL)."
== "TileJSON link for `colormap` layer (Template URL)."
)

assert "asset_bidx=cog%7C1" in resp["links"][2]["href"]
Expand Down
Loading
Loading