import 'dart:convert'; import 'dart:math' as math; import 'package:http/http.dart' as http; import 'road_alert_models.dart'; import 'road_alert_provider_interface.dart'; class TomTomRoadAlertProvider implements RoadAlertProvider { @override String get id => 'tomtom'; @override String get name => 'TomTom Traffic'; @override Set get supportedCategories => const { RoadAlertCategory.accident, RoadAlertCategory.hazard, RoadAlertCategory.construction, RoadAlertCategory.roadClosure, RoadAlertCategory.traffic, }; @override Future testConnection(String apiKey) async { if (apiKey.trim().isEmpty) { return RoadAlertProviderStatus.notConfigured; } try { // Test query using a small bounding box over San Francisco final url = Uri.parse( 'https://api.tomtom.com/traffic/services/5/incidentDetails/s3/-122.42,37.77,-122.40,37.79/10/-1/json?key=$apiKey&language=en-GB'); final response = await http.get(url).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 { if (apiKey == null || apiKey.trim().isEmpty) { return []; } try { // Convert radius to bounding box offset (~111,000m per degree of latitude) final latOffset = radiusMeters / 111000.0; final lngOffset = radiusMeters / (111000.0 * math.cos(latitude * math.pi / 180.0)); final minLng = longitude - lngOffset; final minLat = latitude - latOffset; final maxLng = longitude + lngOffset; final maxLat = latitude + latOffset; final bboxStr = '${minLng.toStringAsFixed(4)},${minLat.toStringAsFixed(4)},${maxLng.toStringAsFixed(4)},${maxLat.toStringAsFixed(4)}'; final url = Uri.parse( 'https://api.tomtom.com/traffic/services/5/incidentDetails/s3/$bboxStr/10/-1/json?key=$apiKey&language=en-GB'); final response = await http.get(url).timeout(const Duration(seconds: 8)); if (response.statusCode != 200) { return []; } final data = json.decode(response.body); final List incidents = data['incidents'] ?? []; final List alerts = []; for (var inc in incidents) { final props = inc['properties'] ?? {}; final geometry = inc['geometry'] ?? {}; final coords = geometry['coordinates']; double incLat = latitude; double incLng = longitude; if (coords is List && coords.isNotEmpty) { if (coords[0] is List) { incLng = (coords[0][0] as num).toDouble(); incLat = (coords[0][1] as num).toDouble(); } else if (coords.length >= 2) { incLng = (coords[0] as num).toDouble(); incLat = (coords[1] as num).toDouble(); } } final iconCategory = (props['iconCategory'] as num?)?.toInt() ?? 0; final RoadAlertCategory category = _mapTomTomIconCategory(iconCategory); final description = (props['events'] != null && (props['events'] as List).isNotEmpty) ? props['events'][0]['description'] ?? 'Traffic incident' : (props['iconCategory'] != null ? 'Incident category $iconCategory' : 'Road Incident'); final alertId = props['id']?.toString() ?? '${incLat}_${incLng}'; alerts.add(RoadAlert( id: alertId, providerId: id, category: category, latitude: incLat, longitude: incLng, roadName: props['from'] ?? props['to'], description: description.toString(), reportedAt: DateTime.now(), expiresAt: DateTime.now().add(const Duration(hours: 1)), lastUpdated: DateTime.now(), severity: _mapTomTomMagnitude(props['magnitudeOfDelay'] as num?), source: 'TomTom Traffic API', sourceType: RoadAlertSourceType.providerReported, )); } return alerts; } catch (_) { return []; } } RoadAlertCategory _mapTomTomIconCategory(int iconCategory) { switch (iconCategory) { case 1: // Accident return RoadAlertCategory.accident; case 2: // Fog / Hazard case 3: // Dangerous Conditions case 4: // Rain / Ice / Snow case 5: // Wind / Weather return RoadAlertCategory.hazard; case 6: // Lane restriction / Construction case 7: // Roadworks return RoadAlertCategory.construction; case 8: // Road closed case 9: // Blocked road return RoadAlertCategory.roadClosure; case 10: // Traffic Jam case 11: // Slow traffic return RoadAlertCategory.traffic; default: return RoadAlertCategory.hazard; } } RoadAlertSeverity _mapTomTomMagnitude(num? magnitude) { if (magnitude == null) return RoadAlertSeverity.medium; if (magnitude >= 3) return RoadAlertSeverity.critical; if (magnitude >= 2) return RoadAlertSeverity.high; if (magnitude >= 1) return RoadAlertSeverity.medium; return RoadAlertSeverity.low; } }