fix: stabilize passenger mapping interactions and finalize localization

This commit is contained in:
Hamza-Ayed
2026-04-18 19:16:01 +03:00
parent a54a7a4189
commit 61343111a2
28 changed files with 14917 additions and 14716 deletions

View File

@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:http/http.dart' as http;
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
@@ -47,7 +47,7 @@ class NavigationController extends GetxController
static const int _offRouteTriggerSeconds = 6;
bool isLoading = false;
MaplibreMapController? mapController;
IntaleqMapController? mapController;
bool isStyleLoaded = false;
final TextEditingController placeDestinationController =
TextEditingController();
@@ -65,11 +65,10 @@ class NavigationController extends GetxController
double currentSpeed = 0.0;
double totalDistance = 0.0;
Symbol? carSymbol;
Symbol? originSymbol;
Symbol? destinationSymbol;
Line? remainingRouteLine;
Line? traveledRouteLine;
Set<Marker> markers = {};
Set<Polyline> polylines = {};
Set<Circle> circles = {};
Set<Polygon> polygons = {};
Timer? _locationUpdateTimer;
LatLng? _lastProcessedLocation;
@@ -102,6 +101,61 @@ class NavigationController extends GetxController
bool _cameraLockedToUser = true;
bool _mapReady = false;
bool isSelectingPlaceLocation = false;
void togglePlaceSelectionMode() {
isSelectingPlaceLocation = !isSelectingPlaceLocation;
update();
}
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;
update();
final String country =
box.read(BoxName.countryCode) == 'SY' ? 'syria' : 'jordan';
final Map<String, dynamic> payload = {
'name': name,
'category': category,
'lat': pickedPos.latitude,
'lng': pickedPos.longitude,
'country': country,
};
try {
final response = await CRUD().postMapSaas(
link: AppLink.mapSaasPlaces,
payload: payload,
);
isLoading = false;
if (response != null) {
HapticFeedback.lightImpact();
mySnackbarSuccess(box.read(BoxName.lang) == 'ar'
? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.'
: 'Place added successfully! Thanks for your contribution.');
isSelectingPlaceLocation = false;
} else {
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
? 'تعذر إضافة المكان. يرجى المحاولة لاحقاً.'
: 'Failed to add place. Please try again later.');
}
update();
} catch (e) {
isLoading = false;
mySnackbarWarning(box.read(BoxName.lang) == 'ar'
? 'حدث خطأ أثناء الاتصال بالخادم.'
: 'An error occurred while connecting to the server.');
update();
}
}
DateTime? _offRouteStartTime;
bool _autoRecalcInProgress = false;
@@ -114,7 +168,7 @@ class NavigationController extends GetxController
List<RouteData> routes = [];
int selectedRouteIndex = 0;
List<Line> alternativeRouteLines = [];
List<Map<String, dynamic>> recentLocations = [];
double get _targetZoom {
@@ -131,6 +185,85 @@ class NavigationController extends GetxController
return 55.0;
}
// Categories list for the picker
static final List<Map<String, String>> placeCategories = [
{
'id': 'restaurant',
'en': 'Restaurant',
'ar': 'مطعم',
'icon': 'restaurant'
},
{'id': 'cafe', 'en': 'Cafe', 'ar': 'مقهى', 'icon': 'coffee'},
{
'id': 'supermarket',
'en': 'Supermarket',
'ar': 'سوبر ماركت',
'icon': 'shopping_basket'
},
{
'id': 'pharmacy',
'en': 'Pharmacy',
'ar': 'صيدلية',
'icon': 'local_pharmacy'
},
{
'id': 'gas_station',
'en': 'Gas Station',
'ar': 'محطة وقود',
'icon': 'local_gas_station'
},
{'id': 'atm', 'en': 'ATM', 'ar': 'صراف آلي', 'icon': 'atm'},
{'id': 'bank', 'en': 'Bank', 'ar': 'بنك', 'icon': 'account_balance'},
{'id': 'mosque', 'en': 'Mosque', 'ar': 'مسجد', 'icon': 'mosque'},
{
'id': 'hospital',
'en': 'Hospital',
'ar': 'مستشفى',
'icon': 'local_hospital'
},
{'id': 'school', 'en': 'School', 'ar': 'مدرسة', 'icon': 'school'},
{
'id': 'university',
'en': 'University',
'ar': 'جامعة',
'icon': 'account_balance'
},
{'id': 'park', 'en': 'Park', 'ar': 'منتزه', 'icon': 'park'},
{'id': 'hotel', 'en': 'Hotel', 'ar': 'فندق', 'icon': 'hotel'},
{
'id': 'mall',
'en': 'Shopping Mall',
'ar': 'مركز تسوق',
'icon': 'shopping_mall'
},
{'id': 'gym', 'en': 'Gym', 'ar': 'نادي رياضي', 'icon': 'fitness_center'},
{
'id': 'salon',
'en': 'Beauty Salon',
'ar': 'صالون تجميل',
'icon': 'content_cut'
},
{'id': 'bakery', 'en': 'Bakery', 'ar': 'مخبز', 'icon': 'bakery_dining'},
{
'id': 'laundry',
'ar': 'مصبغة',
'en': 'Laundry',
'icon': 'local_laundry_service'
},
{
'id': 'car_repair',
'en': 'Car Repair',
'ar': 'تصليح سيارات',
'icon': 'build'
},
{
'id': 'government',
'en': 'Government Office',
'ar': 'دائرة حكومية',
'icon': 'gavel'
},
];
static final String _routeApiBaseUrl =
"${AppLink.routesOsm}/route/v1/driving";
@@ -179,11 +312,11 @@ class NavigationController extends GetxController
if (isStyleLoaded) {
_updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty && _cameraLockedToUser) {
if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading,
zoom: _targetZoom,
tilt: _targetTilt);
zoom: isNavigating ? _targetZoom : 17.0,
tilt: isNavigating ? _targetTilt : 0.0);
}
}
}
@@ -208,7 +341,7 @@ class NavigationController extends GetxController
} else {
parsed = [];
}
recentLocations = parsed
.map((e) => Map<String, dynamic>.from(e))
.toList()
@@ -238,23 +371,29 @@ class NavigationController extends GetxController
super.onClose();
}
void onMapCreated(MaplibreMapController controller) {
void onMapCreated(IntaleqMapController controller) async {
Log.print("DEBUG: NavigationController.onMapCreated called");
mapController = controller;
await onStyleLoaded();
}
Future<void> onStyleLoaded() async {
Log.print("DEBUG: NavigationController.onStyleLoaded called");
isStyleLoaded = true;
await _loadCustomIcons();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 300));
if (!_mapReady) {
Log.print("DEBUG: NavigationController setting _mapReady = true");
_mapReady = true;
if (myLocation != null) {
Log.print("DEBUG: Animating camera to initial location: $myLocation");
animateCameraToPosition(myLocation!);
_updateCarMarker();
}
if (_fullRouteCoordinates.isNotEmpty) {
Log.print("DEBUG: Updating initial polylines");
_updatePolylinesSets([], _fullRouteCoordinates);
}
}
@@ -319,9 +458,11 @@ class NavigationController extends GetxController
Future<void> _getCurrentLocationAndStartUpdates() async {
try {
Log.print("DEBUG: Getting initial location...");
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
myLocation = LatLng(position.latitude, position.longitude);
Log.print("DEBUG: Initial location acquired: $myLocation");
_targetHeading = position.heading;
_oldHeading = position.heading;
_smoothedHeading = position.heading;
@@ -330,7 +471,7 @@ class NavigationController extends GetxController
_startLocationTimer();
_startBatchTimers();
} catch (e) {
Log.print("Error getting initial location: $e");
Log.print("DEBUG: Error getting initial location: $e");
}
}
@@ -340,7 +481,10 @@ class NavigationController extends GetxController
Timer.periodic(const Duration(seconds: 4), (_) => _tick());
}
bool _isTicking = false;
Future<void> _tick() async {
if (_isTicking) return;
_isTicking = true;
try {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
@@ -356,6 +500,8 @@ class NavigationController extends GetxController
);
if (d < _minMoveToProcess) return;
}
Log.print(
"DEBUG: Location tick - Speed: ${currentSpeed.toStringAsFixed(1)} km/h, Loc: $newLoc");
if (_lastDistanceLocation != null) {
final d = Geolocator.distanceBetween(
@@ -396,7 +542,9 @@ class NavigationController extends GetxController
}
update();
} catch (e) {
Log.print("Error occurred: $e");
Log.print("DEBUG: Error in _tick: $e");
} finally {
_isTicking = false;
}
}
@@ -501,32 +649,7 @@ class NavigationController extends GetxController
}
Future<void> _updateCarMarker() async {
if (myLocation == null || mapController == null || !isStyleLoaded) return;
// Check if symbol still exists in map controller's internal list
bool exists =
carSymbol != null && mapController!.symbols.contains(carSymbol);
if (!exists) {
if (carSymbol != null) {
try {
await mapController!.removeSymbol(carSymbol!);
} catch (_) {}
}
carSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: myLocation,
iconImage: 'car_icon',
iconSize: 1.6,
iconRotate: _smoothedHeading,
));
} else {
mapController!.updateSymbol(
carSymbol!,
SymbolOptions(
geometry: myLocation,
iconRotate: _smoothedHeading,
));
}
// Car marker is now handled natively by myLocationEnabled: true.
}
void animateCameraToPosition(LatLng position,
@@ -618,49 +741,41 @@ class NavigationController extends GetxController
Future<void> _updatePolylinesSets(
List<LatLng> traveled, List<LatLng> remaining) async {
if (mapController == null || !isStyleLoaded) return;
// Clear old alternative lines
for (var line in alternativeRouteLines) {
await mapController!.removeLine(line);
}
alternativeRouteLines.clear();
Log.print(
"DEBUG: Updating polylines. Traveled: ${traveled.length}, Remaining: ${remaining.length}");
Set<Polyline> newPolylines = {};
if (remainingRouteLine != null) {
await mapController!.removeLine(remainingRouteLine!);
}
if (traveledRouteLine != null) {
await mapController!.removeLine(traveledRouteLine!);
}
// Render Alternative Routes first (so they are below)
// Render Alternative Routes first
for (int i = 0; i < routes.length; i++) {
if (i == selectedRouteIndex) continue;
final altLine = await mapController!.addLine(LineOptions(
geometry: routes[i].coordinates,
lineColor: '#B0BEC5', // Soft gray for alternatives
lineWidth: 6.0,
lineJoin: 'round',
lineOpacity: 0.8,
newPolylines.add(Polyline(
polylineId: PolylineId('alt_$i'),
points: routes[i].coordinates,
color: const Color(0xFFB0BEC5).withOpacity(0.8),
width: 6,
));
alternativeRouteLines.add(altLine);
}
if (remaining.isNotEmpty) {
remainingRouteLine = await mapController!.addLine(LineOptions(
geometry: remaining,
lineColor: '#00e5ff', // Cyan/Blue for selected
lineWidth: 8.0,
lineJoin: 'round'));
newPolylines.add(Polyline(
polylineId: const PolylineId('remaining'),
points: remaining,
color: const Color(0xFF00E5FF),
width: 8,
));
}
if (traveled.isNotEmpty) {
traveledRouteLine = await mapController!.addLine(LineOptions(
geometry: traveled,
lineColor: '#BDBDBD',
lineWidth: 5.0,
lineJoin: 'round',
lineOpacity: 0.6));
newPolylines.add(Polyline(
polylineId: const PolylineId('traveled'),
points: traveled,
color: const Color(0xFFBDBDBD).withOpacity(0.6),
width: 5,
));
}
polylines = newPolylines;
update();
}
void selectRoute(int index) {
@@ -671,7 +786,7 @@ class NavigationController extends GetxController
routeSteps = r.steps;
_routeTotalDistanceM = r.distanceM;
_routeTotalDurationS = r.durationS;
_lastTraveledIndexInFullRoute = 0;
_recomputeETA();
_updatePolylinesSets([], _fullRouteCoordinates);
@@ -763,7 +878,8 @@ class NavigationController extends GetxController
routes.clear();
final primaryPts = data['points']?.toString() ?? "";
if (primaryPts.isNotEmpty) {
final coords = await compute<String, List<LatLng>>(decodePolylineIsolate, primaryPts);
final coords = await compute<String, List<LatLng>>(
decodePolylineIsolate, primaryPts);
routes.add(RouteData(
coordinates: coords,
steps: List<Map<String, dynamic>>.from(data['instructions'] ?? []),
@@ -778,7 +894,8 @@ class NavigationController extends GetxController
for (var alt in data['alternatives']) {
final altPts = alt['points']?.toString() ?? "";
if (altPts.isEmpty) continue;
final altCoords = await compute<String, List<LatLng>>(decodePolylineIsolate, altPts);
final altCoords = await compute<String, List<LatLng>>(
decodePolylineIsolate, altPts);
routes.add(RouteData(
coordinates: altCoords,
steps: List<Map<String, dynamic>>.from(alt['instructions'] ?? []),
@@ -899,19 +1016,27 @@ class NavigationController extends GetxController
try {
_finalDestination = destination;
await clearRoute(isNewRoute: true);
if (isStyleLoaded && mapController != null) {
destinationSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: destination,
iconImage: 'dest_icon',
iconSize: 1.0,
textField: infoWindowTitle,
textOffset: const Offset(0, 2)));
if (myLocation != null) {
originSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: myLocation, iconImage: 'start_icon', iconSize: 1.0));
}
// Preserve car marker if it exists
markers = markers.where((m) => m.markerId.value == 'car').toSet();
markers.add(Marker(
markerId: const MarkerId('destination'),
position: destination,
icon: InlqBitmap.fromStyleImage('dest_icon'),
infoWindow: infoWindowTitle.isNotEmpty
? InfoWindow(title: infoWindowTitle)
: InfoWindow.noText,
));
if (myLocation != null) {
markers.add(Marker(
markerId: const MarkerId('origin'),
position: myLocation!,
icon: InlqBitmap.fromStyleImage('start_icon'),
));
await getRoute(myLocation!, destination);
}
if (myLocation != null) await getRoute(myLocation!, destination);
} finally {
isLoading = false;
update();
@@ -928,32 +1053,27 @@ class NavigationController extends GetxController
update();
}
Future<void> clearEverything() async {
placeDestinationController.clear();
placesDestination = [];
await clearRoute();
}
Future<void> clearRoute({bool isNewRoute = false}) async {
_offRouteStartTime = null;
_autoRecalcInProgress = false;
if (!isNewRoute) {
if (destinationSymbol != null && mapController != null) {
await mapController!.removeSymbol(destinationSymbol!);
destinationSymbol = null;
}
if (originSymbol != null && mapController != null) {
await mapController!.removeSymbol(originSymbol!);
originSymbol = null;
}
if (remainingRouteLine != null && mapController != null) {
await mapController!.removeLine(remainingRouteLine!);
remainingRouteLine = null;
}
if (traveledRouteLine != null && mapController != null) {
await mapController!.removeLine(traveledRouteLine!);
traveledRouteLine = null;
}
markers = {};
polylines = {};
circles = {};
polygons = {};
_finalDestination = null;
isNavigating = false;
routes = [];
await _flushBufferToServer();
}
routeSteps.clear();
_fullRouteCoordinates.clear();
routeSteps = [];
_fullRouteCoordinates = [];
_lastTraveledIndexInFullRoute = 0;
currentInstruction = "";
nextInstruction = "";
@@ -964,6 +1084,10 @@ class NavigationController extends GetxController
arrivalTime = "--:--";
_routeTotalDistanceM = 0;
_routeTotalDurationS = 0;
if (!isNewRoute) {
await _updateCarMarker();
}
update();
}

File diff suppressed because it is too large Load Diff