import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/optical_rangefinder_controller.dart'; import '../models/angle_unit.dart'; import '../services/camera_sensor_calibration_service.dart'; /// ============================================================================ /// [OpticalRangefinderHud] - واجهة قياس المسافة البصري وشبكة التصويب التكتيكية /// ============================================================================ class OpticalRangefinderHud extends StatelessWidget { final AngleUnit angleUnit; final VoidCallback onClose; final Function(double distanceMeters, double headingDeg, double pitchDeg)? onRangeLocked; const OpticalRangefinderHud({ super.key, required this.angleUnit, required this.onClose, this.onRangeLocked, }); @override Widget build(BuildContext context) { final OpticalRangefinderController ctrl = Get.put(OpticalRangefinderController()); return Scaffold( backgroundColor: Colors.black, body: LayoutBuilder( builder: (context, constraints) { ctrl.updateScreenDimensions(Size(constraints.maxWidth, constraints.maxHeight)); return Stack( fit: StackFit.expand, children: [ // ── 1. Camera Live Stream with Pinch-to-Zoom ───────────── Obx(() { if (!ctrl.isCameraReady.value || ctrl.cameraController == null) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Color(0xFF00F0FF)), SizedBox(height: 16), Text( 'جاري فحص وتهيئة مستشعر الكاميرا البصري...', style: TextStyle(color: Colors.white70, fontSize: 13), ), ], ), ); } double baseScale = 1.0; return GestureDetector( onScaleStart: (_) { baseScale = ctrl.currentZoom.value; }, onScaleUpdate: (details) { final newZoom = baseScale * details.scale; ctrl.setZoom(newZoom); }, child: Center( child: CameraPreview(ctrl.cameraController!), ), ); }), // ── 2. Tactical Reticule Overlay (شبكة التصويب الميدانية) ─ Obx(() { return _buildTacticalReticule(context, ctrl, constraints.maxWidth, constraints.maxHeight); }), // ── 3. Top Operations Bar ──────────────────────────────── Positioned( top: 44, left: 16, right: 16, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Close Button Container( decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white24), ), child: IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: onClose, tooltip: 'إغلاق والعودة للخريطة', ), ), // Title Badge Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF00F0FF).withAlpha(120)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.center_focus_strong, color: Color(0xFF00F0FF), size: 16), SizedBox(width: 8), Text( 'قياس المسافة البصري (بدون GPS)', style: TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold), ), ], ), ), // Sensor Inspector Button Container( decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white24), ), child: IconButton( icon: const Icon(Icons.sensors, color: Color(0xFF38BDF8)), onPressed: () => _openSensorInspectorDialog(context, ctrl), tooltip: 'فحص ومعايرة مستشعر الكاميرا', ), ), ], ), ), // ── 4. Main Telemetry HUD Card ─────────────────────────── Positioned( top: 104, left: 20, right: 20, child: Obx(() { return _buildMainTelemetryCard(ctrl); }), ), // ── 5. Right Zoom Slider Widget ────────────────────────── Positioned( right: 14, top: 230, bottom: 210, child: Obx(() { return _buildVerticalZoomBar(ctrl); }), ), // ── 6. Bottom Target Selector & Actions ────────────────── Positioned( bottom: 20, left: 14, right: 14, child: Obx(() { return _buildBottomControls(context, ctrl); }), ), ], ); }, ), ); } // ── Reticule Drawing ─────────────────────────────────────────────────────── Widget _buildTacticalReticule( BuildContext context, OpticalRangefinderController ctrl, double width, double height, ) { final centerY = height / 2.0; final centerX = width / 2.0; final bracketH = (ctrl.reticuleHeightRatio.value * height).clamp(20.0, height * 0.75); final halfH = bracketH / 2.0; return Stack( children: [ // Center crosshair with mil marks CustomPaint( size: Size(width, height), painter: _MilReticulePainter( centerX: centerX, centerY: centerY, bracketHalfHeight: halfH, bracketWidth: 120.0, rollDeg: ctrl.rollDeg.value, ), ), // Interactive Drag Handles for Brackets Positioned( top: centerY - halfH - 24, left: centerX - 60, width: 120, height: 48, child: GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragUpdate: (details) { final newHalf = halfH - details.delta.dy; ctrl.setReticuleHeightRatio((newHalf * 2.0) / height); }, child: Container( alignment: Alignment.center, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF00F0FF).withAlpha(200), borderRadius: BorderRadius.circular(6), ), child: const Text('قمة الهدف ▲', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)), ), ), ), ), Positioned( top: centerY + halfH - 24, left: centerX - 60, width: 120, height: 48, child: GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragUpdate: (details) { final newHalf = halfH + details.delta.dy; ctrl.setReticuleHeightRatio((newHalf * 2.0) / height); }, child: Container( alignment: Alignment.center, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF00F0FF).withAlpha(200), borderRadius: BorderRadius.circular(6), ), child: const Text('قاعدة الهدف ▼', style: TextStyle(color: Colors.black, fontSize: 10, fontWeight: FontWeight.bold)), ), ), ), ), ], ); } // ── Telemetry Card ───────────────────────────────────────────────────────── Widget _buildMainTelemetryCard(OpticalRangefinderController ctrl) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(235), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFF00F0FF).withAlpha(150), width: 1.2), boxShadow: const [ BoxShadow(color: Color(0x66000000), blurRadius: 16), BoxShadow(color: Color(0x3300F0FF), blurRadius: 8), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Distance Display Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ const Text( 'المسافة البصرية:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontWeight: FontWeight.bold), ), Text( ctrl.formatDistance(), style: const TextStyle( color: Color(0xFF00F0FF), fontSize: 22, fontWeight: FontWeight.w900, letterSpacing: 0.5, ), ), ], ), const SizedBox(height: 8), const Divider(color: Colors.white12, height: 1), const SizedBox(height: 8), // Heading & Pitch Row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildMetricChip( icon: Icons.navigation, label: 'الاتجاه:', value: ctrl.formatHeading(angleUnit), color: const Color(0xFF38BDF8), ), _buildMetricChip( icon: Icons.height, label: 'الميل:', value: '${ctrl.pitchDeg.value.toStringAsFixed(1)}°', color: const Color(0xFFFBBF24), ), _buildMetricChip( icon: Icons.zoom_in, label: 'التقريب:', value: '${ctrl.currentZoom.value.toStringAsFixed(1)}x', color: const Color(0xFF4ADE80), ), ], ), ], ), ); } Widget _buildMetricChip({ required IconData icon, required String label, required String value, required Color color, }) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 4), Text(label, style: const TextStyle(color: Color(0xFF64748B), fontSize: 10.5)), const SizedBox(width: 4), Text(value, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)), ], ); } // ── Vertical Zoom Bar ────────────────────────────────────────────────────── Widget _buildVerticalZoomBar(OpticalRangefinderController ctrl) { return Container( width: 42, padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(220), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white24), ), child: Column( children: [ const Text('10x', style: TextStyle(color: Colors.white54, fontSize: 9)), Expanded( child: RotatedBox( quarterTurns: 3, child: SliderTheme( data: SliderThemeData( trackHeight: 3, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7), activeTrackColor: const Color(0xFF00F0FF), inactiveTrackColor: Colors.white24, thumbColor: Colors.white, ), child: Slider( value: ctrl.currentZoom.value, min: ctrl.minZoom.value, max: ctrl.maxZoom.value, onChanged: (val) => ctrl.setZoom(val), ), ), ), ), const Text('1x', style: TextStyle(color: Colors.white54, fontSize: 9)), ], ), ); } // ── Bottom Controls & Target Selector ────────────────────────────────────── Widget _buildBottomControls(BuildContext context, OpticalRangefinderController ctrl) { final currentTarget = ctrl.selectedTarget.value; final isCustom = ctrl.isCustomTarget.value; final heightText = isCustom ? '${ctrl.customHeight.value.toStringAsFixed(1)} م' : '${currentTarget.heightMeters.toStringAsFixed(1)} م'; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF090E17).withAlpha(240), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Target Selector Header Row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: InkWell( onTap: () => _openTargetPickerSheet(context, ctrl), borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF020617), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF0071E3).withAlpha(150)), ), child: Row( children: [ const Icon(Icons.tune, color: Color(0xFF38BDF8), size: 16), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('الهدف المرجعي المعتمد:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)), Text( isCustom ? 'ارتفاع مخصص ($heightText)' : '${currentTarget.titleAr} ($heightText)', style: const TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ], ), ), const Icon(Icons.arrow_drop_down, color: Colors.white70), ], ), ), ), ), const SizedBox(width: 10), // Quick Lock Button ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0071E3), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), onPressed: () { if (onRangeLocked != null) { onRangeLocked!( ctrl.calculatedDistanceMeters.value, ctrl.headingDeg.value, ctrl.pitchDeg.value, ); } Get.snackbar( '🎯 تم تثبيت المدى والاتجاه', 'المسافة: ${ctrl.formatDistance()} • ${ctrl.formatHeading(angleUnit)}', backgroundColor: const Color(0xFF0F172A), colorText: Colors.white, snackPosition: SnackPosition.TOP, duration: const Duration(seconds: 3), ); }, icon: const Icon(Icons.lock, size: 14), label: const Text('تثبيت المدى', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)), ), ], ), const SizedBox(height: 10), // Quick Presets Row SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[0]), // Infantry const SizedBox(width: 6), _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[1]), // Light Vehicle const SizedBox(width: 6), _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[3]), // Tank const SizedBox(width: 6), _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[7]), // Minaret const SizedBox(width: 6), _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[8]), // Grand Minaret const SizedBox(width: 6), _buildQuickPresetChip(ctrl, CameraSensorCalibrationService.standardTargets[4]), // Flagpole ], ), ), ], ), ); } Widget _buildQuickPresetChip(OpticalRangefinderController ctrl, TargetPreset preset) { final isSelected = !ctrl.isCustomTarget.value && ctrl.selectedTarget.value.id == preset.id; return InkWell( onTap: () => ctrl.selectTarget(preset), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isSelected ? const Color(0xFF0071E3) : const Color(0xFF020617), borderRadius: BorderRadius.circular(8), border: Border.all(color: isSelected ? const Color(0xFF00F0FF) : Colors.white12), ), child: Text( '${preset.titleAr.split(' ').first} (${preset.heightMeters.toStringAsFixed(1)}م)', style: TextStyle( color: isSelected ? Colors.white : const Color(0xFF94A3B8), fontSize: 10.5, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ), ); } // ── Modals & Sheets ──────────────────────────────────────────────────────── void _openTargetPickerSheet(BuildContext context, OpticalRangefinderController ctrl) { showModalBottomSheet( context: context, backgroundColor: const Color(0xFF090E17), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), side: BorderSide(color: Color(0xFF0071E3), width: 1.5), ), builder: (ctx) { return Padding( padding: const EdgeInsets.all(18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'مكتبة الأهداف التكتيكية الميدانية', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(Icons.close, color: Colors.white54, size: 18), onPressed: () => Navigator.pop(ctx), ), ], ), const SizedBox(height: 10), Flexible( child: ListView( shrinkWrap: true, children: [ ...CameraSensorCalibrationService.standardTargets.map((preset) { return ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.adjust, color: Color(0xFF00F0FF), size: 18), ), title: Text(preset.titleAr, style: const TextStyle(color: Colors.white, fontSize: 12.5)), subtitle: Text( 'التصنيف: ${preset.categoryAr} • الارتفاع: ${preset.heightMeters} متر', style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), trailing: const Icon(Icons.chevron_left, color: Colors.white24), onTap: () { ctrl.selectTarget(preset); Navigator.pop(ctx); }, ); }), const Divider(color: Colors.white12), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF0F172A), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.edit, color: Color(0xFFFBBF24), size: 18), ), title: const Text('إدخال ارتفاع يدوي مخصص...', style: TextStyle(color: Color(0xFFFBBF24), fontSize: 12.5, fontWeight: FontWeight.bold)), subtitle: const Text('لتحديد ارتفاع أي معلم أو جبل أو سارية بالأمتار بدقة', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)), onTap: () { Navigator.pop(ctx); _showCustomHeightDialog(context, ctrl); }, ), ], ), ), ], ), ); }, ); } void _showCustomHeightDialog(BuildContext context, OpticalRangefinderController ctrl) { final textCtrl = TextEditingController(text: ctrl.customHeight.value.toStringAsFixed(1)); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF090E17), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: const BorderSide(color: Color(0xFF0071E3)), ), title: const Text('تعيين ارتفاع الهدف التكتيكي', style: TextStyle(color: Colors.white, fontSize: 14)), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'أدخل الارتفاع التقديري الحقيقي للمعلم بالأمتار لحساب المدى البصري بدقة:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11), ), const SizedBox(height: 14), TextField( controller: textCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), decoration: InputDecoration( suffixText: 'متر', suffixStyle: const TextStyle(color: Color(0xFF00F0FF)), filled: true, fillColor: const Color(0xFF020617), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('إلغاء', style: TextStyle(color: Colors.white54)), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)), onPressed: () { final val = double.tryParse(textCtrl.text); if (val != null && val > 0) { ctrl.setCustomTargetHeight(val); } Navigator.pop(ctx); }, child: const Text('حفظ واحتساب', style: TextStyle(color: Colors.white)), ), ], ), ); } void _openSensorInspectorDialog(BuildContext context, OpticalRangefinderController ctrl) { final profile = ctrl.sensorProfile.value; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF090E17), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18), side: const BorderSide(color: Color(0xFF38BDF8), width: 1.5), ), title: const Row( children: [ Icon(Icons.memory, color: Color(0xFF38BDF8), size: 20), SizedBox(width: 8), Text('فحص ومعايرة مستشعر الكاميرا', style: TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.bold)), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInspectorRow('اسم المستشعر:', profile.deviceName), const Divider(color: Colors.white12, height: 12), _buildInspectorRow('البعد البؤري الفيزيائي:', '${profile.focalLengthMm.toStringAsFixed(2)} mm'), const Divider(color: Colors.white12, height: 12), _buildInspectorRow('المكافئ البؤري (35mm Equiv):', '${profile.focalLength35mmEquiv.toStringAsFixed(1)} mm'), const Divider(color: Colors.white12, height: 12), _buildInspectorRow('زاوية الرؤية الأفقية (HFOV):', '${profile.horizontalFovDegrees.toStringAsFixed(1)}°'), const Divider(color: Colors.white12, height: 12), _buildInspectorRow('أبعاد المستشعر (W x H):', '${profile.sensorWidthMm.toStringAsFixed(2)} x ${profile.sensorHeightMm.toStringAsFixed(2)} mm'), const Divider(color: Colors.white12, height: 12), _buildInspectorRow('حالة المعايرة البصرية:', 'معاير تلقائياً 100% (Calibrated)'), ], ), actions: [ ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0071E3)), onPressed: () => Navigator.pop(ctx), child: const Text('تم', style: TextStyle(color: Colors.white)), ), ], ), ); } Widget _buildInspectorRow(String label, String value) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11)), Text(value, style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), ], ); } } // ── Painter ────────────────────────────────────────────────────────────────── class _MilReticulePainter extends CustomPainter { final double centerX; final double centerY; final double bracketHalfHeight; final double bracketWidth; final double rollDeg; _MilReticulePainter({ required this.centerX, required this.centerY, required this.bracketHalfHeight, required this.bracketWidth, required this.rollDeg, }); @override void paint(Canvas canvas, Size size) { final reticulePaint = Paint() ..color = const Color(0xFF00F0FF).withAlpha(200) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final bracketPaint = Paint() ..color = const Color(0xFFFBBF24) ..strokeWidth = 2.5 ..style = PaintingStyle.stroke; final centerPaint = Paint() ..color = const Color(0xFF00F0FF) ..style = PaintingStyle.fill; // Center dot canvas.drawCircle(Offset(centerX, centerY), 3.0, centerPaint); // Crosshair horizontal and vertical lines with mil marks const tickLen = 6.0; for (int i = -4; i <= 4; i++) { if (i == 0) continue; final x = centerX + i * 20.0; canvas.drawLine(Offset(x, centerY - tickLen), Offset(x, centerY + tickLen), reticulePaint); final y = centerY + i * 20.0; canvas.drawLine(Offset(centerX - tickLen, y), Offset(centerX + tickLen, y), reticulePaint); } // Top Bracket final topY = centerY - bracketHalfHeight; canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY), bracketPaint); canvas.drawLine(Offset(centerX - bracketWidth / 2, topY), Offset(centerX - bracketWidth / 2, topY + 12), bracketPaint); canvas.drawLine(Offset(centerX + bracketWidth / 2, topY), Offset(centerX + bracketWidth / 2, topY + 12), bracketPaint); // Bottom Bracket final botY = centerY + bracketHalfHeight; canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY), bracketPaint); canvas.drawLine(Offset(centerX - bracketWidth / 2, botY), Offset(centerX - bracketWidth / 2, botY - 12), bracketPaint); canvas.drawLine(Offset(centerX + bracketWidth / 2, botY), Offset(centerX + bracketWidth / 2, botY - 12), bracketPaint); } @override bool shouldRepaint(covariant _MilReticulePainter oldDelegate) { return oldDelegate.bracketHalfHeight != bracketHalfHeight || oldDelegate.bracketWidth != bracketWidth || oldDelegate.rollDeg != rollDeg; } }