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

175 lines
5.4 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'road_alert_models.dart';
import 'road_alert_provider_interface.dart';
/// Commercial telematics provider for HERE Technologies Safety Cameras.
///
/// Queries HERE Safety Cameras circular search REST endpoint for
/// verified mobile speed traps, stationary radar, and red light cameras.
class HereRoadAlertProvider implements RoadAlertProvider {
final http.Client _client;
HereRoadAlertProvider({http.Client? client})
: _client = client ?? http.Client();
@override
String get id => 'here_safety_cameras';
@override
String get name => 'HERE Safety Cameras';
@override
Set<RoadAlertCategory> get supportedCategories => const {
RoadAlertCategory.police,
RoadAlertCategory.speedCamera,
RoadAlertCategory.hazard,
};
@override
Future<RoadAlertProviderStatus> testConnection(String apiKey) async {
final key = apiKey.trim();
if (key.isEmpty) {
return RoadAlertProviderStatus.notConfigured;
}
try {
// Test circular search around downtown Los Angeles (US sample point)
final url = Uri.parse(
'https://safetycameras.hereapi.com/v2/search?in=circle:34.0522,-118.2437;r=500&apiKey=$key',
);
final response =
await _client.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 {
final key = apiKey?.trim();
if (key == null || key.isEmpty) {
return [];
}
try {
final r = radiusMeters.clamp(100.0, 10000.0).toInt();
final url = Uri.parse(
'https://safetycameras.hereapi.com/v2/search?in=circle:$latitude,$longitude;r=$r&apiKey=$key',
);
final response =
await _client.get(url).timeout(const Duration(seconds: 8));
if (response.statusCode != 200) {
return [];
}
final data = json.decode(response.body);
final List<dynamic> items =
data['items'] ?? (data['safetyCameras'] ?? []);
final List<RoadAlert> alerts = [];
for (var item in items) {
if (item is Map<String, dynamic>) {
try {
final alert = parseHereItem(item);
if (alert != null) {
alerts.add(alert);
}
} catch (e) {
if (kDebugMode) {
debugPrint('[HereRoadAlertProvider] Item parse error: $e');
}
}
}
}
return alerts;
} catch (e) {
if (kDebugMode) {
debugPrint('[HereRoadAlertProvider] Request error: $e');
}
return [];
}
}
/// Parses a single item from the HERE Safety Cameras response
RoadAlert? parseHereItem(Map<String, dynamic> item) {
final id = item['id']?.toString() ?? 'here_${item.hashCode}';
final typeStr =
(item['type'] ?? item['cameraType'] ?? '').toString().toUpperCase();
// Map HERE camera / incident types to RoadAlertCategory
final RoadAlertCategory category;
final RoadAlertSeverity severity;
if (typeStr.contains('MOBILE') || typeStr.contains('POLICE')) {
category = RoadAlertCategory.police;
severity = RoadAlertSeverity.high;
} else if (typeStr.contains('SPEED') ||
typeStr.contains('RED_LIGHT') ||
typeStr.contains('CAMERA') ||
typeStr.contains('FIXED')) {
category = RoadAlertCategory.speedCamera;
severity = RoadAlertSeverity.medium;
} else {
category = RoadAlertCategory.hazard;
severity = RoadAlertSeverity.low;
}
// Coordinates
final position = item['position'] ?? item['coordinate'] ?? {};
final lat = (position['lat'] ?? position['latitude'] as num?)?.toDouble();
final lng = (position['lng'] ?? position['longitude'] as num?)?.toDouble();
if (lat == null || lng == null) return null;
final road = item['road'] ?? item['roadName']?.toString();
final speedLimit = item['speedLimit'];
final unit = item['speedLimitUnit'] ?? 'MPH';
final heading = (item['heading'] ?? item['bearing'] as num?)?.toDouble();
String desc = item['description']?.toString() ?? '';
if (desc.isEmpty) {
if (category == RoadAlertCategory.police) {
desc = speedLimit != null
? 'Mobile Speed Trap ($speedLimit $unit zone)'
: 'Mobile Police Enforcement Zone';
} else {
desc = speedLimit != null
? 'Safety Speed Camera ($speedLimit $unit)'
: 'Enforcement Camera';
}
}
return RoadAlert(
id: id,
providerId: 'here_safety_cameras',
category: category,
latitude: lat,
longitude: lng,
heading: heading,
roadName: road,
description: desc,
reportedAt: DateTime.now(),
lastUpdated: DateTime.now(),
severity: severity,
source: 'HERE Technologies',
sourceType: RoadAlertSourceType.providerReported,
);
}
}