In a previous article I wrote about adding maps to my blog, which is a static site. I use these maps to write about walks I’ve done. These maps can be dragged, rotated and zoomed, but there is no backend tileserver supporting it, it all comes from a single static file. This is thanks to the PMTiles format and support by the frontend renderer maplibre-gl.
In the original article I took a pre-made pmtile file of the world from the Protomaps project and extracted a smaller part using the pmtiles CLI. Later I ended up making my own pmtiles directly from OpenStreetMap data, which I’ll describe below. This provides a lot of geographic data like coastlines, forests, roads etc., but what I really wanted was hillshading, which I’ve now figured out.
Making custom PMTiles
First of all I’ll talk about making custom PMTiles. OpenStreetMap exports can be fetched from Geofabrik in PBF format. Rather than download the whole world you can download just the region you need. To convert this into a pmtiles file I use Planetiler like so:
1java -Xmx1g -jar ~/java/planetiler.jar --download \
2 --osm-path=scotland-latest.osm.pbf \
3 --output scotland-latest.osm.pmtiles
The --download
option does some magic to download and include extra data sources like
coastlines etc. At this point a smaller region can be extracted with the pmtiles
tool
as in the first article:
1pmtiles extract scotland-latest.osm.pmtiles whw.pmtiles \
2 --bbox=-5.11,56.82,-3.15,55.94
For this data we need to use a different style, I went for osm-bright-gl-style. To load this into maplibre-gl looks like this:
1const response = await fetch("/osm-bright-gl-style/style.json");
2const style = await response.json();
3const map = new maplibregl.Map({
4 container: e,
5 style: {
6 version: 8,
7 sprite: `${window.location.origin}/osm-bright-gl-style/sprite`,
8 glyphs: `${window.location.origin}/osm-bright-gl-style/{fontstack}/{range}.pbf`,
9 sources: {
10 openmaptiles: {
11 type: "vector",
12 url: `pmtiles://${tilesUrl}`,
13 attribution: attr,
14 },
15 },
16 layers: style.layers,
17 },
18});
Feel free to check out the source of my blog to see more: https://github.com/georgek/blog/blob/master/assets/js/map.js
Making Hillshade PMTiles
Initially I searched for hillshade data in PMTiles format, but was only able to find a few low quality sources, so I gave up. But more recently I happened upon Sonny’s LiDAR models. Initially looking at this it didn’t make much sense, but it promised data that was free to use, so my interest was piqued.
Before I continue, I must give a disclaimer: I don’t fully understand this and this will not teach you what all this means. This is just what worked for me.
Assuming your region of interest is covered by Sonny’s data then what you want is the ‘DTM 1"’ data. It’s hosted on Google Drive which is a little clunky, and it comes in separate tiles, of which you might need more than one. Each region gives an overview which should enable you to find which individual file(s) you need.
After downloading and unzipping, you should end up with one or more .hgt
files. For
example, for Gran Canaria I have N27W016.hgt
and N28W016.hgt
. The first thing to do
is convert them to tiff using GDAL:
1for f in *.hgt; do
2 gdal_translate $f $(basename $f .hgt).tiff
3done
Next I use Rasterio to merge them into one file, and clip it to a bounding box:
1rio merge *.tiff \
2 --bounds -16.273499,27.508271,-14.889221,28.386568 \
3 -o gran-canaria.tiff
The next part took a long while to figure out. We want to convert it to mbtiles format, but the tiff currently isn’t in quite the right format expected by maplibre-gl, but this can be converted into the right mbtiles format using rio-rgbify:
1rio rgbify --format webp --min-z 0 --max-z 14 \
2 gran-canaria.tiff gran-canaria.mbtiles
Note the max zoom setting --max-z
. Increasing this will dramatically increase both the
processing time and the file size of the output. 16 is great, but even for a small
island like this the file size will be large (176 MiB). I’ve found 14–15 a reasonable
compromise.
This can finally be converted into pmtiles using the pmtiles CLI:
1pmtiles convert gran-canaria.mbtiles gran-canaria.pmtiles
At this point it’s easy to add the source and layer to maplibre-gl:
1map.addSource("relief", {
2 type: "raster-dem",
3 url: `pmtiles://${e.dataset.reliefTilesUrl}`,
4 encoding: "mapbox",
5 tileSize: 512,
6 attribution: '<a href="https://sonny.4lima.de/">Sonny</a>',
7});
8map.addLayer(
9 {
10 id: "relief",
11 source: "relief",
12 type: "hillshade",
13 paint: {
14 "hillshade-accent-color": "#004400",
15 "hillshade-highlight-color": "#ddffdd",
16 "hillshade-exaggeration": 1,
17 },
18 },
19 "water",
20);
Conclusion
This almost certainly isn’t the best way to do this and I don’t really know what I’m doing. But it worked and I’m pleased. GIS data seems like a massive rabbit hole and there doesn’t seem to be a great amount of easily-accessible information out there about it. But it’s getting better. A few years ago I wouldn’t have imagined I could do this without using a 3rd party service. But here it is on a static site!