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! ?

Veröffentlicht von

Bernhard ist fest angestellter Webentwickler, entwickelt in seiner Freizeit Plugins, schreibt in seinem Blog über WordPress und andere Themen, treibt sich gerne bei den WP Meetups in Berlin und Potsdam herum und läuft nach Feierabend den ein oder anderen Halbmarathon.

2 Kommentare » Schreibe einen Kommentar

  1. Cooler Beitrag, sehr interessant. Wir komplex wäre es wohl, statt eines Punktes einen vorhanden GPX Track auf der SVG Map zu visualisieren? Das wäre ein spannender weiterer Teil des Posts…

    • Hallo Martin, für diesen Fall würde ich dir empfehlen den übernächsten Beitrag zu lesen. Darin erkläre ich, wie man eine SVG mit Leaflet verwendet und dann sollte so ein GPS-Track recht einfach visualisiert werden können.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert