feat: add Jordan places migration scripts and enhance map controller with style-load safety and camera controls

This commit is contained in:
Hamza-Ayed
2026-09-23 20:56:34 +03:00
parent dc9a736e26
commit 8dbb336e0a
6 changed files with 35867 additions and 16 deletions
@@ -56,6 +56,7 @@ class NavigationCubit extends Cubit<NavigationState> {
Timer? _movementInterpolationTimer;
LatLng? _currentDisplayPosition;
double _currentDisplayHeading = 0.0;
double _currentCameraBearing = 0.0;
LatLng? _animStartPosition;
LatLng? _animTargetPosition;
double _animStartHeading = 0.0;
@@ -64,6 +65,13 @@ class NavigationCubit extends Cubit<NavigationState> {
static const int _animTotalSteps = 25; // 25 steps * 40ms = 1000ms duration
static const Duration _animTickDuration = Duration(milliseconds: 40);
void updateCameraBearing(double bearing) {
_currentCameraBearing = bearing;
if (_currentDisplayPosition != null) {
_updateCarMarker(_currentDisplayPosition!, _currentDisplayHeading);
}
}
Future<void> _init() async {
print("🚀 [NavigationCubit] Initializing NavigationCubit...");
CarPlatformBridge.ensureInitialized();
@@ -493,20 +501,48 @@ class NavigationCubit extends Cubit<NavigationState> {
void _onNewLocationFix(
LatLng newLoc, double heading, double speedKmH, double alt) {
double resolvedHeading = heading;
// Calculate heading from real movement if GPS course is missing / 0.0
if (resolvedHeading <= 0.0 || resolvedHeading.isNaN) {
if (_currentDisplayPosition != null) {
final dist = locationService.calculateDistance(_currentDisplayPosition!, newLoc);
if (dist >= 1.5) {
final computed = locationService.calculateBearing(_currentDisplayPosition!, newLoc);
resolvedHeading = (computed % 360.0 + 360.0) % 360.0;
} else {
resolvedHeading = _currentDisplayHeading;
}
} else {
resolvedHeading = _currentDisplayHeading;
}
}
// In active navigation, if stopped or crawling, lock heading to route segment ahead
if (state.isNavigating && speedKmH < 4.0 && state.currentRoute != null) {
final coords = state.currentRoute!.coordinates;
if (_lastTraveledIndexInFullRoute + 1 < coords.length) {
resolvedHeading = _calculateBearing(
coords[_lastTraveledIndexInFullRoute],
coords[_lastTraveledIndexInFullRoute + 1],
);
}
}
emit(state.copyWith(
myLocation: newLoc,
altitude: alt,
heading: heading,
heading: resolvedHeading,
speed: speedKmH,
));
// Smooth movement interpolation for the vehicle marker and 3D camera
if (_currentDisplayPosition == null) {
_currentDisplayPosition = newLoc;
_currentDisplayHeading = heading;
_updateCarMarker(newLoc, heading);
_currentDisplayHeading = resolvedHeading;
_updateCarMarker(newLoc, resolvedHeading);
} else {
_startMovementInterpolation(newLoc, heading);
_startMovementInterpolation(newLoc, resolvedHeading);
}
// Proactively move camera on first acquired GPS lock (only when style is loaded)
@@ -514,9 +550,10 @@ class NavigationCubit extends Cubit<NavigationState> {
mapController != null &&
_isMapStyleLoaded) {
_hasInitiallyCenteredCamera = true;
_currentCameraBearing = resolvedHeading;
mapController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: newLoc, zoom: 16.5, bearing: heading),
CameraPosition(target: newLoc, zoom: 16.5, bearing: resolvedHeading),
),
);
}
@@ -527,11 +564,11 @@ class NavigationCubit extends Cubit<NavigationState> {
latitude: newLoc.latitude,
longitude: newLoc.longitude,
speedKmH: speedKmH,
heading: heading,
heading: resolvedHeading,
elevation: alt,
remainingDistance: state.remainingDistance,
);
_processActiveNavigationTick(newLoc, speedKmH, heading);
_processActiveNavigationTick(newLoc, speedKmH, resolvedHeading);
}
}
@@ -624,6 +661,8 @@ class NavigationCubit extends Cubit<NavigationState> {
}
}
_currentCameraBearing = effectiveBearing;
// 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);
@@ -672,13 +711,21 @@ class NavigationCubit extends Cubit<NavigationState> {
);
}
Future<void> _updateCarMarker(LatLng position, double bearing) async {
Future<void> _updateCarMarker(LatLng position, double worldHeading) async {
if (mapController == null || !_isMapStyleLoaded) return;
try {
// MapLibre symbols have viewport-aligned rotation.
// (worldHeading - _currentCameraBearing) keeps the vehicle oriented correctly:
// In active navigation with locked camera, (worldHeading - _currentCameraBearing) = 0° (facing UP/forward along the road).
// During turns, it tilts smoothly into the corner as the car turns.
// In free map exploration, it points towards true geographic direction relative to rotated map.
final double screenRotation =
(worldHeading - _currentCameraBearing + 360.0) % 360.0;
await mapController!.setUserMarker(Marker(
markerId: const MarkerId('current_user_car'),
position: position,
rotation: bearing,
rotation: screenRotation,
anchor: const Offset(0.5, 0.5),
flat: true,
icon: InlqBitmap.fromStyleImage('current_vehicle_icon',
@@ -709,6 +756,7 @@ class NavigationCubit extends Cubit<NavigationState> {
state.distanceToNextStep,
);
} else {
_currentCameraBearing = state.heading;
mapController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
@@ -1705,6 +1705,7 @@ class _MapViewState extends State<MapView> {
zoomControlsEnabled: false,
compassEnabled: false,
onCameraMove: (pos) {
cubit.updateCameraBearing(pos.bearing);
if (state.isSelectingLocationOnMap) {
cubit.updatePickedLocation(pos.target);
}