177 lines
5.5 KiB
Dart
177 lines
5.5 KiB
Dart
// 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<Marker> 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<void> _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<String, dynamic>.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 = <Marker>{};
|
|
|
|
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();
|
|
}
|
|
}
|