Für ein Forschungsprojekt habe ich die Anfrage erhalten, eine Karte mit allen Einbahnstrassen Europas zu erstellen. Ziel war, die «Einbahnstrassen-Systeme» verschiedener europäischer Städte miteinander vergleichen zu können. Ich hatte die Idee, Daten von OpenStreetMap dafür zu nutzen, war mir aber nicht recht sicher, wie und wo ich die Daten ablegen sollte. Und es wäre auch eine Art «Backend» nötig gewesen, um die Daten zu laden, die zum vom Nutzenden gewählten Kartenausschnitt passen. Oder gibt es eine Alternative dazu?
Die Komplexität der Aufgabe hat mich erst etwas abgeschreckt, aber ich erinnerte mich an den Blogpost meines Kollegen Lukas Merz zu PMTiles. Vielleicht könnte dieses «cloud-native» Format die speziellen Anforderungen im oben kurz beschriebenen Projekt erfüllen?
Was sind PMTiles?
PMTiles sind ein Datenformat sowohl für räumlich referenzierte Raster- wie auch für Vektordaten. Die Daten sind so in der PMTiles-Datei abgelegt, dass ein passender Client die Daten einer Kachel respektive einer geographischen Bounding Box – ganz gemäss dem cloud-native-Paradigma – mit einem HTTP Range-Request abfragen kann. Ausser einem Client (zum Beispiel etwas JavaScript-Code in einer HTML-Seite) und einer öffentlich zugänglichen PMTiles-Datei braucht diese Umsetzung keine weitere Infrastruktur.
Aber schauen wir uns das anhand des Beispiels genauer an: Der komplette von mir entwickelte Code ist im GitHub Repository ebp-group/oneway-map zugänglich. Die fertige Karte kann unter «⬆️ Oneway Map Europe» (ebenfalls auf GitHub gehostet) angeschaut werden.
Um die Daten von OpenStreetMap zu beziehen, verwende ich die frei zugängliche Overpass API, welche es mit einer eigenen Abfragesprache ermöglicht, Daten zu beziehen. Um die Last des Overpass-Servers zu reduzieren, setze ich pro Land eine einzelne Abfrage ab. Die Resultate von Overpass werden im GeoJSON-Format im Verzeichnis oneway_countries
abgelegt.
Der folgende Code zeigt das Overpass-Query für die Einbahnstrassen der Schweiz (der Code für alle Länder befindet sich im Skript export_oneway_by_country.py
). Einige Strassenklassen werden dabei bewusst weggelassen, zum Beispiel Autobahnen, Tram- und Zugstrecken oder mehrspurige Strassen:
[out:json][timeout:600];
// gather results
rel["ISO3166-1:alpha2"="CH"]["boundary"="administrative"];
map_to_area;
way["highway"!="motorway"]["highway"!="trunk"]["highway"!="primary_link"]["highway"!="motorway_link"]["highway"!="path"]["highway"!="footway"]["highway"!="pedestrian"]["highway"!="cycleway"]["highway"!="service"]["highway"!="construction"]["highway"!="steps"][!"lanes"][!"tramway"][!"railway"][!"proposed"]["route"!="ferry"]["was:route"!="ferry"]["oneway"="yes"](area);
out geom;
Mit dem Tool tippecanoe kann ich die Daten aus GeoJSON in eine PMTiles-Datei umwandeln. Die PMTiles-Dateien der diversen europäischen Länder lassen sich dann anschliessend mit tile-join
in eine grosse PMTiles Datei zusammenfügen:
DIR="$(cd "$(dirname "$0")" && pwd)"
for country_file in $DIR/*.geojson; do
echo "Convert ${country_file} to PMTiles..."
tippecanoe --projection=EPSG:4326 -o "${country_file}.pmtiles" -l oneway --minimum-zoom=2 --maximum-zoom=18 --drop-densest-as-needed $country_file
done
echo "Merge all PMTiles..."
tile-join --overzoom -z 17 -Z 5 -o $DIR/merged.pmtiles $DIR/*.pmtiles
Für alle Einbahnstrassen (in OSM: oneway=yes
) Europas entsteht somit eine einzige PMTiles-Datei merged.pmtiles
, die circa 4.5 GB gross ist.
Hintergrundkarte mit Protomaps
Neben den eigentlichen Daten, die wir darstellen möchten, brauchen wir auch noch eine geeignete Hintergrundkarte. Wir könnten dazu einen der zahlreichen Dienste nutzen für Tiles oder mit Protomaps unsere eigene Hintergrundkarte erstellen. Der Prozess für Letzteres ist denkbar einfach, das Resultat auch wiederum eine einzelne PMTiles-Datei. Als erstes brauchen wir das Tool pmtiles
, das von Protomaps zur Verfügung gestellt wird. Mit diesem Tool können die Daten innerhalb einer bestimmten Bounding Box aus einem Planet-File, das die OpenStreetMap-Daten für die ganze Welt enthält, extrahiert werden:
# Check the planet file
pmtiles show https://build.protomaps.com/20241111.pmtiles
# Create bounding box for Europe with http://bboxfinder.com/
# => -24.785156,36.315125,36.914063,71.300793
# extract Europe
pmtiles extract https://build.protomaps.com/20241111.pmtiles europe.pmtiles --bbox=-24.785156,36.315125,36.914063,71.300793
Daraus resultiert eine circa 40 GB grosse europe.pmtiles
-Datei. Falls nicht alle Zoomstufen benötigt werden, kann zum Beispiel mit der Option --maxzoom=14
die Dateigrösse reduziert werden.
Visualisierung auf einer Karte
Wenn wir die beiden PMTiles-Dateien (die Basiskarte und die Einbahnstrassen) nun auf einer dynamischen Karte visualisieren möchten, können wir dazu das Mapping-Framework MapLibre nutzen. Ein einfaches Grundgerüst in HTML für MapLibre GL sieht wie folgt aus:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⬆️ Oneway Map Europe | Maplibre</title>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" />
<style>
#map {
height: 100vh;
}
</style>
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/pmtiles@2.11.0/dist/index.js"></script>
</head>
<body>
<div id="map"></div>
<script>
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
let map = new maplibregl.Map({
container: "map",
zoom: 9,
// [lon, lat]
center: [8.5418, 47.3707],
// 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", async () => {
// 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());
});
</script>
</body>
</html>
Hier haben wir bereits die pmtiles
Library hinzugefügt, die es ermöglicht PMTiles-Dateien zu lesen und als Datenquellen für MapLibre zu verwenden.
Im style.json
werden die Quellen beschrieben (Code gekürzt):
{
"version": 8,
"sources": {
"protomaps": {
"type": "vector",
"attribution": "<a href=\"https://github.com/protomaps/basemaps\">Protomaps</a> © <a href=\"https://openstreetmap.org\">OpenStreetMap</a>",
"url": "pmtiles://https://oneway-xyz.fly.dev/oneway-map/europe.pmtiles"
},
"oneway": {
"type": "vector",
"url": "pmtiles://https://oneway-xyz.fly.dev/oneway-map/merged.pmtiles"
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#cccccc"
}
},
{
"id": "earth",
"type": "fill",
"filter": [
"==",
[
"geometry-type"
],
"Polygon"
],
"source": "protomaps",
"source-layer": "earth",
"paint": {
"fill-color": "#e2dfda"
}
},
//{...},
{
"id": "oneway",
"type": "symbol",
"source": "oneway",
"source-layer": "oneway",
"minzoom": 15,
"layout": {
"icon-rotation-alignment": "map",
"icon-pitch-alignment": "map",
"symbol-placement": "line",
"symbol-spacing": 20,
"icon-ignore-placement": true,
"icon-allow-overlap": true,
"text-field": "",
"icon-keep-upright": false,
"icon-text-fit": "none",
"visibility": "visible",
"icon-size": 0.5,
"text-pitch-alignment": "map",
"icon-rotate": 0,
"icon-image": "arrow"
},
"paint": {
"icon-color": "red",
"text-color": "red",
"icon-halo-color": "red",
"icon-opacity": 1,
"text-halo-color": "red"
}
}
],
"sprite": "https://protomaps.github.io/basemaps-assets/sprites/v4/light",
"glyphs": "https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf"
}
Die JavaScript Zeile maplibregl.addProtocol("pmtiles", protocol.tile);
fügt das neue «pmtiles»-Protokoll hinzu, so dass MapLibre dann mit den Quellen pmtiles://https://oneway-xyz.fly.dev/oneway-map/europe.pmtiles
und pmtiles://https://oneway-xyz.fly.dev/oneway-map/merged.pmtiles
umgehen kann.
Für die Visualisierung war es wichtig, nicht nur die Lage, sondern auch die Richtung der Einbahnstrassen zu zeigen. Deshalb habe ich noch einen Pfeil (arrow
) als Icon hinzugefügt:
map.loadImage('arrow.png', function(error, image) {
if (error) throw error;
map.addImage('arrow', image, {pixelRation: 0.25});
});
Die Visualisierung setzt den Pfeil in die Digitialisierungsrichtung (so wie die Strasse digital erfasst wurde) und zeigt diesen ab Zoomstufe 15 an („minzoom“: 15). Auf den tieferen Zoomstufen wären die Pfeile eher störend, wenn man näher ranzoomt und die Details angezeigt werden, helfen die Pfeile die «Einbahnstrassen-Systeme» zu erkennen. OpenStreetMap kennt auch Einbahnstrassen die entgegen der Digitalisierungsrichtung laufen (oneway=-1
), diese machen jedoch einen sehr geringen Anteil aus und wurden für dieses Projekt ignoriert.
Deployment
Um das ganze Projekt online zu deployen, müssen ein paar HTML-, JSON- und JavaScript-Dateien im Web verfügbar sein. Ich habe dafür GitHub Pages verwendet. Daneben braucht es noch ein File-Hosting für die PMTiles-Dateien. Die einzige Anforderung an das Hosting ist, dass der Webserver die für die cloud-native Daten zentralen HTTP-Range Requests unterstützt (wir erinnern uns: PMTiles ist ein cloud-native Format). Das Hosting kann dabei in einem klassischen Web Hosting erfolgen oder über die Cloud. Typische Produkte wären Objects Storage wie Amazon S3, Cloudflare R2 oder eine eigenen MinIO-Instanz. Ich habe für dieses Projekt die letzte Option gewählt und eine eigene MinIO-Instanz erstellt mit der Anleitung von fly.io.
Wie ich in einem anderem Blogpost im Detail erläutert habe, ist es bei Cloud-Projekten wichtig, die Egress-Kosten im Auge zu behalten.
Fazit
Dieses Projekt hat gezeigt, dass für den gezeigten Anwendungsfall mit wenigen Mitteln eine vollwertige dynamische Webkarte mit Custom-Inhalten zur Verfügung gestellt werden kann ohne externe Abhängigkeiten (für komplette Unabhängigkeit müssten noch die verwendeten JavaScript-Bibliotheken des unpkg-Content Delivery Networks (CDN) selbst gehostet sowie die Sprites und Glyphs von Protomaps kopiert werden). Für dieses Projekt sind ein File-Host sowie ein paar Zeilen HTML und JavaScript ausreichend.
Wollten Sie schon mal PMTiles (oder andere cloud-native Formate) ausprobieren? Brauchen Sie Unterstützung beim Generieren der Dateien, beim Hosting bzw. dem Deployment? Oder haben Sie andere Data Engineering-Herausforderungen, über die Sie sich gerne einmal austauschen möchten? Kontaktieren Sie mich gerne per E-Mail oder buchen Sie direkt einen Termin mit mir.
Entdecke mehr von digital.ebp.ch
Subscribe to get the latest posts sent to your email.