Go package providing opinionated HTTP middleware for web-based map tiles.
/v2 of this package is not just a complete refactoring of the code but also a complete rethink about what its function is and how it works. There is basically nothing which is backwards compatible with previous versions.
The original motivation for the go-http-maps package was to define a handful of top-level methods and net/http middleware handlers to manage the drudegry of setting up maps, with a variety of map providers (Leaflet, Protomaps, Nextzen/Tangramjs), It "worked" but, in the end, it was also not "easy".
Version 2 removes most of the functionality of the go-http-maps package and instead focuses on a handful of methods for providing a dynamic map "config" file (exposed as an HTTP endpoint) which can be retrieved and processed from the browser.
This package no longer provides static asset handlers for Leaflet or Protomaps. It is left up to you to bundle (and serve) them from your own code. You could, if you wanted, define a custom http.Handler instance to load those files from the github.com/aaronland/go-http-maps/v2/static/www.FS embedded filesystem but that's still something you'll need to do on your own.
godoc is still incomplete at this time.
The easiest usage example is the cmd/example tool:
package main
import (
"flag"
"fmt"
"log"
"log/slog"
"net/http"
"github.com/aaronland/go-http-maps/v2"
"github.com/aaronland/go-http-maps/v2/static/www"
"github.com/sfomuseum/go-flags/multi"
)
func main() {
var verbose bool
var host string
var port int
var initial_view string
var map_provider string
var map_tile_uri string
var protomaps_theme string
var protomaps_max_data_zoom int
var leaflet_style string
var leaflet_point_style string
flag.StringVar(&host, "host", "localhost", "The host to listen for requests on")
flag.IntVar(&port, "port", 8080, "The port number to listen for requests on")
flag.BoolVar(&verbose, "verbose", false, "Enable verbose (debug) logging.")
flag.StringVar(&map_provider, "map-provider", "leaflet", "Valid options are: leaflet, protomaps")
flag.StringVar(&map_tile_uri, "map-tile-uri", maps.LEAFLET_OSM_TILE_URL, "A valid Leaflet tile layer URI. See documentation for special-case (interpolated tile) URIs.")
flag.StringVar(&protomaps_theme, "protomaps-theme", "white", "A valid Protomaps theme label.")
flag.IntVar(&protomaps_max_data_zoom, "protomaps-max-data-zoom", 0, "The maximum zoom (tile) level for data in a PMTiles database")
flag.StringVar(&leaflet_style, "leaflet-style", "", "A custom Leaflet style definition for geometries. This may either be a JSON-encoded string or a path on disk.")
flag.StringVar(&leaflet_point_style, "leaflet-point-style", "", "A custom Leaflet style definition for points. This may either be a JSON-encoded string or a path on disk.")
flag.Var(&leaflet_label_properties, "leaflet-label-property", "Zero or more (GeoJSON Feature) properties to use to construct a label for a feature's popup menu when it is clicked on.")
flag.StringVar(&initial_view, "initial-view", "", "A comma-separated string indicating the map's initial view. Valid options are: 'LON,LAT', 'LON,LAT,ZOOM' or 'MINX,MINY,MAXX,MAXY'.")
flag.Parse()
if verbose {
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Debug("Verbose logging enabled")
}
mux := http.NewServeMux()
opts := &maps.AssignMapConfigHandlerOptions{
MapProvider: map_provider,
MapTileURI: map_tile_uri,
InitialView: initial_view,
LeafletStyle: leaflet_style,
LeafletPointStyle: leaflet_point_style,
LeafletLabelProperties: leaflet_label_properties,
ProtomapsTheme: protomaps_theme,
ProtomapsMaxDataZoom: protomaps_max_data_zoom,
}
maps.AssignMapConfigHandler(opts, mux, "/map.json")
www_fs := http.FS(www.FS)
www_handler := http.FileServer(www_fs)
mux.Handle("/", www_handler)
addr := fmt.Sprintf("%s:%d", host, port)
slog.Info("Listening for requests", "address", addr)
http.ListenAndServe(addr, mux)
}
Error handling omitted for the sake of brevity.
The "nut" of it being this part:
opts := &maps.AssignMapConfigHandlerOptions{
MapProvider: map_provider,
MapTileURI: map_tile_uri,
InitialView: initial_view,
LeafletStyle: leaflet_style,
LeafletPointStyle: leaflet_point_style,
ProtomapsTheme: protomaps_theme,
ProtomapsMaxDataZoom: protomaps_max_data_zoom,
}
maps.AssignMapConfigHandler(opts, mux, "/map.json")
Which populates the maps.AssignMapConfigHandlerOptions with map-specific command line flags and passes those options (along with your http.ServeMux instance) to the AssignMapConfigHandler ethod. This method will validate all the options and create a new map config http.Handler assigning it to the http.ServeMux instance.
If your map provider start with "protomaps" and the corresponding MapTileURI starts with file:// it will be assumed that you are trying to serve a local Protomaps database and a matching http.Handler will assign to the http.ServeMux instance.
And then in your JavaScript code you would write something like this:
window.addEventListener("load", function load(event){
// Null Island
var map = L.map('map').setView([0.0, 0.0], 1);
fetch("/map.json")
.then((rsp) => rsp.json())
.then((cfg) => {
console.debug("Got map config", cfg);
switch (cfg.provider) {
case "leaflet":
var tile_url = cfg.tile_url;
var tile_layer = L.tileLayer(tile_url, {
maxZoom: 19,
});
tile_layer.addTo(map);
break;
case "protomaps":
var tile_url = cfg.tile_url;
var pm_args = {
url: tile_url,
flavor: cfg.protomaps.theme,
};
// Necessary for "over-zooming"
if ("max_data_zoom" in cfg.protomaps){
pm_args.maxDataZoom = cfg.protomaps.max_data_zoom;
}
var tile_layer = protomapsL.leafletLayer(pm_args)
tile_layer.addTo(map);
break;
default:
console.error("Uknown or unsupported map provider");
return;
}
if (cfg.initial_view) {
var zm = map.getZoom();
if (cfg.initial_zoom){
zm = cfg.initial_zoom;
}
map.setView([cfg.initial_view[1], cfg.initial_view[0]], zm);
} else if (cfg.initial_bounds){
var bounds = [
[ cfg.initial_bounds[1], cfg.initial_bounds[0] ],
[ cfg.initial_bounds[3], cfg.initial_bounds[2] ],
];
map.fitBounds(bounds);
}
console.debug("Finished map setup");
}).catch((err) => {
console.error("Failed to derive map config", err);
return;
});
});
$> make example
go run cmd/example/main.go \
-initial-view '-122.384292,37.621131,13'
2025/03/06 10:00:09 INFO Listening for requests address=localhost:8080
$> make example-pm
go run cmd/example/main.go \
-initial-view '-122.384292,37.621131,13' \
-map-provider protomaps \
-map-tile-uri 'file:///usr/local/go-http-maps/fixtures/sfo.pmtiles'
2025/03/06 09:59:05 INFO Listening for requests address=localhost:8080
Note: The protomaps/protomaps-leaflet JavaScript library has been put in maitainance mode. All new Protomaps features are only being added to the PMTiles for MapLibre GL libraries.
$> make example-pm-paint
go run cmd/example/main.go \
-initial-view '-122.384292,37.621131,13' \
-map-provider protomaps-paint \
-protomaps-max-data-zoom 14 \
-map-tile-uri 'file:///Users/asc/aaronland/go-http-maps/fixtures/sfo.pmtiles'
2026/03/18 10:47:30 INFO Listening for requests address=localhost:8080
Custom paint (or label) rules are not part of the map "config" file and need to be added manually in your JavaScript code. See static/www/javascript/index.init.js for details.
Note: The protomaps/protomaps-leaflet JavaScript library has been put in maitainance mode. All new Protomaps features are only being added to the PMTiles for MapLibre GL libraries.
$> make example-pm-raster
go run cmd/example/main.go \
-initial-view '-122.384292,37.621131,13' \
-map-provider protomaps-raster \
-protomaps-max-data-zoom 14 \
-map-tile-uri https://static.sfomuseum.org/aerial/1936.pmtiles
2026/03/18 10:47:59 INFO Listening for requests address=localhost:8080
Note: The protomaps/protomaps-leaflet JavaScript library has been put in maitainance mode. All new Protomaps features are only being added to the PMTiles for MapLibre GL libraries.
$> make example-pm-ml
go run cmd/example/main.go \
-initial-view '-122.384292,37.621131,13' \
-map-provider protomaps-ml \
-protomaps-max-data-zoom 14 \
-map-tile-uri 'file:///Users/asc/aaronland/go-http-maps/fixtures/sfo.pmtiles'
2026/03/18 10:48:13 INFO Listening for requests address=localhost:8080
MapLibre styling rules are not part of the map "config" file and need to be added manually in your JavaScript code. See static/www/javascript/index.init.js for details.
This is the easiest way to generate Protomaps slices. Simply run the extract command from the go-pmtiles tool with a --bbox flag. For example:
$> cd /usr/local/src/go-pmtiles
$> go run main.go extract \
--bbox=-122.408061,37.601617,-122.354907,37.640167 \
https://build.protomaps.com/{YYYYMMDD}.pmtiles \
sfo-src.pmtiles
fetching 7 dirs, 7 chunks, 7 requests
Region tiles 51, result tile entries 51
fetching 51 tiles, 21 chunks, 17 requests
fetching chunks 100% (1.3/1.3 MB, 1.1 MB/s)
Completed in 7.269754041s with 4 download threads (7.015367816594351 tiles/s).
Extract required 27 total requests.
Extract transferred 1.3 MB (overfetch 0.05) for an archive size of 1.3 MB
$> go run main.go show sfo-src.pmtiles
pmtiles spec version: 3
tile type: mvt
bounds: (long: -122.408061, lat: 37.601617) (long: -122.354907, lat: 37.640167)
min zoom: 0
max zoom: 15
center: (long: -122.381484, lat: 37.620892)
center zoom: 0
addressed tiles count: 51
tile entries count: 51
tile contents count: 51
clustered: true
internal compression: gzip
tile compression: gzip
vector_layers <object...>
pgf:devanagari:name NotoSansDevanagari-Regular
pgf:devanagari:version 1
planetiler:osm:osmosisreplicationseq 115029
planetiler:osm:osmosisreplicationtime 2025-10-27T04:00:00Z
planetiler:osm:osmosisreplicationurl https://planet.osm.org/replication/hour/
type baselayer
version 4.12.0
attribution <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap</a>
description Basemap layers derived from OpenStreetMap and Natural Earth
name Protomaps Basemap
planetiler:buildtime 2025-05-06T00:22:16.609Z
planetiler:githash 2c91725f6d048fd60b02d3e7c29bb88838451048
planetiler:version 0.9.0




