Start a simplified consultation mode for the LTN tool

- starts in a fixed neighborhood
- hides many controls
- lets users jump to route planning from the per-neighborhood screens
This commit is contained in:
Dustin Carlino 2022-05-17 16:08:01 +01:00
parent 0f62e650ea
commit 078265718a
10 changed files with 133 additions and 59 deletions

View File

@ -44,11 +44,7 @@ impl BrowseNeighborhoods {
&top_panel, &top_panel,
Widget::col(vec![ Widget::col(vec![
app.session.alt_proposals.to_widget(ctx, app), app.session.alt_proposals.to_widget(ctx, app),
ctx.style() crate::route_planner::RoutePlanner::button(ctx),
.btn_outline
.text("Plan a route")
.hotkey(Key::R)
.build_def(ctx),
Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev), Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev),
advanced_panel(ctx, app), advanced_panel(ctx, app),
]), ]),
@ -63,6 +59,14 @@ impl BrowseNeighborhoods {
draw_boundary_roads: draw_boundary_roads(ctx, app), draw_boundary_roads: draw_boundary_roads(ctx, app),
}) })
} }
pub fn button(ctx: &EventCtx, app: &App) -> Widget {
ctx.style()
.btn_back("Browse neighborhoods")
.hotkey(Key::Escape)
.build_def(ctx)
.hide(app.session.consultation.is_some())
}
} }
impl State<App> for BrowseNeighborhoods { impl State<App> for BrowseNeighborhoods {

View File

@ -1,6 +1,6 @@
use map_gui::tools::grey_out_map; use map_gui::tools::grey_out_map;
use widgetry::tools::open_browser; use widgetry::tools::open_browser;
use widgetry::{EventCtx, GfxCtx, Line, Panel, SimpleState, State, TextExt, Widget}; use widgetry::{EventCtx, GfxCtx, Line, Panel, SimpleState, State, Text, Widget};
use crate::{App, Transition}; use crate::{App, Transition};
@ -13,9 +13,13 @@ impl About {
Line("About the LTN tool").small_heading().into_widget(ctx), Line("About the LTN tool").small_heading().into_widget(ctx),
ctx.style().btn_close_widget(ctx), ctx.style().btn_close_widget(ctx),
]), ]),
"Created by Dustin Carlino & Cindy Huang".text_widget(ctx), Text::from_multiline(vec![
"Data from OpenStreetMap".text_widget(ctx), Line("Created by Dustin Carlino, Cindy Huang, and Jennifer Ding"),
"See below for full credits and more info".text_widget(ctx), Line("Developed at the Alan Turing Institute"),
Line("Data from OpenStreetMap"),
Line("See below for full credits and more info"),
])
.into_widget(ctx),
ctx.style() ctx.style()
.btn_outline .btn_outline
.text("ltn.abstreet.org") .text("ltn.abstreet.org")

View File

@ -11,25 +11,35 @@ pub struct TopPanel;
impl TopPanel { impl TopPanel {
pub fn panel(ctx: &mut EventCtx, app: &App) -> Panel { pub fn panel(ctx: &mut EventCtx, app: &App) -> Panel {
let consultation = app.session.consultation.is_some();
Panel::new_builder( Panel::new_builder(
Widget::row(vec![ Widget::row(vec![
map_gui::tools::home_btn(ctx), map_gui::tools::home_btn(ctx),
Line("Low traffic neighborhoods") Line(if consultation {
.small_heading() "East Bristol Liveable Neighbourhood"
.into_widget(ctx) } else {
.centered_vert(), "Low traffic neighborhoods"
})
.small_heading()
.into_widget(ctx)
.centered_vert(),
ctx.style() ctx.style()
.btn_plain .btn_plain
.icon("system/assets/tools/info.svg") .icon("system/assets/tools/info.svg")
.build_widget(ctx, "about this tool") .build_widget(ctx, "about this tool")
.centered_vert(), .centered_vert()
map_gui::tools::change_map_btn(ctx, app).centered_vert(), .hide(consultation),
map_gui::tools::change_map_btn(ctx, app)
.centered_vert()
.hide(consultation),
Widget::row(vec![ Widget::row(vec![
ctx.style() ctx.style()
.btn_plain .btn_plain
.text("Export to GeoJSON") .text("Export to GeoJSON")
.build_def(ctx) .build_def(ctx)
.centered_vert(), .centered_vert()
.hide(consultation),
ctx.style() ctx.style()
.btn_plain .btn_plain
.icon("system/assets/tools/search.svg") .icon("system/assets/tools/search.svg")
@ -64,14 +74,20 @@ impl TopPanel {
) -> Option<Transition> { ) -> Option<Transition> {
if let Outcome::Clicked(x) = panel.event(ctx) { if let Outcome::Clicked(x) = panel.event(ctx) {
match x.as_ref() { match x.as_ref() {
"Home" => Some(Transition::Clear(vec![ "Home" => {
map_gui::tools::TitleScreen::new_state( if app.session.consultation.is_none() {
ctx, Some(Transition::Clear(vec![
app, map_gui::tools::TitleScreen::new_state(
map_gui::tools::Executable::LTN, ctx,
Box::new(|ctx, app, _| BrowseNeighborhoods::new_state(ctx, app)), app,
), map_gui::tools::Executable::LTN,
])), Box::new(|ctx, app, _| BrowseNeighborhoods::new_state(ctx, app)),
),
]))
} else {
Some(Transition::Push(super::about::About::new_state(ctx)))
}
}
"change map" => Some(Transition::Push(map_gui::tools::CityPicker::new_state( "change map" => Some(Transition::Push(map_gui::tools::CityPicker::new_state(
ctx, ctx,
app, app,

View File

@ -61,7 +61,6 @@ impl Viewer {
) )
.text_widget(ctx), .text_widget(ctx),
warning.text_widget(ctx), warning.text_widget(ctx),
Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev),
advanced_panel(ctx, app), advanced_panel(ctx, app),
]), ]),
) )
@ -304,7 +303,6 @@ fn make_world(
fn help() -> Vec<&'static str> { fn help() -> Vec<&'static str> {
vec![ vec![
"The colored cells show where it's possible to drive without leaving the neighborhood.", "The colored cells show where it's possible to drive without leaving the neighborhood.",
"Green cells don't allow car-traffic.",
"", "",
"The darker red roads have more predicted shortcutting traffic.", "The darker red roads have more predicted shortcutting traffic.",
"", "",
@ -314,10 +312,14 @@ fn help() -> Vec<&'static str> {
} }
fn advanced_panel(ctx: &EventCtx, app: &App) -> Widget { fn advanced_panel(ctx: &EventCtx, app: &App) -> Widget {
if !app.opts.dev { if app.session.consultation.is_some() {
return Widget::nothing(); return Widget::nothing();
} }
if !app.opts.dev {
return Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev);
}
Widget::col(vec![ Widget::col(vec![
Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev),
Line("Advanced features").small_heading().into_widget(ctx), Line("Advanced features").small_heading().into_widget(ctx),
Widget::row(vec![ Widget::row(vec![
"Draw traffic cells as".text_widget(ctx).centered_vert(), "Draw traffic cells as".text_widget(ctx).centered_vert(),

View File

@ -47,10 +47,7 @@ impl ShowResults {
} }
let contents = Widget::col(vec![ let contents = Widget::col(vec![
ctx.style() BrowseNeighborhoods::button(ctx, app),
.btn_back("Browse neighborhoods")
.hotkey(Key::Escape)
.build_def(ctx),
Line("Impact prediction").small_heading().into_widget(ctx), Line("Impact prediction").small_heading().into_widget(ctx),
Text::from(Line("This tool starts with a travel demand model, calculates the route every trip takes before and after changes, and displays volumes along roads")).wrap_to_pct(ctx, 20).into_widget(ctx), Text::from(Line("This tool starts with a travel demand model, calculates the route every trip takes before and after changes, and displays volumes along roads")).wrap_to_pct(ctx, 20).into_widget(ctx),
Text::from_all(vec![ Text::from_all(vec![

View File

@ -2,6 +2,7 @@
use structopt::StructOpt; use structopt::StructOpt;
use abstio::MapName;
use abstutil::Timer; use abstutil::Timer;
use widgetry::{EventCtx, GfxCtx, Settings}; use widgetry::{EventCtx, GfxCtx, Settings};
@ -46,6 +47,9 @@ struct Args {
/// Load a previously saved proposal with this name. Note this takes a name, not a full path. /// Load a previously saved proposal with this name. Note this takes a name, not a full path.
#[structopt(long)] #[structopt(long)]
proposal: Option<String>, proposal: Option<String>,
/// Lock the user into one fixed neighborhood, and remove many controls
#[structopt(long)]
consultation: bool,
#[structopt(flatten)] #[structopt(flatten)]
app_args: map_gui::SimpleAppArgs, app_args: map_gui::SimpleAppArgs,
} }
@ -77,6 +81,8 @@ fn run(mut settings: Settings) {
main_road_penalty: 1.0, main_road_penalty: 1.0,
current_trip_name: None, current_trip_name: None,
consultation: None,
}; };
map_gui::SimpleApp::new( map_gui::SimpleApp::new(
ctx, ctx,
@ -91,15 +97,32 @@ fn run(mut settings: Settings) {
.as_ref() .as_ref()
.and_then(|name| crate::save::Proposal::load(ctx, app, name)); .and_then(|name| crate::save::Proposal::load(ctx, app, name));
let mut states = vec![ let mut states = Vec::new();
map_gui::tools::TitleScreen::new_state( if args.consultation {
if app.map.get_name() != &MapName::new("gb", "bristol", "east") {
panic!("Consultation mode not supported on this map");
}
// TODO Don't hardcode
app.session.consultation = Some(NeighborhoodID(33));
app.session.alt_proposals = crate::save::AltProposals::new();
ctx.loading_screen("initialize", |ctx, timer| {
crate::clear_current_proposal(ctx, app, timer);
});
states.push(connectivity::Viewer::new_state(
ctx,
app,
app.session.consultation.unwrap(),
));
} else {
states.push(map_gui::tools::TitleScreen::new_state(
ctx, ctx,
app, app,
map_gui::tools::Executable::LTN, map_gui::tools::Executable::LTN,
Box::new(|ctx, app, _| BrowseNeighborhoods::new_state(ctx, app)), Box::new(|ctx, app, _| BrowseNeighborhoods::new_state(ctx, app)),
), ));
BrowseNeighborhoods::new_state(ctx, app), states.push(BrowseNeighborhoods::new_state(ctx, app));
]; }
if let Some(state) = popup_state { if let Some(state) = popup_state {
states.push(state); states.push(state);
} }
@ -146,6 +169,8 @@ pub struct Session {
pub main_road_penalty: f64, pub main_road_penalty: f64,
current_trip_name: Option<String>, current_trip_name: Option<String>,
consultation: Option<NeighborhoodID>,
} }
/// Do the equivalent of `SimpleApp::draw_unzoomed` or `draw_zoomed`, but after the water/park /// Do the equivalent of `SimpleApp::draw_unzoomed` or `draw_zoomed`, but after the water/park

View File

@ -13,7 +13,7 @@ use crate::{colors, App};
/// An opaque ID, won't be contiguous as we adjust boundaries /// An opaque ID, won't be contiguous as we adjust boundaries
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NeighborhoodID(usize); pub struct NeighborhoodID(pub usize);
/// Identifies a single / unmerged block, which never changes /// Identifies a single / unmerged block, which never changes
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]

View File

@ -28,10 +28,7 @@ impl Tab {
) -> PanelBuilder { ) -> PanelBuilder {
let contents = Widget::col(vec![ let contents = Widget::col(vec![
app.session.alt_proposals.to_widget(ctx, app), app.session.alt_proposals.to_widget(ctx, app),
ctx.style() BrowseNeighborhoods::button(ctx, app),
.btn_back("Browse neighborhoods")
.hotkey(Key::Escape)
.build_def(ctx),
Line("Editing neighborhood") Line("Editing neighborhood")
.small_heading() .small_heading()
.into_widget(ctx), .into_widget(ctx),
@ -64,8 +61,9 @@ impl Tab {
]), ]),
]) ])
.section(ctx), .section(ctx),
self.make_buttons(ctx), self.make_buttons(ctx, app),
per_tab_contents, per_tab_contents,
crate::route_planner::RoutePlanner::button(ctx),
]); ]);
crate::components::LeftPanel::builder(ctx, top_panel, contents) crate::components::LeftPanel::builder(ctx, top_panel, contents)
} }
@ -117,11 +115,14 @@ impl Tab {
} }
})) }))
} }
"Plan a route" => Some(Transition::Push(
crate::route_planner::RoutePlanner::new_state(ctx, app),
)),
_ => None, _ => None,
} }
} }
fn make_buttons(self, ctx: &mut EventCtx) -> Widget { fn make_buttons(self, ctx: &mut EventCtx, app: &App) -> Widget {
let mut row = Vec::new(); let mut row = Vec::new();
for (tab, label, key) in [ for (tab, label, key) in [
(Tab::Connectivity, "Connectivity", Key::F1), (Tab::Connectivity, "Connectivity", Key::F1),
@ -144,20 +145,22 @@ impl Tab {
.build_def(ctx), .build_def(ctx),
); );
} }
// TODO The 3rd doesn't really act like a tab if app.session.consultation.is_none() {
row.push( // TODO The 3rd doesn't really act like a tab
ctx.style() row.push(
.btn_tab ctx.style()
.text("Adjust boundary") .btn_tab
.corner_rounding(geom::CornerRadii { .text("Adjust boundary")
top_left: DEFAULT_CORNER_RADIUS, .corner_rounding(geom::CornerRadii {
top_right: DEFAULT_CORNER_RADIUS, top_left: DEFAULT_CORNER_RADIUS,
bottom_left: 0.0, top_right: DEFAULT_CORNER_RADIUS,
bottom_right: 0.0, bottom_left: 0.0,
}) bottom_right: 0.0,
.hotkey(Key::B) })
.build_def(ctx), .hotkey(Key::B)
); .build_def(ctx),
);
}
Widget::row(row) Widget::row(row)
} }

View File

@ -61,6 +61,14 @@ impl RoutePlanner {
Box::new(rp) Box::new(rp)
} }
pub fn button(ctx: &EventCtx) -> Widget {
ctx.style()
.btn_outline
.text("Plan a route")
.hotkey(Key::R)
.build_def(ctx)
}
// Updates the panel and draw_routes // Updates the panel and draw_routes
fn update_everything(&mut self, ctx: &mut EventCtx, app: &mut App) { fn update_everything(&mut self, ctx: &mut EventCtx, app: &mut App) {
self.files.autosave(app); self.files.autosave(app);
@ -68,10 +76,12 @@ impl RoutePlanner {
let contents = Widget::col(vec![ let contents = Widget::col(vec![
app.session.alt_proposals.to_widget(ctx, app), app.session.alt_proposals.to_widget(ctx, app),
BrowseNeighborhoods::button(ctx, app),
ctx.style() ctx.style()
.btn_back("Browse neighborhoods") .btn_back("Analyze neighbourhood")
.hotkey(Key::Escape) .hotkey(Key::Escape)
.build_def(ctx), .build_def(ctx)
.hide(app.session.consultation.is_none()),
Line("Plan a route").small_heading().into_widget(ctx), Line("Plan a route").small_heading().into_widget(ctx),
Widget::col(vec![ Widget::col(vec![
self.files.get_panel_widget(ctx), self.files.get_panel_widget(ctx),
@ -287,6 +297,9 @@ impl State<App> for RoutePlanner {
if x == "Browse neighborhoods" { if x == "Browse neighborhoods" {
return Transition::Replace(BrowseNeighborhoods::new_state(ctx, app)); return Transition::Replace(BrowseNeighborhoods::new_state(ctx, app));
} }
if x == "Analyze neighbourhood" {
return Transition::Pop;
}
if let Some(t) = self.files.on_click(ctx, app, x) { if let Some(t) = self.files.on_click(ctx, app, x) {
// Bit hacky... // Bit hacky...
if matches!(t, Transition::Keep) { if matches!(t, Transition::Keep) {

View File

@ -398,6 +398,16 @@ impl Widget {
self.id = Some(id.into()); self.id = Some(id.into());
self self
} }
/// If the argument is true, don't actually create this widget. May be more readable than an
/// if/else block.
pub fn hide(self, x: bool) -> Widget {
if x {
Widget::nothing()
} else {
self
}
}
} }
// Convenient?? constructors // Convenient?? constructors