import 'dart:async'; import 'dart:math'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:battery_plus/battery_plus.dart'; // **إضافة جديدة:** للتعامل مع حالة البطارية import 'package:sefer_driver/constant/table_names.dart'; import '../../constant/box_name.dart'; import '../../constant/links.dart'; import '../../main.dart'; import '../../print.dart'; import '../home/captin/home_captain_controller.dart'; import '../home/payment/captain_wallet_controller.dart'; import 'crud.dart'; /// LocationController - النسخة النهائية المتكاملة مع وضع توفير الطاقة /// /// تم تصميم هذا المتحكم ليكون المحرك الجديد لإدارة الموقع في تطبيقك. /// يجمع بين الكفاءة العالية، المنطق الذكي، والتوافق الكامل مع بنية الكود الحالية. class LocationController extends GetxController { // =================================================================== // ====== Tunables / المتغيرات القابلة للتعديل ====== // =================================================================== // -- Normal Mode -- static const double onMoveMetersNormal = 15.0; static const double offMoveMetersNormal = 200.0; static const Duration trackInsertEveryNormal = Duration(minutes: 1); static const Duration heartbeatEveryNormal = Duration(minutes: 2); // -- Power Saving Mode -- static const double onMoveMetersPowerSave = 75.0; static const double offMoveMetersPowerSave = 500.0; static const Duration trackInsertEveryPowerSave = Duration(minutes: 2); static const Duration heartbeatEveryPowerSave = Duration(minutes: 5); static const double lowWalletThreshold = -30000; static const int powerSaveTriggerLevel = 20; // نسبة البطارية لتفعيل وضع التوفير static const int powerSaveExitLevel = 25; // نسبة البطارية للخروج من وضع التوفير // =================================================================== // ====== Services & Subscriptions (الخدمات والاشتراكات) ====== // =================================================================== late final Location location = Location(); final Battery _battery = Battery(); // **إضافة جديدة:** للتعامل مع البطارية StreamSubscription? _locSub; StreamSubscription? _batterySub; Timer? _trackInsertTimer; Timer? _heartbeatTimer; // =================================================================== // ====== Cached Controllers (لتجنب Get.find المتكرر) ====== // =================================================================== late final HomeCaptainController _homeCtrl; late final CaptainWalletController _walletCtrl; // =================================================================== // ====== Public state (لواجهة المستخدم والكلاسات الأخرى) ====== // =================================================================== LatLng myLocation = const LatLng(0, 0); double heading = 0.0; double speed = 0.0; double totalDistance = 0.0; // =================================================================== // ====== Internal state (للمنطق الداخلي) ====== // =================================================================== bool _isReady = false; bool _isPowerSavingMode = false; // **إضافة جديدة:** لتتبع وضع توفير الطاقة LatLng? _lastSentLoc; String? _lastSentStatus; DateTime? _lastSentAt; LatLng? _lastPosForDistance; // **إضافة جديدة:** متغيرات لحساب سلوك السائق LatLng? _lastSqlLoc; double? _lastSpeed; DateTime? _lastSpeedAt; @override Future onInit() async { super.onInit(); print('LocationController onInit started...'); bool dependenciesReady = await _awaitDependencies(); if (!dependenciesReady) { print( "❌ CRITICAL ERROR: Dependencies not found. Location services will not start."); return; } _isReady = true; await _initLocationSettings(); await startLocationUpdates(); _listenToBatteryChanges(); // **إضافة جديدة:** بدء الاستماع لتغيرات البطارية print('✅ LocationController is ready and initialized.'); } @override void onClose() { print('🛑 Closing LocationController...'); stopLocationUpdates(); _batterySub?.cancel(); // إيقاف الاستماع للبطارية super.onClose(); } Future _awaitDependencies() async { // ... (الكود لم يتغير) int attempts = 0; while (attempts < 10) { if (Get.isRegistered() && Get.isRegistered()) { _homeCtrl = Get.find(); _walletCtrl = Get.find(); print("✅ Dependencies found and controllers are cached."); return true; } await Future.delayed(const Duration(milliseconds: 500)); attempts++; } return false; } // =================================================================== // ====== Public Control Methods (دوال التحكم العامة) ====== // =================================================================== Future startLocationUpdates() async { // ... (الكود لم يتغير) if (!_isReady) { print("Cannot start updates: LocationController is not ready."); return; } final points = _walletCtrl.totalPoints; if (double.parse(points) <= lowWalletThreshold) { print('❌ Blocked: low wallet balance ($points)'); stopLocationUpdates(); return; } if (_locSub != null) { print('Location updates are already active.'); return; } if (await _ensureServiceAndPermission()) { _subscribeLocationStream(); _startTimers(); } } void stopLocationUpdates() { // ... (الكود لم يتغير) _locSub?.cancel(); _locSub = null; _trackInsertTimer?.cancel(); _trackInsertTimer = null; _heartbeatTimer?.cancel(); _heartbeatTimer = null; print('Location updates and timers stopped.'); } Future getLocation() async { // ... (الكود لم يتغير) try { if (await _ensureServiceAndPermission()) { return await location.getLocation(); } } catch (e) { print('❌ FAILED to get single location: $e'); } return null; } // =================================================================== // ====== Core Logic (المنطق الأساسي) ====== // =================================================================== Future _initLocationSettings() async { // ... (الكود لم يتغير) await location.changeSettings( accuracy: LocationAccuracy.high, interval: 5000, distanceFilter: 0, ); await location.enableBackgroundMode(enable: true); } Future _ensureServiceAndPermission() async { // ... (الكود لم يتغير) bool serviceEnabled = await location.serviceEnabled(); if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) return false; } var perm = await location.hasPermission(); if (perm == PermissionStatus.denied) { perm = await location.requestPermission(); if (perm != PermissionStatus.granted) return false; } return true; } void _subscribeLocationStream() { _locSub?.cancel(); _locSub = location.onLocationChanged.listen( (loc) async { if (!_isReady) return; try { if (loc.latitude == null || loc.longitude == null) return; final now = DateTime.now(); final pos = LatLng(loc.latitude!, loc.longitude!); myLocation = pos; speed = loc.speed ?? 0.0; heading = loc.heading ?? 0.0; if (_lastPosForDistance != null) { final d = _haversineMeters(_lastPosForDistance!, pos); if (d > 2.0) totalDistance += d; } _lastPosForDistance = pos; // ✅ تحديث الكاميرا // _homeCtrl.mapHomeCaptainController?.animateCamera( // CameraUpdate.newCameraPosition( // CameraPosition( // bearing: Get.find().heading, // target: myLocation, // zoom: 17, // Adjust zoom level as needed // ), // ), // ); update(); // تحديث الواجهة الرسومية بالبيانات الجديدة await _smartSend(pos, loc); // **إضافة جديدة:** حفظ سلوك السائق في قاعدة البيانات المحلية await _saveBehaviorIfMoved(pos, now, currentSpeed: speed); } catch (e) { print('Error in onLocationChanged: $e'); } }, onError: (e) => print('Location stream error: $e'), ); print('📡 Subscribed to location stream.'); } void _startTimers() { _trackInsertTimer?.cancel(); _heartbeatTimer?.cancel(); final trackDuration = _isPowerSavingMode ? trackInsertEveryPowerSave : trackInsertEveryNormal; final heartbeatDuration = _isPowerSavingMode ? heartbeatEveryPowerSave : heartbeatEveryNormal; _trackInsertTimer = Timer.periodic(trackDuration, (_) => _addSingleTrackPoint()); _heartbeatTimer = Timer.periodic(heartbeatDuration, (_) => _sendStationaryHeartbeat()); print('⏱️ Background timers started (Power Save: $_isPowerSavingMode).'); } Future _smartSend(LatLng pos, LocationData loc) async { final String driverStatus = box.read(BoxName.statusDriverLocation) ?? 'off'; final distSinceSent = (_lastSentLoc == null) ? 999.0 : _haversineMeters(_lastSentLoc!, pos); final onMoveThreshold = _isPowerSavingMode ? onMoveMetersPowerSave : onMoveMetersNormal; final offMoveThreshold = _isPowerSavingMode ? offMoveMetersPowerSave : offMoveMetersNormal; bool shouldSend = false; if (driverStatus != _lastSentStatus) { shouldSend = true; if (driverStatus == 'on') { totalDistance = 0.0; _lastPosForDistance = pos; } print( 'Status changed: ${_lastSentStatus ?? '-'} -> $driverStatus. Sending...'); } else if (driverStatus == 'on') { if (distSinceSent >= onMoveThreshold) { shouldSend = true; } } else { // driverStatus == 'off' if (distSinceSent >= offMoveThreshold) { shouldSend = true; } } if (!shouldSend) return; await _sendUpdate(pos, driverStatus, loc); } // =================================================================== // ====== Battery Logic (منطق البطارية) ====== // =================================================================== void _listenToBatteryChanges() async { _checkBatteryLevel(await _battery.batteryLevel); _batterySub = _battery.onBatteryStateChanged.listen((BatteryState state) async { _checkBatteryLevel(await _battery.batteryLevel); }); } void _checkBatteryLevel(int level) { final bool wasInPowerSaveMode = _isPowerSavingMode; if (level <= powerSaveTriggerLevel) { _isPowerSavingMode = true; } else if (level >= powerSaveExitLevel) { _isPowerSavingMode = false; } if (_isPowerSavingMode != wasInPowerSaveMode) { if (_isPowerSavingMode) { Get.snackbar( "وضع توفير الطاقة مُفعّل", "البطارية منخفضة. سنقلل تحديثات الموقع للحفاظ على طاقتك.", snackPosition: SnackPosition.TOP, backgroundColor: Get.theme.primaryColor.withOpacity(0.9), colorText: Get.theme.colorScheme.onPrimary, duration: const Duration(seconds: 7), ); } else { Get.snackbar( "العودة للوضع الطبيعي", "تم شحن البطارية. عادت تحديثات الموقع لوضعها الطبيعي.", snackPosition: SnackPosition.TOP, backgroundColor: Get.theme.colorScheme.secondary.withOpacity(0.9), colorText: Get.theme.colorScheme.onSecondary, duration: const Duration(seconds: 5), ); } _startTimers(); } } // =================================================================== // ====== API Communication & Helpers (التواصل مع السيرفر والدوال المساعدة) ====== // =================================================================== Future _sendUpdate(LatLng pos, String status, LocationData loc) async { final payload = _buildPayload(pos, status, loc); try { await CRUD().post( link: '${AppLink.locationServer}/update.php', payload: payload, ); _lastSentLoc = pos; _lastSentStatus = status; _lastSentAt = DateTime.now(); print('✅ Sent to update.php [$status]'); } catch (e) { print('❌ FAILED to send to update.php: $e'); } } Future _addSingleTrackPoint() async { if (!_isReady) return; final String driverStatus = (box.read(BoxName.statusDriverLocation) ?? 'off').toString(); if (myLocation.latitude == 0 && myLocation.longitude == 0) return; if (_lastSentLoc == null) return; // حماية إضافية // قيَم رقمية آمنة final double safeHeading = (heading is num) ? (heading as num).toDouble() : 0.0; final double safeSpeed = (speed is num) ? (speed as num).toDouble() : 0.0; final double safeDistKm = (totalDistance is num) ? (totalDistance as num).toDouble() / 1000.0 : 0.0; final String driverId = (box.read(BoxName.driverID) ?? '').toString().trim(); if (driverId.isEmpty) return; // لا ترسل بدون DriverID // ✅ كل شيء Strings فقط final Map payload = { 'driver_id': driverId, 'latitude': _lastSentLoc!.latitude.toStringAsFixed(6), 'longitude': _lastSentLoc!.longitude.toStringAsFixed(6), 'heading': safeHeading.toStringAsFixed(1), 'speed': (safeSpeed < 0.5 ? 0.0 : safeSpeed) .toString(), // أو toStringAsFixed(2) 'distance': safeDistKm.toStringAsFixed(2), 'status': driverStatus, 'carType': (box.read(BoxName.carType) ?? 'default').toString(), }; try { print('⏱️ Adding a single point to car_track... $payload'); await CRUD().post( link: '${AppLink.locationServer}/add.php', payload: payload, // ← الآن Map ); } catch (e) { print('❌ FAILED to send single track point: $e'); } } Future _sendStationaryHeartbeat() async { if (!_isReady) return; if (_lastSentLoc == null || _lastSentAt == null) return; if (DateTime.now().difference(_lastSentAt!).inSeconds < 90) return; final distSinceSent = _haversineMeters(_lastSentLoc!, myLocation); if (distSinceSent >= onMoveMetersNormal) return; print('🫀 Driver is stationary, sending heartbeat...'); final String driverStatus = (box.read(BoxName.statusDriverLocation) ?? 'off').toString(); // ✅ كل شيء Strings final Map payload = { 'driver_id': (box.read(BoxName.driverID) ?? '').toString(), 'latitude': _lastSentLoc!.latitude.toStringAsFixed(6), 'longitude': _lastSentLoc!.longitude.toStringAsFixed(6), 'heading': heading.toStringAsFixed(1), // ملاحظة: هنا السرعة تبقى بالمتر/ث بعد التحويل أدناه؛ وحّدتّها لكتابة String 'speed': ((speed < 0.5) ? 0.0 : speed).toString(), 'distance': (totalDistance / 1000).toStringAsFixed(2), 'status': driverStatus, 'carType': (box.read(BoxName.carType) ?? 'default').toString(), // 'hb': '1' }; try { await CRUD().post( link: '${AppLink.locationServer}/update.php', payload: payload, ); _lastSentAt = DateTime.now(); } catch (e) { print('❌ FAILED to send Heartbeat: $e'); } } Map _buildPayload( LatLng pos, String status, LocationData loc) { return { 'driver_id': (box.read(BoxName.driverID) ?? '').toString(), 'latitude': pos.latitude.toStringAsFixed(6), 'longitude': pos.longitude.toStringAsFixed(6), 'heading': (loc.heading ?? heading).toStringAsFixed(1), // هنا أنت بتحوّل السرعة إلى كم/س (×3.6) — ممتاز 'speed': ((loc.speed ?? speed) * 3.6).toStringAsFixed(1), 'status': status, 'distance': (totalDistance / 1000).toStringAsFixed(2), 'carType': (box.read(BoxName.carType) ?? 'default').toString(), // 👈 }; } double _haversineMeters(LatLng a, LatLng b) { const p = 0.017453292519943295; final h = 0.5 - cos((b.latitude - a.latitude) * p) / 2 + cos(a.latitude * p) * cos(b.latitude * p) * (1 - cos((b.longitude - a.longitude) * p)) / 2; return 12742 * 1000 * asin(sqrt(h)); } // **إضافة جديدة:** دوال لحفظ سلوك السائق محلياً /// يحسب التسارع بالمتر/ثانية^2 double? _calcAcceleration(double currentSpeed, DateTime now) { if (_lastSpeed != null && _lastSpeedAt != null) { final dt = now.difference(_lastSpeedAt!).inMilliseconds / 1000.0; if (dt > 0.5) { // لتجنب القيم الشاذة في الفترات الزمنية الصغيرة جداً final a = (currentSpeed - _lastSpeed!) / dt; _lastSpeed = currentSpeed; _lastSpeedAt = now; return a; } } _lastSpeed = currentSpeed; _lastSpeedAt = now; return null; } /// يحفظ سلوك السائق (الموقع، التسارع) في قاعدة بيانات SQLite المحلية Future _saveBehaviorIfMoved(LatLng pos, DateTime now, {required double currentSpeed}) async { final dist = (_lastSqlLoc == null) ? 999.0 : _haversineMeters(_lastSqlLoc!, pos); if (dist < 15.0) return; // الحفظ فقط عند التحرك لمسافة 15 متر على الأقل final accel = _calcAcceleration(currentSpeed, now) ?? 0.0; try { final now = DateTime.now(); final double lat = double.parse(pos.latitude.toStringAsFixed(6)); // دقة 6 أرقام final double lon = double.parse(pos.longitude.toStringAsFixed(6)); // دقة 6 أرقام final double acc = double.parse( (accel is num ? accel as num : 0).toStringAsFixed(2)); // دقة منزلتين await sql.insertData({ 'driver_id': (box.read(BoxName.driverID) ?? '').toString(), // TEXT 'latitude': lat, // REAL 'longitude': lon, // REAL 'acceleration': acc, // REAL 'created_at': now.toIso8601String(), // TEXT 'updated_at': now.toIso8601String(), // TEXT }, TableName.behavior); _lastSqlLoc = pos; } catch (e) { print('❌ FAILED to insert to SQLite (behavior): $e'); } } }