diff --git a/game/src/common/mod.rs b/game/src/common/mod.rs index cd92781f48..4e83250d5b 100644 --- a/game/src/common/mod.rs +++ b/game/src/common/mod.rs @@ -16,7 +16,7 @@ pub use self::colors::{ ColorLegend, ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder, }; pub use self::minimap::Minimap; -pub use self::plot::{Plot, Series}; +pub use self::plot::{Histogram, Plot, Series}; pub use self::route_explorer::RouteExplorer; pub use self::trip_explorer::TripExplorer; pub use self::warp::Warping; diff --git a/game/src/common/plot.rs b/game/src/common/plot.rs index f8bc711957..1f2391928f 100644 --- a/game/src/common/plot.rs +++ b/game/src/common/plot.rs @@ -4,7 +4,9 @@ use ezgui::{ }; use geom::{Bounds, Circle, Distance, Duration, FindClosest, PolyLine, Polygon, Pt2D, Time}; +// The X is always time pub struct Plot { + // TODO Could DrawBoth instead of MultiText here draw: Drawable, legend: ColorLegend, labels: MultiText, @@ -200,3 +202,101 @@ pub struct Series { // X-axis is time. Assume this is sorted by X. pub pts: Vec<(Time, T)>, } + +// The X axis is Durations, with positive meaning "faster" (considered good) and negative "slower" +pub struct Histogram { + draw: Drawable, + labels: MultiText, + rect: ScreenRectangle, +} + +impl Histogram { + pub fn new(title: &str, unsorted_dts: Vec, ctx: &EventCtx) -> Histogram { + let mut batch = GeomBatch::new(); + let mut labels = MultiText::new(); + + let x1 = 0.5 * ctx.canvas.window_width; + let x2 = 0.9 * ctx.canvas.window_width; + let y1 = 0.4 * ctx.canvas.window_height; + let y2 = 0.8 * ctx.canvas.window_height; + batch.push( + Color::grey(0.8), + Polygon::rectangle_topleft( + Pt2D::new(x1, y1), + Distance::meters(x2 - x1), + Distance::meters(y2 - y1), + ), + ); + + if unsorted_dts.len() < 10 { + // TODO Add some warning label or something + } else { + // TODO Generic "bucket into 10 groups, give (min, max, count)" + let min_x = *unsorted_dts.iter().min().unwrap(); + let max_x = *unsorted_dts.iter().max().unwrap(); + + let num_buckets = 10; + let bucket_size = (max_x - min_x) / (num_buckets as f64); + // lower, upper, count + let mut bars: Vec<(Duration, Duration, usize)> = (0..num_buckets) + .map(|idx| { + let i = idx as f64; + (min_x + bucket_size * i, min_x + bucket_size * (i + 1.0), 0) + }) + .collect(); + for dt in unsorted_dts { + // TODO Could sort them and do this more efficiently. + if dt == max_x { + // Most bars represent [low, high) except the last + bars[num_buckets - 1].2 += 1; + } else { + let bin = ((dt - min_x) / bucket_size).floor() as usize; + bars[bin].2 += 1; + } + } + + let min_y = 0; + let max_y = bars.iter().map(|(_, _, cnt)| *cnt).max().unwrap(); + for (idx, (min, max, cnt)) in bars.into_iter().enumerate() { + // TODO Or maybe the average? + let color = if min < Duration::ZERO { + Color::RED + } else { + Color::GREEN + }; + let percent_x_left = (idx as f64) / (num_buckets as f64); + let percent_x_right = ((idx + 1) as f64) / (num_buckets as f64); + if let Some(rect) = Polygon::rectangle_two_corners( + // Top-left + Pt2D::new( + x1 + (x2 - x1) * percent_x_left, + y2 - (y2 - y1) * ((cnt as f64) / ((max_y - min_y) as f64)), + ), + // Bottom-right + Pt2D::new(x1 + (x2 - x1) * percent_x_right, y2), + ) { + batch.push(color, rect); + } + labels.add( + Text::from(Line(min.to_string())), + ScreenPt::new(x1 + (x2 - x1) * percent_x_left, y2), + ); + } + } + + Histogram { + draw: batch.upload(ctx), + labels, + rect: ScreenRectangle { x1, y1, x2, y2 }, + } + } + + pub fn draw(&self, g: &mut GfxCtx) { + g.canvas.mark_covered_area(self.rect.clone()); + + g.fork_screenspace(); + g.redraw(&self.draw); + g.unfork(); + self.labels.draw(g); + } +} diff --git a/game/src/sandbox/gameplay/fix_traffic_signals.rs b/game/src/sandbox/gameplay/fix_traffic_signals.rs index 7e2dbcfd6b..1594ca2bc6 100644 --- a/game/src/sandbox/gameplay/fix_traffic_signals.rs +++ b/game/src/sandbox/gameplay/fix_traffic_signals.rs @@ -19,6 +19,7 @@ impl FixTrafficSignals { "Fix traffic signals", vec![ (hotkey(Key::F), "find slowest traffic signals"), + (hotkey(Key::D), "show finished trip distribution"), (hotkey(Key::H), "help"), (hotkey(Key::S), "final score"), ], @@ -57,6 +58,20 @@ impl GameplayState for FixTrafficSignals { ) { *overlays = Overlays::intersection_delay(ctx, ui); } + if manage_overlays( + menu, + ctx, + "show finished trip distribution", + "hide finished trip distribution", + overlays, + match overlays { + Overlays::FinishedTripsHistogram(_, _) => true, + _ => false, + }, + self.time != ui.primary.sim.time(), + ) { + *overlays = Overlays::finished_trips_histogram(ctx, ui, prebaked); + } if self.time != ui.primary.sim.time() { self.time = ui.primary.sim.time(); diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index 7366efa756..37eab02109 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -125,7 +125,10 @@ impl State for SandboxMode { if let Some(t) = self.common.event(ctx, ui) { return t; } - if let Some(t) = self.overlay.event(ctx, ui, &mut self.info_tools) { + if let Some(t) = self + .overlay + .event(ctx, ui, &mut self.info_tools, &self.gameplay.prebaked) + { return t; } self.minimap.event(ui, ctx); diff --git a/game/src/sandbox/overlays.rs b/game/src/sandbox/overlays.rs index e064879d36..7a2b8fe316 100644 --- a/game/src/sandbox/overlays.rs +++ b/game/src/sandbox/overlays.rs @@ -1,5 +1,5 @@ use crate::common::{ - ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series, + Histogram, ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series, }; use crate::game::{Transition, WizardState}; use crate::helpers::{rotating_color, ID}; @@ -11,7 +11,7 @@ use abstutil::{prettyprint_usize, Counter}; use ezgui::{Choice, Color, EventCtx, GfxCtx, Key, Line, MenuUnderButton, Text}; use geom::{Duration, Statistic, Time}; use map_model::{IntersectionID, LaneID, PathConstraints, PathStep, RoadID}; -use sim::{ParkingSpot, TripMode}; +use sim::{Analytics, ParkingSpot, TripMode}; use std::collections::{BTreeMap, HashSet}; pub enum Overlays { @@ -20,6 +20,7 @@ pub enum Overlays { IntersectionDelay(Time, ObjectColorer), CumulativeThroughput(Time, ObjectColorer), FinishedTrips(Time, Plot), + FinishedTripsHistogram(Time, Histogram), Chokepoints(Time, ObjectColorer), BikeNetwork(RoadColorer), BikePathCosts(RoadColorer), @@ -53,6 +54,7 @@ impl Overlays { ctx: &mut EventCtx, ui: &UI, menu: &mut MenuUnderButton, + baseline: &Analytics, ) -> Option { if menu.action("change analytics overlay") { return Some(Transition::Push(WizardState::new(Box::new( @@ -66,6 +68,8 @@ impl Overlays { Choice::new("intersection delay", ()).key(Key::I), Choice::new("cumulative throughput", ()).key(Key::T), Choice::new("finished trips", ()).key(Key::F), + // TODO baseline borrow doesn't live long enough + //Choice::new("finished trips histogram", ()).key(Key::H), Choice::new("chokepoints", ()).key(Key::C), Choice::new("bike network", ()).key(Key::B), Choice::new("bike path costs", ()).key(Key::X), @@ -116,6 +120,9 @@ impl Overlays { Overlays::FinishedTrips(t, _) if now != *t => { *self = Overlays::finished_trips(ctx, ui); } + Overlays::FinishedTripsHistogram(t, _) if now != *t => { + *self = Overlays::finished_trips_histogram(ctx, ui, baseline); + } Overlays::Chokepoints(t, _) if now != *t => { *self = Overlays::chokepoints(ctx, ui); } @@ -153,6 +160,16 @@ impl Overlays { plot.draw(g); true } + Overlays::FinishedTripsHistogram(_, ref hgram) => { + ui.draw( + g, + DrawOptions::new(), + &ui.primary.sim, + &ShowEverything::new(), + ); + hgram.draw(g); + true + } Overlays::BusDelaysOverTime(ref plot) | Overlays::IntersectionDelayOverTime { ref plot, .. } => { ui.draw( @@ -560,6 +577,21 @@ impl Overlays { Overlays::FinishedTrips(ui.primary.sim.time(), plot) } + pub fn finished_trips_histogram(ctx: &EventCtx, ui: &UI, baseline: &Analytics) -> Overlays { + let now = ui.primary.sim.time(); + Overlays::FinishedTripsHistogram( + now, + Histogram::new( + "Finished trip time deltas", + ui.primary + .sim + .get_analytics() + .finished_trip_deltas(now, baseline), + ctx, + ), + ) + } + fn bike_path_costs(ctx: &EventCtx, ui: &UI) -> Overlays { let mut cost_per_lane: BTreeMap = BTreeMap::new(); for l in ui.primary.map.all_lanes() { diff --git a/geom/src/duration.rs b/geom/src/duration.rs index 6065e8d89b..f90d351bbf 100644 --- a/geom/src/duration.rs +++ b/geom/src/duration.rs @@ -221,6 +221,17 @@ impl ops::Div for Duration { } } +impl ops::Div for Duration { + type Output = Duration; + + fn div(self, other: f64) -> Duration { + if other == 0.0 { + panic!("Can't divide {} / {}", self, other); + } + Duration::seconds(self.0 / other) + } +} + impl ops::Rem for Duration { type Output = Duration; diff --git a/sim/src/analytics.rs b/sim/src/analytics.rs index cc7f7da449..3a518e30ad 100644 --- a/sim/src/analytics.rs +++ b/sim/src/analytics.rs @@ -175,6 +175,37 @@ impl Analytics { (all, num_aborted, per_mode) } + // Returns unsorted list of deltas, one for each trip finished in both worlds. Positive dt + // means faster. + pub fn finished_trip_deltas(&self, now: Time, baseline: &Analytics) -> Vec { + let a: BTreeMap = self + .finished_trips + .iter() + .filter_map(|(t, id, mode, dt)| { + if *t <= now && mode.is_some() { + Some((*id, *dt)) + } else { + None + } + }) + .collect(); + let b: BTreeMap = baseline + .finished_trips + .iter() + .filter_map(|(t, id, mode, dt)| { + if *t <= now && mode.is_some() { + Some((*id, *dt)) + } else { + None + } + }) + .collect(); + + a.into_iter() + .filter_map(|(id, dt1)| b.get(&id).map(|dt2| *dt2 - dt1)) + .collect() + } + pub fn bus_arrivals(&self, now: Time, r: BusRouteID) -> BTreeMap { let mut per_bus: BTreeMap> = BTreeMap::new(); for (t, car, route, stop) in &self.bus_arrivals {