663 lines
20 KiB
Dart
663 lines
20 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart' show IconData, Icons;
|
|
import '../map/map_provider_interface.dart';
|
|
|
|
/// Semantic types of route stops and waypoints.
|
|
enum RouteStopType {
|
|
start,
|
|
origin,
|
|
meetup,
|
|
waypoint,
|
|
fuel,
|
|
rest,
|
|
food,
|
|
scenic,
|
|
regroup,
|
|
destination,
|
|
other;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case RouteStopType.start:
|
|
case RouteStopType.origin:
|
|
return 'Start / Origin';
|
|
case RouteStopType.meetup:
|
|
return 'Meeting Point';
|
|
case RouteStopType.waypoint:
|
|
return 'Waypoint';
|
|
case RouteStopType.fuel:
|
|
return 'Fuel Stop';
|
|
case RouteStopType.rest:
|
|
return 'Rest Area';
|
|
case RouteStopType.food:
|
|
return 'Food & Drink';
|
|
case RouteStopType.scenic:
|
|
return 'Scenic Overlook';
|
|
case RouteStopType.regroup:
|
|
return 'Regroup Point';
|
|
case RouteStopType.destination:
|
|
return 'Destination';
|
|
case RouteStopType.other:
|
|
return 'Stop';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A discrete, ordered location stop along a route.
|
|
@immutable
|
|
class RouteStop {
|
|
final String id;
|
|
final int order;
|
|
final RouteStopType type;
|
|
final String displayName;
|
|
final double latitude;
|
|
final double longitude;
|
|
final String address;
|
|
final String notes;
|
|
final int? plannedStopDurationMinutes;
|
|
final String? providerPlaceId;
|
|
|
|
const RouteStop({
|
|
required this.id,
|
|
int? order,
|
|
int? orderIndex,
|
|
RouteStopType? type,
|
|
RouteStopType? stopType,
|
|
required this.displayName,
|
|
required this.latitude,
|
|
required this.longitude,
|
|
this.address = '',
|
|
this.notes = '',
|
|
this.plannedStopDurationMinutes,
|
|
this.providerPlaceId,
|
|
}) : order = order ?? orderIndex ?? 0,
|
|
type = type ?? stopType ?? RouteStopType.waypoint;
|
|
|
|
int get orderIndex => order;
|
|
RouteStopType get stopType => type;
|
|
|
|
AppLatLng get latLng => AppLatLng(latitude, longitude);
|
|
|
|
RouteStop copyWith({
|
|
String? id,
|
|
int? order,
|
|
int? orderIndex,
|
|
RouteStopType? type,
|
|
RouteStopType? stopType,
|
|
String? displayName,
|
|
double? latitude,
|
|
double? longitude,
|
|
String? address,
|
|
String? notes,
|
|
int? plannedStopDurationMinutes,
|
|
String? providerPlaceId,
|
|
}) {
|
|
return RouteStop(
|
|
id: id ?? this.id,
|
|
order: order ?? orderIndex ?? this.order,
|
|
type: type ?? stopType ?? this.type,
|
|
displayName: displayName ?? this.displayName,
|
|
latitude: latitude ?? this.latitude,
|
|
longitude: longitude ?? this.longitude,
|
|
address: address ?? this.address,
|
|
notes: notes ?? this.notes,
|
|
plannedStopDurationMinutes:
|
|
plannedStopDurationMinutes ?? this.plannedStopDurationMinutes,
|
|
providerPlaceId: providerPlaceId ?? this.providerPlaceId,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'order': order,
|
|
'orderIndex': order,
|
|
'type': type.name,
|
|
'stopType': type.name,
|
|
'displayName': displayName,
|
|
'latitude': latitude,
|
|
'longitude': longitude,
|
|
if (address.isNotEmpty) 'address': address,
|
|
if (notes.isNotEmpty) 'notes': notes,
|
|
if (plannedStopDurationMinutes != null)
|
|
'plannedStopDurationMinutes': plannedStopDurationMinutes,
|
|
if (providerPlaceId != null) 'providerPlaceId': providerPlaceId,
|
|
};
|
|
|
|
factory RouteStop.fromJson(Map<String, dynamic> json) {
|
|
final rawOrder = json['orderIndex'] ?? json['order'];
|
|
final rawType = json['stopType'] ?? json['type'];
|
|
return RouteStop(
|
|
id: json['id'] as String? ?? '',
|
|
order: (rawOrder as num?)?.toInt() ?? 0,
|
|
type: RouteStopType.values.firstWhere(
|
|
(t) => t.name == (rawType as String?),
|
|
orElse: () => RouteStopType.waypoint,
|
|
),
|
|
displayName: json['displayName'] as String? ?? 'Stop',
|
|
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
|
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
|
address: json['address'] as String? ?? '',
|
|
notes: json['notes'] as String? ?? '',
|
|
plannedStopDurationMinutes:
|
|
(json['plannedStopDurationMinutes'] as num?)?.toInt(),
|
|
providerPlaceId: json['providerPlaceId'] as String?,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is RouteStop &&
|
|
runtimeType == other.runtimeType &&
|
|
id == other.id &&
|
|
order == other.order &&
|
|
type == other.type &&
|
|
displayName == other.displayName &&
|
|
latitude == other.latitude &&
|
|
longitude == other.longitude &&
|
|
address == other.address &&
|
|
notes == other.notes &&
|
|
plannedStopDurationMinutes == other.plannedStopDurationMinutes &&
|
|
providerPlaceId == other.providerPlaceId;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
id.hashCode ^
|
|
order.hashCode ^
|
|
type.hashCode ^
|
|
displayName.hashCode ^
|
|
latitude.hashCode ^
|
|
longitude.hashCode ^
|
|
address.hashCode ^
|
|
notes.hashCode ^
|
|
plannedStopDurationMinutes.hashCode ^
|
|
providerPlaceId.hashCode;
|
|
}
|
|
|
|
/// Visibility of a route.
|
|
enum RouteVisibility {
|
|
private,
|
|
groupOnly,
|
|
public;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case RouteVisibility.private:
|
|
return 'Private';
|
|
case RouteVisibility.groupOnly:
|
|
return 'Group Ride Only';
|
|
case RouteVisibility.public:
|
|
return 'Public';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Provenance of the route.
|
|
enum RouteSource {
|
|
manual,
|
|
planner,
|
|
gpxImport,
|
|
groupRideConversion;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case RouteSource.manual:
|
|
case RouteSource.planner:
|
|
return 'Manual Planner';
|
|
case RouteSource.gpxImport:
|
|
return 'GPX Import';
|
|
case RouteSource.groupRideConversion:
|
|
return 'Group Ride Adaptation';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reusable MotorSignal planned route model.
|
|
@immutable
|
|
class MotorSignalRoute {
|
|
final String id;
|
|
final String ownerUid;
|
|
final String ownerHandle;
|
|
final String title;
|
|
final String description;
|
|
final RouteVisibility visibility;
|
|
final List<RouteStop> stops;
|
|
final double straightLineDistanceMeters;
|
|
final double? estimatedDistanceMeters;
|
|
final int? estimatedDurationSeconds;
|
|
final RouteSource source;
|
|
final int version;
|
|
final String? attachedGroupRideId;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
MotorSignalRoute({
|
|
required this.id,
|
|
required this.ownerUid,
|
|
this.ownerHandle = '',
|
|
required this.title,
|
|
this.description = '',
|
|
this.visibility = RouteVisibility.private,
|
|
this.stops = const [],
|
|
double? straightLineDistanceMeters,
|
|
this.estimatedDistanceMeters,
|
|
this.estimatedDurationSeconds,
|
|
this.source = RouteSource.manual,
|
|
this.version = 1,
|
|
this.attachedGroupRideId,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
}) : straightLineDistanceMeters = straightLineDistanceMeters ??
|
|
calculateTotalDistanceMeters(stops);
|
|
|
|
/// Total straight-line distance in meters.
|
|
double get totalDistanceMeters => straightLineDistanceMeters;
|
|
|
|
/// Total straight-line distance in miles.
|
|
double get straightLineDistanceMiles =>
|
|
straightLineDistanceMeters * 0.000621371;
|
|
|
|
double get distanceMiles => straightLineDistanceMiles;
|
|
|
|
/// Distance string in miles.
|
|
String get formattedDistanceMiles =>
|
|
'${straightLineDistanceMiles.toStringAsFixed(1)} mi';
|
|
|
|
/// Distance string in kilometers.
|
|
String get formattedDistanceKm =>
|
|
'${(straightLineDistanceMeters / 1000).toStringAsFixed(1)} km';
|
|
|
|
/// Truthful distance string according to calculation method.
|
|
String get formattedDistance {
|
|
if (estimatedDistanceMeters != null) {
|
|
final roadMiles = estimatedDistanceMeters! * 0.000621371;
|
|
return '${roadMiles.toStringAsFixed(1)} mi (Road Est.)';
|
|
}
|
|
return '${straightLineDistanceMiles.toStringAsFixed(1)} mi (Straight-line)';
|
|
}
|
|
|
|
/// Truthful label describing the nature of the distance calculation.
|
|
String get distanceLabel => estimatedDistanceMeters != null
|
|
? 'Estimated road distance'
|
|
: 'Straight-line distance';
|
|
|
|
/// Returns stops strictly ordered by index.
|
|
List<RouteStop> get orderedStops {
|
|
final sorted = List<RouteStop>.from(stops);
|
|
sorted.sort((a, b) => a.order.compareTo(b.order));
|
|
return sorted;
|
|
}
|
|
|
|
/// Polyline points connecting ordered stops (straight-line preview).
|
|
List<AppLatLng> get previewPoints {
|
|
return orderedStops.map((s) => s.latLng).toList();
|
|
}
|
|
|
|
/// Calculates geodesic straight-line distance along ordered stops in meters using Haversine formula.
|
|
static double calculateTotalDistanceMeters(List<RouteStop> stops) {
|
|
if (stops.length < 2) return 0.0;
|
|
final sorted = List<RouteStop>.from(stops);
|
|
sorted.sort((a, b) => a.order.compareTo(b.order));
|
|
|
|
double total = 0.0;
|
|
for (int i = 0; i < sorted.length - 1; i++) {
|
|
total += _haversineMeters(
|
|
sorted[i].latitude,
|
|
sorted[i].longitude,
|
|
sorted[i + 1].latitude,
|
|
sorted[i + 1].longitude,
|
|
);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static double _haversineMeters(
|
|
double lat1,
|
|
double lon1,
|
|
double lat2,
|
|
double lon2,
|
|
) {
|
|
const double r = 6371000.0; // Earth radius in meters
|
|
final double phi1 = lat1 * math.pi / 180.0;
|
|
final double phi2 = lat2 * math.pi / 180.0;
|
|
final double deltaPhi = (lat2 - lat1) * math.pi / 180.0;
|
|
final double deltaLambda = (lon2 - lon1) * math.pi / 180.0;
|
|
|
|
final double a = math.sin(deltaPhi / 2.0) * math.sin(deltaPhi / 2.0) +
|
|
math.cos(phi1) *
|
|
math.cos(phi2) *
|
|
math.sin(deltaLambda / 2.0) *
|
|
math.sin(deltaLambda / 2.0);
|
|
final double c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a));
|
|
return r * c;
|
|
}
|
|
|
|
MotorSignalRoute copyWith({
|
|
String? id,
|
|
String? ownerUid,
|
|
String? ownerHandle,
|
|
String? title,
|
|
String? description,
|
|
RouteVisibility? visibility,
|
|
List<RouteStop>? stops,
|
|
double? straightLineDistanceMeters,
|
|
double? estimatedDistanceMeters,
|
|
int? estimatedDurationSeconds,
|
|
RouteSource? source,
|
|
int? version,
|
|
String? attachedGroupRideId,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
}) {
|
|
return MotorSignalRoute(
|
|
id: id ?? this.id,
|
|
ownerUid: ownerUid ?? this.ownerUid,
|
|
ownerHandle: ownerHandle ?? this.ownerHandle,
|
|
title: title ?? this.title,
|
|
description: description ?? this.description,
|
|
visibility: visibility ?? this.visibility,
|
|
stops: stops ?? this.stops,
|
|
straightLineDistanceMeters:
|
|
straightLineDistanceMeters ?? this.straightLineDistanceMeters,
|
|
estimatedDistanceMeters:
|
|
estimatedDistanceMeters ?? this.estimatedDistanceMeters,
|
|
estimatedDurationSeconds:
|
|
estimatedDurationSeconds ?? this.estimatedDurationSeconds,
|
|
source: source ?? this.source,
|
|
version: version ?? this.version,
|
|
attachedGroupRideId: attachedGroupRideId ?? this.attachedGroupRideId,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'ownerUid': ownerUid,
|
|
'ownerHandle': ownerHandle,
|
|
'title': title,
|
|
'description': description,
|
|
'visibility': visibility.name,
|
|
'stops': stops.map((s) => s.toJson()).toList(),
|
|
'straightLineDistanceMeters': straightLineDistanceMeters,
|
|
if (estimatedDistanceMeters != null)
|
|
'estimatedDistanceMeters': estimatedDistanceMeters,
|
|
if (estimatedDurationSeconds != null)
|
|
'estimatedDurationSeconds': estimatedDurationSeconds,
|
|
'source': source.name,
|
|
'version': version,
|
|
if (attachedGroupRideId != null)
|
|
'attachedGroupRideId': attachedGroupRideId,
|
|
'createdAt': createdAt.toIso8601String(),
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
|
|
factory MotorSignalRoute.fromJson(Map<String, dynamic> json) {
|
|
final rawStops = json['stops'] as List<dynamic>? ?? [];
|
|
final parsedStops = rawStops
|
|
.whereType<Map<String, dynamic>>()
|
|
.map((s) => RouteStop.fromJson(s))
|
|
.toList();
|
|
|
|
final created = json['createdAt'] != null
|
|
? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now()
|
|
: DateTime.now();
|
|
final updated = json['updatedAt'] != null
|
|
? DateTime.tryParse(json['updatedAt'] as String) ?? created
|
|
: created;
|
|
|
|
return MotorSignalRoute(
|
|
id: json['id'] as String? ?? '',
|
|
ownerUid: json['ownerUid'] as String? ?? '',
|
|
ownerHandle: json['ownerHandle'] as String? ?? '',
|
|
title: json['title'] as String? ?? 'Untitled Route',
|
|
description: json['description'] as String? ?? '',
|
|
visibility: RouteVisibility.values.firstWhere(
|
|
(v) => v.name == (json['visibility'] as String?),
|
|
orElse: () => RouteVisibility.private,
|
|
),
|
|
stops: parsedStops,
|
|
straightLineDistanceMeters:
|
|
(json['straightLineDistanceMeters'] as num?)?.toDouble() ??
|
|
calculateTotalDistanceMeters(parsedStops),
|
|
estimatedDistanceMeters:
|
|
(json['estimatedDistanceMeters'] as num?)?.toDouble(),
|
|
estimatedDurationSeconds:
|
|
(json['estimatedDurationSeconds'] as num?)?.toInt(),
|
|
source: RouteSource.values.firstWhere(
|
|
(s) => s.name == (json['source'] as String?),
|
|
orElse: () => RouteSource.manual,
|
|
),
|
|
version: (json['version'] as num?)?.toInt() ?? 1,
|
|
attachedGroupRideId: json['attachedGroupRideId'] as String?,
|
|
createdAt: created,
|
|
updatedAt: updated,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Semantic maneuver types for turn-by-turn navigation.
|
|
enum ManeuverType {
|
|
depart,
|
|
turnRight,
|
|
turnLeft,
|
|
turnSlightRight,
|
|
turnSlightLeft,
|
|
turnSharpRight,
|
|
turnSharpLeft,
|
|
keepRight,
|
|
keepLeft,
|
|
uTurn,
|
|
continueStraight,
|
|
enterRoundabout,
|
|
exitRoundabout,
|
|
ramp,
|
|
arrive,
|
|
unknown;
|
|
|
|
IconData get icon {
|
|
switch (this) {
|
|
case ManeuverType.depart:
|
|
return Icons.navigation;
|
|
case ManeuverType.turnRight:
|
|
case ManeuverType.turnSharpRight:
|
|
return Icons.turn_right;
|
|
case ManeuverType.turnLeft:
|
|
case ManeuverType.turnSharpLeft:
|
|
return Icons.turn_left;
|
|
case ManeuverType.turnSlightRight:
|
|
case ManeuverType.keepRight:
|
|
return Icons.turn_slight_right;
|
|
case ManeuverType.turnSlightLeft:
|
|
case ManeuverType.keepLeft:
|
|
return Icons.turn_slight_left;
|
|
case ManeuverType.uTurn:
|
|
return Icons.u_turn_left;
|
|
case ManeuverType.continueStraight:
|
|
return Icons.straight;
|
|
case ManeuverType.enterRoundabout:
|
|
case ManeuverType.exitRoundabout:
|
|
return Icons.roundabout_right;
|
|
case ManeuverType.ramp:
|
|
return Icons.ramp_right;
|
|
case ManeuverType.arrive:
|
|
return Icons.flag;
|
|
case ManeuverType.unknown:
|
|
return Icons.navigation;
|
|
}
|
|
}
|
|
|
|
String get directionShort {
|
|
switch (this) {
|
|
case ManeuverType.turnRight:
|
|
return 'RIGHT';
|
|
case ManeuverType.turnLeft:
|
|
return 'LEFT';
|
|
case ManeuverType.turnSlightRight:
|
|
return 'SLIGHT RIGHT';
|
|
case ManeuverType.turnSlightLeft:
|
|
return 'SLIGHT LEFT';
|
|
case ManeuverType.turnSharpRight:
|
|
return 'HARD RIGHT';
|
|
case ManeuverType.turnSharpLeft:
|
|
return 'HARD LEFT';
|
|
case ManeuverType.uTurn:
|
|
return 'U-TURN';
|
|
case ManeuverType.keepRight:
|
|
return 'KEEP RIGHT';
|
|
case ManeuverType.keepLeft:
|
|
return 'KEEP LEFT';
|
|
case ManeuverType.continueStraight:
|
|
return 'STRAIGHT';
|
|
case ManeuverType.depart:
|
|
return 'HEAD OUT';
|
|
case ManeuverType.arrive:
|
|
return 'ARRIVE';
|
|
default:
|
|
return 'CONTINUE';
|
|
}
|
|
}
|
|
|
|
bool get isLeft =>
|
|
this == ManeuverType.turnLeft ||
|
|
this == ManeuverType.turnSharpLeft ||
|
|
this == ManeuverType.turnSlightLeft ||
|
|
this == ManeuverType.keepLeft;
|
|
|
|
bool get isRight =>
|
|
this == ManeuverType.turnRight ||
|
|
this == ManeuverType.turnSharpRight ||
|
|
this == ManeuverType.turnSlightRight ||
|
|
this == ManeuverType.keepRight;
|
|
}
|
|
|
|
/// A single turn-by-turn navigation maneuver step.
|
|
@immutable
|
|
class RouteManeuver {
|
|
final int index;
|
|
final String instruction;
|
|
final ManeuverType type;
|
|
final double distanceMeters;
|
|
final int durationSeconds;
|
|
final String? streetName;
|
|
final AppLatLng location;
|
|
final int? geometryOffset;
|
|
|
|
const RouteManeuver({
|
|
required this.index,
|
|
required this.instruction,
|
|
required this.type,
|
|
required this.distanceMeters,
|
|
required this.durationSeconds,
|
|
this.streetName,
|
|
required this.location,
|
|
this.geometryOffset,
|
|
});
|
|
|
|
double get distanceMiles => distanceMeters * 0.000621371;
|
|
|
|
String get formattedDistance {
|
|
if (distanceMiles < 0.1) {
|
|
final feet = (distanceMeters * 3.28084).round();
|
|
return '$feet ft';
|
|
}
|
|
return '${distanceMiles.toStringAsFixed(1)} mi';
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'index': index,
|
|
'instruction': instruction,
|
|
'type': type.name,
|
|
'distanceMeters': distanceMeters,
|
|
'durationSeconds': durationSeconds,
|
|
if (streetName != null) 'streetName': streetName,
|
|
'latitude': location.latitude,
|
|
'longitude': location.longitude,
|
|
if (geometryOffset != null) 'geometryOffset': geometryOffset,
|
|
};
|
|
|
|
factory RouteManeuver.fromJson(Map<String, dynamic> json) {
|
|
return RouteManeuver(
|
|
index: (json['index'] as num?)?.toInt() ?? 0,
|
|
instruction: json['instruction'] as String? ?? '',
|
|
type: ManeuverType.values.firstWhere(
|
|
(t) => t.name == (json['type'] as String?),
|
|
orElse: () => ManeuverType.unknown,
|
|
),
|
|
distanceMeters: (json['distanceMeters'] as num?)?.toDouble() ?? 0.0,
|
|
durationSeconds: (json['durationSeconds'] as num?)?.toInt() ?? 0,
|
|
streetName: json['streetName'] as String?,
|
|
location: AppLatLng(
|
|
(json['latitude'] as num?)?.toDouble() ?? 0.0,
|
|
(json['longitude'] as num?)?.toDouble() ?? 0.0,
|
|
),
|
|
geometryOffset: (json['geometryOffset'] as num?)?.toInt(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Routing optimization preferences supported by MotorSignal and AWS Routes V2.
|
|
enum RoutingPreference {
|
|
fastest,
|
|
shortest,
|
|
twisty,
|
|
scenic;
|
|
|
|
String get label {
|
|
switch (this) {
|
|
case RoutingPreference.fastest:
|
|
return 'Fastest';
|
|
case RoutingPreference.shortest:
|
|
return 'Shortest';
|
|
case RoutingPreference.twisty:
|
|
return 'Twisty (Curvature)';
|
|
case RoutingPreference.scenic:
|
|
return 'Scenic (No Highways)';
|
|
}
|
|
}
|
|
|
|
String get subtitle {
|
|
switch (this) {
|
|
case RoutingPreference.fastest:
|
|
return 'Optimized for minimum travel time';
|
|
case RoutingPreference.shortest:
|
|
return 'Shortest total distance';
|
|
case RoutingPreference.twisty:
|
|
return 'Maximizes curves and elevation changes';
|
|
case RoutingPreference.scenic:
|
|
return 'Avoids limited-access highways and tolls';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Routing options provided to route calculation requests.
|
|
@immutable
|
|
class RoutingOptions {
|
|
final RoutingPreference preference;
|
|
final bool avoidHighways;
|
|
final bool avoidTolls;
|
|
final bool avoidFerries;
|
|
|
|
const RoutingOptions({
|
|
this.preference = RoutingPreference.fastest,
|
|
this.avoidHighways = false,
|
|
this.avoidTolls = false,
|
|
this.avoidFerries = false,
|
|
});
|
|
|
|
RoutingOptions copyWith({
|
|
RoutingPreference? preference,
|
|
bool? avoidHighways,
|
|
bool? avoidTolls,
|
|
bool? avoidFerries,
|
|
}) {
|
|
return RoutingOptions(
|
|
preference: preference ?? this.preference,
|
|
avoidHighways: avoidHighways ?? this.avoidHighways,
|
|
avoidTolls: avoidTolls ?? this.avoidTolls,
|
|
avoidFerries: avoidFerries ?? this.avoidFerries,
|
|
);
|
|
}
|
|
}
|