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
@@ -4,10 +4,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../domain/mission/default_drone_profile.dart';
|
||||
import '../../domain/mission/drone_profile.dart';
|
||||
import '../../domain/mission/route_geometry.dart';
|
||||
import '../../domain/mission/terrain_math.dart';
|
||||
import '../../domain/waypoint/flat_waypoint_list.dart';
|
||||
import '../../domain/wind/wind_math.dart';
|
||||
import '../providers/active_drone_profile_provider.dart';
|
||||
import '../providers/current_mission_provider.dart';
|
||||
import '../providers/terrain_provider.dart';
|
||||
import '../providers/wind_provider.dart';
|
||||
import 'full_value_chart.dart';
|
||||
import 'waypoint_chip.dart';
|
||||
@@ -74,6 +76,16 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActive());
|
||||
// Laedt/aktualisiert das Gelaendeprofil (Doku 3.9) nur, waehrend der
|
||||
// Altitude-Tab tatsaechlich sichtbar ist - aendert sich die Route
|
||||
// waehrenddessen (Editieren, Loeschen, neuer Wegpunkt), muss neu
|
||||
// gesampelt werden. ensureFor() selbst filtert per routeKey, ob ein
|
||||
// erneuter Kachel-Download noetig ist.
|
||||
ref.listenManual(currentMissionProvider, (previous, next) {
|
||||
if (_tab == _PanelTab.altitude) {
|
||||
ref.read(terrainProvider.notifier).ensureFor(next);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -137,6 +149,7 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
Widget build(BuildContext context) {
|
||||
final waypoints = ref.watch(currentMissionProvider);
|
||||
final droneProfile = ref.watch(activeDroneProfileProvider);
|
||||
final terrainState = ref.watch(terrainProvider);
|
||||
final routeGeometry = buildRouteGeometry(
|
||||
waypoints,
|
||||
minTurnRadius: droneProfile.minTurnRadius,
|
||||
@@ -204,6 +217,11 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
pxPerUnit: 5,
|
||||
unit: ' m',
|
||||
onChanged: widget.onAltitudeChanged,
|
||||
terrainPoints: terrainState.profile?.points,
|
||||
terrainDangerRanges: terrainState.profile != null
|
||||
? terrainDangerRanges(waypoints, terrainState.profile!)
|
||||
: null,
|
||||
terrainLoading: terrainState.loading,
|
||||
),
|
||||
_PanelTab.speed => _chartTab(
|
||||
waypoints,
|
||||
@@ -234,6 +252,8 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
setState(() => _tab = tab);
|
||||
if (tab == _PanelTab.list) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActive());
|
||||
} else if (tab == _PanelTab.altitude) {
|
||||
ref.read(terrainProvider.notifier).ensureFor(ref.read(currentMissionProvider));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -272,6 +292,9 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
required void Function(int index, double value) onChanged,
|
||||
List<double?>? legWind,
|
||||
List<double?>? groundSpeeds,
|
||||
List<TerrainSamplePoint>? terrainPoints,
|
||||
List<(double, double)>? terrainDangerRanges,
|
||||
bool terrainLoading = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 16, 8, 8),
|
||||
@@ -289,6 +312,9 @@ class _WaypointListPanelState extends ConsumerState<WaypointListPanel> {
|
||||
onChanged: onChanged,
|
||||
legWind: legWind,
|
||||
groundSpeeds: groundSpeeds,
|
||||
terrainPoints: terrainPoints,
|
||||
terrainDangerRanges: terrainDangerRanges,
|
||||
terrainLoading: terrainLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user