Skip to content

Commit c0dccd3

Browse files
add dst-crs (#647)
1 parent 90f44e1 commit c0dccd3

File tree

5 files changed

+101
-10
lines changed

5 files changed

+101
-10
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
* make HTML `templates` configurable in the factories
88
* rename `index.html` to `map.html`
9+
* rename `dependencies.CRSParams` to `dependencies.CoordCRSParams`
10+
* add `dst-crs` option for `/preview` and `/crop` endpoints to specify the output Coordinate Reference System.
911

1012
### titiler.mosaic
1113

CONTRIBUTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ This repo is set to use `pre-commit` to run *isort*, *flake8*, *pydocstring*, *b
2424
pre-commit install
2525
```
2626

27+
### Run tests
28+
29+
Each `titiler`'s modules has its own test suite which can be ran independently
30+
31+
```
32+
# titiler.core
33+
python -m pytest src/titiler/core --cov=titiler.core --cov-report=xml --cov-append --cov-report=term-missing
34+
35+
# titiler.extensions
36+
python -m pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing
37+
38+
# titiler.mosaic
39+
python -m pytest src/titiler/mosaic --cov=titiler.mosaic --cov-report=xml --cov-append --cov-report=term-missing
40+
41+
# titiler.application
42+
python -m pytest src/titiler/application --cov=titiler.application --cov-report=xml --cov-append --cov-report=term-missing
43+
```
44+
2745
### Docs
2846

2947
```bash

src/titiler/core/tests/test_factories.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import numpy
1717
from fastapi import Depends, FastAPI, HTTPException, Path, Query, security, status
1818
from morecantile.defaults import TileMatrixSets
19+
from rasterio.crs import CRS
1920
from rasterio.io import MemoryFile
2021
from rio_tiler.io import BaseReader, MultiBandReader, Reader, STACReader
2122
from starlette.requests import Request
@@ -1553,3 +1554,54 @@ def custom_rescale_params() -> Optional[RescaleType]:
15531554
assert response.headers["content-type"] == "application/x-binary"
15541555
numpy.load(BytesIO(response.content))
15551556
assert npy_tile.shape == (2, 256, 256) # mask + data
1557+
1558+
1559+
def test_dst_crs_option():
1560+
"""test dst-crs parameter."""
1561+
app = FastAPI()
1562+
app.include_router(TilerFactory().router)
1563+
1564+
with TestClient(app) as client:
1565+
# preview endpoints
1566+
response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif")
1567+
assert response.status_code == 200
1568+
assert response.headers["content-type"] == "image/tiff; application=geotiff"
1569+
meta = parse_img(response.content)
1570+
assert meta["crs"] == CRS.from_epsg(
1571+
32621
1572+
) # return the image in the original CRS
1573+
1574+
response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:4326")
1575+
meta = parse_img(response.content)
1576+
assert meta["crs"] == CRS.from_epsg(4326)
1577+
assert not meta["crs"] == CRS.from_epsg(32621)
1578+
1579+
# /crop endpoints
1580+
response = client.get(
1581+
f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif"
1582+
)
1583+
meta = parse_img(response.content)
1584+
assert meta["crs"] == CRS.from_epsg(
1585+
4326
1586+
) # default is to return image in the bounds-crs
1587+
assert not meta["crs"] == CRS.from_epsg(32621)
1588+
1589+
# Force output in epsg:32621
1590+
response = client.get(
1591+
f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:32621"
1592+
)
1593+
meta = parse_img(response.content)
1594+
assert meta["crs"] == CRS.from_epsg(32621)
1595+
1596+
# coord-crs + dst-crs
1597+
response = client.get(
1598+
f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857"
1599+
)
1600+
meta = parse_img(response.content)
1601+
assert meta["crs"] == CRS.from_epsg(3857)
1602+
1603+
response = client.get(
1604+
f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857&dst-crs=epsg:32621"
1605+
)
1606+
meta = parse_img(response.content)
1607+
assert meta["crs"] == CRS.from_epsg(32621)

src/titiler/core/titiler/core/dependencies.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ def __post_init__(self):
451451
self.range = list(map(float, self.range.split(","))) # type: ignore
452452

453453

454-
def CRSParams(
454+
def CoordCRSParams(
455455
crs: str = Query(
456456
None,
457457
alias="coord-crs",
@@ -463,3 +463,17 @@ def CRSParams(
463463
return CRS.from_user_input(crs)
464464

465465
return None
466+
467+
468+
def DstCRSParams(
469+
crs: str = Query(
470+
None,
471+
alias="dst-crs",
472+
description="Output Coordinate Reference System.",
473+
)
474+
) -> Optional[CRS]:
475+
"""Coordinate Reference System Coordinates Param."""
476+
if crs:
477+
return CRS.from_user_input(crs)
478+
479+
return None

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@
3838
BandsParams,
3939
BidxExprParams,
4040
ColorMapParams,
41-
CRSParams,
41+
CoordCRSParams,
4242
DatasetParams,
4343
DatasetPathParams,
4444
DefaultDependency,
45+
DstCRSParams,
4546
HistogramParams,
4647
ImageParams,
4748
ImageRenderingParams,
@@ -431,7 +432,7 @@ def geojson_statistics(
431432
..., description="GeoJSON Feature or FeatureCollection."
432433
),
433434
src_path=Depends(self.path_dependency),
434-
coord_crs: Optional[CRS] = Depends(CRSParams),
435+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
435436
layer_params=Depends(self.layer_dependency),
436437
dataset_params=Depends(self.dataset_dependency),
437438
image_params=Depends(self.img_dependency),
@@ -850,7 +851,7 @@ def point(
850851
lon: float = Path(..., description="Longitude"),
851852
lat: float = Path(..., description="Latitude"),
852853
src_path=Depends(self.path_dependency),
853-
coord_crs: Optional[CRS] = Depends(CRSParams),
854+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
854855
layer_params=Depends(self.layer_dependency),
855856
dataset_params=Depends(self.dataset_dependency),
856857
reader_params=Depends(self.reader_dependency),
@@ -888,8 +889,9 @@ def preview(
888889
),
889890
src_path=Depends(self.path_dependency),
890891
layer_params=Depends(self.layer_dependency),
892+
dst_crs: Optional[CRS] = Depends(DstCRSParams),
891893
dataset_params=Depends(self.dataset_dependency),
892-
img_params=Depends(self.img_dependency),
894+
image_params=Depends(self.img_dependency),
893895
post_process=Depends(self.process_dependency),
894896
rescale=Depends(self.rescale_dependency), # noqa
895897
color_formula: Optional[str] = Query(
@@ -907,8 +909,9 @@ def preview(
907909
with self.reader(src_path, **reader_params) as src_dst:
908910
image = src_dst.preview(
909911
**layer_params,
910-
**img_params,
912+
**image_params,
911913
**dataset_params,
914+
dst_crs=dst_crs,
912915
)
913916
dst_colormap = getattr(src_dst, "colormap", None)
914917

@@ -957,7 +960,8 @@ def part(
957960
maxy: float = Path(..., description="Bounding box max Y"),
958961
format: ImageType = Query(..., description="Output image type."),
959962
src_path=Depends(self.path_dependency),
960-
coord_crs: Optional[CRS] = Depends(CRSParams),
963+
dst_crs: Optional[CRS] = Depends(DstCRSParams),
964+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
961965
layer_params=Depends(self.layer_dependency),
962966
dataset_params=Depends(self.dataset_dependency),
963967
image_params=Depends(self.img_dependency),
@@ -978,6 +982,7 @@ def part(
978982
with self.reader(src_path, **reader_params) as src_dst:
979983
image = src_dst.part(
980984
[minx, miny, maxx, maxy],
985+
dst_crs=dst_crs,
981986
bounds_crs=coord_crs or WGS84_CRS,
982987
**layer_params,
983988
**image_params,
@@ -1024,7 +1029,7 @@ def geojson_crop(
10241029
None, description="Output image type. Default is auto."
10251030
),
10261031
src_path=Depends(self.path_dependency),
1027-
coord_crs: Optional[CRS] = Depends(CRSParams),
1032+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
10281033
layer_params=Depends(self.layer_dependency),
10291034
dataset_params=Depends(self.dataset_dependency),
10301035
image_params=Depends(self.img_dependency),
@@ -1267,7 +1272,7 @@ def geojson_statistics(
12671272
..., description="GeoJSON Feature or FeatureCollection."
12681273
),
12691274
src_path=Depends(self.path_dependency),
1270-
coord_crs: Optional[CRS] = Depends(CRSParams),
1275+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
12711276
layer_params=Depends(AssetsBidxExprParamsOptional),
12721277
dataset_params=Depends(self.dataset_dependency),
12731278
image_params=Depends(self.img_dependency),
@@ -1460,7 +1465,7 @@ def geojson_statistics(
14601465
..., description="GeoJSON Feature or FeatureCollection."
14611466
),
14621467
src_path=Depends(self.path_dependency),
1463-
coord_crs: Optional[CRS] = Depends(CRSParams),
1468+
coord_crs: Optional[CRS] = Depends(CoordCRSParams),
14641469
bands_params=Depends(BandsExprParamsOptional),
14651470
dataset_params=Depends(self.dataset_dependency),
14661471
image_params=Depends(self.img_dependency),

0 commit comments

Comments
 (0)