Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps

This commit is contained in:
Hamza-Ayed
2026-06-01 23:35:29 +03:00
parent 8f555691b9
commit cbf693c804
56 changed files with 6091 additions and 1217 deletions

View File

@@ -93,13 +93,13 @@ class NavigationController extends GetxController
String totalDistanceRemaining = "";
String estimatedTimeRemaining = "";
dynamic currentManeuverModifier = 0;
String arrivalTime = "--:--";
String arrivalTime = "--:--"; // NEW: For the active navigation HUD
double _routeTotalDistanceM = 0;
double _routeTotalDurationS = 0;
bool isNavigating = false;
bool isMuted = false;
bool isMuted = false; // Sound toggle state
String distanceWithUnit = "";
bool _cameraLockedToUser = true;
bool _mapReady = false;
@@ -114,6 +114,7 @@ class NavigationController extends GetxController
Future<void> submitNewPlace(String name, String category) async {
if (mapController == null || name.isEmpty || category.isEmpty) return;
// Get current center of the map as the picked location
final LatLng pickedPos = mapController!.cameraPosition!.target;
isLoading = true;
@@ -139,15 +140,21 @@ class NavigationController extends GetxController
isLoading = false;
if (response != null) {
HapticFeedback.lightImpact();
mySnackbarSuccess('Place added successfully! Thanks for your contribution.'.tr);
mySnackbarSuccess(box.read(BoxName.lang) == 'ar'
? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.'
: 'Place added successfully! Thanks for your contribution.');
isSelectingPlaceLocation = false;
} else {
mySnackbarWarning('Failed to add place. Please try again later.'.tr);
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
? 'تعذر إضافة المكان. يرجى المحاولة لاحقاً.'
: 'Failed to add place. Please try again later.');
}
update();
} catch (e) {
isLoading = false;
mySnackbarWarning('An error occurred while connecting to the server.'.tr);
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
? 'حدث خطأ أثناء الاتصال بالخادم.'
: 'An error occurred while connecting to the server.');
update();
}
}
@@ -181,6 +188,7 @@ class NavigationController extends GetxController
return 55.0;
}
// Categories list for the picker
static final List<Map<String, String>> placeCategories = [
{
'id': 'restaurant',
@@ -303,6 +311,7 @@ class NavigationController extends GetxController
_smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
if (isStyleLoaded) {
_updateCarMarker();
if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading,
@@ -365,7 +374,6 @@ class NavigationController extends GetxController
void onMapCreated(IntaleqMapController controller) async {
Log.print("DEBUG: NavigationController.onMapCreated called");
mapController = controller;
await onStyleLoaded();
}
Future<void> onStyleLoaded() async {
@@ -381,6 +389,7 @@ class NavigationController extends GetxController
if (myLocation != null) {
Log.print("DEBUG: Animating camera to initial location: $myLocation");
animateCameraToPosition(myLocation!);
_updateCarMarker();
}
if (_fullRouteCoordinates.isNotEmpty) {
Log.print("DEBUG: Updating initial polylines");
@@ -394,7 +403,7 @@ class NavigationController extends GetxController
if (isNavigating || routes.isEmpty) return;
int? bestIndex;
double minDistance = 100.0;
double minDistance = 100.0; // 100 meters threshold for tap
for (int i = 0; i < routes.length; i++) {
for (var coord in routes[i].coordinates) {
@@ -422,12 +431,12 @@ class NavigationController extends GetxController
Get.dialog(
AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text('Start Navigation?'.tr,
style: const TextStyle(fontWeight: FontWeight.bold)),
content: Text('Do you want to go to this location?'.tr),
title: const Text('بدء الملاحة؟',
style: TextStyle(fontWeight: FontWeight.bold)),
content: const Text('هل تريد الذهاب إلى هذا الموقع؟'),
actions: [
TextButton(
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
onPressed: () => Get.back()),
ElevatedButton(
style: ElevatedButton.styleFrom(
@@ -435,10 +444,10 @@ class NavigationController extends GetxController
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
child:
Text('Go Now'.tr, style: const TextStyle(color: Colors.white)),
const Text('اذهب الآن', style: TextStyle(color: Colors.white)),
onPressed: () {
Get.back();
startNavigationTo(tappedPoint, infoWindowTitle: 'Selected Location'.tr);
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
},
),
],
@@ -458,6 +467,7 @@ class NavigationController extends GetxController
_smoothedHeading = position.heading;
update();
if (isStyleLoaded) animateCameraToPosition(myLocation!);
// Start the Location Stream for real-time updates
_startLocationStream();
_startBatchTimers();
} catch (e) {
@@ -467,10 +477,12 @@ class NavigationController extends GetxController
void _startLocationStream() {
_locationStreamSubscription?.cancel();
// Listen to location updates with minimum distance filter of 2 meters
// This provides real-time updates without the 3-4 second delay
_locationStreamSubscription = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 2,
distanceFilter: 2, // Update every 2 meters
),
).listen(
(Position position) {
@@ -489,8 +501,9 @@ class NavigationController extends GetxController
try {
final newLoc = LatLng(position.latitude, position.longitude);
currentSpeed = position.speed * 3.6;
currentSpeed = position.speed * 3.6; // Convert m/s to km/h
// Skip if movement is too small
if (_lastProcessedLocation != null) {
final d = Geolocator.distanceBetween(
newLoc.latitude,
@@ -507,6 +520,7 @@ class NavigationController extends GetxController
Log.print(
"DEBUG: Location update - Speed: ${currentSpeed.toStringAsFixed(1)} km/h, Loc: $newLoc");
// Update total distance
if (_lastDistanceLocation != null) {
final d = Geolocator.distanceBetween(
_lastDistanceLocation!.latitude,
@@ -536,6 +550,8 @@ class NavigationController extends GetxController
_animController?.forward(from: 0.0);
_lastProcessedLocation = newLoc;
if (isStyleLoaded) _updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty) {
_updateTraveledPolylineSmart(newLoc);
_checkNavigationStep(newLoc);
@@ -556,7 +572,7 @@ class NavigationController extends GetxController
}
void _checkOffRoute(LatLng pos) {
if (_autoRecalcInProgress || isLoading) return;
if (!isNavigating || _autoRecalcInProgress || isLoading) return;
if (_fullRouteCoordinates.isEmpty) return;
const int searchWindow = 80;
@@ -591,33 +607,11 @@ class NavigationController extends GetxController
}
}
/// Recalculate immediately from the latest GPS point to the destination.
Future<void> _smartRecalculateRoute(LatLng currentPos) async {
try {
if (routes.isNotEmpty && selectedRouteIndex < routes.length - 1) {
final nextIndex = selectedRouteIndex + 1;
final nextRoute = routes[nextIndex];
double minDist = double.infinity;
for (var coord in nextRoute.coordinates) {
final d = Geolocator.distanceBetween(
currentPos.latitude,
currentPos.longitude,
coord.latitude,
coord.longitude,
);
if (d < minDist) minDist = d;
}
if (minDist < 100) {
selectRoute(nextIndex);
Log.print("DEBUG: Switched to alternative route due to deviation");
_autoRecalcInProgress = false;
return;
}
}
if (_finalDestination != null) {
await recalculateRoute();
await recalculateRoute(origin: currentPos, keepNavigationActive: true);
}
_autoRecalcInProgress = false;
} catch (e) {
@@ -669,13 +663,13 @@ class NavigationController extends GetxController
if (_trackBuffer.isEmpty) return;
final batch = List<Map<String, dynamic>>.from(_trackBuffer);
_trackBuffer.clear();
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
final String passengerId = (box.read(BoxName.passengerID) ?? '').toString();
try {
await CRUD().post(
link: '${AppLink.locationServerSide}/add_batch.php',
payload: {
'driver_id': driverId,
'driver_id': passengerId,
'batch_data': jsonEncode(batch),
'session_dist': totalDistance.toStringAsFixed(1),
},
@@ -685,6 +679,10 @@ class NavigationController extends GetxController
}
}
Future<void> _updateCarMarker() async {
// Car marker is now handled natively by myLocationEnabled: true.
}
void animateCameraToPosition(LatLng position,
{double? zoom, double bearing = 0.0, double tilt = 0.0}) {
if (!_mapReady || mapController == null) return;
@@ -774,8 +772,11 @@ class NavigationController extends GetxController
Future<void> _updatePolylinesSets(
List<LatLng> traveled, List<LatLng> remaining) async {
Log.print(
"DEBUG: Updating polylines. Traveled: ${traveled.length}, Remaining: ${remaining.length}");
Set<Polyline> newPolylines = {};
// Render Alternative Routes first
for (int i = 0; i < routes.length; i++) {
if (i == selectedRouteIndex) continue;
newPolylines.add(Polyline(
@@ -840,7 +841,7 @@ class NavigationController extends GetxController
if (dest != null && myLocation != null) {
getRoute(myLocation!, dest);
} else {
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'الموقع غير متاح حالياً.' : 'Location not available.');
mySnackbarWarning('الموقع غير متاح حالياً.');
}
}
@@ -865,12 +866,13 @@ class NavigationController extends GetxController
LatLng getAirportLatLng() {
final String country = box.read(BoxName.countryCode) ?? 'JO';
if (country == 'SY') {
return const LatLng(33.4111, 36.5147);
return const LatLng(33.4111, 36.5147); // Damascus Airport
}
return const LatLng(31.7225, 35.9933);
return const LatLng(31.7225, 35.9933); // Queen Alia Airport (JO)
}
Future<void> getRoute(LatLng origin, LatLng destination) async {
Future<void> getRoute(LatLng origin, LatLng destination,
{bool keepNavigationActive = false}) async {
isLoading = true;
update();
@@ -899,12 +901,13 @@ class NavigationController extends GetxController
if (response.statusCode != 200) {
isLoading = false;
update();
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'تعذر الاتصال بخدمة التوجيه.' : 'Failed to connect to routing service.');
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
return;
}
final data = jsonDecode(response.body);
// ── Parse primary route (top-level in response) ──
routes.clear();
final primaryPts = data['points']?.toString() ?? "";
if (primaryPts.isNotEmpty) {
@@ -919,8 +922,10 @@ class NavigationController extends GetxController
));
}
// ── Parse alternative routes (in data['alternatives']) ──
// إذا كان هناك routes بديلة متاحة من API
if (data['alternatives'] != null && data['alternatives'] is List) {
_hasAlternativeRoutes = (data['alternatives'] as List).isNotEmpty;
_hasAlternativeRoutes = data['alternatives'].isNotEmpty;
for (var alt in data['alternatives']) {
final altPts = alt['points']?.toString() ?? "";
if (altPts.isEmpty) continue;
@@ -934,6 +939,9 @@ class NavigationController extends GetxController
points: altPts,
));
}
if (_hasAlternativeRoutes) {
Log.print("DEBUG: ${routes.length - 1} alternative routes available");
}
} else {
_hasAlternativeRoutes = false;
}
@@ -941,7 +949,7 @@ class NavigationController extends GetxController
if (routes.isEmpty) {
isLoading = false;
update();
mySnackbarWarning(box.read(BoxName.lang) == 'ar' ? 'لم يتم العثور على مسار.' : 'No route found.');
mySnackbarWarning('لم يتم العثور على مسار.');
return;
}
@@ -965,8 +973,8 @@ class NavigationController extends GetxController
currentStepIndex = 0;
_nextInstructionSpoken = false;
isNavigating = false;
_cameraLockedToUser = false;
isNavigating = keepNavigationActive;
_cameraLockedToUser = keepNavigationActive;
_offRouteStartTime = null;
isLoading = false;
@@ -986,7 +994,13 @@ class NavigationController extends GetxController
}
}
if (_fullRouteCoordinates.length >= 2) {
// Re-add car marker after polyline updates (ensures it stays on top)
if (isStyleLoaded) _updateCarMarker();
if (keepNavigationActive && myLocation != null) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
} else if (_fullRouteCoordinates.length >= 2) {
final bounds =
data['bbox'] != null && (data['bbox'] as List).length == 4
? LatLngBounds(
@@ -1012,17 +1026,22 @@ class NavigationController extends GetxController
final remainingM = _routeTotalDistanceM * fraction;
final remainingS = _routeTotalDurationS * fraction;
// Distance
final String langCode = box.read(BoxName.lang) ?? 'ar';
if (remainingM > 1000) {
totalDistanceRemaining = (remainingM / 1000).toStringAsFixed(1);
// We will handle the unit in the view or provide a unit string here
} else {
totalDistanceRemaining = remainingM.toStringAsFixed(0);
}
// New variable to hold formatted distance with unit
distanceWithUnit = _formatDistance(remainingM, langCode);
// Time Remaining
final minutes = (remainingS / 60).round();
estimatedTimeRemaining = minutes.toString();
// Arrival Time Calculation
final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt()));
final h = arrival.hour > 12
? arrival.hour - 12
@@ -1040,6 +1059,7 @@ class NavigationController extends GetxController
_finalDestination = destination;
await clearRoute(isNewRoute: true);
// Preserve car marker if it exists
markers = markers.where((m) => m.markerId.value == 'car').toSet();
markers.add(Marker(
@@ -1065,12 +1085,23 @@ class NavigationController extends GetxController
}
}
Future<void> recalculateRoute() async {
if (myLocation == null || _finalDestination == null || isLoading) return;
Future<void> recalculateRoute(
{LatLng? origin, bool keepNavigationActive = false}) async {
final LatLng? routeOrigin = origin ?? myLocation;
if (routeOrigin == null || _finalDestination == null || isLoading) return;
isLoading = true;
update();
mySnackbarInfo(box.read(BoxName.lang) == 'ar' ? 'جاري حساب مسار جديد...' : 'Calculating new route...');
await getRoute(myLocation!, _finalDestination!);
markers = markers.where((m) => m.markerId.value != 'origin').toSet();
markers.add(Marker(
markerId: const MarkerId('origin'),
position: routeOrigin,
icon: InlqBitmap.fromStyleImage('start_icon'),
));
await getRoute(routeOrigin, _finalDestination!,
keepNavigationActive: keepNavigationActive);
isLoading = false;
update();
}
@@ -1087,8 +1118,11 @@ class NavigationController extends GetxController
isNavigating = true;
_cameraLockedToUser = true;
// Ensure ETA and distances are up-to-date
_lastTraveledIndexInFullRoute = _lastTraveledIndexInFullRoute;
_recomputeETA();
// Initialize current instruction if available
if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) {
currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
@@ -1105,6 +1139,7 @@ class NavigationController extends GetxController
}
}
// Center camera on user for navigation mode
if (myLocation != null) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
@@ -1145,22 +1180,21 @@ class NavigationController extends GetxController
_routeTotalDistanceM = 0;
_routeTotalDurationS = 0;
if (!isNewRoute) {
await _updateCarMarker();
}
update();
}
Future<void> _loadCustomIcons() async {
if (mapController == null) return;
try {
final carBytes = await rootBundle.load('assets/images/car.png');
final startBytes = await rootBundle.load('assets/images/A.png');
final destBytes = await rootBundle.load('assets/images/b.png');
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
await mapController!
.addImage('start_icon', startBytes.buffer.asUint8List());
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
} catch (e) {
Log.print("Error loading custom icons: $e");
}
final carBytes = await rootBundle.load('assets/images/car.png');
final startBytes = await rootBundle.load('assets/images/A.png');
final destBytes = await rootBundle.load('assets/images/b.png');
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
await mapController!
.addImage('start_icon', startBytes.buffer.asUint8List());
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
}
void _checkNavigationStep(LatLng pos) {
@@ -1233,18 +1267,21 @@ class NavigationController extends GetxController
if (mapController == null) return;
try {
// ✅ Use searchPlaces from intaleq_maps SDK
final results = await mapController!.searchPlaces(q);
if (myLocation != null) {
for (final p in results) {
final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng);
final plng =
double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
p['distanceKm'] = _haversineKm(
myLocation!.latitude, myLocation!.longitude, plat, plng);
}
results.sort((a, b) =>
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
}
placesDestination = results;
update();
} catch (e) {
@@ -1258,7 +1295,7 @@ class NavigationController extends GetxController
final lat = double.parse(place['latitude'].toString());
final lng = double.parse(place['longitude'].toString());
await startNavigationTo(LatLng(lat, lng),
infoWindowTitle: place['name'] ?? (box.read(BoxName.lang) == 'ar' ? 'وجهة' : 'Destination'));
infoWindowTitle: place['name'] ?? 'وجهة');
}
void onSearchChanged(String query) {
@@ -1278,6 +1315,9 @@ class NavigationController extends GetxController
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
}
double _kmToLatDelta(double km) => km / 111.32;
double _kmToLngDelta(double km, double lat) =>
km / (111.32 * cos(lat * pi / 180));
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
double? x0, x1, y0, y1;
for (final ll in list) {
@@ -1328,12 +1368,12 @@ class NavigationController extends GetxController
'name': name,
'lat': myLocation!.latitude.toString(),
'lng': myLocation!.longitude.toString(),
'driver_id': box.read(BoxName.driverID),
'passenger_id': box.read(BoxName.passengerID),
};
await CRUD().post(link: AppLink.getPlacesSyria, payload: payload);
mySnackbarInfo(box.read(BoxName.lang) == 'ar'
? "تم استلام اقتراحك! شكراً لمساهمتك."
: "Suggestion received! Thanks for your contribution.");
? "تم استلام اقتراحك! مكافأتك: +٥٠ نقطة"
: "Suggestion received! Reward: +50 points");
} finally {
isLoading = false;
update();