Mit kombinierten APIs zum Glück 😁

Wie weit bis zur nächsten Bushaltestelle?

Von einem bestimmten Ort die nächstgelegene Bushaltestelle zu finden, kann so einfach sein – einfach auf Google Maps, OpenStreetMap oder bei Apple nachschauen und man sieht die kleinen Bus-Symbole. Dann weiß man auch, wie weit diese ungefähr weg ist.

Ich wollte das jedoch automatisiert machen – also von einer Location, von der ich die Koordinaten habe, die Meter bis zur nächsten Bushaltestelle automatisch berechnen. Die „Reise“ dorthin war jedoch nicht ganz einfach – und damit andere diesen beschwerlichen Weg nicht ohne Hilfestellung antreten müssen, habe ich einen kleinen „Guide“ verfasst 😊

Voraussetzungen

Wir werden die Google Maps – API verwenden, d. h. für diesen wird ein API-Schlüssel benötigt (den gibt es hier). Weiters setze ich das ganze in Firebase direkt am Server um, d. h. ich erstelle Cloud Functions und nutze als Tool Rowy. Die API-Calls bleiben natürlich gleich und das JavaScript ist leicht für andere Plattformen anpassbar. Unsere Ausgangs-Koordinaten befinden sich im Feld Koordinaten; wir arbeiten in Rowys Derivative-Fields, also Funktionen, die ausgeführt werden, wenn sich ein anderes Feld (in unserem Fall eben das Koordinaten-Feld) ändert und die anschließend einen Wert zurückliefern.

Der Beginn: U-Bahn-Stationen

Man mag es kaum glauben, aber die Entfernung zu U-Bahn-Stationen ist einfacher zu bekommen als zu Bus-Stationen. Ich werde später darauf eingehen, warum, aber sehen wir uns einmal an, wie wir mit der Metro umgehen können.

Wir erhalten eine Liste der umliegenden U-Bahn-Stationen (hier mit Radius 1000 m) mit einem Aufruf der Google NearbySearch-API.

// Den Google API-Key aus dem Secrets-Manager holen
const mapsAPISecret = await rowy.secrets.get("GoogleNearbySearch");

// Die API aufrufen und das Ergebnis in der Konstante nearbySearch speichern  
const nearbySearch = await fetch("https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=" + row.Koordinaten.latitude + "," + row.Koordinaten.longitude + "&type=subway_station&radius=1000&key="+mapsAPISecret);

const nearbySearchJson = await nearbySearch.json();

Das liefert uns ein JSON mit folgender Struktur:

Diese Auflistung ist nach Entfernung sortiert – d. h. wir nehmen uns die Koordinaten des ersten (= nullten) Elements und füttern diese an die Google Distance Matrix API, die uns die Gehzeit zurückliefert:

// Die Koordinaten aus der obigen Antwort:
const latLngPoi = nearbySearchJson.results[0].geometry.location.lat + "," + nearbySearchJson.results[0].geometry.location.lng;

// Die Anfrage an die Google Distance Matrix API für die Meter des Weges zwischen zwei Koordinaten:
const response = await fetch("https://maps.googleapis.com/maps/api/distancematrix/json?mode=walking&origins=" + row.Koordinaten.latitude + "," + row.Koordinaten.longitude + "&destinations=" + latLngPoi + "&key="+mapsAPISecret);

const responseJson = await response.json();

// Jetzt noch die Meter aus der neuen Antwort holen und in Integer umwandeln:
return parseInt(responseJson.rows[0].elements[0].distance.value);

Das bedeutet, mit zwei Google API – Calls kommen wir zu unserem Ergebnis, wie weit die nächstgelegene U-Bahn-Station entfernt ist.

Bus-Haltestellen

Ja, so einfach könnte das ja auch für Bus-Haltestellen sein: Einfach den „type“ bei der ersten Abfrage von subway_station zu bus_station ändern (siehe hier). Leider – aus mir unerfindlichen Gründen – fehlen hier aber wichtige Bushaltestellen. Und das mitten in der Stadt – es ist also nicht so, dass das irgendeine Haltestelle in den Bergen ist, die nur einmal pro Jahr angefahren wird. Ich konnte – trotz unzähligen Versuchen – die Google API nicht dazu bewegen, diese fehlenden Haltestellen zu liefern – nicht einmal, wenn sie auf der eigenen Karte angezeigt werden!

Hier eine Haltstelle, die es auf Google Maps gar nicht gibt

Na dann: OpenStreetMaps

Gottseidank ist Google Maps nicht der einzige Kartenanbieter – OpenStreetMaps ist eine gute Alternative. Und die haben auch eine API: Die Overpass API! Und das Beste: Sie liefern die vermissten Bushaltestellen!

Um also alle Haltestellen in einem Radius zu erhalten, können wir folgenden Call verwenden (diesmal mit einem Try-Catch-Block zum Abfangen von Fehlern):

const axios = require("axios");

const latitude = row.Koordinaten.latitude;
const longitude = row.Koordinaten.longitude;
const radius = 500; // search radius in meters

// build Overpass API query
const overpassQuery = `[out:json];node(around:${radius},${latitude},${longitude})[highway=bus_stop];out center;`;

try {
// send Overpass API query to OpenStreetMap server
  const response = await axios.post('https://lz4.overpass-api.de/api/interpreter', overpassQuery);
  return response.data;

} catch(error) {
  logging.error('There was an error with the Overpass API');
  return null;
}

So erhalten wir als Antwort alle Bushaltestellen in diesem Radius:

Und jetzt einfach weiter wie bisher, richtig? Den ersten Eintrag nehmen; Abstand der Koordinaten ausrechnen und ausgeben. Leider nicht…

2 Herausforderungen mit den OpenStreetMap-Daten

Mit den JSON-Daten, die wir erhalten haben, haben wir (trotzdem sie jetzt endlich die vermissten Haltestellen enthalten) zwei Herausforderungen:

  • Die Liste ist nicht nach Entfernung sortiert und
  • die Overpass API kann zwar Entfernungen berechnen – aber nicht die echte Wegzeit, wie es die Google Distance Matrix API kann

Meine Lösung: Wir holen uns die Koordinaten aller Stationen dieser Liste; füttern sie nach der Reihe an Google und nehmen die kürzeste Entfernung. So habe ich das umgesetzt (nun der ganze Code):

const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{
  logging.log("Derivative started: Calc distance to bus station");

  // get the Goole API-key from the Secrets-Manager
  const googleApiKey = await rowy.secrets.get('GoogleNearbySearch');

  const axios = require("axios");

  const latitude = row.Koordinaten.latitude;
  const longitude = row.Koordinaten.longitude;
  const radius = 500; // search radius in meters
  let listOfCoordinates = '';
  let distance = Number.MAX_SAFE_INTEGER;

  // build Overpass API query
  const overpassQuery = `[out:json];node(around:${radius},${latitude},${longitude})[highway=bus_stop];out center;`;

  try {
  // send Overpass API query to OSM server
    const response = await axios.post('https://lz4.overpass-api.de/api/interpreter', overpassQuery);

    // go through the returned json...
    for (const element of response.data.elements) {
      const {lat, lon} = element;
 
      try {
          // ...and feed it to the Google Distance Matrix API; remember the shortest one
          const response = await fetch("https://maps.googleapis.com/maps/api/distancematrix/json?mode=walking&origins=" + row.Koordinaten.latitude + "," + row.Koordinaten.longitude + "&destinations=" + lat + "," + lon + "&key="+googleApiKey);
          const responseJson = await response.json();
          const distanceNew = parseInt(responseJson.rows[0].elements[0].distance.value);
               //listOfCoordinates+=`${lat},${lon} - ${distanceNew} | `;
               if (distanceNew<distance) {
                 distance = distanceNew;
               }
        } catch (error) {
          logging.error('There was an error with the Distance Matrix API');
          return null; 
        }

    }

    if (distance == Number.MAX_SAFE_INTEGER) {
      // there is no bus-stop around
      return null;
    } else {
      // there is one
      return distance;
    }

  } catch(error) {
    logging.error('There was an error with the Overpass API');
    return null;
  }

}

Zusammenfassung

Also kurz zusammengefasst:

  • Wir holen uns alle Bushaltestellen in einem bestimmten Radius von OpenStreetMap
  • Wir geben die Koordinaten, die wir erhalten haben, nacheinander an die Google Distance Matrix API weiter…
  • …und geben die kürzeste Entfernung in Metern aus

Nützliche Links

Disclaimer

Natürlich kann es sein, dass es einfachere und andere Wege gibt. Auch ist meine Erfahrung mit diesen APIs, Cloud Functions und JSON das, was ich mir selbst beigebracht habe – also kann es durchaus sein, dass mein Code nicht „standardkonform“ ist. Das Wichtigste jedoch: Es funktioniert – und ich wollte meine Erfahrungen weitergeben, falls jemand anderer mit ebensolchen Herausforderungen kämpft 😊👌

1 Stern2 Sterne3 Sterne4 Sterne5 Sterne (2)

Loading…
Avatar von manuel

AUTOR

manuel