refactor: إعادة تسمية التطبيقات الأربعة siro_* → intaleq_*
المجلدات وأسماء حزم dart واستيراداتها: siro_rider → intaleq_rider siro_driver → intaleq_driver siro_admin → intaleq_admin siro_service → intaleq_service شمل ذلك `name:` في كل pubspec وكل `package:siro_*` في الاستيرادات (115 ملفاً في rider وحده)، وإشارات أسماء المجلدات في docs/ وتعليق في backend/Admin/notifications/broadcast.php. لم تبقَ إشارة واحدة (تحقّقت). + ضُمّت حزم get و get_storage داخل intaleq_driver/packages/ وحُوّلت مساراتها الثلاثة من `../../Intaleq/packages/*` إلى `./packages/*`. كانت تُحل عرضاً إلى مجلد App/Intaleq القديم المجاور — تبعية خارج المستودع تنكسر بأول نقل. لم يبقَ في أي تطبيق تبعية مسار خارج الشجرة. ⚠️ لم تُمسّ بعد: هويات المتاجر (applicationId · PRODUCT_BUNDLE_IDENTIFIER · shorebird app_id) ولا الألوان ولا النصوص المرئية ولا النطاقات — كل منها كوميت منفصل، والهويات تحتاج قرار المالك. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
92dc6b3641
commit
0aab85c732
@@ -0,0 +1,15 @@
|
||||
class CarLocation {
|
||||
final String id;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double distance;
|
||||
final double duration;
|
||||
|
||||
CarLocation({
|
||||
required this.id,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.distance = 10000,
|
||||
this.duration = 10000,
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,873 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math' show cos, max, min, pi, pow, sqrt;
|
||||
import 'dart:typed_data';
|
||||
import 'package:intaleq_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
// contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/map_widget.dart/cancel_raide_page.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../points_for_rider_controller.dart';
|
||||
import '../../../constant/univeries_polygon.dart';
|
||||
|
||||
class MapEngineController extends GetxController {
|
||||
IntaleqMapController? mapController;
|
||||
bool isStyleLoaded = false;
|
||||
bool isIconsLoaded = false;
|
||||
|
||||
Set<Marker> markers = {};
|
||||
Set<Polyline> polyLines = {};
|
||||
int markerRevision = 0;
|
||||
List<LatLng> polylineCoordinates = [];
|
||||
Set<Polygon> polygons = {};
|
||||
Set<Circle> circles = {};
|
||||
|
||||
LatLngBounds? lastComputedBounds;
|
||||
bool mapType = false;
|
||||
bool mapTrafficON = false;
|
||||
bool isMarkersShown = false;
|
||||
|
||||
String markerIcon = "marker_icon";
|
||||
String tripIcon = "trip_icon";
|
||||
String startIcon = "start_icon";
|
||||
String endIcon = "end_icon";
|
||||
String carIcon = "car_icon";
|
||||
String motoIcon = "moto_icon";
|
||||
String ladyIcon = "lady_icon";
|
||||
int _routeRenderRevision = 0;
|
||||
|
||||
double height = 150;
|
||||
double heightMenu = 0;
|
||||
double widthMenu = 0;
|
||||
double heightPickerContainer = 90;
|
||||
double heightPointsPageForRider = 0;
|
||||
double mainBottomMenuMapHeight = Get.height * .2;
|
||||
double wayPointSheetHeight = 0;
|
||||
bool heightMenuBool = false;
|
||||
bool _isPickerShown = false;
|
||||
bool get isPickerShown => _isPickerShown;
|
||||
set isPickerShown(bool value) {
|
||||
_isPickerShown = value;
|
||||
update();
|
||||
}
|
||||
|
||||
bool isPointsPageForRider = false;
|
||||
bool isBottomSheetShown = false;
|
||||
bool reloadStartApp = false;
|
||||
bool isCancelRidePageShown = false;
|
||||
bool isCashConfirmPageShown = false;
|
||||
bool isPaymentMethodPageShown = false;
|
||||
bool isRideFinished = false;
|
||||
bool rideConfirm = false;
|
||||
bool isMainBottomMenuMap = true;
|
||||
|
||||
bool isWayPointSheet = false;
|
||||
bool isWayPointStopsSheet = false;
|
||||
bool isWayPointStopsSheetUtilGetMap = false;
|
||||
double heightBottomSheetShown = 0;
|
||||
double cashConfirmPageShown = 250;
|
||||
double widthMapTypeAndTraffic = 50;
|
||||
double paymentPageShown = Get.height * .6;
|
||||
|
||||
bool isAnotherOreder = false;
|
||||
bool isWhatsAppOrder = false;
|
||||
|
||||
Map<String, Timer> _animationTimers = {};
|
||||
final int updateIntervalMs = 100;
|
||||
final double minMovementThreshold = 1.0;
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
Log.print('🗺️ Siro Map Style Loaded. Initializing...');
|
||||
isStyleLoaded = true;
|
||||
await _loadMapIcons();
|
||||
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
Get.find<RideLifecycleController>().reinit();
|
||||
|
||||
if (mapController != null) {
|
||||
if (markers.isNotEmpty && lastComputedBounds != null) {
|
||||
await _safeAnimateCameraBounds(lastComputedBounds);
|
||||
} else {
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(locationSearch.passengerLocation),
|
||||
);
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
|
||||
{double left = 60,
|
||||
double top = 60,
|
||||
double right = 60,
|
||||
double bottom = 60}) async {
|
||||
if (bounds == null || mapController == null) return;
|
||||
|
||||
try {
|
||||
if (bounds.northeast.latitude == bounds.southwest.latitude &&
|
||||
bounds.northeast.longitude == bounds.southwest.longitude) {
|
||||
Log.print(
|
||||
'⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.');
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15));
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
bounds,
|
||||
left: left,
|
||||
top: top,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e');
|
||||
try {
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMapIcons() async {
|
||||
isIconsLoaded = false;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
if (mapController != null && isStyleLoaded) break;
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
if (mapController == null || !isStyleLoaded) {
|
||||
Log.print(
|
||||
'⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.');
|
||||
}
|
||||
|
||||
await _addMapImage(startIcon, 'assets/images/A.png');
|
||||
await _addMapImage(endIcon, 'assets/images/b.png');
|
||||
await _addMapImage(carIcon, 'assets/images/car.png');
|
||||
await _addMapImage(motoIcon, 'assets/images/moto.png');
|
||||
await _addMapImage(ladyIcon, 'assets/images/lady.png');
|
||||
await _addMapImage('picker_icon', 'assets/images/picker.png');
|
||||
await _addMapImage('orange_marker', 'assets/images/moto1.png');
|
||||
await _addMapImage('violet_marker', 'assets/images/lady1.png');
|
||||
|
||||
try {
|
||||
final walkBytes = await _createWalkMarkerBytes();
|
||||
await mapController?.addImage('walk_icon', walkBytes);
|
||||
Log.print('delimited: successfully added dynamic walk_icon');
|
||||
} catch (e) {
|
||||
Log.print('❌ Error loading dynamic walk icon: $e');
|
||||
}
|
||||
|
||||
isIconsLoaded = true;
|
||||
markers = markers.map((m) => m.copyWith()).toSet();
|
||||
update();
|
||||
|
||||
if (Get.isRegistered<NearbyDriversController>()) {
|
||||
Get.find<NearbyDriversController>()
|
||||
.getCarsLocationByPassengerAndReloadMarker();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addMapImage(String id, String path) async {
|
||||
try {
|
||||
final ByteData bytes = await rootBundle.load(path);
|
||||
final size = _getImageSize(id);
|
||||
if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) {
|
||||
final resized = await _resizeImage(bytes.buffer.asUint8List(), size);
|
||||
await mapController?.addImage(id, resized);
|
||||
Log.print(
|
||||
'delimited: successfully added resized map image: $id (${size}x${size})');
|
||||
} else {
|
||||
await mapController?.addImage(id, bytes.buffer.asUint8List());
|
||||
Log.print('delimited: successfully added map image: $id');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Error loading map icon $id: $e');
|
||||
}
|
||||
}
|
||||
|
||||
int? _getImageSize(String id) {
|
||||
if (id == carIcon || id == motoIcon || id == ladyIcon) {
|
||||
// Scale based on pixel ratio (48dp for standard sleek car markers)
|
||||
double ratio = ui.window.devicePixelRatio;
|
||||
if (ratio == 0) ratio = 3.0; // fallback just in case
|
||||
return (48 * ratio).toInt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List> _resizeImage(Uint8List bytes, int size) async {
|
||||
return await compute((Uint8List data) {
|
||||
final image = img.decodeImage(data);
|
||||
if (image == null) return data;
|
||||
final resized = img.copyResize(image, width: size, height: size);
|
||||
return Uint8List.fromList(img.encodePng(resized));
|
||||
}, bytes);
|
||||
}
|
||||
|
||||
void clearPolyline() {
|
||||
polyLines = <Polyline>{};
|
||||
update();
|
||||
}
|
||||
|
||||
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
|
||||
const double earthRadius = 6378137.0;
|
||||
|
||||
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
|
||||
double lngDelta =
|
||||
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
|
||||
|
||||
double minLat = lat - latDelta;
|
||||
double maxLat = lat + latDelta;
|
||||
|
||||
double minLng = lng - lngDelta;
|
||||
double maxLng = lng + lngDelta;
|
||||
|
||||
minLat = max(-90.0, minLat);
|
||||
maxLat = min(90.0, maxLat);
|
||||
|
||||
minLng = (minLng + 180) % 360 - 180;
|
||||
maxLng = (maxLng + 180) % 360 - 180;
|
||||
|
||||
if (minLng > maxLng) {
|
||||
double temp = minLng;
|
||||
minLng = maxLng;
|
||||
maxLng = temp;
|
||||
}
|
||||
|
||||
return LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
_routeRenderRevision++;
|
||||
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locationSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locationSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('route_primary_${_routeRenderRevision}_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('route_primary_${_routeRenderRevision}_0'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void _fitCameraToPoints(LatLng p1, LatLng p2) async {
|
||||
if (mapController == null) return;
|
||||
|
||||
if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double minLat = min(p1.latitude, p2.latitude);
|
||||
double maxLat = max(p1.latitude, p2.latitude);
|
||||
double minLng = min(p1.longitude, p2.longitude);
|
||||
double maxLng = max(p1.longitude, p2.longitude);
|
||||
|
||||
if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double padding = 50.0;
|
||||
|
||||
try {
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
left: padding,
|
||||
top: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print("Error animating bounds: $e");
|
||||
try {
|
||||
LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2);
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
void fitCameraToPoints(LatLng p1, LatLng p2) {
|
||||
_fitCameraToPoints(p1, p2);
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEndAndDriver() {
|
||||
const String currentDriverMarkerId = 'assigned_driver_marker';
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
if (id == 'start') return false;
|
||||
if (id == 'end') return false;
|
||||
if (id == currentDriverMarkerId) return false;
|
||||
return true;
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEnd() {
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
return id != 'start' && id != 'end';
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void _updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
const String markerId = 'driverToPassengers';
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker != null) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
} else {
|
||||
markers = {
|
||||
...markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromAsset(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
update();
|
||||
}
|
||||
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
|
||||
}
|
||||
|
||||
void updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
_updateMarkerPosition(newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
oldMarker.position.latitude,
|
||||
oldMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
|
||||
if (distance < 2.0) return;
|
||||
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromAsset(icon),
|
||||
);
|
||||
|
||||
markers = {
|
||||
...markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapController != null) {
|
||||
mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة.
|
||||
// تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين،
|
||||
// تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة).
|
||||
// يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية.
|
||||
void smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
_smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void changeBottomSheetShown({bool? forceValue}) {
|
||||
if (forceValue != null) {
|
||||
isBottomSheetShown = forceValue;
|
||||
} else {
|
||||
isBottomSheetShown = !isBottomSheetShown;
|
||||
}
|
||||
heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCashConfirmPageShown() {
|
||||
isCashConfirmPageShown = !isCashConfirmPageShown;
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
rideLife.isCashSelectedBeforeConfirmRide = true;
|
||||
cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0;
|
||||
update();
|
||||
rideLife.update();
|
||||
}
|
||||
|
||||
void changePaymentMethodPageShown() {
|
||||
isPaymentMethodPageShown = !isPaymentMethodPageShown;
|
||||
paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapType() {
|
||||
mapType = !mapType;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapTraffic() {
|
||||
mapTrafficON = !mapTrafficON;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeisAnotherOreder(bool val) {
|
||||
isAnotherOreder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeIsWhatsAppOrder(bool val) {
|
||||
isWhatsAppOrder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCancelRidePageShow() {
|
||||
showCancelRideBottomSheet();
|
||||
isCancelRidePageShown = !isCancelRidePageShown;
|
||||
update();
|
||||
if (Get.isRegistered<RideLifecycleController>()) {
|
||||
Get.find<RideLifecycleController>().update();
|
||||
}
|
||||
}
|
||||
|
||||
void getDrawerMenu() {
|
||||
heightMenuBool = !heightMenuBool;
|
||||
widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50;
|
||||
heightMenu = heightMenuBool == true ? 80 : 0;
|
||||
widthMenu = heightMenuBool == true ? 110 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMainBottomMenuMap() {
|
||||
if (isWayPointStopsSheetUtilGetMap == true) {
|
||||
changeWayPointSheet();
|
||||
} else {
|
||||
isMainBottomMenuMap = !isMainBottomMenuMap;
|
||||
mainBottomMenuMapHeight =
|
||||
isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6;
|
||||
isWayPointSheet = false;
|
||||
if (heightMenuBool == true) {
|
||||
getDrawerMenu();
|
||||
}
|
||||
Get.find<RideLifecycleController>().initilizeGetStorage();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void downPoints() {
|
||||
if (Get.find<WayPointController>().wayPoints.length < 2) {
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
isWayPointSheet = false;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointSheet() {
|
||||
isWayPointSheet = !isWayPointSheet;
|
||||
wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointStopsSheet() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPointIndex > -1) {
|
||||
isWayPointStopsSheet = true;
|
||||
isWayPointStopsSheetUtilGetMap = true;
|
||||
}
|
||||
isWayPointStopsSheet = !isWayPointStopsSheet;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeHeightPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesDestination.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightStartPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesStart.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlacesAll(int index) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placeListResponseAll[index].isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces1() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint1.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces2() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint2.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces3() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint3.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces4() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint4.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void hidePlaces() {
|
||||
height = 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changePickerShown() {
|
||||
_isPickerShown = !_isPickerShown;
|
||||
heightPickerContainer = _isPickerShown == true ? 150 : 90;
|
||||
update();
|
||||
}
|
||||
|
||||
void _initializePolygons() {
|
||||
List<List<LatLng>> universityPolygons =
|
||||
UniversitiesPolygons.universityPolygons;
|
||||
|
||||
for (int i = 0; i < universityPolygons.length; i++) {
|
||||
Polygon polygon = Polygon(
|
||||
polygonId: PolygonId('univ_$i'),
|
||||
points: universityPolygons[i],
|
||||
fillColor: Colors.blueAccent.withOpacity(0.2),
|
||||
strokeColor: Colors.blueAccent,
|
||||
strokeWidth: 2,
|
||||
);
|
||||
polygons.add(polygon);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _applyLowEndModeIfNeeded() {
|
||||
// Placeholder comment from original
|
||||
}
|
||||
|
||||
Future<void> _initMinimalIcons() async {
|
||||
// Icons are loaded dynamically
|
||||
}
|
||||
|
||||
Future<void> _playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
_routeRenderRevision++;
|
||||
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('route_primary_${_routeRenderRevision}_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('route_primary_${_routeRenderRevision}_0'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
update();
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
isPickerShown = false;
|
||||
isPointsPageForRider = false;
|
||||
isBottomSheetShown = false;
|
||||
isCancelRidePageShown = false;
|
||||
isCashConfirmPageShown = false;
|
||||
isPaymentMethodPageShown = false;
|
||||
isRideFinished = false;
|
||||
rideConfirm = false;
|
||||
isMainBottomMenuMap = true;
|
||||
|
||||
isWayPointSheet = false;
|
||||
isWayPointStopsSheet = false;
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
|
||||
heightBottomSheetShown = 0;
|
||||
mainBottomMenuMapHeight = Get.height * 0.22;
|
||||
wayPointSheetHeight = 0;
|
||||
|
||||
markers = <Marker>{};
|
||||
polyLines = <Polyline>{};
|
||||
polylineCoordinates.clear();
|
||||
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
Future<Uint8List> _createWalkMarkerBytes() async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
const double size = 60.0;
|
||||
|
||||
final Paint paint = Paint()..color = const Color(0xFF0288D1);
|
||||
final Paint borderPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.0;
|
||||
|
||||
canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.5, paint);
|
||||
canvas.drawCircle(
|
||||
const Offset(size / 2, size / 2), size / 2.5, borderPaint);
|
||||
|
||||
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
iconPainter.text = TextSpan(
|
||||
text: String.fromCharCode(Icons.directions_walk.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: 30.0,
|
||||
fontFamily: Icons.directions_walk.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
iconPainter.paint(
|
||||
canvas,
|
||||
Offset((size - iconPainter.width) / 2, (size - iconPainter.height) / 2),
|
||||
);
|
||||
|
||||
final ui.Image image = await pictureRecorder
|
||||
.endRecording()
|
||||
.toImage(size.toInt(), size.toInt());
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return data!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
mapController = null;
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'map_socket_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'ui_interactions_controller.dart';
|
||||
import 'ride_simulation.dart';
|
||||
|
||||
class MapScreenBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// 1. WebSocket Controller: Permanent and immediate
|
||||
Get.put(MapSocketController());
|
||||
|
||||
// 2. Core Controllers (initialized when the screen opens or on demand)
|
||||
Get.lazyPut(() => MapEngineController());
|
||||
Get.lazyPut(() => LocationSearchController());
|
||||
Get.lazyPut(() => NearbyDriversController());
|
||||
|
||||
// 3. Lifecycle and UI Interaction Controllers
|
||||
Get.lazyPut(() => RideLifecycleController());
|
||||
Get.lazyPut(() => UiInteractionsController(), fenix: true);
|
||||
|
||||
// 4. Simulation Controller (for development/testing)
|
||||
Get.lazyPut(() => RideSimulationController(), fenix: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as io_client;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart'; // contains global 'box' + 'storage'
|
||||
import '../../../print.dart';
|
||||
import '../../functions/encrypt_decrypt.dart'; // r()
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class MapSocketController extends GetxController {
|
||||
late io_client.Socket socket;
|
||||
bool isSocketConnected = false;
|
||||
bool _isSocketInitialized = false;
|
||||
// يغطّي نافذة الـ await على قراءة الـ JWT: بلا هذا يقدر نداءان متزامنان
|
||||
// (initializeDataAfterLogin + startSearchingForDriver) يفتحان سوكيتين.
|
||||
bool _isConnecting = false;
|
||||
|
||||
/// جاهز لاستقبال الأحداث — أي منضمّ فعلياً لغرفة passenger_{id} على السيرفر.
|
||||
bool get isReadyForEvents => isSocketConnected && _isSocketInitialized;
|
||||
Timer? _heartbeatTimer;
|
||||
DateTime? _lastSocketLocationTime;
|
||||
int _socketLocationUpdatesCount = 0;
|
||||
Timer? _watchdogTimer;
|
||||
|
||||
// 🚌 مواصلاتي — الخط الحالي المشترَك فيه + مستمع تحديث موقع الباص
|
||||
int? _subscribedTransitRouteId;
|
||||
void Function(Map<String, dynamic> data)? onBusLocationUpdate;
|
||||
void Function(String errorCode)? onTransitError;
|
||||
|
||||
DateTime? get lastDriverLocationTime => _lastSocketLocationTime;
|
||||
int get socketLocationUpdatesCount => _socketLocationUpdatesCount;
|
||||
|
||||
/// يقرأ الـ JWT بنفس آلية CRUD._getJwt (SecureStorage أولاً ثم GetStorage).
|
||||
/// passenger_socket.php يرفض أي اتصال بلا jwt، فبدونه لا تصل أي أحداث للراكب.
|
||||
Future<String> _getJwtForSocket() async {
|
||||
try {
|
||||
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
|
||||
if (encryptedJwt != null && encryptedJwt.isNotEmpty) {
|
||||
return r(encryptedJwt).toString().split(Env.addd)[0];
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error reading JWT from SecureStorage for socket: $e');
|
||||
}
|
||||
final String? fallback = box.read(BoxName.jwt);
|
||||
if (fallback != null && fallback.toString().isNotEmpty) {
|
||||
try {
|
||||
return r(fallback).toString().split(Env.addd)[0];
|
||||
} catch (e) {
|
||||
Log.print('Error decrypting fallback JWT for socket: $e');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// يجب أن يُنادى **قبل** إنشاء الرحلة: غرف Socket.IO لا تخزّن الأحداث، فإن
|
||||
/// قبل كابتن قبل أن ينضم الراكب لغرفته فإن ride_status_change يُرمى نهائياً
|
||||
/// ولا يبقى إلا الـ FCM/polling. النداء آمن ومتكرر (idempotent).
|
||||
Future<void> initConnectionWithSocket() async {
|
||||
if (isSocketConnected || _isSocketInitialized || _isConnecting) return;
|
||||
_isConnecting = true;
|
||||
|
||||
String passengerId = box.read(BoxName.passengerID).toString();
|
||||
final String jwt = await _getJwtForSocket();
|
||||
if (jwt.isEmpty) {
|
||||
_isConnecting = false;
|
||||
Log.print(
|
||||
"⚠️ No JWT available — passenger socket would be rejected by the server. Aborting connect.");
|
||||
return;
|
||||
}
|
||||
if (passengerId.isEmpty || passengerId == 'null') {
|
||||
_isConnecting = false;
|
||||
Log.print("⚠️ No passengerID yet — deferring socket connect.");
|
||||
return;
|
||||
}
|
||||
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
|
||||
|
||||
socket = io_client.io(
|
||||
AppLink.serverSocket,
|
||||
io_client.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({'id': passengerId, 'jwt': jwt})
|
||||
// محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart).
|
||||
// كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق
|
||||
// من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال
|
||||
// يعيش من فتح الخريطة لا من إنشاء الرحلة، فالسقف الثابت خطر أكبر الآن.
|
||||
.setReconnectionAttempts(double.infinity)
|
||||
.setReconnectionDelay(2000)
|
||||
.setReconnectionDelayMax(10000)
|
||||
.enableReconnection()
|
||||
.setTimeout(20000)
|
||||
.setExtraHeaders({'Connection': 'Upgrade'})
|
||||
.build(),
|
||||
);
|
||||
_isSocketInitialized = true;
|
||||
_isConnecting = false;
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
Log.print("✅ Socket Connected Successfully");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after connect");
|
||||
}
|
||||
if (_subscribedTransitRouteId != null) {
|
||||
socket.emit('subscribe_transit_route', {'route_id': _subscribedTransitRouteId});
|
||||
Log.print("🚌 Re-subscribed to transit route after connect");
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
|
||||
isSocketConnected = false;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
|
||||
rideLifecycle.startMasterTimerWithInterval(4);
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnect((_) {
|
||||
Log.print("🔁 Socket Reconnected Successfully!");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after reconnect");
|
||||
}
|
||||
if (_subscribedTransitRouteId != null) {
|
||||
socket.emit('subscribe_transit_route', {'route_id': _subscribedTransitRouteId});
|
||||
Log.print("🚌 Re-subscribed to transit route after reconnect");
|
||||
}
|
||||
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
|
||||
rideLifecycle.cancelMasterTimer();
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnectAttempt((attemptNumber) {
|
||||
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
|
||||
});
|
||||
|
||||
socket.onError((error) {
|
||||
Log.print("❌ Socket Error: $error");
|
||||
isSocketConnected = false;
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) {
|
||||
Log.print("❌ Socket Connect Error: $error");
|
||||
isSocketConnected = false;
|
||||
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
|
||||
// نتأكد يدوياً من إعادة الاتصال
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (!isSocketConnected && _isSocketInitialized) {
|
||||
Log.print("🔄 Manual reconnect after connect_error...");
|
||||
try {
|
||||
socket.connect();
|
||||
} catch (e) {
|
||||
Log.print("Manual reconnect error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('ride_status_change', (data) {
|
||||
Log.print("📩 Socket Event: ride_status_change -> $data");
|
||||
_handleRideStatusChangeWithSocket(data);
|
||||
});
|
||||
|
||||
socket.on('driver_location_update', (data) {
|
||||
handleDriverLocationUpdate(data);
|
||||
});
|
||||
|
||||
// 🚌 مواصلاتي — بثّ موقع الباص الحي لخط مشترَك فيه
|
||||
socket.on('bus_location_update', (data) {
|
||||
if (data == null) return;
|
||||
try {
|
||||
final map = Map<String, dynamic>.from(data as Map);
|
||||
onBusLocationUpdate?.call(map);
|
||||
} catch (e) {
|
||||
Log.print('Error parsing bus_location_update: $e');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('transit_error', (data) {
|
||||
if (data == null) return;
|
||||
try {
|
||||
final map = Map<String, dynamic>.from(data as Map);
|
||||
final code = map['code']?.toString() ?? '';
|
||||
Log.print('⚠️ transit_error: $code');
|
||||
onTransitError?.call(code);
|
||||
} catch (e) {
|
||||
Log.print('Error parsing transit_error: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// يوصل السوكيت وينتظر انضمامه فعلياً لغرفة الراكب، بسقف زمني.
|
||||
/// يُنادى قبل إنشاء الرحلة حتى لا يقبل كابتن والراكب ما زال خارج غرفته
|
||||
/// (الحدث يُفقد نهائياً في تلك الحالة — لا طابور في غرف Socket.IO).
|
||||
/// لا يُفشل إنشاء الرحلة أبداً: عند انتهاء المهلة نكمل ونتّكل على
|
||||
/// الـ FCM/polling كشبكة احتياطية.
|
||||
/// آخر مرة فشل فيها الاتصال — حتى لا نُبطئ كل طلب رحلة بانتظارٍ عقيم إذا كان
|
||||
/// السوكيت غير قابل للوصول أصلاً (منفذ مغلق/TLS غير مُنهى/سيرفر واقف).
|
||||
static DateTime? _lastConnectFailure;
|
||||
static const Duration _failureCooldown = Duration(minutes: 2);
|
||||
|
||||
Future<bool> ensureConnectedBeforeRide({
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
}) async {
|
||||
if (isReadyForEvents) return true;
|
||||
|
||||
final lastFail = _lastConnectFailure;
|
||||
if (lastFail != null &&
|
||||
DateTime.now().difference(lastFail) < _failureCooldown) {
|
||||
// فشل قريباً — نبدأ محاولة الاتصال في الخلفية ولا نُعطّل الطلب إطلاقاً
|
||||
initConnectionWithSocket();
|
||||
Log.print(
|
||||
"⏭️ Skipping socket wait (recent failure). Ride proceeds on FCM/polling.");
|
||||
return false;
|
||||
}
|
||||
|
||||
await initConnectionWithSocket();
|
||||
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (!isSocketConnected && DateTime.now().isBefore(deadline)) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
if (isSocketConnected) {
|
||||
_lastConnectFailure = null;
|
||||
Log.print("✅ Socket ready before ride creation.");
|
||||
return true;
|
||||
}
|
||||
_lastConnectFailure = DateTime.now();
|
||||
Log.print(
|
||||
"⚠️ Socket not ready within ${timeout.inMilliseconds}ms — continuing; FCM/polling will cover.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── مواصلاتي: اشتراك/إلغاء اشتراك ببثّ موقع خط ─────────────
|
||||
// يُستدعى عند فتح/إغلاق شاشة تتبع الباص الحي. يضمن السوكيت متصلاً أولاً.
|
||||
Future<void> subscribeToTransitRoute(int routeId) async {
|
||||
_subscribedTransitRouteId = routeId;
|
||||
if (!isSocketConnected) {
|
||||
await initConnectionWithSocket();
|
||||
// سيُعاد الاشتراك تلقائياً من onConnect إن أضفنا ذلك، لكن نحاول فوراً أيضاً
|
||||
}
|
||||
// socket هو late — لو انسحب initConnectionWithSocket (بلا JWT مثلاً) يبقى
|
||||
// غير مُهيّأ، فقراءته مباشرة ترمي LateInitializationError.
|
||||
if (_isSocketInitialized && socket.connected) {
|
||||
socket.emit('subscribe_transit_route', {'route_id': routeId});
|
||||
Log.print('🚌 Subscribed to transit route #$routeId');
|
||||
}
|
||||
}
|
||||
|
||||
void unsubscribeFromTransitRoute(int routeId) {
|
||||
if (_subscribedTransitRouteId == routeId) _subscribedTransitRouteId = null;
|
||||
if (_isSocketInitialized && isSocketConnected && socket.connected) {
|
||||
socket.emit('unsubscribe_transit_route', {'route_id': routeId});
|
||||
Log.print('🚌 Unsubscribed from transit route #$routeId');
|
||||
}
|
||||
}
|
||||
|
||||
/// ⚠️ زائدة عن الحاجة عملياً: Socket.IO يعمل ping/pong على مستوى البروتوكول،
|
||||
/// ومستمع 'heartbeat' في passenger_socket.php فارغ ولا يفعل شيئاً. أبقيناها
|
||||
/// بفاصل 30ث بدل 15ث لتنصيف حركتها — الاتصال صار يعيش من فتح الخريطة لا من
|
||||
/// إنشاء الرحلة، فعدد الاتصالات الخاملة أعلى بكثير. حذفها كاملاً ممكن لاحقاً
|
||||
/// بعد التأكد أن لا شيء على السيرفر يعتمد عليها لقياس الحضور.
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
if (isSocketConnected && socket.connected) {
|
||||
socket.emit('heartbeat',
|
||||
{'passenger_id': box.read(BoxName.passengerID).toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool isSocketHealthy() {
|
||||
if (!isSocketConnected) return false;
|
||||
if (_lastSocketLocationTime == null) return false;
|
||||
final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds;
|
||||
return diff < 20;
|
||||
}
|
||||
|
||||
void _handleRideStatusChangeWithSocket(dynamic data) {
|
||||
if (data == null || data['status'] == null) return;
|
||||
|
||||
String newStatus = data['status'].toString().toLowerCase();
|
||||
Log.print("🔔 Socket Status Update: $newStatus");
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
Map<String, dynamic>? driverInfo;
|
||||
if (data['driver_info'] != null && data['driver_info'] is Map) {
|
||||
driverInfo = Map<String, dynamic>.from(data['driver_info']);
|
||||
}
|
||||
|
||||
switch (newStatus) {
|
||||
case 'accepted':
|
||||
case 'apply':
|
||||
case 'applied':
|
||||
rideLifecycle.processRideAcceptance(
|
||||
driverData: driverInfo, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'arrived':
|
||||
rideLifecycle.processDriverArrival("Socket");
|
||||
break;
|
||||
|
||||
case 'started':
|
||||
case 'begin':
|
||||
rideLifecycle.processRideBegin(source: "Socket");
|
||||
break;
|
||||
|
||||
case 'finished':
|
||||
case 'ended':
|
||||
_onRideFinishedWithSocket(data);
|
||||
break;
|
||||
|
||||
// cancel_ride_by_driver.php يبعث 'cancelled_by_driver' حرفياً،
|
||||
// و cancel_ride_by_passenger.php يبعث 'cancelled_by_passenger'.
|
||||
// كانت الحالتان تسقطان من الـ switch فيبقى الراكب معلّقاً على الشاشة.
|
||||
case 'cancelled':
|
||||
case 'cancelled_by_driver':
|
||||
case 'canceled_by_driver':
|
||||
rideLifecycle.processRideCancelledByDriver(data, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'no_drivers_found':
|
||||
rideLifecycle.showNoDriverDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onRideFinishedWithSocket(dynamic data) {
|
||||
Log.print("🏁 Ride Finished (Socket)");
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
var rawList = data['DriverList'];
|
||||
List<dynamic> listToSend = [];
|
||||
|
||||
if (rawList != null) {
|
||||
if (rawList is List) {
|
||||
listToSend = rawList;
|
||||
} else if (rawList is String) {
|
||||
try {
|
||||
listToSend = jsonDecode(rawList);
|
||||
} catch (e) {
|
||||
Log.print("Error decoding DriverList: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listToSend.isEmpty && data['price'] != null) {
|
||||
listToSend = [
|
||||
rideLifecycle.driverId,
|
||||
rideLifecycle.rideId,
|
||||
rideLifecycle.driverToken,
|
||||
data['price'].toString()
|
||||
];
|
||||
}
|
||||
|
||||
rideLifecycle.processRideFinished(listToSend, source: "Socket");
|
||||
}
|
||||
|
||||
void handleDriverLocationUpdate(dynamic data) {
|
||||
if (!isSocketConnected || data == null) return;
|
||||
_lastSocketLocationTime = DateTime.now();
|
||||
_socketLocationUpdatesCount++;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.driverId.isEmpty &&
|
||||
(data['driver_id'] ?? data['driverId']) != null) {
|
||||
rideLifecycle.driverId =
|
||||
(data['driver_id'] ?? data['driverId']).toString();
|
||||
}
|
||||
|
||||
if (_socketLocationUpdatesCount >= 3 &&
|
||||
rideLifecycle.locationPollingTimer != null) {
|
||||
Log.print("✅ Socket delivering locations reliably. Stopping polling.");
|
||||
rideLifecycle.stopDriverLocationPolling();
|
||||
}
|
||||
|
||||
try {
|
||||
double lat = double.tryParse(
|
||||
(data['latitude'] ?? data['lat'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double lng = double.tryParse(
|
||||
(data['longitude'] ?? data['lng'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
|
||||
|
||||
if (lat == 0 || lng == 0) return;
|
||||
|
||||
LatLng newPos = LatLng(lat, lng);
|
||||
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos);
|
||||
} else {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos;
|
||||
}
|
||||
|
||||
double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0;
|
||||
rideLifecycle.checkAndRecalculateIfDeviated(
|
||||
newPos,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
);
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (mapEngine.mapController != null) {
|
||||
double zoom = 16.5;
|
||||
if (speed > 0) {
|
||||
zoom = 17.0 - ((speed - 10) / 70) * 2.5;
|
||||
zoom = zoom.clamp(14.5, 17.0);
|
||||
}
|
||||
mapEngine.mapController!
|
||||
.animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom));
|
||||
}
|
||||
|
||||
final dynamic distanceValue =
|
||||
data['distance_m'] ?? data['distance_meters'];
|
||||
final double? distanceMeters =
|
||||
double.tryParse(distanceValue?.toString() ?? '');
|
||||
final int? etaSeconds = data['eta_seconds'] == null
|
||||
? null
|
||||
: int.tryParse(data['eta_seconds'].toString());
|
||||
final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) ||
|
||||
(distanceMeters != null && distanceMeters > 0);
|
||||
if (hasServerMetrics) {
|
||||
rideLifecycle.updateDriverRouteMetrics(
|
||||
etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null,
|
||||
distanceMeters: distanceMeters,
|
||||
);
|
||||
}
|
||||
|
||||
rideLifecycle.updateDriverMarker(newPos, heading);
|
||||
rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics);
|
||||
rideLifecycle.update();
|
||||
} catch (e) {
|
||||
Log.print('Error in handleDriverLocationUpdate: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void disposeRideSocket() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_watchdogTimer?.cancel();
|
||||
if (_isSocketInitialized) {
|
||||
socket.disconnect();
|
||||
socket.dispose();
|
||||
isSocketConnected = false;
|
||||
_isSocketInitialized = false;
|
||||
Log.print("🔌 Socket Disposed");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
disposeRideSocket();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' show Random, atan2, cos, pi, pow, sin, sqrt;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../../../models/model/locations.dart';
|
||||
import 'car_location.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
class NearbyDriversController extends GetxController {
|
||||
List carsLocationByPassenger = [];
|
||||
List<LatLng> driverCarsLocationToPassengerAfterApplied = [];
|
||||
List<CarLocationModel> carLocationsModels = [];
|
||||
String? currentDriverMarkerId;
|
||||
bool lowPerf = false;
|
||||
|
||||
dynamic dataCarsLocationByPassenger;
|
||||
bool noCarString = false;
|
||||
final double minMovementThreshold = 2.0;
|
||||
final Map<String, Timer> _animationTimers = {};
|
||||
final List<Map<String, dynamic>> fakeCarData = [];
|
||||
|
||||
Future<bool> getCarsLocationByPassengerAndReloadMarker() async {
|
||||
carsLocationByPassenger = [];
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.passengerLocation.latitude == 0 && locSearch.passengerLocation.longitude == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getCarsLocationByPassenger,
|
||||
payload: {
|
||||
'lat': locSearch.passengerLocation.latitude.toString(),
|
||||
'lng': locSearch.passengerLocation.longitude.toString(),
|
||||
'radius': '5',
|
||||
'limit': '50',
|
||||
},
|
||||
);
|
||||
|
||||
if (res == 'failure' || res is! Map) {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
|
||||
noCarString = false;
|
||||
var responseData = res;
|
||||
dataCarsLocationByPassenger = responseData;
|
||||
|
||||
List driversList = [];
|
||||
if (responseData['status'] == true && responseData['data'] != null) {
|
||||
driversList = responseData['data'];
|
||||
} else if (responseData['message'] != null) {
|
||||
driversList = responseData['message'];
|
||||
}
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
|
||||
if (driversList.isEmpty) {
|
||||
carsLocationByPassenger.clear();
|
||||
mapEngine.update();
|
||||
return false;
|
||||
}
|
||||
|
||||
carsLocationByPassenger.clear();
|
||||
|
||||
for (var i = 0; i < driversList.length; i++) {
|
||||
var carData = driversList[i];
|
||||
|
||||
double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0;
|
||||
double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0;
|
||||
double heading = double.tryParse(carData['heading'].toString()) ?? 0.0;
|
||||
|
||||
if (lat == 0.0 || lng == 0.0) continue;
|
||||
|
||||
String driverId = (carData['driver_id'] ?? carData['id'] ?? '').toString();
|
||||
if (driverId.isEmpty || driverId == 'null') continue;
|
||||
|
||||
_updateOrCreateMarker(
|
||||
driverId,
|
||||
LatLng(lat, lng),
|
||||
heading,
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _addFakeCarMarkers(LatLng center, int count) {
|
||||
if (fakeCarData.isEmpty) {
|
||||
Random random = Random();
|
||||
double radiusInKm = 2.5;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
double angle = random.nextDouble() * 2 * pi;
|
||||
double distance = sqrt(random.nextDouble()) * radiusInKm;
|
||||
|
||||
double latOffset = (distance / 111.32);
|
||||
double lonOffset =
|
||||
(distance / (111.32 * cos(center.latitude * pi / 180.0)));
|
||||
|
||||
double lat = center.latitude + (latOffset * cos(angle));
|
||||
double lon = center.longitude + (lonOffset * sin(angle));
|
||||
|
||||
double heading = random.nextDouble() * 360;
|
||||
|
||||
fakeCarData.add({
|
||||
'id': 'fake_$i',
|
||||
'latitude': lat,
|
||||
'longitude': lon,
|
||||
'heading': heading,
|
||||
'gender': 'Male',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (var carData in fakeCarData) {
|
||||
_updateOrCreateMarker(
|
||||
carData['id'].toString(),
|
||||
LatLng(carData['latitude'], carData['longitude']),
|
||||
carData['heading'],
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addFakeCarMarkers(LatLng center, int count) {
|
||||
_addFakeCarMarkers(center, count);
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocation() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (!rideLife.rideConfirm) {
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null &&
|
||||
dataCarsLocationByPassenger['message'].length > 0) {
|
||||
double nearestDistance = double.infinity;
|
||||
CarLocation? nearestCar;
|
||||
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
try {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
locSearch.passengerLocation.latitude,
|
||||
locSearch.passengerLocation.longitude,
|
||||
double.parse(carLocation['latitude']),
|
||||
double.parse(carLocation['longitude']),
|
||||
);
|
||||
|
||||
int durationToPassenger = (distance / 1000 / 25 * 3600).round();
|
||||
update();
|
||||
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance,
|
||||
duration: durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error calculating distance/duration: $e');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocationAPIGOOGLE() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (mapEngine.polyLines.isEmpty || rideLife.totalCostPassenger == 0) {
|
||||
return null;
|
||||
}
|
||||
if (!rideLife.rideConfirm) {
|
||||
double nearestDistance = double.infinity;
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null) {
|
||||
if (dataCarsLocationByPassenger['message'].length > 0) {
|
||||
CarLocation? nearestCar;
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
update();
|
||||
String apiUrl =
|
||||
'${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}';
|
||||
var response = await CRUD().getGoogleApi(link: apiUrl, payload: {});
|
||||
if (response != null && response['status'] == "OK") {
|
||||
var data = response;
|
||||
int distance1 =
|
||||
data['rows'][0]['elements'][0]['distance']['value'];
|
||||
rideLife.distanceByPassenger =
|
||||
data['rows'][0]['elements'][0]['distance']['text'];
|
||||
rideLife.durationToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['value'];
|
||||
|
||||
Duration durationFromDriverToPassenger =
|
||||
Duration(seconds: rideLife.durationToPassenger.toInt());
|
||||
rideLife.stringRemainingTimeToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['text'];
|
||||
update();
|
||||
if (distance1 < nearestDistance) {
|
||||
nearestDistance = distance1.toDouble();
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance1.toDouble(),
|
||||
duration: rideLife.durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
Log.print('${response?['status']}: error Google distance matrix');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future getCarForFirstConfirm(String carType) async {
|
||||
bool foundCars = false;
|
||||
int attempt = 0;
|
||||
|
||||
Timer.periodic(const Duration(seconds: 4), (Timer t) async {
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
t.cancel();
|
||||
} else if (attempt >= 4) {
|
||||
t.cancel();
|
||||
if (!foundCars) {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
}
|
||||
update();
|
||||
}
|
||||
attempt++;
|
||||
});
|
||||
}
|
||||
|
||||
void startCarLocationSearch(String carType) {
|
||||
int searchInterval = 5;
|
||||
Log.print('searchInterval: $searchInterval');
|
||||
int boundIncreaseStep = 2500;
|
||||
Log.print('boundIncreaseStep: $boundIncreaseStep');
|
||||
int maxAttempts = 3;
|
||||
int maxBoundIncreaseStep = 6000;
|
||||
int attempt = 0;
|
||||
Log.print('initial attempt: $attempt');
|
||||
|
||||
Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async {
|
||||
Log.print('Current attempt: $attempt');
|
||||
bool foundCars = false;
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (attempt >= maxAttempts) {
|
||||
timer.cancel();
|
||||
if (foundCars == false) {
|
||||
noCarString = true;
|
||||
update();
|
||||
}
|
||||
} else if (mapEngine.reloadStartApp == true) {
|
||||
Log.print('reloadStartApp: ${mapEngine.reloadStartApp}');
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
attempt++;
|
||||
Log.print('Incrementing attempt to: $attempt');
|
||||
|
||||
if (boundIncreaseStep < maxBoundIncreaseStep) {
|
||||
boundIncreaseStep += 1500;
|
||||
if (boundIncreaseStep > maxBoundIncreaseStep) {
|
||||
boundIncreaseStep = maxBoundIncreaseStep;
|
||||
}
|
||||
Log.print('New boundIncreaseStep: $boundIncreaseStep');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _getIconForCar(Map<String, dynamic> carData) {
|
||||
if (carData['model'].toString().contains('دراجة')) {
|
||||
return 'moto_icon';
|
||||
} else if (carData['gender'] == 'Female') {
|
||||
return 'lady_icon';
|
||||
} else {
|
||||
return 'car_icon';
|
||||
}
|
||||
}
|
||||
|
||||
void _updateOrCreateMarker(
|
||||
String markerId, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (!mapEngine.isIconsLoaded) {
|
||||
Log.print("⚠️ Skipping drawing marker $markerId because map icons are not fully loaded yet.");
|
||||
return;
|
||||
}
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = mapEngine.markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker == null) {
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
mapEngine.update();
|
||||
} else {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
existingMarker.position.latitude,
|
||||
existingMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
if (distance >= minMovementThreshold) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
);
|
||||
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapEngine.mapController != null) {
|
||||
mapEngine.mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
double deltaLon = lon2 - lon1;
|
||||
double y = sin(deltaLon) * cos(lat2);
|
||||
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon);
|
||||
double bearing = atan2(y, x);
|
||||
return (bearing * 180 / pi + 360) % 360;
|
||||
}
|
||||
|
||||
void analyzeBehavior(Position currentPosition, List<LatLng> routePoints) {
|
||||
double actualBearing = currentPosition.heading;
|
||||
double expectedBearing = calculateBearing(
|
||||
routePoints[0].latitude,
|
||||
routePoints[0].longitude,
|
||||
routePoints[1].latitude,
|
||||
routePoints[1].longitude,
|
||||
);
|
||||
|
||||
double bearingDifference = (expectedBearing - actualBearing).abs();
|
||||
if (bearingDifference > 30) {
|
||||
Log.print("⚠️ السائق انحرف عن المسار!");
|
||||
}
|
||||
}
|
||||
|
||||
void detectStops(Position currentPosition) {
|
||||
if (currentPosition.speed < 0.5) {
|
||||
Log.print("🚦 السائق توقف في موقع غير متوقع!");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> detectPerfMode() async {
|
||||
try {
|
||||
if (GetPlatform.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
final sdk = info.version.sdkInt;
|
||||
final ram = info.availableRamSize;
|
||||
lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024);
|
||||
} else {
|
||||
lowPerf = false;
|
||||
}
|
||||
} catch (_) {
|
||||
lowPerf = false;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,438 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' show cos, pi, sin;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../print.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'map_socket_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'ride_state.dart';
|
||||
import 'location_search_controller.dart';
|
||||
|
||||
class RideSimulationController extends GetxController {
|
||||
Timer? _simulationTimer;
|
||||
int _step = 0;
|
||||
bool _isSimulating = false;
|
||||
|
||||
List<LatLng> _simulatedRoute = [];
|
||||
int _driverRouteIndex = 0;
|
||||
|
||||
String get simulationStatus => _isSimulating
|
||||
? 'Simulating... Step $_step'
|
||||
: 'Idle';
|
||||
|
||||
// آمود نقاط المسار (Jordan area: Amman center ~ 31.95, 35.91)
|
||||
static const double _centerLat = 31.95;
|
||||
static const double _centerLng = 35.91;
|
||||
static const double _routeDelta = 0.03;
|
||||
static const int _routePointCount = 100;
|
||||
|
||||
void _generateRoutePoints() {
|
||||
_simulatedRoute = [];
|
||||
for (int i = 0; i < _routePointCount; i++) {
|
||||
double fraction = i / (_routePointCount - 1);
|
||||
double lat = _centerLat + fraction * _routeDelta;
|
||||
double lng = _centerLng + fraction * _routeDelta +
|
||||
0.005 * sin(fraction * pi * 2);
|
||||
_simulatedRoute.add(LatLng(lat, lng));
|
||||
}
|
||||
}
|
||||
|
||||
void startFullSimulation() {
|
||||
if (_isSimulating) return;
|
||||
_isSimulating = true;
|
||||
_step = 0;
|
||||
_driverRouteIndex = 0;
|
||||
_generateRoutePoints();
|
||||
|
||||
Log.print('=' * 70);
|
||||
Log.print('🚀 RIDE SIMULATION STARTED — Full lifecycle test');
|
||||
Log.print('=' * 70);
|
||||
|
||||
_simulationTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||
switch (_step) {
|
||||
case 0:
|
||||
_simulateSearchingState();
|
||||
break;
|
||||
case 1:
|
||||
_simulateDriverAppliedState();
|
||||
break;
|
||||
case 2:
|
||||
_simulateDriverEnRoute(isFirst: true);
|
||||
break;
|
||||
case 3:
|
||||
_simulateDriverEnRoute(isFirst: false);
|
||||
break;
|
||||
case 4:
|
||||
_simulateDriverArrivedState();
|
||||
break;
|
||||
case 5:
|
||||
_simulateRideBeginState();
|
||||
break;
|
||||
case 6:
|
||||
_simulateRideInProgress();
|
||||
break;
|
||||
case 7:
|
||||
_simulateRideFinishedState();
|
||||
timer.cancel();
|
||||
_isSimulating = false;
|
||||
Log.print('=' * 70);
|
||||
Log.print('✅ RIDE SIMULATION COMPLETED');
|
||||
Log.print('=' * 70);
|
||||
break;
|
||||
}
|
||||
_step++;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void stopSimulation() {
|
||||
_simulationTimer?.cancel();
|
||||
_simulationTimer = null;
|
||||
_isSimulating = false;
|
||||
_step = 0;
|
||||
_driverRouteIndex = 0;
|
||||
Log.print('🛑 Simulation stopped');
|
||||
update();
|
||||
}
|
||||
|
||||
void _logPolylineState(String stage) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
Log.print('');
|
||||
Log.print('╔══ Polyline State [$stage] ══╗');
|
||||
if (mapEngine.polyLines.isEmpty) {
|
||||
Log.print(' ║ (no polylines)');
|
||||
}
|
||||
for (final p in mapEngine.polyLines) {
|
||||
String colorStr;
|
||||
if (p.color == Colors.amber) {
|
||||
colorStr = 'AMBER';
|
||||
} else if (p.color.value == 0xFF2196F3) {
|
||||
colorStr = 'BLUE';
|
||||
} else if (p.color == Colors.blueGrey) {
|
||||
colorStr = 'BLUE_GREY';
|
||||
} else {
|
||||
colorStr = '#${p.color.value.toRadixString(16).padLeft(8, '0')}';
|
||||
}
|
||||
Log.print(' ║ • ${p.polylineId.value}: ${p.points.length} pts, '
|
||||
'color: $colorStr, w: ${p.width}');
|
||||
}
|
||||
Log.print('╠══ Markers [$stage] ══╣');
|
||||
if (mapEngine.markers.isEmpty) {
|
||||
Log.print(' ║ (no markers)');
|
||||
}
|
||||
for (final m in mapEngine.markers) {
|
||||
Log.print(' ║ • ${m.markerId.value}: '
|
||||
'(${m.position.latitude.toStringAsFixed(5)}, '
|
||||
'${m.position.longitude.toStringAsFixed(5)}), '
|
||||
'heading: ${m.rotation.toStringAsFixed(1)}');
|
||||
}
|
||||
Log.print('╚══════════════════════╝');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _verifyPolylineCondition(bool condition, String description) {
|
||||
if (!condition) {
|
||||
Log.print(' ❌ VERIFY FAILED: $description');
|
||||
} else {
|
||||
Log.print(' ✅ VERIFY PASSED: $description');
|
||||
}
|
||||
}
|
||||
|
||||
LatLng _interpolateRoutePoint(double fraction) {
|
||||
if (_simulatedRoute.isEmpty) return LatLng(_centerLat, _centerLng);
|
||||
int total = _simulatedRoute.length;
|
||||
double exactIdx = fraction * (total - 1);
|
||||
int idx = exactIdx.floor();
|
||||
if (idx >= total - 1) return _simulatedRoute.last;
|
||||
double t = exactIdx - idx;
|
||||
return LatLng(
|
||||
_simulatedRoute[idx].latitude +
|
||||
t * (_simulatedRoute[idx + 1].latitude - _simulatedRoute[idx].latitude),
|
||||
_simulatedRoute[idx].longitude +
|
||||
t * (_simulatedRoute[idx + 1].longitude - _simulatedRoute[idx].longitude),
|
||||
);
|
||||
}
|
||||
|
||||
void _simulateSearchingState() {
|
||||
Log.print('🟡 [Step $_step] STATE: SEARCHING');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
ride.currentRideState.value = RideState.searching;
|
||||
ride.rideId = 'sim_ride_001';
|
||||
ride.isSearchingWindow = true;
|
||||
|
||||
// آمود موقع الراكب
|
||||
LatLng passengerLoc = _simulatedRoute.isNotEmpty
|
||||
? _simulatedRoute.first
|
||||
: LatLng(_centerLat, _centerLng);
|
||||
locSearch.passengerLocation = passengerLoc;
|
||||
ride.passengerLocation = passengerLoc;
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('searching');
|
||||
_verifyPolylineCondition(
|
||||
Get.find<MapEngineController>().polyLines.isEmpty,
|
||||
'No route polylines during searching');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateDriverAppliedState() {
|
||||
Log.print('🟢 [Step $_step] STATE: DRIVER APPLIED');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
ride.currentRideState.value = RideState.driverApplied;
|
||||
ride.statusRide = 'Apply';
|
||||
ride.isSearchingWindow = false;
|
||||
|
||||
// آمود معلومات السائق
|
||||
ride.driverId = 'sim_driver_001';
|
||||
ride.driverName = 'Simulated Captain';
|
||||
ride.make = 'Toyota';
|
||||
ride.model = 'Corolla';
|
||||
ride.carColor = 'White';
|
||||
ride.licensePlate = 'SIM-123';
|
||||
ride.driverRate = '4.8';
|
||||
ride.driverPhone = '+9627XXXXXXXX';
|
||||
ride.driverToken = 'sim_token_001';
|
||||
|
||||
// آمود مواقع البداية والنهاية
|
||||
ride.passengerLocation = _simulatedRoute.first;
|
||||
ride.myDestination = _simulatedRoute.last;
|
||||
locSearch.passengerLocation = _simulatedRoute.first;
|
||||
locSearch.myDestination = _simulatedRoute.last;
|
||||
|
||||
// محاكاة موقع السائق (45% على طول المسار)
|
||||
LatLng driverPos = _interpolateRoutePoint(0.45);
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied = [driverPos];
|
||||
|
||||
// رسم مسار السائق إلى الراكب
|
||||
ride.calculateDriverToPassengerRoute(driverPos, ride.passengerLocation);
|
||||
|
||||
// وضع ماركر السائق على الخريطة
|
||||
ride.updateDriverMarker(driverPos, 45.0);
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('driver_applied');
|
||||
|
||||
// التحقق من وجود الخطوط المطلوبة
|
||||
final polylines = Get.find<MapEngineController>().polyLines;
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value == 'driver_route_solid'),
|
||||
'driver_route_solid (amber solid line) exists');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value.startsWith('passenger_walk_line')),
|
||||
'passenger_walk_line (dashed walk line) exists');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.color == Colors.amber),
|
||||
'Amber-colored polyline exists (driver route)');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.color == Colors.blueGrey),
|
||||
'BlueGrey-colored polyline exists (walk dashes)');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateDriverEnRoute({required bool isFirst}) {
|
||||
String label = isFirst ? '1' : '2';
|
||||
Log.print('🔵 [Step $_step] STATE: DRIVER APPROACHING (update #$label)');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
|
||||
_driverRouteIndex += 18;
|
||||
if (_driverRouteIndex > _routePointCount - 1) {
|
||||
_driverRouteIndex = _routePointCount - 1;
|
||||
}
|
||||
|
||||
LatLng newPos = _simulatedRoute[_driverRouteIndex];
|
||||
|
||||
// تحديث موقع السائق
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied = [newPos];
|
||||
|
||||
ride.updateDriverMarker(newPos, 50.0);
|
||||
ride.updateRemainingRoute(newPos);
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('driver_en_route_$label');
|
||||
|
||||
// التحقق: driver_route_solid يبقى موجود ويتقلص
|
||||
final polylines = Get.find<MapEngineController>().polyLines;
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value == 'driver_route_solid'),
|
||||
'driver_route_solid still exists (shrinking)');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value.startsWith('passenger_walk_line')),
|
||||
'passenger_walk_line still exists');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateDriverArrivedState() {
|
||||
Log.print('🟣 [Step $_step] STATE: DRIVER ARRIVED');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
|
||||
ride.currentRideState.value = RideState.driverArrived;
|
||||
ride.statusRide = 'Arrived';
|
||||
|
||||
// محاكاة وصول السائق: تحديد الموقع كآخر نقطة طريق
|
||||
LatLng driverPos = _simulatedRoute.first;
|
||||
|
||||
// تطبيق arrived logic
|
||||
ride.polyLines = ride.polyLines
|
||||
.where((p) =>
|
||||
p.polylineId.value != 'main_route' &&
|
||||
p.polylineId.value != 'route_direct' &&
|
||||
!p.polylineId.value.startsWith('driver_route'))
|
||||
.toSet();
|
||||
|
||||
// رسم المسار الجديد (من السائق إلى الوجهة النهائية)
|
||||
ride.calculateDriverToPassengerRoute(driverPos, ride.myDestination,
|
||||
isBeginPhase: true);
|
||||
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('driver_arrived');
|
||||
|
||||
final polylines = Get.find<MapEngineController>().polyLines;
|
||||
// في حالة الوصول: يجب أن يظهر main_route (أزرق) بدلاً من driver_route_solid
|
||||
_verifyPolylineCondition(
|
||||
!polylines.any((p) => p.polylineId.value == 'driver_route_solid'),
|
||||
'driver_route_solid (amber) has been cleared');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value == 'main_route'),
|
||||
'main_route (blue) has been drawn to destination');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateRideBeginState() {
|
||||
Log.print('🔴 [Step $_step] STATE: RIDE IN PROGRESS (Begin)');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
|
||||
ride.currentRideState.value = RideState.inProgress;
|
||||
ride.statusRide = 'Begin';
|
||||
|
||||
LatLng driverPos = _simulatedRoute.first;
|
||||
|
||||
// محاكاة processRideBegin: مسح الخطوط القديمة ورسم الخط الجديد
|
||||
ride.polyLines = ride.polyLines
|
||||
.where((p) =>
|
||||
p.polylineId.value != 'main_route' &&
|
||||
p.polylineId.value != 'route_direct' &&
|
||||
!p.polylineId.value.startsWith('driver_route'))
|
||||
.toSet();
|
||||
|
||||
ride.calculateDriverToPassengerRoute(driverPos, ride.myDestination,
|
||||
isBeginPhase: true);
|
||||
|
||||
ride.rideIsBeginPassengerTimer();
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('ride_begin');
|
||||
|
||||
final polylines = Get.find<MapEngineController>().polyLines;
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value == 'main_route'),
|
||||
'main_route (blue) exists for trip');
|
||||
_verifyPolylineCondition(
|
||||
!polylines.any((p) => p.polylineId.value.startsWith('driver_route')),
|
||||
'No driver_route* polylines remain');
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.color.value == 0xFF2196F3),
|
||||
'Blue polyline (main route) is present');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateRideInProgress() {
|
||||
Log.print('📱 [Step $_step] STATE: CAR MOVING (remaining route update)');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
|
||||
_driverRouteIndex += 30;
|
||||
if (_driverRouteIndex > _routePointCount - 1) {
|
||||
_driverRouteIndex = _routePointCount - 1;
|
||||
}
|
||||
|
||||
LatLng newPos = _simulatedRoute[_driverRouteIndex];
|
||||
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied = [newPos];
|
||||
|
||||
ride.updateDriverMarker(newPos, 70.0);
|
||||
ride.updateRemainingRoute(newPos, updateEta: true);
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('ride_in_progress');
|
||||
|
||||
final polylines = Get.find<MapEngineController>().polyLines;
|
||||
_verifyPolylineCondition(
|
||||
polylines.any((p) => p.polylineId.value == 'main_route'),
|
||||
'main_route (blue) exists with remaining points');
|
||||
_verifyPolylineCondition(
|
||||
!polylines.any((p) => p.polylineId.value.startsWith('driver_route')),
|
||||
'No driver_route* polylines remain');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
void _simulateRideFinishedState() {
|
||||
Log.print('🏁 [Step $_step] STATE: RIDE FINISHED');
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
|
||||
ride.currentRideState.value = RideState.finished;
|
||||
ride.statusRide = 'Finished';
|
||||
ride.isSearchingWindow = false;
|
||||
|
||||
ride.stopAllTimers();
|
||||
mapEngine.clearPolyline();
|
||||
mapEngine.markers = {};
|
||||
ride.update();
|
||||
|
||||
_logPolylineState('ride_finished');
|
||||
|
||||
_verifyPolylineCondition(
|
||||
Get.find<MapEngineController>().polyLines.isEmpty,
|
||||
'All polylines cleared');
|
||||
_verifyPolylineCondition(
|
||||
Get.find<MapEngineController>().markers.isEmpty,
|
||||
'All markers removed');
|
||||
Log.print('');
|
||||
}
|
||||
|
||||
// Simulate a single WebSocket location update event
|
||||
void simulateSocketLocationUpdate(LatLng position, double heading) {
|
||||
final ride = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
final mapSocket = Get.find<MapSocketController>();
|
||||
|
||||
Log.print('📡 Simulating socket location update: '
|
||||
'(${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)})');
|
||||
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied = [position];
|
||||
ride.driverCarsLocationToPassengerAfterApplied = [position];
|
||||
|
||||
ride.checkAndRecalculateIfDeviated(
|
||||
position,
|
||||
heading: heading,
|
||||
speed: 30.0,
|
||||
);
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (mapEngine.mapController != null) {
|
||||
mapEngine.mapController!
|
||||
.animateCamera(CameraUpdate.newLatLngZoom(position, 16.5));
|
||||
}
|
||||
|
||||
ride.updateDriverMarker(position, heading);
|
||||
ride.updateRemainingRoute(position);
|
||||
ride.update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_simulationTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
enum RideState {
|
||||
noRide, // لا يوجد رحلة جارية، عرض واجهة البحث
|
||||
cancelled, // تم إلغاء الرحلة
|
||||
preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم
|
||||
searching, // جاري البحث عن كابتن
|
||||
driverApplied, // تم قبول الطلب
|
||||
driverArrived, // وصل السائق
|
||||
inProgress, // الرحلة بدأت بالفعل
|
||||
finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview)
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../services/emergency_signal_service.dart';
|
||||
import '../../../views/widgets/elevated_btn.dart';
|
||||
import '../../../views/widgets/my_textField.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import '../../functions/country_logic.dart';
|
||||
|
||||
class UiInteractionsController extends GetxController {
|
||||
TextEditingController sosPhonePassengerProfile = TextEditingController();
|
||||
TextEditingController whatsAppLocationText = TextEditingController();
|
||||
final sosFormKey = GlobalKey<FormState>();
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
EmergencySignalService.instance.startListening(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.statusRide == 'Begin' ||
|
||||
rideLifecycle.statusRide == 'start') {
|
||||
Log.print("🚨 Emergency shake verified! Prompting SOS...");
|
||||
sosPassenger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ensureSosNumber(Function onSuccess) async {
|
||||
String? storedPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (storedPhone != null && storedPhone.isNotEmpty) {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
sosPhonePassengerProfile.clear();
|
||||
Get.defaultDialog(
|
||||
title: 'Add SOS Phone'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: sosFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
MyTextForm(
|
||||
controller: sosPhonePassengerProfile,
|
||||
label: 'insert sos phone'.tr,
|
||||
hint: CountryLogic.getPhoneHint(
|
||||
box.read(BoxName.countryCode) ?? 'Syria').tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Note: If no country code is entered, it will be saved as Syrian (+963)."
|
||||
.tr,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () async {
|
||||
if (sosFormKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
var numberPhone =
|
||||
CountryLogic.formatCurrentCountryPhone(sosPhonePassengerProfile.text);
|
||||
|
||||
await CRUD().post(
|
||||
link: AppLink.updateprofile,
|
||||
payload: {
|
||||
'id': box.read(BoxName.passengerID),
|
||||
'sosPhone': numberPhone,
|
||||
},
|
||||
);
|
||||
|
||||
box.write(BoxName.sosPhonePassenger, numberPhone);
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
));
|
||||
}
|
||||
|
||||
void sosPassenger() {
|
||||
_ensureSosNumber(() {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Emergency SOS".tr,
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
content: Column(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
size: 50, color: AppColor.redColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Do you want to send an emergency message to your SOS contact?"
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: "Send SOS".tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_shareTripDetailsSOS();
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: "I'm Safe".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _shareTripDetailsSOS() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
String message = "**Emergency SOS from Passenger:**\n";
|
||||
String origin = locSearch.startNameAddress;
|
||||
String destination = locSearch.endNameAddress;
|
||||
|
||||
message += "* ${'Origin'.tr}: $origin\n";
|
||||
message += "* ${'Destination'.tr}: $destination\n";
|
||||
message += "* ${'Driver Name'.tr}: ${rideLifecycle.driverName}\n";
|
||||
message +=
|
||||
"* ${'Car'.tr}: ${rideLifecycle.make} - ${rideLifecycle.model} - ${rideLifecycle.licensePlate}\n";
|
||||
message += "* ${'Phone'.tr}: ${rideLifecycle.driverPhone}\n\n";
|
||||
|
||||
message +=
|
||||
"${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}\n";
|
||||
message += "Please help! Contact me as soon as possible.".tr;
|
||||
|
||||
launchCommunication(
|
||||
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
|
||||
}
|
||||
|
||||
// removed formatSyrianPhone and formatSyrianPhoneNumber
|
||||
|
||||
void sendSMS(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String formattedDriverPhone =
|
||||
rideLifecycle.driverPhone.replaceAll(' ', '').replaceAll('+', '');
|
||||
|
||||
String message =
|
||||
'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with ${rideLifecycle.passengerName} as the driver. ${rideLifecycle.passengerName} \nis driving a ${rideLifecycle.model}\n with license plate ${rideLifecycle.licensePlate}.\n I am currently located at ${Get.find<LocationSearchController>().passengerLocation}.\n If you need to reach me, please contact the driver directly at\n\n $formattedDriverPhone.';
|
||||
|
||||
launchCommunication('sms', to, message);
|
||||
}
|
||||
|
||||
void sendWhatsapp(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
String formattedPhone = CountryLogic.formatCurrentCountryPhone(to);
|
||||
|
||||
String message =
|
||||
'${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} ${rideLifecycle.passengerName}${' as the driver.'.tr} ${rideLifecycle.passengerName} \n${'is driving a '.tr}${rideLifecycle.model}\n${' with license plate '.tr}${rideLifecycle.licensePlate}.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n ${rideLifecycle.driverPhone}.';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhone, message);
|
||||
}
|
||||
|
||||
Future<dynamic> driverArrivePassengerDialoge() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Hi ,I Arrive your location'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'Please go to Car Driver'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok I will go now.'.tr,
|
||||
onPressed: () {
|
||||
NotificationService.sendNotification(
|
||||
target: rideLifecycle.driverToken.toString(),
|
||||
title: 'Hi ,I will go now'.tr,
|
||||
body: 'I will go now'.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
category: 'Hi ,I will go now',
|
||||
);
|
||||
Get.back();
|
||||
rideLifecycle.remainingTime = 0;
|
||||
rideLifecycle.update();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
|
||||
final textToSpeechController = Get.find<TextToSpeechController>();
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
titleStyle: AppStyle.title,
|
||||
middleTextStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await textToSpeechController.speakText(title ?? midTitle!);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Text(
|
||||
midTitle!,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: onPressed,
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel',
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future shareTripWithFamily() async {
|
||||
_ensureSosNumber(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
|
||||
if (rideLifecycle.rideId == 'yet' || rideLifecycle.driverId.isEmpty) {
|
||||
mySnackbarWarning("Wait for the trip to start first".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var numberPhone = CountryLogic.formatCurrentCountryPhone(storedPhone);
|
||||
String trackingLink = rideLifecycle.generateTrackingLink(
|
||||
rideLifecycle.rideId, rideLifecycle.driverId);
|
||||
|
||||
String message = """
|
||||
مرحباً، تابع رحلتي مباشرة على تطبيق سيرو 🚗
|
||||
|
||||
يمكنك تتبع مسار الرحلة من هنا:
|
||||
$trackingLink
|
||||
|
||||
السائق: ${rideLifecycle.passengerName}
|
||||
السيارة: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
شكراً لاستخدامك سيرو!
|
||||
"""
|
||||
.tr;
|
||||
|
||||
String messageEn = """Hello, follow my trip live on Siro 🚗
|
||||
|
||||
Track my ride here:
|
||||
$trackingLink
|
||||
|
||||
Driver: ${rideLifecycle.passengerName}
|
||||
Car: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
Thank you for using Siro!
|
||||
""";
|
||||
|
||||
String userLanguage = box.read(BoxName.lang) ?? 'ar';
|
||||
message = (userLanguage == 'ar') ? message : messageEn;
|
||||
|
||||
Log.print("Sending WhatsApp to: $numberPhone");
|
||||
launchCommunication('whatsapp', numberPhone, message);
|
||||
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
Future getTokenForParent() async {
|
||||
_ensureSosNumber(() async {
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
var numberPhone = CountryLogic.formatCurrentCountryPhone(storedPhone);
|
||||
Log.print("Searching for Parent Token with Phone: $numberPhone");
|
||||
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.getTokenParent, payload: {'phone': numberPhone});
|
||||
|
||||
if (res is Map<String, dynamic>) {
|
||||
handleResponse(res);
|
||||
} else {
|
||||
try {
|
||||
var decoded = jsonDecode(res);
|
||||
handleResponse(decoded);
|
||||
} catch (e) {
|
||||
Log.print("Error parsing parent response: $res");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleResponse(Map<String, dynamic> res) {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (res['status'] == 'failure') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "No user found".tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
"No passenger found for the given phone number".tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Send Siro app to him".tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Send Invite'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
var rawPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (rawPhone == null) return;
|
||||
var phone = CountryLogic.formatCurrentCountryPhone(rawPhone);
|
||||
|
||||
var message = '''Dear Friend,
|
||||
|
||||
🚀 I have just started an exciting trip on Siro!
|
||||
Download the app to track my ride:
|
||||
|
||||
👉 Android: https://play.google.com/store/apps/details?id=com.Siro.siro&hl=en-US
|
||||
👉 iOS: https://apps.apple.com/st/app/siro-rider/id6748075179
|
||||
|
||||
See you there!
|
||||
Siro Team''';
|
||||
|
||||
launchCommunication('whatsapp', phone, message);
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (res['status'] == 'success') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
mySnackbarSuccess("The invitation was sent successfully".tr);
|
||||
|
||||
List tokensData = res['data'];
|
||||
for (var device in tokensData) {
|
||||
String tokenParent = device['token'];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
category: "Trip Monitoring",
|
||||
target: tokenParent,
|
||||
title: "Trip Monitoring".tr,
|
||||
body: "Click to track the trip".tr,
|
||||
isTopic: false,
|
||||
tone: 'tone1',
|
||||
driverList: [rideLifecycle.rideId, rideLifecycle.driverId],
|
||||
);
|
||||
box.write(BoxName.tokenParent, tokenParent);
|
||||
}
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
EmergencySignalService.instance.stopListening();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user