import 'package:flutter/material.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../models/angle_unit.dart'; import '../services/military_grid_utils.dart'; import '../services/offline_los_engine.dart'; import '../services/resection_calculator.dart'; import '../services/tactical_api_service.dart'; class TacticalLosSheet extends StatefulWidget { final LatLng observer; final LatLng target; final LatLng? currentGps; final AngleUnit angleUnit; final Function(LatLng newObserver, LatLng newTarget) onUpdateLocations; final ValueChanged? onVisibilityChanged; final ValueChanged? onReportGenerated; final VoidCallback onPickObserverOnMap; final VoidCallback onPickTargetOnMap; final VoidCallback onClose; const TacticalLosSheet({ super.key, required this.observer, required this.target, this.currentGps, required this.angleUnit, required this.onUpdateLocations, this.onVisibilityChanged, this.onReportGenerated, required this.onPickObserverOnMap, required this.onPickTargetOnMap, required this.onClose, }); @override State createState() => _TacticalLosSheetState(); } class _TacticalLosSheetState extends State with SingleTickerProviderStateMixin { late TabController _tabController; // Controllers for Easting / Northing late TextEditingController _obsEastingCtrl; late TextEditingController _obsNorthingCtrl; late TextEditingController _tgtEastingCtrl; late TextEditingController _tgtNorthingCtrl; // Controllers for Lat / Lng late TextEditingController _obsLatCtrl; late TextEditingController _obsLngCtrl; late TextEditingController _tgtLatCtrl; late TextEditingController _tgtLngCtrl; // Optic Heights (ارتفاع الراصد والهدف عن سطح الأرض) double _observerHeightM = 2.0; double _targetHeightM = 2.0; // Calculation Results (Hybrid Server + Sovereign Offline DEM Engine) OfflineLosReport? _losReport; bool _isCalculating = false; int? _inspectedSampleIndex; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); final obsMil = MilitaryGridUtils.fromLatLng(widget.observer); final tgtMil = MilitaryGridUtils.fromLatLng(widget.target); _obsEastingCtrl = TextEditingController(text: obsMil.eastingStr); _obsNorthingCtrl = TextEditingController(text: obsMil.northingStr); _tgtEastingCtrl = TextEditingController(text: tgtMil.eastingStr); _tgtNorthingCtrl = TextEditingController(text: tgtMil.northingStr); _obsLatCtrl = TextEditingController(text: widget.observer.latitude.toStringAsFixed(6)); _obsLngCtrl = TextEditingController(text: widget.observer.longitude.toStringAsFixed(6)); _tgtLatCtrl = TextEditingController(text: widget.target.latitude.toStringAsFixed(6)); _tgtLngCtrl = TextEditingController(text: widget.target.longitude.toStringAsFixed(6)); _losReport = OfflineLosEngine.calculate( observer: widget.observer, target: widget.target, observerHeightM: _observerHeightM, targetHeightM: _targetHeightM, ); _calculateLos(widget.observer, widget.target); } @override void didUpdateWidget(covariant TacticalLosSheet oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.observer != widget.observer || oldWidget.target != widget.target) { final obsMil = MilitaryGridUtils.fromLatLng(widget.observer); final tgtMil = MilitaryGridUtils.fromLatLng(widget.target); _obsEastingCtrl.text = obsMil.eastingStr; _obsNorthingCtrl.text = obsMil.northingStr; _tgtEastingCtrl.text = tgtMil.eastingStr; _tgtNorthingCtrl.text = tgtMil.northingStr; _obsLatCtrl.text = widget.observer.latitude.toStringAsFixed(6); _obsLngCtrl.text = widget.observer.longitude.toStringAsFixed(6); _tgtLatCtrl.text = widget.target.latitude.toStringAsFixed(6); _tgtLngCtrl.text = widget.target.longitude.toStringAsFixed(6); _calculateLos(widget.observer, widget.target); } } @override void dispose() { _tabController.dispose(); _obsEastingCtrl.dispose(); _obsNorthingCtrl.dispose(); _tgtEastingCtrl.dispose(); _tgtNorthingCtrl.dispose(); _obsLatCtrl.dispose(); _obsLngCtrl.dispose(); _tgtLatCtrl.dispose(); _tgtLngCtrl.dispose(); super.dispose(); } Future _calculateLos(LatLng obs, LatLng tgt) async { setState(() => _isCalculating = true); try { final report = await TacticalApiService.calculateHybridLineOfSight( observer: obs, target: tgt, observerHeight: _observerHeightM, targetHeight: _targetHeightM, ); if (mounted) { setState(() { _losReport = report; _isCalculating = false; }); } widget.onVisibilityChanged?.call(report.isDirectlyVisible); widget.onReportGenerated?.call(report); } catch (e) { debugPrint('LOS calculation error: $e'); if (mounted) { setState(() => _isCalculating = false); } } } void _applyManualCoordinates() { LatLng newObs = widget.observer; LatLng newTgt = widget.target; if (_tabController.index == 0) { // UTM Easting/Northing final obsE = double.tryParse(_obsEastingCtrl.text) ?? 0; final obsN = double.tryParse(_obsNorthingCtrl.text) ?? 0; final tgtE = double.tryParse(_tgtEastingCtrl.text) ?? 0; final tgtN = double.tryParse(_tgtNorthingCtrl.text) ?? 0; if (obsE > 100000 && obsN > 1000000) { newObs = MilitaryGridUtils.toLatLng(easting: obsE, northing: obsN); } if (tgtE > 100000 && tgtN > 1000000) { newTgt = MilitaryGridUtils.toLatLng(easting: tgtE, northing: tgtN); } } else { // Decimal Lat/Lng final obsLat = double.tryParse(_obsLatCtrl.text); final obsLng = double.tryParse(_obsLngCtrl.text); final tgtLat = double.tryParse(_tgtLatCtrl.text); final tgtLng = double.tryParse(_tgtLngCtrl.text); if (obsLat != null && obsLng != null) newObs = LatLng(obsLat, obsLng); if (tgtLat != null && tgtLng != null) newTgt = LatLng(tgtLat, tgtLng); } widget.onUpdateLocations(newObs, newTgt); _calculateLos(newObs, newTgt); } @override Widget build(BuildContext context) { final isVisible = _losReport?.isDirectlyVisible ?? true; final distanceMeters = _losReport?.totalDistanceM ?? ResectionCalculator.haversineDistance( widget.observer.latitude, widget.observer.longitude, widget.target.latitude, widget.target.longitude, ); final azimuthDeg = _losReport?.azimuthDeg ?? ResectionCalculator.calculateBearing( widget.observer.latitude, widget.observer.longitude, widget.target.latitude, widget.target.longitude, ); final maxElev = _losReport?.maxElevationM ?? 950.0; return Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.88, ), padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Color(0xFF090E1A), borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 2.5)), boxShadow: [BoxShadow(color: Colors.black87, blurRadius: 30)], ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle bar Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white24, borderRadius: BorderRadius.circular(2), ), ), ), // Header Bar Row( children: [ Expanded( child: Row( children: [ Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: isVisible ? const Color(0x3322C55E) : const Color(0x33EF4444), borderRadius: BorderRadius.circular(8), border: Border.all( color: isVisible ? const Color(0xFF22C55E) : const Color(0xFFEF4444), ), ), child: Icon( isVisible ? Icons.visibility : Icons.visibility_off, color: isVisible ? const Color(0xFF4ADE80) : const Color(0xFFF87171), size: 22, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'منظومة تحليل تبادل الرؤية العسكري (LOS)', style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), Row( children: [ Flexible( child: Text( isVisible ? '🟢 خط الرؤية مكشوف وسالك (Clear)' : '🔴 خط الرؤية محجوب بتضاريس (Blocked)', style: TextStyle( color: isVisible ? const Color(0xFF4ADE80) : const Color(0xFFF87171), fontSize: 10.5, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), if (_isCalculating) ...[ const SizedBox(width: 6), const SizedBox( width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF38BDF8)), ), ], ], ), ], ), ), ], ), ), IconButton( onPressed: widget.onClose, icon: const Icon(Icons.close, color: Colors.white70, size: 20), ), ], ), const SizedBox(height: 10), // Coordinates Tabs: [شرقيات / شماليات UTM-36N] vs [درجات Lat/Lng] Container( height: 32, decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(10), ), child: TabBar( controller: _tabController, indicator: BoxDecoration( color: const Color(0xFF0071E3), borderRadius: BorderRadius.circular(10), ), labelColor: Colors.white, unselectedLabelColor: const Color(0xFF94A3B8), labelStyle: const TextStyle(fontSize: 10.5, fontWeight: FontWeight.bold), tabs: const [ Tab(text: 'إحداثيات عسكرية (شرقيات / شماليات UTM)'), Tab(text: 'درجات جغرافية (WGS84 Lat/Lng)'), ], ), ), const SizedBox(height: 10), // Coordinates Input Fields Table SizedBox( height: 95, child: TabBarView( controller: _tabController, children: [ // 1. Military UTM Easting / Northing Table Row( children: [ // Observer Card Expanded( child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0x3338BDF8)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('🔴 الراصد (Observer)', style: TextStyle(color: Color(0xFF38BDF8), fontSize: 10.5, fontWeight: FontWeight.bold)), InkWell( onTap: widget.onPickObserverOnMap, child: const Icon(Icons.touch_app, size: 15, color: Color(0xFF38BDF8)), ), ], ), const SizedBox(height: 2), Row( children: [ Expanded( child: TextField( controller: _obsEastingCtrl, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'شرقيات E', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), const SizedBox(width: 4), Expanded( child: TextField( controller: _obsNorthingCtrl, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'شماليات N', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), ], ), ], ), ), ), const SizedBox(width: 8), // Target Card Expanded( child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0x33F59E0B)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('🎯 الهدف (Target)', style: TextStyle(color: Color(0xFFF59E0B), fontSize: 10.5, fontWeight: FontWeight.bold)), InkWell( onTap: widget.onPickTargetOnMap, child: const Icon(Icons.touch_app, size: 15, color: Color(0xFFF59E0B)), ), ], ), const SizedBox(height: 2), Row( children: [ Expanded( child: TextField( controller: _tgtEastingCtrl, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'شرقيات E', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), const SizedBox(width: 4), Expanded( child: TextField( controller: _tgtNorthingCtrl, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'شماليات N', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), ], ), ], ), ), ), ], ), // 2. Lat / Lng Table Row( children: [ // Observer Card Expanded( child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0x3338BDF8)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('🔴 الراصد (Lat/Lng)', style: TextStyle(color: Color(0xFF38BDF8), fontSize: 10.5, fontWeight: FontWeight.bold)), const SizedBox(height: 2), Row( children: [ Expanded( child: TextField( controller: _obsLatCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'خط العرض Lat', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), const SizedBox(width: 4), Expanded( child: TextField( controller: _obsLngCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'خط الطول Lng', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), ], ), ], ), ), ), const SizedBox(width: 8), // Target Card Expanded( child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0x33F59E0B)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('🎯 الهدف (Lat/Lng)', style: TextStyle(color: Color(0xFFF59E0B), fontSize: 10.5, fontWeight: FontWeight.bold)), const SizedBox(height: 2), Row( children: [ Expanded( child: TextField( controller: _tgtLatCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'خط العرض Lat', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), const SizedBox(width: 4), Expanded( child: TextField( controller: _tgtLngCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 11), decoration: const InputDecoration( labelText: 'خط الطول Lng', labelStyle: TextStyle(fontSize: 9, color: Color(0xFF94A3B8)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), ), onSubmitted: (_) => _applyManualCoordinates(), ), ), ], ), ], ), ), ), ], ), ], ), ), const SizedBox(height: 8), // Optic Heights Adjustment (ارتفاع البصريات وسواري الرصد) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.white10), ), child: Row( children: [ const Icon(Icons.height, size: 14, color: Color(0xFF38BDF8)), const SizedBox(width: 6), const Text('ارتفاع الرصد والهدف:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)), const Spacer(), _buildHeightChip('راصد +${_observerHeightM.round()}م', _observerHeightM, (h) { setState(() => _observerHeightM = h); _calculateLos(widget.observer, widget.target); }), const SizedBox(width: 6), _buildHeightChip('هدف +${_targetHeightM.round()}م', _targetHeightM, (h) { setState(() => _targetHeightM = h); _calculateLos(widget.observer, widget.target); }), ], ), ), const SizedBox(height: 10), // ── Tactical Elevation Profile & Intervisibility Cross-Section Chart ── if (_losReport != null && _losReport!.profile.length > 1) ...[ _LosProfileChart( report: _losReport!, visible: isVisible, angleUnit: widget.angleUnit, selectedIndex: _inspectedSampleIndex, onHoverSample: (idx) => setState(() => _inspectedSampleIndex = idx), ), if (_inspectedSampleIndex != null && _inspectedSampleIndex! < _losReport!.profile.length) ...[ const SizedBox(height: 6), _buildSampleInspectorCard(_losReport!.profile[_inspectedSampleIndex!]), ], ], const SizedBox(height: 10), // Tactical Metrics Data Summary Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0x330071E3)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildMetricItem( 'المسافة المباشرة', distanceMeters >= 1000 ? '${(distanceMeters / 1000.0).toStringAsFixed(2)} كم' : '${distanceMeters.toStringAsFixed(0)} م', Icons.straighten, ), _buildMetricItem( 'السمت التكتيكي', AngleFormatter.format(azimuthDeg, widget.angleUnit, includeLabel: true), Icons.explore, ), _buildMetricItem( 'أعلى تضاريس', '${maxElev.round()} م', Icons.terrain, ), ], ), ), if (_losReport != null) ...[ const SizedBox(height: 8), // Ground & air / LOS geometry detail row Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(12), border: Border.all( color: isVisible ? const Color(0x3322C55E) : const Color(0x44EF4444), ), ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildMetricItem( 'ارتفاع الراصد الكلي', '${_losReport!.observerTotalElevM.round()} م', Icons.height, ), _buildMetricItem( 'ارتفاع الهدف الكلي', '${_losReport!.targetTotalElevM.round()} م', Icons.track_changes, ), _buildMetricItem( 'السمت (NATO / Soviet)', '${_losReport!.azimuthMilsNato.round()} ₥ / ${_losReport!.azimuthMilsSoviet.round()} ₥', Icons.explore, ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildMetricItem( 'زاوية ارتفاع الرصد', '${_losReport!.verticalAngleDeg.toStringAsFixed(2)}° (${_losReport!.verticalAngleMils.toStringAsFixed(1)} ₥)', Icons.vertical_align_top, ), _buildMetricItem( 'منطقة ميتة', '${_losReport!.deadGroundPercent}%', Icons.block, ), _buildMetricItem( 'أدنى ارتفاع', '${_losReport!.minElevationM.round()} م', Icons.arrow_downward, ), ], ), if (_losReport!.highestObstacle != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0x22EF4444), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0x55EF4444)), ), child: Row( children: [ const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF87171)), const SizedBox(width: 8), Expanded( child: Text( 'نقطة الحجب القصوى: ارتفاع ${_losReport!.highestObstacle!.elevationM.round()}م على بعد ${_losReport!.highestObstacle!.distanceM.round()}م (اختراق شعاع الرؤية بمقدار +${_losReport!.highestObstacle!.excessM.toStringAsFixed(1)}م)', style: const TextStyle( color: Color(0xFFF87171), fontSize: 10.5, fontWeight: FontWeight.w600, ), ), ), ], ), ), ], ], ), ), ], const SizedBox(height: 12), // Action Buttons Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _applyManualCoordinates, icon: const Icon(Icons.refresh, size: 16), label: const Text('تطبيق وإعادة حساب الرؤية', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0071E3), foregroundColor: Colors.white, padding: const EdgeInsets.all(16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), if (widget.currentGps != null) ...[ const SizedBox(width: 8), OutlinedButton.icon( onPressed: () { widget.onUpdateLocations(widget.currentGps!, widget.target); }, icon: const Icon(Icons.my_location, size: 16), label: const Text('موقعي الحالي', style: TextStyle(fontSize: 11)), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF38BDF8), side: const BorderSide(color: Color(0xFF0071E3)), padding: const EdgeInsets.all(16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ], ], ), ], ), ), ); } Widget _buildHeightChip(String label, double currentHeight, Function(double) onSelected) { return PopupMenuButton( initialValue: currentHeight, onSelected: onSelected, color: const Color(0xFF1E293B), itemBuilder: (ctx) => [ const PopupMenuItem(value: 2.0, child: Text('رصد فردي (2 متر)', style: TextStyle(color: Colors.white, fontSize: 11))), const PopupMenuItem(value: 5.0, child: Text('برج مركبة / سارية (5 متر)', style: TextStyle(color: Colors.white, fontSize: 11))), const PopupMenuItem(value: 10.0, child: Text('برج مراقبة ميداني (10 متر)', style: TextStyle(color: Colors.white, fontSize: 11))), const PopupMenuItem(value: 25.0, child: Text('منطاد استطلاع / درون (25 متر)', style: TextStyle(color: Colors.white, fontSize: 11))), ], child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF0071E3).withValues(alpha: 0.3), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF0071E3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(label, style: const TextStyle(color: Color(0xFF38BDF8), fontSize: 10, fontWeight: FontWeight.bold)), const SizedBox(width: 3), const Icon(Icons.arrow_drop_down, size: 14, color: Color(0xFF38BDF8)), ], ), ), ); } Widget _buildSampleInspectorCard(OfflineLosSample s) { final mil = MilitaryGridUtils.fromLatLng(LatLng(s.lat, s.lng)); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF1E293B), borderRadius: BorderRadius.circular(8), border: Border.all(color: s.isBlocked ? const Color(0xFFEF4444) : const Color(0xFF22C55E)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('محطة عند: ${s.distanceM.round()}م', style: const TextStyle(color: Colors.white70, fontSize: 10)), Text('تضاريس: ${s.groundElevationM.round()}م', style: const TextStyle(color: Color(0xFF38BDF8), fontSize: 10, fontWeight: FontWeight.bold)), Text('شعاع: ${s.rayHeightM.round()}م', style: const TextStyle(color: Color(0xFFF59E0B), fontSize: 10)), Text('خلوص: ${s.clearanceM >= 0 ? "+${s.clearanceM.toStringAsFixed(1)}" : s.clearanceM.toStringAsFixed(1)}م', style: TextStyle(color: s.clearanceM >= 0 ? const Color(0xFF4ADE80) : const Color(0xFFF87171), fontSize: 10, fontWeight: FontWeight.bold)), Text(mil.shortGrid, style: const TextStyle(color: Colors.white54, fontSize: 9)), ], ), ); } Widget _buildMetricItem(String label, String val, IconData icon) { return Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: const Color(0xFF94A3B8)), const SizedBox(width: 4), Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)), ], ), const SizedBox(height: 3), Text( val, style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold), ), ], ); } } /// Tactical terrain elevation profile chart with intervisibility ray & obstacle callout. class _LosProfileChart extends StatelessWidget { final OfflineLosReport report; final bool visible; final AngleUnit angleUnit; final int? selectedIndex; final Function(int?) onHoverSample; const _LosProfileChart({ required this.report, required this.visible, required this.angleUnit, this.selectedIndex, required this.onHoverSample, }); @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(6, 8, 6, 6), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0x330071E3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: visible ? const Color(0xFF22C55E) : const Color(0xFFEF4444), shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( visible ? 'مقطع التضاريس وشعاع الرؤية المباشر' : 'مقطع التضاريس مع العائق الجبلي', style: TextStyle( color: visible ? const Color(0xFF4ADE80) : const Color(0xFFF87171), fontSize: 11, fontWeight: FontWeight.bold, ), ), const Spacer(), Text( '${(report.totalDistanceM / 1000).toStringAsFixed(1)} كم • ${report.azimuthDeg.toStringAsFixed(1)}°', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10, fontWeight: FontWeight.w600), ), ], ), ), const SizedBox(height: 6), GestureDetector( onHorizontalDragUpdate: (details) { final box = context.findRenderObject() as RenderBox?; if (box != null) { final localX = details.localPosition.dx; final width = box.size.width; final fraction = (localX / width).clamp(0.0, 1.0); final idx = (fraction * (report.profile.length - 1)).round(); onHoverSample(idx); } }, onTapDown: (details) { final box = context.findRenderObject() as RenderBox?; if (box != null) { final localX = details.localPosition.dx; final width = box.size.width; final fraction = (localX / width).clamp(0.0, 1.0); final idx = (fraction * (report.profile.length - 1)).round(); onHoverSample(idx); } }, child: CustomPaint( painter: _LosProfilePainter( report: report, clear: visible, selectedIndex: selectedIndex, ), size: const Size(double.infinity, 115), ), ), ], ), ); } } /// Paints the terrain (ground), curvature sagitta, sight ray, obstacle callout, and dead ground. class _LosProfilePainter extends CustomPainter { final OfflineLosReport report; final bool clear; final int? selectedIndex; _LosProfilePainter({ required this.report, required this.clear, this.selectedIndex, }); @override void paint(Canvas canvas, Size size) { final w = size.width; final h = size.height; const pad = 8.0; final samples = report.profile; if (samples.isEmpty) return; double minE = double.infinity; double maxE = -double.infinity; for (final s in samples) { if (s.groundElevationM < minE) minE = s.groundElevationM; if (s.groundElevationM > maxE) maxE = s.groundElevationM; if (s.rayHeightM > maxE) maxE = s.rayHeightM; if (s.rayHeightM < minE) minE = s.rayHeightM; } if ((maxE - minE).abs() < 1.0) { maxE = minE + 10.0; } minE -= 15.0; // Margin below maxE += 20.0; // Margin above double x(num j) => pad + (w - 2 * pad) * (j.toDouble() / (samples.length - 1)); double y(num e) => h - pad - (h - 2 * pad) * ((e.toDouble() - minE) / (maxE - minE)); // 1. Draw Grid Lines & Elevation scale with Text Labels final gridPaint = Paint() ..color = Colors.white12 ..strokeWidth = 0.8; void drawGridLevel(double elev) { final yPos = y(elev); canvas.drawLine(Offset(pad, yPos), Offset(w - pad, yPos), gridPaint); final textPainter = TextPainter( text: TextSpan( text: '${elev.round()}م', style: const TextStyle(color: Colors.white38, fontSize: 8.5), ), textDirection: TextDirection.rtl, )..layout(); textPainter.paint(canvas, Offset(pad + 2, yPos - 11)); } final midE = (minE + maxE) / 2; drawGridLevel(minE + 15); drawGridLevel(midE); drawGridLevel(maxE - 15); // 2. Ground Profile Fill & Stroke final groundPath = Path()..moveTo(x(0), y(samples.first.groundElevationM)); for (int i = 1; i < samples.length; i++) { groundPath.lineTo(x(i), y(samples[i].groundElevationM)); } groundPath.lineTo(x(samples.length - 1), h); groundPath.lineTo(x(0), h); groundPath.close(); final groundGradient = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ const Color(0xFF334155), const Color(0xFF0F172A), ], ); final groundPaint = Paint() ..style = PaintingStyle.fill ..shader = groundGradient.createShader(Rect.fromLTWH(0, 0, w, h)); canvas.drawPath(groundPath, groundPaint); final ridgePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..color = const Color(0xFF38BDF8); final ridgePath = Path()..moveTo(x(0), y(samples.first.groundElevationM)); for (int i = 1; i < samples.length; i++) { ridgePath.lineTo(x(i), y(samples[i].groundElevationM)); } canvas.drawPath(ridgePath, ridgePaint); // 3. Highlight Dead Ground (المنطقة الميتة) for (int i = 0; i < samples.length - 1; i++) { if (!samples[i].isVisible) { final dgRect = Rect.fromLTRB(x(i), y(samples[i].groundElevationM), x(i + 1), h - pad); canvas.drawRect(dgRect, Paint()..color = const Color(0x33EF4444)); } } // 4. Line of Sight Straight Ray final rayPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 2.4 ..color = clear ? const Color(0xFF22C55E) : const Color(0xFFEF4444); final rayPath = Path()..moveTo(x(0), y(samples.first.rayHeightM)); for (int i = 1; i < samples.length; i++) { rayPath.lineTo(x(i), y(samples[i].rayHeightM)); } canvas.drawPath(rayPath, rayPaint); // 5. Highest Obstruction Marker & Deficit Line final obs = report.highestObstacle; if (obs != null && !clear) { final dobj = obs.distanceM; final maxD = report.totalDistanceM > 0 ? report.totalDistanceM : 1.0; final fx = pad + (w - 2 * pad) * (dobj / maxD); final fyGround = y(obs.elevationM); final fyRay = y(obs.sightRayElevationM); // Deficit penetration vertical line final penPaint = Paint() ..color = const Color(0xFFEF4444) ..strokeWidth = 2.0; canvas.drawLine(Offset(fx, fyGround), Offset(fx, fyRay), penPaint); // Obstacle Top Triangle final markerPaint = Paint() ..style = PaintingStyle.fill ..color = const Color(0xFFEF4444); canvas.drawPath( Path() ..moveTo(fx - 5, fyGround + 7) ..lineTo(fx, fyGround - 6) ..lineTo(fx + 5, fyGround + 7) ..close(), markerPaint, ); } // 6. Observer & Target Optic Endpoints void drawEndpoint(double dx, double dyGround, double dyRay, Color color, String label, double groundElev) { // Mast / Optic height line canvas.drawLine( Offset(dx, dyGround), Offset(dx, dyRay), Paint()..color = color..strokeWidth = 2.0, ); // Top Optic Circle canvas.drawCircle(Offset(dx, dyRay), 4.0, Paint()..color = color); canvas.drawCircle(Offset(dx, dyRay), 1.5, Paint()..color = const Color(0xFF0F172A)); // Label final tp = TextPainter( text: TextSpan( text: '$label\n${groundElev.round()}م', style: TextStyle(color: color, fontSize: 9.0, fontWeight: FontWeight.bold), ), textAlign: TextAlign.center, textDirection: TextDirection.rtl, )..layout(); tp.paint(canvas, Offset(dx - tp.width / 2, dyGround + 4)); } drawEndpoint(x(0), y(samples.first.groundElevationM), y(samples.first.rayHeightM), const Color(0xFF38BDF8), 'الراصد', samples.first.groundElevationM); drawEndpoint(x(samples.length - 1), y(samples.last.groundElevationM), y(samples.last.rayHeightM), const Color(0xFFF59E0B), 'الهدف', samples.last.groundElevationM); // 7. Interactive Scrubber Cursor if (selectedIndex != null && selectedIndex! < samples.length) { final cur = samples[selectedIndex!]; final sx = x(selectedIndex!); final syGround = y(cur.groundElevationM); final syRay = y(cur.rayHeightM); final curPaint = Paint()..color = Colors.white..strokeWidth = 1.2; canvas.drawLine(Offset(sx, pad), Offset(sx, h - pad), curPaint); canvas.drawCircle(Offset(sx, syGround), 3.5, Paint()..color = const Color(0xFF38BDF8)); canvas.drawCircle(Offset(sx, syRay), 3.5, Paint()..color = const Color(0xFFF59E0B)); } } @override bool shouldRepaint(covariant _LosProfilePainter oldDelegate) => oldDelegate.report != report || oldDelegate.clear != clear || oldDelegate.selectedIndex != selectedIndex; }