740 lines
26 KiB
Dart
740 lines
26 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
|
|
import '../models/angle_unit.dart';
|
|
import '../models/navigation_state.dart';
|
|
import '../models/tactical_sheet_type.dart';
|
|
import '../services/military_grid_utils.dart';
|
|
import '../services/offline_los_engine.dart';
|
|
|
|
import '../services/local_network_tracker.dart';
|
|
|
|
import '../widgets/interactive_map_picker_hud.dart';
|
|
import 'artillery_controller.dart';
|
|
import 'hlz_controller.dart';
|
|
import 'isochrone_controller.dart';
|
|
import 'los_controller.dart';
|
|
import 'minefield_controller.dart';
|
|
import 'navigation_controller.dart';
|
|
import 'overlays_controller.dart';
|
|
import 'resection_controller.dart';
|
|
import 'symbols_controller.dart';
|
|
import 'viewshed_controller.dart';
|
|
|
|
/// ============================================================================
|
|
/// [TacticalMapController] - المحرك الرئيسي لمنظومة الخريطة والعمليات الميدانية
|
|
/// ============================================================================
|
|
/// English:
|
|
/// Master GetX Controller orchestrating tactical modes, map camera state,
|
|
/// declarative layers (Markers, Polylines, Polygons), and crosshair picker HUDs.
|
|
///
|
|
/// العربية:
|
|
/// المتحكم المركزي لمنظومة العمليات التكتيكية، يربط بين مختلف المتحكمات الفرعية
|
|
/// (المدفعية، المهابط، الألغام، الشفافات، الرموز) وينظم طبقات الخريطة والمؤشر التفاعلي.
|
|
/// ============================================================================
|
|
class TacticalMapController extends GetxController {
|
|
// ── Sub-Controllers / المتحكمات الفرعية ──────────────────────────────────
|
|
final ArtilleryController artillery = Get.find<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>();
|
|
final LocalNetworkTrackerService tracker = Get.find<LocalNetworkTrackerService>();
|
|
|
|
|
|
// ── Observables / الحالات التفاعلية ──────────────────────────────────────
|
|
final RxString currentTacticalMode = 'nav'.obs;
|
|
final Rx<AngleUnit> angleUnit = AngleUnit.dual.obs;
|
|
final RxBool showContours = false.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);
|
|
final Rx<LatLng?> pickedRouteOrigin = Rx<LatLng?>(null);
|
|
final Rx<LatLng?> pickedRouteDestination = Rx<LatLng?>(null);
|
|
|
|
// ── Persistent Sheets / النوافذ التكتيكية السفلية المستمرة ────────────────
|
|
final Rx<ActiveTacticalSheet?> activeSheet = Rx<ActiveTacticalSheet?>(null);
|
|
final RxBool isSheetMinimized = false.obs;
|
|
|
|
void openSheet(ActiveTacticalSheet sheet) {
|
|
activeSheet.value = sheet;
|
|
isSheetMinimized.value = false;
|
|
}
|
|
|
|
void closeSheet() {
|
|
activeSheet.value = null;
|
|
isSheetMinimized.value = false;
|
|
}
|
|
|
|
IntaleqMapController? mapController;
|
|
|
|
/// Clear All Tactical Layers & Reset Map / مسح كافة الطبقات والحسابات التكتيكية وتصفير الخريطة
|
|
void clearAllTacticalLayers() {
|
|
artillery.reset();
|
|
hlz.reset();
|
|
minefield.reset();
|
|
isochrone.reset();
|
|
los.reset();
|
|
viewshed.reset();
|
|
resection.reset();
|
|
navigation.reset();
|
|
symbols.reset();
|
|
pickedRouteOrigin.value = null;
|
|
pickedRouteDestination.value = null;
|
|
activePickerTarget.value = null;
|
|
currentTacticalMode.value = 'nav';
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
initGps();
|
|
}
|
|
|
|
/// Initialize Live GPS Sensor / تهيئة حساس الموقع الجغرافي
|
|
Future<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) {
|
|
// 1. Get initial position and jump camera
|
|
final pos = await Geolocator.getCurrentPosition(
|
|
locationSettings: const LocationSettings(
|
|
accuracy: LocationAccuracy.high,
|
|
timeLimit: Duration(seconds: 5),
|
|
),
|
|
);
|
|
currentGpsPosition.value = LatLng(pos.latitude, pos.longitude);
|
|
currentCameraCenter.value = currentGpsPosition.value!;
|
|
tracker.updateMyPosition(currentGpsPosition.value!, pos.heading);
|
|
|
|
mapController?.animateCamera(
|
|
CameraUpdate.newLatLngZoom(currentGpsPosition.value!, 14.0),
|
|
);
|
|
|
|
// 2. Listen to continuous GPS updates for Blue Force Tracking
|
|
Geolocator.getPositionStream(
|
|
locationSettings: const LocationSettings(
|
|
accuracy: LocationAccuracy.high,
|
|
distanceFilter: 2, // update every 2 meters
|
|
),
|
|
).listen((Position newPos) {
|
|
final latLng = LatLng(newPos.latitude, newPos.longitude);
|
|
currentGpsPosition.value = latLng;
|
|
tracker.updateMyPosition(latLng, newPos.heading);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('GPS init error: $e');
|
|
}
|
|
}
|
|
|
|
/// Switch Tactical Operation Mode / تبديل نمط العملية التكتيكية
|
|
void switchMode(String mode) {
|
|
currentTacticalMode.value = mode;
|
|
}
|
|
|
|
/// Set Angle Unit Format / تبديل نظام قياس الزوايا والسمت (درجات / ميل)
|
|
void setAngleUnit(AngleUnit unit) {
|
|
angleUnit.value = unit;
|
|
}
|
|
|
|
/// Toggle Topographic Contours / إظهار أو إخفاء خطوط الكنتور
|
|
void toggleContours(bool visible) {
|
|
showContours.value = visible;
|
|
}
|
|
|
|
/// Set Active Picker Target / تفعيل المؤشر التفاعلي لاختيار نقطة على الخريطة
|
|
void setPickerTarget(MapPickerTarget? target) {
|
|
activePickerTarget.value = target;
|
|
}
|
|
|
|
/// Handle Map Click Tap Events / معالجة النقر على الخريطة
|
|
void handleMapTap(LatLng point) {
|
|
// 1. Check if placing a tactical symbol
|
|
if (symbols.activePlacementType.value != null) {
|
|
symbols.placeSymbolAt(point);
|
|
Get.snackbar(
|
|
'الرموز العسكرية',
|
|
'تم تثبيت الرمز في الميدان بنجاح ✅',
|
|
backgroundColor: const Color(0xFF22C55E),
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 2. Check if active picker HUD is open
|
|
if (activePickerTarget.value != null) {
|
|
confirmPicker(activePickerTarget.value!, point);
|
|
return;
|
|
}
|
|
|
|
// 3. Mode-specific quick tap handlers
|
|
switch (currentTacticalMode.value) {
|
|
case 'viewshed_360':
|
|
viewshed.setObserver(point);
|
|
break;
|
|
case 'artillery':
|
|
if (artillery.gunPosition.value == null) {
|
|
artillery.setGunPosition(point);
|
|
} else {
|
|
artillery.setTargetPosition(point);
|
|
}
|
|
break;
|
|
case 'hlz':
|
|
hlz.setPosition(point);
|
|
break;
|
|
case 'minefield':
|
|
if (minefield.startPoint.value == null) {
|
|
minefield.setStartPoint(point);
|
|
} else {
|
|
minefield.setEndPoint(point);
|
|
}
|
|
break;
|
|
case 'isochrone':
|
|
isochrone.setCenter(point);
|
|
break;
|
|
case 'los':
|
|
if (los.observerPosition.value == null) {
|
|
los.setObserver(point);
|
|
} else {
|
|
los.setTarget(point);
|
|
}
|
|
break;
|
|
default:
|
|
final mil = MilitaryGridUtils.fromLatLng(point);
|
|
final elev = JordanDemSurface.elevationAt(point.latitude, point.longitude);
|
|
Get.snackbar(
|
|
'الإحداثيات العسكرية',
|
|
'${mil.arabicFullFormat} • منسوب: ${elev.round()}م',
|
|
backgroundColor: const Color(0xFF0F172A),
|
|
colorText: Colors.white,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
duration: const Duration(seconds: 3),
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// Confirm Point Selection from Crosshair HUD / تأكيد النقطة المحددة بالمؤشر
|
|
void confirmPicker(MapPickerTarget target, LatLng pos) {
|
|
activePickerTarget.value = null;
|
|
switch (target) {
|
|
case MapPickerTarget.losObserver:
|
|
los.setObserver(pos);
|
|
break;
|
|
case MapPickerTarget.losTarget:
|
|
los.setTarget(pos);
|
|
break;
|
|
case MapPickerTarget.viewshedCenter:
|
|
viewshed.setObserver(pos);
|
|
break;
|
|
case MapPickerTarget.artilleryGun:
|
|
artillery.setGunPosition(pos);
|
|
break;
|
|
case MapPickerTarget.artilleryTarget:
|
|
artillery.setTargetPosition(pos);
|
|
break;
|
|
case MapPickerTarget.hlzCenter:
|
|
hlz.setPosition(pos);
|
|
break;
|
|
case MapPickerTarget.minefieldStart:
|
|
minefield.setStartPoint(pos);
|
|
break;
|
|
case MapPickerTarget.minefieldEnd:
|
|
minefield.setEndPoint(pos);
|
|
break;
|
|
case MapPickerTarget.isochroneCenter:
|
|
isochrone.setCenter(pos);
|
|
break;
|
|
case MapPickerTarget.resectionLandmark:
|
|
resection.setMapLandmark(pos.latitude, pos.longitude);
|
|
switchMode('resection_cam');
|
|
break;
|
|
case MapPickerTarget.routeOrigin:
|
|
pickedRouteOrigin.value = pos;
|
|
switchMode('routing');
|
|
break;
|
|
case MapPickerTarget.routeDestination:
|
|
pickedRouteDestination.value = pos;
|
|
switchMode('routing');
|
|
final origin = pickedRouteOrigin.value ?? currentGpsPosition.value ?? const LatLng(31.9539, 35.9106);
|
|
navigation.calculateRoute(
|
|
origin: origin,
|
|
destination: pos,
|
|
mapController: mapController,
|
|
).then((plan) {
|
|
if (plan != null && plan.polylinePoints.isNotEmpty) {
|
|
fitBounds(plan.polylinePoints);
|
|
}
|
|
|
|
});
|
|
break;
|
|
}
|
|
if (activeSheet.value != null) { isSheetMinimized.value = false; }
|
|
}
|
|
|
|
/// Cancel Picker / إلغاء وضع المؤشر
|
|
void cancelPicker() {
|
|
activePickerTarget.value = null;
|
|
}
|
|
|
|
/// Fit Map Camera to Bounding Box of Points / تحريك الكاميرا لتحتوي النقاط
|
|
void fitBounds(List<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 'توجيه القوافل التكتيكي';
|
|
case 'rangefinder':
|
|
return 'قياس المسافة البصري (بدون GPS)';
|
|
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: InfoWindow(
|
|
title: '🎯 موقعك المحسوب (GPS-Denied Fix)',
|
|
snippet: 'خطأ الرصد: ±${res.estimatedAccuracyMeters.toStringAsFixed(1)}م • دقة تكتيكية عالية',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 3. Routing Destination Marker
|
|
if (navigation.activeRoute.value != null &&
|
|
navigation.activeRoute.value!.polylinePoints.isNotEmpty) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('convoy_destination'),
|
|
position: navigation.activeRoute.value!.polylinePoints.last,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed),
|
|
infoWindow: const InfoWindow(
|
|
title: 'الهدف التكتيكي (Objective)',
|
|
snippet: 'نقطة الوصول المحددة',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 4. Moving Vehicle Marker
|
|
if (navState.isNavigating && navState.currentPosition != null) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('active_vehicle_position'),
|
|
position: navState.currentPosition!,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan),
|
|
infoWindow: InfoWindow(
|
|
title: 'مركبة العمليات الميدانية',
|
|
snippet:
|
|
'${navState.currentSpeedKmH.round()} كم/س • اتجاه ${navState.currentHeadingDeg.round()}°',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 5. Tactical Placed Symbols
|
|
for (final sym in symbols.placedSymbols) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: MarkerId(sym.id),
|
|
position: sym.position,
|
|
icon: InlqBitmap.defaultMarkerWithHue(
|
|
sym.isEnemy ? InlqBitmap.hueRed : InlqBitmap.hueBlue,
|
|
),
|
|
infoWindow: InfoWindow(
|
|
title: sym.name,
|
|
snippet: MilitaryGridUtils.latLngToMgrs(
|
|
sym.position.latitude, sym.position.longitude),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 6. Artillery Gun & Target Markers
|
|
if (currentTacticalMode.value == 'artillery') {
|
|
if (artillery.gunPosition.value != null) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('artillery_gun_marker'),
|
|
position: artillery.gunPosition.value!,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueBlue),
|
|
infoWindow: const InfoWindow(title: '🎯 مربض المدفعية'),
|
|
),
|
|
);
|
|
}
|
|
if (artillery.targetPosition.value != null) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('artillery_target_marker'),
|
|
position: artillery.targetPosition.value!,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed),
|
|
infoWindow: const InfoWindow(title: '💥 الهدف المعادي'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 7. HLZ Center Marker
|
|
if (currentTacticalMode.value == 'hlz' && hlz.selectedPosition.value != null) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('hlz_center_marker'),
|
|
position: hlz.selectedPosition.value!,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen),
|
|
infoWindow: const InfoWindow(title: '🚁 مهبط الطيران العامودي (HLZ)'),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
// --- Blue Force Tracking (Friendly Units) ---
|
|
for (final unit in tracker.friendlyUnits.values) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: MarkerId('bft_${unit.deviceId}'),
|
|
position: unit.position,
|
|
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure), // Blue for friendly
|
|
infoWindow: InfoWindow(
|
|
title: '${unit.callsign} (${unit.role.name})',
|
|
snippet: 'آخر ظهور: منذ ${DateTime.now().difference(unit.lastSeen).inSeconds} ثوانٍ',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return markers;
|
|
}
|
|
|
|
/// Build all declarative map polylines / تجميع مسارات الخريطة
|
|
Set<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,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 6. Visual Resection Sightlines (خطوط الرؤية البصرية للمعالم المرصودة)
|
|
if (resection.resectionResult.value != null && resection.observations.isNotEmpty) {
|
|
final res = resection.resectionResult.value!;
|
|
final observerPos = LatLng(res.lat, res.lng);
|
|
for (int i = 0; i < resection.observations.length; i++) {
|
|
final obs = resection.observations[i];
|
|
final landmarkPos = LatLng(obs.landmark.lat, obs.landmark.lng);
|
|
polylines.add(
|
|
Polyline(
|
|
polylineId: PolylineId('resection_sightline_$i'),
|
|
points: [observerPos, landmarkPos],
|
|
color: const Color(0xFF00F0FF),
|
|
width: 3.5,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return polylines;
|
|
}
|
|
|
|
/// Build all declarative map polygons / تجميع مضلعات الخريطة
|
|
Set<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(0x3310B981),
|
|
strokeColor: const Color(0xFF10B981),
|
|
strokeWidth: 2,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 4. Resection Position Uncertainty Ring (دائرة دقة الموقع التكتيكي الأخضر)
|
|
if (resection.resectionResult.value != null) {
|
|
final res = resection.resectionResult.value!;
|
|
final radiusMeters = math.max(25.0, res.estimatedAccuracyMeters);
|
|
final ringPoints = <LatLng>[];
|
|
for (int a = 0; a <= 360; a += 15) {
|
|
final rad = a * (math.pi / 180.0);
|
|
final dLat = (radiusMeters / 6371000.0) * (180.0 / math.pi);
|
|
final dLng = (radiusMeters / 6371000.0) * (180.0 / math.pi) / math.cos(res.lat * math.pi / 180.0);
|
|
ringPoints.add(LatLng(res.lat + dLat * math.sin(rad), res.lng + dLng * math.cos(rad)));
|
|
}
|
|
polygons.add(
|
|
Polygon(
|
|
polygonId: const PolygonId('resection_accuracy_circle'),
|
|
points: ringPoints,
|
|
fillColor: const Color(0x3322C55E),
|
|
strokeColor: const Color(0xFF22C55E),
|
|
strokeWidth: 2.0,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 5. Isochrone Response Time Rings
|
|
if (currentTacticalMode.value == 'isochrone' &&
|
|
isochrone.isochroneRings.isNotEmpty) {
|
|
for (int i = 0; i < isochrone.isochroneRings.length; i++) {
|
|
final ring = isochrone.isochroneRings[i];
|
|
polygons.add(
|
|
Polygon(
|
|
polygonId: PolygonId('isochrone_ring_$i'),
|
|
points: ring.polygonCoordinates,
|
|
fillColor: ring.ringColor.withAlpha(30),
|
|
strokeColor: ring.ringColor,
|
|
strokeWidth: 2,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 6. Tactical Overlays Polygons
|
|
for (final layer in overlays.overlayLayers) {
|
|
if (layer.isVisible) {
|
|
for (int i = 0; i < layer.polygons.length; i++) {
|
|
polygons.add(
|
|
Polygon(
|
|
polygonId: PolygonId('overlay_${layer.id}_poly_$i'),
|
|
points: layer.polygons[i],
|
|
fillColor: layer.color.withAlpha(40),
|
|
strokeColor: layer.color,
|
|
strokeWidth: 2,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return polygons;
|
|
}
|
|
}
|