Show distance to home as a fourth field in the fly-mode details pill

Adds computeDistanceToHomeM() (mission_stats.dart) alongside the
existing rubber-band-target helper, wired through MissionFooterBar ->
BottomStatsBar the same way flyMissionStatus already is. Only appears
once a home point is known, as the last field after waypoint/distance/
duration.

Generalized HomePointIcon to take an optional strokeColor: the map
marker keeps its dark-fill-plus-white-outline look, while the details
pill uses a plain white fill (strokeColor: null) to match the existing
monochrome flag/ruler/clock icons instead of looking like a dropped-in
map pin.
This commit is contained in:
Constantin Leue
2026-08-09 21:49:03 +02:00
parent afc380cabc
commit 6b7977eca1
6 changed files with 133 additions and 25 deletions
+13
View File
@@ -136,3 +136,16 @@ LatLng? computeRubberBandTarget({
final target = waypoints[activeWaypointIndex]; final target = waypoints[activeWaypointIndex];
return LatLng(target.lat, target.lon); 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);
}
+19 -5
View File
@@ -301,16 +301,21 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
// Home der Homepoint) kommt aus computeRubberBandTarget, siehe dortige // Home der Homepoint) kommt aus computeRubberBandTarget, siehe dortige
// Doku fuer den RTH-Sonderfall. // Doku fuer den RTH-Sonderfall.
final activeWpIndex = telemetry?.activeWaypointIndex; 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<LatLng>? rubberBand; List<LatLng>? rubberBand;
if (telemetry != null && telemetry.hasFix) { if (currentPosition != null && telemetry != null) {
final target = computeRubberBandTarget( final target = computeRubberBandTarget(
navMode: telemetry.navMode, navMode: telemetry.navMode,
activeWaypointIndex: activeWpIndex, activeWaypointIndex: activeWpIndex,
waypoints: waypoints, waypoints: waypoints,
homePoint: homePoint == null ? null : LatLng(homePoint.lat, homePoint.lon), homePoint: homePointLatLng,
); );
if (target != null) { if (target != null) {
rubberBand = [LatLng(telemetry.lat, telemetry.lon), target]; rubberBand = [currentPosition, target];
} }
} }
@@ -322,11 +327,19 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
final flyMissionStatus = computeFlyMissionStatus( final flyMissionStatus = computeFlyMissionStatus(
waypoints: waypoints, waypoints: waypoints,
activeWaypointIndex: activeWpIndex, activeWaypointIndex: activeWpIndex,
currentLat: telemetry != null && telemetry.hasFix ? telemetry.lat : null, currentLat: currentPosition?.latitude,
currentLon: telemetry != null && telemetry.hasFix ? telemetry.lon : null, currentLon: currentPosition?.longitude,
currentSpeedMs: telemetry?.speedMs, 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 // Drohnen-Status fuer die Drohnen-Pille (Doku: "im fly modus zeigt die
// drone pille den zustand der drohne") - das Aussehen des Fly-Modus darf // drone pille den zustand der drohne") - das Aussehen des Fly-Modus darf
// nicht am Vorhandensein eines Telemetrie-Frames haengen (sonst faellt // nicht am Vorhandensein eines Telemetrie-Frames haengen (sonst faellt
@@ -430,6 +443,7 @@ class _FlyScreenState extends ConsumerState<FlyScreen> {
totalDistanceM: missionStats.distanceM, totalDistanceM: missionStats.distanceM,
totalDurationSec: missionStats.durationSec, totalDurationSec: missionStats.durationSec,
flyMissionStatus: flyMissionStatus, flyMissionStatus: flyMissionStatus,
distanceToHomeM: distanceToHomeM,
droneStatus: droneStatus, droneStatus: droneStatus,
warnings: warnings, warnings: warnings,
showWarningsBanner: false, showWarningsBanner: false,
+29
View File
@@ -11,6 +11,7 @@ import '../providers/active_drone_profile_provider.dart';
import '../providers/altitude_profile_provider.dart'; import '../providers/altitude_profile_provider.dart';
import '../providers/mission_meta_provider.dart'; import '../providers/mission_meta_provider.dart';
import 'drone_status_messages_panel.dart'; import 'drone_status_messages_panel.dart';
import 'home_point_icon.dart';
import 'missions_drones_panel.dart'; import 'missions_drones_panel.dart';
import 'mountain_peak_icon.dart'; import 'mountain_peak_icon.dart';
import 'paper_plane_icon.dart'; import 'paper_plane_icon.dart';
@@ -54,6 +55,7 @@ class BottomStatsBar extends ConsumerWidget {
required this.totalDistanceM, required this.totalDistanceM,
required this.totalDurationSec, required this.totalDurationSec,
this.flyMissionStatus, this.flyMissionStatus,
this.distanceToHomeM,
this.droneStatus, this.droneStatus,
required this.onTap, required this.onTap,
required this.onSettingsTap, required this.onSettingsTap,
@@ -71,6 +73,13 @@ class BottomStatsBar extends ConsumerWidget {
/// die statischen Felder in der Details-Pille. /// die statischen Felder in der Details-Pille.
final FlyMissionStatus? flyMissionStatus; 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 /// Nur im Fly-Modus gesetzt (Doku: "im fly modus zeigt die drone pille
/// den zustand der drohne") - ersetzt dort den Drohnennamen-Chip durch /// den zustand der drohne") - ersetzt dort den Drohnennamen-Chip durch
/// die 3-Icon-Statusanzeige (Verbindung/Position/Zustand). /// die 3-Icon-Statusanzeige (Verbindung/Position/Zustand).
@@ -291,6 +300,26 @@ class BottomStatsBar extends ConsumerWidget {
fontSize: _footerFontSize, fontSize: _footerFontSize,
fontWeight: _footerFontWeight, 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,
)),
],
], ],
), ),
), ),
+40 -20
View File
@@ -1,27 +1,44 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Homepoint-Marker auf der Karte im Fly-Modus (Doku: "den auf der Drohne /// Homepoint-Pentagon-Haus-Form aus design/homepoint-pentagon.svg - nativ
/// gelockten Homepoint abfragen ... benutze dieses Icon"), Pentagon-Haus- /// als [CustomPainter] statt als SVG-Asset nachgebaut (Format bewusst
/// Form aus design/homepoint-pentagon.svg - nativ als [CustomPainter] statt /// geaendert, dasselbe Muster wie DroneMarkerIcon/PaperPlaneIcon: ein
/// als SVG-Asset nachgebaut (Format bewusst geaendert, dasselbe Muster wie /// einzelnes Polygon lohnt keine zusaetzliche SVG-Renderer-Abhaengigkeit).
/// 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 { 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 double size;
final Color color;
final Color? strokeColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint( return CustomPaint(
size: Size.square(size), size: Size.square(size),
painter: const _HomePointPainter(), painter: _HomePointPainter(fill: color, stroke: strokeColor),
); );
} }
} }
class _HomePointPainter extends CustomPainter { 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 // 1:1 aus design/homepoint-pentagon.svg uebernommene Koordinaten (viewBox
// 0 0 72 72): Haus-Silhouette (Fuenfeck) mit spitzem Dach. // 0 0 72 72): Haus-Silhouette (Fuenfeck) mit spitzem Dach.
@@ -33,7 +50,6 @@ class _HomePointPainter extends CustomPainter {
Offset(52, 34), Offset(52, 34),
Offset(52, 52), Offset(52, 52),
]; ];
static const _fill = Color(0xFF161616);
static const _strokeWidth = 3.0; static const _strokeWidth = 3.0;
@override @override
@@ -47,17 +63,21 @@ class _HomePointPainter extends CustomPainter {
} }
path.close(); path.close();
canvas.drawPath(path, Paint()..color = _fill); canvas.drawPath(path, Paint()..color = fill);
canvas.drawPath( final strokeColor = stroke;
path, if (strokeColor != null) {
Paint() canvas.drawPath(
..color = Colors.white path,
..style = PaintingStyle.stroke Paint()
..strokeWidth = _strokeWidth * scale ..color = strokeColor
..strokeJoin = StrokeJoin.round, ..style = PaintingStyle.stroke
); ..strokeWidth = _strokeWidth * scale
..strokeJoin = StrokeJoin.round,
);
}
} }
@override @override
bool shouldRepaint(covariant _HomePointPainter oldDelegate) => false; bool shouldRepaint(covariant _HomePointPainter oldDelegate) =>
oldDelegate.fill != fill || oldDelegate.stroke != stroke;
} }
@@ -24,6 +24,7 @@ class MissionFooterBar extends ConsumerWidget {
required this.totalDistanceM, required this.totalDistanceM,
required this.totalDurationSec, required this.totalDurationSec,
this.flyMissionStatus, this.flyMissionStatus,
this.distanceToHomeM,
this.droneStatus, this.droneStatus,
required this.warnings, required this.warnings,
this.showWarningsBanner = true, this.showWarningsBanner = true,
@@ -48,6 +49,11 @@ class MissionFooterBar extends ConsumerWidget {
/// Distanz/Zeit dorthin. Bleibt im Plan-Modus null. /// Distanz/Zeit dorthin. Bleibt im Plan-Modus null.
final FlyMissionStatus? flyMissionStatus; 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 /// Nur im Fly-Modus gesetzt (Doku: "im fly modus zeigt die drone pille
/// den zustand der drohne") - siehe BottomStatsBar.droneStatus. FlyScreen /// den zustand der drohne") - siehe BottomStatsBar.droneStatus. FlyScreen
/// setzt hier immer einen Wert (auch ohne Telemetrie-Frame, siehe dortige /// setzt hier immer einen Wert (auch ohne Telemetrie-Frame, siehe dortige
@@ -127,6 +133,7 @@ class MissionFooterBar extends ConsumerWidget {
totalDistanceM: totalDistanceM, totalDistanceM: totalDistanceM,
totalDurationSec: totalDurationSec, totalDurationSec: totalDurationSec,
flyMissionStatus: flyMissionStatus, flyMissionStatus: flyMissionStatus,
distanceToHomeM: distanceToHomeM,
droneStatus: droneStatus, droneStatus: droneStatus,
onTap: onDetailsTap, onTap: onDetailsTap,
warnings: warnings, warnings: warnings,
+25
View File
@@ -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,
);
});
});
} }