CoverageJSON output format and API extension for TiTiler.
titiler-covjson adds
CoverageJSON (an Open
Geospatial Consortium (OGC) Community Standard, 21-069r2) as an output format
for TiTiler, so a TiTiler deployment can serve raster data as
standards-compliant, interoperable CoverageJSON. It is a FastAPI router
extension (a titiler.core factory), not a standalone service.
Early alpha. Three value-returning endpoints are implemented over a single dataset, each returning a CoverageJSON coverage in its own domain.
-
Available:
GET {prefix}/bbox/{minx},{miny},{maxx},{maxy}: a 2-D Grid coverage for a bounding box, with output sizing and a cell-count ceiling.GET {prefix}/position?coords=POINT(x y): a Point coverage sampling a single location.GET {prefix}/area?coords=POLYGON((...)): a Polygon coverage reducing the dataset over a polygon to one value per band by astat(defaultmean). The reduction is an unweighted, all-touched pixel statistic: a pixel the polygon boundary merely grazes counts the same as one wholly inside, andstdis the population standard deviation. Expect results to diverge from an area-weighted zonal statistic for polygons only a few pixels across.
All three support band selection (
bidx/expression/ the OGC API - Environmental Data Retrieval (EDR)parameter-namealias) and a singlecrsknob. -
Planned: transect (trajectory), temporal (time series) extraction, further coverage domains, and multi-dataset / STAC sources.
pip install titiler-covjsonRequires Python >= 3.11.
Mount the factory's router on a FastAPI application, and install TiTiler's exception handlers so that reader and dataset errors render as JSON responses with the correct status codes:
from fastapi import FastAPI
from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
from titiler_covjson import CovJSONFactory
app = FastAPI()
app.include_router(CovJSONFactory().router)
add_exception_handlers(app, DEFAULT_STATUS_CODES)Then request a coverage. A bounding box returns a Grid (the four bounds are one
comma-delimited path segment, interpreted in CRS84 by default), a position
returns a Point, and a polygon returns a Polygon reduced to one value per band
(WKT whitespace is percent-encoded as %20):
curl "http://localhost:8000/bbox/-10,-5,10,5?url=/path/to/cog.tif"
curl "http://localhost:8000/position?coords=POINT(0%200)&url=/path/to/cog.tif"
curl "http://localhost:8000/area?coords=POLYGON((-10%20-5,10%20-5,10%205,-10%205,-10%20-5))&url=/path/to/cog.tif"A failure to open or read the dataset url (a missing or unreadable file, an
unreachable or forbidden remote) surfaces as HTTP 500 by default. That is the
conservative choice for an untrusted, multi-tenant deployment: it never blames
the caller for a server-side outage, and it never reveals whether an internal
path the server can reach (but the caller cannot) exists. See
ADR-0003 for the full
rationale.
A single-tenant or otherwise trusted deployment (where the caller's access scope matches the server's, so that existence leak is moot) can remap the failure to a client error by extending TiTiler's status-code map:
from rasterio.errors import RasterioIOError
from starlette import status
from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
add_exception_handlers(
app,
{**DEFAULT_STATUS_CODES, RasterioIOError: status.HTTP_400_BAD_REQUEST},
)A small demo container serves the /bbox, /position, and /area endpoints
against a bundled sample Cloud-Optimized GeoTIFF (COG), so you can try the
format end to end without building your own application.
Local demo only. This container serves an open API that reads whatever
urlyou pass it, with no authentication. Run it on your own machine for testing and confirmation; do not expose it publicly or on a shared network.
Build the image from the repository root (the trailing . build context is
required, because the image installs the package from src/):
docker build -f docker/Dockerfile -t titiler-covjson-demo .Run it:
docker run --rm -p 127.0.0.1:8000:8000 titiler-covjson-demoRequest a coverage for the bundled sample COG (the response is
application/prs.coverage+json; pipe to jq for readable output):
curl "http://localhost:8000/bbox/-10,-5,10,5?url=/data/sample.tif&f=CoverageJSON"
curl "http://localhost:8000/position?coords=POINT(0%200)&url=/data/sample.tif"
curl "http://localhost:8000/area?coords=POLYGON((-10%20-5,10%20-5,10%205,-10%205,-10%20-5))&url=/data/sample.tif"Mount the directory holding your raster read-only, at a path that does not
shadow the bundled /data, then point url at it. Keep the mount as narrow as
possible: the endpoint reads any path under it, so do not mount your home
directory or any tree that holds secrets.
docker run --rm -p 127.0.0.1:8000:8000 -v "/path/to/rasters:/host:ro" titiler-covjson-demo
curl "http://localhost:8000/bbox/<minx>,<miny>,<maxx>,<maxy>?url=/host/your.tif&f=CoverageJSON"docker/data/sample.tif is a small, committed COG generated deterministically
by docker/make_sample_cog.py; reproduce it with:
uv run python docker/make_sample_cog.pydocker/check_sample.py guards the committed file against drifting from its
generator.
See the docs/ directory. The current-direction documents are the
bounding-box endpoint spec and the architecture decision records; earlier design
documents predate the implementation and carry a superseded note where relevant.
- Bounding-box endpoint spec: the implemented
/bboxGrid endpoint - Architecture decision records: cross-cutting decisions
- Background and earlier design exploration: design overview, API definition, data model reference, modeler/converter design, implementation roadmap (retired), libraries analysis, API design alternatives
titiler-covjson/
├── src/titiler_covjson/
│ ├── __init__.py # public API: CovJSONFactory, CovJSONResponse, media type
│ ├── factory.py # CovJSONFactory(BaseFactory): /bbox, /position, /area
│ ├── dependencies.py # CovJSONBandParams, validate_covjson_format, area_stat
│ ├── responses.py # CovJSONResponse + COVJSON_MEDIA_TYPE
│ ├── input.py # CoverageInput intermediate representation
│ ├── modeler.py # to_coverage: CoverageInput -> covjson-pydantic models
│ ├── reduce.py # Stat + reduce_bands: per-band zonal reduction
│ └── helpers.py # CRS, unit, and dtype mapping utilities
├── tests/
├── docs/
├── pyproject.toml
├── LICENSE # MIT
└── README.md
MIT