import '../map/map_provider_interface.dart'; import 'route_models.dart'; /// Exceptions domain mapped from standard provider API. class RouteProviderException implements Exception { final String code; final String message; RouteProviderException(this.code, this.message); @override String toString() => 'RouteProviderException: $message ($code)'; } /// Result of a place/location search query. class PlaceSearchResult { final String providerId; final String primaryText; final String secondaryText; final double? latitude; final double? longitude; final String? formattedAddress; final String? attribution; const PlaceSearchResult({ required this.providerId, required this.primaryText, required this.secondaryText, this.latitude, this.longitude, this.formattedAddress, this.attribution, }); bool get hasCoordinates => latitude != null && longitude != null; AppLatLng? get latLng => hasCoordinates ? AppLatLng(latitude!, longitude!) : null; /// Converts directly into a [ResolvedPlace] when coordinates are already present. ResolvedPlace? toResolvedPlace() { if (!hasCoordinates) return null; return ResolvedPlace( providerId: providerId, displayName: primaryText, formattedAddress: formattedAddress ?? (secondaryText.isNotEmpty ? '$primaryText, $secondaryText' : primaryText), latitude: latitude!, longitude: longitude!, attribution: attribution, ); } } /// Resolved place with authoritative coordinates. class ResolvedPlace { final String providerId; final String displayName; final String? formattedAddress; final double latitude; final double longitude; final String? attribution; const ResolvedPlace({ required this.providerId, required this.displayName, this.formattedAddress, required this.latitude, required this.longitude, this.attribution, }); AppLatLng get latLng => AppLatLng(latitude, longitude); } /// Result of calculating a route along ordered stops. class RouteCalculationResult { final double straightLineDistanceMeters; final double? estimatedDistanceMeters; final int? estimatedDurationSeconds; final List geometry; final bool isStraightLine; final String providerName; final List warnings; const RouteCalculationResult({ required this.straightLineDistanceMeters, this.estimatedDistanceMeters, this.estimatedDurationSeconds, required this.geometry, required this.isStraightLine, required this.providerName, this.warnings = const [], }); /// Alias for geometry points. List get points => geometry; /// Total straight-line distance in meters. double get totalDistanceMeters => straightLineDistanceMeters; /// Alias for isStraightLine. bool get isHonestStraightLine => isStraightLine; /// Total distance in miles. double get distanceMiles { final meters = estimatedDistanceMeters ?? straightLineDistanceMeters; return meters * 0.000621371; } /// Truthful distance description. String get distanceLabel { if (estimatedDistanceMeters != null) { return '${distanceMiles.toStringAsFixed(1)} mi (Road Est.)'; } return '${distanceMiles.toStringAsFixed(1)} mi (Straight-line)'; } } /// Abstract provider interface for calculating routes and searching places. abstract class RouteProvider { String get name; /// Calculates route geometry and metrics between ordered stops. Future calculateRoute( List stops, { bool avoidHighways = false, bool avoidTolls = false, bool avoidFerries = false, }); /// Searches for places by textual query. Future> searchPlaces( String query, { AppLatLng? nearLocation, }); /// Resolves a place ID into full coordinates. Future resolvePlace(String placeId); } /// Default geodesic straight-line preview implementation. /// /// Truthfully connects stops with direct segments and calculates geodesic /// distance without fabricating road paths or speculative turn ETAs. class StraightLineRouteProvider implements RouteProvider { const StraightLineRouteProvider(); @override String get name => 'Straight-line Preview'; @override Future calculateRoute( List stops, { bool avoidHighways = false, bool avoidTolls = false, bool avoidFerries = false, }) async { final ordered = List.from(stops); ordered.sort((a, b) => a.order.compareTo(b.order)); final distanceMeters = MotorSignalRoute.calculateTotalDistanceMeters(ordered); final points = ordered.map((s) => s.latLng).toList(); return RouteCalculationResult( straightLineDistanceMeters: distanceMeters, estimatedDistanceMeters: null, estimatedDurationSeconds: null, geometry: points, isStraightLine: true, providerName: name, ); } @override Future> searchPlaces( String query, { AppLatLng? nearLocation, }) async { final clean = query.trim(); if (clean.isEmpty) return const []; // Check if query is direct coordinates (lat, lng) final parts = clean.split(RegExp(r'[,\s]+')); if (parts.length == 2) { final lat = double.tryParse(parts[0]); final lng = double.tryParse(parts[1]); if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) { return [ PlaceSearchResult( providerId: 'coord_${lat.toStringAsFixed(4)}_${lng.toStringAsFixed(4)}', primaryText: 'Coordinate Pin', secondaryText: '${lat.toStringAsFixed(4)}, ${lng.toStringAsFixed(4)}', latitude: lat, longitude: lng, formattedAddress: '${lat.toStringAsFixed(5)}, ${lng.toStringAsFixed(5)}', ), ]; } } return [ PlaceSearchResult( providerId: 'manual', primaryText: clean, secondaryText: 'Near Current Map Location', latitude: nearLocation?.latitude, longitude: nearLocation?.longitude, ), ]; } @override Future resolvePlace(String placeId) async { return ResolvedPlace( providerId: placeId, displayName: 'Unknown Place', latitude: 0, longitude: 0, ); } }