Visualization & tiles
Every render decision — which bands, what colormap, how to rescale — lives in the backend, so the same item looks identical whether it is drawn by the portal, an SDK, an agent, or an embedded map client.
Geopera serves imagery as standard XYZ map tiles ({z}/{x}/{y}) produced on demand
from Cloud-Optimized GeoTIFFs (COGs). You never compute pixels client-side: you tell the
platform what to show (a visualization, or explicit band-math), and it resolves the how and returns the tile. Three layers cooperate:
- Tile rendering — turn a source COG (or a catalog item) into a PNG/WebP/JPEG tile.
- Visualizations — the named, backend-owned catalog of how an item can be shown (natural colour, colour-infrared, spectral indices, elevation), fully resolved.
- Map-client integration — TileJSON and OGC WMTS so OpenLayers, Leaflet, deck.gl, or QGIS can consume a layer with a plain URL template.
All operations here are side_effect=read and scoped tiles:read (or items:read for
the catalog-item routes). See scopes for what each grants.
The tile contract is mid-migration. An in-flight kernel change is collapsing render decisions onto a single governed operation: the client will send
source+ a visualization id (+ optional AOI) and the backend resolves colormap, band-math, and rescale server-side (auto or AOI-scoped) rather than the client baking a fixedrescale=into the URL. Because of this, the exact request fields forcog.tile.renderandvisualization.list_formay differ from what you see below. Treat this page as the concepts and workflow; always confirm the current parameter shapes against the operations reference before you hardcode a request. Do not bake a globalrescalevalue into tile URLs — let the backend rescale.
How tile rendering works
A tile request names an XYZ coordinate and a source, and the backend reads only the overviews and windows it needs from the COG, applies the render config, and returns the encoded tile. There are two source families:
- Arbitrary COG (
cog.*) — render any SSRF-allowlisted COG URL. Authentication is the only gate; there is no per-resource access check, so these are ideal for public data and your own COG outputs. - Catalog item (
items.tile.*) — render one of your catalog items byitem_id, with item-level access control (project or org membership) and per-tile usage attribution applied automatically.
Both produce byte-identical tiles for the same render config and are heavily cached (a
hot in-process cache plus Redis, with ETag and long Cache-Control headers).
Operations at a glance
| Operation | Source | Scope | Returns |
|---|---|---|---|
cog.tile.render | COG URL | tiles:read | Binary tile (png/webp/jpg) |
cog.tile.terrain | COG URL | tiles:read | Terrain-RGB tile (webp) |
cog.thumbnail | COG URL | tiles:read | JPEG thumbnail |
cog.statistics | COG URL | tiles:read | AOI percentile stats + rescale |
cog.formulas | — | tiles:read | Spectral-index formula templates |
cog.colormaps | — | tiles:read | Curated colormap groups |
visualization.list_for | item bands | tiles:read | Available visualizations |
items.tile.render | item id | items:read | Binary tile |
items.tile.tilejson | item id | items:read | TileJSON metadata |
items.tile.statistics | item id | items:read | Expression stats |
items.tile.wmts_capabilities | item id | items:read | WMTS GetCapabilities XML |
items.tile.wmts_get_tile | item id | items:read | WMTS GetTile (binary) |
Every one is invoked the same way: POST /v1/op/{operation_id} with a JSON body and a
Bearer token. See authentication and concepts for the RPC-over-HTTP model.
Rendering a tile from a COG
cog.tile.render is the workhorse. You give it a COG url, an XYZ coordinate
(z/x/y), and an output format, plus how to colour it. The colouring is expressed
one of two ways, and they are mutually exclusive:
bands— a comma-separated list of 1-based band indices for an RGB composite, e.g."1,2,3"for true colour.expression— a band-math formula usingb1,b2, … references (for a spectral index like NDVI). Pair with acolormapto colour the single-band result.
Supported format values are png, webp, jpg, and jpeg. quality (default 85)
applies to lossy formats.
curl -s -X POST https://api.geopera.com/v1/op/cog.tile.render \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://data.geopera.com/cogs/scene-abc.tif",
"z": 12, "x": 1205, "y": 2654,
"format": "webp",
"bands": "1,2,3"
}' --output tile.webpA band-math render uses expression plus a colormap instead of bands:
curl -s -X POST https://api.geopera.com/v1/op/cog.tile.render \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://data.geopera.com/cogs/scene-abc.tif",
"z": 12, "x": 1205, "y": 2654,
"format": "png",
"expression": "(b4-b3)/(b4+b3)",
"colormap": "rdylgn"
}' --output ndvi.pngThe response is the raw tile bytes (this op is raw_response), with Content-Type set
from the format, a long-lived Cache-Control, and an ETag you can use for
conditional requests.
Rescale: let the backend decide
rescale (a "min,max" string) maps data values to the colour ramp. The platform
direction is to stop baking a fixed global rescale into tile URLs — a global
elevation range flattens a 0–50 m harbour DEM to one colour, for example. Two strategies
the backend applies for you:
- Auto rescale — derived from the dataset’s own statistics (e.g. a 2-σ stretch from embedded/overview stats) when no AOI is supplied.
- AOI rescale — scaled to the data inside an area of interest, so the colour ramp
is consistent at every zoom. Compute it with
cog.statistics(below), or pass anaoiso the render scales and clips to it.
If you do pass an explicit rescale, treat it as an override and confirm the current
field shape in the operations reference — this is one of
the parameters in flux.
AOI-scoped statistics
cog.statistics reads the COG masked to a GeoJSON geometry and returns percentile
statistics plus a ready-to-use rescale string, so colormaps scale to the data inside
your AOI rather than a global range. It works for elevation, RGB band combos, and
band-math indices.
curl -s -X POST https://api.geopera.com/v1/op/cog.statistics \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://data.geopera.com/cogs/dem-harbour.tif",
"geometry": {
"type": "Polygon",
"coordinates": [[[151.20,-33.86],[151.23,-33.86],[151.23,-33.84],[151.20,-33.84],[151.20,-33.86]]]
},
"percentiles": [2.0, 98.0]
}'Use the returned rescale when you render tiles for that AOI so the harbour DEM spans
its real ~0–42 m, not −100…3000 m.
Terrain and thumbnails
cog.tile.terrainrenders a Mapbox Terrain-RGB tile from a single-band elevation/depth COG — the encoding deck.gl and other 3D clients expect for a height mesh. It accepts the samez/x/y, an optionalinvert(for depth), and an optionalaoithat clips the mesh so it does not extend beyond the AOI on zoom-out.cog.thumbnailproduces a quick JPEG preview (default 256×256) from the smallest COG overview — cheap, and handy for catalog cards. It auto-stretches non-uint8data with a 2–98 percentile clip.
curl -s -X POST https://api.geopera.com/v1/op/cog.thumbnail \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "url": "https://data.geopera.com/cogs/scene-abc.tif", "width": 320, "height": 320 }' \
--output thumb.jpgVisualizations: the backend-owned catalog
Rather than each client deciding what “natural colour” or “NDVI” means, Geopera owns the
catalog of visualizations for an item. visualization.list_for takes the bands available
on an item (mapped from its eo:bands) and returns the standard, fully resolved VisualizationSpecs — RGB composites, colour-infrared, the applicable spectral indices
from the authoritative index registry, and elevation for DEMs. Band-math is resolved to b1/b2 references and labels are correct by construction (a DEM is never mislabeled
“Natural Colour”).
You give it band_roles (role name → 1-based band index), a data_type (raster, dem, …), and optional registry render_params:
curl -s -X POST https://api.geopera.com/v1/op/visualization.list_for \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"band_roles": { "RED": 1, "GREEN": 2, "BLUE": 3, "NIR": 4 },
"data_type": "raster"
}'A representative response (fields shown are the resolved spec shape; the exact request contract for this op is part of the in-flight migration — verify it in operations):
{
"visualizations": [
{
"id": "natural-colour",
"label": "Natural Colour",
"kind": "rgb",
"bands": [1, 2, 3],
"is_default": true,
"description": "True-colour RGB"
},
{
"id": "color-infrared",
"label": "Colour Infrared",
"kind": "rgb",
"bands": [4, 1, 2],
"description": "NIR-Red-Green — vegetation in red"
},
{
"id": "ndvi",
"label": "NDVI",
"kind": "index",
"expression": "(b4-b1)/(b4+b1)",
"colormap": "rdylgn",
"rescale": "-1,1",
"category": "vegetation"
}
],
"default_id": "natural-colour"
}Each spec carries everything a render needs: bands (for RGB), or expression + colormap + rescale (for an index), or a colormap + rescale for elevation. The
intended workflow is to read the catalog, let the user pick a visualization id, and have
the backend resolve that id into the render config when it draws the tile — so the client
never re-derives band-math, colormaps, or rescale.
For a DEM, visualization.list_for returns a single elevation spec with a terrain colormap; render it with cog.tile.render (colormapped) for the 2D map and cog.tile.terrain for the 3D mesh, sharing one rescale.
Per-item saved profiles
The derived catalog above is the standard menu. Users can also save their own visualization profiles on an item (a named band/expression/colormap/rescale config); clients merge those with the derived catalog.
| Operation | Side-effect | Scope | Use |
|---|---|---|---|
visualization.profiles.list | read | items:read | List an item’s saved profiles |
visualization.profile.get | read | items:read | Get one profile |
visualization.profile.create | compute | items:write | Save a new profile (editor/admin) |
visualization.profile.update | compute | items:write | Update a profile (editor/admin) |
visualization.profile.set_default | compute | items:write | Make a profile the item default |
visualization.profile.delete | destructive | items:write | Remove a profile (editor/admin) |
Reads require item member access (project or org membership); writes require editor or admin on the item’s project. See scopes.
Catalog-item tiles
For your own catalog items, items.tile.render renders by item_id with item-level
access control and automatic per-tile usage attribution. You can drive the render with a
saved profile, or override directly with bands / expression (+ rescale, colormap); asset_key selects a named asset on multi-asset items.
curl -s -X POST https://api.geopera.com/v1/op/items.tile.render \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"item_id": "itm_8f2c...",
"z": 12, "x": 1205, "y": 2654,
"format": "webp",
"profile": "natural-colour"
}' --output item-tile.webpitems.tile.tilejson returns TileJSON 3.0.0 metadata (name, tile URL template, zoom
range, the resolved profile, and the renderable asset) for clients that bootstrap a layer
from a TileJSON document. items.tile.statistics computes global p2/p98 expression
statistics from the smallest overview for a quick rescale.
Serving tiles to map clients
Map libraries want a plain URL template, not a POST body. Geopera preserves the classic XYZ / TileJSON / WMTS wire contracts through thin GET projections over these ops, so you can wire a layer into OpenLayers, Leaflet, deck.gl, or QGIS directly.
XYZ template
Point any XYZ layer at the catalog-item tile route. The template uses {z}/{x}/{y} and a
format extension:
// OpenLayers / Leaflet style XYZ template for a catalog item
const tileUrl =
'https://api.geopera.com/api/v1/items/itm_8f2c.../tiles/{z}/{x}/{y}.webp?profile=natural-colour';The matching items.tile.tilejson document gives you that template plus zoom bounds if
you prefer to configure the layer from TileJSON.
WMTS
For OGC clients (QGIS, ArcGIS, MapServer-based stacks), items.tile.wmts_capabilities emits a WMTS 1.0.0 GetCapabilities document, and items.tile.wmts_get_tile serves
individual tiles (KVP). Capabilities advertises the GoogleMapsCompatible (EPSG:3857) tile matrix set across zoom 0–18, image/webp and image/png formats, and a ResourceURL template. You supply base_url (and an optional key) so the emitted
absolute URLs resolve back to your deployment.
curl -s -X POST https://api.geopera.com/v1/op/items.tile.wmts_capabilities \
-H "Authorization: Bearer $GEOPERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"item_id": "itm_8f2c...",
"base_url": "https://api.geopera.com",
"profile": "natural-colour"
}'Point a WMTS client at the WMTSCapabilities.xml projection of that op and it discovers
the layer, tile matrix set, and tile URL template automatically. items.tile.wmts_get_tile serves a transparent tile rather than erroring when a coordinate has no data, which keeps
map clients happy at the edges of coverage.
Worked example: NDVI over an AOI
End to end — discover the visualization, scope the rescale to an AOI, and render a tile —
in Python with the geopera package.
from geopera import Client
client = Client(api_key="gpra_...") # or a session token
cog = "https://data.geopera.com/cogs/scene-abc.tif"
aoi = {
"type": "Polygon",
"coordinates": [[
[151.20, -33.86], [151.23, -33.86],
[151.23, -33.84], [151.20, -33.84], [151.20, -33.86],
]],
}
# 1. Discover the visualizations available for these bands.
viz = client.op("visualization.list_for", {
"band_roles": {"RED": 1, "GREEN": 2, "BLUE": 3, "NIR": 4},
"data_type": "raster",
})
ndvi = next(v for v in viz["visualizations"] if v["id"] == "ndvi")
# 2. Scope the colormap to the data inside the AOI.
stats = client.op("cog.statistics", {
"url": cog,
"geometry": aoi,
"expression": ndvi["expression"],
})
# 3. Render a tile using the resolved expression, colormap, and AOI-scoped rescale.
tile = client.op("cog.tile.render", {
"url": cog,
"z": 12, "x": 1205, "y": 2654,
"format": "png",
"expression": ndvi["expression"],
"colormap": ndvi["colormap"],
"rescale": stats["rescale"],
})
with open("ndvi-aoi.png", "wb") as f:
f.write(tile)The same three calls work identically from @geopera/sdk, the geopera CLI, or the MCP
gateway — there is one render decision point, so every client produces the same pixels.
Gotchas
bandsandexpressionare mutually exclusive. Sending both is a400. Usebandsfor an RGB composite,expression(+colormap) for a single-band index.- Band indices are 1-based, comma-separated as a string (
"1,2,3"), matching the band numbering in the visualization catalog. - Don’t bake a global
rescaleinto URLs. A fixed range flattens data outside it; prefer auto or AOI rescale viacog.statistics. The exactrescaleplumbing is part of the in-flight migration. cog.*renders have no per-resource access check — authentication alone is the gate. Useitems.tile.*for catalog items so item-level entitlement and usage attribution apply.- Reuse the cache. Tiles carry an
ETagand a longCache-Control; sendIf-None-Matchto avoid re-rendering unchanged tiles. AOI-scoped tiles are cached separately from global ones. - Renders can time out. A tile or stats computation that exceeds the server budget
returns
504; a malformed expression returns400. Errors are problem+json. - Confirm the contract.
cog.tile.renderandvisualization.list_forare mid-strangle — check the operations reference for current fields rather than relying on the examples here verbatim.
Related
- Operations reference — the authoritative, current parameter list for every tile and visualization operation.
- Processing & analytics — produce the COGs and derived rasters you then visualize.
- Authentication · Scopes · Errors — the cross-cutting contracts every op shares.