175 lines
5.4 KiB
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,
|
|
);
|
|
}
|
|
}
|