mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-25 11:44:25 +03:00
displaying bus delay data over time. needs work, but solid start to a
general plotting mechanism
This commit is contained in:
parent
3b1a659f15
commit
1a28768df3
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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 },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user