Development-Dokumentation

Sandstorm Maps ist ein SaaS Produkt von Sandstorm, welches DSGVO-konforme, datenschutzfreundliche Karteneinbindung ermöglicht, das heißt ohne Requests an Drittanbieter.

Dies wird durch die Kombination zweier Ideen realisiert:

  • Im Projekt muss ein Reverse Proxy für die Kartendaten hinzugefügt werden, der Requests auf maps-api.sandstorm.de proxied.
  • Die Kartendaten selbst sind von OpenStreetMap und sind für den gesamten Planeten vorberechnet

Einbindung der Karten

1. Nginx Proxy Konfiguration

Im Projekt folgende Konfiguration zu Nginx hinzufügen:

# globale Konfiguration -> in http {...} block hinzufügen proxy_cache_path /tmp/nginx-maptiles-cache levels=1:2 keys_zone=MAPTILES:10m inactive=24h max_size=1g; # Hinzufügen in server {...} block location /_maptiles/ { # TO ADJUST: the "t" parameter is your API-KEY, which looks like api-xxxxxxxx set $args t=YOUR_API_KEY; set $backend "maps-api.sandstorm.de"; # we need to strip the _maptiles prefix away, and add the tenant before sending the URL to the upstream. rewrite ^/_maptiles/(.*)$ /$1 break; proxy_pass https://$backend; resolver 1.1.1.1; # we want to cache the tiles etc, according to the upstream server proxy_cache MAPTILES; }

Nicht vergessen, YOUR_API_KEY (in Zeile 9) durch den eigenen API-Key zu ersetzen.

2. Einbindung der Karte im HTML

Hier gibt es folgende Varianten:

  • Alpine.js
    • lazy (Empfehlung, da JS mit ca. 700KB nur geladen werden sollte, wenn Karte tatsächlich angezeigt wird)
    • eager (bei Seitenaufruf)
  • Plain JS
    • lazy
    • eager

Alpine.js

Grundsetup – Wir empfehlen eine Alpine-Komponente wie die folgende:

 
// sollte in map.ts (eigene Komponente) import Alpine, { Alpine as AlpineType } from 'alpinejs'; // NOTE: in your esbuild config, set `external: ['/_maptiles/frontend/v1.1/map-main.js']` // for the import below to work. export function initMap(Alpine: AlpineType) { Alpine.data('map', ({lng, lat, zoom, popupText}: any) => ({ loadMap() { // @ts-ignore import('/_maptiles/frontend/v1.1/map-main.js') .then(async ({maplibregl, createMap}) => { let map = await createMap(window.location.protocol + '//' + window.location.host + '/_maptiles', { container: this.$el, center: [lng, lat], // starting position [lng, lat] zoom: zoom, // starting zoom }); map.addControl(new maplibregl.NavigationControl(), 'top-left'); new maplibregl.Marker() .setLngLat([lng, lat]) .setPopup(new maplibregl.Popup({ offset: 25 }).setText(popupText)) .addTo(map); }); } })); } // sollte in main.ts initMap(Alpine); Alpine.start();

ESBuild Config anpassen:

esbuild.build({ // ... // add the following entry to the externals list, as we do not want to transpile it external: ['/_maptiles/frontend/v1.1/map-main.js'], });

Alpine.js, Lazy

Das Plugin @alpinejs/intersect muss installiert werden, wie in der Doku beschrieben.

Das folgende Snippet lädt die Karte lazy, also nur wenn sie sichtbar ist. Dies ist der empfohlene Weg.

<link rel="stylesheet" href="/_maptiles/frontend/v1.1/map-main.css" /> <div style="width:300px; height: 300px" x-data="map({lat: 51.0567032, lng: 13.7769583, zoom: 14, popupText: 'Mein Text'})" x-intersect.once="loadMap()"></div>

Hier wird die Direktive x-intersect.once verwendet, um die Karte bei der ersten Nutzung anzuzeigen.

Alpine.js, eager

<link rel="stylesheet" href="/_maptiles/frontend/v1.1/map-main.css" /> <div style="width:300px; height: 300px" x-data="map({lat: 51.0567032, lng: 13.7769583, zoom: 14, popupText: 'Mein Text'})" x-init="loadMap()"></div>

Plain JS

Plain JS, Lazy

Das folgende Snippet lädt die Karte lazy, also nur wenn sie sichtbar ist. Dies ist der empfohlene Weg.

<link rel="stylesheet" href="/_maptiles/frontend/v1.1/map-main.css" /> <div id="map" style="width:100%; height: 100vh"></div> <script type="module"> const mapNode = document.getElementById('map'); // we load the map JS only if the map becomes visible. new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { loadMap(); observer.disconnect(); } }); }).observe(mapNode); function loadMap() { import('/_maptiles/frontend/v1.1/map-main.js') .then(async ({maplibregl, createMap}) => { let map = await createMap(window.location.protocol + '//' + window.location.host + '/_maptiles', { container: mapNode, // HTML Element center: [13.7769583, 51.0567032], // starting position [lng, lat] zoom: 14, // starting zoom }); map.addControl(new maplibregl.NavigationControl(), 'top-left'); new maplibregl.Marker() .setLngLat([13.7769583, 51.0567032]) .setPopup(new maplibregl.Popup({ offset: 25 }).setText( 'In diesem Gebäude sitzt die Sandstorm Media GmbH.' )) .addTo(map); }); } </script>

Plain JS, Eager

Ein minimales Snippet zum Kartenrendering sieht bspw. so aus (nicht empfohlen, da nicht lazy):

<link rel="stylesheet" href="/_maptiles/frontend/v1.1/map-main.css" /> <div id="map" style="width:100%; height: 100vh"></div> <script type="module"> import {createMap} from '/_maptiles/frontend/v1.1/map-main.js'; const mapNode = document.getElementById('map'); createMap(window.location.protocol + '//' + window.location.host + '/_maptiles', { container: mapNode, // HTML Element center: [13.7769583, 51.0567032], // starting position [lng, lat] zoom: 14, // starting zoom }); </script>

createMap muss als 1. Parameter die absolute URL zum Nginx Proxy eingestellt haben. Dies ist
in den obrigen Configs jeweils der Fall.

3. Lizenzhinweis

Bitte darauf achten, dass unten in der Karte der Copyrighthinweis »© OpenMapTiles © OpenStreetMap contributors« vorhanden ist (das ist standardmäßig der Fall, da muss nichts geändert werden).

Verwendung in Neos

Ein Beispiel zur Einbindung in Neos-Projekte gibt es in diesem Commit unseres Neos-Kickstarts. (Bitte beachten: einige Dateien wurden nach dem Commit aktualisiert.)

Anpassung der Karte

Die API von https://maps-api.sandstorm.de/_maptiles/frontend/v1.1/map-main.js entspricht grundsätzlich maplibre-gl (in der Version 2.4). Es können dementsprechend die vollständige MapLibre API und ihre Beispiele verwendet werden, etwa um Marker zu setzen oder den Stil der Karte anzupassen.