Skip to content

Latest commit

 

History

History
815 lines (682 loc) · 27.2 KB

route_navigation_tutorial.md

File metadata and controls

815 lines (682 loc) · 27.2 KB

Route Navigation and Navigation Simulation Implementation Guide

This document describes how to implement route navigation and navigation simulation functionality in a Flutter mapping application using the Magic Lane SDK. The implementation provides both real GPS-based navigation and simulated navigation for testing and demonstration purposes.

Simulated navigation demo

Table of Contents

Architecture

The navigation system consists of several key components:

lib/navigation/
├── navigation_engine.dart          # Core navigation service
├── navigation_top_panel.dart       # Turn-by-turn instruction panel
└── navigation_bottom_panel.dart    # Trip progress panel

lib/providers/
└── navigation_state_provider.dart  # Navigation state management

The navigation system integrates with:

  • Routing System - For route calculation and display
  • Map Controller - For GPS following and map updates
  • Position Service - For real-time location tracking
  • Magic Lane NavigationService - For turn-by-turn navigation

Core Components

1. Navigation State Provider

Manages the navigation state throughout the application lifecycle.

lib/providers/navigation_state_provider.dart

import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';

// Provider for managing the navigation state throughout the app
class NavigationStateProvider extends ChangeNotifier { 
  bool _isNavigating = false;
  NavigationInstruction? _currentInstruction;
  TaskHandler? _navigationHandler;

  bool get isNavigating => _isNavigating;
  NavigationInstruction? get currentInstruction => _currentInstruction;
  TaskHandler? get navigationHandler => _navigationHandler;

  set currentInstruction(NavigationInstruction? instruction) {
    _currentInstruction = instruction;
    notifyListeners();
  }

  void startNavigation(TaskHandler navigationHandler) {
    if (!_isNavigating) {
      _isNavigating = true;
      _navigationHandler = navigationHandler;
      notifyListeners();
    }
  }

  void stopNavigation() {
    if (_isNavigating) {
      _isNavigating = false;
      _navigationHandler = null;
      notifyListeners();
    }
  }
}

Key Features:

  • Navigation State Tracking - Manages active navigation status
  • Instruction Management - Stores current turn-by-turn instructions
  • Handler Management - Maintains reference to navigation task handler
  • State Notifications - Notifies UI of navigation state changes

2. Navigation Engine

Core service that handles navigation and simulation operations.

lib/navigation/navigation_engine.dart

import 'package:flutter/material.dart' hide Route; // Avoid name conflict with Magic Lane SDK Route class
import 'package:provider/provider.dart' as provider;

class NavigationEngine {
  /// Internal helper method that safely starts navigation or simulation
  static void _startNavigation(
    BuildContext context,
    TaskHandler? Function(
      Route route, {
      required void Function(NavigationInstruction, dynamic) onNavigationInstruction,
      required void Function(GemError) onError,
      void Function(Landmark)? onDestinationReached,
    })
    navigationStarter,
  ) {
    // Capture all necessary providers up front
    final gemMapProvider = provider.Provider.of<GemMapProvider>(context, listen: false);
    final navigationStateProvider = provider.Provider.of<NavigationStateProvider>(context, listen: false);

    final controller = gemMapProvider.controller;
    if (controller == null) return;

    final routes = controller.preferences.routes;
    if (routes.mainRoute == null) {
      ErrorService.showWarning('No main route available');
      return;
    }

    // Clear any existing routes on map except the main route and remove its label
    controller.preferences.routes.clearAllButMainRoute();
    controller.preferences.routes.hideLabel(routes.mainRoute!);

    void errorHandler(GemError error) {
      if (error == GemError.cancel) return;
      navigationStateProvider.stopNavigation();
      NavigationService.cancelNavigation(navigationStateProvider.navigationHandler);
      gemMapProvider.controller?.stopFollowingPosition();
      gemMapProvider.controller?.preferences.routes.clear();

      ErrorService.handleGemError(error, context: 'Navigation');
    }

    void destinationReachedHandler(Landmark landmark) {
      navigationStateProvider.stopNavigation();

      gemMapProvider.controller?.stopFollowingPosition();
      gemMapProvider.controller?.preferences.routes.clear();
      gemMapProvider.controller?.deactivateAllHighlights();

      ErrorService.showSuccess('Destination reached: ${landmark.name}');
    }

    // Start navigation
    final navigationHandler = navigationStarter(
      routes.mainRoute!,
      onNavigationInstruction: (instruction, events) {
        navigationStateProvider.currentInstruction = instruction;
      },
      onError: errorHandler,
      onDestinationReached: destinationReachedHandler,
    );

    if (navigationHandler == null) {
      ErrorService.handleError('Failed to start navigation', context: 'Navigation');
      return;
    }

    navigationStateProvider.startNavigation(navigationHandler);
    controller.startFollowingPosition();
  }

  static void startNavigation(BuildContext context) {
    _startNavigation(context, (route, {required onNavigationInstruction, required onError, onDestinationReached}) {
      return NavigationService.startNavigation(
        route,
        onNavigationInstruction: onNavigationInstruction,
        onError: onError,
        onDestinationReached: onDestinationReached,
      );
    });
  }

  static void startSimulation(BuildContext context) {
    _startNavigation(context, (route, {required onNavigationInstruction, required onError, onDestinationReached}) {
      return NavigationService.startSimulation(
        route,
        onNavigationInstruction: onNavigationInstruction,
        onError: onError,
        onDestinationReached: onDestinationReached,
      );
    });
  }

  static void stopNavigation(BuildContext context) {
    // Stop navigation
    final navigationStateProvider = provider.Provider.of<NavigationStateProvider>(context, listen: false);
    navigationStateProvider.stopNavigation();
    NavigationService.cancelNavigation(navigationStateProvider.navigationHandler);

    // Reset map state
    final gemMapProvider = provider.Provider.of<GemMapProvider>(context, listen: false);
    gemMapProvider.controller?.stopFollowingPosition();
    gemMapProvider.controller?.deactivateAllHighlights();
    gemMapProvider.controller?.preferences.routes.clear();
  }
}

Key Features:

  • Unified Navigation Interface - Single API for both navigation and simulation
  • State Management Integration - Seamless integration with navigation state provider
  • Error Handling - Comprehensive error handling with user feedback
  • Map Integration - Automatic GPS following and route display
  • Resource Cleanup - Proper cleanup when navigation ends

3. Navigation UI Components

Top Navigation Panel - Turn-by-Turn Instructions

lib/navigation/navigation_top_panel.dart

import 'package:magiclane_maps_flutter/core.dart';
import 'package:magiclane_maps_flutter/navigation.dart';
import 'package:flutter/material.dart';

class TopNavigationPanel extends StatelessWidget {
  // The current navigation instruction to display
  final NavigationInstruction instruction;

  const TopNavigationPanel({super.key, required this.instruction});

  @override
  Widget build(BuildContext context) {
    final topInset = MediaQuery.of(context).padding.top;
    final panelHeight = MediaQuery.of(context).size.height * 0.15 + topInset;

    return Container(
      width: MediaQuery.of(context).size.width,
      height: panelHeight,
      padding: EdgeInsets.only(top: topInset, left: 10, right: 10, bottom: 10),
      decoration: const BoxDecoration(color: Colors.black),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          SizedBox(
            width: 100,
            child: instruction.nextTurnDetails != null && 
                   instruction.nextTurnDetails!.abstractGeometryImg.isValid
                ? Image.memory(
                    instruction.nextTurnDetails!.abstractGeometryImg.getRenderableImageBytes(
                      size: Size(200, 200),
                      format: ImageFileFormat.png,
                    )!,
                    gaplessPlayback: true,
                  )
                : const SizedBox(), // Empty widget
          ),
          SizedBox(
            width: MediaQuery.of(context).size.width - 150,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Text(
                  FormatUtils.getFormattedDistanceToNextTurn(instruction),
                  textAlign: TextAlign.left,
                  style: const TextStyle(
                    color: Colors.white, 
                    fontSize: 25, 
                    fontWeight: FontWeight.w600
                  ),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  instruction.nextStreetName,
                  style: const TextStyle(
                    color: Colors.white, 
                    fontSize: 20, 
                    fontWeight: FontWeight.w600
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Key Features:

  • Turn Visual - Displays turn geometry image if available
  • Distance Display - Shows formatted distance to next turn
  • Street Information - Displays next street name
  • Responsive Design - Adapts to different screen sizes
  • Safe Area - Accounts for device notches and status bars

Bottom Navigation Panel - Trip Progress

lib/navigation/navigation_bottom_panel.dart

import 'package:flutter/material.dart';

// Bottom panel widget displaying navigation details such as remaining duration, distance, and ETA.
class NavigationBottomPanel extends StatelessWidget {
  final String remainingDuration;
  final String remainingDistance;
  final String eta;

  const NavigationBottomPanel({
    super.key,
    required this.remainingDuration,
    required this.remainingDistance,
    required this.eta,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: const BorderRadius.all(Radius.circular(20)),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withValues(alpha: 0.5),
            spreadRadius: 5,
            blurRadius: 7,
            offset: const Offset(0, 3),
          ),
        ],
      ),
      height: 50,
      padding: const EdgeInsets.symmetric(horizontal: 15),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            remainingDuration,
            style: const TextStyle(
              color: Colors.black, 
              fontSize: 24, 
              fontWeight: FontWeight.w500
            ),
          ),
          Text(
            eta,
            style: const TextStyle(
              color: Colors.black, 
              fontSize: 24, 
              fontWeight: FontWeight.w500
            ),
          ),
          Text(
            remainingDistance,
            style: const TextStyle(
              color: Colors.black, 
              fontSize: 24, 
              fontWeight: FontWeight.w500
            ),
          ),
        ],
      ),
    );
  }
}

4. Top Navigation Panel

Add the following _TopNavigationWidget class:

lib/widgets/map_overlay_manager.dart

class _TopNavigationWidget extends StatelessWidget {
  const _TopNavigationWidget();

  @override
  Widget build(BuildContext context) {
    return Selector<NavigationStateProvider, NavigationInstruction?>(
      selector: (context, provider) => provider.isNavigating ? provider.currentInstruction : null,
      builder: (context, instruction, child) {
        if (instruction == null) return const SizedBox.shrink();

        return Align(
          alignment: Alignment.topCenter,
          child: TopNavigationPanel(instruction: instruction),
        );
      },
    );
  }
}

Key Features:

  • Trip Summary - Shows remaining time, distance, and ETA
  • Clean Design - White rounded container with shadow
  • Readable Layout - Large, clear text with proper spacing
  • Flexible Content - Accepts formatted strings for easy customization

5. Navigation UI Integration

The navigation UI is integrated through the map overlay manager which handles the display logic.

lib/widgets/map_overlay_manager.dart (Navigation Controls Section)

class NavigationControls extends StatelessWidget {
  final NavigationInstruction instruction;

  const NavigationControls({super.key, required this.instruction});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                FloatingActionButton.extended(
                  heroTag: 'stopNavigation',
                  backgroundColor: Colors.deepPurple,
                  foregroundColor: Colors.white,
                  icon: const Icon(Icons.stop),
                  label: Text(AppLocalizations.of(context)!.stop),
                  onPressed: () => NavigationEngine.stopNavigation(context),
                ),
                const FollowPositionButton(isInNavigationMode: true),
              ],
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 10.0),
              child: NavigationBottomPanel(
                remainingDuration: FormatUtils.getFormattedRemainingDuration(instruction),
                remainingDistance: FormatUtils.getFormattedRemainingDistance(instruction),
                eta: FormatUtils.getFormattedETA(instruction),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

6. Top Navigation Panel Integration

lib/widgets/map_overlay_manager.dart (Top Panel Integration)

class MapOverlayManager extends StatelessWidget {
  const MapOverlayManager({super.key});

  @override
  Widget build(BuildContext context) {
    return const Stack(
      children: [
        // Bottom controls - only show when not in any specific mode
        Positioned(bottom: 0, left: 0, right: 0, child: _BottomControlsWidget()),

        // New top navigation panel
        Positioned(top: 0, left: 0, right: 0, child: _TopNavigationWidget()),

        // Dynamic bottom panel (landmark/routing/navigation)
        Positioned(bottom: 0, left: 0, right: 0, child: _DynamicBottomPanel()),
      ],
    );
  }
}

Key Features:

  • Stop Navigation - Floating action button to end navigation
  • Follow Position - GPS following toggle in navigation mode
  • Progress Display - Real-time trip progress information
  • Safe Area Layout - Proper positioning avoiding system UI

7. Route Panel Integration

The route panel provides the entry point to start navigation or simulation.

lib/routing/routing_panel.dart (Navigation Actions Section)

Widget _buildActions(BuildContext context, Route route) {
  return Padding(
    padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Expanded(
          child: ElevatedButton.icon(
            icon: const Icon(Icons.near_me),
            label: Text(AppLocalizations.of(context)!.navigate),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.deepPurple[700],
              foregroundColor: Colors.white,
              padding: const EdgeInsets.symmetric(vertical: 12.0),
            ),
            onPressed: () {
              // First start navigation with current context
              NavigationEngine.startNavigation(context);

              // Then reset the routing state after navigation is started
              // This will dispose this panel but navigation is already initialized
              final routingStateProvider = context.read<RoutingStateProvider>();
              routingStateProvider.reset();
            },
          ),
        ),
        SizedBox(width: 16.0),
        Expanded(
          child: ElevatedButton.icon(
            icon: const Icon(Icons.navigation_outlined),
            label: Text(AppLocalizations.of(context)!.simulate),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.deepPurple[700],
              foregroundColor: Colors.white,
              padding: const EdgeInsets.symmetric(vertical: 12.0),
            ),
            onPressed: () {
              // First start simulation with current context
              NavigationEngine.startSimulation(context);

              // Then reset the routing state after simulation is started
              // This will dispose this panel but simulation is already initialized
              final routingStateProvider = context.read<RoutingStateProvider>();
              routingStateProvider.reset();
            },
          ),
        ),
      ],
    ),
  );
}

8. Update Map Overlay Manager _PanelState class

lib/widgets/map_overlay_manager.dart (Panel State Update)

class _PanelState {
  final Landmark? landmark;
  final Route? route;
  final bool isNavigating; // New navigation state flag
  final NavigationInstruction? instruction; // Current navigation instruction

  const _PanelState({this.landmark, this.route, required this.isNavigating, this.instruction});

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is _PanelState &&
          runtimeType == other.runtimeType &&
          landmark == other.landmark &&
          route == other.route &&
          isNavigating == other.isNavigating &&
          instruction == other.instruction;

  @override
  int get hashCode => Object.hash(landmark, route, isNavigating, instruction);
}

9. Update _BottomControlsWidget to not show in navigation mode

lib/widgets/map_overlay_manager.dart (Bottom Controls Update)

class _BottomControlsWidget extends StatelessWidget {
  const _BottomControlsWidget();

  @override
  Widget build(BuildContext context) {
    return Selector3<SelectedLandmarkStateProvider, RoutingStateProvider, NavigationStateProvider, bool>(
      selector: (context, landmark, routing, navigation) =>
          landmark.selected == null && routing.currentRoute == null && !navigation.isNavigating, // New navigation check added
      builder: (context, shouldShow, child) {
        if (!shouldShow) return const SizedBox.shrink();

        return SafeArea(
          child: Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  Row(
                    children: [
                      const TransportModeButton(),
                      const SizedBox(width: 10),
                      const Expanded(child: SearchBarWidget()),
                      const SizedBox(width: 10),
                      const FollowPositionButton(),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

Key Features:

  • Navigate Button - Starts real GPS-based navigation
  • Simulate Button - Starts simulated navigation for testing
  • State Management - Properly transitions from routing to navigation state
  • Visual Design - Consistent styling with the rest of the app

Data Formatting Utilities

The formatting utilities provide consistent display of navigation data throughout the UI.

lib/utils/formatting.dart (Navigation Functions)

// Utility functions to format various NavigationInstruction properties
static String getFormattedDistanceToNextTurn(NavigationInstruction navInstruction) {
  final totalDistanceToTurn =
      navInstruction.timeDistanceToNextTurn.unrestrictedDistanceM +
      navInstruction.timeDistanceToNextTurn.restrictedDistanceM;
  return convertDistance(totalDistanceToTurn);
}

static String getFormattedRemainingDistance(NavigationInstruction navInstruction) {
  final remainingDistance =
      navInstruction.remainingTravelTimeDistance.unrestrictedDistanceM +
      navInstruction.remainingTravelTimeDistance.restrictedDistanceM;
  return convertDistance(remainingDistance);
}

static String getFormattedRemainingDuration(NavigationInstruction navInstruction) {
  final remainingDuration =
      navInstruction.remainingTravelTimeDistance.unrestrictedTimeS +
      navInstruction.remainingTravelTimeDistance.restrictedTimeS;
  return convertDuration(remainingDuration);
}

static String getFormattedETA(NavigationInstruction navInstruction) {
  final remainingDuration =
      navInstruction.remainingTravelTimeDistance.unrestrictedTimeS +
      navInstruction.remainingTravelTimeDistance.restrictedTimeS;
  return getCurrentTime(additionalSeconds: remainingDuration);
}

// Utility function to add the given additional time to current time
static String getCurrentTime({int additionalHours = 0, int additionalMinutes = 0, int additionalSeconds = 0}) {
  var now = DateTime.now();
  var updatedTime = now.add(Duration(hours: additionalHours, minutes: additionalMinutes, seconds: additionalSeconds));
  var formatter = DateFormat('HH:mm');
  return formatter.format(updatedTime);
}

Key Features:

  • Distance Formatting - Converts meters to readable format (km/m)
  • Time Formatting - Converts seconds to readable duration
  • ETA Calculation - Provides estimated arrival time
  • Restricted/Unrestricted - Handles both types of route segments

Integration Requirements

Provider Setup

Add the navigation state provider to your app's provider tree:

return MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => GemMapProvider()),
    ChangeNotifierProvider(create: (_) => FollowPositionProvider()),
    ChangeNotifierProvider(create: (_) => SelectedLandmarkStateProvider()),
    ChangeNotifierProvider(create: (_) => RoutingStateProvider()),
    ChangeNotifierProvider(create: (_) => NavigationStateProvider()), // New provider added
  ],
  child: child,
);

Map Overlay Integration

Integrate navigation UI with your map overlay system:

class _DynamicBottomPanel extends StatelessWidget {
  const _DynamicBottomPanel();

  @override
  Widget build(BuildContext context) {
    return Selector3<SelectedLandmarkStateProvider, RoutingStateProvider, NavigationStateProvider, _PanelState>(
      selector: (context, landmark, routing, navigation) => _PanelState(
        landmark: landmark.selected,
        route: routing.currentRoute,
        isNavigating: navigation.isNavigating, // New navigation state
        instruction: navigation.currentInstruction, // Current navigation instruction
      ),
      builder: (context, state, child) {
        return Align(alignment: Alignment.bottomCenter, child: _buildPanelForState(context, state));
      },
    );
  }

  Widget _buildPanelForState(BuildContext context, _PanelState state) {
    if (state.landmark != null) {
      return const LandmarkPanel();
    }

    if (state.route != null) {
      return const RoutingPanel();
    }

    // Display navigation controls if navigating
    if (state.isNavigating && state.instruction != null) {
      return NavigationControls(instruction: state.instruction!);
    }

    return const SizedBox.shrink();
  }
}

Route Integration

Ensure your routing system can transition to navigation:

// In your routing panel or route display widget
ElevatedButton(
  onPressed: () {
    NavigationEngine.startNavigation(context); // or startSimulation(context)
    routingStateProvider.reset(); // Clear routing state
  },
  child: Text('Navigate'),
)

Usage Workflow

1. Calculate Route

Users first calculate a route using the routing system:

  • Select destination landmark
  • Choose transport mode
  • Calculate and display route

2. Start Navigation

From the route panel, users can choose:

  • Navigate - Real GPS-based navigation
  • Simulate - Simulated navigation for testing

3. Active Navigation

During navigation, the system provides:

  • Turn-by-turn instructions with visual guidance
  • Real-time trip progress (time, distance, ETA)
  • GPS following and map updates
  • Stop navigation capability

4. Navigation Completion

Navigation ends when:

  • Destination is reached (automatic)
  • User manually stops navigation
  • Navigation error occurs

Key Differences: Navigation vs Simulation

Real Navigation (NavigationService.startNavigation)

  • Uses actual GPS position
  • Updates based on real movement
  • Responds to route deviations
  • Provides real-time guidance

Simulation (NavigationService.startSimulation)

  • Uses simulated movement along route
  • Predictable progression for testing
  • No GPS dependency
  • Ideal for development and demos

Error Handling

The navigation system includes comprehensive error handling:

Navigation Errors

  • No Route Available - Warning when no main route exists
  • Navigation Start Failure - Error feedback when navigation cannot start
  • GPS Issues - Graceful handling of position service problems

Conclusion

This navigation system provides a complete solution for both real-world navigation and development/testing scenarios. The modular architecture allows for easy customization and extension, while the integration with the Magic Lane SDK ensures reliable and accurate navigation functionality.

The separation between navigation and simulation modes enables developers to test navigation features without requiring physical movement, while providing users with professional-grade turn-by-turn navigation when needed.