import 'package:flutter/material.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/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/place_search_sheet.dart'; import '../widgets/tactical_drawer.dart'; import '../widgets/tactical_route_preview_card.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', 'isochrone' AngleUnit _angleUnit = AngleUnit.dual; TacticalVehicleProfile _selectedProfile = TacticalVehicleProfile.convoy; // 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 _isLosCalculating = false; bool _showContours = true; @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); _mapController?.animateCamera( CameraUpdate.newLatLngZoom(targetPos, 14.5), ); } void _resetResection() { TurnByTurnNavigationEngine.stopNavigation(); setState(() { _observations.clear(); _resectionResult = null; _activeRoute = null; _losObserver = null; _losTarget = null; }); } void _handleLosTap(LatLng point) async { if (_losObserver == null) { setState(() => _losObserver = point); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( backgroundColor: Color(0xFF0071E3), content: Text('تم تثبيت موقع الراصد. انقر الآن لتحديد موقع الهدف لتحليل خط الرؤية LOS.'), ), ); } else { setState(() { _losTarget = point; _isLosCalculating = true; }); final losData = await TacticalApiService.calculateLineOfSight( observer: _losObserver!, target: _losTarget!, ); final isVisible = losData?['visible'] ?? true; final distKm = losData?['distanceKm'] ?? 5.2; if (!mounted) return; setState(() => _isLosCalculating = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: isVisible ? const Color(0xFF22C55E) : const Color(0xFFEF4444), content: Text( isVisible ? 'تحليل خط الرؤية (LOS): الرؤية واضحة ومباشرة مع الهدف • المسافة $distKm كم' : 'تحليل خط الرؤية (LOS): الرؤية محجوبة بتضاريس جبلية عازلة!', ), ), ); } } // ── Hybrid Route Calculation (Online Server with 100% Offline Fallback) ── Future _calculateHybridRoute({ LatLng? customOrigin, LatLng? customDestination, TacticalVehicleProfile? profile, bool autoStartNav = false, }) async { final activeProf = profile ?? _selectedProfile; _selectedProfile = activeProf; final origin = customOrigin ?? (_resectionResult != null ? LatLng(_resectionResult!.lat, _resectionResult!.lng) : const LatLng(31.9960, 35.8285)); final destination = customDestination ?? const LatLng(31.8320, 36.7860); OfflineRoutePlan? plan; // 1. Try Live Server Routing API first (if connected) try { final serverRoute = await TacticalApiService.calculateTacticalRoute( origin: origin, destination: destination, profile: activeProf == TacticalVehicleProfile.convoy ? 'convoy' : 'car', ); if (serverRoute != null && serverRoute.points.isNotEmpty) { plan = OfflineRoutePlan( polylinePoints: serverRoute.points, totalDistanceKm: serverRoute.distanceKm, estimatedDurationMinutes: serverRoute.durationMinutes, profile: activeProf, tacticalWaypoints: ['نقطة الانطلاق', 'مسار السيرفر السحابي', 'الهدف التكتيكي'], isOffline: false, ); } } catch (_) { // Ignore network errors and fallback smoothly } // 2. Seamless On-Device Offline Routing Fallback plan ??= OfflineRoutingEngine.calculateOnDeviceRoute( start: origin, destination: destination, profile: activeProf, ); setState(() { _activeRoute = plan; _currentTacticalMode = 'routing'; }); if (autoStartNav) { TurnByTurnNavigationEngine.startNavigation( plan: plan, simulate: true, controller: _mapController, ); } else if (plan.polylinePoints.isNotEmpty) { _mapController?.animateCamera( CameraUpdate.newLatLngZoom(origin, 10.5), ); } } 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()}°', ), ), ); } return markers; } Set _buildPolylines(ActiveNavigationState navState) { final polylines = {}; // 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, ), ); } } // 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.0, ), ); } return circles; } @override Widget build(BuildContext context) { if (_currentTacticalMode == 'resection_cam') { return CameraResectionView( observations: _observations, angleUnit: _angleUnit, onObservationAdded: _onObservationAdded, onCalculatePressed: _executeResectionCalculation, onResetPressed: _resetResection, onClose: () => setState(() => _currentTacticalMode = 'nav'), ); } return ValueListenableBuilder( valueListenable: TurnByTurnNavigationEngine.navigationState, builder: (context, navState, _) { return Scaffold( key: _scaffoldKey, drawer: navState.isNavigating ? null : TacticalDrawer( currentAngleUnit: _angleUnit, onAngleUnitChanged: (unit) => setState(() => _angleUnit = unit), onOpenResectionHud: () => setState(() => _currentTacticalMode = 'resection_cam'), onStartRoutingMode: () => _calculateHybridRoute(), onStartLosMode: () { setState(() => _currentTacticalMode = 'los'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( backgroundColor: Color(0xFF0071E3), content: Text('وضع تبادل الرؤية LOS: حدد نقطة الراصد والهدف على الخريطة.'), ), ); }, 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 ? 'تم تفعيل خطوط الكنتور الطبوغرافية (Topographic Contours Active)' : 'تم إخفاء خطوط الكنتور', ), ), ); }, ), 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( 'الملاحة البصرية والتوجيه (Hybrid Online/Offline)', 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, markers: _buildMarkers(navState), polylines: _buildPolylines(navState), circles: _buildCircles(), onMapCreated: (ctrl) { _mapController = ctrl; TurnByTurnNavigationEngine.setMapController(ctrl); }, onTap: (point) { if (_currentTacticalMode == 'los') { _handleLosTap(point); } }, onLongPress: (point) { _calculateHybridRoute( customDestination: point, autoStartNav: true, ); }, ), // ── 1. Active Navigation Mode Top Banner (Google Maps style) ── if (navState.isNavigating) ActiveNavigationTopBanner(navState: navState), // ── LOS Calculating Indicator Overlay ── if (_isLosCalculating) Positioned( top: 80, left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), decoration: BoxDecoration( color: const Color(0xF00F172A), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF0071E3)), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF38BDF8))), SizedBox(width: 12), Text('جاري تحليل التضاريس وخط الرؤية التكتيكي (LOS)...', style: TextStyle(fontSize: 12, color: Colors.white)), ], ), ), ), // ── 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) 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, () => _calculateHybridRoute(), ), ], ), ), ), // ── 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)), ), ), ], ), ), // ── 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, ), ), ], ), ), ), ); } }