Auto-fetch wind, drop manual fetch/validate buttons, fix it in fly mode
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.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
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';
|
||||
@@ -37,10 +40,38 @@ class HeaderWindState {
|
||||
|
||||
/// Haelt den fuer die Kopfleiste interpolierten Wind (120 m AGL ueber dem
|
||||
/// Referenzort: erster Wegpunkt, sonst aktuelles Kartenzentrum) - analog
|
||||
/// refreshHeaderWind()/headerWindLocation() im HTML-Demonstrator.
|
||||
/// 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() => const HeaderWindState();
|
||||
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);
|
||||
@@ -65,27 +96,64 @@ class HeaderWindNotifier extends Notifier<HeaderWindState> {
|
||||
}
|
||||
|
||||
void toggleShowPerWaypoint() {
|
||||
state = state.copyWith(showPerWaypoint: !state.showPerWaypoint);
|
||||
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);
|
||||
|
||||
/// Referenzort fuer Windabfragen ohne festen Bezugspunkt (Wind-
|
||||
/// Validierungspanel aus der Wegpunktliste): erster Wegpunkt, sonst
|
||||
/// aktuelles Kartenzentrum - analog headerWindLocation() im HTML-
|
||||
/// Demonstrator. Null, wenn die Karte noch nicht gemountet/gelayoutet ist.
|
||||
(double, double)? windReferenceLocation(WidgetRef ref) {
|
||||
final waypoints = ref.read(currentMissionProvider);
|
||||
if (waypoints.isNotEmpty) {
|
||||
final first = waypoints.first;
|
||||
return (first.lat, first.lon);
|
||||
}
|
||||
try {
|
||||
final center = ref.read(mapControllerProvider).camera.center;
|
||||
return (center.latitude, center.longitude);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user