Software • 02. März 2023

Wie weit ist es bis zum nächsten Altglascontainer? - Ein Prototyp für dynamische Erreichbarkeitsanalysen mit Express und GraphHopper!

Im Rahmen der Happy Days, den firmeninternen Weiterbildungstagen die einmal alle zwei Monate angeboten werden, habe ich mich mit serverseitigem Javascript beschäftigt. Als (ehemaliger) PHP- und (aktueller) Kotlin-Entwickler bestand nie wirklich die Notwendigkeit, Node.js einzusetzen. Die Happy Days bieten dafür jedoch den perfekten Rahmen.

Der Fokus dieses Blog-Artikels liegt auf der Implementierung einer Node.js-Anwendung. Eine bestehende GraphHopper-Instanz, entsprechend für Fußgänger konfiguriert, wird als gegeben vorausgesetzt.

 

Die Ziele

Folgende Ziele wurden gesetzt:

  • Ein oder zwei API-Endpunkte implementieren, die Anfragen serverseitig abarbeiten
  • Eine Templating-Engine einsetzen
  • Eine Datei mit Umgebungsvariablen zur Konfiguration nutzen

Für einen Entwickler mit ca. 1,5 Arbeitstagen Zeit wirkten diese Ziele auf jeden Fall machbar.

 

Der Anwendungsfall

Die Anwendung sollte außerdem ein ganz banales Problem lösen:

Wie weit muss man laufen, um den nächsten Altglascontainer zu erreichen? Gibt es in Potsdam Wohnflächen, von denen man nicht innerhalb von 10 Minuten Gehzeit Altglas abgeben kann? Mit Hilfe einer Erreichbarkeitsanalyse lassen sich diese Fragen quantitativ beantworten.

 

Die Lösung

Express
Obwohl sich die benötigten Schnittstellen zwar auch nativ in Node.js umsetzen ließen, kommt für diese Anwendung ein Framework zum Einsatz: Express ist ein flexibles und leichtgewichtiges Framework, um schnell eine API zu entwickeln. Es ist ausgereift genug, dass selbst Mozilla einen Abschnitt ihrer Entwicklerdokumentation Express gewidmet hat. Alternativen dazu wären z.B. Koa oder Fastify.

Das Kommandozeilen-Tool express-generator hilft dabei, das Grundgerüst für eine Anwendung zu bauen. Es kann mittels npm installiert werden.

npm install express-generator -g

Express unterstützt eine Reihe von Templating-Engines. Als Symfony-Entwickler hat man natürlich Erfahrung mit Twig, welches ebenfalls als JavaScript-Package verfügbar ist. Allerdings soll an dieser Stelle Pug genutzt werden.

Damit kann das Grundgerüst gebaut werden:

express prototype-network-analysis –view=pug
cd prototype-network-analysis
npm install
DEBUG=prototype-network-analysis:* npm start

Pug und eine Karte
Der Syntax von Pug ist etwas gewöhnungsbedürftig. Genau wie bei Python ist die Einrückung entscheidend, da auf diese Weise verschachtelte HTML-Elemente erzeugt werden. Dazu ein einfaches Beispiel:

Eingabe:

.main
  h1.header Hello World
  p.content Lorem ipsum dolorem bla
  ul
    li Item 1
    li Item 2
    li Item 3

Ausgabe:

<div class="main">
  <h1 class="header">Hello World</h1>
  <p class="content">Lorem ipsum dolorem bla</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
</div>

Mit diesem Wissen lässt sich das Template-Grundgerüst von Express so umbauen, dass eine Karte dargestellt wird.

  • layout.pug enthält das Basislayout, hier werden die Maplibre-Abhängigkeiten eingefügt.
  • index.pug enthält das Layout der Homepage unter der Route localhost:3000, wo das map-Element ergänzt wird.
//- layout.pug
head
  //- ...
  script(src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js")
  link(
    href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css",
    rel="stylesheet"
  )
  //- ...

//- index.pug
block content
  #map(style="position: absolute; top: 0; bottom: 0; width: 100%;")
  //- ...
  script(type="text/javascript").
    const map = new maplibregl.Map({
      container: "map",
      style: "https://wms.wheregroup.com/tileserver/style/osm-bright.json",
      center: [13.0, 52.5],
      zoom: 10,
    });

Eine erste Route: /points
Das (einfache) Templating wäre damit geklärt, und Express läuft bereits. Im nächsten Schritt soll eine Route, genauer gesagt ein API-Endpunkt bereitgestellt werden, den der Client aufrufen kann. Der Endpunkt wiederum soll serverseitig eine Schnittstelle des Open-Data-Portals der Stadt Potsdam aufrufen (ein direkter, clientseitiger Aufruf ist aufgrund von CORS nicht möglich). Diese liefert eine Reihe von Punkten im Geojson-Format mit den Standorten aller Altglascontainer in Potsdam.

Um eine neue Route in der Anwendung zu registrieren werden zwei Dinge benötigt:

  • Eine neue Datei im Ordner routes namens points.js anlegen
  • Die Route in der app.js hinzufügen
// routes/points.js
const express = require("express");
const axios = require("axios");
const router = express.Router();

router.get("/", async (req, res, next) => {
  try {
    const points = await getPoints();
    res.send(points);
  } catch (e) {
    console.error(e);
  }
});

const getPoints = async () => {
  const res = await axios(
    "https://opendata.potsdam.de/explore/dataset/standplatze-glassammlung/download/?format=geojson&timezone=Europe/Berlin&lang=de"
  );
  return await res.data;
};

module.exports = router;


// app.js
// ...
const indexRouter = require("./routes/index");
const pointsRouter = require("./routes/points");

// ...
app.use("/", indexRouter);
app.use("/points", pointsRouter);

// ...

An dieser Stelle gilt es zu beachten, welche Version von Node installiert ist. Die fetch-API, zuständig für clientseitige Serveranfragen, ist nur von Version 17 aufwärts verfügbar. Für Node bis Version 16 bietet sich axios als Alternative an.

Ein einfacher Button im Frontend soll die neue Route ansprechen. Die Rückgabe, das Geojson-Objekt, wird anschließend zur Karte hinzugefügt.

Eine zweite Route: /isochrone
Um die Isochronen, also die Erreichbarkeitsflächen anhand der nun vorhandenen Punkte zu berechnen wird eine zweite Route benötigt: /isochrone. Sie wird analog zur ersten registriert. Die bereits geladenen Punkte werden an das Backend geschickt. Jeder Punkt dient dort als Start für den Aufruf der Isochrone-API von Graphhopper.

Die API liefert bei 136 Punkten 136 Polygone zurück, die aber alle räumlich übereinander liegen. Standardmäßig berechnet GraphHopper ein Intervall (z.B.: “Was ist innerhalb von 10 Minuten erreichbar?”). Fragt man mehr dieser “buckets” ab (“Was erreicht man in 2 min, 4 min, 6 min etc.?”) erhöht sich die Anzahl der Polygone um den Faktor der angeforderten Intervalle. Mit Hilfe einer simplen Geoprozessierungskette lässt sich aus den unübersichtlichen Polygonen ein verständliches Bild erzeugen.

  1. Die unbearbeiteten Rohdaten die beim mehrfachen Anfragen der GraphHopper-API entstanden sind, werden an das Frontend geschickt.

  2. Die Geometrien werden geglättet.

  3. Die Geometrien werden vereint, basierend auf dem gleichen Attribut-Intervall.

  4. Die Geometrien werden zusammengefasst.

  5. Die Schnittflächen, der sich überlagernden Intervalle werden entfernt.

  6. Die Geometrien werden anhand der Intervalle farblich markiert.

Die Prozessierung erfolgt mittels Turf.js, also komplett beim Client.

Umgebungsvariablen
Das letzte Ziel auf der Liste ist der Einsatz einer Konfigurationsdatei mit Umgebungsvariablen, die .env-Datei. Darin können z.B. die URLs des Tileservers oder der GraphHopper-Instanz abhängig von der Umgebung (lokale Entwicklung, Produktion, etc.) definiert werden. Ein üblicher Anwendungsfall für Umgebungsvariablen sind Zugangsdaten.

Mit dem Paket dotenv können die .env-Dateien benutzt werden. Wird eine entsprechende Datei im Hauptverzeichnis des Projekts abgelegt, kann deren Inhalt als Konfigurationsobjekt innerhalb der Anwendung benutzt werden. Vorsicht ist aber geboten, da keine sensitiven Daten an das Frontend weitergegeben werden dürfen.

Zunächst wird der benötigte Parameter in der .env-Datei gesetzt:

# .env
STYLE_URL=http://localhost:8080/tiles/styles/osm-bright-gl-style/style.json

Alle Umgebungsvariablen aus dieser Datei sind nach Initialisierung der Konfiguration (config()) im Objekt process.env enthalten und werden der Route zum Rendern übergeben.

// routes/index.js
const { config } = require("dotenv");

config();

const INDEX_CONFIG = {
  STYLE_URL: process.env.STYLE_URL, // <-- Die benötigte STYLE_URL
};

router.get("/", (req, res, next) => {
  res.render("index", INDEX_CONFIG);
});

Schließlich wird im Template auf den entsprechenden Key aus dem Objekt zugegriffen.

//- views/index.pug
//- ...
script(type="text/javascript").
  const map = new maplibregl.Map({
    container: "map",
    style: "#{ STYLE_URL }", //- <-- Wert der Variable STYLE_URL hier benutzen
    center: [13.0, 52.5],
    zoom: 10,
  });                                    

 

Zusammenfassung

Für den Prototyp wurde das Formular noch etwas angepasst, um den Nutzer*innen die Möglichkeit zu geben, maximale Gehzeit und die Zahl der Intervalle anzupassen:

Diese kleine Anwendung mit einer realen Fragestellung zu entwickeln hat durchaus Spaß gemacht. Bei zukünftigen Projekten, die sowohl Frontend- als auch Backend-Entwicklung beinhalten, würde ich allerdings zum Einsatz von Fullstack-Frameworks wie Next.js oder SvelteKit raten. Diese bieten die gute Entwicklererfahrung, die man von Frontend-Frameworks wie React oder Svelte gwohnt ist und kann sie auf das Backend übertragen. Für die Entwicklung einer reinen API ist die Kombination aus Node.js und Express hingegen gut geeignet.

Der Quellcode ist auf GitHub verfügbar.

PS: Die eingangs gestellte Frage “Gibt es in Potsdam Wohnflächen, von denen man nicht innerhalb von 10 Minuten Gehzeit Altglas abgeben kann?” lässt sich übrigens mit “Ja” beantworten.

Dieser Artikel erschien auf chringel.dev und wurde für diese Veröffentlichung übersetzt und leicht angepasst.

Weitere Beiträge, die Dich interessieren könnten:

Christian Engel

Seit 2017 arbeitet Christian Engel als Software-Entwickler für Web-Anwendungen und fühlt ich sowohl im Front-, wie auch Backend wohl. Sein Studium im Fach Geoinformation und Visualisierung an der Universität Potsdam ermöglicht es ihm, GIS-Expertise in den Entwickleralltag einzubringen.

Artikel teilen: