Basic routing. Still some bugs.

This commit is contained in:
stopflock
2025-10-02 20:08:21 -05:00
parent c6db4396e4
commit dfb8eceaad
4 changed files with 252 additions and 8 deletions

View File

@@ -95,6 +95,8 @@ class AppState extends ChangeNotifier {
bool get isSettingSecondPoint => _navigationState.isSettingSecondPoint;
bool get isCalculating => _navigationState.isCalculating;
bool get showingOverview => _navigationState.showingOverview;
String? get routingError => _navigationState.routingError;
bool get hasRoutingError => _navigationState.hasRoutingError;
// Navigation search state
bool get isNavigationSearchLoading => _navigationState.isSearchLoading;
@@ -340,6 +342,10 @@ class AppState extends ChangeNotifier {
_navigationState.clearSearchResults();
}
void retryRouteCalculation() {
_navigationState.retryRouteCalculation();
}
// ---------- Settings Methods ----------
Future<void> setOfflineMode(bool enabled) async {
await _settingsState.setOfflineMode(enabled);

View File

@@ -0,0 +1,164 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
class RouteResult {
final List<LatLng> waypoints;
final double distanceMeters;
final double durationSeconds;
const RouteResult({
required this.waypoints,
required this.distanceMeters,
required this.durationSeconds,
});
@override
String toString() {
return 'RouteResult(waypoints: ${waypoints.length}, distance: ${(distanceMeters/1000).toStringAsFixed(1)}km, duration: ${(durationSeconds/60).toStringAsFixed(1)}min)';
}
}
class RoutingService {
static const String _baseUrl = 'https://router.project-osrm.org';
static const String _userAgent = 'DeFlock/1.0 (OSM surveillance mapping app)';
static const Duration _timeout = Duration(seconds: 15);
/// Calculate route between two points using OSRM
Future<RouteResult> calculateRoute({
required LatLng start,
required LatLng end,
String profile = 'driving', // driving, walking, cycling
}) async {
debugPrint('[RoutingService] Calculating route from $start to $end');
// OSRM uses lng,lat order (opposite of LatLng)
final startCoord = '${start.longitude},${start.latitude}';
final endCoord = '${end.longitude},${end.latitude}';
final uri = Uri.parse('$_baseUrl/route/v1/$profile/$startCoord;$endCoord')
.replace(queryParameters: {
'overview': 'full', // Get full geometry
'geometries': 'polyline', // Use polyline encoding (more compact)
'steps': 'false', // Don't need turn-by-turn for now
});
debugPrint('[RoutingService] OSRM request: $uri');
try {
final response = await http.get(
uri,
headers: {
'User-Agent': _userAgent,
},
).timeout(_timeout);
if (response.statusCode != 200) {
throw RoutingException('HTTP ${response.statusCode}: ${response.reasonPhrase}');
}
final data = json.decode(response.body) as Map<String, dynamic>;
// Check OSRM response status
final code = data['code'] as String?;
if (code != 'Ok') {
final message = data['message'] as String? ?? 'Unknown routing error';
throw RoutingException('OSRM error ($code): $message');
}
final routes = data['routes'] as List<dynamic>?;
if (routes == null || routes.isEmpty) {
throw RoutingException('No route found between these points');
}
final route = routes[0] as Map<String, dynamic>;
final geometry = route['geometry'] as String?;
final distance = (route['distance'] as num?)?.toDouble() ?? 0.0;
final duration = (route['duration'] as num?)?.toDouble() ?? 0.0;
if (geometry == null) {
throw RoutingException('Route geometry missing from response');
}
// Decode polyline geometry to waypoints
final waypoints = _decodePolyline(geometry);
if (waypoints.isEmpty) {
throw RoutingException('Failed to decode route geometry');
}
final result = RouteResult(
waypoints: waypoints,
distanceMeters: distance,
durationSeconds: duration,
);
debugPrint('[RoutingService] Route calculated: $result');
return result;
} catch (e) {
debugPrint('[RoutingService] Route calculation failed: $e');
if (e is RoutingException) {
rethrow;
} else {
throw RoutingException('Network error: $e');
}
}
}
/// Decode OSRM polyline geometry to LatLng waypoints
List<LatLng> _decodePolyline(String encoded) {
try {
final List<LatLng> points = [];
int index = 0;
int lat = 0;
int lng = 0;
while (index < encoded.length) {
int b;
int shift = 0;
int result = 0;
// Decode latitude
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
final deltaLat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += deltaLat;
shift = 0;
result = 0;
// Decode longitude
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
final deltaLng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += deltaLng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
} catch (e) {
debugPrint('[RoutingService] Manual polyline decoding failed: $e');
return [];
}
}
}
class RoutingException implements Exception {
final String message;
const RoutingException(this.message);
@override
String toString() => 'RoutingException: $message';
}

View File

@@ -3,6 +3,7 @@ import 'package:latlong2/latlong.dart';
import '../models/search_result.dart';
import '../services/search_service.dart';
import '../services/routing_service.dart';
/// Simplified navigation modes - brutalist approach
enum AppNavigationMode {
@@ -14,6 +15,7 @@ enum AppNavigationMode {
/// Simplified navigation state - fewer modes, clearer logic
class NavigationState extends ChangeNotifier {
final SearchService _searchService = SearchService();
final RoutingService _routingService = RoutingService();
// Core state - just 3 modes
AppNavigationMode _mode = AppNavigationMode.normal;
@@ -22,6 +24,7 @@ class NavigationState extends ChangeNotifier {
bool _isSettingSecondPoint = false;
bool _isCalculating = false;
bool _showingOverview = false;
String? _routingError;
// Search state
bool _isSearchLoading = false;
@@ -46,6 +49,8 @@ class NavigationState extends ChangeNotifier {
bool get isSettingSecondPoint => _isSettingSecondPoint;
bool get isCalculating => _isCalculating;
bool get showingOverview => _showingOverview;
String? get routingError => _routingError;
bool get hasRoutingError => _routingError != null;
bool get isSearchLoading => _isSearchLoading;
List<SearchResult> get searchResults => List.unmodifiable(_searchResults);
@@ -113,6 +118,7 @@ class NavigationState extends ChangeNotifier {
_isCalculating = false;
_showingOverview = false;
_nextPointIsStart = false;
_routingError = null;
// Clear search
_clearSearchResults();
@@ -190,28 +196,52 @@ class NavigationState extends ChangeNotifier {
}
_isSettingSecondPoint = false;
_routingError = null; // Clear any previous errors
_calculateRoute();
}
/// Calculate route
/// Retry route calculation (for error recovery)
void retryRouteCalculation() {
if (_routeStart == null || _routeEnd == null) return;
debugPrint('[NavigationState] Retrying route calculation');
_routingError = null;
_calculateRoute();
}
/// Calculate route using OSRM
void _calculateRoute() {
if (_routeStart == null || _routeEnd == null) return;
debugPrint('[NavigationState] Calculating route...');
debugPrint('[NavigationState] Calculating route with OSRM...');
_isCalculating = true;
_routingError = null;
notifyListeners();
// Mock route calculation
Future.delayed(const Duration(seconds: 1), () {
if (!_isCalculating) return; // Canceled
_routingService.calculateRoute(
start: _routeStart!,
end: _routeEnd!,
profile: 'driving', // Could make this configurable later
).then((routeResult) {
if (!_isCalculating) return; // Canceled while calculating
_routePath = [_routeStart!, _routeEnd!];
_routeDistance = const Distance().as(LengthUnit.Meter, _routeStart!, _routeEnd!);
_routePath = routeResult.waypoints;
_routeDistance = routeResult.distanceMeters;
_isCalculating = false;
_showingOverview = true;
_provisionalPinLocation = null; // Hide provisional pin
debugPrint('[NavigationState] Route calculated: ${(_routeDistance! / 1000).toStringAsFixed(1)} km');
debugPrint('[NavigationState] OSRM route calculated: ${routeResult.toString()}');
notifyListeners();
}).catchError((error) {
if (!_isCalculating) return; // Canceled while calculating
debugPrint('[NavigationState] Route calculation failed: $error');
_isCalculating = false;
_routingError = error.toString().replaceAll('RoutingException: ', '');
// Don't show overview on error, stay in current state
notifyListeners();
});
}

View File

@@ -180,6 +180,50 @@ class NavigationSheet extends StatelessWidget {
),
],
// ROUTING ERROR: Show error with retry option
if (appState.hasRoutingError && !appState.isCalculating) ...[
Icon(
Icons.error_outline,
size: 48,
color: Colors.red[400],
),
const SizedBox(height: 16),
Text(
'Route calculation failed',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
appState.routingError ?? 'Unknown error',
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
onPressed: () {
// Retry route calculation
appState.retryRouteCalculation();
},
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.close),
label: const Text('Cancel'),
onPressed: () => appState.cancelNavigation(),
),
),
],
),
],
// ROUTE OVERVIEW: Show route details with start/cancel options
if (appState.showingOverview) ...[
if (appState.routeStart != null) ...[