Verwendung von SSH-Keys zur Signierung von Git-Commits

Im November letzten Jahres hatte ich euch erklärt, wie ihr unterschiedliche Git-Einstellungen für private und berufliche Projekte verwenden könnt. In einem Code-Schnipsel hatte ich dabei SSH-Keys zur Signierung von Commits verwendet. In der Vergangenheit hat man Commits in der Regel mit GPG-Keys signiert. Daher möchte ich euch heute zeigen, was ihr tun müsst, um stattdessen einen SSH-Key zur Signierung euer Commits zu verwenden.

Schritt 0: Erstellt ein SSH-Schlüsselpaar

Ihr habt vermutlich schon ein Schlüsselpaar. Wie sonst würdet ihr Repositories von einer Git-Hosting-Plattform klonen? Aber falls ihr noch kein SSH-Schlüsselpaar habt, führt diesen Befehl aus:

ssh-keygen -t ed25519 -C j.doe@example.com

Ich würde euch empfehlen, den ed25519 Verschlüsselungs-Algorithmus zu verwenden. Ihr könnt auch einen Kommentar setzen, ansonsten wird hier user@machine des Geräts verwendet, auf dem ihr den Schlüssel erstellt. Ich würde euch auch empfehlen eine „passphrase“ zu verwenden, aber diese ist nicht zwingend notwendig, um den Schlüssel für die Signierung von Commits zu verwenden.

Schritt 1: Aktualisiert eure Git-Konfiguration

Nachdem wir nun einen SSH-Key haben, können wir ihn zur Konfiguration hinzufügen. Wir können das mit dem folgenden Befehl tun:

git config --global user.signkey ~/.ssh/id_ed25519

Da Git noch immer GPG-Keys als Standard verwendet, um Commits zu signieren, müssen wir einstellen, dass stattdessen SSH-Keys verwendet werden sollen. Das erreichen wir durch das Hinzufügen der folgenden Einstellung:

git config --global gpg.format ssh

Falls ihr bisher noch nie Commits signiert habt und dies in Zukunft immer tun wollt, dann führt noch diesen Befehl aus:

git config --global commit.gpgsign true

Lasst euch hier nicht durch den Namen gpgsign der Einstellung verwirren, es wird dennoch SSH verwenden, um eure Commits zu signieren.

Schritt 2: Festlegen der „allowed signers“ (optional)

Wenn ihr nun etwas commited und euch das Log inklusive der Signaturen anseht, dann erhaltet ihr folgenden Fehler:

git log --show-signature
error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
No signature
Author: Jo Doe <j.doe@example.com>
Date:   Sun Jan 29 19:07:52 2023 +0000

Das passiert, weil Git nicht wissen kann, ob die Public-Keys valide sind. Um das zu lösen, müssen wir noch eine andere Einstellung in die Git-Konfiguration hinzufügen:

git config --global gpg.ssh.allowedSignersFile ~/.git_allowed_signers

Diese Datei müssen wir jetzt noch erstellen, da wir sonst einen anderen Fehler bekommen. Ihr für pro Zeile jeweils einen Public-Key ein und schreibt in die erste „Zeile“ die dazugehörige E-Mail-Adresse, gefolgt vom Inhalt des Public-Keys. Die Datei könnte wie folgt aussehen:

# ~/.git_allowed_signers
j.doe@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ818qdUM98GriqpTKhqMmwYAgeK3iiCg07Qgya5NwN/ j.doe@example.com

Wenn ihr euch jetzt das Git-Log erneut anseht, dann solltet ihr eine gültige Signatur sehen:

git log --show-signature
commit eccdf56431b052772b09027c5cea585b8e7eee32 (HEAD -> master)
Good "git" signature for j.doe@example.com with ED25519 key SHA256:/qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk
Author: Jo Doe <j.doe@example.com>
Date:   Sun Jan 29 19:07:52 2023 +0000

Nach diesen ganzen Schritten sollte eure Git-Konfiguration (mindestens) wie folgt aussehen:

[user]
	email = j.doe@example.com
	name = Jo Doe
	signingkey = ~/.ssh/id_ed25519
[commit]
	gpgsign = true
[gpg]
	format = ssh
[gpg "ssh"]
	allowedSignersFile = ~/.git_allowed_signers

Schritt 3: Hinzufügen des Schlüssels zu Git-Hosting-Plattformen

Viele von euch werden wohl GitHub, GitLab oder andere Git-Hosting-Plattformen verwenden. Mindestens die beiden genannten Plattformen unterstützen auch SSH-Keys zur Signierung in ihrem UI.

Hinzufügen des Schlüssels zu GitHub

Navigiert zu „Settings | SSH and GPG keys„. Klickt dort auf den „New SSH key“ Button. Im Feld „Key type“ wählt ihr „Signing Key“ aus. Fügt dann euren Schlüssel ein und vergebt einen Titel:

Screenshot des "New SSH key" Formulars zum Hinzufügen des Public-Keys.

Ihr könnt denselben Schlüssel verwenden, den ihr eventuell schon für einen „Authentication Key“ verwendet habt.

Hinzufügen des Schlüssels zu GitLab

Navigiert zu „Preferences (User Settings) | SSH Keys„. Hier könnt ihr den Schlüssel direkt in das Formular eintragen. Wählt dann entweder „Authentication & Signing“ oder nur „Signing“ im „Usage type“ Dropdown aus:

Screenshot des "Add an SSH key Formulars zum Hinzufügen des Public-Keys.

Standardmäßig hat ein Schlüssel bei GitLab ein Verfallsdatum, das ihr aber auch leer lassen könnt. Ihr könnt das Datum aber verlängern, indem ihr den Schlüssel entfernt und neu hinzufügt. Da SSH-Keys normalerweise kein Verfallsdatum haben (im Gegensatz zu GPG-Keys), könnt ihr die Einstellung ja mal testen.

Schritt 4: Prüfen, ob Commits signiert sind

Wir haben ja bereits auf der Kommandozeile im Git-Log überprüft, dass die Signierung funktioniert. In GitHub und GitLab könnt ihr die signierten Commits in fast allen Ansichten direkt neben den Commit-Hashes sehen. In der Commit-Liste auf GitHub könnte das wie folgt aussehen:

Screenshot eines "Verified" Commit bei GitHub.

In der Commit-Liste von GitLab sieht es hingegen wie folgt aus:

Screenshot eines "Verified" Commit bei GitLab.

Der „SSH key fingerprint“ ist hierbei der gleiche, den wir auch schon im Git-Log auf der Kommandozeile gesehen haben: /qGkPHs1/58u7jZgX95+hr5PNFs7gXswbkcRdfZuMWk.

Fazit

Für eine lange Zeit waren es GPG-Keys, die zum Signieren von Commits verwenden wurden. Aber da GPG im Setup und der Verwaltung sehr komplex sein kann, haben viele ihre Commits gar nicht erst signiert. Wenn ihr also GPG nicht ohnehin verwendet – um beispielsweise E-Mails zu signieren und/oder zu verschlüsseln – dann könnt ihr einfach SSH-Keys verwenden, um eure Commits zu signieren. Ihr könnt auch beides für verschiedene Projekte verwenden. In diesem Fall würde ich euch empfehlen, noch einmal meinen Beitrag zu unterschiedlichen Git-Konfigurationen zu lesen und die Einstellungen dann nicht in der globalen Konfigurationsdatei vorzunehmen, sondern in den eingebundenen Dateien.

Marker auf einer Leaflet Karte nach Bundesländern clustern

Wie ich in meinem letzten Blogbeitrag erwähnt habe, gibt es noch ein kleines Bonus-Thema in der Serie zu individuellen Karten. Im vorherigen Beitrag haben wir eine Karte mit Leaflet in JavaScript erstellt. Darauf wurden die Landeshauptstädte visualisiert. Zwei davon liegen recht habe zusammen und man kann sie nur schwer anklicken. Aber wie sähe eine Karte mit allen Großstädten (ca. 100.00 und größer) aus:

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Vor allem in Nordrhein-Westfalen ist es nun unmöglich alle Marker zu sehen, zu hovern und anzuklicken. Aber da wir Leaflet verwenden, gibt es hierfür eine Lösung:

Verwendung von Marker-Clustern

Es gibt eine Erweiterung von Leaflet mit dem Namen Leaflet.markercluster, die wir verwenden können, um die Marker zu clustern. Hierzu müssen wir zuerst ein paar weiter CSS und JavaScript-Dateien einbinden:

<link rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
      integrity="sha256-YU3qCpj/P06tdPBJGPax0bm6Q1wltfwjsho5TR4+TYc="
      crossorigin=""/>
<link rel="stylesheet"
      href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
      integrity="sha256-YSWCMtmNZNwqex4CEw1nQhvFub2lmU7vcCKP+XVwwXA="
      crossorigin=""/>

<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"
        integrity="sha256-Hk4dIpcqOSb0hZjgyvFOP+cEmDXUKKNE/tT542ZbNQg="
        crossorigin=""></script>

Jetzt können wir eine markerClusterGroup erstellen und die Marker hinzufügen. Am Ende müssen wir die Gruppe dann noch zur Karte hinzufügen:

const markers = L.markerClusterGroup();

cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    markers.addLayer( marker );
} );

markers.addTo( map );

Standardmäßig werden Marker in einem max. Radius von 80 Pixeln zu einem Cluster hinzugefügt. Für die Deutschlandkarte würde das dann wie folgt aussehen:

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Das mag zwar die effektivste Clusterung sein, aber es sieht nicht wirklich schön aus, da wir in manchen Bundesländern mehrere Cluster haben, in anderen aber keine. Daher möchte ich ein Cluster pro Bundesland haben.

Marker nach Bundesländern clustern

Wir können mehrere markerClusterGroup Objekte zu einer Karte hinzufügen. Wir erstellen also 16 Gruppen für die 16 Bundesländer und fügen dann die Marker der jeweils korrekten Gruppe hinzu. Sehen wir uns zunächst einmal den Code dafür an:

const stateMarkers = {};

// Get a unique list of all states from the cities array.
[ ...new Set( cities.map( city => city.state ) ) ].map( state => {
    // Create a markerClusterGroup per state.
    stateMarkers[state] = L.markerClusterGroup( {
        maxClusterRadius: 1000,
        spiderfyOnMaxZoom: false,
        showCoverageOnHover: false,
        disableClusteringAtZoom: 8,
    } );
} );

// Create city markers and add them to the correct markerClusterGroup.
cities.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    stateMarkers[city.state].addLayer( marker );
} );

// Add all markerClusterGroups to the map.
Object.keys( stateMarkers ).map( state => {
    stateMarkers[state].addTo( map );
} );

Zuerst einmal erstellen wir ein Objekt für die Gruppen aller Bundesländer. Dann verwenden wir etwas JavaScript-Magie, um eine Liste mit allen Bundesländernamen aus dem cities Objekt zu bekommen und erstellen dann jeweils eine Gruppe. Für die Cluster-Gruppen setzen wir den maxClusterRadius auf 100 Pixel, da unsere Karte kleiner als 1000 Pixel ist und somit jede Cluster-Gruppe mindestens so groß ist, wie jedes Bundesland. Wir deaktivieren dann noch zwei Optionen, die wir nicht brauchen und begrenzen das Zoom-Level auf 8. Bei diesem Zoom-Level werden dann alle Cluster aufgelöst, sobald ein Cluster angeklickt wird.

Nachdem wir unsere Gruppen haben, erstellen wir die Marker und fügen sie den Gruppen hinzu. Am Ende fügen wir dann noch alle Gruppen zu Karte hinzu. Damit erhalten wir dann das folgende Ergebnis (Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Manche Bundesländer haben nur einen Marker. In diesem Fall werden sie einfach angezeigt. Alle andern haben Cluster. Für Nordrhein-Westfalen haben wir ein Cluster mit 30 Markern. Klicken wir darauf, erhalten wir die folgende gezoomte Ansicht (Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, gezoomt, CC BY-NC 4.0

Falls eure Karte mehr Marker hat oder diese näher zusammen sind, müsste ihr die Optionen für die Cluster-Gruppen eventuell etwas anders einstellen. Für dieses Beispiel würde ich sagen, dass sie nun anklickbar sind.

Fazit

Das sollte der letzte Beitrag in meiner kleinen Karten-Reihe gewesen sein. Es gibt sicher noch dutzende Themen rund um Karten im Allgemeinen oder zu Leaflet, aber dazu gibt es andere Blogs oder Dokumentationen.

Ich hoffe, dass ihr nun auch Lust habt, mal eigene (dynamische) Karten zu erstellen. Und auch die Beispiele aus diesem Beitrag findet ihr wieder in einer einzelnen HTML-Datei in einem neuen Branch auf GitHub.

Falls ihr noch weiter Themen habt, die ich behandeln soll, hiterlasst bitte einen Kommentar.

Verwende Marker auf deinem eigenen Bild mit Leaflet

In den letzten beiden Blogbeiträgen haben wir PHP-Code zum Geocoding geschrieben. Diese Woche möchte ich euch zeigen, wie ihr etwas ähnliches Leaflet umsetzen könnt, einer bekannten JavaScript-Bibliothek zum Erstellen von Karten. Da es auch eine Funktion zum Hinzufügen eigener Bild-Layer hat, werden wir dies nutzen, um erneut unsere Karte zu erstellen.

Mit Leaflet loslegen

Wenn man Leaflet verwendet, dann muss man erst einmal ein wenig (externes) CSS und JavaScript laden. Ihr könnt mehr dazu im Quick Start Guide nachlesen, aber im Grunde braucht ihr die folgenden Dinge:

 <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
     integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
     crossorigin=""/>

 <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
     integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
     crossorigin=""></script>

<div id="leaflet-map"></div>

Neben dem CSS und JS braucht ihr noch einen Container, den die Map verwenden wird. Ihr könnt auch ein paar Styles angeben, aber dazu kommen wir gleich.

Die Karte aufsetzen

Wie auch in unseren vorherigen Beispielen werden wir erneut eine Deutschlandkarte verwenden, mit einer spezifischen Größe und Grenzen. Diese müssen wir auch bei Leaflet angeben:

// Create the map object.
const map = L.map( 'leaflet-map', {
    center: [ 51.1642, 10.4541 ],
    maxZoom: 10,
    zoomDelta: 1,
    zoomSnap: 0,
    scrollWheelZoom: false,
    trackResize: true,
    attributionControl: false,
} );

// Define the image overlay and its boundaries.L.marker([52.5162746,13.3777041]).addTo(map);
const imageUrl = './ammap-germany-low.svg';
const imageBounds = [
    [ 55.051693, 5.864765 ],
    [ 47.269299, 15.043380 ],
];

// Add the overlay to the map.
L.imageOverlay( imageUrl, imageBounds ).addTo( map );

// Automatically zoom the map to the boundaries.
map.fitBounds( imageBounds );

Zuerst erstellen wir das map Objekt mit einer Basis-Konfiguration. Dann definieren wir die Bildquelle und die Grenzen als GPS-Koordinaten, bevor wir sie schließlich zur Karte hinzufügen. Im letzten Schritt stellen wir sicher, dass das Bild in den Container eingepasst wird.

Der Container sollte eine gute Größe haben, um die Karte darzustellen und auch deren Seitenverhältnis. Wir verwenden einfach die SVG-Größe in unserem CSS:

#leaflet-map {
    width: 585.506px;
    height: 791.999px;
    background: white;
    max-width: 100%;
    max-height: 100%;
}

Es muss nicht so exakt angegeben werden und ihr könnt auch einen Container wählen, der größer/kleiner als das SVG ist.

Hinzufügen der Marker zur Karte

Nachdem wir nun die Karte haben, stellt sich die Frage, wie wir die Marker hinzufügen. Nun, da wir eine Karten-Bibliothek verwenden, ist das sehr einfach. Für einen Marker auf das Brandenburger Tor brauchen wir nur eine Zeile:

L.marker([52.5162746,13.3777041]).addTo(map);

Das ist wirklich alles! Keine Transformation von GPS zu Pixel-Koordinaten mehr. Das wird alles von Leaflet erledigt. Wir bekommen dann das folgende Ergebnis (nur ein Screenshot):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Der einzige Nachteil ist, dass Leaflet standardmäßig nur die EPSG:3857 Projektion (Web Mercator) unterstützt. Wenn ihr eine andere Projektion benötigt, dann gibt es aber eine PROJ Erweiterung: Proj4Leaflet.

Hinzufügen aller Landeshauptstädte zur Deutschlandkarte

Vermutlich möchtet ihr auf eurer Karte nicht nur einen einzelnen Marker darstellen. Fügen wir also ein paar mehr Marker hinzu. Ich habe hierzu eine Liste aller 16 Landeshauptstädte gemacht:

var capitals = [
    {
        'state': 'BB', 'name': 'Potsdam', 
        'lat': 52.4009309, 'lng': 13.0591397,
        'url': 'https://de.wikipedia.org/wiki/Potsdam'
    },
    {
        'state': 'BE', 'name': 'Berlin', 
        'lat': 52.5170365, 'lng': 13.3888599,
        'url': 'https://de.wikipedia.org/wiki/Berlin'
    },
    {
        'state': 'BW', 'name': 'Stuttgart', 
        'lat': 48.7784485, 'lng': 9.1800132,
        'url': 'https://de.wikipedia.org/wiki/Stuttgart'
    },
    {
        'state': 'BY', 'name': 'München', 
        'lat': 48.1371079, 'lng': 11.5753822,
        'url': 'https://de.wikipedia.org/wiki/München'
    },
    {
        'state': 'HB', 'name': 'Bremen', 
        'lat': 53.0758196, 'lng': 8.8071646,
        'url': 'https://de.wikipedia.org/wiki/Bremen'
    },
    {
        'state': 'HE', 'name': 'Wiesbaden', 
        'lat': 50.0820384, 'lng': 8.2416556,
        'url': 'https://de.wikipedia.org/wiki/Wiesbaden'
    },
    {
        'state': 'HH', 'name': 'Hamburg', 
        'lat': 53.550341, 'lng': 10.000654,
        'url': 'https://de.wikipedia.org/wiki/Hamburg'
    },
    {
        'state': 'MV', 'name': 'Schwerin', 
        'lat': 53.6288297, 'lng': 11.4148038,
        'url': 'https://de.wikipedia.org/wiki/Schwerin'
    },
    {
        'state': 'NI', 'name': 'Hannover', 
        'lat': 52.3744779, 'lng': 52.3744779,
        'url': 'https://de.wikipedia.org/wiki/Hannover'
    },
    {
        'state': 'NW', 'name': 'Düsseldorf', 
        'lat': 51.2254018, 'lng': 6.7763137,
        'url': 'https://de.wikipedia.org/wiki/Düsseldorf'
    },
    {
        'state': 'RP', 'name': 'Mainz', 
        'lat': 50.0012314, 'lng': 8.2762513,
        'url': 'https://de.wikipedia.org/wiki/Mainz'
    },
    {
        'state': 'SH', 'name': 'Kiel', 
        'lat': 54.3227085, 'lng': 10.135555,
        'url': 'https://de.wikipedia.org/wiki/Kiel'
    },
    {
        'state': 'SL', 'name': 'Saarbrücken', 
        'lat': 49.234362, 'lng': 6.996379,
        'url': 'https://de.wikipedia.org/wiki/Saarbrücken'
    },
    {
        'state': 'SN', 'name': 'Dresden', 
        'lat': 51.0493286, 'lng': 13.7381437,
        'url': 'https://de.wikipedia.org/wiki/Dresden'
    },
    {
        'state': 'ST', 'name': 'Magdeburg', 
        'lat': 52.1315889, 'lng': 11.6399609,
        'url': 'https://de.wikipedia.org/wiki/Magdeburg'
    },
    {
        'state': 'TH', 'name': 'Erfurt', 
        'lat': 50.9777974, 'lng': 11.0287364,
        'url': 'https://de.wikipedia.org/wiki/Erfurt'
    },
];

Ähnlich zum Beispiel aus den anderen beiden Blogbeiträgen möchten wir diese Marker auch verlinken. Ich habe hierzu einfach die Links zur deutschen Wikipedia verwendet. Ein Marker kann verschiedene Event-Handler bekommen. Wir verwenden hier den onclick Handler. Das Hinzufügen aller Marker wird dann mit diesem Code umgesetzt:

capitals.map( city => {
    let marker = L.marker( [ city.lat, city.lng ], { title: city.name } );
    marker.on( 'click', function () {
        window.location = city.url;
    } );
    marker.addTo( map );
} );

Das liefert uns das folgende Ergebnis (beim Hover über einen Marker erscheint zusätzlich der Name der Stadt als Titel):

© ammap.com | SVG map of Germany (low detail), verwendet mit Leaflet, verändert durch Bernhard Kau, CC BY-NC 4.0

Standardmäßig können wir in die Karte reinzoomen, aber auch raus. Das ist für ein Bild-Overlay in der Regel nicht optimal. Wenn wir rein und wieder rauszoomen, wollen wir die Karte wieder im Container zentrieren. Beides kann mit folgendem Code erreicht werden, den ich bei den Karten verwendet habe:

// Set the current min zoom to the zoom level after init.
map.setMinZoom( map.getZoom() );

// Re-center the map when zoomed to minZoom level.
map.on( 'zoomend', function () {
    if ( map.getZoom() === map.getMinZoom() ) {
        map.fitBounds( imageBounds );
    }
} );

Fazit

In den letzten drei Blogbeiträgen habt ihr gelernt, wie ihr mit euren eigenen Bildern individuelle Karten erstellen könnt. Wenn ihr nur eine statische Map braucht, dann ist der PHP-Ansatz wohl der beste. Wenn ihr allerdings eine interaktive Karte, ähnlich wie Google Maps (mit Zoomen, etc.) benötigt, dann wollt ihr vielleicht eher Leaflet verwenden.

Falls ihr auch diesen Code wieder selbst testen wollte, dann findet ihr eine lauffähige Version in einem neuen Branch auf GitHub. Ich habe dort alles in einer einzelnen HTML-Datei kombiniert, ihr könnte es aber natürlich auch auf mehrere Dateien aufteilen, z.B. in einem WordPress Theme/Plugin.

Ich habe noch eine Idee für einen „Bonus-Blogbeitrag“ in dieser Serie, aber vielleicht gibt es noch ein weiteres Thema zu interaktiven und individuellen Karten. ☺️

Marker zu einem Satellitenbild hinzufügen

In meinem vorherigen Blogbeitrag habe ich euch gezeigt, wie ihr einen Marker für eine GPS Koordinate zu einer SVG-Bild hinzufügen könnt. In diesem Falle musstet ihr die x/y-Positionen berechnen und konntet dann einen SVG-Kreis oder einen Marker-Pfad zeichnen. Aber was machen wir, wenn wir ein Satellitenbild haben – oder eine andere Pixelgrafik?

Lösung: verwendet auch hier ein SVG 😉

OK, nicht einfach irgendein SVG. Die Technik, die wir hier verwenden werden, könnte man als „moderne Image-Map“ bezeichnen. Diejenigen von euch, die schon länger Websites bauen, werden vermutlich noch den <map> Element kennen, mit dessen Hilfe man eine <area> auf einem <img> anklickbar machen konnte. Die Formen waren dabei aber recht eingeschränkt und bei weitem nicht so flexibel und präzise, wie es mit SVG-Pfaden möglich ist. Statt also eine traditionelle Image-Map zu verwenden, setzen wir ein (leeres) SVG ein, welches wir dann über das Bild legen, um eine „anklickbare Karte“ zu erstellen. Nun, genau genommen erstellen wir nur ein anklickbares SVG, das als Overlay über dem eigentlichen Bild liegt, aber es erzielt genau das Ergebnis, das wir haben wollen. Für diesem Blogbeitrag habe ich folgendes Satellitenbild von Berlin ausgesucht:

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1CC BY 2.0

Die Bildgrenzen berechnen

Wie ich bereits im vorherigen Beitrag erwähnt habe, müsst ihr erst einmal die Grenzen der Karte herausfinden, auf die ihr dann die Marker platzieren möchtet. Wir fangen mit den Grenzen für das Bild an, was der Größe der Pixelgrafik entspricht:

// Init PixelGeocoder using WGS84 and Mercato projection.
$pixel_geocoder = new PixelGeocoder( 'EPSG:4326', 'EPSG:3857' );
// Set boundaries for the map.
$pixel_geocoder->image_boundaries = [
	'xmin' => 0,
	'xmax' => 2400,
	'ymin' => 0,
	'ymax' => 1800,
];

Jetzt müssen wir die GPS-Grenzen des Bildes finden. Diese haben wir für das Bild natürlich nicht. Um sie zu erhalten, habe ich mir einige markante Punkte am Rand der Karte gesucht, die ich dann auf Google Maps finden konnte. So habe ich die folgenden GPS-Koordinaten bekommen, die ich dann als Referenzpunkte verwenden konnte:

$map_edges = [
	[ 13.0467623, 52.5594922 ], // West.
	[ 13.1993623, 52.6484712 ], // North.
	[ 13.5841963, 52.4416892 ], // East.
	[ 13.2766553, 52.4069153 ], // South.
];

$pixel_geocoder->setDstBoundaries(
	$map_edges,
	false,
	true
);

Nachdem wir nun die Grenzen definiert haben, können wir erneut die Koordinaten für das Brandenburger Tor berechnen:

// Calculate the coordinates.
$bb_gate_lat     = 13.3777041;
$bb_gate_lng     = 52.5162746;
$bb_gate_dst_arr = $pixel_geocoder->transformGPStoMapProjection( $bb_gate_lat, $bb_gate_lng );
$bb_gate_coords  = $pixel_geocoder->calculateCoordinatesToPixel( $bb_gate_dst_arr[0], $bb_gate_dst_arr[1] );

var_dump( bb_gate_coords );
/**
 * array(2) {
 *   [0]=>
 *   float(1477.8750879177708)
 *   [1]=>
 *   float(986.3143837577029)
 * }
 */

Aber wie bekommen wir das nun als Marker in einem Overlay über unser Satellitenbild? Hier kommt nun unsere SVG-Image-Map ins Spiel.

Erstellen einer SVG-Image-Map

Eine SVG-Imag-Map ist im Grunde eine leere „SVG-Leinwand“, auf die wir Dinge platzieren können. Wir definieren also ein einzelnes SVG Element mit der gleichen Höhe und Breite wie unser Satellitenbild. Das Bild selbst setzen wir als Geschwister-Element daneben und umschließen beides mit einem Container-Element und setzen ein paar Styles:

<div class="image-map">
	<img class="image-map-background" src="./Berlin-Germany-Flickr-NASA-Goddard-Photo-and-Video1.jpg" alt="Berlin NASA image"/>
	<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800"></svg>
</div>

Um das SVG über dem Bild zu platzieren, definieren wir die Höhe und Breite für den Container (dieser sollte das gleiche „Seitenverhältnis“ wie das Bild haben):

.image-map {
	position: relative;
	width: 600px;
	height: 450px;
}

.image-map-background,
.dynamic-map {
	max-width: 100%;
	height: auto;
}

.dynamic-map {
	position: absolute;
	top: 0;
	left: 0;
}

Jetzt können wir unsere Marker hinzufügen. Ich hole mir dazu meistens eine Liste von Markern, entweder aus einem statischen Array oder aber mit einer WP_Query und einigen Meta-Feldern. Nehmen wir für unser Beispiel einfach ein statisches Array mit dem bereits berechneten Marker:

$markers = [
	[
		'name'  => 'brandenburg-gate',
		'title' => 'Brandenburg Gate',
		'x'	 => $bb_gate_coords[0],
		'y'	 => $bb_gate_coords[1],
		'url'   => 'https://en.wikipedia.org/wiki/Brandenburg_Gate',
	]
];

Da wir vorhaben, die SVG-Pfade und Links auszugeben, können wir hier ein kleines Marker-Template schreiben, das uns die Ausgabe etwas erleichtert:

$marker_markup = '
	<a xlink:title="%1$s" target="_parent" class="marker" id="%2$s" xlink:href="/%3$s/" transform="translate(%4$s,%5$s)">
		<path fill="#c10926" fill-rule="evenodd" d="m -0.266,-28.261 a 4.504,4.504 0 0 0 3.204,-1.343 4.613,4.613 0 0 0 1.327,-3.242 4.615,4.615 0 0 0 -1.327,-3.244 4.508,4.508 0 0 0 -3.204,-1.343 4.512,4.512 0 0 0 -3.206,1.343 4.619,4.619 0 0 0 -1.327,3.244 c 0,1.215 0.478,2.382 1.327,3.242 a 4.51,4.51 0 0 0 3.206,1.343 m -0.613,27.98 -8.895,-28.49 h 0.013 a 10.555,10.555 0 0 1 -0.818,-4.074 c 0,-2.77 1.086,-5.425 3.02,-7.381 a 10.251,10.251 0 0 1 7.294,-3.056 c 2.735,0 5.358,1.099 7.293,3.056 a 10.502,10.502 0 0 1 3.021,7.38 c 0,1.414 -0.284,2.798 -0.819,4.076 h 0.012 z" clip-rule="evenodd"/>
	</a>';

Fügen wir das ganze jetzt in einer Schleife innerhalb unseres SVG zusammen:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="dynamic-map" width="2400" height="1800" viewBox="0 0 2400 1800">
	<?php foreach ( $markers as $marker ) : ?>
		<?php
		printf(
			$marker_markup,
			$marker['title'],
			$marker['name'],
			$marker['url'],
			$marker['x'],
			$marker['y']
		);
		?>
	<?php endforeach; ?>
</svg>

Durch die printf() Funktion ist können wir die dynamischen Teile ins Template einfügen. Wenn ihr das in WordPress macht, dann stellt bitte sicher, dass ihr die esc_*() Escape-Funktionen für dynamische Werte verwendet.

Ich habe den gleichen Ansatz auch gewählt, um die „Karten-Grenzpunkte“ mit einem Cyan-gefärbten Kreis zu markieren. Das Resultat sieht dann wie folgt aus (dies hier ist nur ein Screenshot des Resultats):

NASA Goddard Space Flight Center from Greenbelt, MD, USA, Berlin, Germany – Flickr – NASA Goddard Photo and Video1, bearbeitet von Bernhard Kau, CC BY 2.0

Da die Marker recht klein waren, habe ich diese etwas hochskaliert. Da sich der Marker an der Spitze skaliert, kann das mit einer einzelnen Zeile CSS erreicht werden:

.marker path {
	transform: scale(5);
}

Fazit

In einer sehr ähnlichen Art und Weise haben wir ein SVG-Bild erstellt, das anklickbare Marker auf ein Satellitenbild legt. Für das Bild einer Stadt reicht hier wohl ein normales Satellitenbild aus. Für eine größere Region müsst ihr aber eines in der Mercator-Projektion (oder einer anderen von PROJ unterstützten Projektion) finden. Ich habe einige Zeit gebraucht, um ein geeignetes CC-lizensiertes Bild zu finden, das ich als Beispiel für diesen Blogbeitrag verwenden konnte.

Falls ihr auch diesen Code einmal selbst ausprobieren wollt, dann findet ihr einen neuen Branch mit den verschiedenen Teilen in einer kombinierten neuen PHP-Datei auf GitHub. Dort findet ihr auch die verwendeten Satellitenbilder.

Ich hoffe, dass euch dieser Beitrag einen weiteren schönen Weg zeigen konnte, mit dem ihr individuelle Karten erstellen könnt. Wir haben aber recht viel eigenen PHP-Code und eine externe Bibliothek verwenden. Im nächsten (und vermutlich auch letzten) Beitrag dieser Karten-Reihe, möchte ich euch eine Bibliothek vorstellen, die ich vielleicht schon verwendet habt, aber noch nicht in dieser Weise. Bleibt also weiter dabei, denn das Jahr mag bald zu Ende gehen, aber die Kalenderwoche 52 hat noch einen Tag im Jahr 2023! 😁

Geo-Koordinaten zu Pixel-Positionen – Erstelle eine Karte mit deinen eigenen Bildern

Dieser Beitrag soll der Start einer kleinen Beitragsreihe sein. Für ein Projekt musste ich Marker von Städten auf einer Deutschlandkarte platzieren. Am Anfang habe ich das noch manuell gemacht, aber irgendwann ist die Karte auf über 100 Marker angestiegen und es musste eine programmatische Lösung her.

Kartenprojektionen

Auch wenn es einige Menschen gibt, die denken, dass die Erde flach ist, ist die rund. Also nicht wirklich perfekt rund. Da unsere Monitore aber flach sind (genau wie schon Papierkarten zuvor), gibt es verschiedene „Kartenprojektionen„. Eine der am weitesten verbreiteten ist die Mercator projection, welche unter anderem von Google Maps, Bing Maps, OpenStreetMap und vielen anderen Diensten verwendet wird. Es ist auch unter der Bezeichnung EPSG:3857 bekannt, auf die wir später zurückkommen werden.

GPS-Koordinaten

Wenn man eine Position auf der Erde angibt, verwendet man in der Regel das World Geodetic System 1984 (WGS 84) mit Längen- und Breitengraden für den Ort. Für das Brandenburger Tor in Berlin sind diese beispielsweise 52.5162746 (Breitengrad) und 13.3777041 (Längengrad). Vielleicht habt ihr diese Art von Koordinatenangaben schon einmal gesehen. Eventuell kennt ihr aber auch die Angabe von Grad, Minuten und Sekunden, wobei es dann 52° 30' 58.59" (Breitengrad) und 13° 22' 39.7338" (Längengrad) sind. Aber mit der ersten Darstellungsart kann man sehr viel einfacher Berechnungen durchführen.

SVG-Karten finden

Nachdem wir uns nun ein wenig mit der Theorie beschäftigt haben, können wir über Code sprechen. Zuerst einmal müssen wir eine Karte finde. Das kann alles Mögliche sein. Eine Luftaufnahme, ein Satellitenbild oder ein einfaches Bild. Ich verwende sehr gerne SVG Bilder. Diese findet man auf vielen verschiedenen Plattformen. Wikipedia ist hier oft eine gute Quelle für Karten und viele davon sind unter Creative Commons lizenziert. Wir können dort zum Beispiel eine schöne Karte von Deutschland inkl. der Bundesländergrenzen finden. Leider verwendet sie eine andere Kartenprojektion. Ich habe daher für diesen Blogbeitrag eine kostenlose SVG Karte von amCharts verwendet:

Map of Germany
© ammap.com | SVG map of Germany (low detail), Code optimiert von Bernhard Kau, CC BY-NC 4.0

Es gibt auch kommerzielle Anbieter und spezielle Stock-Foto-Shops für Vektorgrafiken. Aber egal welche ihr verwendet, stellt bitte sicher, dass ihr die Lizenz respektiert.

Marker geokodieren

Wir haben eine Map, fangen wir also an, die Marker zu setzen. Ihr werden von den Orten vermutlich nur die GPS-Koordinaten kennen. Diese müssen wir daher zuvor in „x/y Pixel-Koordinaten“ für die SVG-Karte konvertieren. Nach ein wenig Recherche bin ich auf die PROJ library gestoßen und glücklicherweise gibt es hier auch eine PHP Variante von PROJ. Diese installieren wir uns mit Composer:

composer require proj4php/proj4php

Jetzt müssen wir ein paar Initialisierungen durchführen. Wir konvertieren Werte zwischen zwei Projektionssystemen und erzeugen uns diese wie folgt:

require 'vendor/autoload.php';

// Initialize the PROJ library.
$proj = new Proj4php();

// Set the source and destination projection.
$src_projection = new Proj( 'EPSG:4326', $proj );
$dst_projection = new Proj( 'EPSG:3857', $proj );

Beim EPSG:4326 Projektionssystem, unsere Quellprojektion, handelt es sich um das WGS 84 System für GPS-Koordinaten. Bei EPSG:3857 hingegen handelt es ich um die „Sphärische Mercator“ Projektion, die von Diensten wie Google Maps, OpenStreetMap und eben auch unserer SVG verwendet wird. Die erste Projektion definiert also Koordinaten auf einem „Globus“ (einer sphärischen Karte) während die zweite Projektion Koordinaten auf einer flachen Karte (in einem von mehreren Projektionssystemen) definiert.

Nachdem wir dieses Basissetup abgeschlossen haben, können wir unsere erste Konvertierung durchführen. Wir nehmen die GPS von Berlin und konvertieren diese in Mercator Koordinaten:

$src_berlin = new Point( 13.3777041, 52.5162746, $src_projection );
print_r( $src_berlin->toArray() );
/**
 * Array
 * (
 *     [0] => 52.5162746
 *     [1] => 13.3777041
 *     [2] => 0
 * )
 */

$dst_berlin = $proj->transform( $dst_projection, $src_berlin );
print_r( $dst_berlin->toArray() );
/**
 * Array
 * (
 *     [0] => 1489199.2083951
 *     [1] => 6894018.2850289
 *     [2] => 0
 * )
 */

In Ordnung, jetzt haben wir ein paar neue Zahlen. Aber wie genau können wir diese nun nutzen? Wenn wir diese auf der SVG-Karte verwenden möchten, müssen wir sie noch in Pixel-Koordinaten des Bildes umrechnen.

Die Bildgrenzen finden

Wir brauchen jetzt die minimalen und maximalen Koordinaten auf der Karte. Wenn wir Glück haben, dann bekommen wir diese von der Seite, von der wir auch die Karte selbst bekommen haben. Leider liefert Wikipedia so etwas für seine Karten nicht mit. Ihr könnt sie sicher mit einer Suchmaschine herausfinden oder über eine offene API wie etwa die von OpenStreetMaps oder anderer Dienste herausfinden. Aber manchmal gibt es solche Daten nicht oder ihr wisst nicht, wo ihr sie finden könnt. Dann könnt ihr aber einfach versuchen, die Eckpunkte der Karte zu ermitteln. Für die Karte von Deutschland würde man also die Orte ganz im Norden, Osten, Süden und Westen suchen. Mit diesen Punkten können wir und dann die minimalen und maximalen Werte der Längen- und Breitengrade holen. Glücklicherweise liefert und aber die amCharts Karte diese Eckpunkte im Code der SVG direkt mit:

<amcharts:ammap projection="mercator" leftLongitude="5.864765" topLatitude="55.051693" rightLongitude="15.043380" bottomLatitude="47.269299"></amcharts:ammap>

Mit diesen beiden Referenzpunkten (die sich zwar nicht innerhalb der Landesgrenzen von Deutschland befinden, was aber kein Problem ist), können wir die Bildgrenzen wie folgt berechnen:

$swap_x = false;
$swap_y = true;

$dst_points_x = [];
$dst_points_y = [];

$src_points = [
	[ 15.043380, 47.269133 ],
	[ 5.865010, 55.057722 ],
];

foreach ( $src_points as $point ) {
	$src_point      = new Point( $point[0], $point[1], $src_projection );
	$dst_point      = $proj->transform( $dst_projection, $src_point );
	$dst_point_arr  = $dst_point->toArray();
	$dst_points_x[] = $dst_point_arr[0];
	$dst_points_y[] = $dst_point_arr[1];
}

$src_boundaries = [
	'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
	'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
	'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
	'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),
];

var_dump( $src_boundaries );
/**
 * array(4) {
 *   ["xmin"]=>
 *   float(653037.4250227585)
 *   ["xmax"]=>
 *   float(1668369.9214457471)
 *   ["ymin"]=>
 *   float(5986273.409259587)
 *   ["ymax"]=>
 *   float(7373214.063855921)
 * }
 */

Mit den beiden „swap“ Flags können wir angeben, dass sich die Koordinaten auf der anderen Seite des von 0° Länge oder 0° Breite befindet. Das Maximum würde dann zum Beispiel für die linke Bildgrenze verwendet und das Minimum für die rechte. Im Fall von Deutschland müssen wir $swap_y auf true setzen.

Jetzt können wir endlich mit den Grenzen die Koordinaten auf unserem Bild berechnen. Hierzu müssen wir noch zusätzlich die Größe des Bildes angeben. Die Berechnung sieht dann wie folgt aus:

$image_boundaries = [
	'xmin' => 0,
	'xmax' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,
];

$dst_berlin_arr = $dst_berlin->toArray();
$lng = $dst_berlin_arr[0];
$lat = $dst_berlin_arr[1];

$x_pos = ( $lng - $src_boundaries['xmin'] ) / ( $src_boundaries['xmax'] - $src_boundaries['xmin'] ) * ( $image_boundaries['xmax'] - $image_boundaries['xmin'] );

$y_pos = ( $lat - $src_boundaries['ymin'] ) / ( $src_boundaries['ymax'] - $src_boundaries['ymin'] ) * ( $image_boundaries['ymax'] - $image_boundaries['ymin'] );

var_dump( [ $x_pos, $y_pos ] );
/**
 * array(2) {
 *   [0]=>
 *   float(487.1242093149932)
 *   [1]=>
 *   float(523.9253760603847)
 * }
 */

Zuerst definieren wir also die Größe der SVG Deutschlandkarte in Pixeln. Dann benutzen wir den schon zuvor berechneten $dst_berlin Punkt für den Längen- und Breitengrad. Mit $src_boundaries['xmax'] - $src_boundaries['xmin'] können wir die „Breite“ der Quellenprojektion finden (genau wie auch für die Bildgrenzen). Wir ziehen dann die linke Grenze von unserem Koordinatenpunkt ab. Diesen dividieren wir durch die Breite der Quellprojektion und multiplizieren ihn mit der Breite der Zielprojektion (also der Breite des Bildes). Das Gleiche machen wir für die Y-Achse. Um den Punkt zu visualisieren, können wir einfach einen SVG-Kreis zeichnen. Das könnte im Code dann wie folgt aussehen:

<circle cx="482.18464676347816" cy="273.64009871474894" r="5" stroke="red" stroke-width="2" fill="transparent" />

Wenn wir diesen nun der ursprünglichen SVG-Karte hinzufügen, erhalten wir das Folgende:

Map of Germany with a circle on Berlin
© ammap.com | SVG map of Germany (low detail), Code optimiert und bearbeitet von Bernhard Kau, CC BY-NC 4.0

Alles zusammenfügen

Um mir diese ganzen Schritte zu erleichtern, habe ich mir eine kleine PixelGeocoder Klasse geschrieben, die die Initialisierung übernimmt und auch Hilfsfunktionen für die Berechnung der Grenzen sowie Methoden zum Berechnen der Koordinaten auf dem Bild enthält:

use proj4php\Proj4php;
use proj4php\Proj;
use proj4php\Point;

class PixelGeocoder {
	public $proj;
	public $src_proj;
	public $dst_proj;

	public $src_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,
	];

	public $image_boundaries = [
		'xmin' => 0,
		'xmax' => 0,
		'ymin' => 0,
		'ymax' => 0,
	];

	public function __construct( $src_proj_type = 'EPSG:4326', $dst_proj_type = 'EPSG:3857' ) {
		$this->proj     = new Proj4php();
		$this->src_proj = new Proj( $src_proj_type, $this->proj );
		$this->dst_proj = new Proj( $dst_proj_type, $this->proj );
	}

	public function setDstBoundaries( $points, $swap_x = false, $swap_y = false ) {
		$dst_points_x = [];
		$dst_points_y = [];

		foreach ( $points as $point ) {
			$dst_point      = $this->transformGPStoMapProjection( $point[0], $point[1] );
			$dst_points_x[] = $dst_point[0];
			$dst_points_y[] = $dst_point[1];
		}

		$this->src_boundaries = [
			'xmin' => $swap_x ? max( $dst_points_x ) : min( $dst_points_x ),
			'xmax' => $swap_x ? min( $dst_points_x ) : max( $dst_points_x ),
			'ymin' => $swap_y ? max( $dst_points_y ) : min( $dst_points_y ),
			'ymax' => $swap_y ? min( $dst_points_y ) : max( $dst_points_y ),
		];
	}

	public function transformGPStoMapProjection( $lng, $lat ) {
		$src_point = new Point( $lng, $lat, $this->src_proj );
		$dst_point = $this->proj->transform( $this->dst_proj, $src_point );

		return $dst_point->toArray();
	}

	public function calculateCoordinatesToPixel( $lng, $lat ) {
		return [
			( $lng - $this->src_boundaries['xmin'] ) / ( $this->src_boundaries['xmax'] - $this->src_boundaries['xmin'] ) * ( $this->image_boundaries['xmax'] - $this->image_boundaries['xmin'] ),
			( $lat - $this->src_boundaries['ymin'] ) / ( $this->src_boundaries['ymax'] - $this->src_boundaries['ymin'] ) * ( $this->image_boundaries['ymax'] - $this->image_boundaries['ymin'] ),
		];
	}
}

Wenn ihr diese Klasse nun verwenden wollt, geht das wie folgt:

require_once 'vendor/autoload.php';
require_once 'PixelGeocoder.php';

// Init PixelGeocoder using WGS84 and Mercato projection.
$pixel_geocoder = new PixelGeocoder( 'EPSG:4326', 'EPSG:3857' );
// Set boundaries for the map.
$pixel_geocoder->image_boundaries = [
	'xmin' => 0,
	'xmax' => 585.506,
	'ymin' => 0,
	'ymax' => 791.999,
];
$pixel_geocoder->setDstBoundaries(
	[
		[ 15.043380, 47.269133 ],
		[ 5.865010, 55.057722 ],
	],
	false,
	true
);

// Calculate the coordinates.
$berlin_lat     = 13.3777041;
$berlin_lng     = 52.5162746;
$dst_berlin_arr = $pixel_geocoder->transformGPStoMapProjection( $berlin_lat, $berlin_lng );
$image_coords   = $pixel_geocoder->calculateCoordinatesToPixel( $dst_berlin_arr[0], $dst_berlin_arr[1] );

var_dump( $image_coords );
/**
 * array(2) {
 *   [0]=>
 *   float(479.2493080704524)
 *   [1]=>
 *   float(273.55748351793665)
 * }
 */

Bonus: einen klickbaren Marker verwenden

Der Kreis ist einfach und schön, aber vermutlich wollt ihr eine Karte bauen, die Marker enthält, die dann auch anklickbar sind. Da wir bereits eine SVG verwenden, können wir uns einen solchen Pfad zeichnen und diesen dann mit dem transform="translate(x,y)" auf der Karte verschieben:

<a xlink:title="Link to berlin.de" target="_parent" xlink:href="https://berlin.de/" transform="translate(479.2493080704524,273.55748351793665)">
	<path fill="#c10926" fill-rule="evenodd" d="m -0.266,-28.261 a 4.504,4.504 0 0 0 3.204,-1.343 4.613,4.613 0 0 0 1.327,-3.242 4.615,4.615 0 0 0 -1.327,-3.244 4.508,4.508 0 0 0 -3.204,-1.343 4.512,4.512 0 0 0 -3.206,1.343 4.619,4.619 0 0 0 -1.327,3.244 c 0,1.215 0.478,2.382 1.327,3.242 a 4.51,4.51 0 0 0 3.206,1.343 m -0.613,27.98 -8.895,-28.49 h 0.013 a 10.555,10.555 0 0 1 -0.818,-4.074 c 0,-2.77 1.086,-5.425 3.02,-7.381 a 10.251,10.251 0 0 1 7.294,-3.056 c 2.735,0 5.358,1.099 7.293,3.056 a 10.502,10.502 0 0 1 3.021,7.38 c 0,1.414 -0.284,2.798 -0.819,4.076 h 0.012 z" clip-rule="evenodd"/>
</a>

Wenn wir diesen nun zu unserer Karte hinzufügen, dann sieht es wie folgt aus:

Map of Germany with a clickable marker on Berlin
© ammap.com | SVG map of Germany (low detail), Code optimiert und bearbeitet von Bernhard Kau, CC BY-NC 4.0

Fazit

Wir alle lieben Karten, richtig? Aber manchmal sehen eine Google Map oder OpenStreetMap Version nicht wirklich schön aus. Mit ein wenig eigenem Code und ein wenig Vorbereitung können schöne Karten auf Grundlage (unserer eigenen) Bilder erstellen. Ich habe das oben beschriebene für eine Deutschlandkarte gemacht, die ihre GPS-Koordinaten von einem Custom-Post-Type aus WordPress bekam und dann dynamisch das SVG-Markup der Städte in einem Shortcode (später in einem Server-Side-Rendered Block) generiert.

Falls ihr den Code einmal selbst testen wollt, findet ihr alles auf GitHub. Es ist ein „Proof of Concept“ und funktioniert, aber die PHP-Klasse könnte sicher eine bessere Architektur und vermutlich auch ein paar mehr Hilfsfunktionen gebrauchen. Ihr könnt den Code also gerne für eure Bedürfnisse anpassen.

In meinem nächsten Blogbeitrag werde ich euch zeigen, wie mein eine Pixelgrafik anstelle einer SVG-Karte verwenden kann, bleibt also weiter dabei, das Jahr ist noch nicht zu Ende! 😉

Fatale Fehler in WordPress mit PHP 8+ und einer fehlerhaften Übersetzung

Letzte Woche habe ich die Meldung bekommen, dass eine Seite kaputt war, die mich ein wenig überrascht hat. Ich habe hier den Fehler mal in einem Dummy-Plugin reproduziert, um euch zeigen zu können, was schiefgelaufen ist. Beim Aufruf der defekten Seite wurde ich mir die folgende Fehlermeldung präsentiert:

Fatal error: Uncaught Error: Missing format specifier at end of string
in /var/www/html/wp-content/plugins/broken-format-string/broken-format-string.php on line 15

Also habe ich mir den Code dazu angesehen und dort stand Folgendes:

printf(
	__( 'Publish date: %s', 'broken-format-string' ),
	date_i18n( get_option( 'date_format' ), get_post_datetime() )
);

Nichts wirklich spannendes. Nur ein Format-String mit einem Platzhalter für einen String, der mit dem Veröffentlichungsdatum im WordPress-Datumsformat ersetzt wird.

Fehlerhafte Übersetzung

Ich konnte also nicht wirklich nachvollziehen, was genau hier der Fehler war. Aber da Sprache der Seite nicht „English (US)“ war, habe ich mir mal die Übersetzungsfunktion angesehen. Hier wieder ein einfaches Beispiel mit einem ähnlichen Fehler:

#: broken-format-string.php:14
msgid "Publish date: %s"
msgstr "Veröffentlichungsdatum: %"

Die deutsche Übersetzung des „Publish date:“ ist korrekt, aber der Platzhalter wird hier nur ein % verwendet und nicht %s, was dann zu diesem Fehler führt. WordPress würde hier also den Original-String ins Deutsche übersetzen und an die printf() Funktion weitergeben, was dann zu dem „Fatal error“ führt.

Unterschiedliche Fehlerbehandlung in PHP 8+

Wenn ihr diesen Code in PHP 7.4 oder früher ausführt, dann bekommt ihr keinen fatalen Fehler. Ihr bekommt noch nicht einmal eine PHP Warning oder Notice. Es würde einfach der Platzhalter nicht korrekt ersetzt. Das % würde einfach durch einen leeren String ersetzt werden. Aber sobald ihr auf PHP 8+ aktualisiert, habt ihr eine defekte Seite.

Das war das erste Mal, dass ich wirklich ein Problem mit PHP 8+ auf einer WordPress-Website festgestellt habe. Vielleicht hatte ich einfach Göück, oder aber die Übersetzungen hatten die Platzhalter immer korrekt übersetzt.

Fazit

Ich habe häufiger mal Prüfungen auf PHP 8+ Kompatibilität mit PHPCompatibilityWP gemacht und bisher hat es immer funktioniert. Aber ich hätte nie erwartet, dass eine fehlerhafte Übersetzung mal einen fatalen Fehler auslösen würde. Wenn man Übersetzungen mti GlotPress macht (was auf translate.wordpress.org verwendet wird), dann wird einem eine Warnung angezeigt, wenn in einer Übersetzung ein Platzhalter fehlt. Aber Tools wie Poedit zeigen hier leider keine Warnung an. Wenn ihr also jemanden damit beauftragt euer Plugin/Theme in einer Sprache zu übersetzten, die ihr selbst nicht sprecht, dann prüft am besten sehr genau, ob alle Format-String korrekt sind.

Die „text-underline-offset“ und viele weitere weniger bekannte CSS-Eigenschaften für Links

Die Arbeit für eine große Agentur hat viele Vorteile. Einer davon ist es, dass man mit vielen Menschen zusammenarbeitet, die sehr viel mehr über sehr viele Dinge wissen. Ich lese daher oft Commits von anderen und schaue mir an, was sie so machen. In einem Commit habe ich eine CSS-Eigenschaft entdeckt, sie ich noch nie zuvor gesehen hatte: text-underline-offset 🤯

Links gestalten

In den frühen Tagen des World Wide Web waren alle Links unterstrichen. Mit dem Aufkommen von CSS und modernen Design wollten Leute dann aber andere Styles für Links haben. Die Farbe und Position der text-decoration: underline wurde aber von der Schrift und ihrer Farbe vorgegeben und daher wurde sie oft durch einen border-bottom ersetzt. Das hatte aber mehrere negative Folgen, nicht zuletzt in vielen Fällen eine Verschlechterung der Barrierefreiheit von Links. Mit modernem CSS gibt es nun aber viele verschiedene CSS-Eigenschaften, um Links zu stylen … und von vielen davon habe ich noch nie gehört:

Auf den verlinkten MDN Web Docs Seiten könnt ihr viele der oben erwähnten CSS-Eigenschaften direkt selbst ausprobieren. Ich könnte euch sicher auch viele Bespiele nennen, wie man diese einsetzen kann und wie es dann aussehen würde, aber ich möchte spoilern und euch den Spaß nehmen, es selbst herauszufinden 😉

Fazit

Bei der Entwicklung von Websites gibt es eine unumstößliche Wahrheit: Du kannst nicht alles kennen. Daher kann ich es nur allen empfehlen, den Code von anderen zu lesen und sich von deren Arbeit inspirieren zu lassen. Diese CSS-Eigenschaften mögen für eine von euch bereits bekannt sein, für mich waren die überragend!

Erstellen eines dynamischen iCalendar mit Blog-Beiträgen

Eine kleine lokale Website verwendet ein Terminbuchungs-Plugin. Diese Buchungen wurden anschließend manuell in einen Online-Kalender übertragen, damit das Team einen Überblick über die Buchungen hatte. Leider unterstützte das Plugin keine Funktion, mit der die Buchungen dynamisch in einer Kalender-App angezeigt werden konnten. Daher wurde ich gefragt, ob ich hier helfen kann. Um das Gezeigte aber für euch aber etwas nützlicher zu machen, werde ich nicht zeigen, die ich das für dieses spezielle Terminbuchungs-Plugin umgesetzt habe, sondern wie man den gleichen Ansatz verwenden kann, um alle eure veröffentlichten und geplanten Beiträge anzuzeigen.

Installieren der Abhängigkeiten

Wir werden eine dynamische .ical Datei erstellen. Das ist eine Textdatei und wir könnten den „Code“ einfach selbst erzeugen, aber wir verwenden eine kleine Bibliothek, die uns dabei helfen wird. Für das Projekt habe ich die spatie/icalendar-generator Library verwendet, welche alles hatte, was ich brauchte (und noch vieles mehr). Wir installieren sie einfach mit Composer in unseren Plugin-Ordner:

composer require spatie/icalendar-generator

Anschließend müssen wir die notwendigen Dateien laden. Der einfachste Weg, das mit Paketen zu tun, die mit Composer installiert wurden, ist die Verwendung des Composer-Autoloaders. Hierzu fügen wir diesen der PHP-Datei des Plugins hinzu:

require_once 'vendor/autoload.php';

Erstellung eines Kalenders

Jetzt können wir damit loslegen, einen dynamischen Kalender zu erstellen. Ich werde hier sehr einfache Codebeispiele verwenden, in der Dokumentation des Pakets findet ihr aber auch einige komplexere. Erstellen wir also erst einmal das $calender Objekt:

$calendar = Calendar::create( 'example.com' );

Der Parameter der create() Funktion wird für den Titel verwendet, er kann aber auch leer gelassen werden. Nachdem wir nun unser $calender Objekt haben, würden wir uns die Daten für die Events holen. Hier führen wir lediglich eine Query aus, die uns die letzten veröffentlichten und geplanten Blog-Beiträge holt:

// Get all blog posts.
$query_args = [
	'post_type'      => 'post',
	'post_status'    => [
		'publish',
		'future',
	],
	'posts_per_page' => - 1,
];

$posts = get_posts( $query_args );

Nachdem wir alle Beiträge geladen haben, können wir für jeden davon ein individuelles Event erstellen:

// Create an event per blog post.
foreach ( $posts as $post ) {
	$start_date = new DateTime( $post->post_date_gmt, new DateTimeZone( 'UTC' ) );
	$end_date   = ( clone $start_date )->add( new DateInterval( 'PT15M' ) );

	$event = Event::create();
	$event->name( $post->post_title );
	$event->startsAt( $start_date );
	$event->endsAt( $end_date );
	$event->uniqueIdentifier( $post->ID );

	$calendar->event( $event );
}

Wir verwenden hier GMT/UTC Zeiten, um es dem Kalender zu ermöglichen, sich an die lokale Zeitzone anzupassen. Für das „Enddatum“ addieren wir 15 Minuten dazu. Durch die Verwendung der Post-ID als „unique identifier“ kann eine Kalender-Applikation einfacher einzelne Events aktualisieren/synchronisieren. Zum Schluss fügen wir dann noch das Event zum zuvor erstellten $calendar Objekt hinzu.

Im letzten Schritt müssen wir dann noch sie Ausgabe senden. Das wird mit folgenden Zeilen erreicht:

// Print the calendar output.
header( 'Content-Type: text/calendar; charset=utf-8' );
echo $calendar->get();
exit;

Importieren des iCalendar in eine Kalender-Anwendung

Den fertigen dynamischen Kalender wollen wir jetzt vermutlich in eine Kalender-App importieren (und synchronisieren). Dazu muss er über eine URL aufrufbar sein, die wir in der Anwendung eintragen müssen. Ich habe mich dazu entschieden, hierfür einen eigenen REST-Endpoint zu erstellen. Den bisherigen Code habe ich also in eine Callback-Funktion verschoben und dann den Endpoint wie folgt registriert:

function blog_posts_calendar_register_rest_route() {
	register_rest_route(
		'blog-posts-calendar/v1',
		'/ical.ics',
		[
			'methods'             => 'GET',
			'callback'            => 'blog_posts_calendar_generate_ical',
			'permission_callback' => '__return_true',
		]
	);
}
add_action( 'rest_api_init', 'blog_posts_calendar_register_rest_route' );

Der Kalender kann dann über folgende URL aufgerufen werden: https://example.com/wp-json/blog-posts-calendar/v1/ical.ics

Der permission_callback würde es hier allen erlauben, den Kalender zu abonnieren. Wenn ihr etwas restriktiver sein wollt, könnt ihr hier eure eigene Callback-Funktion implementieren.

Fazit

Selbst wenn ein Plugin eine Funktion mal nicht anbieten, kann man meistens etwas eigenen Code schreiben, um es verfügbar zu machen. Das ist oft auch einfacher als das ganze Plugin durch ein anderes zu ersetzen, was dann vielleicht eine andere Funktion nicht mehr hat. Im Falle eines Terminbuchungs-Plugins kann es vermutlich besonders schwer sein, dies durch ein anderes zu ersetzen, da man auch alle Daten zum andern Plugin migrieren muss.

Wenn ihr den Code mal selbst mit eurer Website testen wollt, dann findet ihr in als Plugin auf GitHub.

Verwendung von unterschiedlichen Git-Einstellungen für persönliche und berufliche Projekte

Wenn ihr für eine Agentur oder selbstständig tätig seid, gleichzeitig aber auch persönliche Projekte habt, dann möchtet ihr vielleicht unterschiedliche Git-Konfigurationen für diese beiden Arten von Projekten verwenden. Wenn ihr euren Rechner aufsetzt, um an Projekten zu arbeiten, dann konfiguriert ihr einige grundlegende Tools wie Git und GitHub. Diese Einstellungen beinhalten in der Regel die Git-Autor E-Mail-Adresse und eventuell auch einen zugehörigen SSH/GPG-Key für die Signierung von Commits. Man kann normalerweise nur eine globale E-Mail-Adresse definieren, ihr verwendet hier dann also vermutlich die professionelle E-Mail-Adresse (des Unternehmens). Wenn ihr aber auch an privaten oder Open-Source-Projekten arbeiten wollt, dann bevorzugt ihr hier vielleicht eher eure persönliche GitHub E-Mail-Adresse. Diese Anleitung soll euch dabei helfen, das einzustellen.

Vorbereitung

In dieser Anleitung gehen wir davon aus, dass ihr PhpStorm verwendet und all eure Projekte, an denen ihr arbeitet, im Ordner ~/PhpstormProjects gespeichert sind. Derselbe Ansatz funktioniert aber natürlich auch mit einer anderen IDE und einer anderen Ordnerstruktur.

Damit ihr zwei verschiedene E-Mail-Adressen mit GitHub verwenden könnt, müsst ihr diese erst einmal verifizieren. Anschließend könnt ihr bei jeder Aktion, wie etwa dem Merge eines PR auf github.com, auswählen, welche ihr verwenden möchtet. Die Verifizierung ist weiterhin notwendig, damit ihr sie für die Signierung von Commits verwenden könnt.

Aufteilung euer Konfiguration und bedingtes Laden der Dateien

Normalerweise speichert ihr alle Git-Konfigurationen in der Datei ~/.gitconfig. Diese Datei könnte wie folgt aussehen:

[user]
	name = Jo Doe
	email = j.doe@company.com
	signingkey = ~/.ssh/id_ed25519.pub

In jedem Projekt, in dem ihr nun einen Commit macht, würde die company.com E-Mail-Adresse verwendet und der Commit mit dieser signiert. Wenn ihr eine andere E-Mail-Adresse verwenden wollt, müsstet ihr diese manuell bei jedem Commit angeben:

git commit -m"message" --author="Jo Doe <1234567+jo.doe@users.noreply.github.com>" --gpg-sign=~/.ssh/id_ed25519.pub

Das ist natürlich nicht wirklich praktikabel und ihr vergesst diese zusätzlichen Parameter vielleicht manchmal. Alternativ könnt ihr auch die Autor-Einstellungen in der projektspezifischen Konfigurationsdatei angeben. Aber das müsstet ihr in jedem einzelnen geklonten Projekt tun.

Es ist zwar ebenfalls technisch möglich, die Autor-Daten nachträglich zu ändern, aber das ist alles andere als leicht und erfordert auch einen interaktiven Rebase, der die Historie überschreibt und daher nicht für bereits gepushte Commits verwendet werden darf. Wie können wir es also sonst erreichen?

Bedingtes Laden einer Konfigurationsdatei

Innerhalb einer Git-Konfigurationsdatei kann man mit includeIf eine Datei aufgrund einer Bedingung laden. Der einfachste Weg ist es hierbei, dies abhängig vom Projektordner zu tun. Wir speichern hierzu einfach alle persönlichen und Open-Source-Projekte direkt im ~/PhpstormProjects Ordner und alle für das Unternehmen im Unterordner ~/PhpstormProjects/company. Wir können dann die folgenden Zeilen in unsere globale Git-Konfiguration einfügen:

# file: ~/.gitconfig

[includeIf "gitdir:~/PhpstormProjects/"]
	path = .gitconfig-general

[includeIf "gitdir:~/PhpstormProjects/company/"]
	path = .gitconfig-company

Wir verschieben dann alle spezifischen Eisntellungen in diese beiden Dateien. Das werden in der Regel mindestens die gesamten [user] Einstellungen sein, so wie eventuell noch ein paar mehr. Die allgemeine Datei könnte dann wie folgt aussehen:

# file: ~/.gitconfig-general

[user]
	name = Jo Doe
	email = 1234567+jo.doe@users.noreply.github.com
	signingkey = ~/.ssh/id_ed25519.pub

In diesem Codeschnippsel habe ich die GitHub „noreply“ E-Mail-Adresse verwendet, die ihr anstelle eurer persönlichen E-Mail-Adresse einstellen könnt, damit diese private belibt und nicht für den Versand von Spam missbraucht wird.

Für den beruflichen company Unterordner sieht die separate Konfigurationsdatei dann etwa so aus:

# file: ~/.gitconfig-company

[user]
	name = Jo Doe
	email = j.doe@company.com
	signingkey = ~/.ssh/id_ed25519_company.pub

Diese beiden Dateien werden in eurem Homeverzeichnis neben der globalen .giconfig Datei gespeichert. Wenn ihr weiter Einstellungen überschreiben möchtet, fügt sie einfach der jeweiligen Datei hinzu.

Auf der Git-Dokumentationsseite zu den includes findet ihr auch andere Bedingungen für das Laden von Dateien, unabhängig vom gitdir. Ihr könnt beispielsweise auch eine Konfigurationsdatei abhängig von der Remote-URL laden. Damit wäre es dann auch möglich ein company Repository in einen anderen Ordner als ~/PhpstormProjects/company/ zu klonen un trotzdem die .gitconfig-company Datei zu laden. Aber es ist vermutlich einfacher zu verstehen, welche Datei geladen wird, wenn man nach dem Ordner geht.

Bonus: Verwaltung von GitHub-Benachrichtigungen für mehrere E-Mail-Adressen

Wenn ihr eine Einladung zu einer GitHub-Organisation erhaltet, dann werden ihr in der Regel automatisch alle Benachrichtigungen von allen Repositories abonniert. Da ihr GitHub vermutlich zuvor nur für private Projekte verwendet habt, erhaltet ihr diese Benachrichtigungen dann auch alle an die private E-Mail-Adresse. Glücklicherweise kann man ein Routing für Benachrichtigungen basierend auf der Organisation einstellen.

Um eine solche Routing-Regel einzustellen, navigiert ihr zu „Settings | Notifications“. Dort klickt ihr dann auf den „Custom Routing“ Button und dann auf den „Add new route“ Button. Anschließend wählt ihr die Organisation und die E-Mail-Adresse aus und klickt auf „Save“. Wenn ihr Einladungen zu mehreren Organisationen bekommen habt, könnt ihr auch mehrere Regeln anlegen.

Falls ihr gitlab.com für private/berufliche Projekte verwendet, dann könnt ihr pro Gruppe oder Projekt eine E-Mail-Adresse für die Benachrichtigungen auswählen. Diese findet ihr in eurem Profil unter „User Settings | Notifications“.

Fazit

Die gleichzeitige Arbeit an privaten und beruflichen Git-Projekten auf einem Gerät kann die Git-Autor-Daten eurer Commits ziemlich durcheinanderbringen. Aber mit dem bedingten Laden von Konfigurationsdateien kann spezifische Einstellungen für die verschiedenen Arten von Projekten festlegen.

Führe deinen eigenen Code innerhalb einer Action nur einmal aus

WordPress bietet sehr viele Hooks an, mit denen man mit dem Code interagieren kann. Filter ändern dabei den Wert einer übergebenen Variablen und in der Regel möchte man diesen Wert jedes Mal ändern, wenn der Filter angewendet wird. Bei Actions möchte man den Code aber vielleicht nur einmal ausführen, gerade wenn er Seiteneffekt, wie etwa den Versand einer E-Mail auslösen.

Prüfe, ob eine Action bereits ausgeführt wurde

Mit der Funktion did_action() kann man prüfen, wie häufig eine Action bereits aufgerufen wurde. Wenn ihr euren eigenen Code also nur dann ausführen wollte, wenn die Action zum ersten Mal ausgeführt wird, aber kein zweites Mal, dann könnt ihr das folgende tun:

function do_this_only_once() {
	// If the action we are hooking in was called more than once, stop execution of the custom code.
	if ( did_action( 'the_hook_name' ) > 1 ) {
		return;
	}

	// Run your custom code
}
add_action( 'the_hook_name', 'do_this_only_once' );

Wenn die the_hook_name Action ausgeführt wird, dann wird beim Ausführen von did_action( 'the_hook_name' ) der Wert 1 zurückgegeben, da die Action gerade eben ausgeführt wurde. Daher könnt ihr nicht einfach auf einen boolschen Wert testen, sondern ihr müsst prüfen, ob der Wert größer als Eins ist, um die Ausführung eures Codes zu stoppen.

Verwendet eure eigene Action, um mehrfache Ausführungen eures Codes zu verhindern

Manchmal könnt ihr nicht einfach prüfen, ob die Action zum ersten Mal ausgeführt wird, sondern ihr müsst noch weiter Dinge prüfen. In einem solchen Fall könnte man diese alle in eine Bedingung schreiben. Alternativ könnt ihr hier aber auch eine eigene Action nutzen, die ihr dann für das frühe Abbrechen der Funktion nutzen könnt:

function do_this_only_once( $hook_parameter ) {
	// If the custom code has been run already, stop execution of custom code.
	if ( did_action( 'do_this_only_once' ) ) {
		return;
	}

	// A second check on a hook parameter.
	if ( 'something' !== $hook_parameter ) {
		return;
	}

	// Run your custom code

	// Call our custom action, so we can check, if it has been called already.
	do_action( 'do_this_only_once' );
}
add_action( 'the_hook_name', 'do_this_only_once' );

In diesem Beispiel prüfen wir in der ersten Bedingung auf unsere eigene Action. Dann führen wir eine weitere Bedingung aus, die die weitere Ausführung abbrechen könnte. Nur wenn auch diese Bedingung die Funktion nicht verlässt, wird der eigene Code schließlich ausgeführt. Ganz am Ende der Funktion führen wir dann unsere eigene Action aus. Die erste Bedingung in der Funktion stellt beim nächsten Aufruf der Funktion nicht nur sicher, dass wir den eigenen Code nicht erneut ausführen, es verhindert auch, dass die weiteren Bedingungen nicht erneut geprüft werden müssen.

Fazit

Es gibt mehrere Wege, um zu verhindern, dass euer Code für eine Action nur einmal ausgeführt wird. Dabei empfiehlt es sich in der Regel, dass man hierzu die did_action() Funktion verwendet. Und wenn eurer Code Seiteneffekte hat, wie etwas das Versenden einer E-Mail, dann solltet ihr wirklich sicherstellen, dass er nur genau so oft aufgerufen wird, wie unbedingt notwendig.