mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-21 10:28:19 +02:00
Basic routing. Still some bugs.
This commit is contained in:
@@ -95,6 +95,8 @@ class AppState extends ChangeNotifier {
|
|||||||
bool get isSettingSecondPoint => _navigationState.isSettingSecondPoint;
|
bool get isSettingSecondPoint => _navigationState.isSettingSecondPoint;
|
||||||
bool get isCalculating => _navigationState.isCalculating;
|
bool get isCalculating => _navigationState.isCalculating;
|
||||||
bool get showingOverview => _navigationState.showingOverview;
|
bool get showingOverview => _navigationState.showingOverview;
|
||||||
|
String? get routingError => _navigationState.routingError;
|
||||||
|
bool get hasRoutingError => _navigationState.hasRoutingError;
|
||||||
|
|
||||||
// Navigation search state
|
// Navigation search state
|
||||||
bool get isNavigationSearchLoading => _navigationState.isSearchLoading;
|
bool get isNavigationSearchLoading => _navigationState.isSearchLoading;
|
||||||
@@ -340,6 +342,10 @@ class AppState extends ChangeNotifier {
|
|||||||
_navigationState.clearSearchResults();
|
_navigationState.clearSearchResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void retryRouteCalculation() {
|
||||||
|
_navigationState.retryRouteCalculation();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Settings Methods ----------
|
// ---------- Settings Methods ----------
|
||||||
Future<void> setOfflineMode(bool enabled) async {
|
Future<void> setOfflineMode(bool enabled) async {
|
||||||
await _settingsState.setOfflineMode(enabled);
|
await _settingsState.setOfflineMode(enabled);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:latlong2/latlong.dart';
|
|||||||
|
|
||||||
import '../models/search_result.dart';
|
import '../models/search_result.dart';
|
||||||
import '../services/search_service.dart';
|
import '../services/search_service.dart';
|
||||||
|
import '../services/routing_service.dart';
|
||||||
|
|
||||||
/// Simplified navigation modes - brutalist approach
|
/// Simplified navigation modes - brutalist approach
|
||||||
enum AppNavigationMode {
|
enum AppNavigationMode {
|
||||||
@@ -14,6 +15,7 @@ enum AppNavigationMode {
|
|||||||
/// Simplified navigation state - fewer modes, clearer logic
|
/// Simplified navigation state - fewer modes, clearer logic
|
||||||
class NavigationState extends ChangeNotifier {
|
class NavigationState extends ChangeNotifier {
|
||||||
final SearchService _searchService = SearchService();
|
final SearchService _searchService = SearchService();
|
||||||
|
final RoutingService _routingService = RoutingService();
|
||||||
|
|
||||||
// Core state - just 3 modes
|
// Core state - just 3 modes
|
||||||
AppNavigationMode _mode = AppNavigationMode.normal;
|
AppNavigationMode _mode = AppNavigationMode.normal;
|
||||||
@@ -22,6 +24,7 @@ class NavigationState extends ChangeNotifier {
|
|||||||
bool _isSettingSecondPoint = false;
|
bool _isSettingSecondPoint = false;
|
||||||
bool _isCalculating = false;
|
bool _isCalculating = false;
|
||||||
bool _showingOverview = false;
|
bool _showingOverview = false;
|
||||||
|
String? _routingError;
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
bool _isSearchLoading = false;
|
bool _isSearchLoading = false;
|
||||||
@@ -46,6 +49,8 @@ class NavigationState extends ChangeNotifier {
|
|||||||
bool get isSettingSecondPoint => _isSettingSecondPoint;
|
bool get isSettingSecondPoint => _isSettingSecondPoint;
|
||||||
bool get isCalculating => _isCalculating;
|
bool get isCalculating => _isCalculating;
|
||||||
bool get showingOverview => _showingOverview;
|
bool get showingOverview => _showingOverview;
|
||||||
|
String? get routingError => _routingError;
|
||||||
|
bool get hasRoutingError => _routingError != null;
|
||||||
|
|
||||||
bool get isSearchLoading => _isSearchLoading;
|
bool get isSearchLoading => _isSearchLoading;
|
||||||
List<SearchResult> get searchResults => List.unmodifiable(_searchResults);
|
List<SearchResult> get searchResults => List.unmodifiable(_searchResults);
|
||||||
@@ -113,6 +118,7 @@ class NavigationState extends ChangeNotifier {
|
|||||||
_isCalculating = false;
|
_isCalculating = false;
|
||||||
_showingOverview = false;
|
_showingOverview = false;
|
||||||
_nextPointIsStart = false;
|
_nextPointIsStart = false;
|
||||||
|
_routingError = null;
|
||||||
|
|
||||||
// Clear search
|
// Clear search
|
||||||
_clearSearchResults();
|
_clearSearchResults();
|
||||||
@@ -190,28 +196,52 @@ class NavigationState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isSettingSecondPoint = false;
|
_isSettingSecondPoint = false;
|
||||||
|
_routingError = null; // Clear any previous errors
|
||||||
_calculateRoute();
|
_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() {
|
void _calculateRoute() {
|
||||||
if (_routeStart == null || _routeEnd == null) return;
|
if (_routeStart == null || _routeEnd == null) return;
|
||||||
|
|
||||||
debugPrint('[NavigationState] Calculating route...');
|
debugPrint('[NavigationState] Calculating route with OSRM...');
|
||||||
_isCalculating = true;
|
_isCalculating = true;
|
||||||
|
_routingError = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Mock route calculation
|
_routingService.calculateRoute(
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
start: _routeStart!,
|
||||||
if (!_isCalculating) return; // Canceled
|
end: _routeEnd!,
|
||||||
|
profile: 'driving', // Could make this configurable later
|
||||||
|
).then((routeResult) {
|
||||||
|
if (!_isCalculating) return; // Canceled while calculating
|
||||||
|
|
||||||
_routePath = [_routeStart!, _routeEnd!];
|
_routePath = routeResult.waypoints;
|
||||||
_routeDistance = const Distance().as(LengthUnit.Meter, _routeStart!, _routeEnd!);
|
_routeDistance = routeResult.distanceMeters;
|
||||||
_isCalculating = false;
|
_isCalculating = false;
|
||||||
_showingOverview = true;
|
_showingOverview = true;
|
||||||
_provisionalPinLocation = null; // Hide provisional pin
|
_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();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// ROUTE OVERVIEW: Show route details with start/cancel options
|
||||||
if (appState.showingOverview) ...[
|
if (appState.showingOverview) ...[
|
||||||
if (appState.routeStart != null) ...[
|
if (appState.routeStart != null) ...[
|
||||||
|
|||||||
Reference in New Issue
Block a user