import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; 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 { final url = Uri.parse( 'https://api.tomtom.com/traffic/services/5/incidentDetails?key=${apiKey.trim()}&bbox=-122.42,37.77,-122.40,37.79&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 { final latOffset = radiusMeters / 111000.0; final lngOffset = radiusMeters / (111000.0 * math.cos(latitude * math.pi / 180.0)); final minLng = (longitude - lngOffset).clamp(-180.0, 180.0); final minLat = (latitude - latOffset).clamp(-85.0, 85.0); final maxLng = (longitude + lngOffset).clamp(-180.0, 180.0); final maxLat = (latitude + latOffset).clamp(-85.0, 85.0); 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?key=${apiKey.trim()}&bbox=$bboxStr&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 category = _mapTomTomIconCategory(iconCategory); final description = (props['events'] != null && (props['events'] as List).isNotEmpty) ? props['events'][0]['description'] ?? 'Traffic incident' : '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, )); } if (kDebugMode) { debugPrint('[RoadAlerts] TomTom returned ${alerts.length} incidents'); } return alerts; } catch (e) { if (kDebugMode) { debugPrint('[RoadAlerts] TomTom fetch error: $e'); } return []; } } RoadAlertCategory _mapTomTomIconCategory(int iconCategory) { switch (iconCategory) { case 1: return RoadAlertCategory.accident; case 2: case 3: case 4: case 5: return RoadAlertCategory.hazard; case 6: case 7: return RoadAlertCategory.construction; case 8: case 9: return RoadAlertCategory.roadClosure; case 10: case 11: 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; } }