Files
maps-saas/packages/tactical_app/lib/controllers/optical_rangefinder_controller.dart
T

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)}م)';
}
}