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:
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user