import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:geolocator/geolocator.dart'; class UsSpeedCamera { final String id; final double latitude; final double longitude; final String roadName; final int? speedLimitMph; final String type; // 'speed_camera', 'red_light_camera', 'speed_trap', 'average_speed_zone' final String state; final String? city; final double? direction; // Heading in degrees (0-360) final double? distanceMeters; // Transient distance computed during query const UsSpeedCamera({ required this.id, required this.latitude, required this.longitude, required this.roadName, this.speedLimitMph, this.type = 'speed_camera', required this.state, this.city, this.direction, this.distanceMeters, }); factory UsSpeedCamera.fromJson(Map json) { return UsSpeedCamera( id: json['id']?.toString() ?? '', latitude: (json['lat'] as num).toDouble(), longitude: (json['lng'] as num).toDouble(), roadName: json['road']?.toString() ?? 'Enforcement Zone', speedLimitMph: json['limit'] != null ? (json['limit'] as num).toInt() : null, type: json['type']?.toString() ?? 'speed_camera', state: json['state']?.toString() ?? 'US', city: json['city']?.toString(), direction: json['direction'] != null ? (json['direction'] as num).toDouble() : null, ); } UsSpeedCamera copyWithDistance(double dist) { return UsSpeedCamera( id: id, latitude: latitude, longitude: longitude, roadName: roadName, speedLimitMph: speedLimitMph, type: type, state: state, city: city, direction: direction, distanceMeters: dist, ); } } class UsSpeedCameraDatabase { static final UsSpeedCameraDatabase _instance = UsSpeedCameraDatabase._internal(); static UsSpeedCameraDatabase get instance => _instance; UsSpeedCameraDatabase._internal(); final List _allCameras = []; final Map> _spatialGrid = {}; bool _isInitialized = false; bool get isInitialized => _isInitialized; int get totalCount => _allCameras.length; Set get supportedStates => _allCameras.map((c) => c.state).toSet(); /// Key format: "${(lat * 10).floor()}_${(lng * 10).floor()}" (approx 0.1 deg ~ 10 km grid) static String _gridKey(double lat, double lng) { return '${(lat * 10).floor()}_${(lng * 10).floor()}'; } /// Initializes the database from an optional custom JSON string or by loading /// the bundled asset `assets/data/us_speed_cameras.json`. Future initialize({String? jsonString}) async { if (_isInitialized && jsonString == null) return; try { String data = jsonString ?? ''; if (data.isEmpty) { try { data = await rootBundle.loadString('assets/data/us_speed_cameras.json'); } catch (e) { if (kDebugMode) { debugPrint('[UsSpeedCameraDatabase] Asset load fallback: $e'); } } } if (data.isNotEmpty) { final List parsed = json.decode(data); loadFromJsonList(parsed); } _isInitialized = true; } catch (e) { if (kDebugMode) { debugPrint('[UsSpeedCameraDatabase] Initialization error: $e'); } } } /// Loads camera records directly from a parsed JSON list void loadFromJsonList(List jsonList) { _allCameras.clear(); _spatialGrid.clear(); for (var item in jsonList) { if (item is Map) { final camera = UsSpeedCamera.fromJson(item); if (camera.id.isNotEmpty) { _allCameras.add(camera); final key = _gridKey(camera.latitude, camera.longitude); _spatialGrid.putIfAbsent(key, () => []).add(camera); } } } _isInitialized = true; if (kDebugMode) { debugPrint('[UsSpeedCameraDatabase] Loaded ${_allCameras.length} US speed cameras into ${_spatialGrid.length} spatial grid cells.'); } } /// Spatial search: Finds all cameras within [radiusMeters] of [latitude, longitude]. /// Utilizes the spatial grid index for sub-millisecond query performance. List findCamerasNear({ required double latitude, required double longitude, required double radiusMeters, }) { if (_allCameras.isEmpty) return []; // Calculate latitude and longitude search boundaries final latOffset = radiusMeters / 111320.0; final cosLat = math.cos(latitude * math.pi / 180.0); final lngOffset = radiusMeters / (111320.0 * (cosLat.abs() > 0.001 ? cosLat.abs() : 0.001)); final minLatBucket = ((latitude - latOffset) * 10).floor(); final maxLatBucket = ((latitude + latOffset) * 10).floor(); final minLngBucket = ((longitude - lngOffset) * 10).floor(); final maxLngBucket = ((longitude + lngOffset) * 10).floor(); final List results = []; final Set checkedIds = {}; for (int latB = minLatBucket; latB <= maxLatBucket; latB++) { for (int lngB = minLngBucket; lngB <= maxLngBucket; lngB++) { final key = '${latB}_$lngB'; final candidates = _spatialGrid[key]; if (candidates == null) continue; for (var camera in candidates) { if (checkedIds.contains(camera.id)) continue; checkedIds.add(camera.id); final dist = Geolocator.distanceBetween( latitude, longitude, camera.latitude, camera.longitude, ); if (dist <= radiusMeters) { results.add(camera.copyWithDistance(dist)); } } } } results.sort((a, b) => (a.distanceMeters ?? 0).compareTo(b.distanceMeters ?? 0)); return results; } /// Resets and clears the in-memory database (useful for testing) void reset() { _allCameras.clear(); _spatialGrid.clear(); _isInitialized = false; } }