feat: add vehicle customization, Arabic search normalization, and smooth movement interpolation
This commit is contained in:
@@ -13,6 +13,8 @@ import '../../../core/services/car_platform_bridge.dart';
|
||||
import '../../../core/services/connectivity_service.dart';
|
||||
import '../../../core/services/location_service.dart';
|
||||
import '../../../core/services/tts_service.dart';
|
||||
import '../../../core/services/vehicle_icon_generator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../data/models/hazard_model.dart';
|
||||
import '../../../data/models/route_model.dart';
|
||||
import '../../../data/repositories/map_saas_repository.dart';
|
||||
@@ -48,11 +50,40 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
_init();
|
||||
}
|
||||
|
||||
// ── MOVEMENT INTERPOLATION ENGINE (30 FPS Smooth Vehicle Motion) ──
|
||||
Timer? _movementInterpolationTimer;
|
||||
LatLng? _currentDisplayPosition;
|
||||
double _currentDisplayHeading = 0.0;
|
||||
LatLng? _animStartPosition;
|
||||
LatLng? _animTargetPosition;
|
||||
double _animStartHeading = 0.0;
|
||||
double _animTargetHeading = 0.0;
|
||||
int _animCurrentStep = 0;
|
||||
static const int _animTotalSteps = 25; // 25 steps * 40ms = 1000ms duration
|
||||
static const Duration _animTickDuration = Duration(milliseconds: 40);
|
||||
|
||||
Future<void> _init() async {
|
||||
print("🚀 [NavigationCubit] Initializing NavigationCubit...");
|
||||
CarPlatformBridge.ensureInitialized();
|
||||
await ttsService.init();
|
||||
|
||||
// Load saved vehicle & search preferences
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedColor = prefs.getInt('siro_vehicle_color') ?? 0xFF007AFF;
|
||||
final savedStyle = prefs.getString('siro_vehicle_style') ?? 'car';
|
||||
final savedScale = prefs.getDouble('siro_vehicle_scale') ?? 1.8;
|
||||
final savedRecent = prefs.getStringList('siro_recent_searches') ?? [];
|
||||
emit(state.copyWith(
|
||||
selectedVehicleColor: savedColor,
|
||||
selectedVehicleStyle: savedStyle,
|
||||
vehicleScale: savedScale,
|
||||
recentSearches: savedRecent,
|
||||
));
|
||||
} catch (e) {
|
||||
print("⚠️ [NavigationCubit] SharedPreferences load error: $e");
|
||||
}
|
||||
|
||||
// Check & listen to network connectivity
|
||||
connectivityService.initialize();
|
||||
final isOnline = await connectivityService.checkConnection();
|
||||
@@ -78,6 +109,8 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
heading: position.heading,
|
||||
speed: position.speed * 3.6,
|
||||
));
|
||||
_currentDisplayPosition = loc;
|
||||
_currentDisplayHeading = position.heading;
|
||||
_updateCarMarker(loc, position.heading);
|
||||
} else {
|
||||
print("⚠️ [NavigationCubit] Initial GPS returned null, using default Amman center");
|
||||
@@ -89,12 +122,12 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
bool _hasInitiallyCenteredCamera = false;
|
||||
bool _isMapStyleLoaded = false;
|
||||
bool get isMapStyleLoaded => _isMapStyleLoaded;
|
||||
int? _hasAnnouncedEarlyStepIndex;
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
print("🗺️ [NavigationCubit] onMapCreated: Native map view created, controller attached.");
|
||||
mapController = controller;
|
||||
emit(state.copyWith(status: NavigationStatus.mapReady));
|
||||
// Defer camera animation to onStyleLoaded to prevent iOS native crashes
|
||||
}
|
||||
|
||||
Future<void> _animateCameraToCurrentPosition() async {
|
||||
@@ -109,19 +142,19 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
final alt = (pos.altitude.isNaN || pos.altitude.isInfinite) ? 0.0 : pos.altitude;
|
||||
target = loc;
|
||||
heading = pos.heading;
|
||||
print("📍 [NavigationCubit] Updated GPS target from fresh fix: lat=${loc.latitude.toStringAsFixed(6)}, lng=${loc.longitude.toStringAsFixed(6)}");
|
||||
emit(state.copyWith(
|
||||
myLocation: loc,
|
||||
altitude: alt,
|
||||
heading: pos.heading,
|
||||
speed: pos.speed * 3.6,
|
||||
));
|
||||
_currentDisplayPosition = loc;
|
||||
_currentDisplayHeading = pos.heading;
|
||||
_updateCarMarker(loc, pos.heading);
|
||||
}
|
||||
}
|
||||
|
||||
if (target != null && mapController != null && _isMapStyleLoaded) {
|
||||
print("🎬 [NavigationCubit] Animating camera to target: lat=${target.latitude.toStringAsFixed(6)}, lng=${target.longitude.toStringAsFixed(6)}, zoom=16.5, bearing=$heading");
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
@@ -131,36 +164,47 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("⏳ [NavigationCubit] Camera animation queued/deferred (styleLoaded=$_isMapStyleLoaded, controller=${mapController != null})");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _registerCurrentVehicleIcon() async {
|
||||
if (mapController == null) return;
|
||||
try {
|
||||
final bytes = await VehicleIconGenerator.generateVehicleIconBytes(
|
||||
styleId: state.selectedVehicleStyle,
|
||||
primaryColor: Color(state.selectedVehicleColor),
|
||||
);
|
||||
await mapController!.addImage('current_vehicle_icon', bytes);
|
||||
print("🚗 [NavigationCubit] Registered dynamic current_vehicle_icon (style=${state.selectedVehicleStyle}, color=0x${state.selectedVehicleColor.toRadixString(16)}, scale=${state.vehicleScale})");
|
||||
} catch (e) {
|
||||
print("⚠️ [NavigationCubit] Error generating vehicle icon: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCustomIcons() async {
|
||||
if (mapController == null) return;
|
||||
|
||||
// 1. Dynamic vehicle icon (with user-selected color & model)
|
||||
await _registerCurrentVehicleIcon();
|
||||
|
||||
// 2. Fallback static car icon
|
||||
try {
|
||||
final carBytes = await rootBundle.load('assets/images/car.png');
|
||||
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
|
||||
print("🚗 [NavigationCubit] car_icon registered into map style successfully.");
|
||||
} catch (e) {
|
||||
print("⚠️ [NavigationCubit] Could not load car_icon asset: $e");
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 3. Start & Destination pins
|
||||
try {
|
||||
final startBytes = await rootBundle.load('assets/images/A.png');
|
||||
await mapController!.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
print("📍 [NavigationCubit] start_icon (Pin A) registered successfully.");
|
||||
} catch (e) {
|
||||
print("⚠️ [NavigationCubit] Could not load start_icon asset: $e");
|
||||
}
|
||||
await mapController!.addImage('asset_assets_images_A_png', startBytes.buffer.asUint8List());
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
final destBytes = await rootBundle.load('assets/images/b.png');
|
||||
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
|
||||
print("📍 [NavigationCubit] dest_icon (Pin B) registered successfully.");
|
||||
} catch (e) {
|
||||
print("⚠️ [NavigationCubit] Could not load dest_icon asset: $e");
|
||||
}
|
||||
await mapController!.addImage('asset_assets_images_b_png', destBytes.buffer.asUint8List());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
@@ -168,11 +212,50 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
_isMapStyleLoaded = true;
|
||||
await _loadCustomIcons();
|
||||
if (state.myLocation != null) {
|
||||
_updateCarMarker(state.myLocation!, state.heading);
|
||||
_updateCarMarker(_currentDisplayPosition ?? state.myLocation!, _currentDisplayHeading);
|
||||
}
|
||||
_animateCameraToCurrentPosition();
|
||||
}
|
||||
|
||||
// ── VEHICLE CUSTOMIZATION HANDLERS ──────────────────────────
|
||||
|
||||
Future<void> setVehicleColor(int colorValue) async {
|
||||
emit(state.copyWith(selectedVehicleColor: colorValue));
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('siro_vehicle_color', colorValue);
|
||||
} catch (_) {}
|
||||
await _registerCurrentVehicleIcon();
|
||||
if (state.myLocation != null) {
|
||||
_updateCarMarker(_currentDisplayPosition ?? state.myLocation!, _currentDisplayHeading);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setVehicleStyle(String styleId) async {
|
||||
emit(state.copyWith(selectedVehicleStyle: styleId));
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('siro_vehicle_style', styleId);
|
||||
} catch (_) {}
|
||||
await _registerCurrentVehicleIcon();
|
||||
if (state.myLocation != null) {
|
||||
_updateCarMarker(_currentDisplayPosition ?? state.myLocation!, _currentDisplayHeading);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setVehicleScale(double scale) async {
|
||||
emit(state.copyWith(vehicleScale: scale));
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble('siro_vehicle_scale', scale);
|
||||
} catch (_) {}
|
||||
if (state.myLocation != null) {
|
||||
_updateCarMarker(_currentDisplayPosition ?? state.myLocation!, _currentDisplayHeading);
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOCATION STREAM & SMOOTH INTERPOLATION ────────────────────
|
||||
|
||||
void _startLocationUpdates() {
|
||||
_positionStreamSub?.cancel();
|
||||
_positionStreamSub = locationService.getPositionStream().listen((pos) {
|
||||
@@ -181,69 +264,183 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
final heading = pos.heading;
|
||||
final alt = (pos.altitude.isNaN || pos.altitude.isInfinite) ? 0.0 : pos.altitude;
|
||||
|
||||
emit(state.copyWith(
|
||||
myLocation: newLoc,
|
||||
altitude: alt,
|
||||
heading: heading,
|
||||
speed: speedKmH,
|
||||
));
|
||||
_onNewLocationFix(newLoc, heading, speedKmH, alt);
|
||||
});
|
||||
}
|
||||
|
||||
void _onNewLocationFix(LatLng newLoc, double heading, double speedKmH, double alt) {
|
||||
emit(state.copyWith(
|
||||
myLocation: newLoc,
|
||||
altitude: alt,
|
||||
heading: heading,
|
||||
speed: speedKmH,
|
||||
));
|
||||
|
||||
// Smooth movement interpolation for the vehicle marker and 3D camera
|
||||
if (_currentDisplayPosition == null) {
|
||||
_currentDisplayPosition = newLoc;
|
||||
_currentDisplayHeading = heading;
|
||||
_updateCarMarker(newLoc, heading);
|
||||
} else {
|
||||
_startMovementInterpolation(newLoc, heading);
|
||||
}
|
||||
|
||||
// Periodic driver telemetry stream (every 5 seconds)
|
||||
final now = DateTime.now();
|
||||
if (_lastTelemetrySent == null || now.difference(_lastTelemetrySent!).inSeconds >= 5) {
|
||||
_lastTelemetrySent = now;
|
||||
repository.sendDriverTelemetry(
|
||||
driverId: _driverId,
|
||||
latitude: newLoc.latitude,
|
||||
longitude: newLoc.longitude,
|
||||
speed: speedKmH,
|
||||
heading: heading,
|
||||
elevation: alt,
|
||||
distance: state.remainingDistance,
|
||||
);
|
||||
}
|
||||
// Periodic driver telemetry stream (every 5 seconds)
|
||||
final now = DateTime.now();
|
||||
if (_lastTelemetrySent == null || now.difference(_lastTelemetrySent!).inSeconds >= 5) {
|
||||
_lastTelemetrySent = now;
|
||||
repository.sendDriverTelemetry(
|
||||
driverId: _driverId,
|
||||
latitude: newLoc.latitude,
|
||||
longitude: newLoc.longitude,
|
||||
speed: speedKmH,
|
||||
heading: heading,
|
||||
elevation: alt,
|
||||
distance: state.remainingDistance,
|
||||
);
|
||||
}
|
||||
|
||||
// Proactively move camera on first acquired GPS lock (only when style is loaded)
|
||||
if (!_hasInitiallyCenteredCamera && mapController != null && _isMapStyleLoaded) {
|
||||
_hasInitiallyCenteredCamera = true;
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(target: newLoc, zoom: 16.5, bearing: heading),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Proactively move camera on first acquired GPS lock (only when style is loaded)
|
||||
if (!_hasInitiallyCenteredCamera && mapController != null && _isMapStyleLoaded) {
|
||||
_hasInitiallyCenteredCamera = true;
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(target: newLoc, zoom: 16.5, bearing: heading),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isNavigating) {
|
||||
_processActiveNavigationTick(newLoc, speedKmH, heading);
|
||||
}
|
||||
}
|
||||
|
||||
void _startMovementInterpolation(LatLng targetPos, double targetHeading) {
|
||||
_movementInterpolationTimer?.cancel();
|
||||
|
||||
_animStartPosition = _currentDisplayPosition ?? targetPos;
|
||||
_animStartHeading = _currentDisplayHeading;
|
||||
_animTargetPosition = targetPos;
|
||||
_animTargetHeading = targetHeading;
|
||||
_animCurrentStep = 0;
|
||||
|
||||
_movementInterpolationTimer = Timer.periodic(_animTickDuration, (timer) {
|
||||
_animCurrentStep++;
|
||||
final double t = (_animCurrentStep / _animTotalSteps).clamp(0.0, 1.0);
|
||||
|
||||
// Smooth ease-out curve for natural vehicle movement
|
||||
final double curvedT = 1.0 - (1.0 - t) * (1.0 - t);
|
||||
|
||||
final interpPos = _lerpLatLng(_animStartPosition!, _animTargetPosition!, curvedT);
|
||||
final interpHeading = _lerpAngle(_animStartHeading, _animTargetHeading, curvedT);
|
||||
|
||||
_currentDisplayPosition = interpPos;
|
||||
_currentDisplayHeading = interpHeading;
|
||||
|
||||
_updateCarMarker(interpPos, interpHeading);
|
||||
|
||||
// Smoothly update Google Maps style 3D camera periodically (~every 200ms) or on completion
|
||||
if (state.isCameraLocked && mapController != null && _isMapStyleLoaded && state.isNavigating) {
|
||||
double effectiveBearing = heading;
|
||||
if (speedKmH < 4.0 && state.currentRoute != null) {
|
||||
final coords = state.currentRoute!.coordinates;
|
||||
if (_lastTraveledIndexInFullRoute + 1 < coords.length) {
|
||||
effectiveBearing = _calculateBearing(
|
||||
coords[_lastTraveledIndexInFullRoute],
|
||||
coords[_lastTraveledIndexInFullRoute + 1],
|
||||
);
|
||||
}
|
||||
if (_animCurrentStep % 5 == 0 || _animCurrentStep == _animTotalSteps) {
|
||||
_updateNavigationCamera(interpPos, interpHeading, state.speed, state.distanceToNextStep);
|
||||
}
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: newLoc,
|
||||
zoom: 17.5,
|
||||
tilt: 55.0,
|
||||
bearing: effectiveBearing,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isNavigating) {
|
||||
_processActiveNavigationTick(newLoc, speedKmH, heading);
|
||||
if (_animCurrentStep >= _animTotalSteps) {
|
||||
timer.cancel();
|
||||
_movementInterpolationTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── GOOGLE MAPS NAVIGATION PERSPECTIVE & DYNAMIC ZOOM ─────────
|
||||
|
||||
void _updateNavigationCamera(LatLng pos, double heading, double speedKmH, double distanceToStep) {
|
||||
if (mapController == null || !_isMapStyleLoaded || !state.isCameraLocked) return;
|
||||
|
||||
// 1. Dynamic speed-adaptive zoom & lookahead calculation
|
||||
double targetZoom;
|
||||
double lookAheadMeters;
|
||||
const double targetTilt = 45.0; // Fixed 45-degree angle as requested by user
|
||||
|
||||
if (distanceToStep > 0 && distanceToStep < 100.0) {
|
||||
// Approaching turn / maneuver: camera descends down close to show intersection
|
||||
targetZoom = 18.2;
|
||||
lookAheadMeters = 35.0;
|
||||
} else if (speedKmH > 70.0) {
|
||||
// High speed (highway): opens up distance ahead ("تكبر تفتح مسافة أكثر")
|
||||
final speedFactor = ((speedKmH - 70.0) / 50.0).clamp(0.0, 1.0);
|
||||
targetZoom = 16.5 - (0.7 * speedFactor); // 16.5 -> 15.8
|
||||
lookAheadMeters = 70.0 + (30.0 * speedFactor); // 70m -> 100m
|
||||
} else if (speedKmH > 20.0) {
|
||||
// Moderate city speed
|
||||
final speedFactor = ((speedKmH - 20.0) / 50.0).clamp(0.0, 1.0);
|
||||
targetZoom = 17.8 - (0.8 * speedFactor); // 17.8 -> 17.0
|
||||
lookAheadMeters = 40.0 + (25.0 * speedFactor); // 40m -> 65m
|
||||
} else {
|
||||
// Stopped / very slow (< 20 km/h)
|
||||
targetZoom = 18.0;
|
||||
lookAheadMeters = 35.0;
|
||||
}
|
||||
|
||||
// 2. Heading stabilization (lock to route if stopped/creeping)
|
||||
double effectiveBearing = heading;
|
||||
if (speedKmH < 4.0 && state.currentRoute != null) {
|
||||
final coords = state.currentRoute!.coordinates;
|
||||
if (_lastTraveledIndexInFullRoute + 1 < coords.length) {
|
||||
effectiveBearing = _calculateBearing(
|
||||
coords[_lastTraveledIndexInFullRoute],
|
||||
coords[_lastTraveledIndexInFullRoute + 1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Shift camera target ahead along heading vector
|
||||
// This positions the car in the bottom ~28% of the viewport with 45° tilt!
|
||||
final cameraTarget = _computeOffset(pos, lookAheadMeters, effectiveBearing);
|
||||
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: cameraTarget,
|
||||
zoom: targetZoom,
|
||||
tilt: targetTilt,
|
||||
bearing: effectiveBearing,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LatLng _computeOffset(LatLng from, double distanceMeters, double bearingDegrees) {
|
||||
const double earthRadius = 6378137.0; // WGS-84 earth radius in meters
|
||||
final double dByR = distanceMeters / earthRadius;
|
||||
final double latRad = from.latitude * (pi / 180.0);
|
||||
final double lonRad = from.longitude * (pi / 180.0);
|
||||
final double bearingRad = bearingDegrees * (pi / 180.0);
|
||||
|
||||
final double targetLatRad = asin(
|
||||
sin(latRad) * cos(dByR) + cos(latRad) * sin(dByR) * cos(bearingRad),
|
||||
);
|
||||
final double targetLonRad = lonRad + atan2(
|
||||
sin(bearingRad) * sin(dByR) * cos(latRad),
|
||||
cos(dByR) - sin(latRad) * sin(targetLatRad),
|
||||
);
|
||||
|
||||
return LatLng(targetLatRad * (180.0 / pi), targetLonRad * (180.0 / pi));
|
||||
}
|
||||
|
||||
double _lerpAngle(double from, double to, double t) {
|
||||
final double diff = ((to - from + 540.0) % 360.0) - 180.0;
|
||||
return (from + diff * t) % 360.0;
|
||||
}
|
||||
|
||||
LatLng _lerpLatLng(LatLng a, LatLng b, double t) {
|
||||
return LatLng(
|
||||
a.latitude + (b.latitude - a.latitude) * t,
|
||||
a.longitude + (b.longitude - a.longitude) * t,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateCarMarker(LatLng position, double bearing) async {
|
||||
if (mapController == null || !_isMapStyleLoaded) return;
|
||||
try {
|
||||
@@ -253,7 +450,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
rotation: bearing,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
icon: InlqBitmap.fromStyleImage('car_icon'),
|
||||
icon: InlqBitmap.fromStyleImage('current_vehicle_icon', size: state.vehicleScale),
|
||||
zIndex: 100,
|
||||
));
|
||||
} catch (e) {
|
||||
@@ -269,29 +466,25 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
print("🎯 [NavigationCubit] relockCameraToUser requested (styleLoaded=$_isMapStyleLoaded, loc=${state.myLocation})");
|
||||
emit(state.copyWith(isCameraLocked: true));
|
||||
if (state.myLocation != null && mapController != null && _isMapStyleLoaded) {
|
||||
double effectiveBearing = state.heading;
|
||||
if (state.isNavigating && state.speed < 4.0 && state.currentRoute != null) {
|
||||
final coords = state.currentRoute!.coordinates;
|
||||
if (_lastTraveledIndexInFullRoute + 1 < coords.length) {
|
||||
effectiveBearing = _calculateBearing(
|
||||
coords[_lastTraveledIndexInFullRoute],
|
||||
coords[_lastTraveledIndexInFullRoute + 1],
|
||||
);
|
||||
}
|
||||
}
|
||||
print("🎬 [NavigationCubit] Centering camera on user: target=${state.myLocation}, zoom=${state.isNavigating ? 17.5 : 16.5}, tilt=${state.isNavigating ? 55.0 : 0.0}, bearing=$effectiveBearing");
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: state.myLocation!,
|
||||
zoom: state.isNavigating ? 17.5 : 16.5,
|
||||
tilt: state.isNavigating ? 55.0 : 0.0,
|
||||
bearing: effectiveBearing,
|
||||
if (state.isNavigating) {
|
||||
_updateNavigationCamera(
|
||||
_currentDisplayPosition ?? state.myLocation!,
|
||||
_currentDisplayHeading,
|
||||
state.speed,
|
||||
state.distanceToNextStep,
|
||||
);
|
||||
} else {
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: state.myLocation!,
|
||||
zoom: 16.5,
|
||||
tilt: 0.0,
|
||||
bearing: state.heading,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("⏳ [NavigationCubit] relockCameraToUser deferred: styleLoaded=$_isMapStyleLoaded, controller=${mapController != null}");
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,25 +509,102 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
_searchDebounce?.cancel();
|
||||
if (query.trim().length < 2) {
|
||||
emit(state.copyWith(searchResults: []));
|
||||
final trimmed = query.trim();
|
||||
if (trimmed.length < 2) {
|
||||
emit(state.copyWith(searchResults: [], isSearching: false));
|
||||
return;
|
||||
}
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 400), () async {
|
||||
emit(state.copyWith(isSearching: true));
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 350), () async {
|
||||
final results = await repository.searchPlaces(
|
||||
query: query,
|
||||
query: trimmed,
|
||||
userLocation: state.myLocation,
|
||||
);
|
||||
emit(state.copyWith(searchResults: results));
|
||||
emit(state.copyWith(searchResults: results, isSearching: false));
|
||||
});
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
emit(state.copyWith(searchResults: []));
|
||||
Future<void> searchImmediately(String query) async {
|
||||
_searchDebounce?.cancel();
|
||||
final trimmed = query.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
emit(state.copyWith(searchResults: [], isSearching: false));
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(isSearching: true));
|
||||
await saveRecentSearch(trimmed);
|
||||
final results = await repository.searchPlaces(
|
||||
query: trimmed,
|
||||
userLocation: state.myLocation,
|
||||
);
|
||||
emit(state.copyWith(searchResults: results, isSearching: false));
|
||||
}
|
||||
|
||||
Future<void> calculateRouteTo(LatLng destination, {String title = 'وجهة مختارة'}) async {
|
||||
print("🛣️ [NavigationCubit] calculateRouteTo: target=$destination, title=$title, myLocation=${state.myLocation}");
|
||||
Future<void> saveRecentSearch(String query) async {
|
||||
final trimmed = query.trim();
|
||||
if (trimmed.length < 2) return;
|
||||
final updated = List<String>.from(state.recentSearches)
|
||||
..remove(trimmed)
|
||||
..insert(0, trimmed);
|
||||
if (updated.length > 8) updated.removeLast();
|
||||
emit(state.copyWith(recentSearches: updated));
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList('siro_recent_searches', updated);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> clearRecentSearches() async {
|
||||
emit(state.copyWith(recentSearches: []));
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('siro_recent_searches');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
_searchDebounce?.cancel();
|
||||
emit(state.copyWith(searchResults: [], isSearching: false));
|
||||
}
|
||||
|
||||
// ── STEP BOUNDING OVERVIEW ──────────────────────────────────
|
||||
|
||||
void overviewStepBounding(int stepIndex) {
|
||||
if (state.currentRoute == null || stepIndex < 0 || stepIndex >= state.routeSteps.length) return;
|
||||
final step = state.routeSteps[stepIndex];
|
||||
final interval = step['interval'];
|
||||
final coords = state.currentRoute!.coordinates;
|
||||
|
||||
if (interval is List && interval.length >= 2) {
|
||||
final startIdx = (interval[0] as num).toInt().clamp(0, coords.length - 1);
|
||||
final endIdx = (interval[1] as num).toInt().clamp(0, coords.length - 1);
|
||||
|
||||
if (startIdx <= endIdx) {
|
||||
final stepCoords = coords.sublist(startIdx, endIdx + 1);
|
||||
if (stepCoords.isNotEmpty) {
|
||||
_fitRouteInView(stepCoords);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final stepLat = (step['lat'] as num?)?.toDouble() ?? 0.0;
|
||||
final stepLng = (step['lng'] as num?)?.toDouble() ?? 0.0;
|
||||
if (stepLat != 0.0 && stepLng != 0.0 && mapController != null && _isMapStyleLoaded) {
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(target: LatLng(stepLat, stepLng), zoom: 17.5, tilt: 45.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> calculateRouteTo(
|
||||
LatLng destination, {
|
||||
String title = 'وجهة مختارة',
|
||||
String originTitle = 'موقعي الحالي',
|
||||
}) async {
|
||||
print("🛣️ [NavigationCubit] calculateRouteTo: origin=$originTitle, target=$destination, title=$title, myLocation=${state.myLocation}");
|
||||
if (state.myLocation == null) {
|
||||
print("⚠️ [NavigationCubit] Cannot calculate route: current GPS location is null!");
|
||||
return;
|
||||
@@ -343,6 +613,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
status: NavigationStatus.loading,
|
||||
destination: destination,
|
||||
destinationTitle: title,
|
||||
originTitle: originTitle,
|
||||
searchResults: [],
|
||||
));
|
||||
|
||||
@@ -370,9 +641,9 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
final startMarker = Marker(
|
||||
markerId: const MarkerId('origin_pin'),
|
||||
position: state.myLocation!,
|
||||
icon: InlqBitmap.fromAsset('assets/images/A.png'),
|
||||
icon: InlqBitmap.fromStyleImage('start_icon'),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
infoWindow: const InfoWindow(title: 'نقطة الانطلاق (أ)'),
|
||||
infoWindow: InfoWindow(title: originTitle, snippet: 'start'),
|
||||
zIndex: 90,
|
||||
);
|
||||
|
||||
@@ -380,9 +651,9 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
final destMarker = Marker(
|
||||
markerId: const MarkerId('dest_pin'),
|
||||
position: destination,
|
||||
icon: InlqBitmap.fromAsset('assets/images/b.png'),
|
||||
icon: InlqBitmap.fromStyleImage('dest_icon'),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
infoWindow: InfoWindow(title: title),
|
||||
infoWindow: InfoWindow(title: title, snippet: 'end'),
|
||||
zIndex: 90,
|
||||
);
|
||||
|
||||
@@ -395,6 +666,9 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
status: NavigationStatus.routePreview,
|
||||
routes: routes,
|
||||
selectedRouteIndex: 0,
|
||||
destination: destination,
|
||||
destinationTitle: title,
|
||||
originTitle: originTitle,
|
||||
routeSteps: primaryRoute.steps,
|
||||
remainingDistance: primaryRoute.distanceM,
|
||||
remainingDuration: primaryRoute.durationS,
|
||||
@@ -403,6 +677,12 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
arrivalTime: _calculateArrivalTime(primaryRoute.durationS),
|
||||
));
|
||||
|
||||
if (mapController != null && _isMapStyleLoaded) {
|
||||
await mapController!.addMarker(startMarker);
|
||||
await mapController!.addMarker(destMarker);
|
||||
print("📍 [NavigationCubit] Directly added startMarker & destMarker to native map engine.");
|
||||
}
|
||||
|
||||
_fitRouteInView(primaryRoute.coordinates);
|
||||
} catch (e) {
|
||||
print("❌ [NavigationCubit] Error calculating route: $e");
|
||||
@@ -467,22 +747,15 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
|
||||
ttsService.speak(firstInstruction);
|
||||
|
||||
// Immediate 3D Heading-Up camera orientation
|
||||
// Immediate 3D Heading-Up camera orientation with 45° tilt and lookahead offset
|
||||
if (state.myLocation != null && mapController != null && _isMapStyleLoaded) {
|
||||
double bearing = state.heading;
|
||||
if (bearing == 0.0 && route.coordinates.length > 1) {
|
||||
bearing = _calculateBearing(route.coordinates[0], route.coordinates[1]);
|
||||
}
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: state.myLocation!,
|
||||
zoom: 17.5,
|
||||
tilt: 55.0,
|
||||
bearing: bearing,
|
||||
),
|
||||
),
|
||||
);
|
||||
_currentDisplayPosition = state.myLocation;
|
||||
_currentDisplayHeading = bearing;
|
||||
_updateNavigationCamera(state.myLocation!, bearing, state.speed, 100.0);
|
||||
}
|
||||
|
||||
CarPlatformBridge.updateNavState(
|
||||
@@ -501,14 +774,22 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
|
||||
void stopNavigation() {
|
||||
print("🛑 [NavigationCubit] stopNavigation triggered.");
|
||||
_movementInterpolationTimer?.cancel();
|
||||
_movementInterpolationTimer = null;
|
||||
ttsService.stop();
|
||||
CarPlatformBridge.stopNavigation();
|
||||
_lastTraveledIndexInFullRoute = 0;
|
||||
_offRouteStartTime = null;
|
||||
_hasAnnouncedEarlyStepIndex = null;
|
||||
|
||||
final remainingMarkers = Set<Marker>.from(state.markers)
|
||||
..removeWhere((m) => m.markerId.value == 'origin_pin' || m.markerId.value == 'dest_pin');
|
||||
|
||||
if (mapController != null && _isMapStyleLoaded) {
|
||||
mapController!.removeMarker(const MarkerId('origin_pin'));
|
||||
mapController!.removeMarker(const MarkerId('dest_pin'));
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
status: NavigationStatus.mapReady,
|
||||
routes: [],
|
||||
@@ -517,6 +798,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
markers: remainingMarkers,
|
||||
destination: null,
|
||||
destinationTitle: '',
|
||||
originTitle: 'موقعي الحالي',
|
||||
currentInstruction: '',
|
||||
nextInstruction: '',
|
||||
isCameraLocked: true,
|
||||
@@ -531,16 +813,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
}
|
||||
|
||||
void simulateLocationTick(LatLng pos, {double speed = 60.0, double heading = 0.0, double altitude = 0.0}) {
|
||||
emit(state.copyWith(
|
||||
myLocation: pos,
|
||||
altitude: altitude,
|
||||
speed: speed,
|
||||
heading: heading,
|
||||
));
|
||||
_updateCarMarker(pos, heading);
|
||||
if (state.isNavigating) {
|
||||
_processActiveNavigationTick(pos, speed, heading);
|
||||
}
|
||||
_onNewLocationFix(pos, heading, speed, altitude);
|
||||
}
|
||||
|
||||
void _processActiveNavigationTick(LatLng pos, double speedKmH, double heading) {
|
||||
@@ -588,6 +861,16 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
final distToStep = locationService.calculateDistance(pos, stepTarget);
|
||||
emit(state.copyWith(distanceToNextStep: distToStep));
|
||||
|
||||
// Advance voice announcement when approaching turn (150m - 200m)
|
||||
if (distToStep <= 180.0 && distToStep > 50.0 && _hasAnnouncedEarlyStepIndex != stepIdx) {
|
||||
_hasAnnouncedEarlyStepIndex = stepIdx;
|
||||
final roundDist = ((distToStep / 50).round() * 50).clamp(50, 200);
|
||||
final text = step['text']?.toString() ?? '';
|
||||
if (text.isNotEmpty) {
|
||||
ttsService.speak('بعد $roundDist متراً، $text');
|
||||
}
|
||||
}
|
||||
|
||||
final stepEndIdx = (step['interval'] is List && (step['interval'] as List).length >= 2)
|
||||
? ((step['interval'] as List)[1] as num).toInt()
|
||||
: -1;
|
||||
@@ -595,6 +878,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
|
||||
if ((distToStep < 35.0 || hasPassedStep) && stepIdx + 1 < steps.length) {
|
||||
stepIdx++;
|
||||
_hasAnnouncedEarlyStepIndex = null;
|
||||
final nextStep = steps[stepIdx];
|
||||
final text = nextStep['text']?.toString() ?? '';
|
||||
final upcomingText = stepIdx + 1 < steps.length ? (steps[stepIdx + 1]['text']?.toString() ?? '') : '';
|
||||
@@ -746,22 +1030,94 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
_isRerouting = false;
|
||||
}
|
||||
|
||||
// ── USER SUBMISSIONS: PLACES & HAZARDS ──────────────────────
|
||||
// ── USER SUBMISSIONS: PLACES & HAZARDS (WITH PIN PICKER) ─────
|
||||
|
||||
Future<bool> submitPlace(String name, String category) async {
|
||||
if (mapController == null) return false;
|
||||
final center = mapController!.cameraPosition?.target ?? state.myLocation;
|
||||
if (center == null) return false;
|
||||
void startLocationPicking(String mode) {
|
||||
final targetLoc = mapController?.cameraPosition?.target ?? state.myLocation ?? const LatLng(31.9539, 35.9106);
|
||||
final pickMarker = Marker(
|
||||
markerId: const MarkerId('picker_pin'),
|
||||
position: targetLoc,
|
||||
icon: InlqBitmap.fromStyleImage('dest_icon'),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
infoWindow: InfoWindow(
|
||||
title: mode == 'place' ? '📍 موقع المنشأة المحددة' : '⚠️ موقع البلاغ المحدد',
|
||||
),
|
||||
zIndex: 95,
|
||||
);
|
||||
|
||||
final updatedMarkers = Set<Marker>.from(state.markers)
|
||||
..removeWhere((m) => m.markerId.value == 'picker_pin')
|
||||
..add(pickMarker);
|
||||
|
||||
emit(state.copyWith(
|
||||
isSelectingLocationOnMap: true,
|
||||
activePickerMode: mode,
|
||||
pickedLocation: targetLoc,
|
||||
markers: updatedMarkers,
|
||||
));
|
||||
|
||||
if (mapController != null && _isMapStyleLoaded) {
|
||||
mapController!.addMarker(pickMarker);
|
||||
}
|
||||
}
|
||||
|
||||
void updatePickedLocation(LatLng newLoc) {
|
||||
if (!state.isSelectingLocationOnMap) return;
|
||||
|
||||
final pickMarker = Marker(
|
||||
markerId: const MarkerId('picker_pin'),
|
||||
position: newLoc,
|
||||
icon: InlqBitmap.fromStyleImage('dest_icon'),
|
||||
anchor: const Offset(0.5, 1.0),
|
||||
infoWindow: InfoWindow(
|
||||
title: state.activePickerMode == 'place' ? '📍 موقع المنشأة المحددة' : '⚠️ موقع البلاغ المحدد',
|
||||
),
|
||||
zIndex: 95,
|
||||
);
|
||||
|
||||
final updatedMarkers = Set<Marker>.from(state.markers)
|
||||
..removeWhere((m) => m.markerId.value == 'picker_pin')
|
||||
..add(pickMarker);
|
||||
|
||||
emit(state.copyWith(
|
||||
pickedLocation: newLoc,
|
||||
markers: updatedMarkers,
|
||||
));
|
||||
|
||||
if (mapController != null && _isMapStyleLoaded) {
|
||||
mapController!.addMarker(pickMarker);
|
||||
}
|
||||
}
|
||||
|
||||
void cancelLocationPicking() {
|
||||
final remainingMarkers = Set<Marker>.from(state.markers)
|
||||
..removeWhere((m) => m.markerId.value == 'picker_pin');
|
||||
|
||||
emit(state.copyWith(
|
||||
isSelectingLocationOnMap: false,
|
||||
clearActivePickerMode: true,
|
||||
clearPickedLocation: true,
|
||||
markers: remainingMarkers,
|
||||
));
|
||||
|
||||
if (mapController != null && _isMapStyleLoaded) {
|
||||
mapController!.removeMarker(const MarkerId('picker_pin'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> submitPlace(String name, String category, {LatLng? position}) async {
|
||||
final targetPos = position ?? state.pickedLocation ?? mapController?.cameraPosition?.target ?? state.myLocation;
|
||||
if (targetPos == null) return false;
|
||||
|
||||
final success = await repository.submitNewPlace(
|
||||
name: name,
|
||||
category: category,
|
||||
position: center,
|
||||
position: targetPos,
|
||||
altitude: state.altitude,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit(state.copyWith(isSelectingLocationOnMap: false));
|
||||
cancelLocationPicking();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -770,20 +1126,26 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
required String type,
|
||||
required String title,
|
||||
required String description,
|
||||
LatLng? position,
|
||||
}) async {
|
||||
if (state.myLocation == null) return false;
|
||||
final targetPos = position ?? state.pickedLocation ?? state.myLocation;
|
||||
if (targetPos == null) return false;
|
||||
|
||||
final hazard = HazardModel(
|
||||
type: type,
|
||||
title: title,
|
||||
description: description,
|
||||
latitude: state.myLocation!.latitude,
|
||||
longitude: state.myLocation!.longitude,
|
||||
latitude: targetPos.latitude,
|
||||
longitude: targetPos.longitude,
|
||||
altitude: state.altitude,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
return await repository.reportHazard(hazard);
|
||||
final success = await repository.reportHazard(hazard);
|
||||
if (success) {
|
||||
cancelLocationPicking();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ── HELPERS ──────────────────────────────────────────────────
|
||||
@@ -907,6 +1269,7 @@ class NavigationCubit extends Cubit<NavigationState> {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_movementInterpolationTimer?.cancel();
|
||||
_positionStreamSub?.cancel();
|
||||
_connectivitySub?.cancel();
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
@@ -29,6 +29,7 @@ class NavigationState extends Equatable {
|
||||
final int selectedRouteIndex;
|
||||
final LatLng? destination;
|
||||
final String destinationTitle;
|
||||
final String originTitle;
|
||||
final List<Map<String, dynamic>> routeSteps;
|
||||
final int currentStepIndex;
|
||||
final String currentInstruction;
|
||||
@@ -45,8 +46,15 @@ class NavigationState extends Equatable {
|
||||
final Set<Polyline> polylines;
|
||||
final List<PlaceModel> searchResults;
|
||||
final bool isSelectingLocationOnMap;
|
||||
final String? activePickerMode;
|
||||
final LatLng? pickedLocation;
|
||||
final bool isOnline;
|
||||
final String? errorMessage;
|
||||
final int selectedVehicleColor;
|
||||
final String selectedVehicleStyle;
|
||||
final double vehicleScale;
|
||||
final List<String> recentSearches;
|
||||
final bool isSearching;
|
||||
|
||||
const NavigationState({
|
||||
this.status = NavigationStatus.initial,
|
||||
@@ -58,6 +66,7 @@ class NavigationState extends Equatable {
|
||||
this.selectedRouteIndex = 0,
|
||||
this.destination,
|
||||
this.destinationTitle = '',
|
||||
this.originTitle = 'موقعي الحالي',
|
||||
this.routeSteps = const [],
|
||||
this.currentStepIndex = 0,
|
||||
this.currentInstruction = '',
|
||||
@@ -74,8 +83,15 @@ class NavigationState extends Equatable {
|
||||
this.polylines = const {},
|
||||
this.searchResults = const [],
|
||||
this.isSelectingLocationOnMap = false,
|
||||
this.activePickerMode,
|
||||
this.pickedLocation,
|
||||
this.isOnline = true,
|
||||
this.errorMessage,
|
||||
this.selectedVehicleColor = 0xFF007AFF,
|
||||
this.selectedVehicleStyle = 'car',
|
||||
this.vehicleScale = 1.8,
|
||||
this.recentSearches = const [],
|
||||
this.isSearching = false,
|
||||
});
|
||||
|
||||
RouteData? get currentRoute =>
|
||||
@@ -112,6 +128,7 @@ class NavigationState extends Equatable {
|
||||
int? selectedRouteIndex,
|
||||
LatLng? destination,
|
||||
String? destinationTitle,
|
||||
String? originTitle,
|
||||
List<Map<String, dynamic>>? routeSteps,
|
||||
int? currentStepIndex,
|
||||
String? currentInstruction,
|
||||
@@ -128,8 +145,17 @@ class NavigationState extends Equatable {
|
||||
Set<Polyline>? polylines,
|
||||
List<PlaceModel>? searchResults,
|
||||
bool? isSelectingLocationOnMap,
|
||||
String? activePickerMode,
|
||||
bool clearActivePickerMode = false,
|
||||
LatLng? pickedLocation,
|
||||
bool clearPickedLocation = false,
|
||||
bool? isOnline,
|
||||
String? errorMessage,
|
||||
int? selectedVehicleColor,
|
||||
String? selectedVehicleStyle,
|
||||
double? vehicleScale,
|
||||
List<String>? recentSearches,
|
||||
bool? isSearching,
|
||||
}) {
|
||||
return NavigationState(
|
||||
status: status ?? this.status,
|
||||
@@ -141,6 +167,7 @@ class NavigationState extends Equatable {
|
||||
selectedRouteIndex: selectedRouteIndex ?? this.selectedRouteIndex,
|
||||
destination: destination ?? this.destination,
|
||||
destinationTitle: destinationTitle ?? this.destinationTitle,
|
||||
originTitle: originTitle ?? this.originTitle,
|
||||
routeSteps: routeSteps ?? this.routeSteps,
|
||||
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
|
||||
currentInstruction: currentInstruction ?? this.currentInstruction,
|
||||
@@ -159,8 +186,15 @@ class NavigationState extends Equatable {
|
||||
searchResults: searchResults ?? this.searchResults,
|
||||
isSelectingLocationOnMap:
|
||||
isSelectingLocationOnMap ?? this.isSelectingLocationOnMap,
|
||||
activePickerMode: clearActivePickerMode ? null : (activePickerMode ?? this.activePickerMode),
|
||||
pickedLocation: clearPickedLocation ? null : (pickedLocation ?? this.pickedLocation),
|
||||
isOnline: isOnline ?? this.isOnline,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedVehicleColor: selectedVehicleColor ?? this.selectedVehicleColor,
|
||||
selectedVehicleStyle: selectedVehicleStyle ?? this.selectedVehicleStyle,
|
||||
vehicleScale: vehicleScale ?? this.vehicleScale,
|
||||
recentSearches: recentSearches ?? this.recentSearches,
|
||||
isSearching: isSearching ?? this.isSearching,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,6 +209,7 @@ class NavigationState extends Equatable {
|
||||
selectedRouteIndex,
|
||||
destination,
|
||||
destinationTitle,
|
||||
originTitle,
|
||||
routeSteps,
|
||||
currentStepIndex,
|
||||
currentInstruction,
|
||||
@@ -191,7 +226,14 @@ class NavigationState extends Equatable {
|
||||
polylines,
|
||||
searchResults,
|
||||
isSelectingLocationOnMap,
|
||||
activePickerMode,
|
||||
pickedLocation,
|
||||
isOnline,
|
||||
errorMessage,
|
||||
selectedVehicleColor,
|
||||
selectedVehicleStyle,
|
||||
vehicleScale,
|
||||
recentSearches,
|
||||
isSearching,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user