refactor(tactical-app): modernize state management with GetX controllers, bindings, and clean MVP architecture
This commit is contained in:
@@ -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>(() => ArtilleryController(), fenix: true);
|
||||
Get.lazyPut<HlzController>(() => HlzController(), fenix: true);
|
||||
Get.lazyPut<MinefieldController>(() => MinefieldController(), fenix: true);
|
||||
Get.lazyPut<IsochroneController>(() => IsochroneController(), fenix: true);
|
||||
Get.lazyPut<SymbolsController>(() => SymbolsController(), fenix: true);
|
||||
Get.lazyPut<OverlaysController>(() => OverlaysController(), fenix: true);
|
||||
Get.lazyPut<LosController>(() => LosController(), fenix: true);
|
||||
Get.lazyPut<ViewshedController>(() => ViewshedController(), fenix: true);
|
||||
Get.lazyPut<ResectionController>(() => ResectionController(), fenix: true);
|
||||
Get.lazyPut<NavigationController>(() => NavigationController(), fenix: true);
|
||||
|
||||
// 2. Master Map Orchestrator Controller
|
||||
Get.lazyPut<TacticalMapController>(() => TacticalMapController(), fenix: true);
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> gunPosition = Rx<LatLng?>(null);
|
||||
final Rx<LatLng?> targetPosition = Rx<LatLng?>(null);
|
||||
final Rx<ArtilleryWeaponSystem> selectedWeapon =
|
||||
Rx<ArtilleryWeaponSystem>(ArtilleryWeaponSystem.standardSystems.first);
|
||||
final RxBool highAngle = false.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<ArtilleryFiringSolution?> firingSolution =
|
||||
Rx<ArtilleryFiringSolution?>(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<void> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> selectedPosition = Rx<LatLng?>(null);
|
||||
final Rx<HelicopterType> helicopterType =
|
||||
Rx<HelicopterType>(HelicopterType.mediumLift);
|
||||
final RxDouble approachAzimuthDeg = 0.0.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<HlzAssessmentResult?> assessmentResult =
|
||||
Rx<HlzAssessmentResult?>(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<void> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> center = Rx<LatLng?>(null);
|
||||
final RxDouble speedKmh = 60.0.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxList<IsochroneRing> isochroneRings = <IsochroneRing>[].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<void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> observerPosition = Rx<LatLng?>(null);
|
||||
final Rx<LatLng?> targetPosition = Rx<LatLng?>(null);
|
||||
final RxDouble observerHeightM = 2.0.obs;
|
||||
final RxDouble targetHeightM = 2.0.obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<OfflineLosReport?> losReport = Rx<OfflineLosReport?>(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<void> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> startPoint = Rx<LatLng?>(null);
|
||||
final Rx<LatLng?> endPoint = Rx<LatLng?>(null);
|
||||
final Rx<MinefieldType> minefieldType =
|
||||
Rx<MinefieldType>(MinefieldType.antiTank);
|
||||
final RxDouble widthMeters = 200.0.obs;
|
||||
final Rx<MinefieldZoneResult?> zoneResult = Rx<MinefieldZoneResult?>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<OfflineRoutePlan?> activeRoute = Rx<OfflineRoutePlan?>(null);
|
||||
final Rx<TacticalVehicleProfile> vehicleProfile =
|
||||
Rx<TacticalVehicleProfile>(TacticalVehicleProfile.convoy);
|
||||
final RxBool isCalculating = false.obs;
|
||||
|
||||
/// Calculate Hybrid Tactical Route / احتساب مسار تكتيكي هجين (سيرفر + محلي)
|
||||
Future<OfflineRoutePlan?> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<TacticalOverlayLayer> overlayLayers = <TacticalOverlayLayer>[
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ResectionObservation> observations = <ResectionObservation>[].obs;
|
||||
final Rx<ResectionResult?> resectionResult = Rx<ResectionResult?>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<TacticalSymbolType?> activePlacementType = Rx<TacticalSymbolType?>(null);
|
||||
final RxList<TacticalSymbolItem> placedSymbols = <TacticalSymbolItem>[
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<ArtilleryController>();
|
||||
final HlzController hlz = Get.find<HlzController>();
|
||||
final MinefieldController minefield = Get.find<MinefieldController>();
|
||||
final IsochroneController isochrone = Get.find<IsochroneController>();
|
||||
final SymbolsController symbols = Get.find<SymbolsController>();
|
||||
final OverlaysController overlays = Get.find<OverlaysController>();
|
||||
final LosController los = Get.find<LosController>();
|
||||
final ViewshedController viewshed = Get.find<ViewshedController>();
|
||||
final ResectionController resection = Get.find<ResectionController>();
|
||||
final NavigationController navigation = Get.find<NavigationController>();
|
||||
|
||||
// ── Observables / الحالات التفاعلية ──────────────────────────────────────
|
||||
final RxString currentTacticalMode = 'nav'.obs;
|
||||
final Rx<AngleUnit> angleUnit = AngleUnit.dual.obs;
|
||||
final RxBool showContours = true.obs;
|
||||
final Rx<LatLng?> currentGpsPosition = Rx<LatLng?>(null);
|
||||
final Rx<LatLng> currentCameraCenter = const LatLng(31.9539, 35.9106).obs;
|
||||
final Rx<MapPickerTarget?> activePickerTarget = Rx<MapPickerTarget?>(null);
|
||||
|
||||
IntaleqMapController? mapController;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initGps();
|
||||
}
|
||||
|
||||
/// Initialize Live GPS Sensor / تهيئة حساس الموقع الجغرافي
|
||||
Future<void> 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<LatLng> 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<Marker> buildMarkers(ActiveNavigationState navState) {
|
||||
final markers = <Marker>{};
|
||||
|
||||
// 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<Polyline> buildPolylines(ActiveNavigationState navState) {
|
||||
final polylines = <Polyline>{};
|
||||
|
||||
// 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<Polygon> buildPolygons() {
|
||||
final polygons = <Polygon>{};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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<LatLng?> observerPosition = Rx<LatLng?>(null);
|
||||
final RxDouble radiusKm = 5.0.obs;
|
||||
final RxDouble observerHeightM = 2.0.obs;
|
||||
final RxBool isCalculating = false.obs;
|
||||
final Rx<Viewshed360Report?> viewshedReport = Rx<Viewshed360Report?>(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<void> 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;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user