Skip to main content

OGC API - Tiles

Shigola serves OGC API - Tiles for vector (Mapbox Vector Tile) data. It is the only tile surface Shigola serves.

Two things to know before deploying

The service root is the landing page

OGC API - Tiles requires a landing page at the service root, so / returns JSON. Point bookmarks, reverse-proxy rules and health checks accordingly. An unknown path returns 404.

Cache keys begin with the tiling scheme

{tileMatrixSetId}/{map}/{layer}/{z}/{x}/{y}

Without the scheme, tiles cut in two schemes collide: WorldCRS84Quad's matrix is twice as wide as WebMercatorQuad's, so the same z/x/y names different ground in each, and one scheme's tiles would be served for the other's.

If a map's tile_matrix_sets changes, its existing entries become unreachable — nothing reads the old keys. Purge and re-seed:

shigola cache purge --config=config.toml --bounds=-180,-85.0511,180,85.0511 --max-zoom=
shigola cache seed --config=config.toml --bounds=-180,-85.0511,180,85.0511 --max-zoom=

For a file or S3 cache, deleting the old directory tree is faster than purging tile by tile.

Endpoints

PathResource
/Landing page — includes shigolaVersion, the build serving the request
/apiThis service's OpenAPI 3.0 definition
/conformanceThe conformance classes implemented
/collectionsEvery collection
/collections/{collectionId}One collection
/collections/{collectionId}/tilesThe collection's tilesets, one per scheme
/collections/{collectionId}/tiles/{tileMatrixSetId}Tileset metadata (?f=tilejson for TileJSON 3.0)
/collections/{collectionId}/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}A vector tile
/tileMatrixSetsThe tiling schemes served
/tileMatrixSets/{tileMatrixSetId}One scheme's definition

Behind a reverse proxy, uri_prefix applies to every route above, and every link and URI template the service emits carries the prefix it was reached on.

Which build is answering

Two resources report the running build, so an operator can tell from the service itself:

GET / -> { "title": "shigola", "shigolaVersion": "v1.2.3", ... }
GET /api -> { "info": { "version": "1.0.0", "x-shigola-version": "v1.2.3", ... } }

Both are extension members. OGC API - Common defines no place for an implementation's version, and OpenAPI's info.version is the version of the API — fixed by the specification this surface implements, and unmoved by a rebuild — so the build is reported alongside it as an x- extension rather than in it. Neither member appears when the binary was built without a version stamped in.

No style document

Shigola serves no style document, by decision rather than omission. Styling is a separate specification — OGC API - Styles — which Shigola does not implement. A client brings its own style and points a vector source at a tileset's TileJSON:

"sources": {
"parks": {
"type": "vector",
"url": "http://localhost:8080/collections/parks/tiles/WebMercatorQuad?f=tilejson"
}
}

Collections

Every map is a collection, and so is every layer of every map:

parks the whole map — tiles carry all its layers
parks:trees one layer — tiles carry only that layer

The map-collection is always published, even for a single-layer map, so a map name is always a usable collection id.

The separator is : rather than /, . or _: a slash would make the id look like two path segments, and a dot or underscore can occur in a Shigola map or layer name, which would make the split ambiguous.

The layer tier is the part a map can decline. With serve_layer_collections = false on a map, that map publishes its whole-map collection only:

parks the whole map — unchanged
parks:trees collection not found

The flag removes the layer ids and nothing else: the whole-map collection keeps its tilesets and keeps serving tiles. It is per map and defaults to on, so a map that declines the tier and a map that says nothing about it sit in the same config.

Tile paths are z/y/x

OGC orders a tile path {tileMatrix}/{tileRow}/{tileCol} — zoom, row, then column. This is worth checking against a client that assumes the z/x/y order most tile URLs use:

/collections/parks/tiles/WebMercatorQuad/3/2/5 z=3 y=2 x=5

Rows and columns are validated separately, so a transposed request is rejected rather than served as a different tile — in WorldCRS84Quad at z1 there are four columns but only two rows.

Content negotiation

?f= selects a representation and overrides Accept. An unrecognised f is a 400, not a fallback, so a typo does not quietly return something else. An Accept header naming only types this service cannot produce gets the default representation, which is what a browser receives.

ResourceAccepted f
a tilemvt, or pbf for the same thing
tileset metadatajson (default), tilejson
everything elsejson

mvt is canonical: it is what every link and template this service emits says, and it is the name in the OGC conformance class. pbf is accepted because it is the name the same tile carries in the format member of the TileJSON above, and the extension vector tiles are commonly served at — being refused for using that word would be surprising. Matching ignores case. The alias resolves to MVT before a resource's own formats are consulted, so ?f=pbf on a JSON-only resource is still a 400.

Caching

Tile requests are keyed as {tileMatrixSetId}/{map}/{layer}/{z}/{x}/{y}. The key does not include the query string, so every spelling of ?f= shares one entry rather than storing the same bytes twice.

A whole-map collection's tiles use the same keys shigola cache seed writes, so a seeded tile is served rather than generated a second time. A layer collection's tiles carry the layer in the key, and cache seed and cache purge pass an empty layer — they address a map's own key only — so layer tiles are cached as they are served and cannot be seeded ahead of a request or purged by the CLI.

A tile request carrying any other query parameter is served uncached: the key cannot describe it. Nothing on this surface passes query parameters through to a provider — [[maps.params]] is configurable but unread — so no such request can reach a different rendering. Serving it uncached is what keeps that true if it ever changes: tiles must not already be pooled under a key that ignores a parameter.

Conformance

/conformance declares:

http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core
http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landingPage
http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections
http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json
http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/mvt
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geodata-tilesets
http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/oas30

Verified against the OGC CITE suite (ets-ogcapi-tiles10 1.2, via TeamEngine), serving the Athens OSM extract out of the repository's PostGIS fixture through ST_AsMVT:

15 passed · 0 failed · 1 untested WebMercatorQuad
15 passed · 0 failed · 1 untested WorldCRS84Quad

The untested assertion is .../conf/dataset-tilesets, which this service does not implement and does not declare — tilesets are per collection, not for the dataset as a whole.

Responses are also validated against the OGC schemas the standard points at, which CITE does not check exhaustively: tileset metadata against tms/2.0/json/tileSet.json, and the tilesets list against the schema embedded in Requirement 10 C. Both validate with no errors.

Running CITE yourself

CI runs the suite on both schemes — weekly and on demand, since the suite is versioned separately from the server and a passing implementation can start failing without a commit.

The fixture data is the Athens OSM extract in the PostGIS fixture, served through the mvt_postgis provider, so a run needs that fixture up first. To reproduce a run locally, from the repository root:

docker compose up -d && docker wait migration # the Athens fixture, in PostGIS
go build -mod vendor -o /tmp/shigola ./cmd/shigola
/tmp/shigola serve --config .github/cite/config.toml --port ":8081" &
.github/cite/run.sh WebMercatorQuad 14 6324 9271
.github/cite/run.sh WorldCRS84Quad 14 4740 18542

Each run prints <scheme>: 15 passed, 1 untested, then <scheme>: OK. The runner enforces a floor of 15 passed assertions, because the EARL report carries no summary line and a run that reached nothing at all reports no failures either.

The fixture's layers declare a narrow zoom window (13–15). That used to be about accuracy: ST_AsMVTGeom maps the bounding box onto the tile grid affinely, and one SQL statement passing !BBOX! could not be affine-correct for a mercator grid and a geographic one at the same time. The !TILE_BBOX! and !TILE_SRID! tokens removed that constraint — see Layer SRID and tiling scheme CRS — and the window is now just the range the suite is run over. The comments in .github/cite/config.toml carry the arithmetic.

Two things about this suite are worth knowing before you blame the server:

  1. Six of its arguments are test inputs it does not discover for itselftilematrixsetdefinitionuri, urltemplatefortiles, tilematrix, and the row and column bounds. Omit them and three MandatoryCore tests fail with "A tile matrix set definition uri was not found in the test inputs", which reads like a defect in the service and is not one.
  2. The EARL report it returns has no summary line. A run that reached nothing at all looks identical to a clean pass unless you count the earl:outcome values. The bundled runner enforces a minimum pass count for exactly this reason.

Pick a row and column inside the tileset's own tileMatrixSetLimits, so the run exercises the content checks against a tile that actually holds data. The arguments are <tileMatrixSetId> <tileMatrix> <tileRow> <tileCol>, row before column, and they do not carry over between schemes: a WorldCRS84Quad tile is half the width and a bit over half the height of a WebMercatorQuad tile at the same zoom, so the same ground has a different index in each. It matters more than it looks — ST_AsMVT emits nothing at all for a layer with no rows, and the suite reports no failure for a tile that came back empty.

The manual equivalent, with TeamEngine and the server as containers on one Docker network. The server now needs to reach PostGIS as well, so the fixture joins that network too, and the config copy the server reads has to name the database by a hostname a container can resolve rather than localhost:

docker network create cite-net

# 0. the fixture, reachable from the network the server runs on. `uri` in the
# config under citedata/ must point at postgis:5432, not localhost.
docker compose up -d && docker wait migration
docker network connect cite-net postgis

# 1. the tile server, serving a map with data
docker run -d --name cite-shigola --network cite-net \
-v "$PWD/citedata:/data" -w /data --entrypoint /data/shigola \
shigola-dev:latest serve --config /data/config.toml

# 2. TeamEngine with the OGC API - Tiles suite
docker run -d --name cite-te --network cite-net -p 8888:8080 ogccite/ets-ogcapi-tiles10

# 3. run the suite. Credentials are ogctest/ogctest.
curl -u ogctest:ogctest -G \
--data-urlencode "iut=http://cite-shigola:8080/" \
--data-urlencode "noofcollections=-1" \
--data-urlencode "tilematrixsetdefinitionuri=http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad" \
--data-urlencode "urltemplatefortiles=http://cite-shigola:8080/collections/athens/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}" \
--data-urlencode "tilematrix=14" \
--data-urlencode "mintilerow=6324" --data-urlencode "maxtilerow=6324" \
--data-urlencode "mintilecol=9271" --data-urlencode "maxtilecol=9271" \
http://localhost:8888/teamengine/rest/suites/ogcapi-tiles-1.0/run

Credit

OGC API - Tiles is a standard of the Open Geospatial Consortium, and conformance is verified with OGC's own CITE executable test suite.