rustsim-transit 0.0.1

Public-transit primitives for rustsim: stops, routes, schedules, boarding/alighting queues, dwell times, headway control
Documentation
//! Transit vehicles: service instances riding a route.

use crate::route::RouteId;
use crate::stop::StopId;
use crate::PassengerId;

/// Stable identifier for a [`TransitVehicle`].
pub type VehicleId = u64;

/// Operating state of a transit vehicle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VehiclePhase {
    /// Depot, not yet dispatched.
    Idle,
    /// En route between two stops.
    InTransit,
    /// Dwelling at a stop.
    Dwelling,
    /// Completed its route for the day.
    Finished,
}

/// A service instance (one bus, tram, metro unit, …).
#[derive(Debug, Clone)]
pub struct TransitVehicle {
    /// Unique identifier.
    pub id: VehicleId,
    /// Which route this vehicle is assigned to.
    pub route: RouteId,
    /// Current stop index along the route (0 == first stop).
    pub current_stop: usize,
    /// Current operating phase.
    pub phase: VehiclePhase,
    /// Maximum seated + standing passengers.
    pub capacity: u32,
    /// On-board passengers.
    pub passengers: Vec<PassengerId>,
    /// Seconds remaining until the next phase transition.
    pub time_remaining: f64,
}

impl TransitVehicle {
    /// Create an idle vehicle ready for dispatch.
    pub fn idle(id: VehicleId, route: RouteId, capacity: u32) -> Self {
        Self {
            id,
            route,
            current_stop: 0,
            phase: VehiclePhase::Idle,
            capacity,
            passengers: Vec::new(),
            time_remaining: 0.0,
        }
    }

    /// Is there room for one more passenger?
    pub fn has_room(&self) -> bool {
        (self.passengers.len() as u32) < self.capacity
    }

    /// Number of free seats/standing room.
    pub fn free_capacity(&self) -> u32 {
        self.capacity.saturating_sub(self.passengers.len() as u32)
    }

    /// Add a passenger. Returns `false` if the vehicle was already full.
    pub fn add_passenger(&mut self, id: PassengerId) -> bool {
        if self.has_room() {
            self.passengers.push(id);
            true
        } else {
            false
        }
    }

    /// Remove and return every passenger whose destination is `stop`.
    /// `destinations` is a slice where `destinations[i]` is the alighting
    /// stop of `self.passengers[i]`.
    pub fn alight_at(&mut self, stop: StopId, destinations: &[StopId]) -> Vec<PassengerId> {
        assert_eq!(self.passengers.len(), destinations.len());
        let mut out = Vec::new();
        let mut kept_dests = Vec::new();
        let mut kept_pax = Vec::new();
        for (p, d) in self.passengers.iter().zip(destinations.iter()) {
            if *d == stop {
                out.push(*p);
            } else {
                kept_pax.push(*p);
                kept_dests.push(*d);
            }
        }
        self.passengers = kept_pax;
        // NOTE: The caller owns the `destinations` vector; this function
        // does not mutate it, so the caller should drop alighters from
        // its own mirror in the same order we did.
        let _ = kept_dests;
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capacity_tracking() {
        let mut v = TransitVehicle::idle(1, 42, 3);
        assert!(v.add_passenger(10));
        assert!(v.add_passenger(11));
        assert!(v.add_passenger(12));
        assert!(!v.add_passenger(13));
        assert_eq!(v.free_capacity(), 0);
    }

    #[test]
    fn alight_removes_targeted_passengers() {
        let mut v = TransitVehicle::idle(1, 42, 10);
        v.add_passenger(10);
        v.add_passenger(11);
        v.add_passenger(12);
        let dests = vec![5, 7, 5];
        let out = v.alight_at(5, &dests);
        assert_eq!(out, vec![10, 12]);
        assert_eq!(v.passengers, vec![11]);
    }
}