import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'road_alert_models.dart'; import 'road_alert_provider_interface.dart'; /// Real-time Waze Police Alerts provider via OpenWebNinja. /// /// Queries OpenWebNinja's Waze REST endpoint exclusively for police alerts /// (`alert_types=POLICE`, `max_jams=0`) around the rider's coordinate. class WazeRoadAlertProvider implements RoadAlertProvider { final http.Client _client; final String apiHost; WazeRoadAlertProvider({ http.Client? client, String? apiHost, }) : _client = client ?? http.Client(), apiHost = apiHost ?? const String.fromEnvironment( 'WAZE_API_HOST', defaultValue: String.fromEnvironment( 'OPENWEBNINJA_API_HOST', defaultValue: 'api.openwebninja.com', ), ); @override String get id => 'waze'; @override String get name => 'Waze Police Alerts'; @override Set get supportedCategories => const { RoadAlertCategory.police, }; @override Future testConnection(String apiKey) async { final key = apiKey.trim(); if (key.isEmpty) { return RoadAlertProviderStatus.notConfigured; } try { // Diagnostic check: test point around downtown Los Angeles with max_alerts=1 final uri = Uri.https( apiHost, '/waze/alerts-and-jams', { 'center': '34.0522,-118.2437', 'radius': '5.0', 'radius_units': 'KM', 'max_alerts': '1', 'max_jams': '0', 'alert_types': 'POLICE', }, ); final response = await _client.get( uri, headers: {'x-api-key': key}, ).timeout(const Duration(seconds: 6)); if (response.statusCode == 200) { return RoadAlertProviderStatus.connected; } else if (response.statusCode == 401 || response.statusCode == 403) { return RoadAlertProviderStatus.authError; } else if (response.statusCode == 429) { return RoadAlertProviderStatus.rateLimited; } return RoadAlertProviderStatus.networkError; } catch (_) { return RoadAlertProviderStatus.networkError; } } @override Future> getAlerts({ required double latitude, required double longitude, required double radiusMeters, String? apiKey, }) async { final key = apiKey?.trim(); if (key == null || key.isEmpty) { return []; } try { final radiusKm = (radiusMeters / 1000.0).clamp(1.0, 50.0).toStringAsFixed(1); final uri = Uri.https( apiHost, '/waze/alerts-and-jams', { 'center': '$latitude,$longitude', 'radius': radiusKm, 'radius_units': 'KM', 'max_alerts': '25', 'max_jams': '0', 'alert_types': 'POLICE', }, ); final response = await _client.get( uri, headers: {'x-api-key': key}, ).timeout(const Duration(seconds: 8)); if (response.statusCode != 200) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] API response error ${response.statusCode}: ${response.body}'); } return []; } final data = json.decode(response.body); List alertsJson = []; if (data is Map) { if (data['data'] is Map && data['data']['alerts'] is List) { alertsJson = data['data']['alerts'] as List; } else if (data['alerts'] is List) { alertsJson = data['alerts'] as List; } } final List alerts = []; for (var item in alertsJson) { if (item is Map) { try { final alert = parseWazeItem(item); if (alert != null) { alerts.add(alert); } } catch (e) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] Item parse error: $e'); } } } } return alerts; } catch (e) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] Request error: $e'); } return []; } } /// Fetches real-time Waze police alerts inside a geographic bounding box /// (e.g. for map viewport queries using bottom_left and top_right). Future> getAlertsInBoundingBox({ required double bottomLat, required double bottomLng, required double topLat, required double topLng, String? apiKey, int maxAlerts = 25, }) async { final key = apiKey?.trim(); if (key == null || key.isEmpty) { return []; } try { final uri = Uri.https( apiHost, '/waze/alerts-and-jams', { 'bottom_left': '$bottomLat,$bottomLng', 'top_right': '$topLat,$topLng', 'max_alerts': '$maxAlerts', 'max_jams': '0', 'alert_types': 'POLICE', }, ); final response = await _client.get( uri, headers: {'x-api-key': key}, ).timeout(const Duration(seconds: 8)); if (response.statusCode != 200) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] Bounding box API error ${response.statusCode}: ${response.body}'); } return []; } final data = json.decode(response.body); List alertsJson = []; if (data is Map) { if (data['data'] is Map && data['data']['alerts'] is List) { alertsJson = data['data']['alerts'] as List; } else if (data['alerts'] is List) { alertsJson = data['alerts'] as List; } } final List alerts = []; for (var item in alertsJson) { if (item is Map) { try { final alert = parseWazeItem(item); if (alert != null) { alerts.add(alert); } } catch (e) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] Item parse error: $e'); } } } } return alerts; } catch (e) { if (kDebugMode) { debugPrint('[WazeRoadAlertProvider] Bounding box request error: $e'); } return []; } } /// Parses a Waze alert item into a [RoadAlert] strictly for police enforcement. RoadAlert? parseWazeItem(Map item) { // Enforce strict category filter: TYPE: POLICE only final typeStr = (item['type'] ?? '').toString().toUpperCase().trim(); if (typeStr != 'POLICE') { return null; } final rawLat = item['latitude']; final rawLng = item['longitude']; if (rawLat == null || rawLng == null) return null; final lat = (rawLat as num).toDouble(); final lng = (rawLng as num).toDouble(); final alertId = item['alert_id']?.toString() ?? 'waze_${item.hashCode}'; final subtype = item['subtype']?.toString().toUpperCase().trim(); final street = item['street']?.toString().trim(); final city = item['city']?.toString().trim(); final rawDesc = item['description']?.toString().trim(); final RoadAlertSeverity severity; final String description; if (subtype == 'POLICE_HIDDEN' || subtype == 'POLICE_HIDING') { severity = RoadAlertSeverity.high; description = (street != null && street.isNotEmpty) ? 'Hidden police speed trap reported on $street' : 'Hidden police speed trap reported'; } else if (subtype == 'POLICE_WITH_MOBILE_CAMERA' || subtype == 'POLICE_WITH_CAMERA' || subtype == 'POLICE_CAMERA') { severity = RoadAlertSeverity.high; description = (street != null && street.isNotEmpty) ? 'Police speed trap with camera reported on $street' : 'Police speed trap with camera reported'; } else if (subtype == 'POLICE_VISIBLE') { severity = RoadAlertSeverity.medium; description = (street != null && street.isNotEmpty) ? 'Police speed trap reported on $street' : 'Police speed trap reported'; } else if (subtype == 'POLICE_ON_OTHER_SIDE' || (subtype != null && subtype.contains('OTHER_SIDE'))) { severity = RoadAlertSeverity.medium; description = (street != null && street.isNotEmpty) ? 'Police reported on opposite side of $street' : 'Police reported on opposite side'; } else if (rawDesc != null && rawDesc.isNotEmpty) { severity = RoadAlertSeverity.medium; description = rawDesc; } else { severity = RoadAlertSeverity.medium; description = (street != null && street.isNotEmpty) ? 'Police reported on $street' : 'Police reported in area'; } String? roadName; if (street != null && street.isNotEmpty) { roadName = (city != null && city.isNotEmpty) ? '$street, $city' : street; } else if (city != null && city.isNotEmpty) { roadName = city; } final timeStr = item['publish_datetime_utc']?.toString(); final reportedAt = timeStr != null ? (DateTime.tryParse(timeStr)?.toLocal() ?? DateTime.now()) : DateTime.now(); final confNum = (item['alert_confidence'] as num?)?.toDouble(); final relNum = (item['alert_reliability'] as num?)?.toDouble(); double confidence = 0.8; if (confNum != null && confNum > 0) { confidence = (confNum / 5.0).clamp(0.2, 1.0); } else if (relNum != null && relNum > 0) { confidence = (relNum / 10.0).clamp(0.2, 1.0); } final thumbsUp = (item['num_thumbs_up'] as num?)?.toInt() ?? 0; if (thumbsUp > 0) { confidence = (confidence + 0.05 * thumbsUp).clamp(0.2, 1.0); } return RoadAlert( id: 'waze_$alertId', providerId: 'waze', category: RoadAlertCategory.police, latitude: lat, longitude: lng, roadName: roadName, description: description, reportedAt: reportedAt, lastUpdated: DateTime.now(), expiresAt: reportedAt.add(const Duration(minutes: 60)), confidence: confidence, severity: severity, source: 'Waze', sourceUrl: 'https://www.waze.com', sourceType: RoadAlertSourceType.providerReported, ); } }