rustsim-mobility 0.0.1

Multi-modal mobility glue for rustsim: leg-based trips, mode transitions, shared obstacle interfaces between crowds, vehicles, and transit
Documentation
//! Mode-transition state machine for travellers.
//!
//! A traveller progresses through their [`TripPlan`] one [`Leg`] at a
//! time. This crate tracks the current phase deterministically so the
//! domain crates (crowd, vehicle, transit) only have to react to state
//! changes, not compute them.

use crate::leg::{Leg, TripPlan};
use rustsim_modes::TravelMode;
use rustsim_transit::{RouteId, StopId, VehicleId};

/// Per-traveller context held by the caller.
#[derive(Debug, Clone)]
pub struct TravellerContext {
    /// Traveller (agent) identifier.
    pub traveller_id: u64,
    /// Index into the current trip's `legs` list.
    pub current_leg: usize,
    /// Current mode state.
    pub state: ModeState,
}

impl TravellerContext {
    /// Start a new traveller at leg 0 in [`ModeState::Idle`].
    pub fn new(traveller_id: u64) -> Self {
        Self {
            traveller_id,
            current_leg: 0,
            state: ModeState::Idle,
        }
    }
}

/// High-level mode state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeState {
    /// No current activity.
    Idle,
    /// Walking on foot.
    Walking,
    /// Driving a self-operated vehicle of the given mode.
    Driving(TravelMode),
    /// Waiting for a transit vehicle at a stop.
    WaitingAt(StopId, RouteId),
    /// Riding a transit vehicle toward an alighting stop.
    RidingTransit {
        /// Transit vehicle carrying the traveller.
        vehicle: VehicleId,
        /// Stop to alight at.
        alight_at: StopId,
    },
    /// Riding a connector (stair/escalator/lift).
    OnConnector(u64),
    /// Trip completed.
    Finished,
}

/// Controller that advances a traveller through a [`TripPlan`].
#[derive(Debug, Clone, Copy, Default)]
pub struct ModeController;

impl ModeController {
    /// Transition the traveller to the *start* of their current leg.
    /// Call this whenever the previous leg signals completion.
    pub fn enter_current_leg(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
        let Some(leg) = plan.legs.get(ctx.current_leg) else {
            ctx.state = ModeState::Finished;
            return;
        };
        ctx.state = match leg {
            Leg::Walk { .. } => ModeState::Walking,
            Leg::DriveSelf { mode, .. } | Leg::Hail { mode, .. } => ModeState::Driving(*mode),
            Leg::Transit {
                route, board_at, ..
            } => ModeState::WaitingAt(*board_at, *route),
            Leg::Connector { connector_id } => ModeState::OnConnector(*connector_id),
        };
    }

    /// Mark the current leg as complete and advance to the next one.
    pub fn complete_leg(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
        ctx.current_leg = ctx.current_leg.saturating_add(1);
        if ctx.current_leg >= plan.legs.len() {
            ctx.state = ModeState::Finished;
        } else {
            self.enter_current_leg(ctx, plan);
        }
    }

    /// Driven by transit code: a transit vehicle has arrived at the
    /// waiting stop, and the traveller boards it.
    pub fn board_transit(&self, ctx: &mut TravellerContext, vehicle: VehicleId, alight_at: StopId) {
        if let ModeState::WaitingAt(_, _) = ctx.state {
            ctx.state = ModeState::RidingTransit { vehicle, alight_at };
        }
    }

    /// Driven by transit code: the alighting stop has been reached.
    pub fn alight_transit(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
        if matches!(ctx.state, ModeState::RidingTransit { .. }) {
            self.complete_leg(ctx, plan);
        }
    }
}

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

    fn make_plan() -> TripPlan {
        TripPlan {
            id: 1,
            legs: vec![
                Leg::Walk {
                    from: Waypoint::ground(0.0, 0.0),
                    to: Waypoint::ground(5.0, 0.0),
                },
                Leg::Transit {
                    route: 42,
                    board_at: 10,
                    alight_at: 20,
                },
                Leg::Walk {
                    from: Waypoint::ground(100.0, 0.0),
                    to: Waypoint::ground(105.0, 0.0),
                },
            ],
        }
    }

    #[test]
    fn traveller_advances_through_all_legs() {
        let plan = make_plan();
        let controller = ModeController;
        let mut ctx = TravellerContext::new(7);
        controller.enter_current_leg(&mut ctx, &plan);
        assert_eq!(ctx.state, ModeState::Walking);

        controller.complete_leg(&mut ctx, &plan);
        assert_eq!(ctx.state, ModeState::WaitingAt(10, 42));

        controller.board_transit(&mut ctx, 99, 20);
        assert_eq!(
            ctx.state,
            ModeState::RidingTransit {
                vehicle: 99,
                alight_at: 20
            }
        );

        controller.alight_transit(&mut ctx, &plan);
        assert_eq!(ctx.state, ModeState::Walking);

        controller.complete_leg(&mut ctx, &plan);
        assert_eq!(ctx.state, ModeState::Finished);
    }
}