From 915b902c9a1f4c924b6908142207e9a8c33dbb2e Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Mon, 11 Oct 2021 15:10:28 -0700 Subject: [PATCH] Warn the user before doing expensive mode shift calculations. #448 (#774) --- abstio/src/abst_data.rs | 6 + abstutil/src/utils.rs | 12 ++ game/src/ungap/predict.rs | 222 +++++++++++++++++++---------------- map_gui/src/tools/updater.rs | 13 +- 4 files changed, 138 insertions(+), 115 deletions(-) diff --git a/abstio/src/abst_data.rs b/abstio/src/abst_data.rs index 7b05a4aa60..d3a7e9197e 100644 --- a/abstio/src/abst_data.rs +++ b/abstio/src/abst_data.rs @@ -136,6 +136,12 @@ impl Manifest { } 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 diff --git a/abstutil/src/utils.rs b/abstutil/src/utils.rs index f3cc492bb0..baffddbc36 100644 --- a/abstutil/src/utils.rs +++ b/abstutil/src/utils.rs @@ -35,6 +35,18 @@ pub fn prettyprint_usize(x: usize) -> String { 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 { if x >= 1000 { let ks = x as f32 / 1000.0; diff --git a/game/src/ungap/predict.rs b/game/src/ungap/predict.rs index 35fb21c6c9..378125cf0e 100644 --- a/game/src/ungap/predict.rs +++ b/game/src/ungap/predict.rs @@ -1,6 +1,7 @@ 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 map_gui::load::FileLoader; use map_gui::tools::{open_browser, ColorNetwork}; @@ -29,39 +30,11 @@ impl TakeLayers for ShowGaps { impl ShowGaps { pub fn new_state(ctx: &mut EventCtx, app: &mut App, layers: Layers) -> Box> { - let map_name = app.primary.map.get_name().clone(); - let change_key = app.primary.map.get_edits_change_key(); - if app.session.mode_shift.key().as_ref() == Some(&(map_name.clone(), change_key)) { - return Box::new(ShowGaps { - 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::::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)) - }), - ) - } + Box::new(ShowGaps { + top_panel: make_top_panel(ctx, app), + layers, + tooltip: None, + }) } } @@ -70,19 +43,20 @@ impl State for ShowGaps { ctx.canvas_movement(); if ctx.redo_mouseover() { self.tooltip = None; - if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) { - Some(ID::Road(r)) => Some(r), - 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); - if count > 0 { - // TODO Word more precisely... or less verbosely. - self.tooltip = Some(Text::from(Line(format!( - "{} trips might cross this high-stress road", - prettyprint_usize(count) - )))); + if let Some(data) = app.session.mode_shift.value() { + if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) { + Some(ID::Road(r)) => Some(r), + Some(ID::Lane(l)) => Some(l.road), + _ => None, + } { + let count = data.gaps.count_per_road.get(r); + if count > 0 { + // TODO Word more precisely... or less verbosely. + self.tooltip = Some(Text::from(Line(format!( + "{} trips might cross this high-stress road", + prettyprint_usize(count) + )))); + } } } } @@ -92,6 +66,28 @@ impl State for ShowGaps { 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"); 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::::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::().ok().unwrap(); + vec![ShowGaps::new_state(ctx, app, state.take_layers())] + })), + ]) + }), + )); } return Tab::PredictImpact @@ -122,8 +118,9 @@ impl State for ShowGaps { self.top_panel.draw(g); self.layers.draw(g, app); - let data = app.session.mode_shift.value().unwrap(); - data.gaps.draw.draw(g); + if let Some(data) = app.session.mode_shift.value() { + data.gaps.draw.draw(g); + } if let Some(ref txt) = self.tooltip { g.draw_mouse_tooltip(txt.clone()); } @@ -131,64 +128,83 @@ impl State for ShowGaps { } 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() { - return Tab::PredictImpact.make_left_panel( - ctx, - app, + if app.session.mode_shift.key().as_ref() == Some(&(map_name.clone(), change_key)) { + let data = app.session.mode_shift.value().unwrap(); + + 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![ - "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)) } diff --git a/map_gui/src/tools/updater.rs b/map_gui/src/tools/updater.rs index f0745beb45..0f2c1bfcca 100644 --- a/map_gui/src/tools/updater.rs +++ b/map_gui/src/tools/updater.rs @@ -4,6 +4,7 @@ use std::fs::File; use futures_channel::mpsc; use abstio::{DataPacks, Manifest, MapName}; +use abstutil::prettyprint_bytes; use widgetry::{EventCtx, Key, Transition}; use crate::load::FutureLoader; @@ -31,18 +32,6 @@ fn size_of_city(map: &MapName) -> u64 { 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 /// not download, maybe a network error), the new map isn't automatically loaded or anything; up to /// the caller to handle that.