mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-29 04:35:51 +03:00
very basic tool to explore an agent's full trip
This commit is contained in:
parent
218082140f
commit
334081b844
@ -1,7 +1,9 @@
|
|||||||
mod score;
|
mod score;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
use crate::common::{time_controls, AgentTools, CommonState, RouteExplorer, SpeedControls};
|
use crate::common::{
|
||||||
|
time_controls, AgentTools, CommonState, RouteExplorer, SpeedControls, TripExplorer,
|
||||||
|
};
|
||||||
use crate::game::{State, Transition};
|
use crate::game::{State, Transition};
|
||||||
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
||||||
use crate::ui::{PerMapUI, UI};
|
use crate::ui::{PerMapUI, UI};
|
||||||
@ -119,6 +121,9 @@ impl State for ABTestMode {
|
|||||||
if let Some(explorer) = RouteExplorer::new(ctx, ui) {
|
if let Some(explorer) = RouteExplorer::new(ctx, ui) {
|
||||||
return Transition::Push(Box::new(explorer));
|
return Transition::Push(Box::new(explorer));
|
||||||
}
|
}
|
||||||
|
if let Some(explorer) = TripExplorer::new(ctx, ui) {
|
||||||
|
return Transition::Push(Box::new(explorer));
|
||||||
|
}
|
||||||
|
|
||||||
self.primary_agent_tools.event(ctx, ui, &mut self.menu);
|
self.primary_agent_tools.event(ctx, ui, &mut self.menu);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ mod route_explorer;
|
|||||||
mod route_viewer;
|
mod route_viewer;
|
||||||
mod speed;
|
mod speed;
|
||||||
mod time;
|
mod time;
|
||||||
|
mod trip_explorer;
|
||||||
mod turn_cycler;
|
mod turn_cycler;
|
||||||
mod warp;
|
mod warp;
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ pub use self::agent::AgentTools;
|
|||||||
pub use self::route_explorer::RouteExplorer;
|
pub use self::route_explorer::RouteExplorer;
|
||||||
pub use self::speed::SpeedControls;
|
pub use self::speed::SpeedControls;
|
||||||
pub use self::time::time_controls;
|
pub use self::time::time_controls;
|
||||||
|
pub use self::trip_explorer::TripExplorer;
|
||||||
use crate::game::Transition;
|
use crate::game::Transition;
|
||||||
use crate::helpers::ID;
|
use crate::helpers::ID;
|
||||||
use crate::render::DrawOptions;
|
use crate::render::DrawOptions;
|
||||||
|
92
editor/src/common/trip_explorer.rs
Normal file
92
editor/src/common/trip_explorer.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::common::CommonState;
|
||||||
|
use crate::game::{State, Transition};
|
||||||
|
use crate::helpers::ID;
|
||||||
|
use crate::ui::UI;
|
||||||
|
use ezgui::{EventCtx, GfxCtx, Key, Text, WarpingItemSlider};
|
||||||
|
use geom::Pt2D;
|
||||||
|
use sim::{TripEnd, TripStart};
|
||||||
|
|
||||||
|
// TODO More info, like each leg of the trip, times, separate driving leg for looking for
|
||||||
|
// parking...
|
||||||
|
pub struct TripExplorer {
|
||||||
|
slider: WarpingItemSlider<ID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TripExplorer {
|
||||||
|
pub fn new(ctx: &mut EventCtx, ui: &UI) -> Option<TripExplorer> {
|
||||||
|
let map = &ui.primary.map;
|
||||||
|
let agent = ui.primary.current_selection.and_then(|id| id.agent_id())?;
|
||||||
|
let trip = ui.primary.sim.agent_to_trip(agent)?;
|
||||||
|
let status = ui.primary.sim.trip_status(trip)?;
|
||||||
|
if !ctx.input.contextual_action(Key::T, "explore trip") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let steps: Vec<(Pt2D, ID, Text)> = vec![
|
||||||
|
match status.start {
|
||||||
|
TripStart::Bldg(b) => (
|
||||||
|
map.get_b(b).front_path.line.pt1(),
|
||||||
|
ID::Building(b),
|
||||||
|
Text::from_line(format!("start at {}", map.get_b(b).get_name())),
|
||||||
|
),
|
||||||
|
TripStart::Appearing(pos) => (
|
||||||
|
pos.pt(map),
|
||||||
|
ID::Lane(pos.lane()),
|
||||||
|
Text::from_line(format!(
|
||||||
|
"start by appearing at {}",
|
||||||
|
map.get_parent(pos.lane()).get_name()
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
(
|
||||||
|
ui.primary.sim.get_canonical_pt_per_trip(trip, map).unwrap(),
|
||||||
|
ID::from_agent(agent),
|
||||||
|
Text::from_line("currently here".to_string()),
|
||||||
|
),
|
||||||
|
match status.end {
|
||||||
|
TripEnd::Bldg(b) => (
|
||||||
|
map.get_b(b).front_path.line.pt1(),
|
||||||
|
ID::Building(b),
|
||||||
|
Text::from_line(format!("end at {}", map.get_b(b).get_name())),
|
||||||
|
),
|
||||||
|
TripEnd::Border(i) => (
|
||||||
|
map.get_i(i).polygon.center(),
|
||||||
|
ID::Intersection(i),
|
||||||
|
Text::from_line(format!("leave map via {}", i)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Some(TripExplorer {
|
||||||
|
slider: WarpingItemSlider::new(
|
||||||
|
steps,
|
||||||
|
&format!("Trip Explorer for {}", trip),
|
||||||
|
"step",
|
||||||
|
ctx,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State for TripExplorer {
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition {
|
||||||
|
if ctx.redo_mouseover() {
|
||||||
|
ui.recalculate_current_selection(ctx);
|
||||||
|
}
|
||||||
|
ctx.canvas.handle_event(ctx.input);
|
||||||
|
|
||||||
|
if let Some((evmode, done_warping)) = self.slider.event(ctx) {
|
||||||
|
if done_warping {
|
||||||
|
ui.primary.current_selection = Some(*self.slider.get().1);
|
||||||
|
}
|
||||||
|
Transition::KeepWithMode(evmode)
|
||||||
|
} else {
|
||||||
|
Transition::Pop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, g: &mut GfxCtx, ui: &UI) {
|
||||||
|
self.slider.draw(g);
|
||||||
|
CommonState::draw_osd(g, ui, ui.primary.current_selection);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,9 @@ mod show_activity;
|
|||||||
mod spawner;
|
mod spawner;
|
||||||
mod time_travel;
|
mod time_travel;
|
||||||
|
|
||||||
use crate::common::{time_controls, AgentTools, CommonState, RouteExplorer, SpeedControls};
|
use crate::common::{
|
||||||
|
time_controls, AgentTools, CommonState, RouteExplorer, SpeedControls, TripExplorer,
|
||||||
|
};
|
||||||
use crate::debug::DebugMode;
|
use crate::debug::DebugMode;
|
||||||
use crate::edit::EditMode;
|
use crate::edit::EditMode;
|
||||||
use crate::game::{State, Transition};
|
use crate::game::{State, Transition};
|
||||||
@ -99,10 +101,13 @@ impl State for SandboxMode {
|
|||||||
if let Some(explorer) = RouteExplorer::new(ctx, ui) {
|
if let Some(explorer) = RouteExplorer::new(ctx, ui) {
|
||||||
return Transition::Push(Box::new(explorer));
|
return Transition::Push(Box::new(explorer));
|
||||||
}
|
}
|
||||||
|
if let Some(explorer) = TripExplorer::new(ctx, ui) {
|
||||||
|
return Transition::Push(Box::new(explorer));
|
||||||
|
}
|
||||||
|
|
||||||
self.agent_tools.event(ctx, ui, &mut self.menu);
|
self.agent_tools.event(ctx, ui, &mut self.menu);
|
||||||
self.show_activity.event(ctx, ui, &mut self.menu);
|
self.show_activity.event(ctx, ui, &mut self.menu);
|
||||||
if self.menu.action("start time traveling") {
|
if ui.primary.current_selection.is_none() && self.menu.action("start time traveling") {
|
||||||
return self.time_travel.start(ctx, ui);
|
return self.time_travel.start(ctx, ui);
|
||||||
}
|
}
|
||||||
if self.menu.action("scoreboard") {
|
if self.menu.action("scoreboard") {
|
||||||
|
@ -20,7 +20,7 @@ pub(crate) use self::router::{ActionAtEnd, Router};
|
|||||||
pub(crate) use self::scheduler::{Command, Scheduler};
|
pub(crate) use self::scheduler::{Command, Scheduler};
|
||||||
pub use self::sim::Sim;
|
pub use self::sim::Sim;
|
||||||
pub(crate) use self::transit::TransitSimState;
|
pub(crate) use self::transit::TransitSimState;
|
||||||
pub use self::trips::{FinishedTrips, TripMode};
|
pub use self::trips::{FinishedTrips, TripEnd, TripMode, TripStart, TripStatus};
|
||||||
pub(crate) use self::trips::{TripLeg, TripManager};
|
pub(crate) use self::trips::{TripLeg, TripManager};
|
||||||
pub use crate::render::{CarStatus, DrawCarInput, DrawPedestrianInput, GetDrawAgents};
|
pub use crate::render::{CarStatus, DrawCarInput, DrawPedestrianInput, GetDrawAgents};
|
||||||
use abstutil::Cloneable;
|
use abstutil::Cloneable;
|
||||||
|
@ -60,10 +60,8 @@ impl SimFlags {
|
|||||||
if self.load.starts_with(Path::new("../data/save/")) {
|
if self.load.starts_with(Path::new("../data/save/")) {
|
||||||
timer.note(format!("Resuming from {}", self.load.display()));
|
timer.note(format!("Resuming from {}", self.load.display()));
|
||||||
|
|
||||||
timer.start("read sim savestate");
|
let sim: Sim = abstutil::read_binary(self.load.to_str().unwrap(), timer)
|
||||||
let sim: Sim =
|
.expect("loading sim state failed");
|
||||||
abstutil::read_json(self.load.to_str().unwrap()).expect("loading sim state failed");
|
|
||||||
timer.stop("read sim savestate");
|
|
||||||
|
|
||||||
let mut map: Map =
|
let mut map: Map =
|
||||||
abstutil::read_binary(&format!("../data/maps/{}.bin", sim.map_name), timer)
|
abstutil::read_binary(&format!("../data/maps/{}.bin", sim.map_name), timer)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
CarID, Command, CreateCar, CreatePedestrian, DrivingGoal, ParkingSimState, ParkingSpot,
|
CarID, Command, CreateCar, CreatePedestrian, DrivingGoal, ParkingSimState, ParkingSpot,
|
||||||
PedestrianID, Scheduler, SidewalkPOI, SidewalkSpot, TripLeg, TripManager, VehicleSpec,
|
PedestrianID, Scheduler, SidewalkPOI, SidewalkSpot, TripLeg, TripManager, TripStart,
|
||||||
VehicleType, MAX_CAR_LENGTH,
|
VehicleSpec, VehicleType, MAX_CAR_LENGTH,
|
||||||
};
|
};
|
||||||
use abstutil::Timer;
|
use abstutil::Timer;
|
||||||
use geom::{Duration, Speed, EPSILON_DIST};
|
use geom::{Duration, Speed, EPSILON_DIST};
|
||||||
@ -187,7 +187,8 @@ impl TripSpawner {
|
|||||||
SidewalkSpot::building(b, map),
|
SidewalkSpot::building(b, map),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let trip = trips.new_trip(start_time, legs);
|
let trip =
|
||||||
|
trips.new_trip(start_time, Some(TripStart::Appearing(start_pos)), legs);
|
||||||
let router = goal.make_router(path, map, vehicle.vehicle_type);
|
let router = goal.make_router(path, map, vehicle.vehicle_type);
|
||||||
scheduler.quick_push(
|
scheduler.quick_push(
|
||||||
start_time,
|
start_time,
|
||||||
@ -225,7 +226,11 @@ impl TripSpawner {
|
|||||||
}
|
}
|
||||||
DrivingGoal::Border(_, _) => {}
|
DrivingGoal::Border(_, _) => {}
|
||||||
}
|
}
|
||||||
let trip = trips.new_trip(start_time, legs);
|
let trip = trips.new_trip(
|
||||||
|
start_time,
|
||||||
|
Some(TripStart::Bldg(vehicle.owner.unwrap())),
|
||||||
|
legs,
|
||||||
|
);
|
||||||
|
|
||||||
scheduler.quick_push(
|
scheduler.quick_push(
|
||||||
start_time,
|
start_time,
|
||||||
@ -246,6 +251,13 @@ impl TripSpawner {
|
|||||||
} => {
|
} => {
|
||||||
let trip = trips.new_trip(
|
let trip = trips.new_trip(
|
||||||
start_time,
|
start_time,
|
||||||
|
match start.connection {
|
||||||
|
SidewalkPOI::Building(b) => Some(TripStart::Bldg(b)),
|
||||||
|
SidewalkPOI::SuddenlyAppear | SidewalkPOI::Border(_) => {
|
||||||
|
Some(TripStart::Appearing(start.sidewalk_pos))
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
vec![TripLeg::Walk(ped_id.unwrap(), ped_speed, goal.clone())],
|
vec![TripLeg::Walk(ped_id.unwrap(), ped_speed, goal.clone())],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -282,7 +294,17 @@ impl TripSpawner {
|
|||||||
}
|
}
|
||||||
DrivingGoal::Border(_, _) => {}
|
DrivingGoal::Border(_, _) => {}
|
||||||
};
|
};
|
||||||
let trip = trips.new_trip(start_time, legs);
|
let trip = trips.new_trip(
|
||||||
|
start_time,
|
||||||
|
match start.connection {
|
||||||
|
SidewalkPOI::Building(b) => Some(TripStart::Bldg(b)),
|
||||||
|
SidewalkPOI::SuddenlyAppear | SidewalkPOI::Border(_) => {
|
||||||
|
Some(TripStart::Appearing(start.sidewalk_pos))
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
legs,
|
||||||
|
);
|
||||||
|
|
||||||
scheduler.quick_push(
|
scheduler.quick_push(
|
||||||
start_time,
|
start_time,
|
||||||
@ -307,6 +329,13 @@ impl TripSpawner {
|
|||||||
let walk_to = SidewalkSpot::bus_stop(stop1, map);
|
let walk_to = SidewalkSpot::bus_stop(stop1, map);
|
||||||
let trip = trips.new_trip(
|
let trip = trips.new_trip(
|
||||||
start_time,
|
start_time,
|
||||||
|
match start.connection {
|
||||||
|
SidewalkPOI::Building(b) => Some(TripStart::Bldg(b)),
|
||||||
|
SidewalkPOI::SuddenlyAppear | SidewalkPOI::Border(_) => {
|
||||||
|
Some(TripStart::Appearing(start.sidewalk_pos))
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
vec![
|
vec![
|
||||||
TripLeg::Walk(ped_id.unwrap(), ped_speed, walk_to.clone()),
|
TripLeg::Walk(ped_id.unwrap(), ped_speed, walk_to.clone()),
|
||||||
TripLeg::RideBus(ped_id.unwrap(), route, stop2),
|
TripLeg::RideBus(ped_id.unwrap(), route, stop2),
|
||||||
|
@ -2,8 +2,8 @@ use crate::{
|
|||||||
AgentID, CarID, Command, CreateCar, DrawCarInput, DrawPedestrianInput, DrivingGoal,
|
AgentID, CarID, Command, CreateCar, DrawCarInput, DrawPedestrianInput, DrivingGoal,
|
||||||
DrivingSimState, Event, FinishedTrips, GetDrawAgents, IntersectionSimState, ParkedCar,
|
DrivingSimState, Event, FinishedTrips, GetDrawAgents, IntersectionSimState, ParkedCar,
|
||||||
ParkingSimState, ParkingSpot, PedestrianID, Router, Scheduler, TransitSimState, TripID,
|
ParkingSimState, ParkingSpot, PedestrianID, Router, Scheduler, TransitSimState, TripID,
|
||||||
TripLeg, TripManager, TripPositions, TripSpawner, TripSpec, VehicleSpec, VehicleType,
|
TripLeg, TripManager, TripPositions, TripSpawner, TripSpec, TripStatus, VehicleSpec,
|
||||||
WalkingSimState, BUS_LENGTH,
|
VehicleType, WalkingSimState, BUS_LENGTH,
|
||||||
};
|
};
|
||||||
use abstutil::{elapsed_seconds, Timer};
|
use abstutil::{elapsed_seconds, Timer};
|
||||||
use derivative::Derivative;
|
use derivative::Derivative;
|
||||||
@ -198,9 +198,9 @@ impl Sim {
|
|||||||
// Bypass some layers of abstraction that don't make sense for buses.
|
// Bypass some layers of abstraction that don't make sense for buses.
|
||||||
|
|
||||||
// TODO Aww, we create an orphan trip if the bus can't spawn.
|
// TODO Aww, we create an orphan trip if the bus can't spawn.
|
||||||
let trip = self
|
let trip =
|
||||||
.trips
|
self.trips
|
||||||
.new_trip(self.time, vec![TripLeg::ServeBusRoute(id, route.id)]);
|
.new_trip(self.time, None, vec![TripLeg::ServeBusRoute(id, route.id)]);
|
||||||
if self.driving.start_car_on_lane(
|
if self.driving.start_car_on_lane(
|
||||||
self.time,
|
self.time,
|
||||||
CreateCar {
|
CreateCar {
|
||||||
@ -698,6 +698,10 @@ impl Sim {
|
|||||||
self.trips.trip_to_agent(id)
|
self.trips.trip_to_agent(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trip_status(&self, id: TripID) -> Option<TripStatus> {
|
||||||
|
self.trips.trip_status(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lookup_car_id(&self, idx: usize) -> Option<CarID> {
|
pub fn lookup_car_id(&self, idx: usize) -> Option<CarID> {
|
||||||
for vt in &[VehicleType::Car, VehicleType::Bike, VehicleType::Bus] {
|
for vt in &[VehicleType::Car, VehicleType::Bike, VehicleType::Bus] {
|
||||||
let id = CarID(idx, *vt);
|
let id = CarID(idx, *vt);
|
||||||
|
@ -5,7 +5,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use abstutil::{deserialize_btreemap, serialize_btreemap};
|
use abstutil::{deserialize_btreemap, serialize_btreemap};
|
||||||
use geom::{Duration, Speed};
|
use geom::{Duration, Speed};
|
||||||
use map_model::{BuildingID, BusRouteID, BusStopID, IntersectionID, Map, PathRequest};
|
use map_model::{BuildingID, BusRouteID, BusStopID, IntersectionID, Map, PathRequest, Position};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
|
||||||
@ -35,7 +35,12 @@ impl TripManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_trip(&mut self, spawned_at: Duration, legs: Vec<TripLeg>) -> TripID {
|
pub fn new_trip(
|
||||||
|
&mut self,
|
||||||
|
spawned_at: Duration,
|
||||||
|
start: Option<TripStart>,
|
||||||
|
legs: Vec<TripLeg>,
|
||||||
|
) -> TripID {
|
||||||
assert!(!legs.is_empty());
|
assert!(!legs.is_empty());
|
||||||
// TODO Make sure the legs constitute a valid state machine.
|
// TODO Make sure the legs constitute a valid state machine.
|
||||||
|
|
||||||
@ -63,6 +68,7 @@ impl TripManager {
|
|||||||
finished_at: None,
|
finished_at: None,
|
||||||
mode,
|
mode,
|
||||||
legs: VecDeque::from(legs),
|
legs: VecDeque::from(legs),
|
||||||
|
start,
|
||||||
};
|
};
|
||||||
if !trip.is_bus_trip() {
|
if !trip.is_bus_trip() {
|
||||||
self.unfinished_trips += 1;
|
self.unfinished_trips += 1;
|
||||||
@ -419,6 +425,23 @@ impl TripManager {
|
|||||||
pub fn collect_events(&mut self) -> Vec<Event> {
|
pub fn collect_events(&mut self) -> Vec<Event> {
|
||||||
self.events.drain(..).collect()
|
self.events.drain(..).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trip_status(&self, id: TripID) -> Option<TripStatus> {
|
||||||
|
let trip = &self.trips[id.0];
|
||||||
|
let start = trip.start.clone()?;
|
||||||
|
let end = match trip.legs.back() {
|
||||||
|
Some(TripLeg::Walk(_, _, ref spot)) => match spot.connection {
|
||||||
|
SidewalkPOI::Building(b) => TripEnd::Bldg(b),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
Some(TripLeg::Drive(_, ref goal)) => match goal {
|
||||||
|
DrivingGoal::ParkNear(b) => TripEnd::Bldg(*b),
|
||||||
|
DrivingGoal::Border(i, _) => TripEnd::Border(*i),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Some(TripStatus { start, end })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||||
@ -428,6 +451,7 @@ struct Trip {
|
|||||||
finished_at: Option<Duration>,
|
finished_at: Option<Duration>,
|
||||||
legs: VecDeque<TripLeg>,
|
legs: VecDeque<TripLeg>,
|
||||||
mode: TripMode,
|
mode: TripMode,
|
||||||
|
start: Option<TripStart>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Trip {
|
impl Trip {
|
||||||
@ -516,3 +540,21 @@ pub enum TripMode {
|
|||||||
Transit,
|
Transit,
|
||||||
Drive,
|
Drive,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Argh no, not more of these variants!
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
pub enum TripStart {
|
||||||
|
Bldg(BuildingID),
|
||||||
|
Appearing(Position),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TripEnd {
|
||||||
|
Bldg(BuildingID),
|
||||||
|
Border(IntersectionID),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TripStatus {
|
||||||
|
pub start: TripStart,
|
||||||
|
pub end: TripEnd,
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user