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.onMapReady, }); final MapController? mapController; /// Kreis-Marker fuer die Wegpunkte der aktuellen Mission (Doku 3.7). final List 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 windMarkers; /// Live-Position der Drohne im Fly-Modus (Doku 3.10), null solange noch /// keine Telemetrie eingetroffen ist. final Marker? droneMarker; /// Geraden + Kreisbogen-Segmente des Flugpfads (Doku 3.7/4.6); rot /// markierte Segmente ueberschreiten Steig-/Sinkrate oder Kurvenradius. final List 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? 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, ?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), ), ), ), ), ); } }