Skip to content

Commit 51ca83e

Browse files
committed
docs(tilepack-api): update info about canonical vs non-canonical
Assisted by: Opus 4.6 LLM
1 parent eea0b76 commit 51ca83e

3 files changed

Lines changed: 96 additions & 8 deletions

File tree

backend/tilepack-api/README.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,18 @@ GET /healthz
3636

3737
The only user input is a STAC item id (regex-validated, max 128 chars,
3838
must exist in the configured `openaerialmap` STAC collection) and the
39-
output format. Optional `min_zoom`/`max_zoom` let callers cap the
40-
output; when omitted the worker derives the max zoom from the source
41-
COG's native ground resolution.
39+
output format.
40+
41+
Zoom behavior has two modes:
42+
43+
- **Canonical request**: omit `min_zoom` and `max_zoom`.
44+
- Worker derives zoom range from source GSD.
45+
- Result is the default archive for that item+format.
46+
- This variant is represented in STAC (asset key `pmtiles` / `mbtiles`).
47+
- **Non-canonical request**: set both `min_zoom` and `max_zoom`.
48+
- Worker generates exactly that zoom range.
49+
- Result is stored in S3 under a zoom-suffixed key (`_z<min>-<max>`).
50+
- This variant is **not** written to STAC; caller receives direct URL in API response.
4251

4352
| Status | Meaning |
4453
| ------ | ---------------------------------- |
@@ -49,9 +58,53 @@ COG's native ground resolution.
4958
| 422 | Item has no COG asset |
5059
| 429 | Per-IP limit or global cap reached |
5160

52-
The endpoint is **idempotent**: re-POSTing the same id+format returns
61+
The endpoint is **idempotent**: re-POSTing the same request returns
5362
`ready` once the artifact lands.
5463

64+
### Response examples (polling flow)
65+
66+
The same endpoint is used to trigger and poll job status.
67+
68+
#### Canonical example (no zoom params)
69+
70+
```http
71+
POST /tilepacks/67ac270a43f18e3e3665bef7?format=pmtiles
72+
```
73+
74+
```json
75+
{ "status": "started" }
76+
```
77+
78+
```json
79+
{ "status": "in_progress" }
80+
```
81+
82+
```json
83+
{ "status": "ready", "url": "https://.../67ac270a43f18e3e3665bef7.pmtiles" }
84+
```
85+
86+
Canonical outputs are patched into STAC assets (`pmtiles` / `mbtiles`).
87+
88+
#### Non-canonical example (custom zoom)
89+
90+
```http
91+
POST /tilepacks/67ac270a43f18e3e3665bef7?format=pmtiles&min_zoom=12&max_zoom=17
92+
```
93+
94+
```json
95+
{ "status": "started" }
96+
```
97+
98+
```json
99+
{
100+
"status": "ready",
101+
"url": "https://.../67ac270a43f18e3e3665bef7_z12-17.pmtiles"
102+
}
103+
```
104+
105+
Non-canonical outputs are served by URL in the API response and are not
106+
written to STAC.
107+
55108
## Design Choices
56109

57110
This service runs standalone and has no auth. It can only operate on

backend/tilepack-api/internal/handler/handler.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ type response struct {
126126
Message string `json:"message,omitempty"`
127127
}
128128

129+
// postTilepack starts or polls tilepack generation for one STAC item.
130+
//
131+
// Endpoint:
132+
//
133+
// POST /tilepacks/{id}?format=pmtiles|mbtiles[&min_zoom=N&max_zoom=N]
134+
//
135+
// The same endpoint is used for trigger + polling. Clients should re-POST
136+
// the same URL until status becomes "ready".
137+
//
138+
// Zoom modes:
139+
// - Canonical request: min_zoom and max_zoom omitted.
140+
// Worker derives zooms from source GSD; resulting artifact is the
141+
// canonical item+format variant and is represented in STAC assets.
142+
// - Non-canonical request: min_zoom and max_zoom both provided.
143+
// Worker generates exactly that range; artifact is served from S3 via
144+
// response URL and is intentionally not written to STAC.
145+
//
146+
// Typical statuses:
147+
// - 202 {"status":"started"} or {"status":"in_progress"}
148+
// - 200 {"status":"ready","url":"https://..."}
129149
func (h *Handler) postTilepack(w http.ResponseWriter, r *http.Request) {
130150
started := time.Now()
131151
id := r.PathValue("id")
@@ -164,6 +184,13 @@ func (h *Handler) postTilepack(w http.ResponseWriter, r *http.Request) {
164184
respond(http.StatusBadRequest, response{Status: "error", Message: err.Error()}, "invalid_input")
165185
return
166186
}
187+
// "canonical" means the default tilepack variant for an item+format:
188+
// the caller did not set min/max zoom, so the worker derives zooms
189+
// from source GSD and this artifact is represented in STAC.
190+
//
191+
// Non-canonical means a caller explicitly requested min/max zoom.
192+
// Those are generated and served from S3, but intentionally not
193+
// written to STAC so one request cannot overwrite catalogue metadata.
167194
canonical := minZoom == 0 && maxZoom == 0
168195

169196
// Per-IP rate limit happens before any expensive work so abusive
@@ -252,8 +279,13 @@ func (h *Handler) postTilepack(w http.ResponseWriter, r *http.Request) {
252279
// If STAC says asset exists but S3 object is missing, fall through
253280
// and regenerate so both systems converge.
254281
} else if s3Exists {
255-
// Non-canonical requests are represented by the concrete S3 object,
256-
// not by a STAC asset entry.
282+
// Non-canonical (custom zoom) requests are not represented in
283+
// STAC. The caller gets a direct S3-backed public URL instead.
284+
//
285+
// Why: STAC should describe the single canonical archive for an
286+
// item+format. If we patched STAC for custom zoom requests, one
287+
// caller could make STAC metadata (href/minzoom/maxzoom) flip to
288+
// an ad-hoc variant that other users did not ask for.
257289
log.Printf("ready: stac_id=%s format=%s mode=non_canonical state=s3", id, format)
258290
respond(http.StatusOK, response{Status: "ready", URL: h.s3.PublicURL(outputKey)}, "ready")
259291
return

backend/tilepack-api/internal/s3/s3.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ func New(ctx context.Context, bucket, publicBaseURL string) (*Client, error) {
4545
//
4646
// oin-hotosm-temp/<metadata-id>/0/<id>.{mbtiles|pmtiles}
4747
//
48-
// For custom-zoom variants we append a suffix so they don't collide
49-
// with the canonical archive.
48+
// For non-canonical (custom-zoom) variants we append a suffix so they
49+
// don't collide with the canonical archive key.
50+
//
51+
// canonical: <key>.{mbtiles|pmtiles}
52+
// non-canonical: <key>_z<min>-<max>.{mbtiles|pmtiles}
5053
//
5154
// Returns an error if the COG URL doesn't reference the configured
5255
// bucket - we refuse to write into a bucket we weren't given.

0 commit comments

Comments
 (0)