Files
dmc/app/test/domain/wind_math_test.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

117 lines
3.5 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
import 'package:dmc_app/domain/wind/wind_math.dart';
void main() {
group('interpolateAngle', () {
test('nimmt den kuerzeren Weg ueber die 0/360-Grenze', () {
expect(interpolateAngle(350, 10, 0.5), closeTo(0, 0.001));
});
test('interpoliert linear ohne Grenzuebergang', () {
expect(interpolateAngle(10, 30, 0.5), closeTo(20, 0.001));
});
});
group('interpolateWindAtHeight', () {
const levels = [
WindLevel(heightAmslM: 100, speedMs: 4, dirFromDeg: 200),
WindLevel(heightAmslM: 300, speedMs: 8, dirFromDeg: 220),
];
test('klemmt unterhalb des tiefsten Stuetzpunkts', () {
final result = interpolateWindAtHeight(levels, 50);
expect(result!.speedMs, 4);
expect(result.dirFromDeg, 200);
});
test('klemmt oberhalb des hoechsten Stuetzpunkts', () {
final result = interpolateWindAtHeight(levels, 500);
expect(result!.speedMs, 8);
expect(result.dirFromDeg, 220);
});
test('interpoliert linear zwischen zwei Stuetzpunkten', () {
final result = interpolateWindAtHeight(levels, 200);
expect(result!.speedMs, closeTo(6, 0.001));
expect(result.dirFromDeg, closeTo(210, 0.001));
});
test('liefert null ohne Stuetzpunkte', () {
expect(interpolateWindAtHeight(const [], 100), isNull);
});
});
group('bearingBetweenDeg', () {
test('nach Norden ergibt ~0 Grad', () {
expect(bearingBetweenDeg(0, 0, 1, 0), closeTo(0, 0.5));
});
test('nach Osten ergibt ~90 Grad', () {
expect(bearingBetweenDeg(0, 0, 0, 1), closeTo(90, 0.5));
});
});
group('headTailwindComponent', () {
test('Wind von hinten ergibt vollen positiven Rueckenwind', () {
// Kurs 0 (Nord), Wind weht aus Sueden (180) - also von hinten.
expect(headTailwindComponent(0, 10, 180), closeTo(10, 0.001));
});
test('Wind von vorne ergibt vollen negativen Gegenwind', () {
// Kurs 0 (Nord), Wind weht aus Norden (0) - also von vorne.
expect(headTailwindComponent(0, 10, 0), closeTo(-10, 0.001));
});
test('Seitenwind ergibt ~0 Laengskomponente', () {
expect(headTailwindComponent(0, 10, 90), closeTo(0, 0.001));
});
});
group('computeLegWindComponent / computeLegWind / computeGroundSpeeds', () {
const a = Waypoint(
lat: 0,
lon: 0,
altitudeM: 60,
speedMs: 15,
catchRadiusM: 60,
);
const bWithWind = Waypoint(
lat: 1,
lon: 0,
altitudeM: 60,
speedMs: 15,
catchRadiusM: 60,
windSpeedMs: 10,
windDirFromDeg: 180,
);
const bWithoutWind = Waypoint(
lat: 1,
lon: 0,
altitudeM: 60,
speedMs: 15,
catchRadiusM: 60,
);
test('null solange am Zielwegpunkt kein Wind geladen ist', () {
expect(computeLegWindComponent(a, bWithoutWind), isNull);
});
test('Rueckenwind auf dem Leg erhoeht die Bodengeschwindigkeit', () {
final component = computeLegWindComponent(a, bWithWind);
expect(component, closeTo(10, 0.001));
final groundSpeeds = computeGroundSpeeds([a, bWithWind]);
expect(groundSpeeds.single, closeTo(25, 0.001));
});
test('computeLegWind liefert eine Komponente pro Leg', () {
final legWind = computeLegWind([a, bWithWind, bWithoutWind]);
expect(legWind, hasLength(2));
expect(legWind[0], isNotNull);
expect(legWind[1], isNull);
});
});
}