diff --git a/app/lib/domain/mission/mission_stats.dart b/app/lib/domain/mission/mission_stats.dart index 0bcbd43..fbd03a4 100644 --- a/app/lib/domain/mission/mission_stats.dart +++ b/app/lib/domain/mission/mission_stats.dart @@ -136,3 +136,16 @@ LatLng? computeRubberBandTarget({ final target = waypoints[activeWaypointIndex]; return LatLng(target.lat, target.lon); } + +/// Entfernung von der aktuellen Position zum Homepoint in Metern (Doku: +/// "Entfernung zum Home Punkt als letztes Element in der Details-Pille im +/// Fly-Modus anzeigen") - null, solange eine der beiden Positionen nicht +/// bekannt ist (kein GPS-Fix bzw. Homepoint noch nicht abgefragt, siehe +/// home_point_provider.dart). +double? computeDistanceToHomeM({ + required LatLng? currentPosition, + required LatLng? homePoint, +}) { + if (currentPosition == null || homePoint == null) return null; + return _distance(currentPosition, homePoint); +} diff --git a/app/lib/ui/screens/fly/fly_screen.dart b/app/lib/ui/screens/fly/fly_screen.dart index 73969f2..e7c624b 100644 --- a/app/lib/ui/screens/fly/fly_screen.dart +++ b/app/lib/ui/screens/fly/fly_screen.dart @@ -301,16 +301,21 @@ class _FlyScreenState extends ConsumerState { // Home der Homepoint) kommt aus computeRubberBandTarget, siehe dortige // Doku fuer den RTH-Sonderfall. final activeWpIndex = telemetry?.activeWaypointIndex; + final homePointLatLng = + homePoint == null ? null : LatLng(homePoint.lat, homePoint.lon); + final currentPosition = telemetry != null && telemetry.hasFix + ? LatLng(telemetry.lat, telemetry.lon) + : null; List? rubberBand; - if (telemetry != null && telemetry.hasFix) { + if (currentPosition != null && telemetry != null) { final target = computeRubberBandTarget( navMode: telemetry.navMode, activeWaypointIndex: activeWpIndex, waypoints: waypoints, - homePoint: homePoint == null ? null : LatLng(homePoint.lat, homePoint.lon), + homePoint: homePointLatLng, ); if (target != null) { - rubberBand = [LatLng(telemetry.lat, telemetry.lon), target]; + rubberBand = [currentPosition, target]; } } @@ -322,11 +327,19 @@ class _FlyScreenState extends ConsumerState { final flyMissionStatus = computeFlyMissionStatus( waypoints: waypoints, activeWaypointIndex: activeWpIndex, - currentLat: telemetry != null && telemetry.hasFix ? telemetry.lat : null, - currentLon: telemetry != null && telemetry.hasFix ? telemetry.lon : null, + currentLat: currentPosition?.latitude, + currentLon: currentPosition?.longitude, currentSpeedMs: telemetry?.speedMs, ); + // Entfernung zum Homepoint als letztes Element der Details-Pille (Doku: + // "zeige die Entfernung zum Home Punkt als letztes element in der + // details pille im fly modus an"). + final distanceToHomeM = computeDistanceToHomeM( + currentPosition: currentPosition, + homePoint: homePointLatLng, + ); + // Drohnen-Status fuer die Drohnen-Pille (Doku: "im fly modus zeigt die // drone pille den zustand der drohne") - das Aussehen des Fly-Modus darf // nicht am Vorhandensein eines Telemetrie-Frames haengen (sonst faellt @@ -430,6 +443,7 @@ class _FlyScreenState extends ConsumerState { totalDistanceM: missionStats.distanceM, totalDurationSec: missionStats.durationSec, flyMissionStatus: flyMissionStatus, + distanceToHomeM: distanceToHomeM, droneStatus: droneStatus, warnings: warnings, showWarningsBanner: false, diff --git a/app/lib/ui/widgets/bottom_stats_bar.dart b/app/lib/ui/widgets/bottom_stats_bar.dart index 254411e..eb78d2f 100644 --- a/app/lib/ui/widgets/bottom_stats_bar.dart +++ b/app/lib/ui/widgets/bottom_stats_bar.dart @@ -11,6 +11,7 @@ import '../providers/active_drone_profile_provider.dart'; import '../providers/altitude_profile_provider.dart'; import '../providers/mission_meta_provider.dart'; import 'drone_status_messages_panel.dart'; +import 'home_point_icon.dart'; import 'missions_drones_panel.dart'; import 'mountain_peak_icon.dart'; import 'paper_plane_icon.dart'; @@ -54,6 +55,7 @@ class BottomStatsBar extends ConsumerWidget { required this.totalDistanceM, required this.totalDurationSec, this.flyMissionStatus, + this.distanceToHomeM, this.droneStatus, required this.onTap, required this.onSettingsTap, @@ -71,6 +73,13 @@ class BottomStatsBar extends ConsumerWidget { /// die statischen Felder in der Details-Pille. final FlyMissionStatus? flyMissionStatus; + /// Nur im Fly-Modus gesetzt (Doku: "zeige die Entfernung zum Home Punkt + /// als letztes element in der details pille im fly modus an") - zeigt + /// dort ein zusaetzliches, viertes Feld nach Wegpunkt/Distanz/Zeit. Bleibt + /// null (kein zusaetzliches Feld), solange kein Homepoint bekannt ist + /// oder [flyMissionStatus] selbst null ist (Plan-Modus). + final double? distanceToHomeM; + /// Nur im Fly-Modus gesetzt (Doku: "im fly modus zeigt die drone pille /// den zustand der drohne") - ersetzt dort den Drohnennamen-Chip durch /// die 3-Icon-Statusanzeige (Verbindung/Position/Zustand). @@ -291,6 +300,26 @@ class BottomStatsBar extends ConsumerWidget { fontSize: _footerFontSize, fontWeight: _footerFontWeight, )), + // Entfernung zum Homepoint als viertes, letztes Feld (Doku: + // "als letztes element in der details pille im fly modus") - + // nur im Fly-Modus und nur, sobald ein Homepoint bekannt ist + // (siehe home_point_provider.dart), sonst kein zusaetzliches + // Feld statt eines erfundenen "—"-Platzhalters. + if (flyStatus != null && distanceToHomeM != null) ...[ + const SizedBox(width: 14), + const HomePointIcon( + size: _footerIconSize, + color: Colors.white, + strokeColor: null, + ), + const SizedBox(width: 4), + Text('${distanceToHomeM!.round()} m', + style: const TextStyle( + color: Colors.white, + fontSize: _footerFontSize, + fontWeight: _footerFontWeight, + )), + ], ], ), ), diff --git a/app/lib/ui/widgets/home_point_icon.dart b/app/lib/ui/widgets/home_point_icon.dart index ac53ab2..e6e60a7 100644 --- a/app/lib/ui/widgets/home_point_icon.dart +++ b/app/lib/ui/widgets/home_point_icon.dart @@ -1,27 +1,44 @@ import 'package:flutter/material.dart'; -/// Homepoint-Marker auf der Karte im Fly-Modus (Doku: "den auf der Drohne -/// gelockten Homepoint abfragen ... benutze dieses Icon"), Pentagon-Haus- -/// Form aus design/homepoint-pentagon.svg - nativ als [CustomPainter] statt -/// als SVG-Asset nachgebaut (Format bewusst geaendert, dasselbe Muster wie -/// DroneMarkerIcon/PaperPlaneIcon: ein einzelnes Polygon lohnt keine -/// zusaetzliche SVG-Renderer-Abhaengigkeit). +/// Homepoint-Pentagon-Haus-Form aus design/homepoint-pentagon.svg - nativ +/// als [CustomPainter] statt als SVG-Asset nachgebaut (Format bewusst +/// geaendert, dasselbe Muster wie DroneMarkerIcon/PaperPlaneIcon: ein +/// einzelnes Polygon lohnt keine zusaetzliche SVG-Renderer-Abhaengigkeit). +/// +/// Zwei Anwendungsfaelle mit unterschiedlichem Stil teilen sich dieselbe +/// Geometrie: als Kartenmarker im Fly-Modus (Doku: "den auf der Drohne +/// gelockten Homepoint abfragen ... benutze dieses Icon") mit dunkler +/// Fuellung + weissem Rand wie die anderen Kartenmarker, und als schlichtes +/// einfarbiges Inline-Icon in der Details-Pille (Doku: "gleiche Pentagon +/// Form, aber im Style von den anderen icons") ohne Rand, passend zu den +/// dortigen Material-Icons (flag/straighten/access_time). [strokeColor] +/// null waehlt Letzteres. class HomePointIcon extends StatelessWidget { - const HomePointIcon({super.key, this.size = 32}); + const HomePointIcon({ + super.key, + this.size = 32, + this.color = const Color(0xFF161616), + this.strokeColor = Colors.white, + }); final double size; + final Color color; + final Color? strokeColor; @override Widget build(BuildContext context) { return CustomPaint( size: Size.square(size), - painter: const _HomePointPainter(), + painter: _HomePointPainter(fill: color, stroke: strokeColor), ); } } class _HomePointPainter extends CustomPainter { - const _HomePointPainter(); + const _HomePointPainter({required this.fill, required this.stroke}); + + final Color fill; + final Color? stroke; // 1:1 aus design/homepoint-pentagon.svg uebernommene Koordinaten (viewBox // 0 0 72 72): Haus-Silhouette (Fuenfeck) mit spitzem Dach. @@ -33,7 +50,6 @@ class _HomePointPainter extends CustomPainter { Offset(52, 34), Offset(52, 52), ]; - static const _fill = Color(0xFF161616); static const _strokeWidth = 3.0; @override @@ -47,17 +63,21 @@ class _HomePointPainter extends CustomPainter { } path.close(); - canvas.drawPath(path, Paint()..color = _fill); - canvas.drawPath( - path, - Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = _strokeWidth * scale - ..strokeJoin = StrokeJoin.round, - ); + canvas.drawPath(path, Paint()..color = fill); + final strokeColor = stroke; + if (strokeColor != null) { + canvas.drawPath( + path, + Paint() + ..color = strokeColor + ..style = PaintingStyle.stroke + ..strokeWidth = _strokeWidth * scale + ..strokeJoin = StrokeJoin.round, + ); + } } @override - bool shouldRepaint(covariant _HomePointPainter oldDelegate) => false; + bool shouldRepaint(covariant _HomePointPainter oldDelegate) => + oldDelegate.fill != fill || oldDelegate.stroke != stroke; } diff --git a/app/lib/ui/widgets/mission_footer_bar.dart b/app/lib/ui/widgets/mission_footer_bar.dart index 4e44fa2..caa4d0c 100644 --- a/app/lib/ui/widgets/mission_footer_bar.dart +++ b/app/lib/ui/widgets/mission_footer_bar.dart @@ -24,6 +24,7 @@ class MissionFooterBar extends ConsumerWidget { required this.totalDistanceM, required this.totalDurationSec, this.flyMissionStatus, + this.distanceToHomeM, this.droneStatus, required this.warnings, this.showWarningsBanner = true, @@ -48,6 +49,11 @@ class MissionFooterBar extends ConsumerWidget { /// Distanz/Zeit dorthin. Bleibt im Plan-Modus null. final FlyMissionStatus? flyMissionStatus; + /// Nur im Fly-Modus gesetzt (Doku: "zeige die Entfernung zum Home Punkt + /// als letztes element in der details pille im fly modus an") - siehe + /// BottomStatsBar.distanceToHomeM. + final double? distanceToHomeM; + /// Nur im Fly-Modus gesetzt (Doku: "im fly modus zeigt die drone pille /// den zustand der drohne") - siehe BottomStatsBar.droneStatus. FlyScreen /// setzt hier immer einen Wert (auch ohne Telemetrie-Frame, siehe dortige @@ -127,6 +133,7 @@ class MissionFooterBar extends ConsumerWidget { totalDistanceM: totalDistanceM, totalDurationSec: totalDurationSec, flyMissionStatus: flyMissionStatus, + distanceToHomeM: distanceToHomeM, droneStatus: droneStatus, onTap: onDetailsTap, warnings: warnings, diff --git a/app/test/domain/mission_stats_test.dart b/app/test/domain/mission_stats_test.dart index a59b5d9..2fb87ab 100644 --- a/app/test/domain/mission_stats_test.dart +++ b/app/test/domain/mission_stats_test.dart @@ -187,4 +187,29 @@ void main() { ); }); }); + + group('computeDistanceToHomeM', () { + test('berechnet die Distanz aus Grosskreis', () { + final distance = computeDistanceToHomeM( + currentPosition: const LatLng(0, 0), + homePoint: const LatLng(0, 0.01), + ); + // 0.01 Grad Laengendifferenz am Aequator entspricht rund 1112 m. + expect(distance, closeTo(1112.6, 1)); + }); + + test('null ohne aktuelle Position (kein GPS-Fix)', () { + expect( + computeDistanceToHomeM(currentPosition: null, homePoint: const LatLng(0, 0)), + isNull, + ); + }); + + test('null ohne bekannten Homepoint', () { + expect( + computeDistanceToHomeM(currentPosition: const LatLng(0, 0), homePoint: null), + isNull, + ); + }); + }); }