Redesigns the waypoint list panel to match the drone-status/missions panels: no title bar, close button floats over the tab row, tabs get more vertical padding. Removes the "Fetch wind" and "Validate" header buttons along with the now-fully-orphaned WindValidatePanel screen and its WindService support code (fetchValidation, WindValidationResult and friends) - nothing else referenced them. Wind is now fetched automatically instead: HeaderWindNotifier gains ensurePerWaypointWind(), called when the waypoint list panel opens and when the header pill's per-waypoint toggle switches on. A lat/lon signature of the waypoint list tracks whether the fetched data is still current, so a ref.listen(currentMissionProvider) in the same notifier refetches only when the list itself changes (position added/ removed/moved) - not on every altitude/speed edit. Waypoints that already carry wind data (e.g. loaded from a saved mission) short- circuit the check instead of re-fetching pointlessly. Also wires per-waypoint wind markers into the fly-mode map, which never had them (plan_screen.dart built windMarkers; fly_screen.dart just never did) - the reported "doesn't work in fly mode" bug. windMarkerWidth() moved from a plan_screen.dart-private method to wind_marker_pill.dart so both screens can share it. Test containers in widget_test.dart now override windServiceProvider with a MockClient so the new auto-fetch trigger can't reach the real network during tests.
160 lines
6.3 KiB
Dart
160 lines
6.3 KiB
Dart
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<WindService>((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<HeaderWindState> {
|
|
/// 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<List<Waypoint>>(currentMissionProvider, (previous, next) {
|
|
if (!_perWaypointActivated) return;
|
|
final signature = _signatureOf(next);
|
|
if (_signatureEquals(signature, _lastSignature)) return;
|
|
unawaited(_fetchPerWaypointWind(next, signature));
|
|
});
|
|
return const HeaderWindState();
|
|
}
|
|
|
|
Future<void> 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<void> 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<void> _fetchPerWaypointWind(
|
|
List<Waypoint> 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<Waypoint> 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, HeaderWindState>(HeaderWindNotifier.new);
|