# Roadmap

What gets built, in what order, and where each piece stands. This is the only document
that tracks status. [README.md](../README.md) describes the API, and
[ARCHITECTURE.md](ARCHITECTURE.md) describes how it works.

The order follows the pipeline, pure stages first. Every phase ends with something
testable without a network, and nothing touches a real tile server until the phase
that needs one.

| Phase | | Status |
|---|---|---|
| 0 | Design: README, architecture, prior-art audit | done |
| 1 | Projection and tile grid | done |
| 2 | Canvas and a synthetic source | done |
| 3 | HTTP sources and fetching | done |
| 4 | Tile cache | done |
| 5 | Fallback sources and mirrors | done |
| 6 | Fitting the viewport | done |
| 7 | Markers and attribution | done |
| 8 | `StaticMaps.Plug` | done |
| 9 | `StaticMaps.Component` for Phoenix LiveView | not started |
| 10 | `Source.PMTiles` | not started |
| 11 | Tile cache shared across a cluster | not started |
| 12 | `StaticMaps.TilePlug`: serving tiles you may redistribute | not started |

## Phase 0 — Design

Done. README and architecture written, and five existing implementations
(StephanGeorg/staticmaps, komoot/staticmap, flopp/py-staticmaps, Luzifer/staticmap,
mapbox/quilt) audited from source. What the audit changed is recorded in
[ARCHITECTURE.md § Lessons from prior art](ARCHITECTURE.md#lessons-from-prior-art).

## Phase 1 — Projection and tile grid

Done. Pure maths, no dependencies.

- `StaticMaps.Projection`: lat/lng ↔ world pixels, latitude clamp.
- `StaticMaps.Viewport`: the resolved view and the one world-to-output rounding rule.
  Fitting joins it in Phase 6.
- `StaticMaps.TileGrid`: tile keys and placements for a centre, zoom, size, scale and
  source tile size, with antimeridian wrap by placement, pole rows dropped and keys
  deduplicated.
- Property tests for round trips and full coverage; explicit cases for zoom 0, the
  antimeridian, the poles and 512-px sources.

**Done when** the grid for any valid viewport covers every output pixel exactly once with
in-range keys.

## Phase 2 — Canvas and a synthetic source

Done. The first image out.

- `StaticMaps.Canvas` over `vix`: background, tile insertion with clipping, PNG and JPEG
  encoding.
- `StaticMaps.Source` behaviour with `fetch/2` and `info/1`.
- A test-only synthetic source drawing labelled, coloured tiles.
- The pixel-tolerance image comparison helper used by every later phase.
- `render/1` end to end with `:center`, `:zoom`, `:size`, `:scale`, `:format` and
  `:background`, and `{:error, _}` for invalid options.

**Done when** golden images for the Phase 1 edge cases match.

## Phase 3 — HTTP sources and fetching

Done.

- `StaticMaps.Source.HTTP`: `User-Agent` with the configured contact, timeouts, retries
  on transport errors, `429` and `5xx` with `Retry-After`, response validation.
- `StaticMaps.Fetcher`: per-source global concurrency limit, joining in-flight fetches.
- `Source.OSM` and `Source.Template`.
- `on_tile_error: :fail | :fill` and `%StaticMaps.TileError{}`.
- Tested against a local Bandit server that can be told to fail, stall or lie about
  content type.

**Done when** a real OSM render works by hand, and every failure mode of the local server
produces the documented result.

## Phase 4 — Tile cache

Done. Expired tiles are also served when revalidation fails, so a tile server outage does
not fail renders it has already served.

- `StaticMaps.TileCache` behaviour and the ETS implementation.
- Freshness from `Cache-Control`, `Expires` or the 7-day default; conditional
  revalidation; byte limit with LRU eviction.

**Done when** a second render of the same view makes no requests, and an expired tile is
revalidated with a conditional request.

## Phase 5 — Fallback sources and mirrors

Done. Attribution is not drawn yet, so that a fallback's image carries the fallback's
attribution is checked in Phase 7. Checking providers' terms for the guide showed that
several forbid server-side caching, so sources can opt out of the tile cache with
`cache: false`.

Two ways to keep rendering when a tile server is slow or down: fallback between different
sources, and load spread across mirrors of the same source.

- `source: [primary, fallback, …]`. When a render fails with a `TileError`, the whole
  map is rendered again from the next source. Tiles are never mixed between sources:
  providers draw different maps, so a mixed image is a visible patchwork, and the
  attribution would credit only one of them.
- The attribution drawn is always that of the source the image came from.
- A circuit breaker per source: after repeated failures that show it is unhealthy, not
  merely missing tiles, a source is skipped for a cooling-off period instead of every
  render waiting for it to fail again.
- A guide listing `Source.Template` configurations for common providers, each with a link
  to its terms, rather than a built-in module per provider. Provider URLs change, and
  built-in presets go stale: [tile-providers.md](tile-providers.md).
- `cache: false` per source, for providers whose terms forbid server-side caching.

Mirrors are several servers serving identical tiles: your own tile servers, or one
provider's hosts.

- `Source.Template` accepts a list of URL templates as `:url`. Each tile goes to a mirror
  chosen from its position, so a tile always has the same URL and HTTP caches along the
  way keep working, and the load spreads evenly across mirrors.
- When a tile fails on its mirror, it is retried on the next one. Unlike fallback, this
  is per tile: the tiles are identical, so the image stays seamless.
- One concurrency limit per source, shared by its mirrors. Per-mirror limits were the
  original plan, but they cannot hold during failover: a down mirror's tiles land on the
  others on top of their own.

Fallback and mirrors exist for availability and capacity. Neither is a way to spread load
across several different free services to stay under each one's limits; the library does
not rotate between different providers.

**Done when** a render whose primary source fails comes back from the fallback with the
fallback's attribution, a failing source is skipped until its cooling-off ends, tiles are
spread across mirrors by position, and a tile that fails on one mirror is served by
another.

## Phase 6 — Fitting the viewport

Done. Markers are fitted but not drawn yet; drawing them, and reserving room for the
attribution strip while fitting, come with Phase 7.

- `StaticMaps.Viewport`: `:bounds`, markers, `:padding`, antimeridian bounds, centre in
  world pixels, zoom limited to the source's range.
- `:center` without `:zoom`, and markers only.

**Done when** fitted views keep every marker and the padding inside the image across the
edge cases, including a single marker and bounds across the antimeridian.

## Phase 7 — Markers and attribution

Done. Markers also take a short `:label`, and the strip can move to the bottom left.

- Default pin and `{lat, lng, opts}` styling; icon extents fed back to fitting.
- Anti-aliased drawing at output scale.
- Attribution strip with wrapping on narrow images, reserved during fitting,
  `attribution: false`.
- The attribution drawn is that of the source the image came from, including a fallback.

**Done when** the README's usage example produces the image it describes.

## Phase 8 — `StaticMaps.Plug`

Done. Custom icons are registered with the Plug by name, since a URL cannot carry them.
The CDN check was made at the level of headers, with `Plug.Test` and a real Bandit
server; a CDN in front of a real deployment is still to be tried.

- `path/3` with canonical parameter order and HMAC signing.
- The request flow in [ARCHITECTURE.md § The Plug](ARCHITECTURE.md#the-plug): 403, 304
  without rendering, 400 on limits, 503 on tile failure, `no-store` on every error,
  `HEAD`.
- `:cache` headers and weak ETag including the library version.
- Render slots and joining identical in-flight renders.

**Done when** a CDN in front of a sample app caches `200`s and passes errors through
uncached.

## Phase 9 — `StaticMaps.Component` for Phoenix LiveView

An optional function component that puts a map in a page as an `<img>`, compiled only
when `phoenix_live_view` is present, like `StaticMaps.Plug`.

- `<StaticMaps.Component.map center={…} zoom={…} size={…} markers={…} alt="…" />`
  renders an `<img>` whose `src` is a signed `StaticMaps.Plug.path/3`, so every map gets
  the Plug's browser and CDN caching.
- A `srcset` with the `scale: 2` version, so high-density screens get sharp maps.
- `width` and `height` on the tag, so the page does not move when the image loads.
- `alt` is required: a map with no text alternative says nothing to a screen reader.
- The image is rendered when the browser asks for it, not in the LiveView process, so a
  slow tile server never blocks a LiveView.

It lives in this library rather than its own package because it and the Plug must agree
on the signed URL format; one package keeps them in step. It moves to a separate package
if it grows beyond one component, or if keeping up with LiveView releases starts forcing
releases here.

Interactive maps stay out of scope; for panning and zooming, use Leaflet or MapLibre
through a hook.

**Done when** a LiveView renders the component, and the browser loads both densities of
the image through the Plug.

## Phase 10 — `Source.PMTiles`

- Raster PMTiles through `ex_pmtiles`, local and S3.
- A clear error for vector archives.
- A guide to producing a raster archive. The basemaps Protomaps publishes are vector
  tiles, so self-hosting means rendering raster tiles from them first; the guide has to
  cover that step, not just the download.

## Phase 11 — Tile cache shared across a cluster

Nodes of one deployment share their tile caches, so a tile fetched on one node serves
every node, and the cluster behaves as one client toward a tile server.

- Nodes find each other through Erlang distribution, however the application already
  clusters (libcluster or otherwise); the library adds no discovery of its own.
- A `StaticMaps.TileCache` implementation that looks in the local cache first, then asks
  peers in a `:pg` group before fetching. Peers answer only from their cache, never by
  fetching on another node's behalf.
- Peers are the application's own nodes, so they are trusted and there is no licence
  question: it is one deployment, one client.
- A slow or missing peer costs a short timeout, then the fetch goes ahead as usual.

**Done when** a tile fetched on one node of a test cluster is served to another without
a second upstream request, and a peer that stops answering does not fail renders.

## Phase 12 — `StaticMaps.TilePlug`: serving tiles you may redistribute

An opt-in endpoint, `/{z}/{x}/{y}.png`, that serves tiles to other applications, making
a deployment a tile server in the ordinary, deliberate sense.

- Serves only from sources whose `info/1` says `redistributable: true`, such as a
  `Source.PMTiles` archive or tiles you render yourself. A source that does not say so is
  refused at startup, since most providers' terms forbid redistribution.
- Never fetches upstream on a request's behalf. A tile not already in the archive or
  cache is a `404`, so the endpoint cannot become an open proxy onto someone else's
  tile server.
- Rate limits on by default, per client and in total, with `Retry-After` on `429`.
- The same HTTP caching as `StaticMaps.Plug`: `Cache-Control`, `ETag` and `304`.
- The attribution of the source, in a response header and in the documentation of the
  endpoint.

**Done when** the endpoint serves an archive's tiles within its limits, refuses a source
not marked redistributable, and answers a missing tile with `404` without any upstream
request.

## Later

Asked for often enough in prior art to be likely, but not needed for the first release.

- Lines and polygons as overlays, feeding fitting like markers do. Every prior
  implementation with overlays supports them.
- WebP output.
- A disk-backed `TileCache`.
- Telemetry events for renders, fetches and cache hits.

## Out of scope

- Fractional zoom. Tiles exist at integer zooms, and scaling them blurs every label.
- Rendering vector tiles. That is a map renderer, a different and much larger project.
- Pre-seeding or bulk-downloading tiles for a region.
- An internet-wide peer-to-peer tile network, where every deployment discovers others and
  shares tiles with strangers. Tiles from OpenStreetMap and commercial providers are not
  signed, so a peer could serve a wrong or malicious tile and nothing could tell;
  commercial terms forbid redistribution; and re-serving OSM tiles to strangers hides
  that load from OSM. For your own tiles, a PMTiles archive is one file with a known
  hash, which existing networks such as BitTorrent or IPFS already distribute and verify.
- Interactive maps.
