// food_tracking_controller.dart — تتبّع سائق التوصيل على الخريطة (جهة الراكب) // // مصدر الموقع: backend/food/order/courier_location.php الذي يقرأ من Redis // (عمر 90 ثانية). لا سوكيت هنا: تطبيق الراكب لا يحمل عميلاً لسوكيت الطعام، // والسحب كل 6 ثوانٍ كافٍ لحركة سلسة على الخريطة مع تحريك مُنعّم للعلامة. // // التتبّع يتوقف تلقائياً بانتهاء الطلب: الخادم يُرجع null خارج نافذة // courier_assigned/picked_up، وعندها نُغلق المؤقّت ولا نسأل مجدداً. import 'dart:async'; import 'dart:ui' show Offset; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../constant/links.dart'; import '../functions/crud.dart'; class FoodTrackingController extends GetxController { final int orderId; FoodTrackingController(this.orderId); IntaleqMapController? mapController; bool isStyleLoaded = false; bool isLoading = true; LatLng? courierPosition; LatLng? merchantPosition; LatLng? destinationPosition; String merchantName = ''; String? destinationAddress; String orderStatus = ''; DateTime? lastFixAt; Set markers = {}; Timer? _timer; bool _cameraSettled = false; bool get isTrackingActive => orderStatus == 'courier_assigned' || orderStatus == 'picked_up'; /// الموقع قديم إن مضى عليه أكثر من دقيقة — نُعلم الراكب بدل إيهامه بحركة حيّة bool get isStale => lastFixAt == null || DateTime.now().difference(lastFixAt!).inSeconds > 60; @override void onInit() { super.onInit(); _fetch(); _timer = Timer.periodic(const Duration(seconds: 6), (_) => _fetch()); } void onMapCreated(IntaleqMapController controller) { mapController = controller; update(); } void onStyleLoaded() { isStyleLoaded = true; _rebuildMarkers(); _settleCamera(); } Future _fetch() async { final res = await CRUD().post( link: '${AppLink.server}/food/order/courier_location.php', payload: {'order_id': orderId.toString()}, ); isLoading = false; if (res is! Map || res['status'] != 'success' || res['message'] is! Map) { update(); return; } final data = Map.from(res['message']); orderStatus = data['status']?.toString() ?? ''; final merchant = data['merchant']; if (merchant is Map) { merchantName = merchant['name_ar']?.toString() ?? ''; merchantPosition = _latLng(merchant['lat'], merchant['lng']); } final dest = data['destination']; if (dest is Map) { destinationAddress = dest['address']?.toString(); destinationPosition = _latLng(dest['lat'], dest['lng']); } final pos = data['courier_position']; if (pos is Map) { final next = _latLng(pos['lat'], pos['lng']); if (next != null) { courierPosition = next; final ts = int.tryParse(pos['ts']?.toString() ?? ''); lastFixAt = ts == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(ts * 1000); } } // انتهى الطلب — لا داعي لمواصلة السؤال، ولا لإبقاء علامة السائق if (!isTrackingActive) { _timer?.cancel(); courierPosition = null; } _rebuildMarkers(); _settleCamera(); update(); } LatLng? _latLng(dynamic lat, dynamic lng) { final la = double.tryParse(lat?.toString() ?? ''); final ln = double.tryParse(lng?.toString() ?? ''); if (la == null || ln == null || (la == 0 && ln == 0)) return null; return LatLng(la, ln); } void _rebuildMarkers() { if (!isStyleLoaded) return; final next = {}; if (merchantPosition != null) { next.add(Marker( markerId: const MarkerId('merchant'), position: merchantPosition!, // نستعمل أصولاً موجودة فعلاً في المشروع — لا أيقونات مطعم/منزل مخصصة بعد icon: InlqBitmap.fromAsset('assets/images/picker.png'), anchor: const Offset(0.5, 0.5), )); } if (destinationPosition != null) { next.add(Marker( markerId: const MarkerId('destination'), position: destinationPosition!, icon: InlqBitmap.fromAsset('assets/images/blob.png'), anchor: const Offset(0.5, 0.5), )); } if (courierPosition != null) { next.add(Marker( markerId: const MarkerId('courier'), position: courierPosition!, icon: InlqBitmap.fromAsset('assets/images/moto1.png'), anchor: const Offset(0.5, 0.5), )); } markers = next; } // نُحرّك الكاميرا مرة واحدة عند أول موقع، ثم نتبع السائق فقط. // التحريك المستمر على كل تحديث يمنع الراكب من تصفّح الخريطة بيده. void _settleCamera() { if (!isStyleLoaded || mapController == null) return; final focus = courierPosition ?? merchantPosition ?? destinationPosition; if (focus == null) return; if (!_cameraSettled) { mapController!.animateCamera(CameraUpdate.newLatLngZoom(focus, 15)); _cameraSettled = true; } else if (courierPosition != null) { mapController!.animateCamera(CameraUpdate.newLatLng(courierPosition!)); } } @override void onClose() { _timer?.cancel(); super.onClose(); } }