import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; import '../../domain/wind/wind_math.dart'; import '../../services/wind/wind_service.dart'; import 'current_mission_provider.dart'; import 'map_controller_provider.dart'; final windServiceProvider = Provider((ref) => WindService()); /// Zustand der Kopfleisten-Wind-Pille (Doku 3.8). class HeaderWindState { const HeaderWindState({ this.sample, this.loading = false, this.showPerWaypoint = false, }); final WindSample? sample; final bool loading; /// Umschaltbar per Antippen der Pille (HTML-Demonstrator: /// showPerWaypointWind) - blendet Wind-Marker an jedem Wegpunkt ein/aus. final bool showPerWaypoint; HeaderWindState copyWith({ WindSample? sample, bool? loading, bool? showPerWaypoint, }) { return HeaderWindState( sample: sample ?? this.sample, loading: loading ?? this.loading, showPerWaypoint: showPerWaypoint ?? this.showPerWaypoint, ); } } /// Haelt den fuer die Kopfleiste interpolierten Wind (120 m AGL ueber dem /// Referenzort: erster Wegpunkt, sonst aktuelles Kartenzentrum) - analog /// refreshHeaderWind()/headerWindLocation() im HTML-Demonstrator. Verwaltet /// ausserdem den automatischen Pro-Wegpunkt-Windabruf (siehe /// [ensurePerWaypointWind]-Doku) - beides teilt sich denselben /// [windServiceProvider] und lebt bewusst in derselben, nicht-autoDispose /// Notifier-Klasse, damit der Abgleich mit der Wegpunktliste app-weit /// weiterlaeuft, unabhaengig davon, welche der beiden Oberflaechen /// (Wegpunktliste, Kopfleisten-Pille) ihn zuletzt ausgeloest hat. class HeaderWindNotifier extends Notifier { /// Wurde der Pro-Wegpunkt-Windabruf schon mindestens einmal ausgeloest /// (Doku: "automatisch abrufen, wenn das menue geoeffnet wird oder die /// windanzeige ... aktiviert wird")? Erst danach reagiert [build] auf /// spaetere Aenderungen der Wegpunktliste automatisch. bool _perWaypointActivated = false; /// Lat/Lon-Signatur der Wegpunktliste, fuer die der zuletzt bekannte /// Windstand gilt (entweder frisch abgerufen oder als bereits vorhanden /// erkannt, siehe [ensurePerWaypointWind]) - Aenderungen an anderen /// Wegpunkt-Feldern (Hoehe/Geschwindigkeit/Aktion) loesen bewusst KEINEN /// erneuten Abruf aus (Doku: "nur noch aktualisieren, wenn sich die /// wegpunktliste geaendert hat"). List<(double, double)>? _lastSignature; @override HeaderWindState build() { ref.listen>(currentMissionProvider, (previous, next) { if (!_perWaypointActivated) return; final signature = _signatureOf(next); if (_signatureEquals(signature, _lastSignature)) return; unawaited(_fetchPerWaypointWind(next, signature)); }); return const HeaderWindState(); } Future refresh() async { final waypoints = ref.read(currentMissionProvider); (double, double)? location; if (waypoints.isNotEmpty) { location = (waypoints.first.lat, waypoints.first.lon); } else { try { final center = ref.read(mapControllerProvider).camera.center; location = (center.latitude, center.longitude); } catch (_) { // Kamera noch nicht bereit (Karte noch nicht gemountet/gelayoutet). location = null; } } if (location == null) return; state = state.copyWith(loading: true); final sample = await ref.read(windServiceProvider).fetchHeaderWind(location.$1, location.$2); state = state.copyWith(loading: false, sample: sample); } void toggleShowPerWaypoint() { final turningOn = !state.showPerWaypoint; state = state.copyWith(showPerWaypoint: turningOn); // Nur beim Einschalten abrufen (Doku: "wenn ... die windanzeige ... // aktiviert wird") - das Ausschalten blendet die Marker nur aus, die // Winddaten selbst bleiben gueltig. if (turningOn) unawaited(ensurePerWaypointWind()); } /// Holt Wind fuer alle Wegpunkte, falls fuer die aktuelle Wegpunktliste /// noch keiner bekannt ist (Doku: "wind automatisch abrufen, wenn das /// menue geoeffnet wird oder die windanzeige ... aktiviert wird") - vom /// Wegpunktlisten-Panel beim Oeffnen und von [toggleShowPerWaypoint] /// aufgerufen. Wiederholte Aufrufe fuer dieselbe (unveraenderte) /// Wegpunktliste loesen keinen erneuten Request aus; danach uebernimmt /// der Listener in [build] die Aktualisierung bei Aenderungen der Liste. /// /// Hat die Wegpunktliste bereits fuer alle Eintraege Winddaten (z.B. aus /// einer gespeicherten/importierten Mission geladen), zaehlt das ebenso /// als "aktuell" wie ein frischer Abruf - kein unnoetiger Request nur /// wegen des Menue-Oeffnens. Future ensurePerWaypointWind() async { _perWaypointActivated = true; final waypoints = ref.read(currentMissionProvider); if (waypoints.isEmpty) return; final signature = _signatureOf(waypoints); if (_signatureEquals(signature, _lastSignature)) return; if (waypoints.every((w) => w.windSpeedMs != null)) { _lastSignature = signature; return; } await _fetchPerWaypointWind(waypoints, signature); } Future _fetchPerWaypointWind( List waypoints, List<(double, double)> signature, ) async { // Vor dem eigentlichen Request gesetzt (nicht danach): fetchWindForWaypoints() // aendert nur die Windfelder, niemals lat/lon - die Signatur der // zurueckgeschriebenen Liste bleibt also identisch und der Listener in // [build] erkennt den eigenen replaceAll()-Aufruf unten korrekt als // "keine Aenderung", statt sich selbst erneut auszuloesen. _lastSignature = signature; final updated = await ref.read(windServiceProvider).fetchWindForWaypoints(waypoints); ref.read(currentMissionProvider.notifier).replaceAll(updated); } List<(double, double)> _signatureOf(List waypoints) => [for (final w in waypoints) (w.lat, w.lon)]; bool _signatureEquals(List<(double, double)> a, List<(double, double)>? b) { if (b == null || a.length != b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i] != b[i]) return false; } return true; } } final headerWindProvider = NotifierProvider(HeaderWindNotifier.new);