ZUG-BirdNet – Vogelzug-Visualisierung auf interaktiver Karte
Eine React-Webanwendung zur Visualisierung von Vogelerkennungen auf einer interaktiven Karte. Die App zeigt Zugvogel-Daten in Clusters auf einer Karte und ermöglicht zeitliche Animation mit Tag/Nacht-Filterung.
ZUG-BirdNet
Herausforderung
Das Projekt ZUG-BirdNet entstand im Kontext eines Naturschutzvorhabens im Raum Regensburg. Ziel war es, Vogelerkennungsdaten des BirdWeather-Netzwerks auf einer interaktiven Karte zu visualisieren — mit zeitlicher Animation, Artenfilterung und der Möglichkeit, die Anwendung als Widget in ein bestehendes CMS (Contao) einzubetten.
Ein erster Prototyp existierte bereits: einfache Marker auf einer Karte, fetch-basierte API-Calls und eine rudimentäre Animationslogik. Die Herausforderungen für den nächsten Schritt waren klar:
- Datenvolumen: Bis zu 20.000 Erkennungen pro Zeitfenster mussten performant dargestellt werden.
- Artenunterscheidung: Mehrere Vogelarten gleichzeitig visuell unterscheidbar auf der Karte.
- Zeitliche Navigation: Flüssige Animation über Tage hinweg, mit der Option, nur Nachtaktivität zu zeigen (Zugvögel sind nachtaktiv).
- Einbettbarkeit: Kein Routing, keine externen Abhängigkeiten zur Laufzeit — ein eigenständiges SPA-Widget.
Ansatz
Ich habe die Anwendung auf Basis des bestehenden Prototyps grundlegend neu strukturiert:
Architektur
Drei React-Context-Provider bilden das Rückgrat — ApolloProvider für GraphQL-Caching, DatesProvider für die gesamte Zeitlogik (Datumsbereich, Animationssteuerung, Nachtmodus) und MapProvider für die MapLibre-GL-Instanz. Diese Trennung ermöglicht saubere Verantwortungsbereiche und vermeidet Prop-Drilling.
Datenanbindung
Statt roher fetch-Calls kommt Apollo Client 4 zum Einsatz. Die GraphQL-Queries und Fragmente liegen in src/api/, die Typen werden automatisch per @graphql-codegen aus dem BirdWeather-Schema generiert. Das Caching nutzt cache-and-network für sofortige UI-Reaktion bei gleichzeitigem Hintergrund-Update.
Kartenrendering
MapLibre GL rendert die Karte im Dark-Theme (Stadia Maps). Die Erkennungsdaten werden als GeoJSON-Source bereitgestellt und über deklarative Layer-Definitionen (src/components/map/mapStyles.ts) als Kreise mit Glow-Effekt dargestellt.
Meine Rolle
Ich habe den kompletten Umbau der Anwendung verantwortet — von der Architektur bis zur Implementierung aller neuen Module. Konkret:
- GraphQL-Integration: Apollo Client Setup (
src/lib/apollo-client.ts), Type Policies für Cache-Steuerung, Query-Definitionen mit Fragmenten (src/api/fragments.ts,src/api/queries.ts), der Custom HookuseDetectionsmit Prefetch-Logik. - Kartenlogik: Komplette Neuimplementierung in
src/components/map/Map.tsxmit Supercluster-Integration, dynamischen Paint Expressions für Artenfarben, Info-Marker-System und Layer-Management. - Timeline & Animation:
DatesProvidermit virtueller Timeline-Abstraktion, Nachtmodus-Berechnung via SunCalc, Autoplay-Steuerung. - Astronomische Berechnungen:
getDayPolygon.tsfür das Terminator-Overlay (Tag/Nacht-Grenze als GeoJSON-Polygon). - UI-Komponenten: SpeciesDropdown mit Suche und Verfügbarkeitsprüfung, LayersDropdown für Zusatzlayer (Lichtverschmutzung, Lärmkartierung), Timeline mit gedrosseltem Slider.
Technische Highlights
Artspezifisches Clustering
Statt eines einzelnen Supercluster-Index für alle Erkennungen erstellt clusterUtils.ts separate Indizes pro Vogelart. Das ermöglicht farbkodierte Cluster ohne Vermischung: Jede Art behält ihre Farbe auch im aggregierten Zustand. Die Cluster werden per MapLibre match-Expression gefärbt:
["match", ["get", "species"],
"grus-grus", "#FF29B4",
"numenius-arquata", "#64BEFF",
"#cccccc"]
Die Farbzuordnung ist persistent: usePersistentColors weist Farben aus einer Palette zu und gibt sie erst frei, wenn eine Art abgewählt wird.
Virtuelle Timeline mit Nachtmodus
DatesProvider abstrahiert die Zeitachse als Folge von TimeSegment-Objekten. Im Nachtmodus berechnet die Komponente per SunCalc für jeden Tag im Datumsbereich die Sonnenuntergangs- und Sonnenaufgangszeiten und erzeugt Segmente, die nur die Nachtstunden abdecken. Der Slider arbeitet dann auf einer virtuellen Minutenzahl, die intern auf die korrekten Realzeiten abgebildet wird. So springt die Animation nahtlos von Nacht zu Nacht, ohne leere Tagesstunden abzuspielen.
Dynamische Query-Generierung
buildAvailableSpeciesQuery.ts erzeugt zur Laufzeit eine GraphQL-Query mit Aliased Fields — ein Feld pro Art — um in einem einzigen API-Call die Verfügbarkeit aller ausgewählten Arten im aktuellen Kartenausschnitt zu prüfen. Das vermeidet N separate Netzwerk-Anfragen und aktualisiert sich automatisch bei Kartenverschiebung (moveend-Event).
Ergebnis
Die überarbeitete Anwendung stellt Zugvogel-Daten performant und visuell ansprechend dar — auch bei fünfstelligen Erkennungszahlen. Die Architektur mit klarer Trennung von Daten-, Zeit- und Kartenlogik macht die Codebasis wartbar und erweiterbar. Durch das SPA-Design ohne Routing ist die Integration in das Ziel-CMS Contao durch Dritte problemlos möglich.
Technisch war das Projekt eine intensive Auseinandersetzung mit Geo-Datenvisualisierung, WebGL-Performance und dem Zusammenspiel von GraphQL-Caching mit animierten Kartenlayern — ein ungewöhnlicher Stack, der in dieser Kombination selten vorkommt.
Highlights
- Separate farb-kodierte Cluster pro Vogelart
- Tag/Nacht Overlay auf der Basis von astronomischer Tag/Nacht-Berechnung mit Suncalc
- Nachtmodus, der die Zeiten zwischen Sonnenaufgang und -untergang herausfiltert
- Dynamische GraphQL-Query-Generierung mit Aliased Queries