MotorSignal-App/lib/services/alerts/tomtom_road_alert_provider....

163 lines
5.5 KiB
Dart

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<RoadAlertCategory> get supportedCategories => const {
RoadAlertCategory.accident,
RoadAlertCategory.hazard,
RoadAlertCategory.construction,
RoadAlertCategory.roadClosure,
RoadAlertCategory.traffic,
};
@override
Future<RoadAlertProviderStatus> 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<List<RoadAlert>> 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<dynamic> incidents = data['incidents'] ?? [];
final List<RoadAlert> 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;
}
}