import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.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 '../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'; import '../widgets/place_search_sheet.dart'; import '../widgets/tactical_artillery_sheet.dart'; import '../widgets/tactical_drawer.dart'; import '../widgets/tactical_hlz_sheet.dart'; import '../widgets/tactical_initial_provisioning_dialog.dart'; import '../widgets/tactical_isochrone_sheet.dart'; import '../widgets/tactical_los_sheet.dart'; import '../widgets/tactical_minefield_sheet.dart'; import '../widgets/tactical_overlays_sheet.dart'; import '../widgets/tactical_route_planner_sheet.dart'; import '../widgets/tactical_symbols_sheet.dart'; import '../widgets/tactical_viewshed_sheet.dart'; class TacticalMapScreen extends StatefulWidget { 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) { 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), ), 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, ), // ── Top Tactical Operations Bar ──────────────────────── Positioned( top: 48, left: 16, right: 16, child: Row( children: [ Container( decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF0071E3).withAlpha(120)), ), child: IconButton( icon: const Icon(Icons.menu, color: Color(0xFF38BDF8)), onPressed: () => _scaffoldKey.currentState?.openDrawer(), ), ), const SizedBox(width: 8), Expanded( child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white12), ), child: Row( children: [ 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, ), ), ], ), ), ), const SizedBox(width: 8), Container( decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white12), ), child: IconButton( icon: const Icon(Icons.search, color: Color(0xFF38BDF8)), onPressed: _openPlaceSearch, ), ), ], ), ), // ── Interactive Pin Picker HUD ──────────────────────── if (_activePickerTarget != null) InteractiveMapPickerHud( target: _activePickerTarget!, centerPosition: _currentCameraCenter, onConfirm: () => _onConfirmPicker(_activePickerTarget!, _currentCameraCenter), onCancel: _onCancelPicker, ), // ── Resection Camera Overlay ─────────────────────────── if (_currentTacticalMode == 'resection_cam') CameraResectionView( observations: _observations, angleUnit: _angleUnit, onObservationAdded: _onObservationAdded, onCalculatePressed: _executeResectionCalculation, onResetPressed: _resetResection, onClose: () => setState(() => _currentTacticalMode = 'nav'), ), // ── Active Turn-by-Turn Navigation HUD ───────────────── if (navState.isNavigating) Positioned( top: 48, left: 16, right: 16, child: ActiveNavigationHud( state: navState, angleUnit: _angleUnit, onStopNavigation: () { TurnByTurnNavigationEngine.stopNavigation(); }, ), ), ], ), ); }, ); } 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)'; } } }