start static method

Future<NavigationSession> start({
  1. required GoogleMapController controller,
  2. required NavigationOptions options,
  3. String polylineId = 'gmns_nav_route',
  4. Color polylineColor = const Color(0xFF1976D2),
})

Implementation

static Future<NavigationSession> start({
  required GoogleMapController controller,
  required NavigationOptions options,
  String polylineId = 'gmns_nav_route',
  Color polylineColor = const Color(0xFF1976D2),
}) async {
  // Fetch route (Directions classic ou Routes v2)
  final route = await _fetchNavigationRoute(options);

  // Draw polyline & fit bounds
  final Color routeClr = options.routeColor ?? polylineColor;
  await controller.addPolyline(PolylineOptions(id: polylineId, points: route.points, color: routeClr, width: options.routeWidth));
  await controller.animateToBounds(route.northeast, route.southwest, padding: 60);

  // Prepare TTS / Audio
  FlutterTts? tts;
  if (options.voiceGuidance) {
    tts = FlutterTts();
    try {
      await tts.setLanguage(options.language);
      await tts.setSpeechRate(options.ttsRate);
      await tts.setPitch(options.ttsPitch);
      if (options.ttsVoice != null) {
        try {
          await tts.setVoice({'name': options.ttsVoice!});
        } catch (_) {}
      }
      await tts.awaitSpeakCompletion(true);
      // iOS ducking
      if (options.iosDuckOthers) {
        try { await (tts as dynamic).setIosAudioCategory('playback', ['duckOthers']); } catch (_) {}
      }
    } catch (_) {}
    // Android AudioFocus
    if (defaultTargetPlatform == TargetPlatform.android && options.androidRequestAudioFocus) {
      try { await AudioFocus.request(); } catch (_) {}
    }
  }

  // Ensure permissions are handled by the host app
  final locPerm = await Geolocator.checkPermission();
  if (locPerm == LocationPermission.denied || locPerm == LocationPermission.deniedForever) {
    // We don't request here to keep plugin passive; consumer app must request beforehand.
  }

  DateTime lastReroute = DateTime.fromMillisecondsSinceEpoch(0);
  final stateCtl = StreamController<NavState>.broadcast();
  final instCtl = StreamController<NavInstruction>.broadcast();
  final progressCtl = StreamController<NavProgress>.broadcast();
  final speedCtl = StreamController<SpeedAlert>.broadcast();
  stateCtl.add(NavState.navigating);

  // Precompute route length cumulatives for progress
  final cumul = _cumulativeDistances(route.points);
  final totalMeters = cumul.isNotEmpty ? cumul.last : 0.0;

  // Estado compartilhado runtime
  final shared = _NavShared();

  // Device heading (compass/rotation)
  double? latestHeading;
  StreamSubscription<double>? headingSub;
  _AngleSmoother? headingSmoother = options.headingFilterAlpha > 0 ? _AngleSmoother(options.headingFilterAlpha) : null;
  if (options.followBearing && options.useDeviceHeading) {
    try {
      headingSub = DeviceHeading.stream.listen((h) { latestHeading = h; });
    } catch (_) {}
  }

  // Fala por aproximação em múltiplos limiares
  final Map<int, Set<double>> spokenByStep = {};

  // Auxiliares de limite de velocidade dinâmico
  double? dynSpeedLimit;
  DateTime lastSpeedFetch = DateTime.fromMillisecondsSinceEpoch(0);
  bool vehicleMarkerAdded = false;

  // Função para processar uma atualização de posição (GPS ou simulação)
  Future<void> onPosition(LatLng user, {double speedKmh = 0.0, double? course}) async {
    if (shared.closed || shared.paused) return;

    // Snapping e bearing
    LatLng camTarget = user;
    double? camBearing;
    double? routeBearing;
    if (options.snapToRoute && route.points.length >= 2) {
      final np = _nearestPointOnPolyline(user, route.points);
      camTarget = np.point;
      final a = route.points[np.segIndex];
      final b = route.points[np.segIndex + 1];
      routeBearing = _normalizeBearing(_bearingDegrees(a, b));
    }

    // Determine camera bearing
    if (options.followBearing) {
      final speed = speedKmh.isFinite ? speedKmh : 0.0;
      double? candidate;
      if (speed >= options.lowSpeedAutoRotateKmh) {
        // Moving: prefer course from location; fallback to route bearing
        candidate = _normalizeBearing(course) ?? routeBearing;
      } else {
        // Low speed / stationary: prefer device heading; fallback to route bearing
        candidate = _normalizeBearing(latestHeading) ?? routeBearing;
      }
      if (candidate != null) {
        if (headingSmoother != null) {
          camBearing = headingSmoother.update(candidate);
        } else {
          camBearing = candidate;
        }
      }
    }

    // Vehicle marker: add/update if enabled
    if (options.showVehicleMarker) {
      final markerPos = options.snapToRoute ? camTarget : user;
      if (!vehicleMarkerAdded) {
        try {
          await controller.addMarker(MarkerOptions(
            id: options.vehicleMarkerId,
            position: markerPos,
            iconUrl: options.vehicleIconUrl,
            iconDp: options.vehicleIconDp,
            anchorU: options.vehicleIconAnchorU,
            anchorV: options.vehicleIconAnchorV,
            rotation: (options.vehicleRotationSource == VehicleRotationSource.deviceHeading)
                ? (camBearing ?? 0)
                : (options.vehicleRotationSource == VehicleRotationSource.route
                    ? (routeBearing ?? camBearing ?? 0)
                    : (_normalizeBearing(course) ?? routeBearing ?? camBearing ?? 0)),
            zIndex: 9999,
          ));
          vehicleMarkerAdded = true;
        } catch (_) {}
      } else {
        double? rot;
        switch (options.vehicleRotationSource) {
          case VehicleRotationSource.course:
            rot = _normalizeBearing(course) ?? routeBearing ?? camBearing;
            break;
          case VehicleRotationSource.route:
            rot = routeBearing ?? _normalizeBearing(course) ?? camBearing;
            break;
          case VehicleRotationSource.deviceHeading:
            rot = camBearing ?? routeBearing ?? _normalizeBearing(course);
            break;
        }
        try {
          await controller.updateMarker(options.vehicleMarkerId, position: markerPos, rotation: rot);
        } catch (_) {}
      }
    }

    await controller.animateCamera(
      camTarget,
      zoom: options.cameraZoom,
      tilt: options.cameraTilt,
      bearing: options.followBearing ? camBearing : null,
    );

    // Velocidade/alertas (opcional provider dinâmico)
    if (options.speedAlertsEnabled) {
      final now = DateTime.now();
      if (options.speedLimitProvider != null && now.difference(lastSpeedFetch) > const Duration(seconds: 20)) {
        lastSpeedFetch = now;
        try { dynSpeedLimit = await options.speedLimitProvider!(user); } catch (_) {}
      }
      final limit = dynSpeedLimit ?? options.speedLimitKmh;
      if (limit != null && speedKmh > limit + 2.0) {
        speedCtl.add(SpeedAlert(speedKmh: speedKmh, speedLimitKmh: limit, overLimit: true));
      }
    }

    // Progresso/ETA
    if (totalMeters > 0) {
      final traveled = _traveledAlongRoute(camTarget, route.points, cumul);
      final remaining = (totalMeters - traveled).clamp(0.0, totalMeters);
      double? etaSeconds;
      if (route.durationSeconds != null && totalMeters > 1) {
        final frac = remaining / totalMeters;
        etaSeconds = (route.durationSeconds! * frac).toDouble();
      } else if (speedKmh > 1) {
        etaSeconds = (remaining / (speedKmh / 3.6));
      }
      progressCtl.add(NavProgress(distanceRemainingMeters: remaining, etaSeconds: etaSeconds));
    }

    // Chegada
    final dDest = _distanceMeters(camTarget, options.destination);
      if (dDest <= options.arrivalThresholdMeters) {
        stateCtl.add(NavState.arrived);
        if (options.voiceGuidance && tts != null) {
          final txt = options.language.startsWith('pt') ? 'Você chegou ao destino' : 'You have arrived at your destination';
          try { await tts.speak(txt); } catch (_) {}
        }
        return;
      }

    // Instruções por passo (aproximação em múltiplos limiares)
    if (options.voiceGuidance && route.steps.isNotEmpty && tts != null) {
      // Passo mais próximo adiante
      int idx = _closestUpcomingStepIndex(camTarget, route.steps) ?? 0;
      final step = route.steps[idx];
      final d = _distanceMeters(camTarget, step.startLocation);
      final spokenSet = spokenByStep.putIfAbsent(idx, () => <double>{});
      for (final th in options.approachSpeakMeters) {
        if (!spokenSet.contains(th) && d <= th) {
          spokenSet.add(th);
          final text = _instructionVoiceText(step, options.language);
          try {
            if (options.interruptOnNewInstruction) { try { await tts.stop(); } catch (_) {} }
            await tts.speak(text);
          } catch (_) {}
          instCtl.add(NavInstruction(stepIndex: idx, text: step.instructionText, maneuver: step.maneuver, distanceMeters: d.round()));
          break;
        }
      }
    }

    // Off-route e Reroute
    final off = _distanceToPolylineMeters(camTarget, route.points);
    if (options.rerouteOnOffRoute && off > options.offRouteThresholdMeters) {
      stateCtl.add(NavState.offRoute);
      final now = DateTime.now();
      if (now.difference(lastReroute) >= options.minTimeBetweenReroutes) {
        lastReroute = now;
        stateCtl.add(NavState.rerouting);
        try {
          if (options.useRoutesV2) {
            final res = await RoutesApi.computeRoutes(
              apiKey: options.apiKey,
              origin: Waypoint(location: camTarget),
              destination: Waypoint(location: options.destination),
              intermediates: options.intermediates,
              languageCode: options.language,
              alternatives: false,
            );
            if (res.routes.isNotEmpty) {
              final r = res.routes.first;
              final newSteps = <DirectionStep>[];
              for (final leg in r.legs) {
                for (final st in leg.steps) {
                  LatLng? s;
                  LatLng? e;
                  if (st.points.isNotEmpty) { s = st.points.first; e = st.points.last; }
                  newSteps.add(DirectionStep(
                    startLocation: s ?? camTarget,
                    endLocation: e ?? options.destination,
                    distanceMeters: st.distanceMeters ?? 0,
                    instructionHtml: st.instruction ?? '',
                    maneuver: st.maneuver,
                  ));
                }
              }
              await controller.addPolyline(PolylineOptions(id: polylineId, points: r.points, color: routeClr, width: options.routeWidth));
              route.points..clear()..addAll(r.points);
              route.steps..clear()..addAll(newSteps);
              cumul..clear()..addAll(_cumulativeDistances(route.points));
              spokenByStep.clear();
            }
          } else {
            final newRoute = await DirectionsService.fetchRoute(
              apiKey: options.apiKey,
              origin: camTarget,
              destination: options.destination,
              mode: options.mode,
              language: options.language,
            );
            await controller.addPolyline(PolylineOptions(id: polylineId, points: newRoute.points, color: routeClr, width: options.routeWidth));
            route.points..clear()..addAll(newRoute.points);
            cumul..clear()..addAll(_cumulativeDistances(route.points));
            spokenByStep.clear();
          }
          stateCtl.add(NavState.navigating);
        } catch (_) {
          stateCtl.add(NavState.navigating);
        }
      }
    }
  }

  StreamSubscription<Position>? sub;
  if (options.simulationSpeedKmh != null && totalMeters > 0) {
    // Simulação baseada no polyline
    double progress = 0.0; // metros ao longo da rota
    shared.simSpeedKmh = options.simulationSpeedKmh!.clamp(1.0, 200.0);
    shared.simTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
      if (shared.paused || shared.closed) return;
      final spd = shared.simSpeedKmh.clamp(1.0, 200.0);
      progress = (progress + spd / 3.6).clamp(0.0, totalMeters);
      final interp = _interpolateAlongRoute(route.points, cumul, progress);
      await onPosition(interp.position, speedKmh: spd, course: interp.bearing);
    });
  } else {
    sub = Geolocator.getPositionStream(
      locationSettings: const LocationSettings(accuracy: LocationAccuracy.best, distanceFilter: 2),
    ).listen((pos) async {
      final user = LatLng(pos.latitude, pos.longitude);
      final speedKmh = (pos.speed.isFinite ? pos.speed : 0.0) * 3.6;
      final course = pos.heading.isFinite ? pos.heading : _bearingFromVelocity(pos);
      await onPosition(user, speedKmh: speedKmh, course: course);
    });
  }

  final sess = NavigationSession._(controller, options, route, sub, tts, polylineId, shared, stateCtl, instCtl, progressCtl, speedCtl);
  sess._headingSub = headingSub;
  return sess;
}