personal_site/_posts/2023-10-30-maps-3.md
2025-02-03 13:09:36 +00:00

99 lines
4.3 KiB
Markdown

---
title: "Interactive web maps from a static file"
layout: post
excerpt: |
PMTiles is a cool new technology to serve interactive vector maps from a static file.
image:
thumbnail: /assets/blog/pmtiles/thumbnail.png
alt: A small map.
head: |
<link rel="prefetch" href="/assets/blog/pmtiles/light.json" as="fetch">
<link rel="stylesheet" href="/assets/css/maplibre-gl.css">
<script src="/assets/js/maplibre-gl.js"></script>
<script src="/assets/js/pmtiles.js"></script>
---
PMTiles is a new project that lets you serve vector map data from static files through the magic of HTTP range requests.
The vector data is entirely served from a static file on this server. Most interactive web maps work by constantly requesting little map images from an external server at different zoom levels. This approach uses much less data and doesn't require an external server to host all the map data.
Getting this to work was a little tricky, I mostly followed the steps from Simon Willison's post but I didn't want to use npm. As I write this I realise that this site is generated with jekyll which uses npm anyway but somehow I would like the individual posts to Just Work™ without worrying about updating libraries and npm.
So I grabbed `maplibre-gl.css`, `maplibre-gl.js` and `pmtiles.js`, plonked them into this site and started hacking around. I ended up mashing up the code from [Simon Willison's post][blog] and [the official examples][official_examples] to get something that worked.
I figured out from [this github issue][github_issue] how to grab a module version of `protomaps-themes-base` without npm. However I don't really like the styles in produces. Instead I played around a bit with the generated json styles to make something that looks a bit more like the Stamen Toner theme.
Looking at the source code for `protomaps-themes-base` I realise I could probably make custom themes much more easily by just swapping out the theme variables in the package.
Todo:
- Figure out how to use [maputnik][maputnik] to generate styles for PMTiles.
[github_issue]: https://github.com/preactjs/preact/issues/1961#issuecomment-537255092
[blog]: https://til.simonwillison.net/gis/pmtiles
[official_examples]: https://github.com/protomaps/PMTiles/blob/main/js/examples/maplibre.html
[maputnik]: https://maputnik.github.io/editor/
<figure id="map" style="width:100%; height:500px;"></figure>
<script type="module">
// import layers from "https://www.unpkg.com/protomaps-themes-base@latest?module";
async function main() {
// add the PMTiles plugin to the maplibregl global.
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles",protocol.tile);
let PMTILES_URL = "/assets/blog/pmtiles/hackney.pmtiles";
const p = new pmtiles.PMTiles(PMTILES_URL);
// this is so we share one instance across the JS code and the map renderer
protocol.add(p);
let isDark = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
async function getLayers(isDark) {
let style_name = isDark ? "dark.json" : "custom.json";
const base = '{{"/assets/blog/pmtiles/" | relative_url}}';
let resp = await fetch(base + style_name);
let layers = await resp.json();
return layers;
}
let style = {
version:8,
glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf",
sources: {
"static_hackney": {
type: "vector",
url: "pmtiles://" + PMTILES_URL,
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
}
},
layers: await getLayers(isDark),
// layers: layers("static_hackney", "grayscale"),
}
let map = new maplibregl.Map({
container: 'map',
style: style,
});
// map.showTileBoundaries = true;
map.on("load", () => {
const myBounds = map.getSource("static_hackney").bounds;
map.setMaxBounds(myBounds);
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async event => {
let isDark = event.matches;
console.log(isDark);
style.layers = await getLayers(isDark);
console.log(style);
map.setStyle(style);
});
};
main();
</script>