import 'dart:math' as math; import '../waypoint/flat_waypoint_list.dart'; /// Ein Stuetzpunkt fuer die Windinterpolation: Windgeschwindigkeit/-richtung /// in einer bestimmten Hoehe ueber dem Meeresspiegel (AMSL). Kombiniert aus /// Druckflaechen- und festen Nabenhoehen-Daten (Doku 3.8, /// buildCombinedWindLevels() im HTML-Demonstrator). class WindLevel { const WindLevel({ required this.heightAmslM, required this.speedMs, required this.dirFromDeg, }); final double heightAmslM; final double speedMs; final double dirFromDeg; } /// Interpoliertes Windergebnis an einer bestimmten Zielhoehe. class WindSample { const WindSample({required this.speedMs, required this.dirFromDeg}); final double speedMs; final double dirFromDeg; } /// Kreisfoermige Interpolation zwischen zwei Windrichtungen (kuerzester Weg /// ueber die 0/360-Grenze), analog interpolateAngle() im HTML-Demonstrator. double interpolateAngle(double a, double b, double frac) { final diff = ((b - a + 540) % 360) - 180; return (a + diff * frac + 360) % 360; } /// Lineare Interpolation zwischen den zwei Hoehen-Stuetzpunkten, die /// [targetHeightAmslM] einschliessen; ausserhalb des Stuetzpunktbereichs wird /// auf den jeweils naechstgelegenen Rand geklemmt (analog /// interpolateWindAtHeight() im HTML-Demonstrator). Gibt null zurueck, wenn /// keine Stuetzpunkte vorhanden sind. WindSample? interpolateWindAtHeight( List levels, double targetHeightAmslM, ) { if (levels.isEmpty) return null; final sorted = [...levels] ..sort((a, b) => a.heightAmslM.compareTo(b.heightAmslM)); final first = sorted.first; if (targetHeightAmslM <= first.heightAmslM) { return WindSample(speedMs: first.speedMs, dirFromDeg: first.dirFromDeg); } final last = sorted.last; if (targetHeightAmslM >= last.heightAmslM) { return WindSample(speedMs: last.speedMs, dirFromDeg: last.dirFromDeg); } for (var i = 0; i < sorted.length - 1; i++) { final a = sorted[i]; final b = sorted[i + 1]; if (targetHeightAmslM >= a.heightAmslM && targetHeightAmslM <= b.heightAmslM) { final span = b.heightAmslM - a.heightAmslM; final frac = span == 0 ? 0.0 : (targetHeightAmslM - a.heightAmslM) / span; return WindSample( speedMs: a.speedMs + (b.speedMs - a.speedMs) * frac, dirFromDeg: interpolateAngle(a.dirFromDeg, b.dirFromDeg, frac), ); } } return WindSample(speedMs: last.speedMs, dirFromDeg: last.dirFromDeg); } /// Grosskreis-Peilung von (lat1,lon1) nach (lat2,lon2) in Grad (0=Nord), /// analog bearingBetween() im HTML-Demonstrator. double bearingBetweenDeg(double lat1, double lon1, double lat2, double lon2) { final lat1Rad = lat1 * math.pi / 180; final lat2Rad = lat2 * math.pi / 180; final dLon = (lon2 - lon1) * math.pi / 180; final y = math.sin(dLon) * math.cos(lat2Rad); final x = math.cos(lat1Rad) * math.sin(lat2Rad) - math.sin(lat1Rad) * math.cos(lat2Rad) * math.cos(dLon); return (math.atan2(y, x) * 180 / math.pi + 360) % 360; } /// Rueckenwind-Komponente (positiv) bzw. Gegenwind-Komponente (negativ) /// entlang eines Kurses, analog headTailwindComponent() im HTML- /// Demonstrator: Wind, der genau von hinten kommt, ergibt +windSpeedMs. double headTailwindComponent( double bearingDeg, double windSpeedMs, double windDirFromDeg, ) { final windToward = (windDirFromDeg + 180) % 360; final diff = (windToward - bearingDeg) * math.pi / 180; return windSpeedMs * math.cos(diff); } /// Windkomponente auf dem Leg von [a] nach [b], interpoliert aus dem am /// Zielwegpunkt [b] hinterlegten Wind (analog computeLegWindComponent() im /// HTML-Demonstrator - der Zielpunkt bestimmt den massgeblichen Wind fuer /// dieses Leg). Null, solange fuer [b] noch kein Wind geladen wurde. double? computeLegWindComponent(Waypoint a, Waypoint b) { final windSpeedMs = b.windSpeedMs; final windDirFromDeg = b.windDirFromDeg; if (windSpeedMs == null || windDirFromDeg == null) return null; final bearing = bearingBetweenDeg(a.lat, a.lon, b.lat, b.lon); return headTailwindComponent(bearing, windSpeedMs, windDirFromDeg); } /// Windkomponente pro Leg der gesamten Route (Laenge waypoints.length - 1). List computeLegWind(List waypoints) { return [ for (var i = 0; i < waypoints.length - 1; i++) computeLegWindComponent(waypoints[i], waypoints[i + 1]), ]; } /// Erreichbare Bodengeschwindigkeit pro Leg (Soll-Geschwindigkeit des /// Zielwegpunkts + Windkomponente), null wo noch kein Wind geladen ist - /// analog der groundSpeeds-Berechnung in buildFullChart() im HTML- /// Demonstrator. List computeGroundSpeeds(List waypoints) { return [ for (var i = 0; i < waypoints.length - 1; i++) switch (computeLegWindComponent(waypoints[i], waypoints[i + 1])) { final wind? => waypoints[i + 1].speedMs + wind, null => null, }, ]; }