Adds MSP_WP (118) support to query the flightcontroller's stored home point (WP#0 is a special case for this in iNAV's getWaypoint(), per navigation.c) - the FC reports (0,0,0) rather than an error before one is set, so parseMspHomePoint() treats that pair as "unset". Rendered with a small pentagon house icon (design/homepoint-pentagon.svg, ported to a CustomPainter like the other map icons). Deliberately not polled continuously: a new home_point_provider.dart refreshes it only at the three moments the FC's home point can actually change - connect/reconnect, GPS fix acquired, and arming (iNAV's default reset_home_type=FIRST_ARM only freezes it at the first arm; before that it continuously follows the aircraft while disarmed). Mock's implementation offsets the point 25m from the anchor so it doesn't sit exactly under the drone marker during UI testing.
142 lines
4.9 KiB
Dart
142 lines
4.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../../domain/mission/route_geometry.dart';
|
|
|
|
/// Vollbild-OSM-Karte, entspricht dem Leaflet-`#map` im HTML-Demonstrator:
|
|
/// randlos, ohne Zoom-Buttons (Pinch-to-Zoom/Pan reicht), Standard-OSM-Tiles.
|
|
class MissionMap extends StatelessWidget {
|
|
const MissionMap({
|
|
super.key,
|
|
this.mapController,
|
|
this.waypointMarkers = const [],
|
|
this.windMarkers = const [],
|
|
this.routeSegments = const [],
|
|
this.rubberBandLine,
|
|
this.droneMarker,
|
|
this.homePointMarker,
|
|
this.onMapReady,
|
|
});
|
|
|
|
final MapController? mapController;
|
|
|
|
/// Kreis-Marker fuer die Wegpunkte der aktuellen Mission (Doku 3.7).
|
|
final List<CircleMarker> waypointMarkers;
|
|
|
|
/// Wind-Anzeigen je Wegpunkt (Doku 3.8), nur befuellt wenn die
|
|
/// Kopfleisten-Pille aktiv ist und fuer den jeweiligen Wegpunkt Wind
|
|
/// geladen wurde (HTML-Demonstrator: showPerWaypointWind).
|
|
final List<Marker> windMarkers;
|
|
|
|
/// Live-Position der Drohne im Fly-Modus (Doku 3.10), null solange noch
|
|
/// keine Telemetrie eingetroffen ist.
|
|
final Marker? droneMarker;
|
|
|
|
/// Homepoint-Marker im Fly-Modus (Doku: "den auf der Drohne gelockten
|
|
/// Homepoint abfragen"), null solange noch keiner abgefragt wurde/der FC
|
|
/// noch keinen gesetzt hat - siehe home_point_provider.dart.
|
|
final Marker? homePointMarker;
|
|
|
|
/// Geraden + Kreisbogen-Segmente des Flugpfads (Doku 3.7/4.6); rot
|
|
/// markierte Segmente ueberschreiten Steig-/Sinkrate oder Kurvenradius.
|
|
final List<RouteSegment> routeSegments;
|
|
|
|
/// Gestrichelte Verbindungslinie, Bedeutung je nach Modus: im Plan-Modus
|
|
/// vom letzten Wegpunkt zur aktuellen Kartenmitte, solange noch kein
|
|
/// neuer Punkt bestaetigt wurde (PlanScreen); im Fly-Modus von der
|
|
/// Drohne zum aktiven Wegpunkt (FlyScreen, TelemetryFrame.
|
|
/// activeWaypointIndex).
|
|
final List<LatLng>? rubberBandLine;
|
|
|
|
final VoidCallback? onMapReady;
|
|
|
|
// Default-Zentrum/Zoom uebernommen aus dem HTML-Demonstrator
|
|
// (Drone Mission Control.html: L.map(...).setView([52.3676, 4.9041], 16)).
|
|
static const _initialCenter = LatLng(52.3676, 4.9041);
|
|
static const _initialZoom = 16.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FlutterMap(
|
|
mapController: mapController,
|
|
options: MapOptions(
|
|
initialCenter: _initialCenter,
|
|
initialZoom: _initialZoom,
|
|
minZoom: 3,
|
|
maxZoom: 19,
|
|
// Kartendrehung (Zwei-Finger-Twist) bewusst deaktiviert - die Drohne
|
|
// fliegt nordorientiert, eine gedrehte Karte wuerde die Peilung der
|
|
// Wegpunkte/des Reticles gegenueber Kompass/HTML-Demonstrator (der
|
|
// gar keine Drehung kennt) nur verwirren.
|
|
interactionOptions: const InteractionOptions(
|
|
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
|
),
|
|
onMapReady: onMapReady,
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'de.dmc.dmc_app',
|
|
maxZoom: 19,
|
|
),
|
|
PolylineLayer(
|
|
polylines: [
|
|
for (final segment in routeSegments)
|
|
Polyline(
|
|
points: segment.points,
|
|
strokeWidth: 3,
|
|
color: segment.bad ? const Color(0xFFE2574A) : Colors.white,
|
|
),
|
|
if (rubberBandLine != null)
|
|
Polyline(
|
|
points: rubberBandLine!,
|
|
strokeWidth: 2,
|
|
color: Colors.white.withValues(alpha: 0.55),
|
|
pattern: StrokePattern.dashed(segments: const [6.0, 6.0]),
|
|
),
|
|
],
|
|
),
|
|
CircleLayer(circles: waypointMarkers),
|
|
MarkerLayer(markers: [
|
|
...windMarkers,
|
|
?homePointMarker,
|
|
?droneMarker,
|
|
]),
|
|
const _CompactAttribution(),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Schlanke Pflicht-Attribution fuer die OSM-Kacheln (Tile Usage Policy) -
|
|
/// bewusst ohne den "flutter_map |"-Praefix von SimpleAttributionWidget und
|
|
/// dezenter gestaltet, damit sie nicht wie ein zweiter Balken ueber der
|
|
/// Bottom-Bar wirkt. Bleibt sichtbar und antippbar, da die Policy sichtbare
|
|
/// Namensnennung verlangt.
|
|
class _CompactAttribution extends StatelessWidget {
|
|
const _CompactAttribution();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Align(
|
|
alignment: Alignment.bottomLeft,
|
|
child: GestureDetector(
|
|
onTap: () =>
|
|
launchUrl(Uri.parse('https://www.openstreetmap.org/copyright')),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
|
color: Colors.white.withValues(alpha: 0.5),
|
|
child: const Text(
|
|
'© OpenStreetMap contributors',
|
|
style: TextStyle(fontSize: 9, color: Colors.black87),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|