Skip to content

Commit ca34df0

Browse files
add support for OGC Maps API (#1197)
* add support for OGC Maps API * fix * Apply suggestions from code review
1 parent db8d93e commit ca34df0

11 files changed

Lines changed: 482 additions & 30 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### titiler.core
66

77
* add OpenTelemetry instrumentation to the tiler factory classes
8+
* add `OGC Maps API` support (`/map` endpoint)
89

910
### titiler.application
1011

docs/src/advanced/endpoints_factories.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reade
5656
- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`.
5757
- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`.
5858
- **render_func**: Image rendering method. Defaults to `titiler.core.utils.render_image`.
59-
- **add_preview**: . Add `/preview` endpoint to the router. Defaults to `True`.
60-
- **add_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`.
61-
- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`.
59+
- **add_preview**: Add `/preview` endpoint to the router. Defaults to `True`.
60+
- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`.
61+
- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`.
62+
- **add_ogc_maps**: Add `/map` endoint (OGC Maps API) to the router. Defaults to `False`.
6263

6364
#### Endpoints
6465

@@ -75,6 +76,7 @@ cog = TilerFactory(
7576
add_preview=True,
7677
add_part=True,
7778
add_viewer=True,
79+
add_ogc_maps=True,
7880
)
7981

8082
# add router endpoint to the main application
@@ -98,6 +100,7 @@ app.include_router(cog.router)
98100
| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional**
99101
| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional**
100102
| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional**
103+
| `GET` | `/maps` | image/bin | create maps from a dataset **Optional**
101104

102105

103106
### MultiBaseTilerFactory
@@ -122,7 +125,13 @@ from rio_tiler.io import STACReader # STACReader is a MultiBaseReader
122125
from titiler.core.factory import MultiBaseTilerFactory
123126

124127
app = FastAPI()
125-
stac = MultiBaseTilerFactory(reader=STACReader)
128+
stac = MultiBaseTilerFactory(
129+
reader=STACReader,
130+
add_preview=True,
131+
add_part=True,
132+
add_viewer=True,
133+
add_ogc_maps=True,
134+
)
126135
app.include_router(stac.router)
127136
```
128137

@@ -145,6 +154,7 @@ app.include_router(stac.router)
145154
| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets **Optional**
146155
| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets **Optional**
147156
| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from assets **Optional**
157+
| `GET` | `/map` | image/bin | create maps from a dataset **Optional**
148158

149159
### MultiBandTilerFactory
150160

@@ -200,7 +210,8 @@ app.include_router(landsat.router)
200210
| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel value from a dataset
201211
| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional**
202212
| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature **Optional**
203-
| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional**
213+
| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional**
214+
| `GET` | `/map` | image/bin | create maps from a dataset **Optional**
204215

205216

206217
### TMSFactory
@@ -315,7 +326,7 @@ Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/d
315326
- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`.
316327
- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`.
317328
- **optional_headers**: List of OptionalHeader which endpoints could add (if implemented). Defaults to `[]`.
318-
- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`.
329+
- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`.
319330

320331
#### Endpoints
321332

@@ -360,7 +371,8 @@ class: `titiler.xarray.factory.TilerFactory`
360371
- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`.
361372
- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`.
362373
- **add_part**: Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`.
363-
- **add_viewer**: Add `/map.html` endpoints to the router. Defaults to `True`.
374+
- **add_viewer**: Add `/{TileMatrixSetId}/map.html` endpoints to the router. Defaults to `True`.
375+
- **add_ogc_maps**: Add `/map` endpoints to the router. Default to `False`.
364376
- **add_preview**: Add `/preview` endpoints to the router. Default to `False`.
365377

366378
```python

docs/src/endpoints/cog.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog
2424
| `GET` | `/cog/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
2525
| `POST` | `/cog/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature
2626
| `GET` | `/cog/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset
27+
| `GET` | `/cog/map` | image/bin | create map image from a dataset
2728
| `GET` | `/cog/validate` | JSON | validate a COG and return dataset info (from `titiler.extensions.cogValidateExtension`)
2829
| `GET` | `/cog/viewer` | HTML | demo webpage (from `titiler.extensions.cogViewerExtension`)
2930
| `GET` | `/cog/stac` | GeoJSON | create STAC Items from a dataset (from `titiler.extensions.stacExtension`)
3031

32+
3133
## Description
3234

3335
### Tiles
@@ -147,6 +149,32 @@ Example:
147149
- `https://myendpoint/cog/bbox/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie`
148150
- `https://myendpoint/cog/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/mycog.tif`
149151

152+
### OGC Maps API - GetMap
153+
154+
`:endpoint:/cog/map`
155+
156+
- QueryParams:
157+
- **url** (str): Cloud Optimized GeoTIFF URL. **Required**
158+
- **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`).
159+
- **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`).
160+
- **nodata** (str, int, float): Overwrite internal Nodata value.
161+
- **unscale** (bool): Apply dataset internal Scale/Offset.
162+
- **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`.
163+
- **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.
164+
- **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`).
165+
- **color_formula** (str): rio-color formula.
166+
- **colormap** (str): JSON encoded custom Colormap.
167+
- **colormap_name** (str): rio-tiler color map name.
168+
- **return_mask** (bool): Add mask to the output data. Default is True.
169+
- **algorithm** (str): Custom algorithm name (e.g `hillshade`).
170+
- **algorithm_params** (str): JSON encoded algorithm parameters.
171+
- **bbox** (str): Comma (',') delimited bounding box.
172+
- **bbox-crs** (str, optional): Coordinate Reference System of the input coordinates. Default to `epsg:4326`.
173+
- **crs** (str, optional): Output Coordinate Reference System. Default to dataset'crs.
174+
- **height** (int, optional): Force output image height. **Also a QueryParam**
175+
- **width** (int, optional): Force output image width. **Also a QueryParam**
176+
- **f** (str): Output [image format](../user_guide/output_format.md)
177+
150178
### Feature
151179

152180
`:endpoint:/cog/feature - [POST]`

docs/src/endpoints/stac.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ Example:
119119

120120
### Bbox
121121

122-
123122
`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}.{format}`
124123

125124
`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}`
@@ -162,6 +161,37 @@ Example:
162161
- `https://myendpoint/stac/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/item.json&assets=B01`
163162
- `https://myendpoint/stac/bbox/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie`
164163

164+
### OGC Maps API - GetMap
165+
166+
`:endpoint:/stac/map`
167+
168+
- QueryParams:
169+
- **url** (str): STAC Item URL. **Required**
170+
- **assets** (array[str]): asset names.
171+
- **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`).
172+
- **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed.
173+
- **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`).
174+
- **nodata** (str, int, float): Overwrite internal Nodata value.
175+
- **unscale** (bool): Apply dataset internal Scale/Offset.
176+
- **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`.
177+
- **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.
178+
- **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`).
179+
- **color_formula** (str): rio-color formula.
180+
- **colormap** (str): JSON encoded custom Colormap.
181+
- **colormap_name** (str): rio-tiler color map name.
182+
- **return_mask** (bool): Add mask to the output data. Default is True.
183+
- **algorithm** (str): Custom algorithm name (e.g `hillshade`).
184+
- **algorithm_params** (str): JSON encoded algorithm parameters.
185+
- **bbox** (str): Comma (',') delimited bounding box.
186+
- **bbox-crs** (str, optional): Coordinate Reference System of the input coordinates. Default to `epsg:4326`.
187+
- **crs** (str, optional): Output Coordinate Reference System. Default to dataset'crs.
188+
- **height** (int, optional): Force output image height. **Also a QueryParam**
189+
- **width** (int, optional): Force output image width. **Also a QueryParam**
190+
- **f** (str): Output [image format](../user_guide/output_format.md)
191+
192+
!!! important
193+
- **assets** OR **expression** is require
194+
165195
### Feature
166196

167197
`:endpoint:/stac/feature - [POST]`

src/titiler/application/titiler/application/main.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ def validate_access_token(access_token: str = Security(api_key_query)):
9999
update_openapi(app)
100100

101101
TITILER_CONFORMS_TO = {
102-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core",
103-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page",
104-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30",
105-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html",
106-
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json",
102+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
103+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page",
104+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30",
105+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html",
106+
"http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json",
107107
}
108108

109109

@@ -113,6 +113,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
113113
cog = TilerFactory(
114114
reader=Reader,
115115
router_prefix="/cog",
116+
add_ogc_maps=True,
116117
extensions=[
117118
cogValidateExtension(),
118119
cogViewerExtension(),
@@ -135,6 +136,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
135136
stac = MultiBaseTilerFactory(
136137
reader=STACReader,
137138
router_prefix="/stac",
139+
add_ogc_maps=True,
138140
extensions=[
139141
stacViewerExtension(),
140142
stacRenderExtension(),

src/titiler/core/tests/test_factories.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,3 +2055,166 @@ def test_colormap_factory():
20552055
assert meta["count"] == 4
20562056
assert meta["width"] == 20
20572057
assert meta["height"] == 256
2058+
2059+
2060+
def test_ogc_maps_cog():
2061+
"""Test TilerFactory class."""
2062+
cog_path = f"{DATA_DIR}/cog.tif"
2063+
2064+
cog = TilerFactory(add_ogc_maps=True)
2065+
assert len(cog.router.routes) == 24
2066+
2067+
assert "https://www.opengis.net/spec/ogcapi-maps-1/1.0/conf/core" in cog.conforms_to
2068+
2069+
app = FastAPI()
2070+
app.include_router(cog.router)
2071+
with TestClient(app) as client:
2072+
# Conformance Class “Core”
2073+
response = client.get(
2074+
"/map",
2075+
params={
2076+
"url": cog_path,
2077+
},
2078+
)
2079+
assert response.status_code == 200
2080+
headers = response.headers
2081+
assert (
2082+
headers["Content-Bbox"]
2083+
== "373185.0,8019284.949381611,639014.9492102272,8286015.0"
2084+
)
2085+
assert headers["Content-Crs"] == "<http://www.opengis.net/def/crs/EPSG/0/32621>"
2086+
assert headers["content-type"] == "image/png"
2087+
meta = parse_img(response.content)
2088+
assert meta["width"] == 1021
2089+
assert meta["height"] == 1024 # default max size
2090+
2091+
response = client.get(
2092+
"/map",
2093+
params={
2094+
"url": cog_path,
2095+
},
2096+
headers={"Accept": "image/jpeg"},
2097+
)
2098+
assert response.status_code == 200
2099+
headers = response.headers
2100+
assert "Content-Bbox" in headers
2101+
assert "Content-Crs" in headers
2102+
assert headers["content-type"] == "image/jpeg"
2103+
2104+
response = client.get("/map", params={"url": cog_path, "f": "tiff"})
2105+
assert response.status_code == 200
2106+
headers = response.headers
2107+
assert "Content-Bbox" in headers
2108+
assert "Content-Crs" in headers
2109+
assert headers["content-type"] == "image/tiff; application=geotiff"
2110+
2111+
# Conformance Class “Scaling”
2112+
# /req/scaling/width-definition
2113+
response = client.get(
2114+
"/map",
2115+
params={
2116+
"url": cog_path,
2117+
"width": 256,
2118+
},
2119+
)
2120+
assert response.status_code == 200
2121+
meta = parse_img(response.content)
2122+
assert meta["width"] == 256
2123+
assert meta["height"] == 257
2124+
2125+
response = client.get(
2126+
"/map",
2127+
params={
2128+
"url": cog_path,
2129+
"width": -256,
2130+
},
2131+
)
2132+
assert response.status_code == 422
2133+
2134+
# /req/scaling/height-definition
2135+
response = client.get(
2136+
"/map",
2137+
params={
2138+
"url": cog_path,
2139+
"height": 256,
2140+
},
2141+
)
2142+
assert response.status_code == 200
2143+
meta = parse_img(response.content)
2144+
assert meta["height"] == 256
2145+
2146+
response = client.get(
2147+
"/map",
2148+
params={
2149+
"url": cog_path,
2150+
"height": -256,
2151+
},
2152+
)
2153+
assert response.status_code == 422
2154+
2155+
# Conformance Class “Spatial Subsetting”
2156+
# /conf/spatial-subsetting/bbox-crs
2157+
response = client.get(
2158+
"/map",
2159+
params={
2160+
"url": cog_path,
2161+
"bbox": "-56.228,72.715,-54.547,73.188",
2162+
},
2163+
)
2164+
headers = response.headers
2165+
assert headers["Content-Crs"] == "<http://www.opengis.net/def/crs/EPSG/0/32621>"
2166+
assert (
2167+
headers["Content-Bbox"]
2168+
== "524922.2217886819,8068852.367048624,581330.6416587981,8123074.564952523"
2169+
)
2170+
assert headers["content-type"] == "image/png"
2171+
meta = parse_img(response.content)
2172+
2173+
response = client.get(
2174+
"/map",
2175+
params={
2176+
"url": cog_path,
2177+
"bbox": "-56.228,72.715,-54.547,73.188",
2178+
"bbox-crs": "http://www.opengis.net/def/crs/OGC/0/CRS84",
2179+
},
2180+
)
2181+
headers = response.headers
2182+
assert headers["Content-Crs"] == "<http://www.opengis.net/def/crs/EPSG/0/32621>"
2183+
assert (
2184+
headers["Content-Bbox"]
2185+
== "524922.2217886819,8068852.367048624,581330.6416587981,8123074.564952523"
2186+
)
2187+
assert headers["content-type"] == "image/png"
2188+
2189+
response = client.get(
2190+
"/map",
2191+
params={
2192+
"url": cog_path,
2193+
"bbox": "-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913",
2194+
"bbox-crs": "[EPSG:3857]",
2195+
},
2196+
)
2197+
assert response.status_code == 200
2198+
headers = response.headers
2199+
assert headers["Content-Crs"] == "<http://www.opengis.net/def/crs/EPSG/0/32621>"
2200+
assert (
2201+
headers["Content-Bbox"]
2202+
== "524922.2217886819,8068852.367048624,581330.6416587983,8123074.564952523"
2203+
)
2204+
assert headers["content-type"] == "image/png"
2205+
2206+
# Abstract Test for Requirement crs parameter definition
2207+
response = client.get(
2208+
"/map",
2209+
params={
2210+
"url": cog_path,
2211+
"bbox": "-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913",
2212+
"bbox-crs": "[EPSG:3857]",
2213+
"crs": "[EPSG:4326]",
2214+
},
2215+
)
2216+
assert response.status_code == 200
2217+
headers = response.headers
2218+
assert headers["Content-Crs"] == "<http://www.opengis.net/def/crs/EPSG/0/4326>"
2219+
assert headers["Content-Bbox"] == "-56.228,72.715,-54.54699999999999,73.188"
2220+
assert headers["content-type"] == "image/png"

0 commit comments

Comments
 (0)