displaying bus delay data over time. needs work, but solid start to a

general plotting mechanism
This commit is contained in:
Dustin Carlino 2019-11-05 13:00:51 -08:00
parent 3b1a659f15
commit 1a28768df3
5 changed files with 173 additions and 40 deletions

View File

@ -1,6 +1,7 @@
use crate::game::{msg, Transition, WizardState};
use crate::render::AgentColorScheme;
use crate::sandbox::{bus_explorer, overlays, spawner, SandboxMode};
use crate::sandbox::overlays::Overlays;
use crate::sandbox::{bus_explorer, spawner, trip_stats, SandboxMode};
use crate::ui::UI;
use abstutil::{prettyprint_usize, Timer};
use ezgui::{hotkey, Choice, Color, EventCtx, GfxCtx, Key, Line, ModalMenu, Text, Wizard};
@ -100,6 +101,7 @@ impl GameplayState {
&format!("Optimize {}", route_name),
vec![
(hotkey(Key::E), "show bus route"),
(hotkey(Key::T), "show delays over time"),
(hotkey(Key::S), "change statistic"),
(hotkey(Key::H), "help"),
],
@ -199,7 +201,7 @@ impl GameplayState {
&mut self,
ctx: &mut EventCtx,
ui: &mut UI,
overlays: &mut overlays::Overlays,
overlays: &mut Overlays,
) -> Option<Transition> {
match self.state {
State::Freeform => {
@ -250,17 +252,36 @@ impl GameplayState {
"hide bus route",
overlays,
match overlays {
overlays::Overlays::BusRoute(_) => true,
Overlays::BusRoute(_) => true,
_ => false,
},
*time != ui.primary.sim.time(),
) {
*overlays = overlays::Overlays::BusRoute(bus_explorer::ShowBusRoute::new(
*overlays = Overlays::BusRoute(bus_explorer::ShowBusRoute::new(
ui.primary.map.get_br(route),
ui,
ctx,
));
}
if manage_overlays(
&mut self.menu,
ctx,
"show delays over time",
"hide delays over time",
overlays,
match overlays {
Overlays::BusDelaysOverTime(_) => true,
_ => false,
},
*time != ui.primary.sim.time(),
) {
if let Some(s) = trip_stats::ShowTripStats::bus_delays(route, ui, ctx) {
*overlays = Overlays::BusDelaysOverTime(s);
} else {
println!("No route delay info yet");
*overlays = Overlays::Inactive;
}
}
// TODO Expensive
if *time != ui.primary.sim.time() {
@ -511,7 +532,7 @@ fn manage_overlays(
ctx: &mut EventCtx,
show: &str,
hide: &str,
overlay: &mut overlays::Overlays,
overlay: &mut Overlays,
active_originally: bool,
time_changed: bool,
) -> bool {
@ -525,7 +546,7 @@ fn manage_overlays(
if !active_originally && menu.swap_action(show, hide, ctx) {
true
} else if active_originally && menu.swap_action(hide, show, ctx) {
*overlay = overlays::Overlays::Inactive;
*overlay = Overlays::Inactive;
false
} else {
active_originally && time_changed

View File

@ -23,6 +23,7 @@ pub enum Overlays {
BikeNetwork(RoadColorer),
// Only set by certain gameplay modes
BusRoute(ShowBusRoute),
BusDelaysOverTime(ShowTripStats),
}
impl Overlays {
@ -66,7 +67,7 @@ impl Overlays {
Overlays::FinishedTrips(t, _) => ("finished trips", *t),
Overlays::Chokepoints(t, _) => ("chokepoints", *t),
Overlays::BikeNetwork(_) => ("bike network", ui.primary.sim.time()),
Overlays::BusRoute(_) => {
Overlays::BusRoute(_) | Overlays::BusDelaysOverTime(_) => {
// The gameplay mode will update it.
return None;
}
@ -91,7 +92,7 @@ impl Overlays {
heatmap.draw(g, ui);
true
}
Overlays::FinishedTrips(_, ref s) => {
Overlays::FinishedTrips(_, ref s) | Overlays::BusDelaysOverTime(ref s) => {
ui.draw(
g,
DrawOptions::new(),

View File

@ -5,6 +5,7 @@ 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;
@ -64,14 +65,42 @@ impl ShowTripStats {
}
}
Some(plot(
plot(
"finished trips",
lines
.into_iter()
.map(|(name, color, m)| (name, color, pts_per_mode.remove(&m).unwrap()))
.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<ShowTripStats> {
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) {
@ -86,16 +115,60 @@ impl ShowTripStats {
}
}
fn plot(
trait Yvalue<T> {
// 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<usize> 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<Duration> 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<T> {
label: String,
color: Color,
// X-axis is time. Assume this is sorted by X.
pts: Vec<(Duration, T)>,
}
fn plot<T: Ord + PartialEq + Copy + Yvalue<T>>(
title: &str,
series: Vec<(&str, Color, Vec<(Duration, usize)>)>,
series: Vec<Series<T>>,
ctx: &EventCtx,
) -> ShowTripStats {
) -> Option<ShowTripStats> {
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.2 * ctx.canvas.window_width;
let x2 = 0.8 * ctx.canvas.window_width;
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(
@ -107,52 +180,54 @@ fn plot(
),
);
// Assume every series has data at exactly the same durations
// 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;
let max_x = series[0].2.last().unwrap().0;
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = series[0].2[(percent_x * ((series[0].2.len() - 1) as f64)) as usize].0;
let t = percent_x * max_x;
labels.add(
Text::from(Line(t.to_string())),
ScreenPt::new(x1 + percent_x * (x2 - x1), y2),
);
}
// Don't assume the data is cumulative
let max_y = series
.iter()
.map(|(_, _, pts)| pts.iter().map(|(_, cnt)| *cnt).max().unwrap())
.max()
.unwrap();
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(abstutil::prettyprint_usize(
(percent_y * (max_y as f64)) as usize,
))),
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(|(name, color, _)| (*name, *color))
.collect(),
series.iter().map(|s| (s.label.as_str(), s.color)).collect(),
);
for (_, color, raw_pts) in series {
for s in series {
let mut pts = Vec::new();
if max_y == 0 {
if max_y == T::zero() {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
for (t, y) in raw_pts {
for (t, y) in s.pts {
let percent_x = t / max_x;
let percent_y = (y as f64) / (max_y as f64);
let percent_y = y.to_percent(max_y);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
@ -161,15 +236,15 @@ fn plot(
}
}
batch.push(
color,
s.color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
ShowTripStats {
Some(ShowTripStats {
draw: ctx.prerender.upload(batch),
labels,
legend,
rect: ScreenRectangle { x1, y1, x2, y2 },
}
})
}

View File

@ -1081,9 +1081,14 @@ fn make_half_map(
}
if i.is_border() {
if i.roads.len() != 1 {
let mut roads = Vec::new();
let id = i.id;
for r in i.roads.clone() {
roads.push(map.get_r(r).orig_id);
}
panic!(
"{} is a border, but is connected to >1 road: {:?}",
i.id, i.roads
id, roads
);
}
continue;

View File

@ -120,4 +120,35 @@ impl Analytics {
}
delay_to_stop
}
// TODO Refactor!
// For each stop, a list of (time, delay)
pub fn bus_arrivals_over_time(
&self,
now: Duration,
r: BusRouteID,
) -> BTreeMap<BusStopID, Vec<(Duration, Duration)>> {
let mut per_bus: BTreeMap<CarID, Vec<(Duration, BusStopID)>> = BTreeMap::new();
for (t, car, route, stop) in &self.bus_arrivals {
if *t > now {
break;
}
if *route == r {
per_bus
.entry(*car)
.or_insert_with(Vec::new)
.push((*t, *stop));
}
}
let mut delays_to_stop: BTreeMap<BusStopID, Vec<(Duration, Duration)>> = BTreeMap::new();
for events in per_bus.values() {
for pair in events.windows(2) {
delays_to_stop
.entry(pair[1].1)
.or_insert_with(Vec::new)
.push((pair[1].0, pair[1].0 - pair[0].0));
}
}
delays_to_stop
}
}