import 'package:latlong2/latlong.dart'; import '../waypoint/flat_waypoint_list.dart'; /// Distanzbasierte Sampling-Strategie (Doku 3.9): alle 30 m, mind. 10 / /// max. 2000 Samples - unabhaengig von der Anzahl Wegpunkte, damit auch /// lange Geradenstuecke zwischen zwei Wegpunkten ein glattes Profil /// bekommen. const terrainSampleSpacingM = 30.0; const terrainMaxSamples = 2000; /// Mindest-Bodenabstand, unterhalb dessen ein Streckenabschnitt als nicht /// sicher markiert wird (Doku 3.9/3.10). const minTerrainClearanceM = 10.0; const _distance = Distance(roundResult: false); /// Eindeutiger Schluessel fuer die aktuelle Route (Rundung auf 5 /// Nachkommastellen wie bei der Wind-Interpolation) - aendert sich, sobald /// sich ein Wegpunkt verschiebt, nicht aber bei reinen Hoehen-/ /// Geschwindigkeitsaenderungen. Damit laesst sich ein bereits geladenes /// Terrainprofil cachen, analog routeKeyForTerrain() im HTML-Demonstrator. String routeKeyForTerrain(List waypoints) { return waypoints .map((w) => '${w.lat.toStringAsFixed(5)},${w.lon.toStringAsFixed(5)}') .join('|'); } /// Ein Streckenpunkt in [distanceM] Entfernung vom ersten Wegpunkt. class RouteSamplePoint { const RouteSamplePoint({required this.distanceM, required this.point}); final double distanceM; final LatLng point; } List _cumulativeDistances(List waypoints) { final cum = [0]; for (var i = 1; i < waypoints.length; i++) { cum.add(cum[i - 1] + _distance( LatLng(waypoints[i - 1].lat, waypoints[i - 1].lon), LatLng(waypoints[i].lat, waypoints[i].lon), )); } return cum; } /// Wie viele Samples fuer die aktuelle Routenlaenge angemessen sind /// (Doku 3.9), analog `Math.min(Math.max(Math.ceil(totalRouteDist / /// TERRAIN_SAMPLE_SPACING), 10), TERRAIN_MAX_SAMPLES)`. int sampleCountFor(List waypoints) { if (waypoints.length < 2) return 0; final cum = _cumulativeDistances(waypoints); final total = cum.last; final raw = (total / terrainSampleSpacingM).ceil(); return raw.clamp(10, terrainMaxSamples); } /// Sampelt [numSamples] + 1 Punkte gleichmaessig nach zurueckgelegter /// Strecke (nicht nach Wegpunktindex) - analog sampleRouteLatLng() im /// HTML-Demonstrator. List sampleRouteByDistance( List waypoints, int numSamples, ) { final n = waypoints.length; if (n < 2 || numSamples <= 0) return []; final cum = _cumulativeDistances(waypoints); final total = cum.last == 0 ? 1.0 : cum.last; final points = []; for (var s = 0; s <= numSamples; s++) { final target = (s / numSamples) * total; var legIdx = 0; while (legIdx < n - 2 && cum[legIdx + 1] < target) { legIdx++; } final legStart = cum[legIdx]; final legEnd = cum[legIdx + 1]; final frac = legEnd > legStart ? (target - legStart) / (legEnd - legStart) : 0.0; final a = waypoints[legIdx]; final b = waypoints[legIdx + 1]; points.add(RouteSamplePoint( distanceM: target, point: LatLng( a.lat + (b.lat - a.lat) * frac, a.lon + (b.lon - a.lon) * frac, ), )); } return points; } /// Ein Gelaende-Sample in [distanceM] Streckenentfernung; [elevationRelM] /// ist relativ zum ersten gueltigen Sample der Route (nicht absolut ueber /// dem Meeresspiegel) - null, wo die Kachel nicht geladen werden konnte. class TerrainSamplePoint { const TerrainSamplePoint({required this.distanceM, this.elevationRelM}); final double distanceM; final double? elevationRelM; } /// Ein geladenes Gelaendeprofil, an [routeKey] gebunden - passt der /// aktuelle routeKeyForTerrain()-Wert nicht mehr, muss neu geladen werden. class TerrainProfile { const TerrainProfile({required this.routeKey, required this.points}); final String routeKey; final List points; } /// Lineare Interpolation der geplanten (Soll-)Hoehe an einer beliebigen /// Streckenposition - analog plannedAltAt() im HTML-Demonstrator. double plannedAltitudeAt(List waypoints, double distanceM) { final n = waypoints.length; if (n == 1) return waypoints.first.altitudeM; final cum = _cumulativeDistances(waypoints); if (distanceM <= cum.first) return waypoints.first.altitudeM; if (distanceM >= cum.last) return waypoints.last.altitudeM; for (var k = 0; k < n - 1; k++) { if (distanceM >= cum[k] && distanceM <= cum[k + 1]) { final span = cum[k + 1] - cum[k]; final frac = span > 0 ? (distanceM - cum[k]) / span : 0.0; return waypoints[k].altitudeM + (waypoints[k + 1].altitudeM - waypoints[k].altitudeM) * frac; } } return waypoints.last.altitudeM; } /// Streckenabschnitte, an denen der Bodenabstand [minTerrainClearanceM] /// unterschreitet (als [start, end]-Distanzpaare) - analog /// checkTerrainClearance() im HTML-Demonstrator. List<(double, double)> terrainDangerRanges( List waypoints, TerrainProfile profile, ) { if (waypoints.length < 2) return []; final ranges = <(double, double)>[]; double? rangeStart; for (final p in profile.points) { final elevRel = p.elevationRelM; if (elevRel == null) continue; final clearance = plannedAltitudeAt(waypoints, p.distanceM) - elevRel; final bad = clearance < minTerrainClearanceM; if (bad) { rangeStart ??= p.distanceM; } else if (rangeStart != null) { ranges.add((rangeStart, p.distanceM)); rangeStart = null; } } if (rangeStart != null) { ranges.add((rangeStart, profile.points.last.distanceM)); } return ranges; }