Packages

Render map images on the server: tiles, markers and attribution, fitted to bounds and served from signed, cacheable URLs.

Current section

Files

Jump to
staticmaps README.md
Raw

README.md

# StaticMaps

Render a map to an image on the server, with no map library in the browser.

StaticMaps takes a centre, a zoom and a size, works out which tiles cover that view,
fetches them, stitches them into one picture, draws your markers on top and hands you the
bytes. Nothing ships to the client but an `<img>`.

![Your options become a choice of tiles, which are fetched through the tile cache, stitched and cropped, then markers and attribution are drawn and the image is encoded.](assets/pipeline.svg)

It is for maps that do not move: a property listing, an office address, a photo's
location, an OG image, a PDF. If your users need to pan and zoom, you want Leaflet or
MapLibre in the browser; that is a different problem, and this library does not try to
solve it.

A map that never moves needs none of a client-side map's machinery. One render produces
one image that is cached for as long as you like, and a page view costs no tile requests,
which keeps you within the
[tile usage policy](https://operations.osmfoundation.org/policies/tiles/) of
OpenStreetMap's volunteer-run servers.

## Installation

```elixir
def deps do
  [
    {:staticmaps, "~> 0.1"},
    # only if you serve maps over HTTP with StaticMaps.Plug
    {:plug, "~> 1.16"}
  ]
end
```

StaticMaps sends an identifying `User-Agent` with every tile request, as OSM's policy
requires. Tell it how the tile provider can reach you:

```elixir
# config/config.exs
config :staticmaps, contact: "https://example.com/contact"
# User-Agent: StaticMaps/<version> (+https://example.com/contact)
```

The attribution strip is text, drawn with the fonts installed through fontconfig. Slim
Docker images often have none, and there a render fails with an error naming the missing
font. Install one, for example `apt-get install fonts-dejavu-core` or
`apk add font-dejavu`.

## Usage

```elixir
{:ok, png} =
  StaticMaps.render(
    center: {14.5547, 121.0244},
    zoom: 15,
    size: {800, 400},
    markers: [{14.5547, 121.0244}, {14.5176, 121.0509, color: "#1565c0", label: "B"}]
  )

File.write!("makati.png", png)
```

Coordinates are always `{latitude, longitude}`. Leave out `:center` and `:zoom` and the
view is fitted to `:bounds` or the markers instead:

```elixir
StaticMaps.render(bounds: {{14.50, 120.98}, {14.60, 121.06}}, size: {600, 400})
```

`render/1` returns `{:ok, binary}` or `{:error, exception}`, and never raises for bad
input. By default it fails rather than return a map with missing tiles, and it draws the
tile source's attribution into the image, because the tiles' licence requires it.

Serving maps to browsers takes a mounted Plug and one call per image:

```elixir
# router.ex
forward "/maps", StaticMaps.Plug, secret: {MyApp.Config, :map_secret, []}

# in a template
<img src={StaticMaps.Plug.path("/maps", [center: {14.5547, 121.0244}, zoom: 15], secret: secret)}>
```

The URL is signed, so nobody else can render maps through your server, and its responses
carry cache headers, so browsers and a CDN keep the image and you store nothing.

## Guides

- [Rendering maps](guides/rendering.md) — every option, fitting the view, markers,
  attribution, and what happens when a tile fails.
- [Tile sources and caching](guides/tile-sources.md) — the built-in sources, writing
  your own, backup sources and mirrors, and the tile cache.
- [Tile providers](guides/tile-providers.md) — configurations for commercial and
  community tile servers, and what each one's terms allow.
- [Serving over HTTP](guides/serving-over-http.md) — `StaticMaps.Plug`: signed URLs,
  cache headers, limits and responses.
- [Architecture](guides/ARCHITECTURE.md) — how it works inside, for contributors.
- [Roadmap](guides/ROADMAP.md) — what is planned next.

## Prior art

The problem is well solved outside Elixir, and this library learns from
[StephanGeorg/staticmaps](https://github.com/StephanGeorg/staticmaps) (Node),
[komoot/staticmap](https://github.com/komoot/staticmap) (Python),
[flopp/py-staticmaps](https://github.com/flopp/py-staticmaps) (Python),
[Luzifer/staticmap](https://github.com/Luzifer/staticmap) (Go) and
[mapbox/quilt](https://github.com/mapbox/quilt) (Node). Where it departs from them,
[Architecture](guides/ARCHITECTURE.md#lessons-from-prior-art) says why.

## License

MIT; see [LICENSE](LICENSE).