startNavigation method

Future<void> startNavigation(
  1. PowerMapRouteResult route, {
  2. PowerNavigationMode mode = PowerNavigationMode.simulation,
})

Implementation

Future<void> startNavigation(
  PowerMapRouteResult route, {
  PowerNavigationMode mode = PowerNavigationMode.simulation,
}) async {
  _currentRoute = route;
  _originalRoute ??= route; // Save the very first route only
  _mode = mode;
  _distanceTraveled = 0.0;
  _playedInstructions.clear();
  _cameraBearing = null;
  _vehicleBearing = null;
  _smoothedGps = null;
  _smoothedZoom = null; // Fix: Reset zoom state so it doesn't jump back to summary zoom
  _isTracking = true; // Fix: Always start tracking when navigation begins
  _isReturningToVehicle = false;
  _isCameraTransitioning = false;
  _nearDestSince = null;
  _hasTriggeredArrival = false;
  _lastSegmentIndex = 0; // D+1: Reset monotonic progress
  _lastGpsUpdateTime = null; // D+2: Reset throttle
  _playedDistanceAnnouncements.clear(); // D+3: Reset voice announcements
  _registerLifecycleObserver();

  if (route.geometry.isEmpty) {
    throw ArgumentError(
      'Route cannot be empty. Please ensure you have successfully fetched a route from the routing API before starting navigation.',
    );
  }

  // Save current camera state before entering navigation
  final currentCam = _controller.mapController.cameraPosition;
  _savedCenter = currentCam?.target;
  _savedZoom = currentCam?.zoom;

  // A2 Fix: Use real GPS position for GPS mode to prevent warp to route start
  LatLng startPos;
  if (mode == PowerNavigationMode.gps) {
    _locationTracker.startTracking(); // Immediately trigger GPS listener start
    final currentGps = _locationTracker.currentLocation.value;
    if (currentGps != null) {
      startPos = LatLng(currentGps.latitude, currentGps.longitude);
    } else {
      startPos = route.geometry.first; // fallback
    }
  } else {
    startPos = route.geometry.first;
  }
  // Calculate initial bearing so camera faces the route direction from the start
  double initialBearing = 0.0;
  if (route.geometry.length > 1) {
    initialBearing = NavigationMathUtils.computeBearing(
      route.geometry[0],
      route.geometry[1],
    );
  }

  // Initialize internal bearing state
  _cameraBearing = initialBearing;
  _vehicleBearing = initialBearing;

  // Calculate initial ETA and Distance so they are not hidden on start
  final initialEtaSec = route.duration > 0 ? route.duration : 0;
  final initialEta = DateTime.now().add(Duration(seconds: initialEtaSec.round()));

  stateNotifier.value = NavigationState.initial(
    startPos,
  ).copyWith(
    mode: _mode,
    vehicleBearing: initialBearing,
    totalRemainingDistance: route.distance,
    distanceToNextManeuver: route.steps.isNotEmpty ? route.steps.first.distance : 0.0,
    estimatedArrivalTime: initialEta,
    instructionText: route.steps.isNotEmpty ? route.steps.first.text.replaceAll(RegExp(r'<[^>]*>'), ' ').trim() : '',
  );

  _checkAndUpdateSpeedLimit(startPos);

  // In GPS mode, start listening to GPS stream immediately so hardware fixes are processed without delay
  if (_mode == PowerNavigationMode.gps) {
    _startGpsLoop();
  }

  // ── PHASE 1: Lock camera during initial fly-to transition ──
  _isCameraTransitioning = true;
  _isReturningToVehicle = true;

  // Initial camera setup — smooth transition from route overview to navigation view
  // Offset camera target forward so vehicle appears in the lower portion of screen
  final offsetStartPos = NavigationMathUtils.computeDestination(
    startPos, initialBearing, 70.0,
  );
  await _controller.mapController.animateCamera(
    CameraUpdate.newCameraPosition(
      CameraPosition(target: offsetStartPos, zoom: 17.0, tilt: _cameraTilt, bearing: initialBearing),
    ),
    duration: const Duration(milliseconds: 800),
  );

  // Hide native user location indicator to prevent overlap with navigation vehicle
  await _controller.showUserLocation(false);

  // Add 2D Vehicle Symbol via GeoJSON source to prevent flickering
  await _ensureArrowImage();

  final sourceData = {
    "type": "FeatureCollection",
    "features": [
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [startPos.longitude, startPos.latitude],
        },
        "properties": {"bearing": 0.0},
      },
    ],
  };

  try {
    await _controller.mapController.addGeoJsonSource(
      "vehicle-source",
      sourceData,
    );
    await _controller.mapController.addSymbolLayer(
      "vehicle-source",
      "vehicle-layer",
      const SymbolLayerProperties(
        iconImage: 'nav-arrow',
        iconSize: 1.0,
        iconAnchor: 'center',
        iconPitchAlignment: 'map',
        iconRotationAlignment: 'map',
        iconRotate: ['get', 'bearing'],
        iconAllowOverlap: true,
        iconIgnorePlacement: true,
      ),
    );
    //  Add invisible circle layer specifically to increase hit area size!
    await _controller.mapController.addCircleLayer(
      "vehicle-source",
      "vehicle-hitbox-layer",
      const CircleLayerProperties(
        circleRadius: 35.0, // Large hit radius
        circleColor: 'rgba(0,0,0,0)', // Fully transparent
        circleOpacity: 0.01, // Prevent engine from stripping out the hit-test
        circlePitchAlignment: 'map',
      ),
    );
    _vehicleSymbolAdded = true;
  } catch (e) {
    // In case source already exists, just update it
    await _controller.mapController.setGeoJsonSource(
      "vehicle-source",
      sourceData,
    );
    _vehicleSymbolAdded = true;
  }

  // Register tap listener for the vehicle icon (using MapLibre's built-in tap event)
  _controller.mapController.onFeatureTapped.add(_handleFeatureTapped);
  // C4: Initialize traveled route layer (gray faded line)
  await _initTraveledRoute(startPos);

  // Render active navigation turn guidance arrows (Option A: compact white sub-polyline arrows on route line)
  await _updateActiveNavigationTurnArrows(0);

  // ── PHASE 2: Unlock camera — enable follow mode and start simulation loop ──
  _isCameraTransitioning = false;
  _isReturningToVehicle = false;
  debugPrint('[NAV] Camera transition complete. Enabling follow mode.');

  if (_mode == PowerNavigationMode.simulation) {
    _startSimulationLoop();
  }
}