import 'dart:async'; import 'dart:math' as math; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_compass/flutter_compass.dart'; import 'package:get/get.dart'; import 'package:sensors_plus/sensors_plus.dart'; import '../models/angle_unit.dart'; import '../services/camera_sensor_calibration_service.dart'; /// ============================================================================ /// [OpticalRangefinderController] - متحكم منظومة قياس المدى البصري وحساسات الكاميرا /// ============================================================================ /// English: /// GetX Controller managing live optical stadiametric rangefinding, camera pinch-to-zoom, /// sensor intrinsics calibration, inclination pitch, and directional compass heading (الاتجاه). /// /// العربية: /// متحكم GetX لإدارة قياس المسافة البصري عبر الكاميرا والتقريب والتبعيد بدون GPS، /// مع متابعة زوايا الميل والاتجاه المغناطيسي وفحص مستشعر الجهاز. /// ============================================================================ class OpticalRangefinderController extends GetxController { CameraController? cameraController; // ── Observables / الحالات التفاعلية ────────────────────────────────────── final RxBool isCameraReady = false.obs; final RxDouble currentZoom = 1.0.obs; final RxDouble minZoom = 1.0.obs; final RxDouble maxZoom = 8.0.obs; /// زاوية الاتجاه بالدرجات (0 - 360) — مصطلح "الاتجاه" final RxDouble headingDeg = 0.0.obs; /// زاوية الميل الرأسي بالدرجات (-90 إلى +90) final RxDouble pitchDeg = 0.0.obs; /// زاوية الميل الجانبي (Roll) final RxDouble rollDeg = 0.0.obs; /// الهدف التكتيكي المختار final Rx selectedTarget = CameraSensorCalibrationService.standardTargets[0].obs; final RxDouble customHeight = 2.0.obs; final RxBool isCustomTarget = false.obs; /// ارتفاع شبكة التصويب بالبكسل / نسبة الشاشة (0.05 إلى 0.8) final RxDouble reticuleHeightRatio = 0.15.obs; /// المسافة المحسوبة بالأمتار final RxDouble calculatedDistanceMeters = 0.0.obs; final RxDouble errorMarginMeters = 0.0.obs; /// معلومات المستشعر الحالية final Rx sensorProfile = CameraSensorCalibrationService.currentProfile.obs; // Stream Subscriptions StreamSubscription? _compassSub; StreamSubscription? _accelerometerSub; double _screenHeight = 800.0; @override void onInit() { super.onInit(); initSensors(); initCamera(); } @override void onClose() { _compassSub?.cancel(); _accelerometerSub?.cancel(); cameraController?.dispose(); super.onClose(); } /// تهيئة مستشعرات الاتجاه والميل void initSensors() { // 1. Compass for Direction / الاتجاه _compassSub = FlutterCompass.events?.listen((event) { if (event.heading != null) { headingDeg.value = (event.heading! + 360.0) % 360.0; } }); // 2. Accelerometer for Pitch & Roll Inclination _accelerometerSub = accelerometerEventStream().listen((event) { // Calculate Pitch angle from gravity vector final gX = event.x; final gY = event.y; final gZ = event.z; final pitch = math.atan2(-gY, math.sqrt(gX * gX + gZ * gZ)) * (180.0 / math.pi); final roll = math.atan2(gX, gZ) * (180.0 / math.pi); pitchDeg.value = pitch; rollDeg.value = roll; }); } /// تهيئة الكاميرا وفحص المستشعر Future initCamera() async { try { final cameras = await availableCameras(); if (cameras.isEmpty) return; final backCamera = cameras.firstWhere( (c) => c.lensDirection == CameraLensDirection.back, orElse: () => cameras.first, ); cameraController = CameraController( backCamera, ResolutionPreset.high, enableAudio: false, ); await cameraController!.initialize(); // Get Zoom limits minZoom.value = await cameraController!.getMinZoomLevel(); maxZoom.value = math.min(10.0, await cameraController!.getMaxZoomLevel()); currentZoom.value = minZoom.value; // Inspect & Calibrate sensor parameters final previewSize = cameraController!.value.previewSize ?? const Size(1920, 1080); sensorProfile.value = CameraSensorCalibrationService.inspectSensor( backCamera, Size(previewSize.width, previewSize.height), ); isCameraReady.value = true; recalculateDistance(); } catch (e) { debugPrint('Optical Rangefinder Camera Error: $e'); } } /// تحديث ارتفاع الشاشة الفعلي عند الرسم void updateScreenDimensions(Size size) { if (size.height > 0 && size.height != _screenHeight) { _screenHeight = size.height; recalculateDistance(); } } /// تغيير مستوى التقريب (Pinch or Slider) Future setZoom(double newZoom) async { final clamped = newZoom.clamp(minZoom.value, maxZoom.value); currentZoom.value = clamped; try { await cameraController?.setZoomLevel(clamped); } catch (_) {} recalculateDistance(); } /// تعديل حجم مؤشر التصويب (Pinch / Drag Reticule) void setReticuleHeightRatio(double newRatio) { reticuleHeightRatio.value = newRatio.clamp(0.02, 0.75); recalculateDistance(); } /// اختيار هدف تكتيكي جاهز void selectTarget(TargetPreset preset) { selectedTarget.value = preset; isCustomTarget.value = false; recalculateDistance(); } /// تعيين ارتفاع مخصص للهدف void setCustomTargetHeight(double heightMeters) { customHeight.value = heightMeters.clamp(0.2, 500.0); isCustomTarget.value = true; recalculateDistance(); } /// إعادة احتساب المسافة بناءً على المعادلة البصرية void recalculateDistance() { final targetH = isCustomTarget.value ? customHeight.value : selectedTarget.value.heightMeters; final reticulePx = reticuleHeightRatio.value * _screenHeight; final dist = CameraSensorCalibrationService.calculateStadiametricDistance( targetRealHeightMeters: targetH, reticulePixelHeight: reticulePx, screenHeightPixels: _screenHeight, zoomMultiplier: currentZoom.value, ); calculatedDistanceMeters.value = dist; errorMarginMeters.value = CameraSensorCalibrationService.estimateErrorMarginMeters( dist, currentZoom.value, ); } /// تنسيق قيمة الاتجاه بحسب نظام الزوايا المعتمد String formatHeading(AngleUnit unit) { return unit.formatHeading(headingDeg.value); } /// تنسيق نص المسافة (متر / كم) String formatDistance() { final dist = calculatedDistanceMeters.value; final err = errorMarginMeters.value; if (dist >= 1000.0) { final km = dist / 1000.0; final errKm = err / 1000.0; return '${km.toStringAsFixed(2)} كم (±${(errKm * 1000).toInt()}م)'; } return '${dist.toStringAsFixed(0)} م (±${err.toStringAsFixed(0)}م)'; } }