MotorSignal-App/lib/services/alerts/ga511_road_alert_provider.dart

150 lines
5.1 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';
/// Client provider for Georgia 511 official traffic feeds via MotorSignal FastAPI backend.
class Georgia511RoadAlertProvider implements RoadAlertProvider {
final http.Client _client;
final String backendBaseUrl;
Georgia511RoadAlertProvider({
http.Client? client,
String? backendBaseUrl,
}) : _client = client ?? http.Client(),
backendBaseUrl = backendBaseUrl ??
const String.fromEnvironment(
'MOTORSIGNAL_API_URL',
defaultValue: 'http://127.0.0.1:8000',
);
@override
String get id => 'ga511_official';
@override
String get name => 'Georgia 511';
@override
Set<RoadAlertCategory> get supportedCategories => const {
RoadAlertCategory.accident,
RoadAlertCategory.roadClosure,
RoadAlertCategory.construction,
RoadAlertCategory.hazard,
};
@override
Future<RoadAlertProviderStatus> testConnection(String apiKey) async {
try {
final uri = Uri.parse('$backendBaseUrl/api/v1/alerts/ops/health');
final res = await _client.get(uri).timeout(const Duration(seconds: 4));
if (res.statusCode == 200) {
return RoadAlertProviderStatus.connected;
}
return RoadAlertProviderStatus.networkError;
} catch (e) {
return RoadAlertProviderStatus.networkError;
}
}
@override
Future<List<RoadAlert>> getAlerts({
required double latitude,
required double longitude,
required double radiusMeters,
String? apiKey,
}) async {
const metersPerDegreeLat = 111320.0;
final latDelta = radiusMeters / metersPerDegreeLat;
final metersPerDegreeLng = metersPerDegreeLat * math.cos(latitude * math.pi / 180.0).abs();
final lngDelta = metersPerDegreeLng > 0 ? radiusMeters / metersPerDegreeLng : latDelta;
final minLat = (latitude - latDelta).clamp(-90.0, 90.0);
final maxLat = (latitude + latDelta).clamp(-90.0, 90.0);
final minLng = (longitude - lngDelta).clamp(-180.0, 180.0);
final maxLng = (longitude + lngDelta).clamp(-180.0, 180.0);
final queryParams = {
'min_lat': minLat.toStringAsFixed(4),
'min_lng': minLng.toStringAsFixed(4),
'max_lat': maxLat.toStringAsFixed(4),
'max_lng': maxLng.toStringAsFixed(4),
};
final uri = Uri.parse('$backendBaseUrl/api/v1/alerts/ga511').replace(
queryParameters: queryParams,
);
try {
final response = await _client.get(uri).timeout(const Duration(seconds: 5));
if (response.statusCode != 200) {
return [];
}
final data = json.decode(response.body);
final rawAlerts = data['alerts'] as List<dynamic>? ?? [];
final List<RoadAlert> alerts = [];
for (final raw in rawAlerts) {
if (raw is! Map<String, dynamic>) continue;
final id = raw['id']?.toString() ?? '';
final catStr = raw['category']?.toString() ?? 'hazard';
final category = RoadAlertCategory.values.firstWhere(
(c) => c.name.toLowerCase() == catStr.toLowerCase(),
orElse: () => RoadAlertCategory.hazard,
);
final lat = (raw['latitude'] as num?)?.toDouble() ?? 0.0;
final lng = (raw['longitude'] as num?)?.toDouble() ?? 0.0;
if (lat < -90.0 || lat > 90.0 || lng < -180.0 || lng > 180.0) continue;
final reportedAtStr = raw['reported_at_utc']?.toString();
final reportedAt = reportedAtStr != null
? (DateTime.tryParse(reportedAtStr) ?? DateTime.now())
: DateTime.now();
final expiresAtStr = raw['expires_at_utc']?.toString();
final expiresAt = expiresAtStr != null ? DateTime.tryParse(expiresAtStr) : null;
final dLat = (lat - latitude) * 111320.0;
final dLng = (lng - longitude) * 111320.0 * math.cos(latitude * math.pi / 180.0);
final distance = math.sqrt(dLat * dLat + dLng * dLng);
alerts.add(
RoadAlert(
id: id,
providerId: this.id,
category: category,
subtype: raw['subtype']?.toString(),
latitude: lat,
longitude: lng,
roadName: raw['road_name']?.toString(),
description: raw['description']?.toString() ?? 'GDOT 511 Incident',
reportedAt: reportedAt,
expiresAt: expiresAt,
lastUpdated: DateTime.now(),
confidence: (raw['confidence'] as num?)?.toDouble() ?? 0.95,
severity: category == RoadAlertCategory.accident || category == RoadAlertCategory.roadClosure
? RoadAlertSeverity.high
: RoadAlertSeverity.medium,
source: 'Georgia 511',
sourceType: RoadAlertSourceType.providerReported,
status: RoadAlertStatus.active,
distanceFromUser: distance,
confirmations: 1,
dismissals: 0,
),
);
}
return alerts;
} catch (e) {
if (kDebugMode) {
debugPrint('[Georgia511RoadAlertProvider] Failed to fetch alerts: $e');
}
return [];
}
}
}