Skip to content

Commit a4f4a95

Browse files
add GeoJSON Feature /crop POST endpoint (#339)
* add POST crop endpoint using geojson feature * update docs * update changelog * fix failing tests * Update docs/endpoints/stac.md
1 parent 9a8d579 commit a4f4a95

File tree

7 files changed

+197
-17
lines changed

7 files changed

+197
-17
lines changed

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## Unrelease
4+
5+
### titiler.core
6+
7+
* add `/crop` POST endpoint to return an image from a GeoJSON feature (https://github.com/developmentseed/titiler/pull/339)
8+
9+
310
## 0.3.3 (2021-06-29)
411

512
### titiler.core

docs/advanced/tiler_factories.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"])
2323
| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document
2424
| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities
2525
| `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset
26-
| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset
27-
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset
26+
| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset
27+
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
28+
| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature
2829

2930
### `titiler.core.factory.MultiBaseTilerFactory`
3031

@@ -52,8 +53,9 @@ app.include_router(cog.router, tags=["STAC"])
5253
| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document
5354
| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities
5455
| `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset
55-
| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset
56-
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset
56+
| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset
57+
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
58+
| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature
5759

5860
### `titiler.core.factory.MultiBandTilerFactory`
5961

@@ -87,8 +89,9 @@ app.include_router(cog.router, tags=["Landsat"])
8789
| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document
8890
| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities
8991
| `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset
90-
| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset
91-
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset
92+
| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset
93+
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
94+
| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature
9295

9396

9497
### `titiler.mosaic.factory.MosaicTilerFactory`

docs/endpoints/cog.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Read Info/Metadata and create Web map Tiles from a **single** COG. The `cog` rou
1919
| `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel value from a dataset
2020
| `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset
2121
| `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
22+
| `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a dataset
2223
| `GET` | `/cog/validate` | JSON | validate a COG and return dataset info
2324
| `GET` | `/cog/viewer` | HTML | demo webpage
2425

@@ -106,13 +107,42 @@ Example:
106107
- **colormap**: JSON encoded custom Colormap. OPTIONAL
107108
- **resampling_method**: rasterio resampling method. Default is `nearest`.
108109

109-
Note: if `height` and `width` are provided `max_size` will be ignored.
110-
111110
Example:
112111

113112
- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif`
114113
- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie`
115114

115+
116+
`:endpoint:/cog/crop[/{width}x{height}][].{format}] - [POST]`
117+
118+
- Body:
119+
- **feature**: A valida GeoJSON feature (Polygon or MultiPolygon)
120+
121+
- PathParams:
122+
- **height**: Force output image height. OPTIONAL
123+
- **width**: Force output image width. OPTIONAL
124+
- **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL
125+
126+
- QueryParams:
127+
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**
128+
- **bidx**: Comma (',') delimited band indexes. OPTIONAL
129+
- **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL
130+
- **nodata**: Overwrite internal Nodata value. OPTIONAL
131+
- **max_size**: Max image size, default is 1024. OPTIONAL
132+
- **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL
133+
- **color_formula**: rio-color formula. OPTIONAL
134+
- **colormap_name**: rio-tiler color map name. OPTIONAL
135+
- **colormap**: JSON encoded custom Colormap. OPTIONAL
136+
- **resampling_method**: rasterio resampling method. Default is `nearest`.
137+
138+
Example:
139+
140+
- `https://myendpoint/cog/crop?url=https://somewhere.com/mycog.tif`
141+
- `https://myendpoint/cog/crop.png?url=https://somewhere.com/mycog.tif`
142+
- `https://myendpoint/cog/crop/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie`
143+
144+
Note: if `height` and `width` are provided `max_size` will be ignored.
145+
116146
### Point
117147

118148
`:endpoint:/cog/point/{lon},{lat}`

docs/endpoints/stac.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Read Info/Metadata and create Web map Tiles from a **single** STAC Item. The `s
2020
| `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from a dataset
2121
| `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from a dataset
2222
| `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
23+
| `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a STAC Item
2324
| `GET` | `/stac/viewer` | HTML | demo webpage (Not created by the factory)
2425

2526
## Description
@@ -115,13 +116,46 @@ Example:
115116

116117
***assets** OR **expression** is required
117118

118-
Note: if `height` and `width` are provided `max_size` will be ignored.
119-
120119
Example:
121120

122121
- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01`
123122
- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie`
124123

124+
125+
`:endpoint:/stac/crop[/{width}x{height}][].{format}] - [POST]`
126+
127+
- Body:
128+
- **feature**: A valida GeoJSON feature (Polygon or MultiPolygon)
129+
130+
- PathParams:
131+
- **height**: Force output image height. OPTIONAL
132+
- **width**: Force output image width. OPTIONAL
133+
- **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL
134+
135+
- QueryParams:
136+
- **url**: STAC Item URL. **REQUIRED**
137+
- **assets**: Comma (',') delimited asset names. OPTIONAL*
138+
- **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL
139+
- **bidx**: Comma (',') delimited band indexes. OPTIONAL
140+
- **nodata**: Overwrite internal Nodata value. OPTIONAL
141+
142+
- **max_size**: Max image size, default is 1024. OPTIONAL
143+
- **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL
144+
- **color_formula**: rio-color formula. OPTIONAL
145+
- **colormap_name**: rio-tiler color map name. OPTIONAL
146+
- **colormap**: JSON encoded custom Colormap. OPTIONAL
147+
- **resampling_method**: rasterio resampling method. Default is `nearest`.
148+
149+
***assets** OR **expression** is required
150+
151+
Example:
152+
153+
- `https://myendpoint/stac/crop?url=https://somewhere.com/item.json&assets=B01`
154+
- `https://myendpoint/stac/crop.png?url=https://somewhere.com/item.json&assets=B01`
155+
- `https://myendpoint/stac/crop/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie`
156+
157+
Note: if `height` and `width` are provided `max_size` will be ignored.
158+
125159
### Point
126160

127161
`:endpoint:/cog/point/{lon},{lat}`
@@ -241,4 +275,3 @@ Demonstration viewer added to the router created by the factory (https://github.
241275
Example:
242276

243277
- `https://myendpoint/stac/viewer?url=https://somewhere.com/item.json`
244-

src/titiler/application/tests/routes/test_demos.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33

44
def test_cog_viewer(app):
55
"""Test COG Viewer."""
6-
response = app.get("/cog/viewer")
6+
response = app.get("/cog/viewer", headers={"accept-encoding": "gzip"})
77
assert response.status_code == 200
88
assert response.headers["content-type"] == "text/html; charset=utf-8"
99
assert response.headers["content-encoding"] == "gzip"
1010

1111

1212
def test_stac_viewer(app):
1313
"""Test STAC Viewer."""
14-
response = app.get("/stac/viewer")
14+
response = app.get("/stac/viewer", headers={"accept-encoding": "gzip"})
1515
assert response.status_code == 200
1616
assert response.headers["content-type"] == "text/html; charset=utf-8"
1717
assert response.headers["content-encoding"] == "gzip"

src/titiler/core/tests/test_factories.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
def test_TilerFactory():
3232
"""Test TilerFactory class."""
3333
cog = TilerFactory()
34-
assert len(cog.router.routes) == 21
34+
assert len(cog.router.routes) == 24
3535
assert cog.tms_dependency == TMSParams
3636

3737
cog = TilerFactory(router_prefix="something", tms_dependency=WebMercatorTMSParams)
@@ -272,14 +272,55 @@ def test_TilerFactory():
272272
assert meta["dtype"] == "int16"
273273
assert meta["count"] == 1
274274

275+
feature = json.dumps(
276+
{
277+
"type": "Feature",
278+
"properties": {},
279+
"geometry": {
280+
"type": "Polygon",
281+
"coordinates": [
282+
[
283+
[-59.23828124999999, 74.16408546675687],
284+
[-59.83154296874999, 73.15680773175981],
285+
[-58.73291015624999, 72.88087095711504],
286+
[-56.62353515625, 73.06104462497655],
287+
[-55.17333984375, 73.41588526207096],
288+
[-55.2392578125, 74.09799577518739],
289+
[-56.88720703125, 74.2895142503942],
290+
[-57.23876953124999, 74.30735341486248],
291+
[-59.23828124999999, 74.16408546675687],
292+
]
293+
],
294+
},
295+
}
296+
)
297+
298+
response = client.post(f"/crop?url={DATA_DIR}/cog.tif", data=feature)
299+
assert response.status_code == 200
300+
assert response.headers["content-type"] == "image/png"
301+
302+
response = client.post(f"/crop.tif?url={DATA_DIR}/cog.tif", data=feature)
303+
assert response.status_code == 200
304+
assert response.headers["content-type"] == "image/tiff; application=geotiff"
305+
meta = parse_img(response.content)
306+
assert meta["dtype"] == "uint16"
307+
assert meta["count"] == 2
308+
309+
response = client.post(f"/crop/100x100.jpeg?url={DATA_DIR}/cog.tif", data=feature)
310+
assert response.status_code == 200
311+
assert response.headers["content-type"] == "image/jpeg"
312+
meta = parse_img(response.content)
313+
assert meta["width"] == 100
314+
assert meta["height"] == 100
315+
275316

276317
@patch("rio_tiler.io.cogeo.rasterio")
277318
def test_MultiBaseTilerFactory(rio):
278319
"""test MultiBaseTilerFactory."""
279320
rio.open = mock_rasterio_open
280321

281322
stac = MultiBaseTilerFactory(reader=STACReader)
282-
assert len(stac.router.routes) == 22
323+
assert len(stac.router.routes) == 25
283324

284325
app = FastAPI()
285326
app.include_router(stac.router)
@@ -359,7 +400,7 @@ def test_MultiBandTilerFactory():
359400
"""test MultiBandTilerFactory."""
360401

361402
bands = MultiBandTilerFactory(reader=BandFileReader)
362-
assert len(bands.router.routes) == 22
403+
assert len(bands.router.routes) == 25
363404

364405
app = FastAPI()
365406
app.include_router(bands.router)

src/titiler/core/titiler/core/factory.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from titiler.core.resources.responses import GeoJSONResponse, XMLResponse
3636
from titiler.core.utils import Timer, bbox_to_feature
3737

38-
from fastapi import APIRouter, Depends, Path, Query
38+
from fastapi import APIRouter, Body, Depends, Path, Query
3939

4040
from starlette.requests import Request
4141
from starlette.responses import Response
@@ -706,6 +706,72 @@ def part(
706706

707707
return Response(content, media_type=format.mediatype, headers=headers)
708708

709+
@self.router.post(
710+
r"/crop", **img_endpoint_params,
711+
)
712+
@self.router.post(
713+
r"/crop.{format}", **img_endpoint_params,
714+
)
715+
@self.router.post(
716+
r"/crop/{width}x{height}.{format}", **img_endpoint_params,
717+
)
718+
def geojson_crop(
719+
feature: Feature = Body(..., descriptiom="GeoJSON Feature."),
720+
format: ImageType = Query(
721+
None, description="Output image type. Default is auto."
722+
),
723+
src_path=Depends(self.path_dependency),
724+
layer_params=Depends(self.layer_dependency),
725+
image_params=Depends(self.img_dependency),
726+
dataset_params=Depends(self.dataset_dependency),
727+
render_params=Depends(self.render_dependency),
728+
colormap=Depends(self.colormap_dependency),
729+
kwargs: Dict = Depends(self.additional_dependency),
730+
):
731+
"""Create image from a geojson feature."""
732+
timings = []
733+
headers: Dict[str, str] = {}
734+
735+
with Timer() as t:
736+
with rasterio.Env(**self.gdal_config):
737+
with self.reader(src_path, **self.reader_options) as src_dst:
738+
data = src_dst.feature(
739+
feature.dict(exclude_none=True),
740+
**layer_params.kwargs,
741+
**image_params.kwargs,
742+
**dataset_params.kwargs,
743+
**kwargs,
744+
)
745+
dst_colormap = getattr(src_dst, "colormap", None)
746+
timings.append(("dataread", round(t.elapsed * 1000, 2)))
747+
748+
with Timer() as t:
749+
image = data.post_process(
750+
in_range=render_params.rescale_range,
751+
color_formula=render_params.color_formula,
752+
)
753+
timings.append(("postprocess", round(t.elapsed * 1000, 2)))
754+
755+
if not format:
756+
format = ImageType.jpeg if data.mask.all() else ImageType.png
757+
758+
with Timer() as t:
759+
content = image.render(
760+
add_mask=render_params.return_mask,
761+
img_format=format.driver,
762+
colormap=colormap or dst_colormap,
763+
**format.profile,
764+
**render_params.kwargs,
765+
)
766+
timings.append(("format", round(t.elapsed * 1000, 2)))
767+
768+
if OptionalHeader.server_timing in self.optional_headers:
769+
headers["Server-Timing"] = ", ".join(
770+
[f"{name};dur={time}" for (name, time) in timings]
771+
)
772+
773+
return Response(content, media_type=format.mediatype, headers=headers)
774+
709775

710776
@dataclass
711777
class MultiBaseTilerFactory(TilerFactory):

0 commit comments

Comments
 (0)