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.
Folgende Ziele wurden gesetzt:
Für einen Entwickler mit ca. 1,5 Arbeitstagen Zeit wirkten diese Ziele auf jeden Fall machbar.
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.
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
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:
// 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.
Die unbearbeiteten Rohdaten die beim mehrfachen Anfragen der GraphHopper-API entstanden sind, werden an das Frontend geschickt.
Die Geometrien werden geglättet.
Die Geometrien werden vereint, basierend auf dem gleichen Attribut-Intervall.
Die Geometrien werden zusammengefasst.
Die Schnittflächen, der sich überlagernden Intervalle werden entfernt.
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,
});
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: