Zum Inhalt springen

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.

Dezember 2025
React 19 TypeScript MapLibre GL Supercluster Apollo Client (GraphQL) Tailwind CSS Ant Design Vite
ZUG-BirdNet – Vogelzug-Visualisierung auf interaktiver Karte

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 Hook useDetections mit Prefetch-Logik.
  • Kartenlogik: Komplette Neuimplementierung in src/components/map/Map.tsx mit Supercluster-Integration, dynamischen Paint Expressions für Artenfarben, Info-Marker-System und Layer-Management.
  • Timeline & Animation: DatesProvider mit virtueller Timeline-Abstraktion, Nachtmodus-Berechnung via SunCalc, Autoplay-Steuerung.
  • Astronomische Berechnungen: getDayPolygon.ts fü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