Warn the user before doing expensive mode shift calculations. #448 (#774)

This commit is contained in:
Dustin Carlino 2021-10-11 15:10:28 -07:00 committed by GitHub
parent 112848f23b
commit 915b902c9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 115 deletions

View File

@ -136,6 +136,12 @@ impl Manifest {
} }
None None
} }
/// Look up an entry.
pub fn get_entry(&self, path: &str) -> Option<&Entry> {
let path = path.strip_prefix(&crate::path("")).unwrap_or(path);
self.entries.get(&format!("data/{}", path))
}
} }
/// Player-chosen groups of files to opt into downloading /// Player-chosen groups of files to opt into downloading

View File

@ -35,6 +35,18 @@ pub fn prettyprint_usize(x: usize) -> String {
result result
} }
pub fn prettyprint_bytes(bytes: u64) -> String {
if bytes < 1024 {
return format!("{} bytes", bytes);
}
let kb = (bytes as f64) / 1024.0;
if kb < 1024.0 {
return format!("{} KB", kb as usize);
}
let mb = kb / 1024.0;
format!("{} MB", mb as usize)
}
pub fn abbreviated_format(x: usize) -> String { pub fn abbreviated_format(x: usize) -> String {
if x >= 1000 { if x >= 1000 {
let ks = x as f32 / 1000.0; let ks = x as f32 / 1000.0;

View File

@ -1,6 +1,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use abstutil::{prettyprint_usize, Counter, Timer}; use abstio::Manifest;
use abstutil::{prettyprint_bytes, prettyprint_usize, Counter, Timer};
use geom::{Distance, Duration, Polygon, UnitFmt}; use geom::{Distance, Duration, Polygon, UnitFmt};
use map_gui::load::FileLoader; use map_gui::load::FileLoader;
use map_gui::tools::{open_browser, ColorNetwork}; use map_gui::tools::{open_browser, ColorNetwork};
@ -29,39 +30,11 @@ impl TakeLayers for ShowGaps {
impl ShowGaps { impl ShowGaps {
pub fn new_state(ctx: &mut EventCtx, app: &mut App, layers: Layers) -> Box<dyn State<App>> { pub fn new_state(ctx: &mut EventCtx, app: &mut App, layers: Layers) -> Box<dyn State<App>> {
let map_name = app.primary.map.get_name().clone(); Box::new(ShowGaps {
let change_key = app.primary.map.get_edits_change_key(); top_panel: make_top_panel(ctx, app),
if app.session.mode_shift.key().as_ref() == Some(&(map_name.clone(), change_key)) { layers,
return Box::new(ShowGaps { tooltip: None,
top_panel: make_top_panel(ctx, app), })
layers,
tooltip: None,
});
}
let scenario_name = crate::pregame::default_scenario_for_map(&map_name);
if scenario_name == "home_to_work" {
// TODO Should we generate and use this scenario? Or maybe just disable this mode
// entirely?
app.session
.mode_shift
.set((map_name, change_key), ModeShiftData::empty(ctx));
ShowGaps::new_state(ctx, app, layers)
} else {
FileLoader::<App, Scenario>::new_state(
ctx,
abstio::path_scenario(&map_name, &scenario_name),
Box::new(move |ctx, app, _, maybe_scenario| {
// TODO Handle corrupt files
let scenario = maybe_scenario.unwrap();
let data = ctx.loading_screen("predict mode shift", |ctx, timer| {
ModeShiftData::from_scenario(ctx, app, scenario, timer)
});
app.session.mode_shift.set((map_name, change_key), data);
Transition::Replace(ShowGaps::new_state(ctx, app, layers))
}),
)
}
} }
} }
@ -70,19 +43,20 @@ impl State<App> for ShowGaps {
ctx.canvas_movement(); ctx.canvas_movement();
if ctx.redo_mouseover() { if ctx.redo_mouseover() {
self.tooltip = None; self.tooltip = None;
if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) { if let Some(data) = app.session.mode_shift.value() {
Some(ID::Road(r)) => Some(r), if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) {
Some(ID::Lane(l)) => Some(l.road), Some(ID::Road(r)) => Some(r),
_ => None, Some(ID::Lane(l)) => Some(l.road),
} { _ => None,
let data = app.session.mode_shift.value().unwrap(); } {
let count = data.gaps.count_per_road.get(r); let count = data.gaps.count_per_road.get(r);
if count > 0 { if count > 0 {
// TODO Word more precisely... or less verbosely. // TODO Word more precisely... or less verbosely.
self.tooltip = Some(Text::from(Line(format!( self.tooltip = Some(Text::from(Line(format!(
"{} trips might cross this high-stress road", "{} trips might cross this high-stress road",
prettyprint_usize(count) prettyprint_usize(count)
)))); ))));
}
} }
} }
} }
@ -92,6 +66,28 @@ impl State<App> for ShowGaps {
if x == "read about how this prediction works" { if x == "read about how this prediction works" {
open_browser("https://a-b-street.github.io/docs/software/bike_network/tech_details.html#predict-impact"); open_browser("https://a-b-street.github.io/docs/software/bike_network/tech_details.html#predict-impact");
return Transition::Keep; return Transition::Keep;
} else if x == "Calculate" {
let change_key = app.primary.map.get_edits_change_key();
let map_name = app.primary.map.get_name().clone();
let scenario_name = crate::pregame::default_scenario_for_map(&map_name);
return Transition::Push(FileLoader::<App, Scenario>::new_state(
ctx,
abstio::path_scenario(&map_name, &scenario_name),
Box::new(move |ctx, app, timer, maybe_scenario| {
// TODO Handle corrupt files
let scenario = maybe_scenario.unwrap();
let data = ModeShiftData::from_scenario(ctx, app, scenario, timer);
app.session.mode_shift.set((map_name, change_key), data);
Transition::Multi(vec![
Transition::Pop,
Transition::ConsumeState(Box::new(|state, ctx, app| {
let state = state.downcast::<ShowGaps>().ok().unwrap();
vec![ShowGaps::new_state(ctx, app, state.take_layers())]
})),
])
}),
));
} }
return Tab::PredictImpact return Tab::PredictImpact
@ -122,8 +118,9 @@ impl State<App> for ShowGaps {
self.top_panel.draw(g); self.top_panel.draw(g);
self.layers.draw(g, app); self.layers.draw(g, app);
let data = app.session.mode_shift.value().unwrap(); if let Some(data) = app.session.mode_shift.value() {
data.gaps.draw.draw(g); data.gaps.draw.draw(g);
}
if let Some(ref txt) = self.tooltip { if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone()); g.draw_mouse_tooltip(txt.clone());
} }
@ -131,64 +128,83 @@ impl State<App> for ShowGaps {
} }
fn make_top_panel(ctx: &mut EventCtx, app: &App) -> Panel { fn make_top_panel(ctx: &mut EventCtx, app: &App) -> Panel {
let data = app.session.mode_shift.value().unwrap(); let map_name = app.primary.map.get_name().clone();
let change_key = app.primary.map.get_edits_change_key();
let col;
if data.all_candidate_trips.is_empty() { if app.session.mode_shift.key().as_ref() == Some(&(map_name.clone(), change_key)) {
return Tab::PredictImpact.make_left_panel( let data = app.session.mode_shift.value().unwrap();
ctx,
app, col = vec![
ctx.style()
.btn_plain
.icon_text(
"system/assets/tools/info.svg",
"How many drivers might switch to biking?",
)
.build_widget(ctx, "read about how this prediction works"),
percentage_bar(
ctx,
Text::from(Line(format!(
"{} total driving trips in this area",
prettyprint_usize(data.all_candidate_trips.len())
))),
0.0,
),
Widget::col(vec![ Widget::col(vec![
"This city doesn't have travel demand model data available".text_widget(ctx), "Who might cycle if it was safer?".text_widget(ctx),
]), data.filters.to_controls(ctx),
); percentage_bar(
ctx,
Text::from(Line(format!(
"{} / {} trips, based on these thresholds",
data.filtered_trips.len(),
data.all_candidate_trips.len()
))),
pct(data.filtered_trips.len(), data.all_candidate_trips.len()),
),
])
.section(ctx),
Widget::col(vec![
"How many would switch based on your proposal?".text_widget(ctx),
percentage_bar(
ctx,
Text::from(Line(format!(
"{} / {} trips would switch",
data.results.num_trips,
data.all_candidate_trips.len()
))),
pct(data.results.num_trips, data.all_candidate_trips.len()),
),
data.results.describe().into_widget(ctx),
])
.section(ctx),
];
} else {
let scenario_name = crate::pregame::default_scenario_for_map(&map_name);
if scenario_name == "home_to_work" {
col =
vec!["This city doesn't have travel demand model data available".text_widget(ctx)];
} else {
let size = Manifest::load()
.get_entry(&abstio::path_scenario(&map_name, &scenario_name))
.map(|entry| prettyprint_bytes(entry.compressed_size_bytes))
.unwrap_or("???".to_string());
col = vec![
Text::from_multiline(vec![
Line("Predicting impact of your proposal may take a moment."),
Line("The application may freeze up during that time."),
Line(format!("We need to load a {} file", size)),
])
.into_widget(ctx),
ctx.style()
.btn_solid_primary
.text("Calculate")
.build_def(ctx),
];
}
} }
let col = vec![
ctx.style()
.btn_plain
.icon_text(
"system/assets/tools/info.svg",
"How many drivers might switch to biking?",
)
.build_widget(ctx, "read about how this prediction works"),
percentage_bar(
ctx,
Text::from(Line(format!(
"{} total driving trips in this area",
prettyprint_usize(data.all_candidate_trips.len())
))),
0.0,
),
Widget::col(vec![
"Who might cycle if it was safer?".text_widget(ctx),
data.filters.to_controls(ctx),
percentage_bar(
ctx,
Text::from(Line(format!(
"{} / {} trips, based on these thresholds",
data.filtered_trips.len(),
data.all_candidate_trips.len()
))),
pct(data.filtered_trips.len(), data.all_candidate_trips.len()),
),
])
.section(ctx),
Widget::col(vec![
"How many would switch based on your proposal?".text_widget(ctx),
percentage_bar(
ctx,
Text::from(Line(format!(
"{} / {} trips would switch",
data.results.num_trips,
data.all_candidate_trips.len()
))),
pct(data.results.num_trips, data.all_candidate_trips.len()),
),
data.results.describe().into_widget(ctx),
])
.section(ctx),
];
Tab::PredictImpact.make_left_panel(ctx, app, Widget::col(col)) Tab::PredictImpact.make_left_panel(ctx, app, Widget::col(col))
} }

View File

@ -4,6 +4,7 @@ use std::fs::File;
use futures_channel::mpsc; use futures_channel::mpsc;
use abstio::{DataPacks, Manifest, MapName}; use abstio::{DataPacks, Manifest, MapName};
use abstutil::prettyprint_bytes;
use widgetry::{EventCtx, Key, Transition}; use widgetry::{EventCtx, Key, Transition};
use crate::load::FutureLoader; use crate::load::FutureLoader;
@ -31,18 +32,6 @@ fn size_of_city(map: &MapName) -> u64 {
bytes bytes
} }
fn prettyprint_bytes(bytes: u64) -> String {
if bytes < 1024 {
return format!("{} bytes", bytes);
}
let kb = (bytes as f64) / 1024.0;
if kb < 1024.0 {
return format!("{} KB", kb as usize);
}
let mb = kb / 1024.0;
format!("{} MB", mb as usize)
}
/// Prompt to download a missing city. On either success or failure (maybe the player choosing to /// Prompt to download a missing city. On either success or failure (maybe the player choosing to
/// not download, maybe a network error), the new map isn't automatically loaded or anything; up to /// not download, maybe a network error), the new map isn't automatically loaded or anything; up to
/// the caller to handle that. /// the caller to handle that.