169 lines
5.4 KiB
Dart
169 lines
5.4 KiB
Dart
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<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 {
|
|
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<List<RoadAlert>> 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<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 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;
|
|
}
|
|
}
|