fix: stabilize passenger mapping interactions and finalize localization
This commit is contained in:
@@ -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
Reference in New Issue
Block a user