527 lines
19 KiB
Dart
Executable File
527 lines
19 KiB
Dart
Executable File
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<LocationData>? _locSub;
|
||
StreamSubscription<BatteryState>? _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<void> 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<bool> _awaitDependencies() async {
|
||
// ... (الكود لم يتغير)
|
||
int attempts = 0;
|
||
while (attempts < 10) {
|
||
if (Get.isRegistered<HomeCaptainController>() &&
|
||
Get.isRegistered<CaptainWalletController>()) {
|
||
_homeCtrl = Get.find<HomeCaptainController>();
|
||
_walletCtrl = Get.find<CaptainWalletController>();
|
||
print("✅ Dependencies found and controllers are cached.");
|
||
return true;
|
||
}
|
||
await Future.delayed(const Duration(milliseconds: 500));
|
||
attempts++;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ===================================================================
|
||
// ====== Public Control Methods (دوال التحكم العامة) ======
|
||
// ===================================================================
|
||
|
||
Future<void> 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<LocationData?> getLocation() async {
|
||
// ... (الكود لم يتغير)
|
||
try {
|
||
if (await _ensureServiceAndPermission()) {
|
||
return await location.getLocation();
|
||
}
|
||
} catch (e) {
|
||
print('❌ FAILED to get single location: $e');
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ===================================================================
|
||
// ====== Core Logic (المنطق الأساسي) ======
|
||
// ===================================================================
|
||
|
||
Future<void> _initLocationSettings() async {
|
||
// ... (الكود لم يتغير)
|
||
await location.changeSettings(
|
||
accuracy: LocationAccuracy.high,
|
||
interval: 5000,
|
||
distanceFilter: 0,
|
||
);
|
||
await location.enableBackgroundMode(enable: true);
|
||
}
|
||
|
||
Future<bool> _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<LocationController>().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<void> _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<void> _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<void> _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<String, String> 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<String,String>
|
||
);
|
||
} catch (e) {
|
||
print('❌ FAILED to send single track point: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _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<String, String> 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<String, String> _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<void> _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');
|
||
}
|
||
}
|
||
}
|