chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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)}م)';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user