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.
The routing functionality consists of five main components:
- Transport Mode Button - A floating action button for selecting transportation modes
- Routing State Provider - State management for routing operations
- Routing Engine - Core routing calculation and map integration service
- Routing Panel - UI component displaying route details and navigation options
- Route Integration - Integration with map display and landmark selection
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
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;
}The transport mode button provides an intuitive interface for users to select their preferred transportation method.
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
Manages the routing state and transport mode selection throughout the application.
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
Core service for route calculation and map integration.
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
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);
}UI component that displays detailed route information and navigation options.
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
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,
);
}
});
}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,
);
}
}Add this utility function for proper duration formatting:
// 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';
}
}
}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 unavailableAdd 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
}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(),
],
),
],
),
),
),
);
},
);
}
}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();
}
}
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]);
}- 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
- 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
- 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.
