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/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/turn_by_turn_navigation_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_drawer.dart'; import '../widgets/tactical_los_sheet.dart'; import '../widgets/tactical_viewshed_sheet.dart'; import '../widgets/tactical_route_planner_sheet.dart'; import '../widgets/tactical_route_preview_card.dart'; import '../services/viewshed_360_engine.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', 'isochrone' 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; bool _isLosVisible = true; OfflineLosReport? _activeLosReport; // Tactical 360 Viewshed State Viewshed360Report? _activeViewshedReport; // Topographic Contours / Style Layer State bool _showContours = true; @override void initState() { super.initState(); _initGps(); } 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; } }); if (target == MapPickerTarget.losObserver || target == MapPickerTarget.losTarget) { _openLosSheet(); } else if (target == MapPickerTarget.routeOrigin || target == MapPickerTarget.routeDestination) { _openRoutePlanner(); } } void _onCancelPicker() { final target = _activePickerTarget; setState(() => _activePickerTarget = null); if (target == MapPickerTarget.losObserver || target == MapPickerTarget.losTarget) { _openLosSheet(); } else if (target == MapPickerTarget.routeOrigin || target == MapPickerTarget.routeDestination) { _openRoutePlanner(); } } void _resetResection() { TurnByTurnNavigationEngine.stopNavigation(); setState(() { _observations.clear(); _resectionResult = null; _activeRoute = null; _losObserver = null; _losTarget = null; _activeLosReport = null; _currentTacticalMode = 'nav'; }); } void _startLosMode() { setState(() { _currentTacticalMode = 'los'; }); if (_losObserver != null && _losTarget != null) { _openLosSheet(); } else { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 4), backgroundColor: Color(0xFF0F172A), content: Row( children: [ Icon(Icons.touch_app, size: 18, color: Color(0xFF38BDF8)), SizedBox(width: 10), Expanded( child: Text( 'انقر على الخريطة لتحديد موقع الراصد 👁️ ثم انقر لتحديد الهدف 🎯', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), ), ), ], ), ), ); } } void _openLosSheet() { if (_losObserver == null || _losTarget == null) return; final initialLos = OfflineLosEngine.calculate( observer: _losObserver!, target: _losTarget!, ); setState(() { _isLosVisible = initialLos.isDirectlyVisible; _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(() { _isLosVisible = updatedLos.isDirectlyVisible; _activeLosReport = updatedLos; }); _fitRouteBounds([newObs, newTgt]); }, onVisibilityChanged: (vis) { if (mounted) setState(() => _isLosVisible = vis); }, onReportGenerated: (report) { if (mounted) { setState(() { _activeLosReport = report; _isLosVisible = report.isDirectlyVisible; }); } }, onPickObserverOnMap: () { Navigator.pop(context); setState(() => _activePickerTarget = MapPickerTarget.losObserver); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 3), backgroundColor: Color(0xFF0F172A), content: Text('انقر على الخريطة لتحديد موقع الراصد الجديد 👁️', style: TextStyle(color: Colors.white)), ), ); }, onPickTargetOnMap: () { Navigator.pop(context); setState(() => _activePickerTarget = MapPickerTarget.losTarget); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 3), backgroundColor: Color(0xFF0F172A), content: Text('انقر على الخريطة لتحديد موقع الهدف الجديد 🎯', style: TextStyle(color: Colors.white)), ), ); }, onClose: () { Navigator.pop(context); setState(() => _currentTacticalMode = 'nav'); }, ), ); } void _startViewshed360Mode() { setState(() { _currentTacticalMode = 'viewshed_360'; }); if (_losObserver != null) { _openViewshed360Sheet(); } else if (_currentGpsPosition != null) { setState(() => _losObserver = _currentGpsPosition); _openViewshed360Sheet(); } else { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 4), backgroundColor: Color(0xFF0F172A), content: Row( children: [ Icon(Icons.radar, size: 18, color: Color(0xFF38BDF8)), SizedBox(width: 10), Expanded( child: Text( 'انقر على الخريطة لتحديد مركز الرصد والرادار 360° 🌐', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), ), ), ], ), ), ); } } 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.losObserver); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 3), backgroundColor: Color(0xFF0F172A), content: Text('انقر على الخريطة لتحديد مركز الرصد الجديد 👁️', style: TextStyle(color: Colors.white)), ), ); }, onClose: () { Navigator.pop(context); }, ), ); } void _handleMapTap(LatLng point) { if (_activePickerTarget != null) { if (_activePickerTarget == MapPickerTarget.losObserver) { setState(() { _losObserver = point; _activePickerTarget = null; }); if (_currentTacticalMode == 'viewshed_360') { _openViewshed360Sheet(); } else if (_losTarget != null) { _openLosSheet(); } } else if (_activePickerTarget == MapPickerTarget.losTarget) { setState(() { _losTarget = point; _activePickerTarget = null; }); if (_losObserver != null) { _openLosSheet(); } } else if (_activePickerTarget == MapPickerTarget.routeOrigin) { setState(() => _activePickerTarget = null); _openRoutePlanner(initialOrigin: point); } else if (_activePickerTarget == MapPickerTarget.routeDestination) { setState(() => _activePickerTarget = null); _openRoutePlanner(initialDestination: point); } return; } if (_currentTacticalMode == 'viewshed_360') { setState(() => _losObserver = point); _openViewshed360Sheet(); return; } if (_currentTacticalMode == 'los') { if (_losObserver == null) { setState(() => _losObserver = point); ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 3), backgroundColor: Color(0xFF0F172A), content: Row( children: [ Icon(Icons.check_circle, size: 16, color: Color(0xFF22C55E)), SizedBox(width: 8), Text( 'تم تحديد الراصد 👁️ • انقر لتحديد موقع الهدف 🎯', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), ), ], ), ), ); } else if (_losTarget == null) { setState(() => _losTarget = point); _openLosSheet(); } else { setState(() => _losTarget = point); _openLosSheet(); } return; } 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)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 1), backgroundColor: Color(0xFF0071E3), content: Text('جاري احتساب مسار القوافل التكتيكي...'), ), ); 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 failed, falling back to sovereign offline engine: $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 IntaleqMap Set _buildMarkers(ActiveNavigationState navState) { final markers = {}; // 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), ), ), ); } // Calculated Observer 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: 'تم استخراج الموقع بالتقاطع البصري العكسي', ), ), ); } // 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: 'نقطة الوصول المحددة', ), ), ); } // Live Moving Vehicle Marker during Active Navigation 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()}°', ), ), ); } // Tactical Line of Sight (LOS) Markers with Military Grid Coordinates if (_currentTacticalMode == 'los') { if (_losObserver != null) { final obsMil = MilitaryGridUtils.fromLatLng(_losObserver!); final obsElev = JordanDemSurface.elevationAt( _losObserver!.latitude, _losObserver!.longitude); markers.add( Marker( markerId: const MarkerId('los_observer_marker'), position: _losObserver!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan), infoWindow: InfoWindow( title: '🔴 موقع الراصد (Observer)', snippet: '${obsMil.arabicFullFormat} • ${obsElev.round()}م', ), ), ); } if (_losTarget != null) { final tgtMil = MilitaryGridUtils.fromLatLng(_losTarget!); final tgtElev = JordanDemSurface.elevationAt( _losTarget!.latitude, _losTarget!.longitude); markers.add( Marker( markerId: const MarkerId('los_target_marker'), position: _losTarget!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed), infoWindow: InfoWindow( title: '🎯 موقع الهدف (Target)', snippet: '${tgtMil.arabicFullFormat} • ${tgtElev.round()}م', ), ), ); } // Critical Obstacle Marker (نقطة الحجب التضاريسي الأعلى) if (_activeLosReport?.highestObstacle != null && !_isLosVisible) { final obs = _activeLosReport!.highestObstacle!; final obsMil = MilitaryGridUtils.fromLatLng(LatLng(obs.lat, obs.lng)); markers.add( Marker( markerId: const MarkerId('los_obstacle_marker'), position: LatLng(obs.lat, obs.lng), icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueViolet), infoWindow: InfoWindow( title: '⚠️ عائق جبلي حاجب للرؤية: ${obs.elevationM.round()}م', snippet: 'اختراق خط الرؤية: +${obs.excessM.toStringAsFixed(1)}م • ${obsMil.arabicFullFormat}', ), ), ); } } // Tactical 360 Viewshed Observer Marker if (_currentTacticalMode == 'viewshed_360' && _losObserver != null) { final obsMil = MilitaryGridUtils.fromLatLng(_losObserver!); final obsElev = JordanDemSurface.elevationAt( _losObserver!.latitude, _losObserver!.longitude); markers.add( Marker( markerId: const MarkerId('viewshed_360_observer'), position: _losObserver!, icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan), infoWindow: InfoWindow( title: '🌐 مركز الرصد والرادار 360°', snippet: '${obsMil.arabicFullFormat} • ${obsElev.round()}م', ), ), ); } return markers; } Set _buildPolylines(ActiveNavigationState navState) { final polylines = {}; // 1. Tactical Line of Sight (LOS) Ray (Segmented or Direct) 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, ), ); } } else { polylines.add( Polyline( polylineId: const PolylineId('tactical_los_ray'), points: [_losObserver!, _losTarget!], color: _isLosVisible ? const Color(0xFF22C55E) : const Color(0xFFEF4444), width: 4.5, ), ); } } // 2. Tactical 360 Viewshed Perimeter Polygon Border if (_currentTacticalMode == 'viewshed_360' && _activeViewshedReport != null) { polylines.add( Polyline( polylineId: const PolylineId('viewshed_360_perimeter'), points: _activeViewshedReport!.polygonVertices, color: const Color(0xFF22C55E), width: 4.0, ), ); } // 3. Resection Triangulation Rays if (_resectionResult != null && !navState.isNavigating) { final observerPos = LatLng(_resectionResult!.lat, _resectionResult!.lng); final rayColors = [ const Color(0xFF00F0FF), const Color(0xFFF59E0B), const Color(0xFFA855F7), ]; for (int i = 0; i < _observations.length; i++) { final obs = _observations[i]; final color = rayColors[i % rayColors.length]; polylines.add( Polyline( polylineId: PolylineId('resection_ray_${obs.landmark.id}'), points: [ observerPos, LatLng(obs.landmark.lat, obs.landmark.lng), ], color: color, width: 3.5, ), ); } } // 4. 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 _buildCircles() { final circles = {}; if (_resectionResult != null) { circles.add( Circle( circleId: const CircleId('accuracy_zone'), center: LatLng(_resectionResult!.lat, _resectionResult!.lng), radius: _resectionResult!.estimatedAccuracyMeters, fillColor: const Color(0x3322C55E), strokeColor: const Color(0xFF22C55E), strokeWidth: 2, ), ); } // 360 Viewshed Radar Max Range Ring if (_currentTacticalMode == 'viewshed_360' && _activeViewshedReport != null) { circles.add( Circle( circleId: const CircleId('viewshed_360_range_ring'), center: _activeViewshedReport!.center, radius: _activeViewshedReport!.radiusMeters, fillColor: const Color(0x1538BDF8), strokeColor: const Color(0x6638BDF8), strokeWidth: 1.5, ), ); } return circles; } Set _buildPolygons() { final polygons = {}; // 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(0x3322C55E), strokeColor: const Color(0xFF22C55E), strokeWidth: 2, ), ); } return polygons; } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: TurnByTurnNavigationEngine.navigationState, builder: (context, _) { final navState = TurnByTurnNavigationEngine.navigationState.value; return Scaffold( key: _scaffoldKey, drawer: navState.isNavigating ? null : TacticalDrawer( currentAngleUnit: _angleUnit, onAngleUnitChanged: (unit) => setState(() => _angleUnit = unit), onOpenResectionHud: () => setState(() => _currentTacticalMode = 'resection_cam'), onStartRoutingMode: _openRoutePlanner, onStartLosMode: _startLosMode, onStartIsochroneMode: () { setState(() => _currentTacticalMode = 'isochrone'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( backgroundColor: Color(0xFFA855F7), content: Text( 'وضع مضلعات الحركة Isochrone: جاري تحليل نطاق الوصول الميداني.'), ), ); }, onLandmarksSynced: () => setState(() {}), showContours: _showContours, onToggleContours: (val) { setState(() => _showContours = val); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: const Color(0xFFF59E0B), content: Text( val ? 'تم تفعيل طبقات التضاريس والكنتور' : 'تم إخفاء طبقات التضاريس والكنتور', ), ), ); }, ), appBar: navState.isNavigating ? null : AppBar( backgroundColor: const Color(0xFF0F172A), elevation: 2, leading: IconButton( icon: const Icon(Icons.menu, color: Color(0xFF38BDF8)), onPressed: () => _scaffoldKey.currentState?.openDrawer(), ), title: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: const Color(0xFF0071E3), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.shield, size: 20, color: Colors.white), ), const SizedBox(width: 10), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'منظومة الملاحة التكتيكية (Intaleq Navigation)', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), Text( 'الملاحة البصرية وتبادل الرؤية والتضاريس', style: TextStyle( fontSize: 9.5, color: Color(0xFF94A3B8)), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), actions: [ IconButton( icon: const Icon(Icons.search, color: Color(0xFF38BDF8)), onPressed: _openPlaceSearch, ), GestureDetector( onTap: () { setState(() { if (_angleUnit == AngleUnit.degrees) { _angleUnit = AngleUnit.mils; } else if (_angleUnit == AngleUnit.mils) { _angleUnit = AngleUnit.dual; } else { _angleUnit = AngleUnit.degrees; } }); }, child: Container( margin: const EdgeInsets.symmetric( horizontal: 4, vertical: 12), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF0071E3)), ), child: Text( _angleUnit == AngleUnit.degrees ? '° DEG' : (_angleUnit == AngleUnit.mils ? '₥ MILS' : '°/₥ DUAL'), style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w900, color: Color(0xFF38BDF8)), ), ), ), ], ), body: Stack( children: [ // ── Sovereign Intaleq Map Engine ───────────────────────── IntaleqMap( apiKey: AppConfig.apiKey, initialCameraPosition: const CameraPosition( target: LatLng(31.9539, 35.9106), zoom: 12.0, ), mapType: IntaleqMapType.normal, styleUrl: 'asset://assets/style_offline.json', markers: _buildMarkers(navState), polylines: _buildPolylines(navState), circles: _buildCircles(), polygons: _buildPolygons(), onMapCreated: (ctrl) async { _mapController = ctrl; TurnByTurnNavigationEngine.setMapController(ctrl); // الانتقال الفوري والمباشر إلى موقع المستخدم الحالي بمجرد إنشاء الخريطة if (_currentGpsPosition != null) { ctrl.animateCamera( CameraUpdate.newLatLngZoom(_currentGpsPosition!, 15.0), ); } else { await _initGps(); } }, onCameraMove: (CameraPosition pos) { _currentCameraCenter = pos.target; }, onTap: _handleMapTap, onLongPress: (point) { _calculateHybridRoute( customDestination: point, autoStartNav: true, ); }, ), // ── 0. Interactive Map Center Pin Picker HUD ── if (_activePickerTarget != null) InteractiveMapPickerHud( target: _activePickerTarget!, centerPosition: _currentCameraCenter, onConfirm: () => _onConfirmPicker( _activePickerTarget!, _currentCameraCenter), onCancel: _onCancelPicker, ), // ── 1. Active Navigation Mode Top Banner ── if (navState.isNavigating) ActiveNavigationTopBanner(navState: navState), // ── 2. Active Navigation Mode Bottom HUD (ETA, KM, Stop) ─────── if (navState.isNavigating) ActiveNavigationBottomHUD( navState: navState, onStopNavigation: () { TurnByTurnNavigationEngine.stopNavigation(); setState(() {}); }, ), // ── 3. Normal Explore Mode Top Floating Bar ─────────────────── if (!navState.isNavigating && _activePickerTarget == null) Positioned( top: 12, right: 12, left: 12, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: const Color(0xEB0F172A), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white12), ), child: Row( children: [ _buildModeButton( 'nav', 'الخريطة', Icons.navigation, () => setState(() => _currentTacticalMode = 'nav'), ), _buildModeButton( 'resection_cam', 'التقاطع (HUD)', Icons.camera_alt, () => setState( () => _currentTacticalMode = 'resection_cam'), ), _buildModeButton( 'routing', 'القوافل', Icons.alt_route, _openRoutePlanner, ), _buildModeButton( 'los', 'تبادل الرؤية', Icons.visibility, _startLosMode, ), _buildModeButton( 'viewshed_360', 'رصد 360°', Icons.radar, _startViewshed360Mode, ), ], ), ), ), // ── 3.1 Line of Sight (LOS) Step-by-Step Flow Banner ────────── if (!navState.isNavigating && _currentTacticalMode == 'los' && _activePickerTarget == null) Positioned( top: 66, right: 12, left: 12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xF20F172A), borderRadius: BorderRadius.circular(12), border: Border.all( color: const Color(0xFF38BDF8), width: 1.2), boxShadow: const [ BoxShadow( color: Colors.black54, blurRadius: 8, offset: Offset(0, 3)) ], ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _losObserver == null ? const Color(0xFF0284C7) : (_losTarget == null ? const Color(0xFFF59E0B) : const Color(0xFF22C55E)), borderRadius: BorderRadius.circular(6), ), child: Text( _losObserver == null ? 'خطوة 1/2' : (_losTarget == null ? 'خطوة 2/2' : 'جاهز'), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 8), Expanded( child: Text( _losObserver == null ? 'انقر على الخريطة لتحديد موقع الراصد 👁️' : (_losTarget == null ? 'انقر على الخريطة لتحديد الهدف 🎯' : 'تم حساب خط الرؤية بالكامل'), style: const TextStyle( color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ), if (_losObserver == null && _currentGpsPosition != null) TextButton.icon( onPressed: () { setState( () => _losObserver = _currentGpsPosition); }, icon: const Icon(Icons.my_location, size: 14, color: Color(0xFF38BDF8)), label: const Text('موقعي', style: TextStyle( color: Color(0xFF38BDF8), fontSize: 11)), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2)), ) else if (_losObserver != null && _losTarget == null) TextButton( onPressed: () => setState(() => _losObserver = null), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2)), child: const Text('تغيير الراصد', style: TextStyle( color: Color(0xFFF59E0B), fontSize: 11)), ) else if (_losObserver != null && _losTarget != null) ...[ TextButton( onPressed: _openLosSheet, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2)), child: const Text('عرض التقرير 📊', style: TextStyle( color: Color(0xFF38BDF8), fontSize: 11, fontWeight: FontWeight.bold)), ), TextButton( onPressed: () { setState(() { _losObserver = null; _losTarget = null; _activeLosReport = null; }); }, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2)), child: const Text('إعادة', style: TextStyle( color: Color(0xFFEF4444), fontSize: 11)), ), ], ], ), ), ), // ── 3.2 Tactical 360 Viewshed Step Flow Banner ──────────────── if (!navState.isNavigating && _currentTacticalMode == 'viewshed_360' && _activePickerTarget == null) Positioned( top: 66, right: 12, left: 12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xF20F172A), borderRadius: BorderRadius.circular(12), border: Border.all( color: const Color(0xFF4ADE80), width: 1.2), boxShadow: const [ BoxShadow( color: Colors.black54, blurRadius: 8, offset: Offset(0, 3)) ], ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF16A34A), borderRadius: BorderRadius.circular(6), ), child: const Text( 'رصد 360°', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 8), Expanded( child: Text( _losObserver == null ? 'انقر على الخريطة لتحديد مركز الرصد 🌐' : 'تم مسح محيط الرصد ${_activeViewshedReport != null ? "(${_activeViewshedReport!.visibilityPercentage}%)" : ""}', style: const TextStyle( color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ), if (_losObserver == null && _currentGpsPosition != null) TextButton.icon( onPressed: () { setState( () => _losObserver = _currentGpsPosition); _openViewshed360Sheet(); }, icon: const Icon(Icons.my_location, size: 14, color: Color(0xFF4ADE80)), label: const Text('موقعي', style: TextStyle( color: Color(0xFF4ADE80), fontSize: 11)), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2)), ) else if (_losObserver != null) TextButton.icon( onPressed: _openViewshed360Sheet, icon: const Icon(Icons.tune, size: 14, color: Color(0xFF38BDF8)), label: const Text('الخصائص ⚙️', style: TextStyle( color: Color(0xFF38BDF8), fontSize: 11, fontWeight: FontWeight.bold)), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2)), ), ], ), ), ), // ── 4. Quick Trigger Buttons in Explore Mode ─────────────────── if (!navState.isNavigating && _currentTacticalMode == 'nav' && _resectionResult == null && _activeRoute == null) Positioned( bottom: 24, right: 16, left: 16, child: Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _openPlaceSearch, icon: const Icon(Icons.search, size: 18), label: const Text('البحث عن وجهة وتوجيه', style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.bold)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0071E3), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14)), ), ), ), const SizedBox(width: 10), IconButton.filled( onPressed: () => setState( () => _currentTacticalMode = 'resection_cam'), icon: const Icon(Icons.camera_alt, size: 20), style: IconButton.styleFrom( backgroundColor: const Color(0xFF0F172A), foregroundColor: const Color(0xFF00F0FF), padding: const EdgeInsets.all(14), side: const BorderSide(color: Color(0xFF0071E3)), ), ), ], ), ), // ── 4.1. Floating "My Location" Button ───────────────────────── if (!navState.isNavigating && _activePickerTarget == null) Positioned( bottom: _activeRoute != null ? 220 : 90, right: 16, child: FloatingActionButton.small( heroTag: 'fab_my_location', backgroundColor: const Color(0xFF0F172A), foregroundColor: const Color(0xFF38BDF8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide( color: Color(0xFF0071E3), width: 1.5), ), onPressed: () { if (_currentGpsPosition != null) { _mapController?.animateCamera( CameraUpdate.newLatLngZoom( _currentGpsPosition!, 15.0), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( duration: Duration(seconds: 2), backgroundColor: Color(0xFF0071E3), content: Text('تم التمركز على موقعك الحالي GPS 📍'), ), ); } else { _initGps(); } }, child: const Icon(Icons.my_location, size: 20), ), ), // ── 5. Tactical Route Preview Card with "Start Navigation" Button ─ if (!navState.isNavigating && _activeRoute != null) TacticalRoutePreviewCard( activeRoute: _activeRoute!, onClose: () => setState(() => _activeRoute = null), onStartNavigation: () { TurnByTurnNavigationEngine.startNavigation( plan: _activeRoute!, simulate: true, controller: _mapController, ); }, ), ], ), ); }, ); } Widget _buildModeButton( String mode, String title, IconData icon, VoidCallback onTap) { final active = _currentTacticalMode == mode; return Expanded( child: GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), decoration: BoxDecoration( color: active ? const Color(0xFF0071E3) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: active ? Colors.white : const Color(0xFF94A3B8)), const SizedBox(width: 4), Flexible( child: Text( title, style: TextStyle( fontSize: 10.5, fontWeight: FontWeight.bold, color: active ? Colors.white : const Color(0xFF94A3B8), ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ], ), ), ), ); } }