25-10-2/1

This commit is contained in:
Hamza-Ayed
2025-10-02 01:19:38 +03:00
parent f08ee61a7e
commit 95fb065bdb
43 changed files with 13917 additions and 13426 deletions

View File

@@ -4,241 +4,523 @@ import 'dart:math';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart';
import 'package:sefer_driver/constant/table_names.dart';
import 'package:sefer_driver/controller/home/captin/map_driver_controller.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 'battery_status.dart';
import 'crud.dart';
import 'encrypt_decrypt.dart';
/// LocationController - النسخة النهائية المتكاملة مع وضع توفير الطاقة
///
/// تم تصميم هذا المتحكم ليكون المحرك الجديد لإدارة الموقع في تطبيقك.
/// يجمع بين الكفاءة العالية، المنطق الذكي، والتوافق الكامل مع بنية الكود الحالية.
class LocationController extends GetxController {
LocationData? _currentLocation;
late Location location = Location();
bool isLoading = false;
late double heading = 0;
late double previousTime = 0;
late double latitude;
late double totalDistance = 0;
late double longitude;
late DateTime time;
late double speed = 0;
bool isActive = false;
late LatLng myLocation = LatLng(0, 0);
String totalPoints = '0';
LocationData? get currentLocation => _currentLocation;
Timer? _locationTimer;
// ===================================================================
// ====== 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);
LatLng? _lastSavedPosition;
// -- 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
void onInit() async {
Future<void> onInit() async {
super.onInit();
location = Location();
await location.changeSettings(
accuracy: LocationAccuracy.high, interval: 5000, distanceFilter: 0);
location.enableBackgroundMode(enable: true);
await getLocation();
await startLocationUpdates();
print('LocationController onInit started...');
totalPoints = Get.put(CaptainWalletController()).totalPoints.toString();
isActive = Get.put(HomeCaptainController()).isActive;
}
String getLocationArea(double latitude, double longitude) {
final locations = box.read(BoxName.locationName) ?? [];
for (final location in locations) {
final locationData = location as Map<String, dynamic>;
final minLatitude =
double.tryParse(locationData['min_latitude'].toString()) ?? 0.0;
final maxLatitude =
double.tryParse(locationData['max_latitude'].toString()) ?? 0.0;
final minLongitude =
double.tryParse(locationData['min_longitude'].toString()) ?? 0.0;
final maxLongitude =
double.tryParse(locationData['max_longitude'].toString()) ?? 0.0;
if (latitude >= minLatitude &&
latitude <= maxLatitude &&
longitude >= minLongitude &&
longitude <= maxLongitude) {
box.write(BoxName.serverChosen, (locationData['server_link']));
return locationData['name'];
}
bool dependenciesReady = await _awaitDependencies();
if (!dependenciesReady) {
print(
"❌ CRITICAL ERROR: Dependencies not found. Location services will not start.");
return;
}
box.write(BoxName.serverChosen, AppLink.seferCairoServer);
return 'Cairo';
_isReady = true;
await _initLocationSettings();
await startLocationUpdates();
_listenToBatteryChanges(); // **إضافة جديدة:** بدء الاستماع لتغيرات البطارية
print('✅ LocationController is ready and initialized.');
}
int _insertCounter = 0;
double? _lastSpeed;
DateTime? _lastSpeedTime;
@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 (box.read(BoxName.driverID) != null) {
_locationTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
try {
await BatteryNotifier.checkBatteryAndNotify();
totalPoints =
Get.find<CaptainWalletController>().totalPoints.toString();
isActive = Get.find<HomeCaptainController>().isActive;
if (isActive && double.parse(totalPoints) > -30000) {
await getLocation();
if (myLocation.latitude == 0 && myLocation.longitude == 0) return;
String area =
getLocationArea(myLocation.latitude, myLocation.longitude);
final payload = {
'driver_id': box.read(BoxName.driverID).toString(),
'latitude': myLocation.latitude.toString(),
'longitude': myLocation.longitude.toString(),
'heading': heading.toString(),
'speed': (speed * 3.6).toStringAsFixed(1),
'distance': totalDistance.toStringAsFixed(2),
'status': box.read(BoxName.statusDriverLocation) ?? 'off',
};
// ✅ تحديث للسيرفر
await CRUD().post(
link: '${AppLink.server}/ride/location/update.php',
payload: payload,
);
// ✅ تخزين في SQLite فقط إذا الرحلة On + تحرك أكثر من 10 متر
if ((box.read(BoxName.statusDriverLocation) ?? 'off') == 'on') {
if (_lastSavedPosition == null ||
_calculateDistanceInMeters(_lastSavedPosition!, myLocation) >=
10) {
double currentSpeed = speed; // m/s
double? acceleration = _calculateAcceleration(currentSpeed);
await sql.insertData({
'driver_id': box.read(BoxName.driverID).toString(),
'latitude': myLocation.latitude,
'longitude': myLocation.longitude,
'acceleration': acceleration ?? 0.0,
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
}, TableName.behavior);
_lastSavedPosition = myLocation;
}
}
// ✅ إدخال للسيرفر كل دقيقة
_insertCounter++;
// Log.print('_insertCounter: ${_insertCounter}');
if (_insertCounter == 12) {
_insertCounter = 0;
await CRUD().post(
link: '${AppLink.server}/ride/location/add.php',
payload: payload,
);
}
// ✅ تحديث الكاميرا
Get.find<HomeCaptainController>()
.mapHomeCaptainController
?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
bearing: Get.find<LocationController>().heading,
target: myLocation,
zoom: 17, // Adjust zoom level as needed
),
),
);
// if (Get.isRegistered()) {
// Get.find<MapDriverController>().mapController?.animateCamera(
// CameraUpdate.newCameraPosition(
// CameraPosition(
// bearing: Get.find<LocationController>().heading,
// target: myLocation,
// zoom: 17, // Adjust zoom level as needed
// ),
// ),
// );
// }
}
} catch (e) {
print('Location update error: $e');
}
});
// ... (الكود لم يتغير)
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() {
_locationTimer?.cancel();
// ... (الكود لم يتغير)
_locSub?.cancel();
_locSub = null;
_trackInsertTimer?.cancel();
_trackInsertTimer = null;
_heartbeatTimer?.cancel();
_heartbeatTimer = null;
print('Location updates and timers stopped.');
}
Future<void> getLocation() async {
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;
if (!serviceEnabled) return false;
}
PermissionStatus permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) {
permissionGranted = await location.requestPermission();
if (permissionGranted != PermissionStatus.granted) return;
var perm = await location.hasPermission();
if (perm == PermissionStatus.denied) {
perm = await location.requestPermission();
if (perm != PermissionStatus.granted) return false;
}
return true;
}
Future.delayed(Duration(milliseconds: 500), () async {
try {
LocationData _locationData = await location.getLocation();
if (_locationData.latitude != null && _locationData.longitude != null) {
myLocation =
LatLng(_locationData.latitude!, _locationData.longitude!);
} else {
myLocation = LatLng(0, 0);
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.');
}
speed = _locationData.speed ?? 0;
heading = _locationData.heading ?? 0;
void _startTimers() {
_trackInsertTimer?.cancel();
_heartbeatTimer?.cancel();
update();
} catch (e) {
print("Error getting location: $e");
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);
});
}
double _calculateDistanceInMeters(LatLng start, LatLng end) {
const p = 0.017453292519943295;
final a = 0.5 -
cos((end.latitude - start.latitude) * p) / 2 +
cos(start.latitude * p) *
cos(end.latitude * p) *
(1 - cos((end.longitude - start.longitude) * p)) /
2;
return 12742 * 1000 * asin(sqrt(a)); // meters
}
void _checkBatteryLevel(int level) {
final bool wasInPowerSaveMode = _isPowerSavingMode;
double? _calculateAcceleration(double currentSpeed) {
final now = DateTime.now();
if (_lastSpeed != null && _lastSpeedTime != null) {
final deltaTime =
now.difference(_lastSpeedTime!).inMilliseconds / 1000.0; // seconds
if (deltaTime > 0) {
final acceleration = (currentSpeed - _lastSpeed!) / deltaTime;
_lastSpeed = currentSpeed;
_lastSpeedTime = now;
return double.parse(acceleration.toStringAsFixed(2));
}
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.server}/ride/location/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.server}/ride/location/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.server}/ride/location/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;
_lastSpeedTime = now;
_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');
}
}
}