maprender

package module
v0.0.0-...-ca8cfbf Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 27 Imported by: 0

README

maprender

Go library that renders Mapbox Vector Tiles into raster images using a Mapbox GL Style JSON specification.

maprender

Features

  • Fetches MVT tiles from any tile server with {z}/{x}/{y} URL templates
  • Auto-resolves tile URL from style's vector source when TileURLTemplate is omitted
  • Overzoom/underzoom: requests the closest available source zoom and scales when the client zoom exceeds the source's maxzoom/minzoom
  • Handles gzip-compressed and raw tile responses
  • Parses Mapbox GL Style JSON and applies paint properties per layer
  • Zoom-dependent expressions: interpolate, step, coalesce, match, get
  • Data expressions: case, concat, to-string, to-number, literal, ...
  • Filter expressions: ==, !=, >, >=, <, <=, in, !in, has, !has, all, any, !
  • Renders background, fill, line, and symbol (text) layer types
  • Text labels rendered from system fonts, with halo and collision detection
  • Sprite icons rendered alongside labels (POIs, shields, one-way arrows, ...)
  • All geometry types: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon
  • HiDPI/Retina rendering via configurable device pixel ratio
  • Context cancellation support
  • Multiple output formats via RenderCanvas (PNG, SVG, PDF, EPS, ...)

Usage

import "github.com/akhenakh/maprender"

style, err := maprender.FetchStyle("https://tiles.openfreemap.org/styles/liberty")
if err != nil {
    log.Fatal(err)
}

img, err := maprender.Render(ctx, maprender.RenderRequest{
    CenterLat:        48.864716,
    CenterLng:        2.349014,
    Zoom:             14,
    Width:            800,
    Height:           600,
    DevicePixelRatio: 1.0,
    Style:            style,
})
if err != nil {
    log.Fatal(err)
}

TileURLTemplate is optional — when omitted, it is automatically resolved from the style's vector source TileJSON. You can also set it explicitly:

TileURLTemplate: "https://tiles.openfreemap.org/planet/20260422_001001_pt/{z}/{x}/{y}.pbf",

When the requested Zoom is higher (or lower) than the source's available range, the renderer automatically fetches the closest available zoom and scales it (overzoom/underzoom). If you set TileURLTemplate manually, the source range is unknown; provide it via SourceMinZoom/SourceMaxZoom to enable the same behavior:

SourceMaxZoom: 14,
Text labels, fonts and icons

symbol layers are rendered as text. The renderer loads the fonts referenced by the style's text-font stacks (e.g. "Noto Sans Bold") from the operating system and falls back to the first available family. A default FontManager is used automatically; you can customise the families or provide your own:

fonts := maprender.NewFontManager("Noto Sans", "DejaVu Sans")

req := maprender.RenderRequest{
    // ...
    Fonts: fonts,
}

Sprite icons (the style's sprite field, e.g. POI markers and road shields) are fetched automatically from <sprite>.json and <sprite>.png and drawn next to their labels. You can override the sprite via RenderRequest.Sprite, loaded with maprender.FetchSprite:

sprite, err := maprender.FetchSprite("https://tiles.openfreemap.org/sprites/ofm_f384/ofm")

req := maprender.RenderRequest{
    // ...
    Sprite: sprite,
}
Tile cache

Downloaded tiles are cached on disk under ~/.cache/maprender by default and reused across renders and processes. The directory and expiry (default 2 weeks) are configurable; set TileCacheTTL to a negative value to disable expiry:

req := maprender.RenderRequest{
    // ...
    TileCacheDir: "/tmp/maprender-cache",
    TileCacheTTL: 7 * 24 * time.Hour,
}

Tiles are written to a temporary file and atomically moved into place, so multiple processes can safely share the same cache directory.

Incremental panning

When panning, RenderIncremental shifts the label-free pixels of the previous frame and renders only the newly exposed strips instead of redrawing the whole viewport:

prev, err := maprender.RenderIncremental(ctx, firstReq, nil) // nil prev = full redraw

// on pan: pass back the previous frame
next, err := maprender.RenderIncremental(ctx, newReq, prev)

draw(next.Image)        // complete frame, ready to display
// next.Base holds the same frame without labels; it is what the next call reuses

RenderIncremental returns a PanFrame with two images: Image (labels included — display this) and Base (geometry only — hand it back as prev). Keeping labels out of the reused pixels is what prevents text/icons from being re-stamped onto themselves and growing bolder with every pan. A nil or mismatching prev (zoom change, resize, ...) falls back to a full redraw through the same pipeline.

Trade-off: geometry (background, fill and line layers) is composited incrementally from reused pixels plus freshly rendered strips, but text labels are re-rendered for the whole viewport on every pan. Label placement uses viewport-wide collision detection, so rendering labels per strip would produce seam artifacts — clipped, duplicated or overlapping labels that also pop in and out across successive pans. Re-running the symbol pass over the composited frame (blending only the drawn label boxes) keeps every frame identical to a full render.

Run the demo with go run ./cmd/example -pan.

Measured on a dense city view at zoom 17 (512x512), panning runs ~38x faster than a full render (BenchmarkRenderIncremental); gains shrink on lighter views where absolute costs are sub-millisecond anyway.

Overlays

Draw arbitrary geometries (WGS84 lon/lat) on top of the map from GeoJSON, WKT, WKB, or a geom.Geometry directly. The stroke defaults to red and the fill to transparent; both can be set explicitly or derived from GeoJSON feature properties (keys stroke/stroke-color/strokeColor and fill/fill-color/fillColor):

// GeoJSON (a Feature or FeatureCollection; properties drive colors)
overlays, err := maprender.OverlayFromGeoJSON([]byte(`{
    "type": "Feature",
    "properties": {"fill": "#ff000080", "stroke": "#ff0000"},
    "geometry": {"type": "Polygon", "coordinates": [[[2.34,48.85],[2.36,48.85],[2.36,48.87],[2.34,48.87],[2.34,48.85]]]}
}`))

// or WKT / WKB / a geometry
overlay, err := maprender.OverlayFromWKT("LINESTRING(2.33 48.86, 2.37 48.86)")

req := maprender.RenderRequest{
    // ...
    Overlays:   overlays,
    FitOverlays: true, // compute center/zoom from the overlays' combined bounds
}
Output formats

Render returns a raster *image.RGBA (ready to encode as PNG). For other formats, use RenderCanvas to obtain a vector *canvas.Canvas, then render it with any of the canvas writers (github.com/tdewolff/canvas/renderers/...): svg, pdf, ps, eps, png, jpeg, gif, tiff, bmp, webp, ...

import (
    "os"
    "github.com/tdewolff/canvas/renderers/pdf"
    "github.com/tdewolff/canvas/renderers/svg"
)

c, err := maprender.RenderCanvas(ctx, req)

// SVG
f, _ := os.Create("/tmp/map.svg")
w := svg.New(f, c.W, c.H, nil)
c.RenderTo(w)
w.Close()
f.Close()

// PDF
f, _ = os.Create("/tmp/map.pdf")
p := pdf.New(f, c.W, c.H, nil)
c.RenderTo(p)
p.Close()
f.Close()

The cmd/example program demonstrates this: pass -svg to write output.svg instead of the default output.png:

go run ./cmd/example        # writes output.png
go run ./cmd/example -svg   # writes output.svg

Dependencies

License

MIT

Documentation

Index

Constants

View Source
const TileSize = 512

Variables

This section is empty.

Functions

func DefaultCacheDir

func DefaultCacheDir() (string, error)

DefaultCacheDir returns the default tile cache directory (~/.cache/maprender) based on the current user's home directory.

func FetchTileURLTemplate

func FetchTileURLTemplate(sourceURL string) (string, error)

func FitOverlaysBounds

func FitOverlaysBounds(overlays []Overlay, width, height float64) (lat, lng float64, zoom int, err error)

FitOverlaysBounds computes the center (lat, lng) and integer zoom so that the combined bounds of the overlays fit within a viewport of width x height logical pixels. MultiPolygons and collections are handled via their combined envelope.

func Render

func Render(ctx context.Context, req RenderRequest) (*image.RGBA, error)

Render renders the map to a raster image (PNG-ready *image.RGBA).

func RenderCanvas

func RenderCanvas(ctx context.Context, req RenderRequest) (*canvas.Canvas, error)

RenderCanvas renders the map to a vector canvas that can be rasterized or exported to other formats (SVG, PDF, EPS, ...) via canvas.Write / WriteFile.

Types

type FontManager

type FontManager struct {
	// contains filtered or unexported fields
}

FontManager resolves Mapbox GL Style font stacks ("Noto Sans Regular", "Noto Sans Bold", ...) to canvas font faces backed by system fonts.

func DefaultFonts

func DefaultFonts() *FontManager

DefaultFonts returns a lazily-initialized, shared FontManager that loads the most common sans-serif fonts from the system.

func NewFontManager

func NewFontManager(families ...string) *FontManager

NewFontManager loads the given (or default) font families from the system. Any family that cannot be found is skipped.

func (*FontManager) Face

func (fm *FontManager) Face(fontStacks []string, sizePx float64, col color.Color, haloColor color.Color, haloWidthPx float64) *canvas.FontFace

Face returns a canvas font face for the given font stacks (in priority order). sizePx is the font size in logical pixels. When haloWidthPx is greater than zero, the face is decorated with a text halo (stroke) of the given color. It returns nil if no usable font family was loaded.

type LayoutProps

type LayoutProps struct {
	TextField     any      `json:"text-field"`
	TextFont      []string `json:"text-font"`
	TextSize      any      `json:"text-size"`
	TextAnchor    any      `json:"text-anchor"`
	TextTransform any      `json:"text-transform"`
	IconImage     any      `json:"icon-image"`
	IconSize      any      `json:"icon-size"`
	IconAnchor    any      `json:"icon-anchor"`
}

type MapStyle

type MapStyle struct {
	Layers    []StyleLayer `json:"layers"`
	SourceURL string
	SpriteURL string
	GlyphsURL string
}

func FetchStyle

func FetchStyle(styleURL string) (*MapStyle, error)

func (*MapStyle) ResolveTileJSON

func (s *MapStyle) ResolveTileJSON() (*TileJSON, error)

func (*MapStyle) ResolveTileURL

func (s *MapStyle) ResolveTileURL() (string, error)

type Overlay

type Overlay struct {
	// Geometry is the geometry to draw. Coordinates are interpreted as
	// (longitude, latitude).
	Geometry geom.Geometry

	// Properties are optional free-form properties (e.g. from a GeoJSON
	// feature). They are used to derive stroke/fill colors when the explicit
	// colors below are nil.
	Properties map[string]any

	// StrokeColor is the outline color. When nil, it is derived from
	// Properties (keys "stroke", "stroke-color", "strokeColor") and finally
	// defaults to red.
	StrokeColor color.Color

	// FillColor is the polygon fill color. When nil, it is derived from
	// Properties (keys "fill", "fill-color", "fillColor") and finally defaults
	// to transparent (no fill).
	FillColor color.Color

	// StrokeWidth is the outline width in pixels. Zero means the default (2).
	StrokeWidth float64
}

Overlay is a geometry (in WGS84 / lon-lat coordinates) drawn on top of the rendered map.

func OverlayFromGeoJSON

func OverlayFromGeoJSON(data []byte) ([]Overlay, error)

OverlayFromGeoJSON parses GeoJSON (a Geometry, Feature, or FeatureCollection) into overlays. Feature properties are retained for color extraction.

func OverlayFromWKB

func OverlayFromWKB(wkb []byte) (Overlay, error)

OverlayFromWKB parses a WKB byte slice into an Overlay.

func OverlayFromWKT

func OverlayFromWKT(wkt string) (Overlay, error)

OverlayFromWKT parses a WKT string into an Overlay.

type PaintProps

type PaintProps struct {
	BackgroundColor any `json:"background-color"`
	FillColor       any `json:"fill-color"`
	FillOpacity     any `json:"fill-opacity"`
	LineColor       any `json:"line-color"`
	LineWidth       any `json:"line-width"`
	LineOpacity     any `json:"line-opacity"`
	LineDashArray   any `json:"line-dasharray"`
	TextColor       any `json:"text-color"`
	TextHaloColor   any `json:"text-halo-color"`
	TextHaloWidth   any `json:"text-halo-width"`
	TextOpacity     any `json:"text-opacity"`
}

type PanFrame

type PanFrame struct {
	Image *image.RGBA
	Base  *image.RGBA

	CenterLat float64
	CenterLng float64
	Zoom      int
}

PanFrame is the result of an incremental pan. Image is the complete frame (text labels, icons and marker included) ready for display; Base is the same frame without labels — pass it back as Prev on the next RenderIncremental call so labels are never stacked onto already drawn ones.

func RenderIncremental

func RenderIncremental(ctx context.Context, req RenderRequest, prev *PanFrame) (*PanFrame, error)

RenderIncremental renders req while reusing pixels from prev (the PanFrame returned by an earlier call at the same zoom, size and style): the previous label-free base is shifted according to the pan delta, only the newly exposed strips are re-rendered, and text labels/icons/marker are drawn fresh onto a copy of the result. This makes panning dramatically cheaper than a full render while keeping every frame identical to one.

Everything else in req must be unchanged since prev was produced; when that does not hold (zoom change, resize, new overlays, ...) use Render instead. A nil prev (or a mismatching one) performs a full redraw through the same pipeline, so callers do not need special cases.

type RenderRequest

type RenderRequest struct {
	CenterLat        float64
	CenterLng        float64
	Zoom             int
	Width            int // physical pixels
	Height           int // physical pixels
	DevicePixelRatio float64
	Style            *MapStyle
	TileURLTemplate  string
	SourceMinZoom    int // optional; when TileURLTemplate is set, use this source min zoom for underzoom
	SourceMaxZoom    int // optional; when TileURLTemplate is set, use this source max zoom for overzoom
	Fonts            *FontManager
	Sprite           *Sprite
	Overlays         []Overlay
	FitOverlays      bool // when true, center and zoom are computed to fit Overlays
	MarkerLat        *float64
	MarkerLng        *float64

	TileCacheDir string        // directory for downloaded tiles; empty defaults to ~/.cache/maprender
	TileCacheTTL time.Duration // tile cache expiry; 0 defaults to 2 weeks, negative disables expiry

	Logger *slog.Logger
	// contains filtered or unexported fields
}

type Sprite

type Sprite struct {
	// contains filtered or unexported fields
}

Sprite is a Mapbox sprite: a set of named icons cropped from a sprite sheet.

func FetchSprite

func FetchSprite(spriteURL string) (*Sprite, error)

FetchSprite loads a Mapbox sprite from the given base URL (the style's `sprite` field). It fetches `<url>.json` and `<url>.png`.

func (*Sprite) Icon

func (s *Sprite) Icon(name string) (image.Image, float64, bool)

Icon returns the cropped image and pixel ratio for the named icon.

type StyleLayer

type StyleLayer struct {
	ID          string      `json:"id"`
	Type        string      `json:"type"`
	SourceLayer string      `json:"source-layer"`
	Paint       PaintProps  `json:"paint"`
	Layout      LayoutProps `json:"layout"`
	Filter      []any       `json:"filter"`
	MinZoom     *float64    `json:"minzoom"`
	MaxZoom     *float64    `json:"maxzoom"`
}

func GetLayerByID

func GetLayerByID(style *MapStyle, id string) *StyleLayer

type TileCache

type TileCache struct {
	// contains filtered or unexported fields
}

TileCache stores downloaded tiles on disk so they can be reused across renders and processes. It is safe for concurrent use by multiple processes sharing the same directory: tiles are written to a temporary file and then atomically moved into place, so a reader never observes a partial tile.

func NewTileCache

func NewTileCache(dir string, ttl time.Duration) (*TileCache, error)

NewTileCache returns a TileCache rooted at dir. When dir is empty the default cache directory (~/.cache/maprender) is used. Entries older than ttl are treated as missing and re-downloaded; a non-positive ttl disables expiry.

func (*TileCache) Fetch

func (c *TileCache) Fetch(url string) ([]byte, error)

Fetch returns the tile data for url, using the cache when possible and downloading (and caching) it otherwise.

func (*TileCache) Get

func (c *TileCache) Get(url string) ([]byte, bool)

Get returns the cached tile data for url, or ok=false if it is not cached or has expired.

func (*TileCache) Put

func (c *TileCache) Put(url string, data []byte) error

Put stores tile data for url atomically. The data is written to a temporary file in the cache directory and then renamed into place, which is atomic on the same filesystem and therefore safe when several processes download into the same directory concurrently.

type TileJSON

type TileJSON struct {
	Tiles    []string `json:"tiles"`
	MinZoom  int      `json:"minzoom"`
	MaxZoom  int      `json:"maxzoom"`
	TileSize int      `json:"tileSize"`
}

func FetchTileJSON

func FetchTileJSON(sourceURL string) (*TileJSON, error)

Directories

Path Synopsis
cmd
example command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL