Files
dmc/app/lib/ui/widgets/header_wind_pill.dart
T
Constantin LeueandClaude Sonnet 5 1b13c45cac Add wind analysis: header pill, fetch/validate menu, speed-plot overlay
Portiert das Wind-System des HTML-Demonstrators (Doku 3.8) vollstaendig:
Open-Meteo-Winddaten, interpoliert zwischen Druckflaechen- und festen
Nabenhoehen-Stuetzpunkten auf die tatsaechliche Zielhoehe.

Domain (lib/domain/wind/wind_math.dart, reines Dart): Hoehen-
Interpolation (linear + zirkulaer fuer Windrichtung), Peilung zwischen
zwei Punkten, Kopf-/Rueckenwind-Komponente pro Flugleg - 1:1 uebernommen
aus interpolateWindAtHeight()/bearingBetween()/headTailwindComponent()
des Demonstrators, per Unit-Tests abgesichert.

Service (lib/services/wind/wind_service.dart): zwei getrennte,
fokussierte Open-Meteo-Requests statt eines kombinierten (Doku 4.10 -
ein 35-Variablen-Request verursachte im Demonstrator Modellauswahl-
Fehler). Drei Verbraucher: Kopfleisten-Pille (120 m AGL ueber
Referenzort), gebuendelter Wegpunkt-Fetch (ein Request fuer alle
Koordinaten via Open-Meteos Mehrfach-Location-Syntax) und das
Validierungspanel (rohe Messwerte je Nabenhoehe/Druckflaeche). Per
MockClient getestet (kein echter Netzwerkzugriff in Tests).

UI:
- HeaderWindPill (TopModeBar, nur ausserhalb Fly-Modus): zeigt
  Windrichtung/-geschwindigkeit, Antippen schaltet perspektivisch die
  Wind-Marker der Wegpunkte um (Zustand vorbereitet, HTML-Demonstrator:
  showPerWaypointWind).
- WaypointListPanel: "Fetch wind"/"Validate"-Buttons im Panel-Header,
  neue WIND-Spalte in der Liste.
- WindValidatePanel: neue Vollbild-Route (gleiches Muster wie
  WaypointListPanel) mit Rohdaten je Hoehenstufe inkl. Boeen (Doku 4.9:
  Boeen nur hier, nicht in der Wegpunktanzeige) und Interpolationsergebnis.
- FullValueChart (Speed-Tab): Kopf-/Rueckenwind-Diamanten pro Leg samt
  gestrichelter Nulllinie sowie gestrichelte Bodengeschwindigkeits-
  Stufenlinie.

Bug gefunden und gefixt waehrend der Emulator-Verifikation: die
Y-Achsen-Autoskalierung klemmte weiterhin auf den gueltigen Drehrad-
Wertebereich (13-25 m/s), wodurch nahe Null liegende Windkomponenten
(bei schwachem Wind der Normalfall) ausserhalb des sichtbaren Bereichs
lagen und weder Diamanten noch Nulllinie zu sehen waren. Der HTML-
Demonstrator klemmt computeSpeedChartRange() bewusst nicht auf SPD_MIN/
SPD_MAX; das Klemmen in FullValueChart._range() jetzt entsprechend
entfernt.

Waypoint-Modell um windSpeedMs/windDirFromDeg/windElevationM erweitert
(nullable, reine Planungshilfe - der Encoder liest diese Felder nie,
Doku 3.2). currentMissionProvider bekommt replaceAll() fuer den
gebuendelten Wind-Fetch, der alle Wegpunkte gleichzeitig aktualisiert.

Verifiziert: flutter analyze (0 issues), flutter test (44/44, davon 14
neue Wind-Mathematik- und 6 neue WindService-Tests), manuell auf
Pixel_10a-Emulator mit echtem Open-Meteo-Netzwerkzugriff - Kopfleisten-
Pille zeigt Live-Wind, Fetch/Validate fuellen Liste bzw. oeffnen das
Validierungspanel mit echten Messwerten, Speed-Plot zeigt nach dem Fix
Diamanten/Nulllinie/Bodengeschwindigkeit korrekt skaliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:32:15 +02:00

56 lines
1.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/wind_provider.dart';
import 'wind_arrow_icon.dart';
/// Kopfleisten-Pille mit Windrichtung/-geschwindigkeit auf 120 m AGL ueber
/// dem Referenzort (Doku 3.8). Antippen schaltet die Wind-Marker an jedem
/// Wegpunkt ein/aus (HTML-Demonstrator: showPerWaypointWind).
class HeaderWindPill extends ConsumerWidget {
const HeaderWindPill({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(headerWindProvider);
final sample = state.sample;
final active = state.showPerWaypoint;
return GestureDetector(
onTap: () => ref.read(headerWindProvider.notifier).toggleShowPerWaypoint(),
child: Container(
height: 34,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: active
? const Color(0xFF4D9DFF).withValues(alpha: 0.32)
: Colors.black.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(17),
border: Border.all(
color: active
? const Color(0xFF4D9DFF)
: Colors.white.withValues(alpha: 0.4),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (sample != null) ...[
WindArrowIcon(dirFromDeg: sample.dirFromDeg, size: 15),
const SizedBox(width: 4),
],
Text(
sample != null ? '${sample.speedMs.round()} m/s' : '—',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}