Add terrain/elevation data fetching (Doku 3.9)
Rundet das Altitude-Tab um ein reales Gelaendeprofil ab, damit sichtbar wird, ob die geplante Flughoehe ausreichend Bodenabstand haelt: - domain/mission/terrain_math.dart: reine Sampling-/Geometrie-Funktionen (routeKeyForTerrain fuer Caching, distanzbasiertes Sampling alle 30m, lineare Sollhoehen-Interpolation, terrainDangerRanges fuer Abschnitte mit < 10m Bodenabstand) - unabhaengig testbar ohne Netzwerk/UI. - services/terrain/terrain_service.dart: laedt AWS-Terrarium-PNG-Kacheln (elevation-tiles-prod, zoom 13) und dekodiert sie ueber dart:ui (instantiateImageCodec/toByteData), ohne zusaetzliches Bildpaket. Elevation = R*256 + G + B/256 - 32768 pro Pixel; Kacheln werden pro Route nur einmal geladen (nach Kachel gruppierte Sample-Punkte). - ui/providers/terrain_provider.dart: cached das geladene Profil ueber routeKeyForTerrain - reine Werteaenderungen (Hoehe/Speed) loesen keinen erneuten Kachel-Download aus, nur eine tatsaechliche Ortsverschiebung. - ui/widgets/full_value_chart.dart: _paintDangerBands/_paintTerrain zeichnen rote Gefahrenbaender bzw. die braune Gelaende-Flaeche, in der gleichen Reihenfolge wie im HTML-Demonstrator (dangerHtml, grid, terrainHtml, dann Soll-Kurve obenauf). - ui/widgets/waypoint_list_panel.dart: triggert ensureFor() beim Wechsel auf den Altitude-Tab sowie bei Routenaenderungen waehrend dieser aktiv ist (ref.listenManual auf currentMissionProvider). Getestet: 13 neue Unit-Tests fuer die reine Geometrie/Sampling-Logik, 3 Service-Tests mit einer zur Testzeit synthetisch erzeugten PNG (dart:ui-Encoding, keine Testasset-Datei noetig). Alle 80 Tests sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert: Altitude-Tab zeigt das reale Amsterdam-Gelaendeprofil (nahe Meereshoehe) korrekt als Flaeche unter der Sollhoehen-Linie. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5505bb3741
commit
b176ad294e
@@ -0,0 +1,159 @@
|
||||
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<Waypoint> 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<double> _cumulativeDistances(List<Waypoint> waypoints) {
|
||||
final cum = <double>[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<Waypoint> 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<RouteSamplePoint> sampleRouteByDistance(
|
||||
List<Waypoint> 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 = <RouteSamplePoint>[];
|
||||
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<TerrainSamplePoint> points;
|
||||
}
|
||||
|
||||
/// Lineare Interpolation der geplanten (Soll-)Hoehe an einer beliebigen
|
||||
/// Streckenposition - analog plannedAltAt() im HTML-Demonstrator.
|
||||
double plannedAltitudeAt(List<Waypoint> 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<Waypoint> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user