231 lines
6.3 KiB
Dart
231 lines
6.3 KiB
Dart
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<AppLatLng> geometry;
|
|
final bool isStraightLine;
|
|
final String providerName;
|
|
final List<String> 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<AppLatLng> 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<RouteCalculationResult> calculateRoute(
|
|
List<RouteStop> stops, {
|
|
bool avoidHighways = false,
|
|
bool avoidTolls = false,
|
|
bool avoidFerries = false,
|
|
});
|
|
|
|
/// Searches for places by textual query.
|
|
Future<List<PlaceSearchResult>> searchPlaces(
|
|
String query, {
|
|
AppLatLng? nearLocation,
|
|
});
|
|
|
|
/// Resolves a place ID into full coordinates.
|
|
Future<ResolvedPlace> 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<RouteCalculationResult> calculateRoute(
|
|
List<RouteStop> stops, {
|
|
bool avoidHighways = false,
|
|
bool avoidTolls = false,
|
|
bool avoidFerries = false,
|
|
}) async {
|
|
final ordered = List<RouteStop>.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<List<PlaceSearchResult>> 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<ResolvedPlace> resolvePlace(String placeId) async {
|
|
return ResolvedPlace(
|
|
providerId: placeId,
|
|
displayName: 'Unknown Place',
|
|
latitude: 0,
|
|
longitude: 0,
|
|
);
|
|
}
|
|
}
|