diff --git a/game/src/common/trip_explorer.rs b/game/src/common/trip_explorer.rs index 836c640ceb..e4486c1e74 100644 --- a/game/src/common/trip_explorer.rs +++ b/game/src/common/trip_explorer.rs @@ -24,17 +24,8 @@ impl TripExplorer { // TODO Hack because ColorLegend only takes &str let mut rows = Vec::new(); for (idx, p) in phases.iter().enumerate() { - let label = if let Some(t2) = p.end_time { - format!("{} .. {} ({})", p.start_time, t2, t2 - p.start_time) - } else { - format!( - "{} .. ongoing ({} so far)", - p.start_time, - ui.primary.sim.time() - p.start_time - ) - }; rows.push(( - format!("{}: {}", label, p.description), + p.describe(ui.primary.sim.time()), rotating_color_map(idx + 1), )); } diff --git a/game/src/debug/objects.rs b/game/src/debug/objects.rs index 4a669db41b..072e6fa9e3 100644 --- a/game/src/debug/objects.rs +++ b/game/src/debug/objects.rs @@ -93,8 +93,8 @@ fn dump_debug(id: ID, map: &Map, sim: &Sim, draw_map: &DrawMap) { sim.debug_car(id); if let Some(t) = sim.agent_to_trip(AgentID::Car(id)) { println!("Trip log for {}", t); - for ev in sim.get_analytics().get_trip_log(t) { - println!("- {}", ev); + for p in sim.get_analytics().get_trip_phases(t, map) { + println!("- {}", p.describe(sim.time())); } } } @@ -102,8 +102,8 @@ fn dump_debug(id: ID, map: &Map, sim: &Sim, draw_map: &DrawMap) { sim.debug_ped(id); if let Some(t) = sim.agent_to_trip(AgentID::Pedestrian(id)) { println!("Trip log for {}", t); - for ev in sim.get_analytics().get_trip_log(t) { - println!("- {}", ev); + for p in sim.get_analytics().get_trip_phases(t, map) { + println!("- {}", p.describe(sim.time())); } } } diff --git a/game/src/sandbox/score.rs b/game/src/sandbox/score.rs index 61783e4a02..0a303d1648 100644 --- a/game/src/sandbox/score.rs +++ b/game/src/sandbox/score.rs @@ -1,5 +1,5 @@ use crate::common::TripExplorer; -use crate::game::{State, Transition, WizardState}; +use crate::game::{msg, State, Transition, WizardState}; use crate::sandbox::gameplay::{cmp_count_fewer, cmp_count_more, cmp_duration_shorter}; use crate::ui::UI; use abstutil::prettyprint_usize; @@ -23,6 +23,7 @@ impl Scoreboard { vec![ (hotkey(Key::Escape), "quit"), (hotkey(Key::B), "browse trips"), + (hotkey(Key::P), "examine parking overhead"), ], ctx, ); @@ -89,7 +90,7 @@ impl Scoreboard { } impl State for Scoreboard { - fn event(&mut self, ctx: &mut EventCtx, _: &mut UI) -> Transition { + fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { self.menu.event(ctx); if self.menu.action("quit") { return Transition::Pop; @@ -97,6 +98,12 @@ impl State for Scoreboard { if self.menu.action("browse trips") { return Transition::Push(WizardState::new(Box::new(browse_trips))); } + if self.menu.action("examine parking overhead") { + return Transition::Push(msg( + "Parking overhead", + ui.primary.sim.get_analytics().analyze_parking_phases(), + )); + } Transition::Keep } diff --git a/sim/src/analytics.rs b/sim/src/analytics.rs index 35492c16d2..5ed6a4a792 100644 --- a/sim/src/analytics.rs +++ b/sim/src/analytics.rs @@ -283,23 +283,6 @@ impl Analytics { per_mode } - pub fn get_trip_log(&self, trip: TripID) -> Vec { - self.trip_log - .iter() - .filter_map(|(t, id, maybe_req, md)| { - if *id == trip { - if let Some(req) = maybe_req { - Some(format!("At {}: {} via {}", t, md, req)) - } else { - Some(format!("At {}: {}", t, md)) - } - } else { - None - } - }) - .collect() - } - pub fn get_trip_phases(&self, trip: TripID, map: &Map) -> Vec { let mut phases: Vec = Vec::new(); for (t, id, maybe_req, md) in &self.trip_log { @@ -324,6 +307,61 @@ impl Analytics { } phases } + + fn get_all_trip_phases(&self) -> BTreeMap> { + let mut trips = BTreeMap::new(); + for (t, id, _, md) in &self.trip_log { + let phases: &mut Vec = trips.entry(*id).or_insert_with(Vec::new); + if let Some(ref mut last) = phases.last_mut() { + last.end_time = Some(*t); + } + if md == "trip finished" || md == "trip aborted for some reason" { + // TODO Remove aborted trips? + continue; + } + phases.push(TripPhase { + start_time: *t, + end_time: None, + // Don't compute any paths + path: None, + description: md.clone(), + }) + } + trips + } + + pub fn analyze_parking_phases(&self) -> Vec { + // Of all completed trips involving parking, what percentage of total time was spent as + // "overhead" -- not the main driving part of the trip? + // TODO This is misleading for border trips -- the driving lasts longer. + for (_, phases) in self.get_all_trip_phases() { + if phases.last().as_ref().unwrap().end_time.is_none() { + continue; + } + let mut driving_time = Duration::ZERO; + let mut overhead = Duration::ZERO; + for p in phases { + let dt = p.end_time.unwrap() - p.start_time; + // TODO New enum instead of strings, if there'll be more analyses like this + if p.description.starts_with("CarID(") { + driving_time += dt; + } else if p.description == "parking somewhere else" + || p.description == "parking on the current lane" + { + overhead += dt; + } else if p.description.starts_with("PedestrianID(") { + overhead += dt; + } else { + // Waiting for a bus. Irrelevant. + } + } + // Only interested in trips with both + if driving_time == Duration::ZERO || overhead == Duration::ZERO { + continue; + } + } + vec![format!("TODO: need a generic histogram")] + } } pub struct TripPhase { @@ -333,3 +371,24 @@ pub struct TripPhase { pub path: Option<(Distance, Path)>, pub description: String, } + +impl TripPhase { + pub fn describe(&self, now: Time) -> String { + if let Some(t2) = self.end_time { + format!( + "{} .. {} ({}): {}", + self.start_time, + t2, + t2 - self.start_time, + self.description + ) + } else { + format!( + "{} .. ongoing ({} so far): {}", + self.start_time, + now - self.start_time, + self.description + ) + } + } +}