import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../../../domain/mission/mission_stats.dart'; import '../../../domain/mission/mission_warnings.dart'; import '../../../domain/mission/route_geometry.dart'; import '../../../domain/wind/wind_math.dart'; import '../../providers/active_drone_profile_provider.dart'; import '../../providers/current_mission_provider.dart'; import '../../providers/map_controller_provider.dart'; import '../../providers/telemetry_provider.dart'; import '../../providers/terrain_provider.dart'; import '../settings/settings_screen.dart'; import '../../widgets/drone_marker_icon.dart'; import '../../widgets/mission_footer_bar.dart'; import '../../widgets/mission_map.dart'; import '../../widgets/waypoint_list_panel.dart'; import '../../widgets/warnings_panel.dart'; /// Fly-Modus (Architektur-Doku 3.10/3.11): zeigt dieselbe Karte + Fusszeile /// wie der Plan-Modus, aber ohne Reticle/Draeder - im Flug wird die Route /// beobachtet statt neu geplant. Der Kartencontroller ist derselbe /// app-lebenslange Provider wie im Plan-Modus (Doku: mapControllerProvider), /// die Kameraposition bleibt also beim Umschalten erhalten. Wegpunkte lassen /// sich weiterhin ueber die Liste anpassen (z.B. spontane Hoehenaenderung), /// nur das Karten-Reticle zum Anlegen/Verschieben entfaellt hier. class FlyScreen extends ConsumerWidget { const FlyScreen({super.key}); void _onCenterOnWaypoint(WidgetRef ref, int index) { final waypoints = ref.read(currentMissionProvider); if (index < 0 || index >= waypoints.length) return; final wp = waypoints[index]; final controller = ref.read(mapControllerProvider); controller.move(LatLng(wp.lat, wp.lon), controller.camera.zoom); } void _openWaypointList( BuildContext context, WidgetRef ref, { PanelTab initialTab = PanelTab.list, }) { Navigator.of(context).push( PageRouteBuilder( opaque: true, transitionDuration: const Duration(milliseconds: 150), reverseTransitionDuration: const Duration(milliseconds: 150), pageBuilder: (routeContext, animation, secondaryAnimation) { return FadeTransition( opacity: animation, child: WaypointListPanel( activeIndex: -1, initialTab: initialTab, onClose: () => Navigator.of(routeContext).pop(), onCenterOnWaypoint: (index) { _onCenterOnWaypoint(ref, index); Navigator.of(routeContext).pop(); }, onDelete: (index) => ref.read(currentMissionProvider.notifier).removeAt(index), onAltitudeChanged: (index, value) => ref .read(currentMissionProvider.notifier) .setAltitude(index, value), onSpeedChanged: (index, value) => ref.read(currentMissionProvider.notifier).setSpeed(index, value), onCatchRadiusChanged: (index, value) => ref .read(currentMissionProvider.notifier) .setCatchRadius(index, value), onActionChanged: (index, action) => ref.read(currentMissionProvider.notifier).setAction(index, action), ), ); }, ), ); } void _openWarningsPanel( BuildContext context, WidgetRef ref, List warnings, ) { Navigator.of(context).push(MaterialPageRoute( builder: (routeContext) => WarningsPanel( warnings: warnings, onClose: () => Navigator.of(routeContext).pop(), onWarningTap: (warning) { Navigator.of(routeContext).pop(); switch (warning.action) { case WarningAction.altitude: _openWaypointList(context, ref, initialTab: PanelTab.altitude); case WarningAction.speed: _openWaypointList(context, ref, initialTab: PanelTab.speed); case WarningAction.map: fitMapToWaypoints(ref, ref.read(currentMissionProvider)); } }, ), )); } @override Widget build(BuildContext context, WidgetRef ref) { final waypoints = ref.watch(currentMissionProvider); final droneProfile = ref.watch(activeDroneProfileProvider); final telemetry = ref.watch(telemetryProvider).value; final routeGeometry = buildRouteGeometry( waypoints, minTurnRadius: droneProfile.minTurnRadius, maxClimbRate: droneProfile.maxClimbRate, maxDescentRate: droneProfile.maxDescentRate, ); final missionStats = computeMissionStats(waypoints); final safeEdgeInset = math.max( MediaQuery.of(context).padding.left, MediaQuery.of(context).padding.right, ); final terrainProfile = ref.watch(terrainProvider).profile; final warnings = computeMissionWarnings( waypoints: waypoints, routeGeometry: routeGeometry, legWindBad: computeLegWindBad(waypoints, droneProfile.maxSpeed), droneProfile: droneProfile, totalDistanceM: missionStats.distanceM, totalDurationSec: missionStats.durationSec, terrain: terrainProfile, ); // Vor der ersten Telemetrie (Verbindungsaufbau laeuft noch, oder Mock // hat noch nicht getickt) und ohne GPS-Fix (TelemetryFrame wird bewusst // nicht auf einen Fix gegated, siehe dortige Doku - ohne Fix meldet der // FC aber oft (0,0) oder eine veraltete Position) auf dem ersten // Wegpunkt statt an einer erfundenen Stelle anzeigen. final droneMarker = telemetry != null && telemetry.hasFix ? Marker( point: LatLng(telemetry.lat, telemetry.lon), width: 28, height: 28, alignment: Alignment.center, child: DroneMarkerIcon(headingDeg: telemetry.headingDeg), ) : waypoints.isEmpty ? null : Marker( point: LatLng(waypoints.first.lat, waypoints.first.lon), width: 28, height: 28, alignment: Alignment.center, child: const DroneMarkerIcon(), ); return Stack( children: [ MissionMap( mapController: ref.watch(mapControllerProvider), routeSegments: routeGeometry.segments, waypointMarkers: [ for (var i = 0; i < waypoints.length; i++) CircleMarker( point: LatLng(waypoints[i].lat, waypoints[i].lon), radius: 6, color: i < routeGeometry.vertexBad.length && routeGeometry.vertexBad[i] ? const Color(0xFFE2574A) : const Color(0xFF2B2B28), borderStrokeWidth: 1.5, borderColor: Colors.white, ), ], droneMarker: droneMarker, ), // Live-Hoehe/-Geschwindigkeit aus der Telemetrie (HTML-Demonstrator: // #altReadout/#spdReadout im Fly-Modus) - anders als dort keine // SET-Zeile, weil es im Waypoint-Fly-Modus kein editierbares // Rad-Widget (curAlt/curSpeed) gibt, dessen Wert sinnvoll waere. Positioned( top: 56, left: 14 + safeEdgeInset, child: _liveReadout('Alt', telemetry?.altitudeM, 'm'), ), Positioned( top: 56, right: 14 + safeEdgeInset, child: _liveReadout('Speed', telemetry?.speedMs, 'm/s'), ), Positioned( left: 0, right: 0, bottom: 0, child: MissionFooterBar( waypoints: waypoints, legClimbBad: routeGeometry.legClimbBad, vertexBad: routeGeometry.vertexBad, activeIndex: -1, totalDistanceM: missionStats.distanceM, totalDurationSec: missionStats.durationSec, warnings: warnings, onDetailsTap: () => _openWaypointList(context, ref), onWarningsTap: () => _openWarningsPanel(context, ref, warnings), onSettingsTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => const SettingsScreen(), )), ), ), ], ); } Widget _liveReadout(String label, double? value, String unit) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(6), ), child: Text( '$label: ${value != null ? '${value.round()} $unit' : '—'}', style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500, ), ), ); } }