//! Domain model for Vehicle Routing Problem. //! //! # Overview //! //! Models a vehicle routing problem with: //! - Geographic [`Location`]s with haversine distance calculation //! - Customer [`Visit`]s with time windows, demand, and service duration //! - [`Vehicle`]s with capacity constraints and routes //! - [`VehicleRoutePlan`] as the complete planning solution //! //! # Design //! //! All scoring uses direct access to the plan's travel time matrix. //! No global state or RwLock overhead. use serde::{Deserialize, Serialize}; use solverforge::prelude::*; use std::collections::HashMap; /// Average driving speed in km/h for travel time estimation. pub const AVERAGE_SPEED_KMPH: f64 = 50.0; /// Earth radius in meters for haversine calculation. const EARTH_RADIUS_M: f64 = 6_371_000.0; /// A geographic location with latitude and longitude. /// /// Supports haversine distance calculation for travel time estimation. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::Location; /// /// let philadelphia = Location::new(0, 39.9526, -75.1652); /// let new_york = Location::new(1, 40.7128, -74.0060); /// /// // Distance is approximately 130 km /// let distance = philadelphia.distance_meters(&new_york); /// assert!(distance > 120_000.0 && distance < 140_000.0); /// /// // Travel time at 50 km/h is approximately 2.6 hours /// let travel_secs = philadelphia.travel_time_seconds(&new_york); /// assert!(travel_secs > 8000 && travel_secs < 10000); /// ``` #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Location { /// Index in `VehicleRoutePlan.locations`. pub index: usize, /// Latitude in degrees (-90 to 90). pub latitude: f64, /// Longitude in degrees (-180 to 180). pub longitude: f64, } impl PartialEq for Location { fn eq(&self, other: &Self) -> bool { self.index == other.index } } impl Eq for Location {} impl std::hash::Hash for Location { fn hash(&self, state: &mut H) { self.index.hash(state); } } impl Location { /// Creates a new location. pub fn new(index: usize, latitude: f64, longitude: f64) -> Self { Self { index, latitude, longitude, } } /// Calculates the great-circle distance in meters using the haversine formula. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::Location; /// /// let a = Location::new(0, 0.0, 0.0); /// let b = Location::new(1, 0.0, 1.0); /// /// // 1 degree of longitude at equator is about 111 km /// let dist = a.distance_meters(&b); /// assert!(dist > 110_000.0 && dist < 112_000.0); /// ``` pub fn distance_meters(&self, other: &Location) -> f64 { if self.latitude == other.latitude && self.longitude == other.longitude { return 0.0; } let lat1 = self.latitude.to_radians(); let lat2 = other.latitude.to_radians(); let lon1 = self.longitude.to_radians(); let lon2 = other.longitude.to_radians(); // Haversine formula let dlat = lat2 - lat1; let dlon = lon2 - lon1; let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().asin(); EARTH_RADIUS_M * c } /// Calculates travel time in seconds assuming average driving speed. /// /// Uses [`AVERAGE_SPEED_KMPH`] (50 km/h) for conversion. pub fn travel_time_seconds(&self, other: &Location) -> i64 { let meters = self.distance_meters(other); // seconds = meters / (km/h * 1000 / 3600) = meters * 3.6 / km/h (meters * 3.6 / AVERAGE_SPEED_KMPH).round() as i64 } } /// A customer visit with time window and demand constraints. /// /// # Time Window /// /// - `min_start_time`: Earliest time service can begin (vehicle may wait) /// - `max_end_time`: Latest time service must finish (hard constraint) /// - `service_duration`: Time required to complete the visit /// /// All times are in seconds from midnight. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::{Visit, Location}; /// /// let location = Location::new(0, 39.95, -75.17); /// /// // A restaurant delivery: 6am-10am window, 5-minute service /// let visit = Visit::new(0, "Restaurant A", location) /// .with_demand(8) /// .with_time_window(6 * 3600, 10 * 3600) /// .with_service_duration(300); /// /// assert_eq!(visit.demand, 8); /// assert_eq!(visit.min_start_time, 21600); // 6 * 3600 /// ``` #[problem_fact] #[derive(Serialize, Deserialize)] pub struct Visit { /// Index in `VehicleRoutePlan.visits`. pub index: usize, /// Customer name. pub name: String, /// The geographic location of this visit. pub location: Location, /// Quantity demanded (must fit in vehicle capacity). pub demand: i32, /// Earliest service start time (seconds from midnight). #[serde(rename = "minStartTime")] pub min_start_time: i64, /// Latest service end time (seconds from midnight). #[serde(rename = "maxEndTime")] pub max_end_time: i64, /// Service duration in seconds. #[serde(rename = "serviceDuration")] pub service_duration: i64, } impl Visit { /// Creates a new visit with default time window (all day). pub fn new(index: usize, name: impl Into, location: Location) -> Self { Self { index, name: name.into(), location, demand: 1, min_start_time: 0, max_end_time: 24 * 3600, service_duration: 0, } } /// Sets the demand. pub fn with_demand(mut self, demand: i32) -> Self { self.demand = demand; self } /// Sets the time window (min_start_time, max_end_time) in seconds from midnight. pub fn with_time_window(mut self, min_start: i64, max_end: i64) -> Self { self.min_start_time = min_start; self.max_end_time = max_end; self } /// Sets the service duration in seconds. pub fn with_service_duration(mut self, duration: i64) -> Self { self.service_duration = duration; self } } /// A delivery vehicle with capacity and assigned route. /// /// The route is stored as a list of visit indices in order. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::{Vehicle, Location}; /// /// let depot = Location::new(0, 39.95, -75.17); /// let vehicle = Vehicle::new(0, "Truck 1", 100, depot) /// .with_departure_time(8 * 3600); // Departs at 8am /// /// assert_eq!(vehicle.capacity, 100); /// assert!(vehicle.visits.is_empty()); /// ``` #[planning_entity] #[derive(Serialize, Deserialize)] pub struct Vehicle { /// Unique vehicle ID. #[planning_id] pub id: usize, /// Vehicle name for display. pub name: String, /// Maximum capacity (sum of visit demands must not exceed). pub capacity: i32, /// Home depot location. #[serde(rename = "homeLocation")] pub home_location: Location, /// Departure time from depot (seconds from midnight). #[serde(rename = "departureTime")] pub departure_time: i64, /// Ordered list of visit indices (the route). #[serde(default)] pub visits: Vec, } impl Vehicle { /// Creates a new vehicle with empty route. pub fn new(id: usize, name: impl Into, capacity: i32, home_location: Location) -> Self { Self { id, name: name.into(), capacity, home_location, departure_time: 8 * 3600, // Default 8am visits: Vec::new(), } } /// Sets the departure time in seconds from midnight. pub fn with_departure_time(mut self, time: i64) -> Self { self.departure_time = time; self } } /// Arrival and departure times for a visit in a route. #[derive(Debug, Clone, Copy)] pub struct VisitTiming { /// Visit index. pub visit_idx: usize, /// Arrival time at the visit (seconds from midnight). pub arrival: i64, /// Departure time from the visit (seconds from midnight). pub departure: i64, } /// The complete vehicle routing solution. /// /// Contains all problem facts (locations, visits) and planning entities (vehicles). /// Call `finalize()` after construction to populate the travel time matrix. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan}; /// /// let depot = Location::new(0, 39.95, -75.17); // Philadelphia /// let customer_loc = Location::new(1, 40.00, -75.10); /// /// let locations = vec![depot.clone(), customer_loc.clone()]; /// let visits = vec![ /// Visit::new(0, "Customer 1", customer_loc).with_demand(5), /// ]; /// let vehicles = vec![ /// Vehicle::new(0, "Truck 1", 100, depot), /// ]; /// /// let mut plan = VehicleRoutePlan::new("Philadelphia", locations, visits, vehicles); /// plan.finalize(); /// /// // Travel time matrix is now populated /// assert!(plan.travel_time(0, 1) > 0); /// ``` #[planning_solution] #[derive(Serialize, Deserialize)] pub struct VehicleRoutePlan { /// Problem name. pub name: String, /// South-west corner of bounding box (for map display). #[serde(rename = "southWestCorner")] pub south_west_corner: [f64; 2], /// North-east corner of bounding box (for map display). #[serde(rename = "northEastCorner")] pub north_east_corner: [f64; 2], /// All locations (depot and customer locations). #[problem_fact_collection] pub locations: Vec, /// All customer visits. #[problem_fact_collection] pub visits: Vec, /// All vehicles. #[planning_entity_collection] pub vehicles: Vec, /// Current score. #[planning_score] pub score: Option, /// Solver status for REST API. #[serde(rename = "solverStatus", skip_serializing_if = "Option::is_none")] pub solver_status: Option, /// Precomputed travel times: `travel_time_matrix[from][to]` in seconds. #[serde(skip)] pub travel_time_matrix: Vec>, /// Route geometries: `(from_loc, to_loc)` -> list of (lat, lng) waypoints. #[serde(skip)] pub route_geometries: HashMap<(usize, usize), Vec<(f64, f64)>>, } impl VehicleRoutePlan { /// Creates a new vehicle route plan. pub fn new( name: impl Into, locations: Vec, visits: Vec, vehicles: Vec, ) -> Self { // Compute bounding box from locations let (sw, ne) = Self::compute_bounds(&locations); Self { name: name.into(), south_west_corner: sw, north_east_corner: ne, locations, visits, vehicles, score: None, solver_status: None, travel_time_matrix: Vec::new(), route_geometries: HashMap::new(), } } /// Computes bounding box from locations. fn compute_bounds(locations: &[Location]) -> ([f64; 2], [f64; 2]) { if locations.is_empty() { return ([0.0, 0.0], [0.0, 0.0]); } let mut min_lat = f64::MAX; let mut max_lat = f64::MIN; let mut min_lon = f64::MAX; let mut max_lon = f64::MIN; for loc in locations { min_lat = min_lat.min(loc.latitude); max_lat = max_lat.max(loc.latitude); min_lon = min_lon.min(loc.longitude); max_lon = max_lon.max(loc.longitude); } // No padding here - init_routing() adds expansion ([min_lat, min_lon], [max_lat, max_lon]) } /// Populates travel time matrix using haversine distances. /// /// Must be called after construction and before solving. /// For real road routing, use `init_routing()` instead. pub fn finalize(&mut self) { let n = self.locations.len(); self.travel_time_matrix = vec![vec![0; n]; n]; for i in 0..n { for j in 0..n { if i != j { self.travel_time_matrix[i][j] = self.locations[i].travel_time_seconds(&self.locations[j]); } } } } /// Initializes with real road routing from OSM data. /// /// Downloads road network via Overpass API (cached), builds graph, /// and computes travel times using Dijkstra shortest paths. /// Also stores route geometries for visualization. pub async fn init_routing(&mut self) -> Result<(), crate::routing::RoutingError> { use crate::routing::{BoundingBox, RoadNetwork}; // Build bounding box from plan bounds (with expansion) let bbox = BoundingBox::new( self.south_west_corner[0], self.south_west_corner[1], self.north_east_corner[0], self.north_east_corner[1], ) .expand(0.05); // 5% expansion to catch nearby roads // Load or fetch road network let network = RoadNetwork::load_or_fetch(&bbox).await?; // Extract coordinates let coords: Vec<(f64, f64)> = self .locations .iter() .map(|l| (l.latitude, l.longitude)) .collect(); // Compute travel time matrix self.travel_time_matrix = network.compute_matrix(&coords); // Compute route geometries for visualization self.route_geometries = network.compute_all_geometries(&coords); Ok(()) } /// Returns the bounding box for this plan. pub fn bounding_box(&self) -> crate::routing::BoundingBox { crate::routing::BoundingBox::new( self.south_west_corner[0], self.south_west_corner[1], self.north_east_corner[0], self.north_east_corner[1], ) } /// Gets travel time between two locations in seconds. /// /// Returns 0 if indices are out of bounds or matrix not initialized. #[inline] pub fn travel_time(&self, from_idx: usize, to_idx: usize) -> i64 { self.travel_time_matrix .get(from_idx) .and_then(|row| row.get(to_idx)) .copied() .unwrap_or(0) } /// Gets route geometry between two locations. /// /// Returns the waypoints if real road routing was initialized, /// or `None` if using haversine fallback. #[inline] pub fn route_geometry(&self, from_idx: usize, to_idx: usize) -> Option<&[(f64, f64)]> { self.route_geometries.get(&(from_idx, to_idx)).map(|v| v.as_slice()) } /// Gets a location by index. #[inline] pub fn get_location(&self, idx: usize) -> Option<&Location> { self.locations.get(idx) } /// Gets a visit by index. #[inline] pub fn get_visit(&self, idx: usize) -> Option<&Visit> { self.visits.get(idx) } /// Calculates arrival and departure times for each visit in a vehicle's route. /// /// Returns a vector of [`VisitTiming`] in route order. /// /// # Examples /// /// ``` /// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan}; /// /// let depot = Location::new(0, 0.0, 0.0); /// let customer_loc = Location::new(1, 0.0, 0.01); // ~1.1 km away /// /// let locations = vec![depot.clone(), customer_loc.clone()]; /// let visits = vec![ /// Visit::new(0, "A", customer_loc) /// .with_service_duration(300) /// .with_time_window(0, 86400), /// ]; /// let mut vehicle = Vehicle::new(0, "V1", 100, depot); /// vehicle.departure_time = 8 * 3600; // 8am /// vehicle.visits = vec![0]; /// /// let mut plan = VehicleRoutePlan::new("test", locations, visits, vec![vehicle]); /// plan.finalize(); /// /// let timings = plan.calculate_route_times(&plan.vehicles[0]); /// assert_eq!(timings.len(), 1); /// assert!(timings[0].arrival > 8 * 3600); // Arrives after departure /// assert_eq!(timings[0].departure, timings[0].arrival + 300); // Service takes 5 min /// ``` pub fn calculate_route_times(&self, vehicle: &Vehicle) -> Vec { let mut timings = Vec::with_capacity(vehicle.visits.len()); let mut current_time = vehicle.departure_time; let mut current_loc = vehicle.home_location.index; for &visit_idx in &vehicle.visits { let Some(visit) = self.visits.get(visit_idx) else { continue; }; // Travel to this visit let travel = self.travel_time(current_loc, visit.location.index); let arrival = current_time + travel; // Service starts at max(arrival, min_start_time) let service_start = arrival.max(visit.min_start_time); let departure = service_start + visit.service_duration; timings.push(VisitTiming { visit_idx, arrival, departure, }); current_time = departure; current_loc = visit.location.index; } timings } /// Calculates total driving time for a vehicle's route in seconds. /// /// Includes travel from depot, between visits, and back to depot. pub fn total_driving_time(&self, vehicle: &Vehicle) -> i64 { if vehicle.visits.is_empty() { return 0; } let mut total = 0i64; let mut current_loc = vehicle.home_location.index; for &visit_idx in &vehicle.visits { if let Some(visit) = self.visits.get(visit_idx) { total += self.travel_time(current_loc, visit.location.index); current_loc = visit.location.index; } } // Return to depot total += self.travel_time(current_loc, vehicle.home_location.index); total } /// Calculates total driving time across all vehicles. pub fn total_driving_time_all(&self) -> i64 { self.vehicles.iter().map(|v| self.total_driving_time(v)).sum() } }