Start to change navigation to alprwatch API

This only covers the success path. Known todos:

* failure modes in routing
* developer documentation
* feature flags
This commit is contained in:
ALPR Watch
2025-12-01 21:48:25 +01:00
parent db5c7311b1
commit 9782352909
2 changed files with 50 additions and 89 deletions

View File

@@ -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<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');
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<String, dynamic>;
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<dynamic>?;
if (routes == null || routes.isEmpty) {
final route = data['result']['route'] as Map<String, dynamic>?;
if (route == null) {
throw RoutingException('No route found between these points');
}
final route = routes[0] as Map<String, dynamic>;
final geometry = route['geometry'] as String?;
final waypoints = (route['coordinates'] as List<dynamic>?)
?.map((inner) {
final pair = inner as List<dynamic>;
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<LatLng>().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<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 {
@@ -161,4 +123,4 @@ class RoutingException implements Exception {
@override
String toString() => 'RoutingException: $message';
}
}

View File

@@ -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();
}
}
}
}