From 17e8f8eeac098c3ca5d0aa3a1a41d2125c6393f6 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 22 Aug 2026 15:27:53 +0300 Subject: [PATCH] refactor(tactical-app): modernize state management with GetX controllers, bindings, and clean MVP architecture --- .../lib/bindings/tactical_binding.dart | 43 + .../lib/controllers/artillery_controller.dart | 90 + .../lib/controllers/hlz_controller.dart | 73 + .../lib/controllers/isochrone_controller.dart | 61 + .../lib/controllers/los_controller.dart | 81 + .../lib/controllers/minefield_controller.dart | 81 + .../controllers/navigation_controller.dart | 99 + .../lib/controllers/overlays_controller.dart | 85 + .../lib/controllers/resection_controller.dart | 46 + .../lib/controllers/symbols_controller.dart | 80 + .../controllers/tactical_map_controller.dart | 605 ++++++ .../lib/controllers/viewshed_controller.dart | 70 + packages/tactical_app/lib/main.dart | 17 +- .../lib/screens/tactical_map_screen.dart | 1711 ++++------------- packages/tactical_app/pubspec.lock | 8 + packages/tactical_app/pubspec.yaml | 1 + 16 files changed, 1855 insertions(+), 1296 deletions(-) create mode 100644 packages/tactical_app/lib/bindings/tactical_binding.dart create mode 100644 packages/tactical_app/lib/controllers/artillery_controller.dart create mode 100644 packages/tactical_app/lib/controllers/hlz_controller.dart create mode 100644 packages/tactical_app/lib/controllers/isochrone_controller.dart create mode 100644 packages/tactical_app/lib/controllers/los_controller.dart create mode 100644 packages/tactical_app/lib/controllers/minefield_controller.dart create mode 100644 packages/tactical_app/lib/controllers/navigation_controller.dart create mode 100644 packages/tactical_app/lib/controllers/overlays_controller.dart create mode 100644 packages/tactical_app/lib/controllers/resection_controller.dart create mode 100644 packages/tactical_app/lib/controllers/symbols_controller.dart create mode 100644 packages/tactical_app/lib/controllers/tactical_map_controller.dart create mode 100644 packages/tactical_app/lib/controllers/viewshed_controller.dart diff --git a/packages/tactical_app/lib/bindings/tactical_binding.dart b/packages/tactical_app/lib/bindings/tactical_binding.dart new file mode 100644 index 0000000..13e7f72 --- /dev/null +++ b/packages/tactical_app/lib/bindings/tactical_binding.dart @@ -0,0 +1,43 @@ +import 'package:get/get.dart'; +import '../controllers/artillery_controller.dart'; +import '../controllers/hlz_controller.dart'; +import '../controllers/isochrone_controller.dart'; +import '../controllers/los_controller.dart'; +import '../controllers/minefield_controller.dart'; +import '../controllers/navigation_controller.dart'; +import '../controllers/overlays_controller.dart'; +import '../controllers/resection_controller.dart'; +import '../controllers/symbols_controller.dart'; +import '../controllers/tactical_map_controller.dart'; +import '../controllers/viewshed_controller.dart'; + +/// ============================================================================ +/// [TacticalBinding] - تهيئة وحقن التبعيات والمتحكمات (GetX Dependency Injection) +/// ============================================================================ +/// English: +/// Master GetX Binding registering all tactical operation controllers +/// for seamless dependency injection and lifecycle management across the app. +/// +/// العربية: +/// مسجل التبعيات المركزي لـ GetX، يقوم بإنشاء وحقن كافة المتحكمات التكتيكية +/// لضمان استقرار دورة حياة التطبيق وإمكانية الوصول إليها من أي شاشة أو نافذة. +/// ============================================================================ +class TacticalBinding extends Bindings { + @override + void dependencies() { + // 1. Core Tactical Sub-Controllers + Get.lazyPut(() => ArtilleryController(), fenix: true); + Get.lazyPut(() => HlzController(), fenix: true); + Get.lazyPut(() => MinefieldController(), fenix: true); + Get.lazyPut(() => IsochroneController(), fenix: true); + Get.lazyPut(() => SymbolsController(), fenix: true); + Get.lazyPut(() => OverlaysController(), fenix: true); + Get.lazyPut(() => LosController(), fenix: true); + Get.lazyPut(() => ViewshedController(), fenix: true); + Get.lazyPut(() => ResectionController(), fenix: true); + Get.lazyPut(() => NavigationController(), fenix: true); + + // 2. Master Map Orchestrator Controller + Get.lazyPut(() => TacticalMapController(), fenix: true); + } +} diff --git a/packages/tactical_app/lib/controllers/artillery_controller.dart b/packages/tactical_app/lib/controllers/artillery_controller.dart new file mode 100644 index 0000000..2ae28d3 --- /dev/null +++ b/packages/tactical_app/lib/controllers/artillery_controller.dart @@ -0,0 +1,90 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../models/military_operations_models.dart'; +import '../services/artillery_ballistics_engine.dart'; + +/// ============================================================================ +/// [ArtilleryController] - وحدة التحكم برماية المدفعية وقوس القذيفة +/// ============================================================================ +/// English: +/// GetX Controller managing artillery firing mission parameters, ballistic +/// trajectory calculations, weapon profiles, and crest clearance checks. +/// +/// العربية: +/// متحكم GetX لإدارة معايير رماية المدفعية الميدانية، حلول الرماية البالستية، +/// اختيار أنظمة السلاح، وفحص سلامة المسار من الاصطدام بقمم الجبال (Crest Clearance). +/// ============================================================================ +class ArtilleryController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx gunPosition = Rx(null); + final Rx targetPosition = Rx(null); + final Rx selectedWeapon = + Rx(ArtilleryWeaponSystem.standardSystems.first); + final RxBool highAngle = false.obs; + final RxBool isLoading = false.obs; + final Rx firingSolution = + Rx(null); + + /// Set Battery Gun Position / تعيين موقع مربض المدفعية + void setGunPosition(LatLng pos) { + gunPosition.value = pos; + calculateFiringSolution(); + } + + /// Set Enemy Target Position / تعيين موقع الهدف المعادي + void setTargetPosition(LatLng pos) { + targetPosition.value = pos; + calculateFiringSolution(); + } + + /// Swap Gun and Target Positions / تبديل مربض المدفعية مع الهدف + void swapPositions() { + final temp = gunPosition.value; + gunPosition.value = targetPosition.value; + targetPosition.value = temp; + calculateFiringSolution(); + } + + /// Select Weapon Profile / اختيار نوع نظام المدفعية أو الهاون + void selectWeapon(ArtilleryWeaponSystem weapon) { + selectedWeapon.value = weapon; + calculateFiringSolution(); + } + + /// Toggle High-Angle Trajectory / التبديل بين قوس الرماية المنخفض والعالي + void toggleHighAngle(bool value) { + highAngle.value = value; + calculateFiringSolution(); + } + + /// Calculate Ballistic Firing Solution / احتساب الحل البالستي الشامل + Future calculateFiringSolution() async { + if (gunPosition.value == null || targetPosition.value == null) { + firingSolution.value = null; + return; + } + + try { + isLoading.value = true; + final sol = await ArtilleryBallisticsEngine.calculateFireMission( + weapon: selectedWeapon.value, + gunPos: gunPosition.value!, + targetPos: targetPosition.value!, + highAngle: highAngle.value, + ); + firingSolution.value = sol; + } catch (e) { + firingSolution.value = null; + } finally { + isLoading.value = false; + } + } + + /// Clear artillery state / إعادة ضبط رماية المدفعية + void reset() { + gunPosition.value = null; + targetPosition.value = null; + firingSolution.value = null; + highAngle.value = false; + } +} diff --git a/packages/tactical_app/lib/controllers/hlz_controller.dart b/packages/tactical_app/lib/controllers/hlz_controller.dart new file mode 100644 index 0000000..5e23cad --- /dev/null +++ b/packages/tactical_app/lib/controllers/hlz_controller.dart @@ -0,0 +1,73 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../models/military_operations_models.dart'; +import '../services/hlz_assessment_engine.dart'; + +/// ============================================================================ +/// [HlzController] - وحدة التحكم بتقييم مهابط الطيران العامودي +/// ============================================================================ +/// English: +/// GetX Controller managing Helicopter Landing Zone (HLZ) parameters, terrain +/// slope gradient calculations, approach corridor clearances, and suitability scoring. +/// +/// العربية: +/// متحكم GetX لإدارة تقييم مهابط المروحيات العسكرية، فحص نسبة انحدار الأرض، +/// سلامة ممرات الاقتراب والإقلاع، وتحديد درجة صلاحية المهبط تكتيكياً. +/// ============================================================================ +class HlzController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx selectedPosition = Rx(null); + final Rx helicopterType = + Rx(HelicopterType.mediumLift); + final RxDouble approachAzimuthDeg = 0.0.obs; + final RxBool isLoading = false.obs; + final Rx assessmentResult = + Rx(null); + + /// Set Landing Pad Center Location / تعيين موقع مركز المهبط المقترح + void setPosition(LatLng pos) { + selectedPosition.value = pos; + assessLandingZone(); + } + + /// Select Helicopter Airframe Type / تحديد فئة الطوافة (خفيفة / متوسطة / ثقيلة) + void setHelicopterType(HelicopterType type) { + helicopterType.value = type; + assessLandingZone(); + } + + /// Set Approach Corridor Azimuth / تحديد سمت ممر الاقتراب + void setApproachAzimuth(double azimuthDeg) { + approachAzimuthDeg.value = azimuthDeg; + assessLandingZone(); + } + + /// Run Full HLZ Assessment / تنفيذ التقييم الميداني الشامل للمهبط + Future assessLandingZone() async { + if (selectedPosition.value == null) { + assessmentResult.value = null; + return; + } + + try { + isLoading.value = true; + final res = await HlzAssessmentEngine.assessLandingZone( + center: selectedPosition.value!, + helicopterType: helicopterType.value, + approachAzimuthDeg: approachAzimuthDeg.value, + ); + assessmentResult.value = res; + } catch (e) { + assessmentResult.value = null; + } finally { + isLoading.value = false; + } + } + + /// Reset HLZ state / إعادة ضبط المهبط + void reset() { + selectedPosition.value = null; + assessmentResult.value = null; + approachAzimuthDeg.value = 0.0; + } +} diff --git a/packages/tactical_app/lib/controllers/isochrone_controller.dart b/packages/tactical_app/lib/controllers/isochrone_controller.dart new file mode 100644 index 0000000..6e0a9e5 --- /dev/null +++ b/packages/tactical_app/lib/controllers/isochrone_controller.dart @@ -0,0 +1,61 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../services/tactical_isochrone_engine.dart'; + +/// ============================================================================ +/// [IsochroneController] - وحدة التحكم بنطاق التدخل السريع وزمن الاستجابة +/// ============================================================================ +/// English: +/// GetX Controller managing Quick Reaction Force (QRF) and emergency reachability +/// isochrone polygons with terrain slope resistance factors. +/// +/// العربية: +/// متحكم GetX لإدارة مضلعات زمن الاستجابة لقوات التدخل السريع (QRF) والإسعاف، +/// مع احتساب تأثير انحدار التضاريس ومقاومة حركة الآليات. +/// ============================================================================ +class IsochroneController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx center = Rx(null); + final RxDouble speedKmh = 60.0.obs; + final RxBool isLoading = false.obs; + final RxList isochroneRings = [].obs; + + /// Set QRF Base Location / تعيين مركز انطلاق قوة التدخل السريع + void setCenter(LatLng pos) { + center.value = pos; + calculateIsochrones(); + } + + /// Set Average Movement Speed / ضبط متوسط سرعة الآليات (كم/ساعة) + void setSpeed(double speed) { + speedKmh.value = speed; + calculateIsochrones(); + } + + /// Compute Multi-tier Reachability Rings / احتساب مضلعات الوصول (5، 10، 15 دقيقة) + Future calculateIsochrones() async { + if (center.value == null) { + isochroneRings.clear(); + return; + } + + try { + isLoading.value = true; + final rings = await TacticalIsochroneEngine.calculateIsochrones( + center: center.value!, + baseSpeedKmh: speedKmh.value, + ); + isochroneRings.assignAll(rings); + } catch (e) { + isochroneRings.clear(); + } finally { + isLoading.value = false; + } + } + + /// Reset isochrone state / إعادة ضبط نطاق الوصول + void reset() { + center.value = null; + isochroneRings.clear(); + } +} diff --git a/packages/tactical_app/lib/controllers/los_controller.dart b/packages/tactical_app/lib/controllers/los_controller.dart new file mode 100644 index 0000000..961f398 --- /dev/null +++ b/packages/tactical_app/lib/controllers/los_controller.dart @@ -0,0 +1,81 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../services/offline_los_engine.dart'; + +/// ============================================================================ +/// [LosController] - وحدة التحكم بتبادل الرؤية والمقطع التضاريسي (LOS) +/// ============================================================================ +/// English: +/// GetX Controller managing Line of Sight (LOS) observer & target coordinates, +/// elevation sampling, terrain obstruction detection, and cross-section reports. +/// +/// العربية: +/// متحكم GetX لإدارة تبادل الرؤية والمراقبة (LOS)، استخراج المقطع التضاريسي، +/// كشف نقاط الحجب الجبلي والنقاط الميتة (Dead Ground) بالارتفاعات الحقيقية. +/// ============================================================================ +class LosController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx observerPosition = Rx(null); + final Rx targetPosition = Rx(null); + final RxDouble observerHeightM = 2.0.obs; + final RxDouble targetHeightM = 2.0.obs; + final RxBool isLoading = false.obs; + final Rx losReport = Rx(null); + + /// Set Observer Position / تعيين موقع الراصد الميداني + void setObserver(LatLng pos) { + observerPosition.value = pos; + computeLos(); + } + + /// Set Target Position / تعيين موقع الهدف التكتيكي + void setTarget(LatLng pos) { + targetPosition.value = pos; + computeLos(); + } + + /// Swap Observer & Target / تبديل موقع الراصد والهدف + void swapPositions() { + final temp = observerPosition.value; + observerPosition.value = targetPosition.value; + targetPosition.value = temp; + computeLos(); + } + + /// Set Eye and Target Heights / ضبط ارتفاع العين والهدف بالمتر + void setHeights({double? obsHeight, double? tgtHeight}) { + if (obsHeight != null) observerHeightM.value = obsHeight; + if (tgtHeight != null) targetHeightM.value = tgtHeight; + computeLos(); + } + + /// Compute High-Precision LOS Report / احتساب تقرير تبادل الرؤية التضاريسي + Future computeLos() async { + if (observerPosition.value == null || targetPosition.value == null) { + losReport.value = null; + return; + } + + try { + isLoading.value = true; + final rep = OfflineLosEngine.calculate( + observer: observerPosition.value!, + target: targetPosition.value!, + observerHeightM: observerHeightM.value, + targetHeightM: targetHeightM.value, + ); + losReport.value = rep; + } catch (e) { + losReport.value = null; + } finally { + isLoading.value = false; + } + } + + /// Reset LOS state / إعادة ضبط خط الرؤية + void reset() { + observerPosition.value = null; + targetPosition.value = null; + losReport.value = null; + } +} diff --git a/packages/tactical_app/lib/controllers/minefield_controller.dart b/packages/tactical_app/lib/controllers/minefield_controller.dart new file mode 100644 index 0000000..c2f5f39 --- /dev/null +++ b/packages/tactical_app/lib/controllers/minefield_controller.dart @@ -0,0 +1,81 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../models/military_operations_models.dart'; +import '../services/minefield_engine.dart'; + +/// ============================================================================ +/// [MinefieldController] - وحدة التحكم بحقول الألغام وممرات الثغرات +/// ============================================================================ +/// English: +/// GetX Controller managing minefield zone geometry, barrier types, threat +/// envelope generation, mine density estimation, and safe breaching corridor calculations. +/// +/// العربية: +/// متحكم GetX لإدارة هندسة حقول الألغام والموانع، تصنيف نوع الألغام، +/// حساب الكثافة التقديرية، وتخطيط ممرات الثغرات الآمنة (Breaching Lanes). +/// ============================================================================ +class MinefieldController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx startPoint = Rx(null); + final Rx endPoint = Rx(null); + final Rx minefieldType = + Rx(MinefieldType.antiTank); + final RxDouble widthMeters = 200.0.obs; + final Rx zoneResult = Rx(null); + + /// Set Minefield Start Point (A) / تعيين بداية حقل الألغام + void setStartPoint(LatLng pos) { + startPoint.value = pos; + calculateZone(); + } + + /// Set Minefield End Point (B) / تعيين نهاية حقل الألغام + void setEndPoint(LatLng pos) { + endPoint.value = pos; + calculateZone(); + } + + /// Swap Start and End Points / تبديل نقاط بداية ونهاية الحقل + void swapPoints() { + final temp = startPoint.value; + startPoint.value = endPoint.value; + endPoint.value = temp; + calculateZone(); + } + + /// Select Minefield Type / تحديد نوع المانع (ضد دروع / ضد أفراد / مركب) + void setType(MinefieldType type) { + minefieldType.value = type; + calculateZone(); + } + + /// Set Frontage Depth / تحديد عمق الحقل بالمتر + void setWidth(double width) { + widthMeters.value = width; + calculateZone(); + } + + /// Calculate Minefield Threat Box & Breaching Corridor / حساب منطقة الخطر وممر الثغرة + void calculateZone() { + if (startPoint.value == null || endPoint.value == null) { + zoneResult.value = null; + return; + } + + final res = MinefieldEngine.calculateMinefieldZone( + startPoint: startPoint.value!, + endPoint: endPoint.value!, + type: minefieldType.value, + widthMeters: widthMeters.value, + ); + + zoneResult.value = res; + } + + /// Reset minefield state / إعادة ضبط حقل الألغام + void reset() { + startPoint.value = null; + endPoint.value = null; + zoneResult.value = null; + } +} diff --git a/packages/tactical_app/lib/controllers/navigation_controller.dart b/packages/tactical_app/lib/controllers/navigation_controller.dart new file mode 100644 index 0000000..ff784b4 --- /dev/null +++ b/packages/tactical_app/lib/controllers/navigation_controller.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../services/offline_routing_engine.dart'; +import '../services/tactical_api_service.dart'; +import '../services/turn_by_turn_navigation_engine.dart'; + +/// ============================================================================ +/// [NavigationController] - وحدة التحكم بالملاحة وتوجيه القوافل التكتيكية +/// ============================================================================ +/// English: +/// GetX Controller managing hybrid tactical convoy routing (Online API + Sovereign +/// Offline Engine fallback), active turn-by-turn guidance, and navigation metrics. +/// +/// العربية: +/// متحكم GetX لإدارة توجيه القوافل والآليات العسكرية (هجين: سيرفر أونلاين + محرك أوفلاين محلي)، +/// وتتبع مسار الملاحة خطوة بخطوة وحساب السرعة والوقت المتبقي للهدف. +/// ============================================================================ +class NavigationController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx activeRoute = Rx(null); + final Rx vehicleProfile = + Rx(TacticalVehicleProfile.convoy); + final RxBool isCalculating = false.obs; + + /// Calculate Hybrid Tactical Route / احتساب مسار تكتيكي هجين (سيرفر + محلي) + Future calculateRoute({ + required LatLng origin, + required LatLng destination, + TacticalVehicleProfile? profile, + bool autoStart = false, + IntaleqMapController? mapController, + }) async { + final activeProfile = profile ?? vehicleProfile.value; + vehicleProfile.value = activeProfile; + isCalculating.value = true; + + OfflineRoutePlan? plan; + + // 1. Try Online NestJS Server Routing API + try { + final serverRoute = await TacticalApiService.calculateTacticalRoute( + origin: origin, + destination: destination, + profile: activeProfile.name, + ); + + if (serverRoute != null && serverRoute.points.isNotEmpty) { + plan = OfflineRoutePlan( + polylinePoints: serverRoute.points, + totalDistanceKm: serverRoute.distanceKm, + estimatedDurationMinutes: serverRoute.durationMinutes, + profile: activeProfile, + tacticalWaypoints: const ['موقع الانطلاق', 'الهدف التكتيكي المحدد'], + isOffline: false, + ); + } + } catch (e) { + debugPrint('Online route fallback: $e'); + } + + // 2. Fallback to 100% Sovereign On-Device Offline Routing Engine + plan ??= OfflineRoutingEngine.calculateOnDeviceRoute( + start: origin, + destination: destination, + profile: activeProfile, + ); + + activeRoute.value = plan; + isCalculating.value = false; + + if (autoStart) { + startNavigation(mapController: mapController); + } + + return plan; + } + + /// Start Active Turn-by-Turn Guidance / بدء الإرشاد الملاحي الحي + void startNavigation({IntaleqMapController? mapController}) { + if (activeRoute.value == null) return; + TurnByTurnNavigationEngine.startNavigation( + plan: activeRoute.value!, + simulate: false, + controller: mapController, + ); + } + + /// Stop Navigation / إنهاء الملاحة + void stopNavigation() { + TurnByTurnNavigationEngine.stopNavigation(); + } + + /// Reset Route State / إعادة ضبط المسار + void reset() { + stopNavigation(); + activeRoute.value = null; + } +} diff --git a/packages/tactical_app/lib/controllers/overlays_controller.dart b/packages/tactical_app/lib/controllers/overlays_controller.dart new file mode 100644 index 0000000..cdfc788 --- /dev/null +++ b/packages/tactical_app/lib/controllers/overlays_controller.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../models/military_operations_models.dart'; + +/// ============================================================================ +/// [OverlaysController] - وحدة التحكم بمنظومة الشفافات العسكرية التكتيكية (IPB) +/// ============================================================================ +/// English: +/// GetX Controller managing Intelligence Preparation of Battlefield (IPB) +/// transparent overlays, FEBA lines, mobility corridors, and No-Fire Areas. +/// +/// العربية: +/// متحكم GetX لإدارة منظومة الشفافات العسكرية التكتيكية (IPB)، +/// والخطوط الأمامية لمنطقة القتال (FEBA)، وممرات الحركة، ومناطق حظر الرماية. +/// ============================================================================ +class OverlaysController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final RxList overlayLayers = [ + TacticalOverlayLayer( + id: 'layer_feba', + nameAr: 'الخط الأمامي لمنطقة القتال (FEBA / FLOT)', + color: const Color(0xFF0071E3), + isVisible: true, + lines: [ + [ + const LatLng(32.1000, 35.7500), + const LatLng(32.0200, 35.8500), + const LatLng(31.9500, 35.9000), + const LatLng(31.8500, 35.9800), + ] + ], + ), + TacticalOverlayLayer( + id: 'layer_mobility', + nameAr: 'ممرات الحركة ومحاور التقدم (Mobility Corridors)', + color: const Color(0xFF10B981), + isVisible: true, + lines: [ + [ + const LatLng(31.9539, 35.9106), + const LatLng(32.0000, 35.9500), + const LatLng(32.0500, 36.0200), + ] + ], + ), + TacticalOverlayLayer( + id: 'layer_no_fire', + nameAr: 'مناطق الحظر وعدم الرماية (No-Fire Areas - NFA)', + color: const Color(0xFFEF4444), + isVisible: false, + polygons: [ + [ + const LatLng(31.9800, 35.9300), + const LatLng(32.0000, 35.9300), + const LatLng(32.0000, 35.9600), + const LatLng(31.9800, 35.9600), + const LatLng(31.9800, 35.9300), + ] + ], + ), + ].obs; + + /// Toggle Overlay Layer Visibility / تبديل إظهار أو إخفاء طبقة معينة + void toggleLayer(String layerId, bool isVisible) { + final idx = overlayLayers.indexWhere((l) => l.id == layerId); + if (idx != -1) { + overlayLayers[idx] = overlayLayers[idx].copyWith(isVisible: isVisible); + } + } + + /// Show All Overlay Layers / إظهار كافة الشفافات + void showAll() { + for (int i = 0; i < overlayLayers.length; i++) { + overlayLayers[i] = overlayLayers[i].copyWith(isVisible: true); + } + } + + /// Hide All Overlay Layers / إخفاء كافة الشفافات + void hideAll() { + for (int i = 0; i < overlayLayers.length; i++) { + overlayLayers[i] = overlayLayers[i].copyWith(isVisible: false); + } + } +} diff --git a/packages/tactical_app/lib/controllers/resection_controller.dart b/packages/tactical_app/lib/controllers/resection_controller.dart new file mode 100644 index 0000000..22dc45c --- /dev/null +++ b/packages/tactical_app/lib/controllers/resection_controller.dart @@ -0,0 +1,46 @@ +import 'package:get/get.dart'; +import '../models/landmark.dart'; +import '../services/resection_calculator.dart'; + +/// ============================================================================ +/// [ResectionController] - وحدة التحكم بالتقاطع البصري العكسي (GPS-Denied Fix) +/// ============================================================================ +/// English: +/// GetX Controller managing camera-based optical resection observations, +/// magnetic azimuth readings, and inverse triangulation coordinate resolution. +/// +/// العربية: +/// متحكم GetX لإدارة عملية التقاطع البصري العكسي عبر الكاميرا والبوصلة المغناطيسية، +/// واستخراج إحداثيات الموقع بدقة عند انقطاع الـ GPS أو التشويش الإلكتروني. +/// ============================================================================ +class ResectionController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final RxList observations = [].obs; + final Rx resectionResult = Rx(null); + final RxDouble currentHeadingDeg = 0.0.obs; + + /// Add Observation / إضافة رصد اتجاهي لمعلم جغرافي + void addObservation(ResectionObservation obs) { + observations.removeWhere((o) => o.landmark.id == obs.landmark.id); + observations.add(obs); + } + + /// Remove Observation / إزالة رصد معلم معين + void removeObservation(String landmarkId) { + observations.removeWhere((o) => o.landmark.id == landmarkId); + } + + /// Compute Position from Observations / استخراج الإحداثيات بالتقاطع العكسي + ResectionResult? computePosition() { + if (observations.length < 2) return null; + final res = ResectionCalculator.calculatePosition(observations); + resectionResult.value = res; + return res; + } + + /// Reset all observations / تصفير وإعادة ضبط الرصد + void reset() { + observations.clear(); + resectionResult.value = null; + } +} diff --git a/packages/tactical_app/lib/controllers/symbols_controller.dart b/packages/tactical_app/lib/controllers/symbols_controller.dart new file mode 100644 index 0000000..96b61c1 --- /dev/null +++ b/packages/tactical_app/lib/controllers/symbols_controller.dart @@ -0,0 +1,80 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../models/military_operations_models.dart'; + +/// ============================================================================ +/// [SymbolsController] - وحدة التحكم بالرموز والتشكيلات العسكرية +/// ============================================================================ +/// English: +/// GetX Controller managing Mil-Std-2525C tactical military symbology palette, +/// interactive placement on the map canvas, echelon levels, and unit lists. +/// +/// العربية: +/// متحكم GetX لإدارة لوحة الرموز العسكرية القياسية (Mil-Std-2525C)، +/// وتثبيت الوحدات الميدانية والصديقة والمعادية على الخريطة التكتيكية. +/// ============================================================================ +class SymbolsController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx activePlacementType = Rx(null); + final RxList placedSymbols = [ + TacticalSymbolItem( + id: 'sym-default-1', + name: 'كتيبة المشاة الآلية 4', + type: TacticalSymbolType.friendlyInfantry, + position: const LatLng(31.9800, 35.8800), + echelon: MilitaryEchelon.battalion, + ), + TacticalSymbolItem( + id: 'sym-default-2', + name: 'مربض مدفعية M109', + type: TacticalSymbolType.friendlyArtillery, + position: const LatLng(31.9300, 35.9100), + echelon: MilitaryEchelon.battalion, + ), + TacticalSymbolItem( + id: 'sym-default-3', + name: 'رادار كشف تكتيكي', + type: TacticalSymbolType.friendlyRadar, + position: const LatLng(32.0200, 35.8400), + echelon: MilitaryEchelon.company, + ), + ].obs; + + /// Select Symbol for Map Placement Mode / اختيار رمز لتثبيته على الخريطة + void selectSymbolForPlacement(TacticalSymbolType type) { + activePlacementType.value = type; + } + + /// Cancel Placement Mode / إلغاء وضع التثبيت + void cancelPlacement() { + activePlacementType.value = null; + } + + /// Place Unit at Coordinate / تثبيت الرمز في موقع جغرافي محدد + void placeSymbolAt(LatLng position, {String? customName}) { + if (activePlacementType.value == null) return; + + final type = activePlacementType.value!; + final name = customName ?? 'وحدة ميدانية ${placedSymbols.length + 1}'; + + placedSymbols.add(TacticalSymbolItem( + id: 'sym_${DateTime.now().millisecondsSinceEpoch}', + name: name, + type: type, + position: position, + )); + + activePlacementType.value = null; + } + + /// Remove Placed Symbol / إزالة رمز مثبت من الميدان + void removeSymbol(String symbolId) { + placedSymbols.removeWhere((item) => item.id == symbolId); + } + + /// Clear All Symbols / مسح كافة الرموز العسكرية + void clearAll() { + placedSymbols.clear(); + activePlacementType.value = null; + } +} diff --git a/packages/tactical_app/lib/controllers/tactical_map_controller.dart b/packages/tactical_app/lib/controllers/tactical_map_controller.dart new file mode 100644 index 0000000..8ab3dc4 --- /dev/null +++ b/packages/tactical_app/lib/controllers/tactical_map_controller.dart @@ -0,0 +1,605 @@ +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 '../services/military_grid_utils.dart'; +import '../services/offline_los_engine.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(); + + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final RxString currentTacticalMode = 'nav'.obs; + final Rx angleUnit = AngleUnit.dual.obs; + final RxBool showContours = true.obs; + final Rx currentGpsPosition = Rx(null); + final Rx currentCameraCenter = const LatLng(31.9539, 35.9106).obs; + final Rx activePickerTarget = Rx(null); + + IntaleqMapController? mapController; + + @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) { + 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!; + mapController?.animateCamera( + CameraUpdate.newLatLngZoom(currentGpsPosition.value!, 14.0), + ); + } + } 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.routeOrigin: + case MapPickerTarget.routeDestination: + break; + } + } + + /// 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 'توجيه القوافل التكتيكي'; + 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: const InfoWindow( + title: 'موقع الراصد المحسوب (GPS-Denied Fix)', + snippet: 'تم استخراج الموقع بالتقاطع البصري العكسي', + ), + ), + ); + } + + // 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)'), + ), + ); + } + + 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, + ), + ); + } + + 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(0x6610B981), + strokeColor: const Color(0xFF10B981), + strokeWidth: 2, + ), + ); + } + + // 4. 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, + ), + ); + } + } + + // 5. 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; + } +} diff --git a/packages/tactical_app/lib/controllers/viewshed_controller.dart b/packages/tactical_app/lib/controllers/viewshed_controller.dart new file mode 100644 index 0000000..761e040 --- /dev/null +++ b/packages/tactical_app/lib/controllers/viewshed_controller.dart @@ -0,0 +1,70 @@ +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../services/viewshed_360_engine.dart'; + +/// ============================================================================ +/// [ViewshedController] - وحدة التحكم برادار الرصد الدائري 360° +/// ============================================================================ +/// English: +/// GetX Controller managing 360° radial viewshed calculations, observer radar +/// radius, visible coverage polygons, and dead ground area metrics. +/// +/// العربية: +/// متحكم GetX لإدارة رادار الرصد الدائري 360 درجة، وحساب مضلع التغطية البصرية +/// الفعلي ومساحة الرصد بالكيلومتر المربع مع تضاريس الأردن الحقيقية. +/// ============================================================================ +class ViewshedController extends GetxController { + // ── Observables / الحالات التفاعلية ────────────────────────────────────── + final Rx observerPosition = Rx(null); + final RxDouble radiusKm = 5.0.obs; + final RxDouble observerHeightM = 2.0.obs; + final RxBool isCalculating = false.obs; + final Rx viewshedReport = Rx(null); + + /// Set Viewshed Radar Center / تعيين مركز الرادار والرصد + void setObserver(LatLng pos) { + observerPosition.value = pos; + computeViewshed(); + } + + /// Set Radar Range Radius in KM / ضبط نصف قطر دائرة الرصد بالكيلومتر + void setRadius(double km) { + radiusKm.value = km; + computeViewshed(); + } + + /// Set Observer Antenna/Tower Height / ضبط ارتفاع سارية الرصد بالمتر + void setHeight(double heightM) { + observerHeightM.value = heightM; + computeViewshed(); + } + + /// Compute 360 Viewshed / تنفيذ الحساب التفاعلي لحقل الرؤية الدائري + Future computeViewshed() async { + if (observerPosition.value == null) { + viewshedReport.value = null; + return; + } + + try { + isCalculating.value = true; + final rep = await Viewshed360Engine.calculateAsync( + observer: observerPosition.value!, + radiusMeters: radiusKm.value * 1000.0, + observerHeightM: observerHeightM.value, + rayCount: 360, + ); + viewshedReport.value = rep; + } catch (e) { + viewshedReport.value = null; + } finally { + isCalculating.value = false; + } + } + + /// Reset viewshed state / إعادة ضبط الرادار الدائري + void reset() { + observerPosition.value = null; + viewshedReport.value = null; + } +} diff --git a/packages/tactical_app/lib/main.dart b/packages/tactical_app/lib/main.dart index 763422d..e4436f5 100644 --- a/packages/tactical_app/lib/main.dart +++ b/packages/tactical_app/lib/main.dart @@ -1,9 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:get/get.dart'; +import 'bindings/tactical_binding.dart'; import 'screens/tactical_map_screen.dart'; import 'services/landmark_database.dart'; import 'services/offline_routing_engine.dart'; +/// ============================================================================ +/// [main] - المدخل الرئيسي لتطبيق العمليات التكتيكية الميدانية (Intaleq Defense) +/// ============================================================================ +/// English: +/// Application entry point initializing local SQLite cache, topological routing +/// graphs, GetX State Management, and tactical dark sovereign UI themes. +/// +/// العربية: +/// نقطة انطلاق التطبيق، تقوم بتهيئة قواعد بيانات المعالم المحلية، محرك التوجيه، +/// حقن متحكمات GetX وتفعيل الثيم العسكري الليلي مع دعم اتجاه الكتابة من اليمين (RTL). +/// ============================================================================ void main() async { WidgetsFlutterBinding.ensureInitialized(); await LandmarkDatabase.initCache(); @@ -16,10 +29,12 @@ class TacticalApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return GetMaterialApp( title: 'منظومة الخرائط التكتيكية والملاحة الميدانية - Intaleq Tactical Navigation', debugShowCheckedModeBanner: false, + initialBinding: TacticalBinding(), locale: const Locale('ar', 'JO'), + fallbackLocale: const Locale('en', 'US'), supportedLocales: const [ Locale('ar', 'JO'), Locale('en', 'US'), diff --git a/packages/tactical_app/lib/screens/tactical_map_screen.dart b/packages/tactical_app/lib/screens/tactical_map_screen.dart index cdcba6e..3cabebb 100644 --- a/packages/tactical_app/lib/screens/tactical_map_screen.dart +++ b/packages/tactical_app/lib/screens/tactical_map_screen.dart @@ -1,20 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../config/app_config.dart'; -import '../models/angle_unit.dart'; -import '../models/landmark.dart'; -import '../models/military_operations_models.dart'; +import '../controllers/tactical_map_controller.dart'; import '../models/navigation_state.dart'; -import '../services/military_grid_utils.dart'; -import '../services/offline_los_engine.dart'; -import '../services/offline_routing_engine.dart'; -import '../services/resection_calculator.dart'; -import '../services/tactical_api_service.dart'; -import '../services/tactical_isochrone_engine.dart'; import '../services/turn_by_turn_navigation_engine.dart'; -import '../services/viewshed_360_engine.dart'; import '../widgets/active_navigation_hud.dart'; import '../widgets/camera_resection_view.dart'; import '../widgets/interactive_map_picker_hud.dart'; @@ -31,1246 +22,106 @@ import '../widgets/tactical_route_planner_sheet.dart'; import '../widgets/tactical_symbols_sheet.dart'; import '../widgets/tactical_viewshed_sheet.dart'; -class TacticalMapScreen extends StatefulWidget { +/// ============================================================================ +/// [TacticalMapScreen] - الشاشة التكتيكية الرئيسية (GetView Architecture) +/// ============================================================================ +/// English: +/// Master map screen implemented as a reactive GetView listening to [TacticalMapController]. +/// Decoupled from direct state mutations, using Obx for declarative UI rendering. +/// +/// العربية: +/// شاشة العمليات والخريطة الرئيسية بنمط GetView التفاعلي، ترتبط بمتحكم [TacticalMapController] +/// وتستخدم Obx لتحديث طبقات الخريطة واللوحات المنبثقة فور تغير الحالة الميدانية. +/// ============================================================================ +class TacticalMapScreen extends GetView { const TacticalMapScreen({super.key}); - @override - State createState() => _TacticalMapScreenState(); -} - -class _TacticalMapScreenState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - IntaleqMapController? _mapController; - String _currentTacticalMode = - 'nav'; // 'nav', 'resection_cam', 'routing', 'los', 'viewshed_360', 'artillery', 'hlz', 'minefield', 'isochrone', 'symbols', 'overlays' - AngleUnit _angleUnit = AngleUnit.dual; - TacticalVehicleProfile _selectedProfile = TacticalVehicleProfile.convoy; - - // Live GPS Location - LatLng? _currentGpsPosition; - - // Camera Center for Interactive Map Pin Picker - LatLng _currentCameraCenter = const LatLng(31.9539, 35.9106); - MapPickerTarget? _activePickerTarget; - - // Resection Observations & Result - final List _observations = []; - ResectionResult? _resectionResult; - - // Tactical Route Plan (Hybrid: Online Server + Offline Fallback) - OfflineRoutePlan? _activeRoute; - - // Tactical Line of Sight (LOS) State - LatLng? _losObserver; - LatLng? _losTarget; - OfflineLosReport? _activeLosReport; - - // Tactical 360 Viewshed State - Viewshed360Report? _activeViewshedReport; - - // Artillery Fire Mission State - LatLng? _artilleryGun; - LatLng? _artilleryTarget; - ArtilleryFiringSolution? _activeArtillerySolution; - - // Helicopter Landing Zone State - LatLng? _hlzCenter; - HlzAssessmentResult? _activeHlzResult; - - // Minefield & Breaching State - LatLng? _minefieldStart; - LatLng? _minefieldEnd; - MinefieldZoneResult? _activeMinefieldResult; - - // Isochrone QRF Reachability State - LatLng? _isochroneCenter; - List? _activeIsochroneRings; - - // Tactical Placed Symbols - TacticalSymbolType? _activePlacementSymbolType; - final List _placedSymbols = [ - TacticalSymbolItem( - id: 'sym-default-1', - name: 'كتيبة المشاة الآلية 4', - type: TacticalSymbolType.friendlyInfantry, - position: const LatLng(31.9800, 35.8800), - echelon: MilitaryEchelon.battalion, - ), - TacticalSymbolItem( - id: 'sym-default-2', - name: 'مربض مدفعية M109', - type: TacticalSymbolType.friendlyArtillery, - position: const LatLng(31.9300, 35.9100), - echelon: MilitaryEchelon.battalion, - ), - TacticalSymbolItem( - id: 'sym-default-3', - name: 'رادار كشف تكتيكي', - type: TacticalSymbolType.friendlyRadar, - position: const LatLng(32.0200, 35.8400), - echelon: MilitaryEchelon.company, - ), - ]; - - // Tactical Overlays (IPB Layers) - List _overlayLayers = [ - TacticalOverlayLayer( - id: 'layer_feba', - nameAr: 'الخط الأمامي لمنطقة القتال (FEBA / FLOT)', - color: const Color(0xFF0071E3), - isVisible: true, - lines: [ - [ - const LatLng(32.1000, 35.7500), - const LatLng(32.0200, 35.8500), - const LatLng(31.9500, 35.9000), - const LatLng(31.8500, 35.9800), - ] - ], - ), - TacticalOverlayLayer( - id: 'layer_mobility', - nameAr: 'ممرات الحركة ومحاور التقدم (Mobility Corridors)', - color: const Color(0xFF10B981), - isVisible: true, - lines: [ - [ - const LatLng(31.9539, 35.9106), - const LatLng(32.0000, 35.9500), - const LatLng(32.0500, 36.0200), - ] - ], - ), - TacticalOverlayLayer( - id: 'layer_no_fire', - nameAr: 'مناطق الحظر وعدم الرماية (No-Fire Areas - NFA)', - color: const Color(0xFFEF4444), - isVisible: false, - polygons: [ - [ - const LatLng(31.9800, 35.9300), - const LatLng(32.0000, 35.9300), - const LatLng(32.0000, 35.9600), - const LatLng(31.9800, 35.9600), - const LatLng(31.9800, 35.9300), - ] - ], - ), - ]; - - // Topographic Contours / Style Layer State - bool _showContours = true; - - @override - void initState() { - super.initState(); - _initGps(); - _checkInitialProvisioning(); - } - - Future _checkInitialProvisioning() async { - final needed = await TacticalInitialProvisioningDialog.isProvisioningNeeded(); - if (needed && mounted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => TacticalInitialProvisioningDialog( - onCompleted: () { - Navigator.pop(ctx); - setState(() {}); - }, - ), - ); - }); - } - } - - 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) { - final pos = await Geolocator.getCurrentPosition( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 5)), - ); - if (mounted) { - setState(() { - _currentGpsPosition = LatLng(pos.latitude, pos.longitude); - _currentCameraCenter = _currentGpsPosition!; - }); - _mapController?.animateCamera( - CameraUpdate.newLatLngZoom(_currentGpsPosition!, 14.0), - ); - } - } - } catch (e) { - debugPrint('GPS init error: $e'); - } - } - - @override - void dispose() { - TurnByTurnNavigationEngine.stopNavigation(); - super.dispose(); - } - - void _onObservationAdded(ResectionObservation obs) { - setState(() { - _observations.removeWhere((o) => o.landmark.id == obs.landmark.id); - _observations.add(obs); - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: const Color(0xFF0071E3), - content: Text( - 'تم تثبيت رصد (${obs.landmark.name}) بـ ${AngleFormatter.format(obs.trueAzimuthDeg, _angleUnit)}!', - ), - ), - ); - } - - void _executeResectionCalculation() { - final result = ResectionCalculator.calculatePosition(_observations); - if (result == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - backgroundColor: Colors.redAccent, - content: Text( - 'خطأ: زوايا الرصد متوازية. يرجى اختيار معالم بزوايا متباعدة.'), - ), - ); - return; - } - - setState(() { - _resectionResult = result; - _currentTacticalMode = 'nav'; - }); - - final targetPos = LatLng(result.lat, result.lng); - Future.delayed(const Duration(milliseconds: 300), () { - if (mounted && _mapController != null) { - _mapController!.animateCamera( - CameraUpdate.newLatLngZoom(targetPos, 15.0), - ); - } - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: const Color(0xFF22C55E), - duration: const Duration(seconds: 4), - content: Row( - children: [ - const Icon(Icons.gps_fixed, color: Colors.white, size: 20), - const SizedBox(width: 10), - Expanded( - child: Text( - 'تم استخراج موقعك بنجاح: ${result.lat.toStringAsFixed(5)}, ${result.lng.toStringAsFixed(5)} • دقة ±${result.estimatedAccuracyMeters.round()}م', - style: - const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - ), - ), - ], - ), - ), - ); - } - - void _openRoutePlanner({LatLng? initialOrigin, LatLng? initialDestination}) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalRoutePlannerSheet( - currentGps: _currentGpsPosition ?? const LatLng(31.9539, 35.9106), - resectionFix: _resectionResult != null - ? LatLng(_resectionResult!.lat, _resectionResult!.lng) - : null, - initialOrigin: initialOrigin, - initialDestination: initialDestination, - onRouteConfirmed: (origin, destination, profile, autoStartNav) { - _calculateHybridRoute( - customOrigin: origin, - customDestination: destination, - selectedProfile: profile, - autoStartNav: autoStartNav, - ); - }, - onPickOriginOnMap: () { - setState(() => _activePickerTarget = MapPickerTarget.routeOrigin); - }, - onPickDestinationOnMap: () { - setState( - () => _activePickerTarget = MapPickerTarget.routeDestination); - }, - onClose: () => Navigator.pop(ctx), - ), - ); - } - - void _onConfirmPicker(MapPickerTarget target, LatLng selectedPos) { - setState(() { - _activePickerTarget = null; - if (target == MapPickerTarget.losObserver) { - _losObserver = selectedPos; - } else if (target == MapPickerTarget.losTarget) { - _losTarget = selectedPos; - } else if (target == MapPickerTarget.artilleryGun) { - _artilleryGun = selectedPos; - } else if (target == MapPickerTarget.artilleryTarget) { - _artilleryTarget = selectedPos; - } else if (target == MapPickerTarget.hlzCenter) { - _hlzCenter = selectedPos; - } else if (target == MapPickerTarget.minefieldStart) { - _minefieldStart = selectedPos; - } else if (target == MapPickerTarget.minefieldEnd) { - _minefieldEnd = selectedPos; - } else if (target == MapPickerTarget.isochroneCenter) { - _isochroneCenter = selectedPos; - } else if (target == MapPickerTarget.viewshedCenter) { - _losObserver = selectedPos; - } - }); - - if (target == MapPickerTarget.losObserver || - target == MapPickerTarget.losTarget) { - _openLosSheet(); - } else if (target == MapPickerTarget.viewshedCenter) { - _openViewshed360Sheet(); - } else if (target == MapPickerTarget.routeOrigin || - target == MapPickerTarget.routeDestination) { - _openRoutePlanner(); - } else if (target == MapPickerTarget.artilleryGun || - target == MapPickerTarget.artilleryTarget) { - _openArtillerySheet(); - } else if (target == MapPickerTarget.hlzCenter) { - _openHlzSheet(); - } else if (target == MapPickerTarget.minefieldStart || - target == MapPickerTarget.minefieldEnd) { - _openMinefieldSheet(); - } else if (target == MapPickerTarget.isochroneCenter) { - _openIsochroneSheet(); - } - } - - void _onCancelPicker() { - final target = _activePickerTarget; - setState(() => _activePickerTarget = null); - - if (target == MapPickerTarget.losObserver || - target == MapPickerTarget.losTarget) { - _openLosSheet(); - } else if (target == MapPickerTarget.viewshedCenter) { - _openViewshed360Sheet(); - } else if (target == MapPickerTarget.routeOrigin || - target == MapPickerTarget.routeDestination) { - _openRoutePlanner(); - } else if (target == MapPickerTarget.artilleryGun || - target == MapPickerTarget.artilleryTarget) { - _openArtillerySheet(); - } else if (target == MapPickerTarget.hlzCenter) { - _openHlzSheet(); - } else if (target == MapPickerTarget.minefieldStart || - target == MapPickerTarget.minefieldEnd) { - _openMinefieldSheet(); - } else if (target == MapPickerTarget.isochroneCenter) { - _openIsochroneSheet(); - } - } - - void _resetResection() { - TurnByTurnNavigationEngine.stopNavigation(); - setState(() { - _observations.clear(); - _resectionResult = null; - _activeRoute = null; - _losObserver = null; - _losTarget = null; - _activeLosReport = null; - _activeArtillerySolution = null; - _activeHlzResult = null; - _activeMinefieldResult = null; - _activeIsochroneRings = null; - _currentTacticalMode = 'nav'; - }); - } - - // ── Operations Modal Launchers ───────────────────────────── - - void _openLosSheet() { - if (_losObserver == null || _losTarget == null) return; - - final initialLos = OfflineLosEngine.calculate( - observer: _losObserver!, - target: _losTarget!, - ); - setState(() { - _activeLosReport = initialLos; - }); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalLosSheet( - observer: _losObserver!, - target: _losTarget!, - currentGps: _currentGpsPosition, - angleUnit: _angleUnit, - onUpdateLocations: (newObs, newTgt) { - setState(() { - _losObserver = newObs; - _losTarget = newTgt; - }); - final updatedLos = OfflineLosEngine.calculate( - observer: newObs, - target: newTgt, - ); - setState(() { - _activeLosReport = updatedLos; - }); - _fitRouteBounds([newObs, newTgt]); - }, - onVisibilityChanged: (_) {}, - onReportGenerated: (report) { - if (mounted) { - setState(() { - _activeLosReport = report; - }); - } - }, - onPickObserverOnMap: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.losObserver); - }, - onPickTargetOnMap: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.losTarget); - }, - onClose: () { - Navigator.pop(context); - setState(() => _currentTacticalMode = 'nav'); - }, - ), - ); - } - - void _openViewshed360Sheet() { - if (_losObserver == null) return; - - final initialReport = Viewshed360Engine.calculate( - observer: _losObserver!, - radiusMeters: 5000.0, - observerHeightM: 2.0, - rayCount: 360, - ); - setState(() { - _activeViewshedReport = initialReport; - }); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalViewshedSheet( - observer: _losObserver!, - currentGps: _currentGpsPosition, - onUpdateObserver: (newObs) { - setState(() => _losObserver = newObs); - }, - onReportUpdated: (rep) { - if (mounted) { - setState(() => _activeViewshedReport = rep); - } - }, - onPickObserverOnMap: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.viewshedCenter); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openArtillerySheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalArtillerySheet( - gunPosition: _artilleryGun ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106), - targetPosition: _artilleryTarget, - angleUnit: _angleUnit, - onPickGun: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.artilleryGun); - }, - onPickTarget: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.artilleryTarget); - }, - onSwap: () { - setState(() { - final temp = _artilleryGun; - _artilleryGun = _artilleryTarget; - _artilleryTarget = temp; - }); - }, - onSolutionCalculated: (sol) { - setState(() => _activeArtillerySolution = sol); - _fitRouteBounds([sol.gunPosition, sol.targetPosition]); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openHlzSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalHlzSheet( - selectedPosition: _hlzCenter ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106), - onPickLocation: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.hlzCenter); - }, - onAssessmentCompleted: (res) { - setState(() => _activeHlzResult = res); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openMinefieldSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalMinefieldSheet( - startPoint: _minefieldStart ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106), - endPoint: _minefieldEnd, - onPickStart: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.minefieldStart); - }, - onPickEnd: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.minefieldEnd); - }, - onSwap: () { - setState(() { - final temp = _minefieldStart; - _minefieldStart = _minefieldEnd; - _minefieldEnd = temp; - }); - }, - onZoneCalculated: (res) { - setState(() => _activeMinefieldResult = res); - _fitRouteBounds([res.startPoint, res.endPoint]); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openIsochroneSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalIsochroneSheet( - center: _isochroneCenter ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106), - onPickCenter: () { - Navigator.pop(context); - setState(() => _activePickerTarget = MapPickerTarget.isochroneCenter); - }, - onIsochronesCalculated: (rings) { - setState(() => _activeIsochroneRings = rings); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openSymbolsSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalSymbolsSheet( - activePlacementType: _activePlacementSymbolType, - placedSymbols: _placedSymbols, - onSelectSymbolType: (type) { - setState(() => _activePlacementSymbolType = type); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: const Color(0xFF0071E3), - duration: const Duration(seconds: 4), - content: const Row( - children: [ - Icon(Icons.touch_app, color: Colors.white, size: 18), - SizedBox(width: 10), - Expanded( - child: Text( - 'انقر على الخريطة لتثبيت الرمز العسكري في الميدان 📍', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ], - ), - ), - ); - }, - onDeleteSymbol: (s) { - setState(() => _placedSymbols.removeWhere((item) => item.id == s.id)); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _openOverlaysSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => TacticalOverlaysSheet( - layers: _overlayLayers, - onToggleLayer: (layerId, isVisible) { - setState(() { - _overlayLayers = _overlayLayers.map((l) { - if (l.id == layerId) return l.copyWith(isVisible: isVisible); - return l; - }).toList(); - }); - }, - onClose: () => Navigator.pop(context), - ), - ); - } - - void _handleMapTap(LatLng point) { - // 1. Check Active Placement of Tactical Symbol - if (_activePlacementSymbolType != null) { - final type = _activePlacementSymbolType!; - setState(() { - _placedSymbols.add(TacticalSymbolItem( - id: 'sym_${DateTime.now().millisecondsSinceEpoch}', - name: 'وحدة ميدانية ${_placedSymbols.length + 1}', - type: type, - position: point, - )); - _activePlacementSymbolType = null; - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - backgroundColor: Color(0xFF22C55E), - content: Text('تم تثبيت الرمز العسكري على الخريطة بنجاح ✅'), - ), - ); - return; - } - - // 2. Check Active Picker - if (_activePickerTarget != null) { - _onConfirmPicker(_activePickerTarget!, point); - return; - } - - // 3. Mode handlers - if (_currentTacticalMode == 'viewshed_360') { - setState(() => _losObserver = point); - _openViewshed360Sheet(); - return; - } - - if (_currentTacticalMode == 'artillery') { - if (_artilleryGun == null) { - setState(() => _artilleryGun = point); - } else { - setState(() => _artilleryTarget = point); - } - _openArtillerySheet(); - return; - } - - if (_currentTacticalMode == 'hlz') { - setState(() => _hlzCenter = point); - _openHlzSheet(); - return; - } - - if (_currentTacticalMode == 'minefield') { - if (_minefieldStart == null) { - setState(() => _minefieldStart = point); - } else { - setState(() => _minefieldEnd = point); - } - _openMinefieldSheet(); - return; - } - - if (_currentTacticalMode == 'isochrone') { - setState(() => _isochroneCenter = point); - _openIsochroneSheet(); - return; - } - - if (_currentTacticalMode == 'los') { - if (_losObserver == null) { - setState(() => _losObserver = point); - } else { - setState(() => _losTarget = point); - _openLosSheet(); - } - return; - } - - // Default Tap Info - final mil = MilitaryGridUtils.fromLatLng(point); - final elev = JordanDemSurface.elevationAt(point.latitude, point.longitude); - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 3), - backgroundColor: const Color(0xFF0F172A), - content: Row( - children: [ - const Icon(Icons.location_searching, size: 16, color: Color(0xFF38BDF8)), - const SizedBox(width: 8), - Expanded( - child: Text( - '${mil.arabicFullFormat} • منسوب: ${elev.round()}م', - style: const TextStyle( - color: Colors.white, - fontSize: 11.5, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ); - } - - void _fitRouteBounds(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, - ), - ); - } - - Future _calculateHybridRoute({ - LatLng? customOrigin, - LatLng? customDestination, - TacticalVehicleProfile? selectedProfile, - bool autoStartNav = false, - }) async { - final profile = selectedProfile ?? _selectedProfile; - setState(() => _selectedProfile = profile); - - final startPoint = customOrigin ?? - _currentGpsPosition ?? - (_resectionResult != null - ? LatLng(_resectionResult!.lat, _resectionResult!.lng) - : const LatLng(31.9539, 35.9106)); - - final targetPoint = customDestination ?? - (_observations.isNotEmpty - ? LatLng(_observations.first.landmark.lat, _observations.first.landmark.lng) - : LatLng(startPoint.latitude + 0.05, startPoint.longitude + 0.05)); - - OfflineRoutePlan? plan; - - // 1. Try Online NestJS Server Routing API - try { - final serverRoute = await TacticalApiService.calculateTacticalRoute( - origin: startPoint, - destination: targetPoint, - profile: profile.name, - ); - - if (serverRoute != null && serverRoute.points.isNotEmpty) { - plan = OfflineRoutePlan( - polylinePoints: serverRoute.points, - totalDistanceKm: serverRoute.distanceKm, - estimatedDurationMinutes: serverRoute.durationMinutes, - profile: profile, - tacticalWaypoints: const ['موقع الانطلاق', 'الهدف التكتيكي المحدد'], - isOffline: false, - ); - } - } catch (e) { - debugPrint('Online server route fallback: $e'); - } - - // 2. Fallback to 100% Sovereign On-Device Offline Routing Engine - plan ??= OfflineRoutingEngine.calculateOnDeviceRoute( - start: startPoint, - destination: targetPoint, - profile: profile, - ); - - setState(() { - _activeRoute = plan; - _currentTacticalMode = 'routing'; - }); - - _fitRouteBounds(plan.polylinePoints); - - if (autoStartNav) { - TurnByTurnNavigationEngine.startNavigation( - plan: plan, - simulate: false, - controller: _mapController, - ); - } - } - - void _openPlaceSearch() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (ctx) => PlaceSearchSheet( - onDestinationSelected: (landmark, startNav) { - final dest = LatLng(landmark.lat, landmark.lng); - _calculateHybridRoute( - customDestination: dest, - autoStartNav: startNav, - ); - }, - ), - ); - } - - // ── Build Declarative Overlays for Map ───────────────────── - - Set _buildMarkers(ActiveNavigationState navState) { - final markers = {}; - - // 1. Landmark markers - for (final obs in _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), - ), - ), - ); - } - - // 2. Resection Fix Marker - if (_resectionResult != null && !navState.isNavigating) { - markers.add( - Marker( - markerId: const MarkerId('observer_calculated_position'), - position: LatLng(_resectionResult!.lat, _resectionResult!.lng), - icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen), - infoWindow: const InfoWindow( - title: 'موقع الراصد المحسوب (GPS-Denied Fix)', - snippet: 'تم استخراج الموقع بالتقاطع البصري العكسي', - ), - ), - ); - } - - // 3. Routing Destination Marker - if (_activeRoute != null && _activeRoute!.polylinePoints.isNotEmpty) { - markers.add( - Marker( - markerId: const MarkerId('convoy_destination'), - position: _activeRoute!.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 _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 == 'artillery') { - if (_artilleryGun != null) { - markers.add( - Marker( - markerId: const MarkerId('artillery_gun_marker'), - position: _artilleryGun!, - icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueBlue), - infoWindow: const InfoWindow(title: '🎯 مربض المدفعية'), - ), - ); - } - if (_artilleryTarget != null) { - markers.add( - Marker( - markerId: const MarkerId('artillery_target_marker'), - position: _artilleryTarget!, - icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed), - infoWindow: const InfoWindow(title: '💥 الهدف المعادي'), - ), - ); - } - } - - // 7. HLZ Center Marker - if (_currentTacticalMode == 'hlz' && _hlzCenter != null) { - markers.add( - Marker( - markerId: const MarkerId('hlz_center_marker'), - position: _hlzCenter!, - icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen), - infoWindow: const InfoWindow(title: '🚁 مهبط الطيران العامودي (HLZ)'), - ), - ); - } - - return markers; - } - - Set _buildPolylines(ActiveNavigationState navState) { - final polylines = {}; - - // 1. Tactical Line of Sight (LOS) - if (_currentTacticalMode == 'los' && _losObserver != null && _losTarget != null) { - if (_activeLosReport != null && _activeLosReport!.profile.length > 1) { - final visSegments = _activeLosReport!.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 = _activeLosReport!.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 == 'artillery' && _activeArtillerySolution != null) { - final points = _activeArtillerySolution!.trajectoryProfile.map((p) => p.coordinate).toList(); - polylines.add( - Polyline( - polylineId: const PolylineId('artillery_trajectory_track'), - points: points, - color: _activeArtillerySolution!.isCrestClear - ? const Color(0xFF00F0FF) - : const Color(0xFFEF4444), - width: 4.5, - ), - ); - } - - // 3. Minefield Safe Breaching Lane Centerline - if (_currentTacticalMode == 'minefield' && _activeMinefieldResult != null) { - polylines.add( - Polyline( - polylineId: const PolylineId('minefield_breach_centerline'), - points: _activeMinefieldResult!.breachLaneCenterline, - color: const Color(0xFF10B981), - width: 4.0, - ), - ); - } - - // 4. Tactical Overlays Lines - for (final layer in _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 (_activeRoute != null && _activeRoute!.polylinePoints.isNotEmpty) { - polylines.add( - Polyline( - polylineId: const PolylineId('tactical_on_device_route'), - points: _activeRoute!.polylinePoints, - color: navState.isNavigating ? const Color(0xFF00F0FF) : const Color(0xFF38BDF8), - width: navState.isNavigating ? 6.5 : 5.0, - ), - ); - } - - return polylines; - } - - Set _buildPolygons() { - final polygons = {}; - - // 1. 360 Viewshed Radar Visible Polygon Fill - if (_currentTacticalMode == 'viewshed_360' && - _activeViewshedReport != null && - _activeViewshedReport!.polygonVertices.isNotEmpty) { - polygons.add( - Polygon( - polygonId: const PolygonId('viewshed_360_fill'), - points: _activeViewshedReport!.polygonVertices, - fillColor: const Color(0x4422C55E), - strokeColor: const Color(0xFF22C55E), - strokeWidth: 2, - ), - ); - } - - // 2. HLZ Pad Boundary & Approach Corridor - if (_currentTacticalMode == 'hlz' && _activeHlzResult != null) { - polygons.add( - Polygon( - polygonId: const PolygonId('hlz_pad_boundary'), - points: _activeHlzResult!.padBoundary, - fillColor: _activeHlzResult!.gradeColor.withAlpha(50), - strokeColor: _activeHlzResult!.gradeColor, - strokeWidth: 2, - ), - ); - - polygons.add( - Polygon( - polygonId: const PolygonId('hlz_approach_corridor'), - points: _activeHlzResult!.approachFunnel, - fillColor: const Color(0x3338BDF8), - strokeColor: const Color(0xFF38BDF8), - strokeWidth: 1.5, - ), - ); - } - - // 3. Minefield Boundary & Safe Breaching Lane - if (_currentTacticalMode == 'minefield' && _activeMinefieldResult != null) { - polygons.add( - Polygon( - polygonId: const PolygonId('minefield_threat_boundary'), - points: _activeMinefieldResult!.boundaryPolygon, - fillColor: const Color(0x44EF4444), - strokeColor: const Color(0xFFEF4444), - strokeWidth: 2, - ), - ); - - polygons.add( - Polygon( - polygonId: const PolygonId('minefield_safe_breach_polygon'), - points: _activeMinefieldResult!.breachLanePolygon, - fillColor: const Color(0x6610B981), - strokeColor: const Color(0xFF10B981), - strokeWidth: 2, - ), - ); - } - - // 4. Isochrone Response Time Rings - if (_currentTacticalMode == 'isochrone' && _activeIsochroneRings != null) { - for (int i = 0; i < _activeIsochroneRings!.length; i++) { - final ring = _activeIsochroneRings![i]; - polygons.add( - Polygon( - polygonId: PolygonId('isochrone_ring_$i'), - points: ring.polygonCoordinates, - fillColor: ring.ringColor.withAlpha(30), - strokeColor: ring.ringColor, - strokeWidth: 2, - ), - ); - } - } - - // 5. Tactical Overlays Polygons - for (final layer in _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; - } - @override Widget build(BuildContext context) { + final GlobalKey scaffoldKey = GlobalKey(); + + // Check Initial Provisioning on startup + _checkProvisioning(context); + return ValueListenableBuilder( valueListenable: TurnByTurnNavigationEngine.navigationState, builder: (context, navState, _) { - final markers = _buildMarkers(navState); - final polylines = _buildPolylines(navState); - final polygons = _buildPolygons(); - return Scaffold( - key: _scaffoldKey, - drawer: TacticalDrawer( - currentAngleUnit: _angleUnit, - onAngleUnitChanged: (unit) => setState(() => _angleUnit = unit), - onOpenResectionHud: () => setState(() => _currentTacticalMode = 'resection_cam'), - onStartRoutingMode: () => _openRoutePlanner(), - onStartLosMode: () { - setState(() => _currentTacticalMode = 'los'); - _openLosSheet(); - }, - onStartViewshedMode: () { - setState(() => _currentTacticalMode = 'viewshed_360'); - _openViewshed360Sheet(); - }, - onStartArtilleryMode: () { - setState(() => _currentTacticalMode = 'artillery'); - _openArtillerySheet(); - }, - onStartHlzMode: () { - setState(() => _currentTacticalMode = 'hlz'); - _openHlzSheet(); - }, - onStartMinefieldMode: () { - setState(() => _currentTacticalMode = 'minefield'); - _openMinefieldSheet(); - }, - onStartIsochroneMode: () { - setState(() => _currentTacticalMode = 'isochrone'); - _openIsochroneSheet(); - }, - onStartSymbolsMode: () { - setState(() => _currentTacticalMode = 'symbols'); - _openSymbolsSheet(); - }, - onStartOverlaysMode: () { - setState(() => _currentTacticalMode = 'overlays'); - _openOverlaysSheet(); - }, - onLandmarksSynced: () => setState(() {}), - showContours: _showContours, - onToggleContours: (v) => setState(() => _showContours = v), + key: scaffoldKey, + drawer: Obx( + () => TacticalDrawer( + currentAngleUnit: controller.angleUnit.value, + onAngleUnitChanged: controller.setAngleUnit, + onOpenResectionHud: () => controller.switchMode('resection_cam'), + onStartRoutingMode: () => _openRoutePlanner(context), + onStartLosMode: () { + controller.switchMode('los'); + _openLosSheet(context); + }, + onStartViewshedMode: () { + controller.switchMode('viewshed_360'); + _openViewshed360Sheet(context); + }, + onStartArtilleryMode: () { + controller.switchMode('artillery'); + _openArtillerySheet(context); + }, + onStartHlzMode: () { + controller.switchMode('hlz'); + _openHlzSheet(context); + }, + onStartMinefieldMode: () { + controller.switchMode('minefield'); + _openMinefieldSheet(context); + }, + onStartIsochroneMode: () { + controller.switchMode('isochrone'); + _openIsochroneSheet(context); + }, + onStartSymbolsMode: () { + controller.switchMode('symbols'); + _openSymbolsSheet(context); + }, + onStartOverlaysMode: () { + controller.switchMode('overlays'); + _openOverlaysSheet(context); + }, + onLandmarksSynced: () => controller.update(), + showContours: controller.showContours.value, + onToggleContours: controller.toggleContours, + ), ), body: Stack( children: [ - // ── Map Canvas ───────────────────────────────────────── - IntaleqMap( - apiKey: AppConfig.apiKey, - initialCameraPosition: CameraPosition( - target: _currentCameraCenter, - zoom: 13.0, - ), - styleUrl: _showContours - ? 'assets/style_dark.json' - : 'assets/style.json', - onMapCreated: (ctrl) { - _mapController = ctrl; - }, - onCameraMove: (pos) { - _currentCameraCenter = pos.target; - }, - onTap: _handleMapTap, - markers: markers, - polylines: polylines, - polygons: polygons, - ), + // ── 1. Map Canvas with Declarative Layer Overlays ────── + Obx(() { + final markers = controller.buildMarkers(navState); + final polylines = controller.buildPolylines(navState); + final polygons = controller.buildPolygons(); - // ── Top Tactical Operations Bar ──────────────────────── + return IntaleqMap( + apiKey: AppConfig.apiKey, + initialCameraPosition: CameraPosition( + target: controller.currentCameraCenter.value, + zoom: 13.0, + ), + styleUrl: controller.showContours.value + ? 'assets/style_dark.json' + : 'assets/style.json', + onMapCreated: (ctrl) { + controller.mapController = ctrl; + }, + onCameraMove: (pos) { + controller.currentCameraCenter.value = pos.target; + }, + onTap: controller.handleMapTap, + markers: markers, + polylines: polylines, + polygons: polygons, + ); + }), + + // ── 2. Top Tactical Operations Header Bar ────────────── Positioned( top: 48, left: 16, @@ -1281,11 +132,12 @@ class _TacticalMapScreenState extends State { decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFF0071E3).withAlpha(120)), + border: Border.all( + color: const Color(0xFF0071E3).withAlpha(120)), ), child: IconButton( icon: const Icon(Icons.menu, color: Color(0xFF38BDF8)), - onPressed: () => _scaffoldKey.currentState?.openDrawer(), + onPressed: () => scaffoldKey.currentState?.openDrawer(), ), ), const SizedBox(width: 8), @@ -1300,14 +152,17 @@ class _TacticalMapScreenState extends State { ), child: Row( children: [ - const Icon(Icons.shield, color: Color(0xFF0071E3), size: 20), + const Icon(Icons.shield, + color: Color(0xFF0071E3), size: 20), const SizedBox(width: 8), - Text( - _getModeTitle(), - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, + Obx( + () => Text( + controller.getModeTitle(), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -1322,35 +177,56 @@ class _TacticalMapScreenState extends State { border: Border.all(color: Colors.white12), ), child: IconButton( - icon: const Icon(Icons.search, color: Color(0xFF38BDF8)), - onPressed: _openPlaceSearch, + icon: const Icon(Icons.search, + color: Color(0xFF38BDF8)), + onPressed: () => _openPlaceSearch(context), ), ), ], ), ), - // ── Interactive Pin Picker HUD ──────────────────────── - if (_activePickerTarget != null) - InteractiveMapPickerHud( - target: _activePickerTarget!, - centerPosition: _currentCameraCenter, - onConfirm: () => _onConfirmPicker(_activePickerTarget!, _currentCameraCenter), - onCancel: _onCancelPicker, - ), + // ── 3. Interactive Crosshair Target Picker HUD ───────── + Obx(() { + if (controller.activePickerTarget.value == null) { + return const SizedBox.shrink(); + } + return InteractiveMapPickerHud( + target: controller.activePickerTarget.value!, + centerPosition: controller.currentCameraCenter.value, + onConfirm: () => controller.confirmPicker( + controller.activePickerTarget.value!, + controller.currentCameraCenter.value, + ), + onCancel: controller.cancelPicker, + ); + }), - // ── Resection Camera Overlay ─────────────────────────── - if (_currentTacticalMode == 'resection_cam') - CameraResectionView( - observations: _observations, - angleUnit: _angleUnit, - onObservationAdded: _onObservationAdded, - onCalculatePressed: _executeResectionCalculation, - onResetPressed: _resetResection, - onClose: () => setState(() => _currentTacticalMode = 'nav'), - ), + // ── 4. Optical Resection Camera HUD (GPS-Denied) ─────── + Obx(() { + if (controller.currentTacticalMode.value != 'resection_cam') { + return const SizedBox.shrink(); + } + return CameraResectionView( + observations: controller.resection.observations, + angleUnit: controller.angleUnit.value, + onObservationAdded: controller.resection.addObservation, + onCalculatePressed: () { + final res = controller.resection.computePosition(); + if (res != null) { + controller.switchMode('nav'); + controller.mapController?.animateCamera( + CameraUpdate.newLatLngZoom( + LatLng(res.lat, res.lng), 15.0), + ); + } + }, + onResetPressed: controller.resection.reset, + onClose: () => controller.switchMode('nav'), + ); + }), - // ── Active Turn-by-Turn Navigation HUD ───────────────── + // ── 5. Active Turn-by-Turn Navigation HUD ────────────── if (navState.isNavigating) Positioned( top: 48, @@ -1358,10 +234,8 @@ class _TacticalMapScreenState extends State { right: 16, child: ActiveNavigationHud( state: navState, - angleUnit: _angleUnit, - onStopNavigation: () { - TurnByTurnNavigationEngine.stopNavigation(); - }, + angleUnit: controller.angleUnit.value, + onStopNavigation: controller.navigation.stopNavigation, ), ), ], @@ -1371,28 +245,275 @@ class _TacticalMapScreenState extends State { ); } - String _getModeTitle() { - switch (_currentTacticalMode) { - 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 'توجيه القوافل التكتيكي'; - default: - return 'منظومة العمليات الميدانية (Off-Grid)'; + // ── Operations Modal Launchers ───────────────────────────── + + Future _checkProvisioning(BuildContext context) async { + final needed = + await TacticalInitialProvisioningDialog.isProvisioningNeeded(); + if (needed) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => TacticalInitialProvisioningDialog( + onCompleted: () => Navigator.pop(ctx), + ), + ); + }); } } + + void _openRoutePlanner(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => TacticalRoutePlannerSheet( + currentGps: controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + resectionFix: controller.resection.resectionResult.value != null + ? LatLng(controller.resection.resectionResult.value!.lat, + controller.resection.resectionResult.value!.lng) + : null, + onRouteConfirmed: (origin, destination, profile, autoStartNav) { + controller.navigation.calculateRoute( + origin: origin, + destination: destination, + profile: profile, + autoStart: autoStartNav, + mapController: controller.mapController, + ); + }, + onPickOriginOnMap: () => + controller.setPickerTarget(MapPickerTarget.routeOrigin), + onPickDestinationOnMap: () => + controller.setPickerTarget(MapPickerTarget.routeDestination), + onClose: () => Navigator.pop(ctx), + ), + ); + } + + void _openLosSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalLosSheet( + observer: controller.los.observerPosition.value ?? + const LatLng(31.9539, 35.9106), + target: controller.los.targetPosition.value ?? + const LatLng(31.9800, 35.9500), + currentGps: controller.currentGpsPosition.value, + angleUnit: controller.angleUnit.value, + onUpdateLocations: (newObs, newTgt) { + controller.los.setObserver(newObs); + controller.los.setTarget(newTgt); + controller.fitBounds([newObs, newTgt]); + }, + onVisibilityChanged: (_) {}, + onReportGenerated: (_) {}, + onPickObserverOnMap: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.losObserver); + }, + onPickTargetOnMap: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.losTarget); + }, + onClose: () { + Navigator.pop(context); + controller.switchMode('nav'); + }, + ), + ), + ); + } + + void _openViewshed360Sheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalViewshedSheet( + observer: controller.viewshed.observerPosition.value ?? + controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + currentGps: controller.currentGpsPosition.value, + onUpdateObserver: controller.viewshed.setObserver, + onReportUpdated: (_) {}, + onPickObserverOnMap: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.viewshedCenter); + }, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openArtillerySheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalArtillerySheet( + gunPosition: controller.artillery.gunPosition.value ?? + controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + targetPosition: controller.artillery.targetPosition.value, + angleUnit: controller.angleUnit.value, + onPickGun: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.artilleryGun); + }, + onPickTarget: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.artilleryTarget); + }, + onSwap: controller.artillery.swapPositions, + onSolutionCalculated: (sol) { + controller.fitBounds([sol.gunPosition, sol.targetPosition]); + }, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openHlzSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalHlzSheet( + selectedPosition: controller.hlz.selectedPosition.value ?? + controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + onPickLocation: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.hlzCenter); + }, + onAssessmentCompleted: (_) {}, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openMinefieldSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalMinefieldSheet( + startPoint: controller.minefield.startPoint.value ?? + controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + endPoint: controller.minefield.endPoint.value, + onPickStart: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.minefieldStart); + }, + onPickEnd: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.minefieldEnd); + }, + onSwap: controller.minefield.swapPoints, + onZoneCalculated: (res) { + controller.fitBounds([res.startPoint, res.endPoint]); + }, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openIsochroneSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalIsochroneSheet( + center: controller.isochrone.center.value ?? + controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106), + onPickCenter: () { + Navigator.pop(context); + controller.setPickerTarget(MapPickerTarget.isochroneCenter); + }, + onIsochronesCalculated: (_) {}, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openSymbolsSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalSymbolsSheet( + activePlacementType: controller.symbols.activePlacementType.value, + placedSymbols: controller.symbols.placedSymbols, + onSelectSymbolType: (type) { + controller.symbols.selectSymbolForPlacement(type); + Navigator.pop(context); + Get.snackbar( + 'الرموز العسكرية', + 'انقر على الخريطة لتثبيت الرمز في الميدان 📍', + backgroundColor: const Color(0xFF0071E3), + colorText: Colors.white, + snackPosition: SnackPosition.BOTTOM, + ); + }, + onDeleteSymbol: (s) => controller.symbols.removeSymbol(s.id), + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openOverlaysSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => Obx( + () => TacticalOverlaysSheet( + layers: controller.overlays.overlayLayers, + onToggleLayer: controller.overlays.toggleLayer, + onClose: () => Navigator.pop(context), + ), + ), + ); + } + + void _openPlaceSearch(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => PlaceSearchSheet( + onDestinationSelected: (landmark, startNav) { + final dest = LatLng(landmark.lat, landmark.lng); + final origin = controller.currentGpsPosition.value ?? + const LatLng(31.9539, 35.9106); + controller.navigation.calculateRoute( + origin: origin, + destination: dest, + autoStart: startNav, + mapController: controller.mapController, + ); + }, + ), + ); + } } diff --git a/packages/tactical_app/pubspec.lock b/packages/tactical_app/pubspec.lock index 752c6bb..e293510 100644 --- a/packages/tactical_app/pubspec.lock +++ b/packages/tactical_app/pubspec.lock @@ -373,6 +373,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.5" + get: + dependency: "direct main" + description: + name: get + sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a" + url: "https://pub.dev" + source: hosted + version: "4.7.3" glob: dependency: transitive description: diff --git a/packages/tactical_app/pubspec.yaml b/packages/tactical_app/pubspec.yaml index a58b2dc..d2c3705 100644 --- a/packages/tactical_app/pubspec.yaml +++ b/packages/tactical_app/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: envied: ^1.1.1 sqflite: ^2.3.3+1 sqflite_common_ffi: ^2.3.3 + get: ^4.6.6 dev_dependencies: flutter_test: