215 lines
7.4 KiB
Dart
215 lines
7.4 KiB
Dart
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<TargetPreset> 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<CameraSensorProfile> 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<void> 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<void> 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)}م)';
|
|
}
|
|
}
|