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 GeoJSON Feature /crop POST endpoint #339

Merged
merged 5 commits into from
Jul 23, 2021
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
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release Notes

## Unrelease

### titiler.core

* add `/crop` POST endpoint to return an image from a GeoJSON feature (https://github.com/developmentseed/titiler/pull/339)


## 0.3.3 (2021-06-29)

### titiler.core
Expand Down
15 changes: 9 additions & 6 deletions docs/advanced/tiler_factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"])
| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document
| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities
| `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset
| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset
| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset
| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature

### `titiler.core.factory.MultiBaseTilerFactory`

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

### `titiler.core.factory.MultiBandTilerFactory`

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


### `titiler.mosaic.factory.MosaicTilerFactory`
Expand Down
34 changes: 32 additions & 2 deletions docs/endpoints/cog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Read Info/Metadata and create Web map Tiles from a **single** COG. The `cog` rou
| `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel value from a dataset
| `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset
| `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
| `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a dataset
| `GET` | `/cog/validate` | JSON | validate a COG and return dataset info
| `GET` | `/cog/viewer` | HTML | demo webpage

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

Note: if `height` and `width` are provided `max_size` will be ignored.

Example:

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


`:endpoint:/cog/crop[/{width}x{height}][].{format}] - [POST]`

- Body:
- **feature**: A valida GeoJSON feature (Polygon or MultiPolygon)

- PathParams:
- **height**: Force output image height. OPTIONAL
- **width**: Force output image width. OPTIONAL
- **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL

- QueryParams:
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**
- **bidx**: Comma (',') delimited band indexes. OPTIONAL
- **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL
- **nodata**: Overwrite internal Nodata value. OPTIONAL
- **max_size**: Max image size, default is 1024. OPTIONAL
- **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL
- **color_formula**: rio-color formula. OPTIONAL
- **colormap_name**: rio-tiler color map name. OPTIONAL
- **colormap**: JSON encoded custom Colormap. OPTIONAL
- **resampling_method**: rasterio resampling method. Default is `nearest`.

Example:

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

Note: if `height` and `width` are provided `max_size` will be ignored.

### Point

`:endpoint:/cog/point/{lon},{lat}`
Expand Down
39 changes: 36 additions & 3 deletions docs/endpoints/stac.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Read Info/Metadata and create Web map Tiles from a **single** STAC Item. The `s
| `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from a dataset
| `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from a dataset
| `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset
| `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a STAC Item
| `GET` | `/stac/viewer` | HTML | demo webpage (Not created by the factory)

## Description
Expand Down Expand Up @@ -115,13 +116,46 @@ Example:

***assets** OR **expression** is required

Note: if `height` and `width` are provided `max_size` will be ignored.

Example:

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


`:endpoint:/stac/crop[/{width}x{height}][].{format}] - [POST]`

- Body:
- **feature**: A valida GeoJSON feature (Polygon or MultiPolygon)

- PathParams:
- **height**: Force output image height. OPTIONAL
- **width**: Force output image width. OPTIONAL
- **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL

- QueryParams:
- **url**: STAC Item URL. **REQUIRED**
- **assets**: Comma (',') delimited asset names. OPTIONAL*
- **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL
- **bidx**: Comma (',') delimited band indexes. OPTIONAL
- **nodata**: Overwrite internal Nodata value. OPTIONAL

- **max_size**: Max image size, default is 1024. OPTIONAL
- **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL
- **color_formula**: rio-color formula. OPTIONAL
- **colormap_name**: rio-tiler color map name. OPTIONAL
- **colormap**: JSON encoded custom Colormap. OPTIONAL
- **resampling_method**: rasterio resampling method. Default is `nearest`.

***assets** OR **expression** is required

Example:

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

Note: if `height` and `width` are provided `max_size` will be ignored.

### Point

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

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

4 changes: 2 additions & 2 deletions src/titiler/application/tests/routes/test_demos.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

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


def test_stac_viewer(app):
"""Test STAC Viewer."""
response = app.get("/stac/viewer")
response = app.get("/stac/viewer", headers={"accept-encoding": "gzip"})
assert response.status_code == 200
assert response.headers["content-type"] == "text/html; charset=utf-8"
assert response.headers["content-encoding"] == "gzip"
47 changes: 44 additions & 3 deletions src/titiler/core/tests/test_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
def test_TilerFactory():
"""Test TilerFactory class."""
cog = TilerFactory()
assert len(cog.router.routes) == 21
assert len(cog.router.routes) == 24
assert cog.tms_dependency == TMSParams

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

feature = json.dumps(
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-59.23828124999999, 74.16408546675687],
[-59.83154296874999, 73.15680773175981],
[-58.73291015624999, 72.88087095711504],
[-56.62353515625, 73.06104462497655],
[-55.17333984375, 73.41588526207096],
[-55.2392578125, 74.09799577518739],
[-56.88720703125, 74.2895142503942],
[-57.23876953124999, 74.30735341486248],
[-59.23828124999999, 74.16408546675687],
]
],
},
}
)

response = client.post(f"/crop?url={DATA_DIR}/cog.tif", data=feature)
assert response.status_code == 200
assert response.headers["content-type"] == "image/png"

response = client.post(f"/crop.tif?url={DATA_DIR}/cog.tif", data=feature)
assert response.status_code == 200
assert response.headers["content-type"] == "image/tiff; application=geotiff"
meta = parse_img(response.content)
assert meta["dtype"] == "uint16"
assert meta["count"] == 2

response = client.post(f"/crop/100x100.jpeg?url={DATA_DIR}/cog.tif", data=feature)
assert response.status_code == 200
assert response.headers["content-type"] == "image/jpeg"
meta = parse_img(response.content)
assert meta["width"] == 100
assert meta["height"] == 100


@patch("rio_tiler.io.cogeo.rasterio")
def test_MultiBaseTilerFactory(rio):
"""test MultiBaseTilerFactory."""
rio.open = mock_rasterio_open

stac = MultiBaseTilerFactory(reader=STACReader)
assert len(stac.router.routes) == 22
assert len(stac.router.routes) == 25

app = FastAPI()
app.include_router(stac.router)
Expand Down Expand Up @@ -359,7 +400,7 @@ def test_MultiBandTilerFactory():
"""test MultiBandTilerFactory."""

bands = MultiBandTilerFactory(reader=BandFileReader)
assert len(bands.router.routes) == 22
assert len(bands.router.routes) == 25

app = FastAPI()
app.include_router(bands.router)
Expand Down
68 changes: 67 additions & 1 deletion src/titiler/core/titiler/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from titiler.core.resources.responses import GeoJSONResponse, XMLResponse
from titiler.core.utils import Timer, bbox_to_feature

from fastapi import APIRouter, Depends, Path, Query
from fastapi import APIRouter, Body, Depends, Path, Query

from starlette.requests import Request
from starlette.responses import Response
Expand Down Expand Up @@ -706,6 +706,72 @@ def part(

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

@self.router.post(
r"/crop", **img_endpoint_params,
)
@self.router.post(
r"/crop.{format}", **img_endpoint_params,
)
@self.router.post(
r"/crop/{width}x{height}.{format}", **img_endpoint_params,
)
def geojson_crop(
feature: Feature = Body(..., descriptiom="GeoJSON Feature."),
format: ImageType = Query(
None, description="Output image type. Default is auto."
),
src_path=Depends(self.path_dependency),
layer_params=Depends(self.layer_dependency),
image_params=Depends(self.img_dependency),
dataset_params=Depends(self.dataset_dependency),
render_params=Depends(self.render_dependency),
colormap=Depends(self.colormap_dependency),
kwargs: Dict = Depends(self.additional_dependency),
):
"""Create image from a geojson feature."""
timings = []
headers: Dict[str, str] = {}

with Timer() as t:
with rasterio.Env(**self.gdal_config):
with self.reader(src_path, **self.reader_options) as src_dst:
data = src_dst.feature(
feature.dict(exclude_none=True),
**layer_params.kwargs,
**image_params.kwargs,
**dataset_params.kwargs,
**kwargs,
)
dst_colormap = getattr(src_dst, "colormap", None)
timings.append(("dataread", round(t.elapsed * 1000, 2)))

with Timer() as t:
image = data.post_process(
in_range=render_params.rescale_range,
color_formula=render_params.color_formula,
)
timings.append(("postprocess", round(t.elapsed * 1000, 2)))

if not format:
format = ImageType.jpeg if data.mask.all() else ImageType.png

with Timer() as t:
content = image.render(
add_mask=render_params.return_mask,
img_format=format.driver,
colormap=colormap or dst_colormap,
**format.profile,
**render_params.kwargs,
)
timings.append(("format", round(t.elapsed * 1000, 2)))

if OptionalHeader.server_timing in self.optional_headers:
headers["Server-Timing"] = ", ".join(
[f"{name};dur={time}" for (name, time) in timings]
)

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


@dataclass
class MultiBaseTilerFactory(TilerFactory):
Expand Down