Geocoding in Verbindung mit einer Leaflet-App

Gestern schrieb ich in einem Artikel, wie Adressen aus Koordinaten mittels des Dienstes Nominatim, unter Zuhilfenahme von OpenStreetMap-Daten, ermittelt werden können. Mithilfe der freien Bibliothek Leafleat, kann dies nun in eine kleine Webapplikation gegossen werden.

Anhand der Geo-Koordinate wird die Adresse ermittelt und dargestellt.

Ziel ist es eine Applikation zu bauen, welche eine Karte darstellt und bei einem Klick auf diese Karte den Geocoding-Dienst anfragt. Sobald die Antwort vom Dienst eingetroffen ist, soll in einem Pop-up die Adresse dargestellt werden. Nachdem ein Grundgerüst in Form einer HTML-Datei erstellt wurde, werden dort die benötigten Bibliotheken (Leaflet und jQuery) eingebunden. Anschließend kann die Karte erzeugt werden. Damit diese im Vollbild dargestellt wird, findet sich eine entsprechende CSS-Definition in der Datei:

<style type="text/css">
	body {
		padding: 0;
		margin: 0;
	}

	html, body, #map {
		height: 100%;
	}
</style>

Anschließend kann der TileLayer angelegt werden und der Map zugewiesen werden:

// Options
var centerLatitude=53.49577;
var centerLongitude=13.30873;
var zoomLevel=15;

// Define layer
var openStreetMapLayer = new L.TileLayer(
		'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
		{attribution: '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a>'});

// Define map
var map = L.map('map', {
	center: new L.LatLng(centerLatitude, centerLongitude),
	zoom: zoomLevel,
	layers: [openStreetMapLayer]
});

Für die Karte muss im nächsten Schritt ein Click-Event definiert werden, welches den AJAX-Request gegen den Geocoding-Dienst auslöst und den Marker erstellt. Der jeweilige Marker wird in eine separate Ebene gepackt und beim nächsten Klick wieder gelöscht und anschließend ein neuer Marker erstellt:

// Marker managment
var markerLayer;
map.on('click', function(e){

	// Console debug
	var coord = e.latlng;
	var lat = coord.lat;
	var lng = coord.lng;
	console.log("You clicked the map at latitude: " + lat + " and longitude: " + lng);

	// Marker managment		
	if(markerLayer != undefined) {
		map.removeLayer(markerLayer);
	}
	
	markerLayer = new L.Marker(e.latlng);
	map.addLayer(markerLayer);

	// Call geocode api
	var request =	$.ajax({
		method: "GET",
		url: "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lng + "&format=json"
	});

	request.done(function( json ) {
		console.log(json);
		markerLayer.bindPopup(json.display_name).openPopup();
	});
});

Am Stück sieht die Applikation wie folgt aus:

<!DOCTYPE html>
<html>
<head>
	<title>Leaflet address encoder</title>
	<meta charset="utf-8" />

	<meta name="viewport" content="width=device-width, initial-scale=1.0">

	<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"
   integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
   crossorigin=""/>
	
	<style type="text/css">
		body {
			padding: 0;
			margin: 0;
		}

		html, body, #map {
			height: 100%;
		}
	</style>
	
	<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"
   integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og=="
   crossorigin=""></script>
	<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
	<div id="map"></div>
	
	<script>
	        // Options
		var centerLatitude=53.49577;
		var centerLongitude=13.30873;
		var zoomLevel=15;
		
		// Define layer
		var openStreetMapLayer = new L.TileLayer(
				'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
				{attribution: '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a>'});
		
		// Define map
		var map = L.map('map', {
		    center: new L.LatLng(centerLatitude, centerLongitude),
		    zoom: zoomLevel,
		    layers: [openStreetMapLayer]
		});
		
		// Marker managment
		var markerLayer;
		map.on('click', function(e){

			// Console debug
			var coord = e.latlng;
			var lat = coord.lat;
			var lng = coord.lng;
			console.log("You clicked the map at latitude: " + lat + " and longitude: " + lng);

			// Marker managment		
			if(markerLayer != undefined) {
				map.removeLayer(markerLayer);
			}
			
			markerLayer = new L.Marker(e.latlng);
       		map.addLayer(markerLayer);

			// Call geocode api
			var request =	$.ajax({
				method: "GET",
				url: "https://nominatim.openstreetmap.org/reverse?lat=" + lat + "&lon=" + lng + "&format=json"
			});

			request.done(function( json ) {
				console.log(json);
				markerLayer.bindPopup(json.display_name).openPopup();
			});
  		});
	</script>
</body>
</html>

Alternativ kann der Quelltext heruntergeladen bzw. ausprobiert werden.

Adresse anhand einer Koordinate ermitteln

Im Umfeld von OpenStreetMap existieren eine Reihe von Diensten und APIs. Einer dieser Dienste ist Nominatim. Dabei handelt es sich um einen Geocoding-Dienst auf Basis der OpenStreetMap-Daten. Mithilfe der API kann anhand einer Koordinate in Form von Latitude und Longitude, eine Adresse ermittelt werden. Ein Beispielaufruf könnte wie folgt aussehen:

GET https://nominatim.openstreetmap.org/reverse?lat=53.49567958129127&lon=13.302898406982424&format=json

Gewöhnungsbedürftig an diesem Aufruf ist der Parameter format, über welchen das Format des Response definiert wird. Im Sinne einer sinnvollen REST-API, hätte dies besser über den Header-Parameter Accept gelöst werden sollen. Als Ergebnis erhält der Response die ermittelte Adresse:

{
    "place_id": 239463758,
    "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
    "osm_type": "way",
    "osm_id": 626862936,
    "lat": "53.4957878878448",
    "lon": "13.3026220240811",
    "display_name": "Weinbergsweg, Burg Stargard, Stargarder Land, Mecklenburgische Seenplatte, Mecklenburg-Vorpommern, 17094, Deutschland",
    "address": {
        "road": "Weinbergsweg",
        "town": "Burg Stargard",
        "county": "Stargarder Land",
        "state": "Mecklenburg-Vorpommern",
        "postcode": "17094",
        "country": "Deutschland",
        "country_code": "de"
    },
    "boundingbox": [
        "53.4949976",
        "53.4960744",
        "13.3023076",
        "13.3027343"
    ]
}

Neben der Geocoding-Funktionalität enthält der Dienst eine Reihe weiterer Funktionen, welche ihn zu einer Art Suchmaschine für OpenStreetMap-Daten machen. Die offizielle Projektseite des Dienstes ist unter nominatim.org zu finden. Der Quelltext des Dienstes ist auf GitHub zu finden. Das Projekt ist unter der GPL2 lizenziert und damit freie Software.

API-Kataloge im Web

Jeder und kleinere und größere Webdienst verfügt mittlerweile über eine API, meist ausgeführt in Form eines RESTful-Webservice. Interessant ist es natürlich zu schauen, welche APIs öffentlich verfügbar sind. Hierbei helfen entsprechende Kataloge wie AnyAPI, welcher unter any-api.com zu finden ist. Dieser bietet Informationen über 1400 öffentlich zugängliche APIs.

Any API listet über 1400 öffentlich zugängliche APIs

Die APIs bei AnyAPI sind nach Kategorien geordnet. Zu jeder API finden sich in dem Katalog die Auflistung der Ressourcen und Dokumentation zur Nutzung der API. Daneben existieren weitere Kataloge wie die API List oder die Public APIs-Sammlung. Letztere wiederrum verfügt über eine API zum Abruf der APIs.

curl zur Abfrage von REST-APIs benutzen

Die Aufgabe des freien Kommandozeilentools curl ist einfach beschrieben: Datentransfer. So unterstützt curl unterschiedlichste Protokolle wie FTP, HTTP, HTTPS, IMAP, SCP, SMB und viele mehr. Ein einfacher Download einer Datei über HTTP bzw. HTTPS würde mit curl wie folgt aussehen:

curl -O https://example.com/file.zip

Auch ein Transfer z.B. per FTP ist kein Problem:

ftp://example.com/file.zip

Allerdings beherrscht curl wesentlich mehr Operationen als nur das Herunterladen von Dateien. So kann curl genutzt werden, um REST-APIs zu benutzen. Diese APIs arbeiten nicht nur mit dem HTTP-Verb GET, sondern auch mit anderen Verben wie POST und PUT. Ein einfacher GET-Request wurde mittels curl wie folgt aussehen:

curl -X GET https://example.com/

Ein POST-Request wird auf die gleiche Art durchgeführt:

curl -X POST https://example.com/

Sollen zusätzlich Daten übertragen werden, so geschieht dies mit dem Parameter -d:

curl -X POST https://example.com/  -d '{
	field: "data",
	field2: "data",
	field3: "data"
}'

Damit werden die Daten im Body des Requests mitgesendet. Auch die Übergabe von Headern ist mittels curl möglich:

curl -X POST https://example.com/ 
 -H 'HeaderField: headerValue'
 -d '{
	field: "data",
	field2: "data",
	field3: "data"
}'

Manche APIs und andere Services blockieren Abrufe über curl manchmal. Dabei wird der Useragent von curl ausgesperrt. Allerdings kann dieser einfach geändert werden:

curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0" -X GET https://example.com/

Damit können solche fragwürdigen Maßnahmen, welche zum Ausschluss von curl führen, umgangen werden. Daneben verfügt curl über viele weitere Operationen bzw. Optionen. So kann z.B. mittels des Parameters -I nur der Header des Response bezogen werden:

curl -I -X GET https://example.com

Somit bietet curl die entsprechende Funktionalität um REST-APIs für Tests und ähnliches flexibel abzufragen.

Nachkommastellen von Pi als REST-Service

Die Zahl Pi beschreibt das Verhältnis des Umfangs eines Kreises zu seinem Durchmesser und gehört sicherlich zu den bekannteren mathematischen Konstanten. Anlässlich des Pi-Days am 14. März 2019 veröffentlichte Google die Zahl Pi mit bisher unerreichten 31,4 Billionen Nachkommastellen.

3.1415926535897…

Passend dazu stellte Google die Seite pi.delivery in Netz. Neben einigen Spielereien rund um Pi, wie die Nutzung von Pi zur Generierung von Musik, findet sich dort eine REST-API, mit welcher die Nachkommastellen von Pi erfragt werden können. Ein einfacher Request des Services sieht wie folgt aus:

GET https://api.pi.delivery/v1/pi?start=0&numberOfDigits=42

Für den Request existieren zwei Parameter. Der erste Parameter mit dem Namen start definiert ab welcher Stelle die Nachkommastellen geliefert werden sollen. Der zweite Parameter numberOfDigits setzt die Anzahl der zurückgelieferten Nachkommastellen. Als Antwort erhält der Nutzer des Service anschließend die entsprechenden Nachkommastellen (sowie die 3 am Anfang von Pi):

{
    "content": "314159265358979323846264338327950288419716"
}