From 9782352909c164c126aa9e6dd7515e7beffc6f8d Mon Sep 17 00:00:00 2001 From: ALPR Watch Date: Mon, 1 Dec 2025 21:48:25 +0100 Subject: [PATCH 1/4] Start to change navigation to alprwatch API This only covers the success path. Known todos: * failure modes in routing * developer documentation * feature flags --- lib/services/routing_service.dart | 130 +++++++++++------------------- lib/state/navigation_state.dart | 9 +-- 2 files changed, 50 insertions(+), 89 deletions(-) diff --git a/lib/services/routing_service.dart b/lib/services/routing_service.dart index 71afcc5..62eaa03 100644 --- a/lib/services/routing_service.dart +++ b/lib/services/routing_service.dart @@ -21,73 +21,81 @@ class RouteResult { } class RoutingService { - static const String _baseUrl = 'https://router.project-osrm.org'; + static const String _baseUrl = 'https://alprwatch.org/api/v1/deflock/directions'; 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 + /// Calculate route between two points using alprwatch Future 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'); + final params = { + 'start': { + 'longitude': start.longitude, + 'latitude': start.latitude + }, + 'end': { + 'longitude': end.longitude, + 'latitude': end.latitude + }, + 'enabled_profiles': [ // revise to be dynamic based on user input + { + 'id': 'generic-ALPR', + 'name': 'ALPR', + 'tags': { + 'surveillance:type': 'ALPR' + } + } + ] + }; - 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'); + debugPrint('[RoutingService] alprwatch request: $uri $params'); try { - final response = await http.get( + final response = await http.post( uri, headers: { 'User-Agent': _userAgent, + 'Content-Type': 'application/json' }, + body: json.encode(params) ).timeout(_timeout); - + if (response.statusCode != 200) { throw RoutingException('HTTP ${response.statusCode}: ${response.reasonPhrase}'); } final data = json.decode(response.body) as Map; + debugPrint('[RoutingService] alprwatch response data: $data'); - // 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'); + // Check alprwatch response status + final ok = data['ok'] as bool? ?? false; + if ( ! ok ) { + final code = data['error']['code'] as String? ?? 'Unknown routing error code'; + final message = data['error']['message'] as String? ?? 'Unknown routing error'; + throw RoutingException('alprwatch error ($code): $message'); } - final routes = data['routes'] as List?; - if (routes == null || routes.isEmpty) { + final route = data['result']['route'] as Map?; + if (route == null) { throw RoutingException('No route found between these points'); } - - final route = routes[0] as Map; - final geometry = route['geometry'] as String?; + + final waypoints = (route['coordinates'] as List?) + ?.map((inner) { + final pair = inner as List; + if (pair.length != 2) return null; + final lng = (pair[0] as num).toDouble(); + final lat = (pair[1] as num).toDouble(); + return LatLng(lat, lng); + }).whereType().toList() ?? []; 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, @@ -106,52 +114,6 @@ class RoutingService { } } } - - /// Decode OSRM polyline geometry to LatLng waypoints - List _decodePolyline(String encoded) { - try { - final List 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 { @@ -161,4 +123,4 @@ class RoutingException implements Exception { @override String toString() => 'RoutingException: $message'; -} \ No newline at end of file +} diff --git a/lib/state/navigation_state.dart b/lib/state/navigation_state.dart index cea90d1..e94c7cb 100644 --- a/lib/state/navigation_state.dart +++ b/lib/state/navigation_state.dart @@ -221,11 +221,11 @@ class NavigationState extends ChangeNotifier { _calculateRoute(); } - /// Calculate route using OSRM + /// Calculate route using alprwatch void _calculateRoute() { if (_routeStart == null || _routeEnd == null) return; - debugPrint('[NavigationState] Calculating route with OSRM...'); + debugPrint('[NavigationState] Calculating route with alprwatch...'); _isCalculating = true; _routingError = null; notifyListeners(); @@ -233,7 +233,6 @@ class NavigationState extends ChangeNotifier { _routingService.calculateRoute( start: _routeStart!, end: _routeEnd!, - profile: 'driving', // Could make this configurable later ).then((routeResult) { if (!_isCalculating) return; // Canceled while calculating @@ -243,7 +242,7 @@ class NavigationState extends ChangeNotifier { _showingOverview = true; _provisionalPinLocation = null; // Hide provisional pin - debugPrint('[NavigationState] OSRM route calculated: ${routeResult.toString()}'); + debugPrint('[NavigationState] alprwatch route calculated: ${routeResult.toString()}'); notifyListeners(); }).catchError((error) { @@ -348,4 +347,4 @@ class NavigationState extends ChangeNotifier { notifyListeners(); } } -} \ No newline at end of file +} From 0ec53c3a112ae6c51c191443d9ab340a45252fd4 Mon Sep 17 00:00:00 2001 From: ALPR Watch Date: Wed, 3 Dec 2025 07:33:50 +0100 Subject: [PATCH 2/4] Enable avoidance distance settings --- lib/app_state.dart | 6 ++ lib/screens/navigation_settings_screen.dart | 67 ++++++++------------- lib/services/routing_service.dart | 8 ++- lib/state/navigation_state.dart | 2 +- lib/state/settings_state.dart | 20 +++++- 5 files changed, 57 insertions(+), 46 deletions(-) diff --git a/lib/app_state.dart b/lib/app_state.dart index c5508f7..4b805c2 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -122,6 +122,7 @@ class AppState extends ChangeNotifier { // Navigation search state bool get isNavigationSearchLoading => _navigationState.isSearchLoading; List get navigationSearchResults => _navigationState.searchResults; + int get navigationAvoidanceDistance => _settingsState.navigationAvoidanceDistance; // Profile state List get profiles => _profileState.profiles; @@ -645,6 +646,11 @@ class AppState extends ChangeNotifier { await _settingsState.setSuspectedLocationMinDistance(distance); } + /// Set navigation avoidance distance + Future setNavigationAvoidanceDistance(int distance) async { + await _settingsState.setNavigationAvoidanceDistance(distance); + } + // ---------- Queue Methods ---------- void clearQueue() { _uploadQueueState.clearQueue(); diff --git a/lib/screens/navigation_settings_screen.dart b/lib/screens/navigation_settings_screen.dart index 9bc8ab3..6e3b218 100644 --- a/lib/screens/navigation_settings_screen.dart +++ b/lib/screens/navigation_settings_screen.dart @@ -8,6 +8,7 @@ class NavigationSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final appState = context.watch(); final locService = LocalizationService.instance; return AnimatedBuilder( @@ -26,48 +27,28 @@ class NavigationSettingsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Coming soon message - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.info_outline, color: Colors.blue), - const SizedBox(width: 8), - Text( - 'Navigation Features', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Navigation and routing settings will be available here. Coming soon:\n\n' - '• Surveillance avoidance distance\n' - '• Route planning preferences\n' - '• Search history management\n' - '• Distance units (metric/imperial)', - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - ), - - const SizedBox(height: 24), - - // Placeholder settings (disabled for now) - _buildDisabledSetting( - context, - icon: Icons.warning_outlined, - title: locService.t('navigation.avoidanceDistance'), - subtitle: locService.t('navigation.avoidanceDistanceSubtitle'), - value: '100 ${locService.t('navigation.meters')}', + ListTile( + leading: const Icon(Icons.social_distance), + title: Text(locService.t('navigation.avoidanceDistance')), + subtitle: Text(locService.t('navigation.avoidanceDistanceSubtitle')), + trailing: SizedBox( + width: 80, + child: TextFormField( + initialValue: appState.navigationAvoidanceDistance.toString(), + keyboardType: const TextInputType.numberWithOptions(signed: false, decimal: false), + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + border: OutlineInputBorder(), + suffixText: 'm', + ), + onFieldSubmitted: (value) { + final distance = int.tryParse(value) ?? 250; + appState.setNavigationAvoidanceDistance(distance.clamp(0, 2000)); + } + ) + ) ), const Divider(), @@ -126,4 +107,4 @@ class NavigationSettingsScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/services/routing_service.dart b/lib/services/routing_service.dart index 62eaa03..6b3c95f 100644 --- a/lib/services/routing_service.dart +++ b/lib/services/routing_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class RouteResult { final List waypoints; @@ -31,6 +32,9 @@ class RoutingService { required LatLng end, }) async { debugPrint('[RoutingService] Calculating route from $start to $end'); + + final prefs = await SharedPreferences.getInstance(); + final avoidance_distance = await prefs.getInt('navigation_avoidance_distance'); final uri = Uri.parse('$_baseUrl'); final params = { @@ -42,6 +46,7 @@ class RoutingService { 'longitude': end.longitude, 'latitude': end.latitude }, + 'avoidance_distance': avoidance_distance, 'enabled_profiles': [ // revise to be dynamic based on user input { 'id': 'generic-ALPR', @@ -50,7 +55,8 @@ class RoutingService { 'surveillance:type': 'ALPR' } } - ] + ], + 'show_exclusion_zone': false, // if true, returns a GeoJSON Feature MultiPolygon showing what areas are avoided in calculating the route }; debugPrint('[RoutingService] alprwatch request: $uri $params'); diff --git a/lib/state/navigation_state.dart b/lib/state/navigation_state.dart index e94c7cb..f090c64 100644 --- a/lib/state/navigation_state.dart +++ b/lib/state/navigation_state.dart @@ -224,7 +224,7 @@ class NavigationState extends ChangeNotifier { /// Calculate route using alprwatch void _calculateRoute() { if (_routeStart == null || _routeEnd == null) return; - + debugPrint('[NavigationState] Calculating route with alprwatch...'); _isCalculating = true; _routingError = null; diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart index c07ef32..8b0eb15 100644 --- a/lib/state/settings_state.dart +++ b/lib/state/settings_state.dart @@ -29,6 +29,7 @@ class SettingsState extends ChangeNotifier { static const String _networkStatusIndicatorEnabledPrefsKey = 'network_status_indicator_enabled'; static const String _suspectedLocationMinDistancePrefsKey = 'suspected_location_min_distance'; static const String _pauseQueueProcessingPrefsKey = 'pause_queue_processing'; + static const String _navigationAvoidanceDistancePrefsKey = 'navigation_avoidance_distance'; bool _offlineMode = false; bool _pauseQueueProcessing = false; @@ -41,6 +42,7 @@ class SettingsState extends ChangeNotifier { int _suspectedLocationMinDistance = 100; // meters List _tileProviders = []; String _selectedTileTypeId = ''; + int _navigationAvoidanceDistance = 250; // meters // Getters bool get offlineMode => _offlineMode; @@ -54,6 +56,7 @@ class SettingsState extends ChangeNotifier { int get suspectedLocationMinDistance => _suspectedLocationMinDistance; List get tileProviders => List.unmodifiable(_tileProviders); String get selectedTileTypeId => _selectedTileTypeId; + int get navigationAvoidanceDistance => _navigationAvoidanceDistance; /// Get the currently selected tile type TileType? get selectedTileType { @@ -100,6 +103,11 @@ class SettingsState extends ChangeNotifier { // Load max nodes _maxNodes = prefs.getInt(_maxNodesPrefsKey) ?? kDefaultMaxNodes; + + // Load navigation avoidance distance + if (prefs.containsKey(_navigationAvoidanceDistancePrefsKey)) { + _navigationAvoidanceDistance = prefs.getInt(_navigationAvoidanceDistancePrefsKey) ?? 250; + } // Load proximity alerts settings _proximityAlertsEnabled = prefs.getBool(_proximityAlertsEnabledPrefsKey) ?? false; @@ -351,4 +359,14 @@ class SettingsState extends ChangeNotifier { } } -} \ No newline at end of file + // Set distance for avoidance of nodes during navigation + Future setNavigationAvoidanceDistance(int distance) async { + if (_navigationAvoidanceDistance != distance) { + _navigationAvoidanceDistance = distance; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_navigationAvoidanceDistancePrefsKey, distance); + notifyListeners(); + } + } + +} From 043a0360755cbb6ba6662bba44a539d92404abfb Mon Sep 17 00:00:00 2001 From: ALPR Watch Date: Tue, 2 Dec 2025 12:46:01 +0100 Subject: [PATCH 3/4] Enable user-selected profiles in navigation --- lib/services/routing_service.dart | 25 ++++++++++++++----------- lib/widgets/navigation_sheet.dart | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/services/routing_service.dart b/lib/services/routing_service.dart index 6b3c95f..5fd96b0 100644 --- a/lib/services/routing_service.dart +++ b/lib/services/routing_service.dart @@ -4,6 +4,8 @@ import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../app_state.dart'; + class RouteResult { final List waypoints; final double distanceMeters; @@ -26,7 +28,7 @@ class RoutingService { static const String _userAgent = 'DeFlock/1.0 (OSM surveillance mapping app)'; static const Duration _timeout = Duration(seconds: 15); - /// Calculate route between two points using alprwatch + // Calculate route between two points using alprwatch Future calculateRoute({ required LatLng start, required LatLng end, @@ -35,6 +37,15 @@ class RoutingService { final prefs = await SharedPreferences.getInstance(); final avoidance_distance = await prefs.getInt('navigation_avoidance_distance'); + + final enabled_profiles = AppState.instance.enabledProfiles.map((p) { + final full = p.toJson(); + return { + 'id': full['id'], + 'name': full['name'], + 'tags': full['tags'], + }; + }).toList(); final uri = Uri.parse('$_baseUrl'); final params = { @@ -47,16 +58,8 @@ class RoutingService { 'latitude': end.latitude }, 'avoidance_distance': avoidance_distance, - 'enabled_profiles': [ // revise to be dynamic based on user input - { - 'id': 'generic-ALPR', - 'name': 'ALPR', - 'tags': { - 'surveillance:type': 'ALPR' - } - } - ], - 'show_exclusion_zone': false, // if true, returns a GeoJSON Feature MultiPolygon showing what areas are avoided in calculating the route + 'enabled_profiles': enabled_profiles, + 'show_exclusion_zone': false, // for debugging: if true, returns a GeoJSON Feature MultiPolygon showing what areas are avoided in calculating the route }; debugPrint('[RoutingService] alprwatch request: $uri $params'); diff --git a/lib/widgets/navigation_sheet.dart b/lib/widgets/navigation_sheet.dart index ab41955..2cd2250 100644 --- a/lib/widgets/navigation_sheet.dart +++ b/lib/widgets/navigation_sheet.dart @@ -334,4 +334,4 @@ class NavigationSheet extends StatelessWidget { }, ); } -} \ No newline at end of file +} From 3f83d67bc180fca122b79d1af0e38b8c94d0254d Mon Sep 17 00:00:00 2001 From: ALPR Watch Date: Wed, 3 Dec 2025 07:35:18 +0100 Subject: [PATCH 4/4] Add error handling and update documentation --- DEVELOPER.md | 5 ++--- lib/dev_config.dart | 1 + lib/services/routing_service.dart | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index f631e58..15ac2e3 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -488,8 +488,7 @@ The major performance issue was discovered to be double caching with expensive o **Current state:** - **Search functionality**: Fully implemented and active -- **Basic routing**: Complete but disabled pending API integration -- **Avoidance routing**: Awaiting alprwatch.org/directions API +- **Avoidance routing**: Fully implemented and active - **Offline routing**: Requires vector map tiles **Architecture:** @@ -848,4 +847,4 @@ debugPrint('[ComponentName] Detailed message: $data'); --- -This documentation should be updated as the architecture evolves. When making significant changes, update both the relevant section here and add a brief note explaining the rationale for the change. \ No newline at end of file +This documentation should be updated as the architecture evolves. When making significant changes, update both the relevant section here and add a brief note explaining the rationale for the change. diff --git a/lib/dev_config.dart b/lib/dev_config.dart index b0597d0..5baf113 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -77,6 +77,7 @@ const bool kEnableNodeExtraction = false; // Set to true to enable extract from /// Navigation availability: only dev builds, and only when online bool enableNavigationFeatures({required bool offlineMode}) { + return true; if (!kEnableDevelopmentModes) { return false; // Release builds: never allow navigation } else { diff --git a/lib/services/routing_service.dart b/lib/services/routing_service.dart index 5fd96b0..781936d 100644 --- a/lib/services/routing_service.dart +++ b/lib/services/routing_service.dart @@ -84,9 +84,8 @@ class RoutingService { // Check alprwatch response status final ok = data['ok'] as bool? ?? false; if ( ! ok ) { - final code = data['error']['code'] as String? ?? 'Unknown routing error code'; - final message = data['error']['message'] as String? ?? 'Unknown routing error'; - throw RoutingException('alprwatch error ($code): $message'); + final message = data['error'] as String? ?? 'Unknown routing error'; + throw RoutingException('alprwatch error: $message'); } final route = data['result']['route'] as Map?;