Interaktive Vektorkarten für REDAXO – datenschutzkonform, ohne API-Key, vollständig selbst
- Kein API-Key erforderlich – nutzt OpenFreeMap (freie Vektorkacheln)
- DSGVO-konform – externer Datenverkehr wird vollständig über einen PHP-Proxy geleitet; Whitelist per Extension Point
VECTOR_MAPS_PROXY_DOMAINSund Backend-Einstellungen erweiterbar - Web Component
<vectormap>– direkt im Template oder Modul verwendbar, kein JavaScript nötig - Lazy-Init + Build-Queue – WebGL-Kontext wird sequentiell für jede Karte aufgebaut, erst wenn sie in den Viewport scrollt (verhindert Browserlimit von ~10 parallelen WebGL-Kontexten)
- Backend-Koordinatenpicker – Koordinatenfelder in REDAXO-Modulen mit Karte und Adresssuche
- Allgemeiner Geodaten-Picker – framework-unabhängiger Kartenpicker für normale Input-Felder, auch außerhalb von Modulen
- YForm-Integration – eigenes Value-Feld
vector_map_locationmit Picker, Stil- und Theme-Vorgabe - Builder-Integration – eigener Feldtyp
vector_map_pickerund ElementVector Mapfürplain,bootstrapunduikit - Routing – Koordinaten oder Adressen, mit Auto-, Fuß- und Fahrradmodus (OSRM)
- Interaktives Routenpanel – Von/Nach-Adressen direkt auf der Karte eingeben (Autocomplete); GPS-Locate-Button übernimmt den aktuellen Gerätestandort als Startpunkt
- Umgebungssuche (nearby) – POI-Suche via Overpass API (z. B. Ladestationen, Restaurants)
- Dynamisches Datennachladen – GeoJSON-Source per
moveendaktualisieren (z. B. Ladestationen live aus OSM) - Freie Wetter-APIs – Open-Meteo und Bright Sky (DWD) via Proxy, kein API-Key, Badge-Marker mit Live-Daten
- Standortsuche – optionale Geolokalisierung des Nutzers als Ausgangspunkt
- Themes –
dark,warm,monoeingebaut, frei erweiterbar via Theme-Editor im Backend; Routing-Linienfarbe im Theme-Editor konfigurierbar; Themes werden per Fetch geladen und erst danach eingeblendet (kein Flash of Unstyled Map) - Theme-Fade-in – Karte blendet sich sanft ein sobald das Theme geladen ist; Dauer per
theme-transition-Attribut (ms) steuerbar - 3D-Gebäude – aktivierbar per Attribut
- Mehrsprachigkeit – Sprachcode per
language-Attribut, steuert Kartenbeschriftungen von OpenFreeMap - Cluster – automatische Marker-Bündelung bei vielen Punkten
- Satellitenbild – ESRI World Imagery als
map-style="satellite"oder pershow-satelliteToggle-Button, kein API-Key (Tiles laufen DSGVO-konform über den Proxy) - maxPitch 60° – Kamerakippung auf 60° begrenzt (verhindert extreme Perspektiven)
Über den REDAXO-Installer oder manuell:
- AddOn-Verzeichnis nach
redaxo/src/addons/vector_maps/kopieren - Im REDAXO-Backend unter Installer > Eigene AddOns installieren und aktivieren
Keine weiteren Abhängigkeiten oder Node-Build-Schritte notwendig.
Nach der Installation lädt das AddOn alle benötigten Assets automatisch im Frontend — keine manuelle Einbindung notwendig. Die CSS- und JS-Dateien werden von REDAXO im <head> / vor </body> ausgegeben:
maplibre-gl.css → MapLibre GL JS Kern-Styles
vectormaps.css → Web Component Styles + Dark Mode
maplibre-gl.js → MapLibre GL JS Kern-Script
vectormaps.js → <vectormap> Custom Element
Danach kann <vectormap> direkt in jedem Template, Modul oder Artikel verwendet werden:
<!-- Karte einfach per Tag ausgeben – kein JS nötig -->
<vectormap lat="51.43" lng="6.77" zoom="13" height="400px"></vectormap>Über Vector Maps → Einstellungen kann das automatische Laden deaktiviert werden (load_frontend = false). In diesem Fall müssen die Assets im Template manuell eingebunden werden:
<?php
// In project/templates/template.php oder einem Fragment:
$vmAddon = rex_addon::get('vector_maps');
?>
<!DOCTYPE html>
<html>
<head>
<!-- MapLibre GL JS Styles -->
<link rel="stylesheet" href="<?= $vmAddon->getAssetsUrl('maplibre/maplibre-gl.css') ?>">
<!-- Vector Maps Styles (Dark Mode, Custom Marker, Route-Panel …) -->
<link rel="stylesheet" href="<?= $vmAddon->getAssetsUrl('build/vectormaps.css') ?>">
</head>
<body>
<!-- Karten-Tags hier platzieren -->
<vectormap lat="51.43" lng="6.77" zoom="13"></vectormap>
<!-- MapLibre GL JS Core (defer empfohlen) -->
<script defer src="<?= $vmAddon->getAssetsUrl('maplibre/maplibre-gl.js') ?>"></script>
<!-- Vector Maps Web Component (defer empfohlen) -->
<script defer src="<?= $vmAddon->getAssetsUrl('build/vectormaps.js') ?>"></script>
</body>
</html>Hinweis: Die tatsächlichen Asset-Pfade variieren je nach REDAXO-Installation.
getAssetsUrl()liefert immer die korrekte absolute URL.
Im Modul-Output kann die Karte direkt mit REX_VALUE-Platzhaltern ausgegeben werden:
<?php
// Module output
$lat = rex_escape(REX_VALUE[1]);
$lng = rex_escape(REX_VALUE[2]);
$zoom = rex_escape(REX_VALUE[3] ?: '14');
$popup = rex_escape(REX_VALUE[4]);
?>
<vectormap
lat="<?= $lat ?>"
lng="<?= $lng ?>"
zoom="<?= $zoom ?>"
height="400px"
markers='[{"lat":<?= $lat ?>,"lng":<?= $lng ?>,"popup":"<?= $popup ?>"}]'>
</vectormap>Ein normales Input-Feld kann framework-unabhängig durch den Vector-Maps-Picker erweitert werden.
HTML:
<input
type="text"
name="location"
value="52.520008,13.404954"
data-vector-picker="1"
data-vector-picker-style="bright">Optional per PHP-Helper:
<?php
echo \KLXM\VectorMaps\Picker\PickerWidget::factory('location')
->setValue('52.520008,13.404954')
->setMapStyle('bright')
->setTheme('warm')
->parse();Das Input-Feld wird automatisch mit einem Picker-Button erweitert. Im Modal können Koordinaten per Klick, Drag oder Adresssuche gesetzt werden.
Hinweis: Bei data-vector-picker-style="satellite" wird die Theme-Auswahl bewusst deaktiviert, da Themes nur auf Vektorstile wirken.
Für einen schnellen Funktionstest gibt es im Backend die Seite Demo → Geocoding mit Beispielen für Picker, YForm und Builder.
Für YForm steht ein eigenes Value-Feld zur Verfügung, das Koordinaten als lat,lng speichert und den Vector-Maps-Picker direkt im Formular rendert.
Beispiel:
value|vector_map_location|location|Standort|liberty|warm|1
Schema:
value|vector_map_location|name|label|[map_style]|[theme]|[required]
Beispiel mit Notice:
value|vector_map_location|location|Standort|bright|redaxo|1|Bitte Position auf der Karte wählen
Das Feld eignet sich besonders für Kontakt-, Filial- oder Event-Datensätze, weil Redakteure die Position nicht manuell als Zahlenpaar pflegen müssen.
Das AddOn registriert ein eigenes Builder-Element Vector Map und einen eigenen Builder-Feldtyp für den Koordinaten-Picker.
Merkmale:
- Theme-Auswahl direkt im Element
- Basis-Kartenstil wählbar (
liberty,bright,positron,satellite) - Marker + Popup
- optionales globales Infofenster
- Controls, Locate, Satellit und Fullscreen
- optionaler Routenplaner mit Start/Ziel, Modus, fixiertem Ziel und Panel-Layout
- Templates für
uikit,bootstrapundplain
Der Koordinatenwert wird im Builder über den gleichen Picker gesetzt wie bei normalen Input-Feldern.
Für typische Kontaktseiten kann das Element jetzt ohne Sondercode als Anfahrtskarte genutzt werden:
route_panelblendet das interaktive Von/Nach-Panel einroute_tosetzt ein festes Ziel als Adresse oderlat,lngroute_to_lockedfixiert dieses Ziel für Redakteure und Besucherroute_panel_layouterlaubt Overlay oder Seiten-Spalte links/rechtsroute_panel_styleübernimmt die vorhandenen Panel-Skinsglass,contrastundbrand
Damit eignet sich dasselbe Element sowohl für eine einfache Marker-Karte als auch für eine vollwertige Anfahrts- oder Routenkarte.
Falls eine Content-Security-Policy aktiv ist, müssen folgende Direktiven ergänzt werden — nur für Seiten mit Karten:
worker-src blob:;
Alle Tile-Requests laufen über den REDAXO-Proxy (
/?rex_api_vector_maps_proxy=1), daher sind keine externenconnect-src-Einträge für OpenFreeMap, Nominatim oder OSRM notwendig.
<!-- Einfache Karte -->
<vectormap lat="51.51" lng="-0.12" zoom="12"></vectormap>
<!-- Mit Markern und Popup -->
<vectormap lat="51.51" lng="-0.12" zoom="13"
markers='[{"lat":51.51,"lng":-0.12,"popup":"London"}]'>
</vectormap>
<!-- Vollbreite, 3D, Dark-Theme -->
<vectormap lat="48.85" lng="2.35" zoom="15"
height="500px" theme="dark" 3d>
</vectormap>| Attribut | Standard | Beispiel | Beschreibung |
|---|---|---|---|
lat |
51.505 |
lat="51.51" |
Breitengrad des Kartenzentrums |
lng |
7.0 |
lng="-0.12" |
Längengrad des Kartenzentrums |
center |
– | center="51.51,-0.12" |
Kartenmitte als lat,lng (Alternative zu lat+lng) |
zoom |
6 |
zoom="15" |
Anfangszoomstufe (0–22) |
height |
400 |
height="600px" |
CSS-Höhe des Karten-Containers |
pitch |
0 |
pitch="45" |
Kamerakippung in Grad (0–60) |
bearing |
0 |
bearing="45" |
Kartenausrichtung in Grad |
map-style |
liberty |
map-style="satellite" |
OFM-Stilname: liberty, bright, positron, satellite (ESRI World Imagery) – oder Theme-Name |
show-satellite |
false |
show-satellite |
Toggle-Button zum Wechsel zwischen Vektor- und Satellitenbild (ESRI World Imagery, kein API-Key) |
theme |
– | theme="dark" |
Farb-Theme: dark, warm, mono oder eigener Theme-Name |
theme-transition |
350 |
theme-transition="600" |
Einblend-Dauer in ms nach Theme-Load (0 = sofort, Standard: 350) |
language |
de |
language="en" |
Sprachcode für Kartenbeschriftungen (ISO 639-1) |
markers |
– | markers='[{"lat":51.51,"lng":-0.12}]' |
JSON-Array mit Marker-Objekten |
fit-bounds |
auto | fit-bounds="false" |
Kartenausschnitt an alle Marker anpassen. Standard: aktiv bei mehreren Markern; mit fit-bounds="false" deaktivieren |
cluster |
false |
cluster |
Marker bei hoher Dichte clustern |
3d |
false |
3d |
3D-Gebäude aktivieren |
interactive |
true |
interactive="false" |
Karte scrollbar/zoombar (Standard: aktiv). Mit interactive="false" deaktivieren |
locate |
false |
locate |
Standort-Button anzeigen |
fullscreen |
false |
fullscreen |
Fullscreen-Button anzeigen (Karte im Browser-Vollbild umschalten) |
controls-position |
top-right |
controls-position="top-left" |
Position der MapLibre-Navigation (top-left, top-right, bottom-left, bottom-right) |
controls-cluster |
right |
controls-cluster="left" |
Einfacher Shortcut für sinnvolle Defaults: clustert Navigation + Locate/Satellit gemeinsam (left, right, oder direkte Ecke wie bottom-left) |
controls-style |
– | controls-style="rail" |
Vorgefertigte UI-Stile für Navigation/Kompass/Fullscreen plus Locate/Satellit: soft, rail, minimal, bold |
fullscreen-position |
wie controls-position |
fullscreen-position="bottom-right" |
Position des Fullscreen-Buttons |
no-navigation |
false |
no-navigation |
Zoom-/Kompass-Controls ausblenden |
no-attribution |
false |
no-attribution |
Attributionszeile ausblenden |
buttons-position |
wie controls-position |
buttons-position="top-left" |
Basisposition für locate/show-satellite Buttons |
locate-position |
wie buttons-position |
locate-position="bottom-left" |
Individuelle Position des Locate-Buttons |
satellite-position |
wie buttons-position |
satellite-position="top-left" |
Individuelle Position des Satellit-Buttons |
buttons-offset |
10 |
buttons-offset="16" |
Außenabstand der Overlay-Buttons in px |
buttons-gap |
38 |
buttons-gap="44" |
Vertikaler Abstand zwischen Locate- und Satelliten-Button in derselben Ecke |
popup-focus-mode |
auto |
popup-focus-mode="restore" |
Fokusverhalten beim Schließen von Popups: restore (zurück zum Auslöser), canvas (zur Karte), none (kein Fokuswechsel). Standard: Marker-Popups restore, Layer-Popups canvas |
info-html |
– | info-html="<strong>Info</strong>" |
Globales Infofenster innerhalb der Karte (HTML erlaubt) |
info-position |
top-left |
info-position="bottom-right" |
Position des Infofensters (top-left, top-right, bottom-left, bottom-right) |
info-offset |
12 |
info-offset="20" |
Außenabstand des Infofensters in px |
info-class |
– | info-class="my-info-box" |
Zusätzliche CSS-Klasse für eigenes Styling |
info-visible |
true |
info-visible="false" |
Infofenster ein-/ausblenden |
info-closable |
false |
info-closable |
Schließen-Button im Infofenster anzeigen |
route-panel-position |
top-left |
route-panel-position="bottom-right" |
Position des Route-Panels |
route-panel-offset |
12 |
route-panel-offset="18" |
Außenabstand des Route-Panels in px |
route-panel-style |
– | route-panel-style="glass" |
Vorgefertigte Panel-Skins: glass, contrast, brand |
route-panel-layout |
overlay |
route-panel-layout="side-right" |
Layout des Route-Panels: overlay (in der Karte), side-right, side-left |
route-panel-width |
340 |
route-panel-width="360" |
Breite der Seiten-Spalte bei side-right/side-left |
fly-to |
– | fly-to="48.85,2.35" |
Kartenansicht nach dem Laden animiert zentrieren |
min-zoom |
0 |
min-zoom="5" |
Minimaler Zoom-Level |
max-zoom |
22 |
max-zoom="18" |
Maximaler Zoom-Level |
Reihenfolge der Positionslogik:
- Wenn gesetzt, gilt
locate-positionbzw.satellite-position(pro Button). - Sonst gilt
buttons-position(gemeinsame Basis für beide Buttons). - Sonst folgt
buttons-positionautomatisch der Control-Ecke auscontrols-position. - Wenn
controls-positionfehlt, greiftcontrols-cluster(right= oben rechts,left= oben links).
Standard ohne zusätzliche Attribute (alles gemeinsam in einer Ecke):
<vectormap
center="51.165691,10.451526"
zoom="6"
locate
show-satellite
fullscreen>
</vectormap>Einfach komplett nach links clustern:
<vectormap
center="51.165691,10.451526"
zoom="6"
locate
show-satellite
fullscreen
controls-cluster="left">
</vectormap>Manuell trennen (optional):
<vectormap
center="51.165691,10.451526"
zoom="6"
locate
show-satellite
controls-position="top-left"
buttons-position="top-left"
locate-position="bottom-left"
satellite-position="top-right">
</vectormap>Variante A: per Attribut info-html
<vectormap
center="51.43,6.77"
zoom="12"
height="380"
info-position="top-left"
info-html="<strong>Service</strong><br>Mo-Fr 08:00-18:00"
info-closable>
</vectormap>Variante B: Inline als Child <div class="mapinfo">
<vectormap center="51.43,6.77" zoom="12" height="380">
<div class="mapinfo" data-position="top-left" data-closable data-class="my-info-box">
<strong>Kontakt</strong><br>
Mo-Fr 08:00-18:00<br>
<a href="/kontakt">Zur Kontaktseite</a>
</div>
</vectormap>Unterstützte mapinfo-Optionen:
data-position="top-left|top-right|bottom-left|bottom-right"data-class="deine-klasse"data-closable
Drei kurze Copy-Paste-Vorlagen fuer typische Einsaetze.
<vectormap
center="52.520008,13.404954"
zoom="12"
height="420"
locate
show-satellite
fullscreen
controls-cluster="left"
controls-style="soft"
info-position="top-right"
info-html="<strong>Standort Berlin</strong><br>Mo-Fr 08:00-18:00"
info-closable>
</vectormap><vectormap
center="51.4298,6.7742"
zoom="12"
height="480"
route-panel
route-to="Am Schuermannshuett 40g, 47441 Moers"
route-to-popup="<strong>KLXM Crossmedia GmbH</strong><br>klxm.de"
route-to-locked
route-mode="driving"
controls-cluster="left"
controls-style="minimal">
</vectormap><vectormap
center="51.165691,10.451526"
zoom="6"
height="520"
route-panel
route-panel-layout="side-right"
route-panel-width="360"
route-panel-style="brand"
route-from="Berlin HBF"
route-to="Leipzig Hauptbahnhof"
controls-cluster="left"
locate
show-satellite>
</vectormap>Die MapLibre-Buttons (+, −, Kompass, Fullscreen) können gemeinsam mit Locate/Satellit eingefärbt werden:
<vectormap
locate
show-satellite
fullscreen
controls-style="rail"
controls-position="top-right"
buttons-position="top-right"
style="
--vm-nav-bg: rgba(12, 20, 32, .82);
--vm-nav-color: #dbeafe;
--vm-nav-hover-bg: rgba(30, 64, 175, .75);
--vm-nav-active-bg: #38bdf8;
--vm-nav-active-color: #0c1b2d;
--vm-tool-bg: rgba(12, 20, 32, .82);
--vm-tool-color: #dbeafe;
">
</vectormap>Wichtige Variablen: --vm-nav-bg, --vm-nav-color, --vm-nav-hover-bg, --vm-nav-active-bg, --vm-nav-active-color, --vm-nav-border, --vm-nav-shadow, --vm-nav-size, --vm-tool-*.
- Navigation + Locate + Satellit werden standardmäßig als gemeinsamer Control-Cluster positioniert.
buttons-positionfolgt automatisch der Control-Ecke (viacontrols-positionodercontrols-cluster).- Für individuelle Layouts können
controls-position,buttons-position,locate-position,satellite-positionweiterhin optional separat gesetzt werden.
- Der Abstand für Locate/Satellit wird nicht über fixe Schätzwerte, sondern über die reale Höhe der gerenderten MapLibre-Control-Ecke berechnet.
- Dadurch werden Überlagerungen mit
+/-, Kompass und Fullscreen in unterschiedlichen Themes/Styles zuverlässig vermieden.
Zwei kuratierte Richtungen, wenn die Karte bewusst nicht nach klassischer Standard-Map aussehen soll.
<style>
vectormap.vm-skin-signature-editorial {
--vm-nav-size: 34px;
--vm-nav-radius: 14px;
--vm-nav-bg: #f7f3ea;
--vm-nav-color: #2f2a24;
--vm-nav-border: 1px solid rgba(87, 76, 61, .22);
--vm-nav-shadow: 0 10px 26px rgba(67, 56, 42, .22);
--vm-nav-hover-bg: #efe7d8;
--vm-nav-hover-color: #111827;
--vm-nav-active-bg: #c67b4e;
--vm-nav-active-color: #fffaf4;
--vm-tool-bg: #f7f3ea;
--vm-tool-color: #2f2a24;
--vm-tool-border: 1px solid rgba(87, 76, 61, .22);
--vm-tool-shadow: 0 10px 26px rgba(67, 56, 42, .22);
--vm-tool-hover-bg: #efe7d8;
--vm-tool-hover-color: #111827;
--vm-tool-active-bg: #c67b4e;
--vm-tool-active-color: #fffaf4;
}
</style>
<vectormap
class="vm-skin-signature-editorial"
locate
show-satellite
fullscreen
controls-position="top-left"
buttons-position="top-left"
route-panel
route-panel-style="brand"
route-panel-position="bottom-left">
</vectormap><style>
vectormap.vm-skin-signature-transit {
--vm-nav-size: 35px;
--vm-nav-radius: 8px;
--vm-nav-bg: rgba(20, 26, 38, .9);
--vm-nav-color: #d9e7ff;
--vm-nav-border: 1px solid rgba(99, 120, 160, .38);
--vm-nav-shadow: 0 12px 28px rgba(3, 8, 17, .45);
--vm-nav-hover-bg: rgba(37, 54, 92, .92);
--vm-nav-hover-color: #f0f6ff;
--vm-nav-active-bg: #00c2a8;
--vm-nav-active-color: #06211d;
--vm-nav-icon-filter: invert(1) brightness(1.15);
--vm-tool-bg: rgba(20, 26, 38, .9);
--vm-tool-color: #d9e7ff;
--vm-tool-border: 1px solid rgba(99, 120, 160, .38);
--vm-tool-shadow: 0 12px 28px rgba(3, 8, 17, .45);
--vm-tool-hover-bg: rgba(37, 54, 92, .92);
--vm-tool-hover-color: #f0f6ff;
--vm-tool-active-bg: #00c2a8;
--vm-tool-active-color: #06211d;
}
</style>
<vectormap
class="vm-skin-signature-transit"
locate
show-satellite
fullscreen
controls-position="top-right"
buttons-position="top-right"
route-panel
route-panel-style="glass"
route-panel-position="bottom-left">
</vectormap>Routen zwischen zwei Punkten berechnen – sowohl mit Koordinaten als auch mit Adressen. Alle Routing-Anfragen laufen über OSRM und werden DSGVO-konform über den PHP-Proxy geleitet.
<!-- Routing mit Koordinaten -->
<vectormap lat="51.43" lng="6.77" zoom="13"
route-from="51.4298,6.7742"
route-to="51.4539,6.7658"
route-mode="walking">
</vectormap>
<!-- Routing mit Adressen -->
<vectormap lat="48.85" lng="2.35" zoom="6"
route-from="Eiffelturm, Paris"
route-to="Duisburg Hauptbahnhof"
route-mode="driving">
</vectormap>| Attribut | Standard | Beispiel | Beschreibung |
|---|---|---|---|
route-from |
– | route-from="51.43,6.77" |
Startpunkt: lat,lng oder Adresse als Text |
route-to |
– | route-to="51.45,6.76" |
Zielpunkt: lat,lng oder Adresse als Text |
route-mode |
driving |
route-mode="walking" |
Routing-Profil: driving, walking, cycling |
route-panel |
false |
route-panel |
Interaktives Von/Nach-Panel auf der Karte einblenden |
route-to-locked |
false |
route-to-locked |
Zieladresse im Panel fixieren – nicht editierbar, kein Autocomplete |
route-to-popup |
– | route-to-popup="Firmenname<br>firmenname.de" |
Eigener Popup-Inhalt (HTML) am Ziel-Marker, wenn nur route-to gesetzt ist |
route-no-steps |
false |
route-no-steps |
Abbiegehinweise (Turn-by-Turn) im Panel ausblenden |
Das Routenpanel wird als schwebende Seitenleiste auf der Karte angezeigt und ermöglicht es, Start- und Zielpunkt direkt per Texteingabe (mit Autocomplete) sowie das Profil per Klick zu wählen:
<vectormap lat="51.43" lng="6.77" zoom="13"
height="500px" interactive route-panel
route-from="Duisburg Hauptbahnhof"
route-to="Duisburg Innenstadt">
</vectormap>Das Routenpanel zeigt SVG-Icons für die drei Modi Auto, Zu Fuß und Fahrrad. Es überschreibt
route-from/route-to-Attribute nicht, sondern verwendet sie als Startwerte.
Wenn das Ziel fest vorgegeben ist (z.B. ein Firmenstandort), kann das Zielfeld eingesperrt werden. Der Nutzer gibt nur seine Startadresse ein — das Ziel ist nicht editierbar und hat kein Autocomplete:
<vectormap lat="51.4298" lng="6.7742" zoom="12"
height="450px"
interactive
route-panel
route-to="Duisburg Hauptbahnhof"
route-to-locked
route-no-steps>
</vectormap>Mit
route-no-stepswird nur der berechnete Weg auf der Karte angezeigt — ohne Schritt-für-Schritt-Abbiegehinweise darunter. Ideal für Anfahrtskarten auf Kontaktseiten.
Wenn nur route-to gesetzt ist (kein route-from), wird das Ziel automatisch geocodiert, ein Marker gesetzt und die Karte zentriert. Mit route-to-popup kann der Popup-Inhalt des Markers frei definiert werden:
<vectormap
height="500px"
route-panel
route-to="Am Schürmannshütt 40g, 47441 Moers"
route-to-popup="<strong>KLXM Crossmedia GmbH</strong><br>klxm.de"
route-to-locked
route-mode="driving">
</vectormap>Der Nutzer sieht sofort den Ziel-Marker mit Popup. Sobald er seine Startadresse eingibt, wird die Route berechnet.
Zwei Möglichkeiten, ESRI World Imagery (kostenlos, kein API-Key) zu nutzen:
<!-- Reines Satellitenbild -->
<vectormap lat="48.858844" lng="2.294351" zoom="16" height="400px"
map-style="satellite">
<marker lat="48.858844" lng="2.294351" popup="Eiffelturm"></marker>
</vectormap>
<!-- Vektor-Karte mit Toggle-Button für Satellitenansicht -->
<vectormap lat="52.520008" lng="13.404954" zoom="13" height="400px"
show-satellite>
<marker lat="52.520008" lng="13.404954" popup="Brandenburger Tor"></marker>
</vectormap>| Stil | Beschreibung |
|---|---|
satellite |
Reines Satellitenbild (ESRI World Imagery) |
show-satellite |
Toggle-Button zum Wechsel zwischen aktuellem Vektorstil und Satellitenbild |
Der show-satellite Toggle-Button erscheint in der aktiven Button-/Control-Ecke (Default: gemeinsam oben rechts). Marker bleiben beim Wechsel erhalten, GeoJSON-Layer werden nach Rückkehr zur Vektorkarte automatisch neu geladen.
Hinweis: ESRI World Imagery und das Referenz-Overlay sind frei nutzbar (Lizenz: Esri Master License Agreement). Attribution wird automatisch gesetzt. Für gewerbliche Hochlast-Szenarien empfiehlt sich ein eigener Tile-Server.
Für saubere Tastatur-Navigation (Tab-Reihenfolge) kann das Fokusverhalten nach dem Schließen eines Popups pro Karte gesteuert werden:
<!-- Fokus springt zurück zum auslösenden Marker -->
<vectormap ... popup-focus-mode="restore"></vectormap>
<!-- Fokus geht zurück auf die Kartenfläche -->
<vectormap ... popup-focus-mode="canvas"></vectormap>
<!-- Kein automatischer Fokuswechsel nach dem Schließen -->
<vectormap ... popup-focus-mode="none"></vectormap>Standardverhalten ohne Attribut:
- Marker-Popup: Fokus zurück zum Marker (
restore) - Layer-/Feature-Popup ohne Marker-Trigger: Fokus auf Karte (
canvas)
Die Overlay-UI ist pro Karte vollständig per CSS-Variablen anpassbar:
<vectormap
center="52.52,13.405"
zoom="12"
height="420"
locate
show-satellite
buttons-position="top-left"
route-panel
route-panel-style="glass"
info-html="<strong>Service</strong><br>Mo-Fr 08:00-18:00"
style="
--vm-widget-size: 34px;
--vm-widget-radius: 10px;
--vm-widget-bg: #0f172a;
--vm-widget-color: #e2e8f0;
--vm-info-bg: rgba(15,23,42,.88);
--vm-info-color: #e2e8f0;
--vm-rp-accent: #5eead4;
">
</vectormap>Infofenster lässt sich zusätzlich per JS steuern:
const el = document.getElementById('my-map');
el.setInfoHtml('<strong>Live-Update</strong><br>Stand: 12:30');
el.setInfoPosition('bottom-right');
el.showInfoWindow();
el.hideInfoWindow();Alternative ohne Attribut (Inline-Markup als Child):
<vectormap center="51.43,6.77" zoom="12">
<div class="mapinfo" data-position="top-left" data-closable>
<strong>Kontakt</strong><br>
Mo-Fr 08:00-18:00
</div>
</vectormap>Optional für .mapinfo:
data-position="top-left|top-right|bottom-left|bottom-right"data-class="deine-klasse"data-closable
Mit dem nearby-Attribut können Points of Interest (POI) aus OpenStreetMap rund um den Kartenmittelpunkt oder den Gerätestandort des Nutzers geladen werden. Die Abfragen laufen über die Overpass API und werden DSGVO-konform über den PHP-Proxy geleitet.
<!-- E-Ladestationen im Umkreis von 2 km um Duisburg HBF -->
<vectormap lat="51.4298" lng="6.7742" zoom="13"
height="500px"
nearby="amenity=charging_station"
nearby-radius="2000"
nearby-label="Ladestation">
</vectormap>
<!-- Supermärkte und Discounter in der Nähe des Nutzers -->
<vectormap lat="51.43" lng="6.77" zoom="13"
height="500px"
nearby="shop=supermarket|shop=discount"
nearby-locate>
</vectormap>| Attribut | Standard | Beispiel | Beschreibung |
|---|---|---|---|
nearby |
– | nearby="amenity=restaurant" |
Overpass-Filter (key=value), mehrere mit | trennen |
nearby-radius |
1000 |
nearby-radius="2000" |
Suchradius in Metern |
nearby-label |
OSM-Tags | nearby-label="Restaurant" |
Fallback-Label für Popups (wenn kein OSM-Name) |
nearby-locate |
false |
nearby-locate |
Nutzerstandort (Geolokalisierung) als Suchmittelpunkt verwenden |
| Filter | Beschreibung |
|---|---|
amenity=charging_station |
E-Ladestationen |
amenity=fuel |
Tankstellen |
amenity=restaurant |
Restaurants |
amenity=cafe |
Cafés |
amenity=pharmacy |
Apotheken |
amenity=hospital |
Krankenhäuser |
amenity=bank |
Banken |
shop=supermarket |
Supermärkte |
shop=supermarket|shop=discount |
Supermärkte und Discounter |
leisure=park |
Parks |
tourism=hotel |
Hotels |
tourism=museum |
Museen |
Hinweis: Bei
nearby-locatefragt die Karte den Browser nach dem Gerätestandort. Fällt die Geolokalisierung fehl, wird automatisch auf den Kartenmittelpunkt zurückgegriffen.
Über die globale Hilfsfunktion vmProxyUrl() können externe APIs auch client-seitig per JavaScript angesprochen werden – DSGVO-konform über den REDAXO-Proxy, ohne CORS-Probleme.
Das folgende Muster lädt E-Ladesäulen dynamisch für den aktuellen Kartenausschnitt, sobald die Karte bewegt wird:
// Zugriff auf die MapLibre-Instanz eines <vectormap>-Elements
const el = document.getElementById('meine-karte');
const map = el._vmMap; // nach dem 'load'-Event verfügbar
// GeoJSON-Source + Layer anlegen
map.addSource('stationen', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
map.addLayer({
id: 'stationen-punkte', type: 'circle', source: 'stationen',
paint: { 'circle-color': '#27ae60', 'circle-stroke-color': '#fff', 'circle-stroke-width': 2 }
});
// Bei Kartenbewegung Daten nachladen (debounced)
let timer;
map.on('moveend', () => {
clearTimeout(timer);
timer = setTimeout(async () => {
if (map.getZoom() < 9) return; // erst ab Zoom 9 laden
const b = map.getBounds();
const q = `[out:json][timeout:10];`
+ `node["amenity"="charging_station"]`
+ `(${b.getSouth()},${b.getWest()},${b.getNorth()},${b.getEast()});`
+ `out 200;`;
const data = await fetch(vmProxyUrl(
'https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(q)
)).then(r => r.json());
const features = (data.elements || []).map(n => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [n.lon, n.lat] },
properties: n.tags
}));
map.getSource('stationen').setData({ type: 'FeatureCollection', features });
}, 600);
});Hinweis:
vmProxyUrlist nach dem Laden vonvectormaps.jsglobal verfügbar.overpass-api.deist bereits in der Proxy-Whitelist (lib/Proxy.php) eingetragen.
// Aktuelles Wetter für Berlin via Open-Meteo
const omUrl = 'https://api.open-meteo.com/v1/forecast'
+ '?latitude=52.52&longitude=13.405¤t=temperature_2m,weather_code&timezone=auto';
const omData = await fetch(vmProxyUrl(omUrl)).then(r => r.json());
console.log('Berlin:', omData.current.temperature_2m, '°C');
// Aktuelles DWD-Wetter für Hamburg via Bright Sky
const bsUrl = 'https://api.brightsky.dev/current_weather?lat=53.55&lon=10.0';
const bsData = await fetch(vmProxyUrl(bsUrl)).then(r => r.json());
console.log('Hamburg:', bsData.weather.temperature, '°C', bsData.weather.condition);Beide Dienste sind bereits in der Proxy-Whitelist eingetragen (api.open-meteo.com, api.brightsky.dev).
Fünf eingebaute Themes stehen sofort zur Verfügung:
| Theme | Attribut | Beschreibung |
|---|---|---|
| Standard | (leer) | Helles Standardlayout (OpenFreeMap Liberty) |
dark |
theme="dark" |
Dunkles Layout |
redaxo |
theme="redaxo" |
REDAXO-Backend-Farben (Blau/Neutral) |
bright |
theme="bright" |
Leuchtendes, kontrastreiches Layout |
warm |
theme="warm" |
Warmes Orange-/Beigelayout |
mono |
theme="mono" |
Graustufen-Layout |
Eigene Themes können im Backend unter Vector Maps → Themes angelegt und live per Theme-Editor konfiguriert werden. Die Routing-Linienfarbe (route_line) lässt sich ebenfalls pro Theme einstellen.
Bei Karten mit einem Theme (theme="..." oder map-style="<eigener-name>") bleibt die Karte zunächst unsichtbar und blendet sich sanft ein, sobald das Theme vollständig geladen und angewendet wurde. Das verhindert das kurze Aufblitzen des ungestylten Grundstils ("Flash of Unstyled Map").
Die Einblend-Dauer ist per Attribut steuerbar:
<!-- Standard: 350 ms -->
<vectormap map-style="klxm" height="400px"></vectormap>
<!-- Langsam: 800 ms -->
<vectormap map-style="klxm" theme-transition="800" height="400px"></vectormap>
<!-- Sofort (kein Fade): 0 ms -->
<vectormap map-style="klxm" theme-transition="0" height="400px"></vectormap>Marker werden als JSON-Array übergeben:
<vectormap lat="51.43" lng="6.77" zoom="12"
markers='[
{"lat":51.4298,"lng":6.7742,"popup":"<strong>Duisburg HBF</strong>"},
{"lat":51.4539,"lng":6.7658,"popup":"Duisburg Zoo","color":"#e74c3c"}
]'>
</vectormap>Unterstützte Marker-Eigenschaften:
| Eigenschaft | Beschreibung |
|---|---|
lat, lng |
Position (erforderlich) |
popup |
Popup-Inhalt (HTML erlaubt) |
color |
Marker-Farbe für Standard-Pin (CSS-Farbwert, Standard: #2b7095) |
icon |
URL zu einem Bild (PNG, SVG, WebP) → Custom-Pin als <img> |
html |
Beliebiger HTML-String, Emoji oder SVG → Custom-Pin als HTML-Element |
size |
[breite, höhe] in px für icon-Marker, z. B. [40, 48] (Standard: 32×32) |
anchor |
Ankerpunkt des Custom-Pins: bottom (Standard), center, top, left, right |
Jeder Marker kann als individueller Pin gestaltet werden – per Emoji, SVG, HTML-Div oder externem Bild:
<!-- Emoji-Pins -->
<vectormap lat="51.43" lng="6.77" zoom="13"
markers='[
{"lat":51.4298,"lng":6.7742,"html":"🚉","anchor":"center","popup":"Bahnhof"},
{"lat":51.4339,"lng":6.7621,"html":"🏛️","anchor":"center","popup":"Museum"},
{"lat":51.4218,"lng":6.7660,"html":"🏬","anchor":"center","popup":"Einkauf"}
]'>
</vectormap>
<!-- HTML-Div-Pins mit eigenem CSS -->
<vectormap lat="51.43" lng="6.77" zoom="13"
markers='[
{"lat":51.4298,"lng":6.7742,
"html":"<div class=\"mein-pin mein-pin--rot\">A</div>",
"popup":"Punkt A"}
]'>
</vectormap>
<!-- Icon-URL (SVG/PNG) mit benutzerdefinierter Größe -->
<vectormap lat="51.43" lng="6.77" zoom="13"
markers='[
{"lat":51.4298,"lng":6.7742,
"icon":"/assets/custom/pin.svg",
"size":[40,48],
"anchor":"bottom",
"popup":"Unser Standort"}
]'>
</vectormap>Custom-Marker (
icon/html) nutzen intern MapLibreselement-Option und sind vollständig per CSS gestaltbar. Im Cluster-Modus werden sie als Standard-GeoJSON-Punkte dargestellt.
Mit dem geojson-Attribut können externe oder inline GeoJSON-Daten direkt auf der Karte angezeigt werden. Unterstützt werden Punkte, Linien und Flächen (auch gemischt in einer FeatureCollection).
<!-- Relative URL (wird direkt geladen) -->
<vectormap lat="51.43" lng="6.77" zoom="10"
geojson="/assets/karte-data.geojson"
geojson-color="#27ae60"
geojson-popup="name">
</vectormap>
<!-- Externe URL (wird automatisch über den REDAXO-Proxy geleitet) -->
<vectormap lat="51.43" lng="6.77" zoom="6"
geojson="https://example.com/data.geojson"
geojson-color="#e74c3c"
geojson-opacity="0.4"
geojson-popup="description">
</vectormap>
<!-- Inline-JSON (kein Server-Request) -->
<vectormap lat="51.434" lng="6.763" zoom="14"
geojson='{"type":"FeatureCollection","features":[
{"type":"Feature","properties":{"name":"Innenhafen"},"geometry":{"type":"Polygon","coordinates":[...]}},
{"type":"Feature","properties":{"name":"Museum"},"geometry":{"type":"Point","coordinates":[6.762,51.434]}}
]}'
geojson-color="#2980b9"
geojson-opacity="0.25">
</vectormap>| Attribut | Standard | Beispiel | Beschreibung |
|---|---|---|---|
geojson |
– | geojson="/data.geojson" |
Relative/absolute URL oder Inline-JSON-String |
geojson-color |
#2b7095 |
geojson-color="#e74c3c" |
Standardfarbe für Punkte, Linien und Flächen |
geojson-opacity |
0.3 |
geojson-opacity="0.5" |
Transparenz der Flächen-Füllung (0–1) |
geojson-popup |
(alle) | geojson-popup="name" |
Property-Name für Klick-Popup; leer = automatische Tabelle aller Properties |
geojsonundmarkerslassen sich kombinieren — GeoJSON-Layer und individuelle Marker erscheinen gleichzeitig auf der Karte.
Punkte können pro Feature individuell gestaltet werden — direkt über GeoJSON-Properties ohne zusätzliche Attribute:
<vectormap lat="51.434" lng="6.763" zoom="13"
geojson='{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Ladestation",
"Betreiber": "Stadtwerke",
"marker-color": "#27ae60",
"marker-symbol": "⚡"
},
"geometry": { "type": "Point", "coordinates": [6.763, 51.434] }
},
{
"type": "Feature",
"properties": {
"name": "Museum",
"marker-color": "#8e44ad",
"marker-symbol": "🏛"
},
"geometry": { "type": "Point", "coordinates": [6.770, 51.430] }
},
{
"type": "Feature",
"properties": { "name": "Stadtpark" },
"geometry": {
"type": "Polygon",
"coordinates": [[[6.760, 51.435],[6.780, 51.435],[6.780, 51.425],[6.760, 51.425],[6.760, 51.435]]]
}
}
]
}'
geojson-color="#2980b9"
geojson-opacity="0.25"
geojson-popup="name">
</vectormap>Simplestyle-Properties (pro GeoJSON-Feature, nur für Punkte):
| Property | Beispiel | Beschreibung |
|---|---|---|
marker-color |
"#e74c3c" |
Pin-Farbe — überschreibt geojson-color für diesen Punkt |
marker-symbol |
"⚡" "🏥" "A" |
Emoji oder Text-Zeichen im Pin. Ohne Angabe: Standard-Farb-Pin |
Flächen und Linien nutzen weiterhin geojson-color und geojson-opacity global.
In PHP lässt sich das komfortabel mit json_encode() aufbauen:
$locations = [
['name' => 'Rathaus', 'lat' => 51.434, 'lng' => 6.762, 'symbol' => '🏛', 'color' => '#8e44ad'],
['name' => 'Ladestation','lat' => 51.437, 'lng' => 6.770, 'symbol' => '⚡', 'color' => '#27ae60'],
['name' => 'Parkplatz', 'lat' => 51.431, 'lng' => 6.758, 'symbol' => '🅿', 'color' => '#2980b9'],
];
$features = array_map(static function(array $loc): array {
return [
'type' => 'Feature',
'properties' => [
'name' => $loc['name'],
'marker-color' => $loc['color'],
'marker-symbol' => $loc['symbol'],
],
'geometry' => [
'type' => 'Point',
'coordinates' => [$loc['lng'], $loc['lat']],
],
];
}, $locations);
$geoJson = json_encode(['type' => 'FeatureCollection', 'features' => $features]);
echo '<vectormap lat="51.434" lng="6.763" zoom="13"'
. ' geojson=\'' . rex_escape($geoJson) . '\''
. ' geojson-popup="name">'
. '</vectormap>';Für REDAXO-Module kann ein Koordinatenpicker aktiviert werden, der ein einfaches Eingabefeld mit Kartenpicker-Button ausstattet.
<div class="form-group">
<label>Koordinaten</label>
<input type="text"
name="REX_INPUT_VALUE[1]"
value="REX_VALUE[1]"
class="form-control"
data-vector-picker="1"
placeholder="lat,lng">
</div>Durch data-vector-picker="1" erscheint neben dem Feld ein Karten-Button. Beim Klick öffnet sich ein Modal mit einer Vollbild-Karte, Adresssuche (Autocomplete via Nominatim) und Koordinatenübernahme per Klick oder Suche.
<vectormap lat="REX_VALUE[1]" lng="REX_VALUE[2]" zoom="14"
markers='[{"lat":REX_VALUE[1],"lng":REX_VALUE[2],"popup":"Unser Standort"}]'>
</vectormap>Alternativ: lat,lng aus einem einzelnen Feld parsen:
<?php
$coords = explode(',', 'REX_VALUE[1]');
$lat = trim($coords[0] ?? '51.5');
$lng = trim($coords[1] ?? '7.0');
?>
<vectormap lat="<?= $lat ?>" lng="<?= $lng ?>" zoom="14"></vectormap>Alle externen Dienste werden über den internen PHP-Proxy geleitet, sodass keine IP-Adressen der Websitebesucher an Dritte übertragen werden:
| Dienst | Verwendung |
|---|---|
| OpenFreeMap | Vektorkacheln (Karte) |
| Nominatim / OpenStreetMap | Adresssuche (Geocoding) |
| OSRM | Routing (Fahrt-, Fuß-, Radwege) |
| Overpass API | POI-Suche & dynamisches Datennachladen |
| Open-Meteo | Weltweite Wettervorhersage (kostenlos, kein API-Key) |
| Bright Sky / DWD | Deutsche Wetterdaten (DWD Open Data, Server in DE) |
| Esri ArcGIS / World Imagery | Satellitenbilder (Raster-Tiles) |
Der Proxy unter lib/Proxy.php validiert alle Anfragen gegen eine Whitelist und leitet nur Anfragen an erlaubte Domains weiter. SSRF-Attacks werden durch strikte Domain-Prüfung verhindert.
Per Extension Point (andere AddOns):
rex_extension::register('VECTOR_MAPS_PROXY_DOMAINS', static function(rex_extension_point $ep): array {
$list = $ep->getSubject();
$list[] = 'https://my-tiles.example.com/';
return $list;
});Per Backend-Einstellungen (manuell): Unter Vector Maps → Einstellungen → Proxy: Zusätzliche erlaubte Domains können URL-Präfixe (eine pro Zeile) hinterlegt werden.
<vectormap
lat="51.4298"
lng="6.7742"
zoom="14"
height="500px"
theme="dark"
3d
interactive
locate
markers='[
{"lat":51.4298,"lng":6.7742,"popup":"<strong>Duisburg Hauptbahnhof</strong><br>Bahnhofsvorplatz, 47119 Duisburg","color":"#3498db"}
]'
route-from="51.4298,6.7742"
route-to="51.4539,6.7658"
route-mode="walking">
</vectormap>| Seite | Beschreibung |
|---|---|
| Demo | Interaktive Übersicht aller Features mit Live-Beispielen |
| Themes | Theme-Editor zum Erstellen und Anpassen benutzerdefinierter Themes (inkl. Routing-Linienfarbe) |
| Einstellungen | Frontend-Einbindung, Proxy-Whitelist (manuelle Domains), Tile-Cache verwalten |
- REDAXO >= 5.14
- PHP >= 8.1
- PHP-Extensions:
curl,json
Der build/-Ordner enthält das Build-Tooling für Entwickler. Für den normalen Einsatz des AddOns wird kein Build-Schritt benötigt — alle kompilierten Assets liegen bereits fertig im Repository.
vector_maps/
├── assets/
│ ├── build/ # Eigene Assets (Quelle + kompiliert)
│ │ ├── vectormaps.js # Quelle: <vectormap> Web Component + Picker
│ │ ├── vectormaps.min.js # Minifiziert (vom Build generiert)
│ │ ├── vectormaps_i18n.js # Quelle: Frontend-Übersetzungen
│ │ ├── vectormaps_i18n.min.js
│ │ ├── theme-editor.js # Quelle: Backend Theme-Editor
│ │ ├── theme-editor.min.js
│ │ ├── vectormaps.css # Quelle: Styles
│ │ └── vectormaps.min.css # Minifiziert (vom Build generiert)
│ └── maplibre/ # Vendor-Assets (von npm kopiert)
│ ├── maplibre-gl.js # MapLibre GL JS
│ ├── maplibre-gl.css # MapLibre GL JS Styles
│ └── pmtiles.js # PMTiles-Decoder
├── build/ # Build-Tooling (nicht im Release-ZIP)
│ ├── package.json # npm-Abhängigkeiten (esbuild, maplibre-gl, pmtiles)
│ └── build.mjs # Build-Script
└── .github/
└── workflows/
└── release.yml # GitHub Actions: Release-ZIP auf Tag-Push
build/node_modules/und*.map-Dateien sind per.gitignoreausgeschlossen und landen nicht im Release-ZIP.
- Node.js >= 18
- npm
cd vector_maps/build
npm install| Befehl | Beschreibung |
|---|---|
npm run build |
Eigene Assets (assets/build/) minifizieren |
npm run build:all |
Vendor-Assets aus npm aktualisieren und eigene Assets minifizieren |
npm run watch |
Watch-Modus: automatisch neu bauen bei Dateiänderungen (+ Source Maps) |
cd build
npm run buildErzeugt *.min.js / *.min.css aus den Quelldateien in assets/build/.
cd build
npm install # package.json aktualisieren falls nötig
npm run build:allKopiert die Distributionsdateien aus node_modules/maplibre-gl/dist/ und node_modules/pmtiles/dist/ nach assets/maplibre/ und baut anschließend alle eigenen Assets.
cd build
npm run watchÜberwacht alle Quelldateien in assets/build/ und kompiliert auf Änderungen sofort neu. Im Watch-Modus werden zusätzlich Source Maps (.map) generiert — sie sind per .gitignore ausgeschlossen und landen nicht im Repository.
Releases werden automatisch per GitHub Actions erstellt, wenn ein Tag (v*.*.*) gepusht wird:
git tag v1.1.0
git push origin v1.1.0Der Workflow (release.yml) installiert npm-Abhängigkeiten, baut alle Assets (npm run build:all) und erstellt ein Release-ZIP ohne den build/-Ordner, node_modules, .map-Dateien und andere Dev-Artefakte.
- MapLibre GL JS – Open-Source-Kartenbibliothek (BSD-2-Clause)
- OpenFreeMap – Kostenlose Vektorkacheln ohne API-Key
- Nominatim / OSM – Adress-Geocoding (ODbL)
- OSRM – Open Source Routing Machine (BSD-2-Clause)
- Overpass API – OSM-Datenbankabfragen (AGPL)
- Open-Meteo – Kostenlose Wetter-API, kein API-Key (CC BY 4.0)
- Bright Sky – DWD-Wetterdaten für Deutschland (CC BY 4.0)
MIT License – siehe LICENSE.md
Entwickelt von KLXM Crossmedia