Selbstgehostete Openstreetmaps-Karten mit Protomaps

Veröffentlicht von am

Styled Protomaps Embed

Vor einiger Zeit habe ich einen Blogartikel geschrieben zum Thema OpenStreetMap Karten selbst hosten. Da einige der darin beschriebenen Techniken heute (Stand Mai 2024) nicht mehr problemlos funktionieren oder inzwischen überflüssig sind, habe ich mich dazu entschlossen, diese Aktualisierung zu schreiben.

Warum interaktive Karten selbst hosten?

Warum selbst eine interaktive Karte hosten, wenn man sie leicht über Mapbox oder Google Maps einbetten kann?

Ich persölich bevorzuge es, die Kartendaten auf meinem eigenen Server zu haben, da ich mich dann nicht um externe Anbieter kümmern muss. Das erleichtert mir die Anpassung oder den Umzug meiner Website.

Außerdem muss ich mir keine Sorgen um Datenschutz oder DSGVO-Konformität eines Drittanbieters machen.

Ich bin vor einiger Zeit auf Protomaps gestoßen, ein Datenformat für Kartendaten. Dieses vereinfacht die Veröffentlichung und das Selbst-Hosten von Open-Steetmaps Karten.

Um eine selbstgehostete Karte mithilfe von Protomaps zu veröffentlichen, sind folgene Schritte notwendig:

  1. Bestimmen des gewünschten Kartenausschnitts
  2. Herunterladen des entsprechenden Kartenmaterials
  3. Einbinden und Anzeigen des Kartenmaterials in eine Webseite
  4. (optional) Anpassen des Kartenstils

Schritt 1: Bestimmen des Kartenumrisses

Bevor wir starten, legen wir als erstes einen neuen Ordner an für das Projekt an. Hier legen wir im laufe dieses Tutorials alle wichtigen Daten ab.

Zunächst werden wir den Umriss unserer Karte festlegen und speichern ihn als Datei speichern. Diesen Umriss werden wir dann im nächsten Schritt benutzen, um das passende Kartenmaterial herunterzuladen.

Je größer der Umriss, desto mehr Kartendaten müssen heruntergeladen werden, und desto mehr Speicherplatz wird benötigt.

Entsprechend empfehle ich, zunächst mit einem kleineren Umriss zu starten (ca. 10 x 10 km).

Diesen Umriss beschreiben wir im GeoJSON-Format und legen ihn als .geojson-Datei in unserem Projektordner ab.

Die graphische Editor geojson.io ist kostenlos, und ermöglcht es uns, eine GeoJSON-Datei anzulegen und zu bearbeiten:

Unter Save > GeoJSON können wir die Datei herunterladen. Diese legen wir dann unserem Projektordner ab und benennen sie in bounds.geojson um.

Unser Projektordner sieht zu diesem Zeitpunkt dann so aus:

.
└── bounds.geojson

Alternativ ist es natürlich auch möglich, die GeoJSON-Datei manuell oder mit anderen Editoren zu erstellen.

Schritt 2: Herunterladen der Kartendaten

Nachdem wir den Kartenumriss in eine Datei gespeichert haben, können wir nun daran gehen, die Kartendaten herunterzuladen.

Die Openstreetmaps-Kartendaten im Protomaps-Dateiformat werden von Protomaps zuf Verfügung gestellt. Die zur Verfügung gestellten Datensätze enthalten allerdings die Kartendaten für die ganze Welt und sind entsprechend viele Gigabytes groß. Für die allermeissten Kartenanwendungen reicht ein kleinerer Kartenausschnitt aus.

Um den in Schritt 1 festgelegten Kartenauschnitt herunterzuladen, verwenden wir ein Kommandozeilen-Tool namens go-pmtiles.

Dieses Tool kann man unter https://github.com/protomaps/go-pmtiles/releases herunterladen. Auf der Seite sind verschiedene Dateien aufgelistet. Hier laden wir die passende zip-Datei für unser Betriebssystem herunter und entpacken diese.

Im heruntergeladenen zip-Archiv befindet ich die ausführbare Datei pmtiles. Diese Datei legen wir in unserem Projektordner ab:

.
├── bounds.geojson
└── pmtiles

Nachdem wir das go-pmtiles-Tool erfolgreich heruntergeladen haben, können wir nun daran gehen, die Kartendaten herunterzuladen.

Unter https://maps.protomaps.com/builds/ befindet sich eine Liste mit den aktuellsten Kartendaten, von hier können wir uns eine Version aussuchen und die Daten für unseren Kartenausschnitt herunterladen. Wir kopieren uns zunächst die Link-Adresse des download-Links (in meinem Fall zum Beispiel https://build.protomaps.com/20231002.pmtiles).

Diese Link-Adresse nutzen wir dann in unserem Terminal-Befehl, um die Kartendaten herunterzuladen:

./pmtiles extract https://build.protomaps.com/20231002.pmtiles mapdata.pmtiles --region=bounds.geojson

Nach kurzer Zeit sollten die Daten heruntergeladen werden…

fetching 9 dirs, 9 chunks, 8 requests
Region tiles 165, result tile entries 165
fetching 165 tiles, 28 chunks, 20 requests
fetching chunks  11% |██                 | (567 kB/4.9 MB, 43 kB/s) [17s:1m44s]

Danach sollte unser Projektordner so aussehen:

.
├── bounds.geojson
├── mapdata.pmtiles
└── pmtiles

Damit haben wir den Download der Kartendaten abgeschlossen und wir können nun im nächsten Schritt daran gehen, die Kartendaten auf einer Webseite anzuzeigen.

Schritt 3: Kartendaten in Webseite einbinden und anzeigen

Zum Einbinden der Karte werden wir die JavaScript-Bibliothek Maplibre GL JS verwenden.

Wir laden dazu die folgenden JavaScript und CSS-Dateien herunter, und speichern sie ebenfalls in unserem Projektordner:

Weiterhin verschieben wir die Datei mapdata.pmtiles in den assets-Ordner.

Die Inhalte Projektordners sollten nun in etwa so aussehen:

.
├── assets
│   ├── css
│   │   └── maplibre-gl.css
│   ├── js
│   │   ├── maplibre-gl.js
│   │   └── pmtiles.js
│   └── mapdata.pmtiles
├── bounds.geojson
└── pmtiles

Als nächstes erstellen wir in unserem Projektordner noch eine simple_map.html-Datei mit folgendem Inhalt:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>Protomaps Simple Map Example</title>
</head>

<body>
    <link rel="stylesheet" href="assets/css/maplibre-gl.css" />
    <style>
        /* Make the map container take all available space on the page */
        #my_map {
            position: absolute;
            top: 0;
            left: 0;
            width: 100svw;
            height: 100svh;
        }
    </style>
    <div id="my_map"></div>
    <script src="assets/js/maplibre-gl.js"></script>
    <script src="assets/js/pmtiles.js"></script>
    <script type="module">
        // add the PMTiles plugin to the maplibregl global.
        const protocol = new pmtiles.Protocol();
        maplibregl.addProtocol('pmtiles', (request) => {
            return new Promise((resolve, reject) => {
                const callback = (err, data) => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve({ data });
                    }
                };

                protocol.tile(request, callback);
            });
        });

        // the location of our pmtiles file
        const PMTILES_URL = './assets/mapdata.pmtiles';

        // create a new PmTiles instance
        const pmtilesInstance = new pmtiles.PMTiles(PMTILES_URL);

        // this is so we share one instance across the JS code and the map renderer
        protocol.add(pmtilesInstance);

        // we first fetch the header so we can get the center lon, lat of the map.
        const mapMetaData = await pmtilesInstance.getHeader();

        const map = new maplibregl.Map({
            // sets the element we want to add our map to
            container: document.querySelector('#my_map'),

            // set the initial center of the map to the center
            // of the map data
            center: [mapMetaData.centerLon, mapMetaData.centerLat],

            // sets the initial zoom of the map according to the
            // map zoom
            zoom: mapMetaData.maxZoom - 2,

            style: {
                version: 8,

                // ading protomaps as the data source for our map
                sources: {
                    'protomaps': {
                        type: 'vector',
                        url: `pmtiles://${PMTILES_URL}`,
                        attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
                    }
                },

                // simple map layer definitions
                // for more information about structure see
                // https://maplibre.org/maplibre-style-spec/layers/
                layers: [
                    {
                        'id': 'buildings',
                        'source': 'protomaps',
                        'source-layer': 'landuse',
                        'type': 'fill',
                        'paint': {
                            'fill-color': '#0066ff'
                        }
                    },
                    {
                        'id': 'roads',
                        'source': 'protomaps',
                        'source-layer': 'roads',
                        'type': 'line',
                        'paint': {
                            'line-color': 'black'
                        }
                    },
                    {
                        'id': 'mask',
                        'source': 'protomaps',
                        'source-layer': 'mask',
                        'type': 'fill',
                        'paint': {
                            'fill-color': 'white'
                        }
                    }
                ]
            }
        });
    </script>
</body>

</html>

Wenn wir den Projektordner auf einen Webserver hochladen (bzw. einen Webserver in diesem Ordner starten), und dann die simple_map.html-Datei im Browser öffnen, wird unsere Karte jetzt dargestellt. (Wichtig dabei: der Webserver muss HTTP Range Requests unterstützen.)

Einfache Protomaps Karte

Schritt 4: Kartendarstellung anpassen

Die visuelle Kartendarstellung kann mit sogenannten Style-Regeln definiert und erweitert werden. Das ermöglicht es z.B. Gebäude einzufärben oder Straßen farblich zu markieren. Diese Style-Regeln werden auf der Seite von Maplibre genauer erklärt.

Netterweise werden von Protomaps einige fertige Kartenstile zur Verfügung gestellt. Diese kann man unter auf der Testseite ausprobieren, anpassen und kopieren. Den Kartenstil speichern wir in unserem Projektordner als Datei unter assets/map_style.json.

Für die Textdarstellung werden neben den Kartendaten zusätzlich auch noch Schriftarten im .pbf-Format benötigt. Diese kann man ebenfalls bei GitHub aus dem basemaps-assets Repository von Protomaps herunterladen, (oder einfach diesen Link klicken). Den enthaltenen fonts-Order fügen wir ebenfalls unserem Projektordner hinzu, unter /assets.

Alternativ erlaubt es die Webseite Maplibre Font Maker eigene Schriftdateien (.ttf, .otf) in das .pbf-Format zu konvertieren.

.
├── assets
│   ├── css
│   │   └── maplibre-gl.css
│   ├── fonts
│   │   └── ... (sehr viele .pbf-Dateien)
│   ├── js
│   │   ├── maplibre-gl.js
│   │   └── pmtiles.js
│   ├── map_style.json
│   └── mapdata.pmtiles
├── bounds.geojson
├── pmtiles
└── simple_map.html

Als nächstes erstellen wir in unserem Projektordner eine neue Datei namens styled_map.html mit folgendem Inhalt:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>Protomaps Styled Map Example</title>
</head>

<body>
    <link rel="stylesheet" href="assets/css/maplibre-gl.css" />
    <style>
        /* Make the map container take all available space on the page */
        #my_map {
            position: absolute;
            top: 0;
            left: 0;
            width: 100svw;
            height: 100svh;
        }
    </style>
    <div id="my_map"></div>
    <script src="assets/js/maplibre-gl.js"></script>
    <script src="assets/js/pmtiles.js"></script>
    <script type="module">
        // dynamically loads the 'map_style.json' file and then
        // gets the layers array from it
        // we could also add this data inline and skip this fetch call,
        // but we add it here to maintain readability
        const { layers } = await fetch('./assets/map_style.json').then(res => res.json());

        // add the PMTiles plugin to the maplibregl global.
        const protocol = new pmtiles.Protocol();
        maplibregl.addProtocol('pmtiles', (request) => {
            return new Promise((resolve, reject) => {
                const callback = (err, data) => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve({ data });
                    }
                };

                protocol.tile(request, callback);
            });
        });

        // the location of our pmtiles file
        const PMTILES_URL = './assets/mapdata.pmtiles';

        // create a new PmTiles instance
        const pmtilesInstance = new pmtiles.PMTiles(PMTILES_URL);

        // this is so we share one instance across the JS code and the map renderer
        protocol.add(pmtilesInstance);

        // we first fetch the header so we can get the center lon, lat of the map.
        const mapMetaData = await pmtilesInstance.getHeader();

        const map = new maplibregl.Map({
            // sets the element we want to add our map to
            container: document.querySelector('#my_map'),

            // set the initial center of the map to the center
            // of the map data
            center: [mapMetaData.centerLon, mapMetaData.centerLat],

            // sets the initial zoom of the map according to the
            // map zoom
            zoom: mapMetaData.maxZoom - 2,

            style: {
                version: 8,

                // ading protomaps as the data source for our map
                sources: {
                    'protomaps': {
                        type: 'vector',
                        url: `pmtiles://${PMTILES_URL}`,
                        attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
                    }
                },

                // adding the map layers (the visual styles)
                layers,
                // the location of our font files
                glyphs: './assets/fonts/{fontstack}/{range}.pbf'
            }
        });
    </script>
</body>

</html>

Wenn wir diese Datei im Browser öffnen, sieht es ungefähr so aus:

Protomaps Styled Map

Mit unserer Selbsgehosteten Karte als Basis stehen uns alle Wege offen: Auf der Seite von MapLibre gibt es die verschiedensten Beispiele, dafür was man mit einer Karte so alles anstellen kann.

Ein einfaches Beispiel: Die Darstellung einer GPX-Datei

GPX-Datei Visualisierung

Hinterlasse einen Kommentar

Verfügbare Formatierungen

Benutze Markdown-Befehle oder ihre HTML-Äquivalente, um deinen Kommentar zu formatieren:

Textauszeichnungen
*kursiv*, **fett**, ~~durchgestrichen~~, `Code` und <mark>markierter Text</mark>.
Listen
- Listenpunkt 1
- Listenpunkt 1
1. Nummerierte Liste 1
2. Nummerierte Liste 2
Zitate
> Zitierter Text
Code-Blöcke
```
// Ein einfacher Code-Block
```
```php
// Etwas PHP-Code
phpinfo();
```
Verlinkungen
[Link-Text](https://example.com)
Vollständige URLs werden automatisch in Links umgewandelt.

Auf der eigenen Website geantwortet? Sende eine Webmention!