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,78 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
import 'package:dmc_app/domain/waypoint/flat_waypoint_list.dart';
|
||||
import 'package:dmc_app/services/terrain/terrain_service.dart';
|
||||
|
||||
/// Baut eine 256x256-PNG, deren Pixel alle dieselbe Terrarium-kodierte
|
||||
/// Hoehe tragen - reicht aus, um die komplette Pipeline (HTTP-Fetch,
|
||||
/// PNG-Decodierung, Terrarium-Formel, Relativierung) zu pruefen, ohne dass
|
||||
/// die exakte Pixelposition innerhalb der Kachel eine Rolle spielt.
|
||||
Future<Uint8List> _uniformElevationPng(int r, int g, int b) async {
|
||||
const size = 256;
|
||||
final pixels = Uint8List(size * size * 4);
|
||||
for (var i = 0; i < size * size; i++) {
|
||||
pixels[i * 4] = r;
|
||||
pixels[i * 4 + 1] = g;
|
||||
pixels[i * 4 + 2] = b;
|
||||
pixels[i * 4 + 3] = 255;
|
||||
}
|
||||
final completer = Completer<ui.Image>();
|
||||
ui.decodeImageFromPixels(
|
||||
pixels,
|
||||
size,
|
||||
size,
|
||||
ui.PixelFormat.rgba8888,
|
||||
completer.complete,
|
||||
);
|
||||
final image = await completer.future;
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const waypoints = [
|
||||
Waypoint(lat: 48.1351, lon: 11.5820, altitudeM: 60, speedMs: 15, catchRadiusM: 60),
|
||||
Waypoint(lat: 48.1360, lon: 11.5830, altitudeM: 60, speedMs: 15, catchRadiusM: 60),
|
||||
];
|
||||
|
||||
test('fetchProfile decodiert die Terrarium-PNG in relative Hoehen', () async {
|
||||
// R=128, G=100, B=0 -> (128*256 + 100 + 0/256) - 32768 = 100.
|
||||
final pngBytes = await _uniformElevationPng(128, 100, 0);
|
||||
final client = MockClient((request) async => http.Response.bytes(pngBytes, 200));
|
||||
final service = TerrainService(client: client);
|
||||
|
||||
final profile = await service.fetchProfile(waypoints);
|
||||
|
||||
expect(profile, isNotNull);
|
||||
expect(profile!.points, isNotEmpty);
|
||||
// Gleichfoermiges Gelaende: jedes Sample hat dieselbe absolute Hoehe
|
||||
// wie das erste gueltige -> relative Hoehe ueberall 0.
|
||||
for (final p in profile.points) {
|
||||
expect(p.elevationRelM, isNotNull);
|
||||
expect(p.elevationRelM, closeTo(0, 0.01));
|
||||
}
|
||||
expect(profile.points.first.distanceM, 0);
|
||||
});
|
||||
|
||||
test('fetchProfile liefert null, wenn keine Kachel geladen werden kann', () async {
|
||||
final client = MockClient((request) async => http.Response('', 500));
|
||||
final service = TerrainService(client: client);
|
||||
|
||||
expect(await service.fetchProfile(waypoints), isNull);
|
||||
});
|
||||
|
||||
test('fetchProfile liefert null fuer eine einzelne Wegpunkt-Route', () async {
|
||||
final client = MockClient((request) async => http.Response('', 500));
|
||||
final service = TerrainService(client: client);
|
||||
|
||||
expect(await service.fetchProfile([waypoints.first]), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user