Skip to content

Latest commit

 

History

History
1127 lines (954 loc) · 39.1 KB

File metadata and controls

1127 lines (954 loc) · 39.1 KB

Routing Functionality Implementation Guide

This document provides a comprehensive guide for implementing routing functionality in a Flutter map application using the Magic Lane SDK. The routing system includes transport mode selection, route calculation, and route display with detailed navigation information.

Routing demo

Table of Contents

Overview

The routing functionality consists of five main components:

  1. Transport Mode Button - A floating action button for selecting transportation modes
  2. Routing State Provider - State management for routing operations
  3. Routing Engine - Core routing calculation and map integration service
  4. Routing Panel - UI component displaying route details and navigation options
  5. Route Integration - Integration with map display and landmark selection

Architecture

lib/routing/
├── transport_mode_button.dart  # Transport mode selection UI
├── routing_engine.dart         # Core routing service
└── routing_panel.dart          # Route details display

lib/providers/
└── routing_state_provider.dart # Routing state management

lib/shared/
└── constants/
    └── app_constants.dart      # Application-wide constants

The routing system integrates with:

  • Map Controller - For route display and centering
  • Position Service - For current location as departure point
  • Selected Landmark Provider - For destination selection
  • Magic Lane RoutingService - For route calculation

Implementation

1. App Constants

lib/shared/constants/app_constants.dart

class AppConstants {
  /// Private constructor to prevent instantiation of this utility class.
  AppConstants._();

  // ═══════════════════════════════════════════════════════════════════════════
  // COLORS
  // ═══════════════════════════════════════════════════════════════════════════

  /// Primary brand color used throughout the application
  static const Color primaryColor = Colors.deepPurple;

  /// Standard grey color for subtle UI elements (Material Design Grey 400)
  static const Color grey400 = Color(0xFFBDBDBD);

  /// Darker grey color for secondary text and disabled states (Material Design Grey 700)
  static const Color grey700 = Color(0xFF616161);

  // ═══════════════════════════════════════════════════════════════════════════
  // SIZING
  // ═══════════════════════════════════════════════════════════════════════════

  /// Icon size for the follow position button
  static const double followPositionButtonIconSize = 24.0;

  // ═══════════════════════════════════════════════════════════════════════════
  // MESSAGES (TODO: Move to localization)
  // ═══════════════════════════════════════════════════════════════════════════

  /// Error message shown when map controller is not ready
  static const String mapNotReadyMessage = 'Map not ready yet';

  /// Error message shown when location permission is denied
  static const String locationPermissionNotGrantedMessage = 'Location permission not granted';

  // ═══════════════════════════════════════════════════════════════════════════
  // HERO TAGS
  // ═══════════════════════════════════════════════════════════════════════════

  /// Unique identifier for the follow position floating action button
  static const String followPositionButtonHeroTag = 'followPositionButton';

  /// Unique identifier for the transport mode floating action button
  static const String transportModeButtonHeroTag = 'transportModeButton';

  // ═══════════════════════════════════════════════════════════════════════════
  // SPACING
  // ═══════════════════════════════════════════════════════════════════════════

  /// Standard padding used throughout the application (16dp)
  static const double defaultPadding = 16.0;

  /// Small padding for tight spacing (8dp)
  static const double smallPadding = 8.0;

  /// Large padding for generous spacing (24dp)
  static const double largePadding = 24.0;

  // ═══════════════════════════════════════════════════════════════════════════
  // BORDER RADIUS
  // ═══════════════════════════════════════════════════════════════════════════

  /// Default border radius for consistent rounded corners
  static const double defaultBorderRadius = 12.0;

  // ═══════════════════════════════════════════════════════════════════════════
  // MODAL BOTTOM SHEET
  // ═══════════════════════════════════════════════════════════════════════════

  /// Width of the drag handle on modal bottom sheets
  static const double modalHandleWidth = 40.0;

  /// Height of the drag handle on modal bottom sheets
  static const double modalHandleHeight = 4.0;

  /// Border radius for the modal bottom sheet drag handle
  static const double modalHandleBorderRadius = 2.0;
}

2. Transport Mode Button

The transport mode button provides an intuitive interface for users to select their preferred transportation method.

lib/routing/transport_mode_button.dart

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

  /// Returns the appropriate icon for each transport mode
  IconData _iconForMode(RouteTransportMode mode) {
    switch (mode) {
      case RouteTransportMode.car:
        return Icons.directions_car;
      case RouteTransportMode.lorry:
        return Icons.local_shipping;
      case RouteTransportMode.pedestrian:
        return Icons.directions_walk;
      case RouteTransportMode.bicycle:
        return Icons.directions_bike;
      case RouteTransportMode.public:
        return Icons.directions_transit;
      case RouteTransportMode.sharedVehicles:
        return Icons.two_wheeler;
    }
  }

  @override
  Widget build(BuildContext context) {
    final provider = context.watch<RoutingStateProvider>();
    final current = provider.transportMode;

    return FloatingActionButton(
      heroTag: AppConstants.transportModeButtonHeroTag,
      backgroundColor: AppConstants.primaryColor,
      foregroundColor: Colors.white,
      child: Icon(_iconForMode(current)),

      onPressed: () {
        showModalBottomSheet<void>(
          context: context,
          isScrollControlled: true,
          shape: const RoundedRectangleBorder(
            borderRadius: BorderRadius.vertical(
              top: Radius.circular(AppConstants.defaultBorderRadius)
            ),
          ),
          builder: (context) {
            return Padding(
              padding: EdgeInsets.only(
                bottom: MediaQuery.of(context).viewInsets.bottom,
                left: AppConstants.defaultPadding,
                right: AppConstants.defaultPadding,
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const SizedBox(height: AppConstants.defaultPadding),
                  // Modal handle
                  Container(
                    width: AppConstants.modalHandleWidth,
                    height: AppConstants.modalHandleHeight,
                    decoration: BoxDecoration(
                      color: AppConstants.grey400,
                      borderRadius: BorderRadius.circular(
                        AppConstants.modalHandleBorderRadius
                      ),
                    ),
                  ),
                  const SizedBox(height: AppConstants.defaultPadding),
                  Text(
                    AppLocalizations.of(context)!.selectTransportMode,
                    style: const TextStyle(
                      fontSize: 16, 
                      fontWeight: FontWeight.bold
                    ),
                  ),
                  const SizedBox(height: AppConstants.smallPadding),
                  // Transport mode options
                  ...RouteTransportMode.values.map((mode) {
                    final label = _labelForMode(context, mode);
                    final selected = mode == provider.transportMode;
                    return ListTile(
                      leading: Icon(
                        _iconForMode(mode),
                        color: selected 
                          ? AppConstants.primaryColor 
                          : AppConstants.grey700,
                      ),
                      title: Text(label),
                      trailing: selected 
                        ? const Icon(
                            Icons.check, 
                            color: AppConstants.primaryColor
                          ) 
                        : null,
                      onTap: () {
                        provider.setTransportMode(mode);
                        Navigator.of(context).pop();
                      },
                    );
                  }),
                  const SizedBox(height: AppConstants.defaultPadding),
                ],
              ),
            );
          },
        );
      },
    );
  }

  /// Returns the localized label for each transport mode
  String _labelForMode(BuildContext context, RouteTransportMode mode) {
    final localizations = AppLocalizations.of(context)!;
    switch (mode) {
      case RouteTransportMode.car:
        return localizations.transportModeCar;
      case RouteTransportMode.lorry:
        return localizations.transportModeLorry;
      case RouteTransportMode.pedestrian:
        return localizations.transportModePedestrian;
      case RouteTransportMode.bicycle:
        return localizations.transportModeBicycle;
      case RouteTransportMode.public:
        return localizations.transportModePublic;
      case RouteTransportMode.sharedVehicles:
        return localizations.transportModeSharedVehicles;
    }
  }
}

Key Features:

  • Modal Bottom Sheet: Clean, accessible interface for mode selection
  • Visual Feedback: Shows current selection with icons and checkmarks
  • Internationalization: Fully localized transport mode labels
  • Consistent Design: Uses app constants for styling consistency
  • Hero Animation: Prevents conflicts with multiple FABs

3. Routing State Provider

Manages the routing state and transport mode selection throughout the application.

lib/providers/routing_state_provider.dart

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

/// Defines the different states of the routing feature
enum RoutingState {
  /// No routing active
  inactive,
  /// Actively calculating route
  calculatingRoute,
  /// Viewing route details
  viewingRoute,
}

/// Provider for managing the routing state throughout the app
class RoutingStateProvider extends ChangeNotifier {
  RoutingState _state = RoutingState.inactive;
  RouteTransportMode _transportMode = RouteTransportMode.car;
  Route? _currentRoute;
  Landmark? _destinationLandmark;

  // Getters
  RoutingState get state => _state;
  RouteTransportMode get transportMode => _transportMode;
  Route? get currentRoute => _currentRoute;
  Landmark? get destinationLandmark => _destinationLandmark;

  /// Set the routing state to a new value
  void setState(RoutingState newState) {
    if (_state != newState) {
      _state = newState;
      notifyListeners();
    }
  }

  /// Switch to route calculation state
  void calculateRoute() {
    setState(RoutingState.calculatingRoute);
  }

  /// Switch to route viewing state with the specified route
  void viewRoute({Route? route}) {
    final routeChanged = _currentRoute != route;
    final stateChanged = _state != RoutingState.viewingRoute;

    _currentRoute = route;
    _state = RoutingState.viewingRoute;

    // Notify listeners if either route or state changed
    if (routeChanged || stateChanged) {
      notifyListeners();
    }
  }

  /// Reset to inactive state
  void reset() {
    final stateChanged = _state != RoutingState.inactive;
    final routeChanged = _currentRoute != null;

    _state = RoutingState.inactive;
    _currentRoute = null;

    // Notify listeners if either route or state changed
    if (routeChanged || stateChanged) {
      notifyListeners();
    }
  }

  void setDestinationLandmark(Landmark landmark) {
    _destinationLandmark = landmark;
    notifyListeners();
  }

  void setTransportMode(RouteTransportMode mode) {
    if (_transportMode != mode) {
      _transportMode = mode;
      notifyListeners();
    }
  }

  /// Check if currently showing a route
  bool get isShowingRoute => _state == RoutingState.viewingRoute;

  /// Check if currently calculating a route
  bool get isCalculatingRoute => _state == RoutingState.calculatingRoute;
}

Key Features:

  • State Management: Tracks routing lifecycle (inactive → calculating → viewing)
  • Transport Mode: Persists user's preferred transportation method
  • Route Storage: Maintains current route and destination landmark
  • Optimized Updates: Only notifies listeners when state actually changes
  • Convenience Methods: Boolean getters for common state checks

3. Routing Engine

Core service for route calculation and map integration.

lib/routing/routing_engine.dart

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

/// A service class for handling routing operations
class RoutingEngine {
  /// Calculate a route between a departure and destination landmark (departure is current position which is required)
  static TaskHandler? calculateRoute({
    required BuildContext context,
    required Landmark destinationLandmark,
    required Function(GemError err, List<Route>? routes) onComplete,
    RoutePreferences? preferences,
  }) {
    final gemMapProvider = context.read<GemMapProvider>();
    final routingStateProvider = context.read<RoutingStateProvider>();
    if (gemMapProvider.controller == null) {
      onComplete(GemError.networkFailed, null);
      return null;
    }

    // Get current position as departure
    final Landmark departureLandmark = gemMapProvider.getCurrentPositionLandmark();
    if (departureLandmark.coordinates.latitude == 0 && departureLandmark.coordinates.longitude == 0) {
      onComplete(GemError.networkFailed, null);
      return null;
    }

    // Use provided preferences or create default ones
    final routePreferences = preferences ?? RoutePreferences();
    routePreferences.transportMode = routingStateProvider.transportMode;

    // Calculate route
    return RoutingService.calculateRoute([departureLandmark, destinationLandmark], routePreferences, (err, routes) {
      if (err == GemError.success && routes.isNotEmpty) {
        onComplete(err, routes);
        context.read<RoutingStateProvider>().setDestinationLandmark(destinationLandmark);
      } else {
        onComplete(err, null);
      }
    });
  }
}

Key Features:

  • Automatic Departure: Uses current GPS position as start point
  • Transport Mode Integration: Applies selected transport mode to route preferences
  • Error Handling: Graceful fallbacks for position and network failures
  • Map Integration: Direct integration with map controller for route display
  • Flexible API: Supports custom route preferences and completion callbacks

4. Gem Map Provider Enhancements

Add the follwoing methods to GemMapProvider for managing routes display and centering:

/// Display a route on the map
void displayRoute({required Route route, required bool isMainRoute, String? label}) {
  if (_controller == null) ErrorService.handleError('GemMapController is not initialized', context: 'Display Route');

  // Add route to map view
  _controller!.preferences.routes.add(route, isMainRoute, autoGenerateLabel: true);
}

/// Clear all routes from the map
void clearRoutes() {
  if (_controller == null) {
    ErrorService.handleError('GemMapController is not initialized', context: 'Clear Routes');
    return;
  }

  _controller!.preferences.routes.clear();
}

/// Center the map on a route
void centerOnRoute(Route route) {
  if (_controller == null) {
    ErrorService.handleError('GemMapController is not initialized', context: 'Center On Route');
    return;
  }

  final vp = _controller!.viewport;

  // Center above the routing panel
  _controller!.centerOnRoute(route, screenRect: Rectangle(0, 200, vp.width, vp.height ~/ 2));
}

/// Get the current position as a Landmark
Landmark getCurrentPositionLandmark() {
  // Get the current position from the position service
  final position = PositionService.position;

  // If we have a position, return a landmark
  if (position != null) {
    return Landmark.withLatLng(latitude: position.latitude, longitude: position.longitude);
  }

  // Fallback to map center if position service fails
  if (_controller != null) {
    final screenCenter = _controller!.viewportCenter;
    final mapCoords = _controller!.transformScreenToWgs(screenCenter);
    return Landmark.withLatLng(latitude: mapCoords.latitude, longitude: mapCoords.longitude);
  }
  ErrorService.handleError('GemMapController is not initialized', context: 'Get Current Position Landmark');
  // Default fallback landmark at (0,0)
  return Landmark.withLatLng(latitude: 0, longitude: 0);
}

5. Routing Panel

UI component that displays detailed route information and navigation options.

lib/routing/routing_panel.dart

import 'package:intl/intl.dart';
import 'package:flutter/material.dart' hide Route; // Avoid name conflict between Magic Lane and Flutter Route

class RoutingPanel extends StatefulWidget {
  const RoutingPanel({super.key});

  @override
  State<RoutingPanel> createState() => _RoutingPanelState();
}

class _RoutingPanelState extends State<RoutingPanel> {
  @override
  Widget build(BuildContext context) {
    final route = context.watch<RoutingStateProvider>().currentRoute;
    if (route == null) {
      return const SizedBox.shrink();
    }

    return Container(
      margin: EdgeInsets.only(
        left: 16.0,
        right: 16.0,
        bottom: MediaQuery.of(context).padding.bottom + 16.0
      ),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.0),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withAlpha(80),
            blurRadius: 10.0,
            offset: const Offset(0, 3)
          )
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _buildHeader(route),
          _buildContent(route),
          _buildActions(context, route)
        ],
      ),
    );
  }

  // Build the header section with destination name and close button
  Widget _buildHeader(Route route) {
    final destinationLandmark = context.watch<RoutingStateProvider>()
        .destinationLandmark;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      decoration: BoxDecoration(
        color: Colors.deepPurple[700],
        borderRadius: const BorderRadius.only(
          topLeft: Radius.circular(12.0),
          topRight: Radius.circular(12.0)
        ),
      ),
      child: Row(
        children: [
          Expanded(
            child: Text(
              AppLocalizations.of(context)!.routeTo(
                destinationLandmark?.name ?? 
                AppLocalizations.of(context)!.destination
              ),
              style: const TextStyle(
                fontSize: 18.0,
                fontWeight: FontWeight.bold,
                color: Colors.white
              ),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.close, color: Colors.white),
            onPressed: () {
              _clearPresentedRoutes(context);
            },
          ),
        ],
      ),
    );
  }

  // Build the content section with route details
  Widget _buildContent(Route route) {
    // Get route time and distance information
    final timeDistance = route.getTimeDistance();

    // Calculate estimated arrival time
    final now = DateTime.now();
    final arrivalTime = now.add(Duration(seconds: timeDistance.totalTimeS));
    final formattedArrivalTime = DateFormat('HH:mm').format(arrivalTime);

    // Convert distance to km
    final distanceInKm = FormatUtils.convertDistance(timeDistance.totalDistanceM);

    // Convert time to minutes
    final totalTimeString = FormatUtils.convertDuration(timeDistance.totalTimeS);

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Total Distance
          Row(
            children: [
              const Icon(Icons.straighten, color: Colors.deepPurple),
              const SizedBox(width: 8.0),
              Expanded(
                child: Text(
                  AppLocalizations.of(context)!.totalDistance(distanceInKm)
                )
              ),
            ],
          ),
          const SizedBox(height: 12.0),

          // Total Time
          Row(
            children: [
              const Icon(Icons.timer, color: Colors.deepPurple),
              const SizedBox(width: 8.0),
              Expanded(
                child: Text(
                  AppLocalizations.of(context)!.travelTime(totalTimeString)
                )
              ),
            ],
          ),
          const SizedBox(height: 12.0),

          // ETA
          Row(
            children: [
              const Icon(Icons.access_time_filled, color: Colors.deepPurple),
              const SizedBox(width: 8.0),
              Expanded(
                child: Text(
                  AppLocalizations.of(context)!.eta(formattedArrivalTime)
                )
              ),
            ],
          ),

          // Additional route warnings
          if (route.hasTollRoads) ...[
            const SizedBox(height: 12.0),
            Row(
              children: [
                const Icon(Icons.attach_money, color: Colors.orange),
                const SizedBox(width: 8.0),
                Expanded(
                  child: Text(
                    AppLocalizations.of(context)!.tollRoadsWarning
                  )
                ),
              ],
            ),
          ],

          if (route.hasFerryConnections) ...[
            const SizedBox(height: 12.0),
            Row(
              children: [
                const Icon(Icons.directions_boat, color: Colors.blue),
                const SizedBox(width: 8.0),
                Expanded(
                  child: Text(
                    AppLocalizations.of(context)!.ferryConnectionsWarning
                  )
                ),
              ],
            ),
          ],
        ],
      ),
    );
  }

  // Build the action buttons for navigation and simulation
  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: () {
                // TODO: Implement navigation functionality
              },
            ),
          ),
          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: () {
                // TODO: Implement simulation functionality
              },
            ),
          ),
        ],
      ),
    );
  }

  // Reset map view and clear routes
  void _clearPresentedRoutes(BuildContext context) {
    final gemMapProvider = context.read<GemMapProvider>();
    final selectedLandmarkProvider = context.read<SelectedLandmarkStateProvider>();
    final routingStateProvider = context.read<RoutingStateProvider>();

    if (gemMapProvider.controller != null) {
      gemMapProvider.controller!.deactivateAllHighlights();
    }

    selectedLandmarkProvider.clear();
    routingStateProvider.reset();

    // Clear routes
    gemMapProvider.clearRoutes();
  }
}

Key Features:

  • Comprehensive Route Info: Distance, time, ETA with formatted display
  • Route Warnings: Visual indicators for toll roads and ferry connections
  • Action Buttons: Navigate and simulate options (ready for implementation)
  • State Integration: Automatically clears related state when closed

6. Route selection

Inside the lib/widgets/app_map.dart update the _registerOnMapTouchGesture method to include route selection logic:

void _registerMapTouchGesture(GemMapController controller) {
  controller.registerOnTouch((pos) async {
    final gemProvider = context.read<GemMapProvider>();
    final landmarkProvider = context.read<SelectedLandmarkStateProvider>();
    final routingProvider = context.read<RoutingStateProvider>();
    await controller.setCursorScreenPosition(pos);

    // First, check if the user tapped on any displayed routes. If so,
    // set that route as the main route and update app state.
    final tappedRoutes = controller.cursorSelectionRoutes();

    if (tappedRoutes.isNotEmpty) {
      // Mark the tapped route as the main route in the SDK
      controller.preferences.routes.mainRoute = tappedRoutes[0];
      // Update routing provider state to show this route
      routingProvider.viewRoute(route: tappedRoutes[0]);
      // Clear any landmark highlights / selection
      gemProvider.controller?.deactivateAllHighlights();
      landmarkProvider.clear();
      // Center the camera on the selected route without using BuildContext
      final vp = controller.viewport;
      controller.centerOnRoute(tappedRoutes[0], screenRect: Rectangle(0, 200, vp.width, vp.height ~/ 2));
      return;
    }

    // Landmark selection behavior
    final landmarks = controller.cursorSelectionLandmarks();
    
    if (landmarks.isNotEmpty) {
      controller.activateHighlight(landmarks);
      landmarkProvider.setSelected(landmarks[0]);
      controller.centerOnCoordinates(
        landmarks[0].coordinates,
        zoomLevel: 70,
        animation: GemAnimation(type: AnimationType.linear, duration: 1),
        viewAngle: 0.0,
        screenPosition: controller.viewportCenter,
      );
    }
  });
}

Integration Requirements

Provider Setup

The routing functionality requires these providers to be configured in your app:

// lib/providers/app_providers.dart
class AppProviders extends StatelessWidget {
  final Widget child;

  const AppProviders({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => GemMapProvider()),
        ChangeNotifierProvider(create: (_) => FollowPositionProvider()),
        ChangeNotifierProvider(create: (_) => SelectedLandmarkStateProvider()),
        ChangeNotifierProvider(create: (_) => RoutingStateProvider()),
      ],
      child: child,
    );
  }
}

Utility Functions

Add this utility function for proper duration formatting:

lib/utils/formatting.dart

// lib/utils/formatting.dart
class FormatUtils {
  // Convert duration in seconds to readable format
  static String convertDuration(int seconds) {
    if (seconds < 60) {
      return '${seconds}s';
    } else if (seconds < 3600) {
      final minutes = seconds ~/ 60;
      return '${minutes}min';
    } else {
      final hours = seconds ~/ 3600;
      final minutes = (seconds % 3600) ~/ 60;
      return minutes > 0 ? '${hours}h ${minutes}min' : '${hours}h';
    }
  }
}

Localization Strings

Add these localization keys to your app localization files:

// Required localization keys:
- selectTransportMode
- transportModeCar
- transportModeLorry  
- transportModePedestrian
- transportModeBicycle
- transportModePublic
- transportModeSharedVehicles
- routeTo
- destination
- totalDistance
- travelTime
- eta
- tollRoadsWarning
- ferryConnectionsWarning
- navigate
- simulate
- calculating          // For route calculation loading state
- route               // For route button label
- routingNotAvailable // When GPS position unavailable

State Variables

Add these state variables to components that integrate with routing:

// In LandmarkPanel or similar components
class _LandmarkPanelState extends State<LandmarkPanel> {
  TaskHandler? _routingHandler;  // Handle for route calculation task
  bool _isCalculatingRoute = false;  // Loading state for UI
  
  // ... rest of implementation
}

Usage in Your App

1. Add Transport Mode Button to Map Interface

lib/map/map_overlay_manager.dart

class _BottomControlsWidget extends StatelessWidget {
  const _BottomControlsWidget();

  @override
  Widget build(BuildContext context) {
    return Selector2<SelectedLandmarkStateProvider, RoutingStateProvider, bool>( // New condition added
      selector: (context, landmark, routing) => landmark.selected == null && routing.currentRoute == null,
      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(), // New transport mode button
                      const SizedBox(width: 10),
                      const Expanded(child: SearchBarWidget()),
                      const SizedBox(width: 10),
                      const FollowPositionButton(),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

2. Integrate with Landmark Selection

The actual integration with landmark selection is implemented in the LandmarkPanel component:

lib/landmark/landmark_panel.dart

// In landmark_panel.dart - Route calculation button implementation
Widget _buildActions(BuildContext context, Landmark landmark) {
  return Padding(
    padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        PositionService.position != null
          ? Expanded(
              child: ElevatedButton.icon(
                icon: _isCalculatingRoute 
                  ? const LoadingIndicator.button() 
                  : const Icon(Icons.directions),
                label: Text(
                  _isCalculatingRoute
                    ? AppLocalizations.of(context)!.calculating
                    : AppLocalizations.of(context)!.route,
                ),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.deepPurple[700],
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(vertical: 12.0),
                ),
                onPressed: _isCalculatingRoute 
                  ? null 
                  : () => _calculateRoute(context, landmark),
              ),
            )
          : Expanded(
              child: ElevatedButton.icon(
                icon: const Icon(Icons.location_disabled),
                label: Text(AppLocalizations.of(context)!.routingNotAvailable),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.grey,
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(vertical: 12.0),
                ),
                onPressed: null,
              ),
            ),
      ],
    ),
  );
}

// Route calculation implementation
void _calculateRoute(BuildContext context, Landmark destinationLandmark) {
  final routingStateProvider = context.read<RoutingStateProvider>();
  final gemMapProvider = context.read<GemMapProvider>();

  // Set state to calculating - with mounted check
  if (mounted) {
    setState(() {
      _isCalculatingRoute = true;
    });
  }
  routingStateProvider.calculateRoute();

  _routingHandler = RoutingEngine.calculateRoute(
    context: context,
    destinationLandmark: destinationLandmark,
    onComplete: (err, routes) {
      // Safety check: only call setState if widget is still mounted
      if (mounted) {
        setState(() {
          _isCalculatingRoute = false;
          _routingHandler = null;
        });
      }

      // Early return if widget is disposed
      if (!mounted) return;

      if (err == GemError.success && routes != null && routes.isNotEmpty) {
        // Clear any existing routes
        gemMapProvider.clearRoutes();
        context.read<SelectedLandmarkStateProvider>().clear();

        // Add each route to the map
        for (final route in routes) {
          gemMapProvider.displayRoute(route: route, isMainRoute: route == routes.first);
        }

        // Center the camera on the route
        gemMapProvider.centerOnRoute(routes.first);

        // Pass the route to the routing state provider and switch to route view
        routingStateProvider.viewRoute(route: routes.first);
      }
    },
  );
  if (_routingHandler == null) {
    ErrorService.handleError('Failed to start route calculation', context: 'Route calculation');
  }
}

Key Integration Features:

  • Position-Aware UI: Disables routing when GPS position unavailable
  • Loading States: Shows loading indicator during route calculation
  • Error Handling: Uses ErrorService for proper error reporting
  • State Management: Updates both local and provider state
  • Route Display: Handles multiple routes with main route prioritization
  • Map Integration: Automatically centers map on calculated route
  • Clean Transitions: Clears landmark selection when switching to route view

### 3. Add Routing Panel to Overlay Manager

`lib/widgets/map_overlay_manager.dart`

```dart
// In your map overlay manager
class _DynamicBottomPanel extends StatelessWidget {
  const _DynamicBottomPanel();

  @override
  Widget build(BuildContext context) {
    return Selector2<SelectedLandmarkStateProvider, RoutingStateProvider, _PanelState>(
      selector: (context, landmark, routing) => _PanelState(landmark: landmark.selected, route: routing.currentRoute),
      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();
    }

    return const SizedBox.shrink();
  }
}

4. Add Route to MapOverlayManager _PanelState

class _PanelState {
  final Landmark? landmark;
  final Route? route; // New route field

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

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

  @override
  int get hashCode => Object.hashAll([landmark, route]);
}

Features

Core Routing Features

  • Multi-Modal Transport: Support for car, truck, walking, cycling, and public transport
  • Real-time Calculation: Uses Magic Lane RoutingService for accurate routes
  • Current Position Integration: Automatically uses GPS or map center as departure
  • Route Visualization: Direct integration with map display
  • Route Centering: Automatic map adjustment to show entire route

Transport Mode Features

  • Visual Selection: Icon-based interface with modal bottom sheet
  • Persistent Settings: Remembers user's preferred transport mode
  • Internationalization: Fully localized mode labels
  • Accessibility: Proper hero tags and semantic labels

Route Information Features

  • Comprehensive Details: Distance, time, ETA with proper formatting
  • Route Warnings: Visual indicators for tolls and ferry connections
  • Real-time ETA: Calculated arrival time based on current time
  • Professional UI: Card-based design with consistent styling

This implementation provides a robust, professional routing experience that integrates seamlessly with the Magic Lane SDK and follows Flutter best practices for state management, UI design, and user experience.