From 21724aa826bca6ee56e6cdbbac8b6c81c14682c7 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Tue, 5 Nov 2019 13:13:09 -0800 Subject: [PATCH] moving generic plotting stuff away from trip stats --- game/src/common/mod.rs | 2 + game/src/common/plot.rs | 158 +++++++++++++++++++++ game/src/sandbox/gameplay.rs | 28 +++- game/src/sandbox/mod.rs | 1 - game/src/sandbox/overlays.rs | 76 +++++++++- game/src/sandbox/trip_stats.rs | 250 --------------------------------- 6 files changed, 255 insertions(+), 260 deletions(-) create mode 100644 game/src/common/plot.rs delete mode 100644 game/src/sandbox/trip_stats.rs diff --git a/game/src/common/mod.rs b/game/src/common/mod.rs index ff731bcf15..017ad3b698 100644 --- a/game/src/common/mod.rs +++ b/game/src/common/mod.rs @@ -3,6 +3,7 @@ mod associated; mod colors; mod info; mod navigate; +mod plot; mod route_explorer; mod route_viewer; mod shortcuts; @@ -16,6 +17,7 @@ pub use self::agent::AgentTools; pub use self::colors::{ ColorLegend, ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder, }; +pub use self::plot::{Plot, Series}; pub use self::route_explorer::RouteExplorer; pub use self::speed::SpeedControls; pub use self::time::time_controls; diff --git a/game/src/common/plot.rs b/game/src/common/plot.rs new file mode 100644 index 0000000000..d16fbb1673 --- /dev/null +++ b/game/src/common/plot.rs @@ -0,0 +1,158 @@ +use crate::common::ColorLegend; +use ezgui::{ + Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenPt, ScreenRectangle, Text, +}; +use geom::{Distance, Duration, PolyLine, Polygon, Pt2D}; + +pub struct Plot { + draw: Drawable, + legend: ColorLegend, + labels: MultiText, + rect: ScreenRectangle, +} + +impl Plot { + pub fn new>( + title: &str, + series: Vec>, + ctx: &EventCtx, + ) -> Option { + let mut batch = GeomBatch::new(); + let mut labels = MultiText::new(); + + let x1 = 0.1 * ctx.canvas.window_width; + let x2 = 0.7 * ctx.canvas.window_width; + let y1 = 0.2 * 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), + ), + ); + + // Assume min_x is Duration::ZERO and min_y is 0 + let max_x = series + .iter() + .map(|s| s.pts.iter().map(|(t, _)| *t).max().unwrap()) + .max() + .unwrap(); + let max_y = series + .iter() + .map(|s| s.pts.iter().map(|(_, cnt)| *cnt).max().unwrap()) + .max() + .unwrap(); + if max_x == Duration::ZERO { + return None; + } + + let num_x_labels = 5; + for i in 0..num_x_labels { + let percent_x = (i as f64) / ((num_x_labels - 1) as f64); + let t = percent_x * max_x; + labels.add( + Text::from(Line(t.to_string())), + ScreenPt::new(x1 + percent_x * (x2 - x1), y2), + ); + } + + let num_y_labels = 5; + for i in 0..num_y_labels { + let percent_y = (i as f64) / ((num_y_labels - 1) as f64); + labels.add( + Text::from(Line(max_y.from_percent(percent_y).prettyprint())), + ScreenPt::new(x1, y2 - percent_y * (y2 - y1)), + ); + } + + let legend = ColorLegend::new( + Text::prompt(title), + series.iter().map(|s| (s.label.as_str(), s.color)).collect(), + ); + + for s in series { + let mut pts = Vec::new(); + if max_y == T::zero() { + pts.push(Pt2D::new(x1, y2)); + pts.push(Pt2D::new(x2, y2)); + } else { + for (t, y) in s.pts { + let percent_x = t / max_x; + let percent_y = y.to_percent(max_y); + pts.push(Pt2D::new( + x1 + (x2 - x1) * percent_x, + // Y inversion! :D + y2 - (y2 - y1) * percent_y, + )); + } + } + batch.push( + s.color, + PolyLine::new(pts).make_polygons(Distance::meters(5.0)), + ); + } + + Some(Plot { + draw: ctx.prerender.upload(batch), + labels, + legend, + rect: ScreenRectangle { x1, y1, x2, y2 }, + }) + } + pub fn draw(&self, g: &mut GfxCtx) { + self.legend.draw(g); + + g.fork_screenspace(); + g.redraw(&self.draw); + g.unfork(); + self.labels.draw(g); + + g.canvas.mark_covered_area(self.rect.clone()); + } +} + +pub trait Yvalue { + // percent is [0.0, 1.0] + fn from_percent(self, percent: f64) -> T; + fn to_percent(self, max: T) -> f64; + fn prettyprint(self) -> String; + fn zero() -> T; +} + +impl Yvalue for usize { + fn from_percent(self, percent: f64) -> usize { + ((self as f64) * percent) as usize + } + fn to_percent(self, max: usize) -> f64 { + (self as f64) / (max as f64) + } + fn prettyprint(self) -> String { + abstutil::prettyprint_usize(self) + } + fn zero() -> usize { + 0 + } +} +impl Yvalue for Duration { + fn from_percent(self, percent: f64) -> Duration { + percent * self + } + fn to_percent(self, max: Duration) -> f64 { + self / max + } + fn prettyprint(self) -> String { + self.minimal_tostring() + } + fn zero() -> Duration { + Duration::ZERO + } +} + +pub struct Series { + pub label: String, + pub color: Color, + // X-axis is time. Assume this is sorted by X. + pub pts: Vec<(Duration, T)>, +} diff --git a/game/src/sandbox/gameplay.rs b/game/src/sandbox/gameplay.rs index d041d5aa6b..b8fef53d75 100644 --- a/game/src/sandbox/gameplay.rs +++ b/game/src/sandbox/gameplay.rs @@ -1,7 +1,8 @@ +use crate::common::{Plot, Series}; use crate::game::{msg, Transition, WizardState}; use crate::render::AgentColorScheme; use crate::sandbox::overlays::Overlays; -use crate::sandbox::{bus_explorer, spawner, trip_stats, SandboxMode}; +use crate::sandbox::{bus_explorer, spawner, SandboxMode}; use crate::ui::UI; use abstutil::{prettyprint_usize, Timer}; use ezgui::{hotkey, Choice, Color, EventCtx, GfxCtx, Key, Line, ModalMenu, Text, Wizard}; @@ -275,7 +276,7 @@ impl GameplayState { }, *time != ui.primary.sim.time(), ) { - if let Some(s) = trip_stats::ShowTripStats::bus_delays(route, ui, ctx) { + if let Some(s) = bus_delays(route, ui, ctx) { *overlays = Overlays::BusDelaysOverTime(s); } else { println!("No route delay info yet"); @@ -424,6 +425,29 @@ fn bus_route_panel(id: BusRouteID, ui: &UI, stat: Statistic, prebaked: &Analytic txt } +fn bus_delays(route: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Option { + let delays_per_stop = ui + .primary + .sim + .get_analytics() + .bus_arrivals_over_time(ui.primary.sim.time(), route); + if delays_per_stop.is_empty() { + return None; + } + + let mut series = Vec::new(); + for (stop, delays) in delays_per_stop { + series.push(Series { + // TODO idx + label: stop.to_string(), + color: Color::RED, + pts: delays, + }); + break; + } + Plot::new(&format!("delays for {}", route), series, ctx) +} + fn gridlock_panel(ui: &UI, _prebaked: &Analytics) -> Text { let now = GridlockDelays::from(&ui.primary.sim); // TODO Derive this from something in Analytics diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index 7c9cba28c6..7fa4b61f8b 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -3,7 +3,6 @@ mod gameplay; mod overlays; mod score; mod spawner; -mod trip_stats; use crate::common::{time_controls, AgentTools, CommonState, SpeedControls}; use crate::debug::DebugMode; diff --git a/game/src/sandbox/overlays.rs b/game/src/sandbox/overlays.rs index e41e20810a..3932beaf4c 100644 --- a/game/src/sandbox/overlays.rs +++ b/game/src/sandbox/overlays.rs @@ -1,5 +1,6 @@ -use super::trip_stats::ShowTripStats; -use crate::common::{ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder}; +use crate::common::{ + ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series, +}; use crate::game::{Transition, WizardState}; use crate::helpers::ID; use crate::render::DrawOptions; @@ -10,20 +11,20 @@ use abstutil::{prettyprint_usize, Counter}; use ezgui::{Choice, Color, EventCtx, GfxCtx, Line, MenuUnderButton, Text}; use geom::Duration; use map_model::PathStep; -use sim::ParkingSpot; -use std::collections::HashSet; +use sim::{ParkingSpot, TripMode}; +use std::collections::{BTreeMap, HashSet}; pub enum Overlays { Inactive, ParkingAvailability(Duration, RoadColorer), IntersectionDelay(Duration, ObjectColorer), Throughput(Duration, ObjectColorer), - FinishedTrips(Duration, ShowTripStats), + FinishedTrips(Duration, Plot), Chokepoints(Duration, ObjectColorer), BikeNetwork(RoadColorer), // Only set by certain gameplay modes BusRoute(ShowBusRoute), - BusDelaysOverTime(ShowTripStats), + BusDelaysOverTime(Plot), } impl Overlays { @@ -121,7 +122,7 @@ impl Overlays { } "cumulative throughput" => Overlays::Throughput(time, calculate_thruput(ctx, ui)), "finished trips" => { - if let Some(s) = ShowTripStats::new(ui, ctx) { + if let Some(s) = trip_stats(ui, ctx) { Overlays::FinishedTrips(time, s) } else { println!("No data on finished trips yet"); @@ -341,3 +342,64 @@ fn calculate_bike_network(ctx: &mut EventCtx, ui: &UI) -> RoadColorer { } colorer.build(ctx, &ui.primary.map) } + +fn trip_stats(ui: &UI, ctx: &mut EventCtx) -> Option { + if ui.primary.sim.get_analytics().finished_trips.is_empty() { + return None; + } + + let lines: Vec<(&str, Color, Option)> = vec![ + ( + "walking", + ui.cs.get("unzoomed pedestrian"), + Some(TripMode::Walk), + ), + ("biking", ui.cs.get("unzoomed bike"), Some(TripMode::Bike)), + ( + "transit", + ui.cs.get("unzoomed bus"), + Some(TripMode::Transit), + ), + ("driving", ui.cs.get("unzoomed car"), Some(TripMode::Drive)), + ("aborted", Color::PURPLE.alpha(0.5), None), + ]; + + // What times do we use for interpolation? + let num_x_pts = 100; + let mut times = Vec::new(); + for i in 0..num_x_pts { + let percent_x = (i as f64) / ((num_x_pts - 1) as f64); + let t = ui.primary.sim.time() * percent_x; + times.push(t); + } + + // Gather the data + let mut counts = Counter::new(); + let mut pts_per_mode: BTreeMap, Vec<(Duration, usize)>> = + lines.iter().map(|(_, _, m)| (*m, Vec::new())).collect(); + for (t, m, _) in &ui.primary.sim.get_analytics().finished_trips { + counts.inc(*m); + if *t > times[0] { + times.remove(0); + for (_, _, mode) in &lines { + pts_per_mode + .get_mut(mode) + .unwrap() + .push((*t, counts.get(*mode))); + } + } + } + + Plot::new( + "finished trips", + lines + .into_iter() + .map(|(name, color, m)| Series { + label: name.to_string(), + color, + pts: pts_per_mode.remove(&m).unwrap(), + }) + .collect(), + ctx, + ) +} diff --git a/game/src/sandbox/trip_stats.rs b/game/src/sandbox/trip_stats.rs deleted file mode 100644 index 338c8c02ad..0000000000 --- a/game/src/sandbox/trip_stats.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::common::ColorLegend; -use crate::ui::UI; -use abstutil::Counter; -use ezgui::{ - Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenPt, ScreenRectangle, Text, -}; -use geom::{Distance, Duration, PolyLine, Polygon, Pt2D}; -use map_model::BusRouteID; -use sim::TripMode; -use std::collections::BTreeMap; - -// TODO Show active trips too -pub struct ShowTripStats { - draw: Drawable, - legend: ColorLegend, - labels: MultiText, - rect: ScreenRectangle, -} - -impl ShowTripStats { - pub fn new(ui: &UI, ctx: &mut EventCtx) -> Option { - if ui.primary.sim.get_analytics().finished_trips.is_empty() { - return None; - } - - let lines: Vec<(&str, Color, Option)> = vec![ - ( - "walking", - ui.cs.get("unzoomed pedestrian"), - Some(TripMode::Walk), - ), - ("biking", ui.cs.get("unzoomed bike"), Some(TripMode::Bike)), - ( - "transit", - ui.cs.get("unzoomed bus"), - Some(TripMode::Transit), - ), - ("driving", ui.cs.get("unzoomed car"), Some(TripMode::Drive)), - ("aborted", Color::PURPLE.alpha(0.5), None), - ]; - - // What times do we use for interpolation? - let num_x_pts = 100; - let mut times = Vec::new(); - for i in 0..num_x_pts { - let percent_x = (i as f64) / ((num_x_pts - 1) as f64); - let t = ui.primary.sim.time() * percent_x; - times.push(t); - } - - // Gather the data - let mut counts = Counter::new(); - let mut pts_per_mode: BTreeMap, Vec<(Duration, usize)>> = - lines.iter().map(|(_, _, m)| (*m, Vec::new())).collect(); - for (t, m, _) in &ui.primary.sim.get_analytics().finished_trips { - counts.inc(*m); - if *t > times[0] { - times.remove(0); - for (_, _, mode) in &lines { - pts_per_mode - .get_mut(mode) - .unwrap() - .push((*t, counts.get(*mode))); - } - } - } - - plot( - "finished trips", - lines - .into_iter() - .map(|(name, color, m)| Series { - label: name.to_string(), - color, - pts: pts_per_mode.remove(&m).unwrap(), - }) - .collect(), - ctx, - ) - } - - // TODO lumped in here temporarily - pub fn bus_delays(route: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Option { - let delays_per_stop = ui - .primary - .sim - .get_analytics() - .bus_arrivals_over_time(ui.primary.sim.time(), route); - if delays_per_stop.is_empty() { - return None; - } - - let mut series = Vec::new(); - for (stop, delays) in delays_per_stop { - series.push(Series { - // TODO idx - label: stop.to_string(), - color: Color::RED, - pts: delays, - }); - break; - } - plot(&format!("delays for {}", route), series, ctx) - } - - pub fn draw(&self, g: &mut GfxCtx) { - self.legend.draw(g); - - g.fork_screenspace(); - g.redraw(&self.draw); - g.unfork(); - self.labels.draw(g); - - g.canvas.mark_covered_area(self.rect.clone()); - } -} - -trait Yvalue { - // percent is [0.0, 1.0] - fn from_percent(self, percent: f64) -> T; - fn to_percent(self, max: T) -> f64; - fn prettyprint(self) -> String; - fn zero() -> T; -} - -impl Yvalue for usize { - fn from_percent(self, percent: f64) -> usize { - ((self as f64) * percent) as usize - } - fn to_percent(self, max: usize) -> f64 { - (self as f64) / (max as f64) - } - fn prettyprint(self) -> String { - abstutil::prettyprint_usize(self) - } - fn zero() -> usize { - 0 - } -} -impl Yvalue for Duration { - fn from_percent(self, percent: f64) -> Duration { - percent * self - } - fn to_percent(self, max: Duration) -> f64 { - self / max - } - fn prettyprint(self) -> String { - self.minimal_tostring() - } - fn zero() -> Duration { - Duration::ZERO - } -} - -struct Series { - label: String, - color: Color, - // X-axis is time. Assume this is sorted by X. - pts: Vec<(Duration, T)>, -} - -fn plot>( - title: &str, - series: Vec>, - ctx: &EventCtx, -) -> Option { - let mut batch = GeomBatch::new(); - let mut labels = MultiText::new(); - - let x1 = 0.1 * ctx.canvas.window_width; - let x2 = 0.7 * ctx.canvas.window_width; - let y1 = 0.2 * 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), - ), - ); - - // Assume min_x is Duration::ZERO and min_y is 0 - let max_x = series - .iter() - .map(|s| s.pts.iter().map(|(t, _)| *t).max().unwrap()) - .max() - .unwrap(); - let max_y = series - .iter() - .map(|s| s.pts.iter().map(|(_, cnt)| *cnt).max().unwrap()) - .max() - .unwrap(); - if max_x == Duration::ZERO { - return None; - } - - let num_x_labels = 5; - for i in 0..num_x_labels { - let percent_x = (i as f64) / ((num_x_labels - 1) as f64); - let t = percent_x * max_x; - labels.add( - Text::from(Line(t.to_string())), - ScreenPt::new(x1 + percent_x * (x2 - x1), y2), - ); - } - - let num_y_labels = 5; - for i in 0..num_y_labels { - let percent_y = (i as f64) / ((num_y_labels - 1) as f64); - labels.add( - Text::from(Line(max_y.from_percent(percent_y).prettyprint())), - ScreenPt::new(x1, y2 - percent_y * (y2 - y1)), - ); - } - - let legend = ColorLegend::new( - Text::prompt(title), - series.iter().map(|s| (s.label.as_str(), s.color)).collect(), - ); - - for s in series { - let mut pts = Vec::new(); - if max_y == T::zero() { - pts.push(Pt2D::new(x1, y2)); - pts.push(Pt2D::new(x2, y2)); - } else { - for (t, y) in s.pts { - let percent_x = t / max_x; - let percent_y = y.to_percent(max_y); - pts.push(Pt2D::new( - x1 + (x2 - x1) * percent_x, - // Y inversion! :D - y2 - (y2 - y1) * percent_y, - )); - } - } - batch.push( - s.color, - PolyLine::new(pts).make_polygons(Distance::meters(5.0)), - ); - } - - Some(ShowTripStats { - draw: ctx.prerender.upload(batch), - labels, - legend, - rect: ScreenRectangle { x1, y1, x2, y2 }, - }) -}