I have previously talked about Protomaps and PMTiles files (intro to use Protomaps with Leaflet, styling it in Leaflet, creating PMTiles with custom data and tippecanoe, and converting OSM data to PMTiles with Tilemaker). Getting to visualise the data was always done by means of the Leaflet JavaScript library. Leaflet is, however, rather focused on raster tiling. Protomaps are vector tiles. Showing those more cleanly can be done with another JavaScript library: Maplibre, a fork of the now closed source Mapbox.

At first I had dismissed Maplibre because the Protomaps example only showed setting up with NPM. Leaflet was beginning to bother me in some cases, however, particularly relating to zoom. Thus I decided to give Maplibre another look. I still try to avoid an NPM-related build pipeline though. For now.

For how to get map data, I redirect you to that first intro post. I assume you now have a .pmtiles file following Protomaps’ default format.

Vendor Necessary Libraries

As before, I want to be able to render the map entirely offline or self-hosted. As before, that is a matter of fetching the right JavaScript and CSS files. First, I get Maplibre’s JavaScript and CSS file via the unpkg CDN.

Note: The JS version of Maplibre is called “Maplibre GL JS”, they also have versions for native (mobile, desktop, …) and some other stuff.

wget https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js
wget https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css

Both of which I rename to include the version number (maplibre-gl.3.6.2.js for example). By default, Maplibre does not know what to do with a Protomaps source, so you also need the PMTiles JavaScript library (the js/ folder in there seems to hold the source). This one I rename in the same manner to pmtiles.2.11.0.js.

wget https://unpkg.com/pmtiles@2.11.0/dist/index.js

These three files I throw in a vendor/ folder.

Get a Style File

Specifying data sources and how to render them happens in a style JSON object or file. To get going easily, I take a style file from Protomaps. At https://maps.protomaps.com/ I get the light style by using “get style JSON”. I save it as style.json.

I make two changes to it: the url line in sources gets changed to pmtiles://nearphilly.pmtiles where nearphilly.pmtiles is the PMTiles file I have extracted (see intro post). Replace with whatever you got. I also change the glyphs line near the end of the file to have vendor/fonts/{fontstack}/{range}.pbf as value. See further down in this section for more explanation on that.

As I understand it, you can also generate Protomaps’ default styles locally by following the instructions in the basemaps repository, but just getting it from them seemed simpler.

The style file follows this structure.

{
  "version": 8,
  "sources": {
    "sourcename": {
      "type": "vector",
      "url": "pmtiles://path.pmtiles",
      "attribution": "OSM Contributors and such"
    },
    "canhavemultiplesources": {}
  },
  "layers": [
    {
      "id": "nameforthelayer",
      "type": "line,fill,symbol,raster,...",
      "source": "whichsourcetoworkon",
      "source-layer": "whichlayerinthatsourcetoconsider"
    }
  ],
  "glyphs": "vendor/fonts/{fontstack}/{range}.pbf"
}

The sources describe where to find the data. You can use vector data, such as a .pmtiles file, but you can also use raster data, like a regular tile server.

The layers describe how to render the data. In the example some basic keys are given, but there is also paint, layout, and some others. You can look at the style file we downloaded for more examples or dig through the Maplibre style specification for more info on that. Things are rendered in the order they appear in the layers array. Later elements get rendered over earlier ones. The file we got describes the layers as using data from a source named protomaps, so be sure to use that name when adding your .pmtiles file as a source.

Finally, glyphs specifies where to find fonts. At the time of writing the style file you just got already has a complete URL. I still want to make an entirely offline solution though, so we fetch those individual files too. From the URL given there, we can deduce the fonts can be gotten at https://github.com/protomaps/basemaps-assets in the fonts folder. So just clone that repository (or download the .zip file) and copy the fonts folder out.

In the style example given above, I already updated the glyphs key to match how I store the fonts locally. In the vendor/ folder I have a fonts/ folder and then in there a folder for every font filled with .pbf files which hold the actual characters for different Unicode ranges. Be sure to keep the {fontstack} and {range} in the glyphs entry, Maplibre will fill those in as needed.

If you go through the style file we downloaded, you will see some Noto fonts mentioned: Noto Sans Italic, Medium, and Regular. So that vendor/ folder now looks like this.

vendor
├── fonts
│   ├── Noto Sans Italic
│   │   ├── manymany.pbf
│   │   └── files.pbf
│   ├── Noto Sans Medium
│   │   └── andhere.pbf
│   └── Noto Sans Regular
│       └── andheretoo.pbf
├── maplibre-gl.3.6.2.css
├── maplibre-gl.3.6.2.js
└── pmtiles.2.11.0.js

The HTML Skeleton

Not much action happens here. I save this file as index.html.

<!doctype html>
<html>
  <head>
    <script src="vendor/maplibre-gl.3.6.2.js"></script>
    <link href="vendor/maplibre-gl.3.6.2.css" rel="stylesheet" />
    <script src="vendor/pmtiles.2.11.0.js"></script>
    <style>
      body {
        margin: 0;
      }
      #map {
        height: 95vh;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script src="index.js"></script>
  </body>
</html>

Tying It Together

Only one thing is still missing: the index.js file mentioned in the index.html file. Nothing crazy happens here either, just calling the Maplibre API to tell it how to handle pmtiles and then setting up the map.

// Enable pmtiles in maplibregl
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);

let map = new maplibregl.Map({
  // id of the div in HTML
  container: "map",
  zoom: 12,
  // [lon, lat]
  center: [-75.612, 40.0474],
  // When changing map zoom and position, a hash is added to the URL. Loading
  // the URL+hash bring you directly to that map zoom and position, ignoring
  // the zoom and center specified here.
  hash: true,
  // You can do a 3D pitch of the map in Maplibre. 99.9% of the time I just
  // triggered it accidentally and it bothers me.
  pitchWithRotate: false,
  // Your style file. Can also provide a JSON object.
  style: "style.json",
});

// Adding controls I do after the map has loaded because sometimes it would have
// a race condition and error.
map.on("load", function () {
  // Zoom in and out buttons
  map.addControl(new maplibregl.NavigationControl());
  // The little line in the bottom showing how long a metre (or somesuch) is on
  // the map.
  map.addControl(new maplibregl.ScaleControl({ unit: "metric" }));
  // Button to make the map fullscreen
  map.addControl(new maplibregl.FullscreenControl());
});

Running It

As mentioned in the intro post, you can use whatever capable-enough web server you have at your disposal. For this map related development, I have since switched to using Caddy. Running it for a folder can be done with

caddy file-server --debug --listen 127.0.0.1:2024 --root .

Again, I also provide a file with the necessities to get going, except for the PMTiles file. That you still have to get yourself. All the fonts are included too. See the included file for their license. Here is an overview of what is in the archive file.

Makefile
index.html
index.js
style.json
vendor
├── fonts
│   ├── Noto Sans Italic
│   │   └── many.pbf
│   ├── Noto Sans Medium
│   │   └── many.pbf
│   ├── Noto Sans Regular
│   │   └── many.pbf
│   └── OFL.txt
├── maplibre-gl.3.6.2.css
├── maplibre-gl.3.6.2.js
└── pmtiles.2.11.0.js