import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../models/angle_unit.dart'; import '../models/navigation_state.dart'; import '../models/tactical_sheet_type.dart'; import '../services/military_grid_utils.dart'; import '../services/offline_los_engine.dart'; import '../services/local_network_tracker.dart'; import '../widgets/interactive_map_picker_hud.dart'; import 'artillery_controller.dart'; import 'hlz_controller.dart'; import 'isochrone_controller.dart'; import 'los_controller.dart'; import 'minefield_controller.dart'; import 'navigation_controller.dart'; import 'overlays_controller.dart'; import 'resection_controller.dart'; import 'symbols_controller.dart'; import 'viewshed_controller.dart'; /// ============================================================================ /// [TacticalMapController] - المحرك الرئيسي لمنظومة الخريطة والعمليات الميدانية /// ============================================================================ /// English: /// Master GetX Controller orchestrating tactical modes, map camera state, /// declarative layers (Markers, Polylines, Polygons), and crosshair picker HUDs. /// /// العربية: /// المتحكم المركزي لمنظومة العمليات التكتيكية، يربط بين مختلف المتحكمات الفرعية /// (المدفعية، المهابط، الألغام، الشفافات، الرموز) وينظم طبقات الخريطة والمؤشر التفاعلي. /// ============================================================================ class TacticalMapController extends GetxController { // ── Sub-Controllers / المتحكمات الفرعية ────────────────────────────────── final ArtilleryController artillery = Get.find(); final HlzController hlz = Get.find(); final MinefieldController minefield = Get.find(); final IsochroneController isochrone = Get.find(); final SymbolsController symbols = Get.find(); final OverlaysController overlays = Get.find(); final LosController los = Get.find(); final ViewshedController viewshed = Get.find(); final ResectionController resection = Get.find(); final NavigationController navigation = Get.find(); final LocalNetworkTrackerService tracker = Get.find(); // ── Observables / الحالات التفاعلية ────────────────────────────────────── final RxString currentTacticalMode = 'nav'.obs; final Rx angleUnit = AngleUnit.dual.obs; final RxBool showContours = false.obs; final Rx currentGpsPosition = Rx(null); final Rx currentCameraCenter = const LatLng(31.9539, 35.9106).obs; final Rx activePickerTarget = Rx(null); final Rx pickedRouteOrigin = Rx(null); final Rx pickedRouteDestination = Rx(null); // ── Persistent Sheets / النوافذ التكتيكية السفلية المستمرة ──────────────── final Rx activeSheet = Rx(null); final RxBool isSheetMinimized = false.obs; void openSheet(ActiveTacticalSheet sheet) { activeSheet.value = sheet; isSheetMinimized.value = false; } void closeSheet() { activeSheet.value = null; isSheetMinimized.value = false; } IntaleqMapController? mapController; /// Clear All Tactical Layers & Reset Map / مسح كافة الطبقات والحسابات التكتيكية وتصفير الخريطة void clearAllTacticalLayers() { artillery.reset(); hlz.reset(); minefield.reset(); isochrone.reset(); los.reset(); viewshed.reset(); resection.reset(); navigation.reset(); symbols.reset(); pickedRouteOrigin.value = null; pickedRouteDestination.value = null; activePickerTarget.value = null; currentTacticalMode.value = 'nav'; } @override void onInit() { super.onInit(); initGps(); } /// Initialize Live GPS Sensor / تهيئة حساس الموقع الجغرافي Future initGps() async { try { final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; LocationPermission perm = await Geolocator.checkPermission(); if (perm == LocationPermission.denied) { perm = await Geolocator.requestPermission(); } if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) { // 1. Get initial position and jump camera final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 5), ), ); currentGpsPosition.value = LatLng(pos.latitude, pos.longitude); currentCameraCenter.value = currentGpsPosition.value!; tracker.updateMyPosition(currentGpsPosition.value!, pos.heading); mapController?.animateCamera( CameraUpdate.newLatLngZoom(currentGpsPosition.value!, 14.0), ); // 2. Listen to continuous GPS updates for Blue Force Tracking Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 2, // update every 2 meters ), ).listen((Position newPos) { final latLng = LatLng(newPos.latitude, newPos.longitude); currentGpsPosition.value = latLng; tracker.updateMyPosition(latLng, newPos.heading); }); } } catch (e) { debugPrint('GPS init error: $e'); } } /// Switch Tactical Operation Mode / تبديل نمط العملية التكتيكية void switchMode(String mode) { currentTacticalMode.value = mode; } /// Set Angle Unit Format / تبديل نظام قياس الزوايا والسمت (درجات / ميل) void setAngleUnit(AngleUnit unit) { angleUnit.value = unit; } /// Toggle Topographic Contours / إظهار أو إخفاء خطوط الكنتور void toggleContours(bool visible) { showContours.value = visible; } /// Set Active Picker Target / تفعيل المؤشر التفاعلي لاختيار نقطة على الخريطة void setPickerTarget(MapPickerTarget? target) { activePickerTarget.value = target; } /// Handle Map Click Tap Events / معالجة النقر على الخريطة void handleMapTap(LatLng point) { // 1. Check if placing a tactical symbol if (symbols.activePlacementType.value != null) { symbols.placeSymbolAt(point); Get.snackbar( 'الرموز العسكرية', 'تم تثبيت الرمز في الميدان بنجاح ✅', backgroundColor: const Color(0xFF22C55E), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, ); return; } // 2. Check if active picker HUD is open if (activePickerTarget.value != null) { confirmPicker(activePickerTarget.value!, point); return; } // 3. Mode-specific quick tap handlers switch (currentTacticalMode.value) { case 'viewshed_360': viewshed.setObserver(point); break; case 'artillery': if (artillery.gunPosition.value == null) { artillery.setGunPosition(point); } else { artillery.setTargetPosition(point); } break; case 'hlz': hlz.setPosition(point); break; case 'minefield': if (minefield.startPoint.value == null) { minefield.setStartPoint(point); } else { minefield.setEndPoint(point); } break; case 'isochrone': isochrone.setCenter(point); break; case 'los': if (los.observerPosition.value == null) { los.setObserver(point); } else { los.setTarget(point); } break; default: final mil = MilitaryGridUtils.fromLatLng(point); final elev = JordanDemSurface.elevationAt(point.latitude, point.longitude); Get.snackbar( 'الإحداثيات العسكرية', '${mil.arabicFullFormat} • منسوب: ${elev.round()}م', backgroundColor: const Color(0xFF0F172A), colorText: Colors.white, snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 3), ); break; } } /// Confirm Point Selection from Crosshair HUD / تأكيد النقطة المحددة بالمؤشر void confirmPicker(MapPickerTarget target, LatLng pos) { activePickerTarget.value = null; switch (target) { case MapPickerTarget.losObserver: los.setObserver(pos); break; case MapPickerTarget.losTarget: los.setTarget(pos); break; case MapPickerTarget.viewshedCenter: viewshed.setObserver(pos); break; case MapPickerTarget.artilleryGun: artillery.setGunPosition(pos); break; case MapPickerTarget.artilleryTarget: artillery.setTargetPosition(pos); break; case MapPickerTarget.hlzCenter: hlz.setPosition(pos); break; case MapPickerTarget.minefieldStart: minefield.setStartPoint(pos); break; case MapPickerTarget.minefieldEnd: minefield.setEndPoint(pos); break; case MapPickerTarget.isochroneCenter: isochrone.setCenter(pos); break; case MapPickerTarget.resectionLandmark: resection.setMapLandmark(pos.latitude, pos.longitude); switchMode('resection_cam'); break; case MapPickerTarget.routeOrigin: pickedRouteOrigin.value = pos; switchMode('routing'); break; case MapPickerTarget.routeDestination: pickedRouteDestination.value = pos; switchMode('routing'); final origin = pickedRouteOrigin.value ?? currentGpsPosition.value ?? const LatLng(31.9539, 35.9106); navigation.calculateRoute( origin: origin, destination: pos, mapController: mapController, ).then((plan) { if (plan != null && plan.polylinePoints.isNotEmpty) { fitBounds(plan.polylinePoints); } }); break; } if (activeSheet.value != null) { isSheetMinimized.value = false; } } /// Cancel Picker / إلغاء وضع المؤشر void cancelPicker() { activePickerTarget.value = null; } /// Fit Map Camera to Bounding Box of Points / تحريك الكاميرا لتحتوي النقاط void fitBounds(List points) { if (points.isEmpty || mapController == null) return; double minLat = points.first.latitude; double maxLat = points.first.latitude; double minLng = points.first.longitude; double maxLng = points.first.longitude; for (final p in points) { if (p.latitude < minLat) minLat = p.latitude; if (p.latitude > maxLat) maxLat = p.latitude; if (p.longitude < minLng) minLng = p.longitude; if (p.longitude > maxLng) maxLng = p.longitude; } final bounds = LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ); mapController?.animateCamera( CameraUpdate.newLatLngBounds( bounds, left: 40.0, top: 60.0, right: 40.0, bottom: 120.0, ), ); } /// Get Mode Title / الحصول على عنوان النمط الميداني الحالي String getModeTitle() { switch (currentTacticalMode.value) { case 'los': return 'تبادل الرؤية والمراقبة (LOS)'; case 'viewshed_360': return 'رادار الرصد الدائري 360°'; case 'artillery': return 'رماية المدفعية وقوس القذيفة'; case 'hlz': return 'مهابط المروحيات (HLZ)'; case 'minefield': return 'حقول الألغام وممرات العبور'; case 'isochrone': return 'نطاق التدخل السريع (QRF)'; case 'symbols': return 'الرموز والتشكيلات العسكرية'; case 'overlays': return 'منظومة الشفافات (IPB)'; case 'routing': return 'توجيه القوافل التكتيكي'; case 'rangefinder': return 'قياس المسافة البصري (بدون GPS)'; default: return 'منظومة العمليات الميدانية (Off-Grid)'; } } /// Build all declarative map markers / تجميع كافة علامات الخريطة Set buildMarkers(ActiveNavigationState navState) { final markers = {}; // 1. Landmark & Observation Markers for (final obs in resection.observations) { markers.add( Marker( markerId: MarkerId(obs.landmark.id), position: LatLng(obs.landmark.lat, obs.landmark.lng), icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure), infoWindow: InfoWindow( title: obs.landmark.name, snippet: AngleFormatter.format(obs.trueAzimuthDeg, angleUnit.value), ), ), ); } // 2. Resection Fix Marker (موقع الراصد المحسوب - ماركر بارز باللون الأخضر التكتيكي) if (resection.resectionResult.value != null && !navState.isNavigating) { final res = resection.resectionResult.value!; markers.add( Marker( markerId: const MarkerId('observer_calculated_position'), position: LatLng(res.lat, res.lng), icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen), infoWindow: InfoWindow( title: '🎯 موقعك المحسوب (GPS-Denied Fix)', snippet: 'خطأ الرصد: ±${res.estimatedAccuracyMeters.toStringAsFixed(1)}م • دقة تكتيكية عالية', ), ), ); } // 3. Routing Destination Marker if (navigation.activeRoute.value != null && navigation.activeRoute.value!.polylinePoints.isNotEmpty) { markers.add( Marker( markerId: const MarkerId('convoy_destination'), position: navigation.activeRoute.value!.polylinePoints.last, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed), infoWindow: const InfoWindow( title: 'الهدف التكتيكي (Objective)', snippet: 'نقطة الوصول المحددة', ), ), ); } // 4. Moving Vehicle Marker if (navState.isNavigating && navState.currentPosition != null) { markers.add( Marker( markerId: const MarkerId('active_vehicle_position'), position: navState.currentPosition!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan), infoWindow: InfoWindow( title: 'مركبة العمليات الميدانية', snippet: '${navState.currentSpeedKmH.round()} كم/س • اتجاه ${navState.currentHeadingDeg.round()}°', ), ), ); } // 5. Tactical Placed Symbols for (final sym in symbols.placedSymbols) { markers.add( Marker( markerId: MarkerId(sym.id), position: sym.position, icon: InlqBitmap.defaultMarkerWithHue( sym.isEnemy ? InlqBitmap.hueRed : InlqBitmap.hueBlue, ), infoWindow: InfoWindow( title: sym.name, snippet: MilitaryGridUtils.latLngToMgrs( sym.position.latitude, sym.position.longitude), ), ), ); } // 6. Artillery Gun & Target Markers if (currentTacticalMode.value == 'artillery') { if (artillery.gunPosition.value != null) { markers.add( Marker( markerId: const MarkerId('artillery_gun_marker'), position: artillery.gunPosition.value!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueBlue), infoWindow: const InfoWindow(title: '🎯 مربض المدفعية'), ), ); } if (artillery.targetPosition.value != null) { markers.add( Marker( markerId: const MarkerId('artillery_target_marker'), position: artillery.targetPosition.value!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed), infoWindow: const InfoWindow(title: '💥 الهدف المعادي'), ), ); } } // 7. HLZ Center Marker if (currentTacticalMode.value == 'hlz' && hlz.selectedPosition.value != null) { markers.add( Marker( markerId: const MarkerId('hlz_center_marker'), position: hlz.selectedPosition.value!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen), infoWindow: const InfoWindow(title: '🚁 مهبط الطيران العامودي (HLZ)'), ), ); } // --- Blue Force Tracking (Friendly Units) --- for (final unit in tracker.friendlyUnits.values) { markers.add( Marker( markerId: MarkerId('bft_${unit.deviceId}'), position: unit.position, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure), // Blue for friendly infoWindow: InfoWindow( title: '${unit.callsign} (${unit.role.name})', snippet: 'آخر ظهور: منذ ${DateTime.now().difference(unit.lastSeen).inSeconds} ثوانٍ', ), ), ); } return markers; } /// Build all declarative map polylines / تجميع مسارات الخريطة Set buildPolylines(ActiveNavigationState navState) { final polylines = {}; // 1. Tactical Line of Sight (LOS) if (currentTacticalMode.value == 'los' && los.observerPosition.value != null && los.targetPosition.value != null && los.losReport.value != null && los.losReport.value!.profile.length > 1) { final visSegments = los.losReport.value!.visibleSegments; for (int i = 0; i < visSegments.length; i++) { polylines.add( Polyline( polylineId: PolylineId('tactical_los_vis_$i'), points: visSegments[i], color: const Color(0xFF22C55E), width: 5.0, ), ); } final blkSegments = los.losReport.value!.blockedSegments; for (int i = 0; i < blkSegments.length; i++) { polylines.add( Polyline( polylineId: PolylineId('tactical_los_blk_$i'), points: blkSegments[i], color: const Color(0xFFEF4444), width: 5.0, ), ); } } // 2. Artillery Ballistic Arc Ground Track if (currentTacticalMode.value == 'artillery' && artillery.firingSolution.value != null) { final points = artillery.firingSolution.value!.trajectoryProfile .map((p) => p.coordinate) .toList(); polylines.add( Polyline( polylineId: const PolylineId('artillery_trajectory_track'), points: points, color: artillery.firingSolution.value!.isCrestClear ? const Color(0xFF00F0FF) : const Color(0xFFEF4444), width: 4.5, ), ); } // 3. Minefield Safe Breaching Lane Centerline if (currentTacticalMode.value == 'minefield' && minefield.zoneResult.value != null) { polylines.add( Polyline( polylineId: const PolylineId('minefield_breach_centerline'), points: minefield.zoneResult.value!.breachLaneCenterline, color: const Color(0xFF10B981), width: 4.0, ), ); } // 4. Tactical Overlays Lines for (final layer in overlays.overlayLayers) { if (layer.isVisible) { for (int i = 0; i < layer.lines.length; i++) { polylines.add( Polyline( polylineId: PolylineId('overlay_${layer.id}_line_$i'), points: layer.lines[i], color: layer.color, width: 4.0, ), ); } } } // 5. Active Route Polyline if (navigation.activeRoute.value != null && navigation.activeRoute.value!.polylinePoints.isNotEmpty) { polylines.add( Polyline( polylineId: const PolylineId('tactical_on_device_route'), points: navigation.activeRoute.value!.polylinePoints, color: navState.isNavigating ? const Color(0xFF00F0FF) : const Color(0xFF38BDF8), width: navState.isNavigating ? 6.5 : 5.0, ), ); } // 6. Visual Resection Sightlines (خطوط الرؤية البصرية للمعالم المرصودة) if (resection.resectionResult.value != null && resection.observations.isNotEmpty) { final res = resection.resectionResult.value!; final observerPos = LatLng(res.lat, res.lng); for (int i = 0; i < resection.observations.length; i++) { final obs = resection.observations[i]; final landmarkPos = LatLng(obs.landmark.lat, obs.landmark.lng); polylines.add( Polyline( polylineId: PolylineId('resection_sightline_$i'), points: [observerPos, landmarkPos], color: const Color(0xFF00F0FF), width: 3.5, ), ); } } return polylines; } /// Build all declarative map polygons / تجميع مضلعات الخريطة Set buildPolygons() { final polygons = {}; // 1. 360 Viewshed Radar Visible Polygon Fill if (currentTacticalMode.value == 'viewshed_360' && viewshed.viewshedReport.value != null && viewshed.viewshedReport.value!.polygonVertices.isNotEmpty) { polygons.add( Polygon( polygonId: const PolygonId('viewshed_360_fill'), points: viewshed.viewshedReport.value!.polygonVertices, fillColor: const Color(0x4422C55E), strokeColor: const Color(0xFF22C55E), strokeWidth: 2, ), ); } // 2. HLZ Pad Boundary & Approach Corridor if (currentTacticalMode.value == 'hlz' && hlz.assessmentResult.value != null) { polygons.add( Polygon( polygonId: const PolygonId('hlz_pad_boundary'), points: hlz.assessmentResult.value!.padBoundary, fillColor: hlz.assessmentResult.value!.gradeColor.withAlpha(50), strokeColor: hlz.assessmentResult.value!.gradeColor, strokeWidth: 2, ), ); polygons.add( Polygon( polygonId: const PolygonId('hlz_approach_corridor'), points: hlz.assessmentResult.value!.approachFunnel, fillColor: const Color(0x3338BDF8), strokeColor: const Color(0xFF38BDF8), strokeWidth: 1.5, ), ); } // 3. Minefield Boundary & Safe Breaching Lane if (currentTacticalMode.value == 'minefield' && minefield.zoneResult.value != null) { polygons.add( Polygon( polygonId: const PolygonId('minefield_threat_boundary'), points: minefield.zoneResult.value!.boundaryPolygon, fillColor: const Color(0x44EF4444), strokeColor: const Color(0xFFEF4444), strokeWidth: 2, ), ); polygons.add( Polygon( polygonId: const PolygonId('minefield_safe_breach_polygon'), points: minefield.zoneResult.value!.breachLanePolygon, fillColor: const Color(0x3310B981), strokeColor: const Color(0xFF10B981), strokeWidth: 2, ), ); } // 4. Resection Position Uncertainty Ring (دائرة دقة الموقع التكتيكي الأخضر) if (resection.resectionResult.value != null) { final res = resection.resectionResult.value!; final radiusMeters = math.max(25.0, res.estimatedAccuracyMeters); final ringPoints = []; for (int a = 0; a <= 360; a += 15) { final rad = a * (math.pi / 180.0); final dLat = (radiusMeters / 6371000.0) * (180.0 / math.pi); final dLng = (radiusMeters / 6371000.0) * (180.0 / math.pi) / math.cos(res.lat * math.pi / 180.0); ringPoints.add(LatLng(res.lat + dLat * math.sin(rad), res.lng + dLng * math.cos(rad))); } polygons.add( Polygon( polygonId: const PolygonId('resection_accuracy_circle'), points: ringPoints, fillColor: const Color(0x3322C55E), strokeColor: const Color(0xFF22C55E), strokeWidth: 2.0, ), ); } // 5. Isochrone Response Time Rings if (currentTacticalMode.value == 'isochrone' && isochrone.isochroneRings.isNotEmpty) { for (int i = 0; i < isochrone.isochroneRings.length; i++) { final ring = isochrone.isochroneRings[i]; polygons.add( Polygon( polygonId: PolygonId('isochrone_ring_$i'), points: ring.polygonCoordinates, fillColor: ring.ringColor.withAlpha(30), strokeColor: ring.ringColor, strokeWidth: 2, ), ); } } // 6. Tactical Overlays Polygons for (final layer in overlays.overlayLayers) { if (layer.isVisible) { for (int i = 0; i < layer.polygons.length; i++) { polygons.add( Polygon( polygonId: PolygonId('overlay_${layer.id}_poly_$i'), points: layer.polygons[i], fillColor: layer.color.withAlpha(40), strokeColor: layer.color, strokeWidth: 2, ), ); } } } return polygons; } }