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>.
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
- Rendering maps — every option, fitting the view, markers, attribution, and what happens when a tile fails.
- Tile sources and caching — the built-in sources, writing your own, backup sources and mirrors, and the tile cache.
- Tile providers — configurations for commercial and community tile servers, and what each one's terms allow.
- Serving over HTTP —
StaticMaps.Plug: signed URLs, cache headers, limits and responses. - Architecture — how it works inside, for contributors.
- Roadmap — what is planned next.
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.