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.

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 of OpenStreetMap's volunteer-run servers.

Installation

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:

# 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

{: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:

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:

# 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

Prior art

The problem is well solved outside Elixir, and this library learns from StephanGeorg/staticmaps (Node), komoot/staticmap (Python), flopp/py-staticmaps (Python), Luzifer/staticmap (Go) and mapbox/quilt (Node). Where it departs from them, Architecture says why.

License

MIT; see LICENSE.