diff --git a/apps/ltn/src/app.rs b/apps/ltn/src/app.rs index 473d4e4692..82479a0e3e 100644 --- a/apps/ltn/src/app.rs +++ b/apps/ltn/src/app.rs @@ -36,6 +36,9 @@ pub struct PerMap { pub partitioning: Partitioning, pub edits: Edits, + // The last edited neighbourhood + pub current_neighbourhood: Option, + // These capture modal filters that exist in the map already. Whenever we pathfind in this app // in the "before changes" case, we have to use these. Do NOT use the map's built-in // pathfinder. (https://github.com/a-b-street/abstreet/issues/852 would make this more clear) @@ -76,6 +79,8 @@ impl PerMap { partitioning: Partitioning::empty(), edits: Edits::default(), + current_neighbourhood: None, + routing_params_before_changes: RoutingParams::default(), alt_proposals: crate::save::AltProposals::new(), impact: crate::impact::Impact::empty(ctx), diff --git a/apps/ltn/src/browse.rs b/apps/ltn/src/browse.rs index 1c54876200..9399c01765 100644 --- a/apps/ltn/src/browse.rs +++ b/apps/ltn/src/browse.rs @@ -4,7 +4,7 @@ use abstutil::Counter; use map_gui::tools::{ColorNetwork, DrawSimpleRoadLabels}; use widgetry::mapspace::{World, WorldOutcome}; use widgetry::{ - Choice, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel, + Choice, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, State, TextExt, Toggle, Widget, }; @@ -43,13 +43,12 @@ impl BrowseNeighbourhoods { (make_world(ctx, app), draw_over_roads(ctx, app)) }); - let top_panel = crate::components::TopPanel::panel(ctx, app); + let top_panel = crate::components::TopPanel::panel(ctx, app, Mode::BrowseNeighbourhoods); let left_panel = crate::components::LeftPanel::builder( ctx, &top_panel, Widget::col(vec![ app.per_map.alt_proposals.to_widget(ctx, app), - crate::route_planner::RoutePlanner::button(ctx), Toggle::checkbox(ctx, "Advanced features", None, app.opts.dev), advanced_panel(ctx, app), ]), @@ -64,14 +63,6 @@ impl BrowseNeighbourhoods { draw_boundary_roads: draw_boundary_roads(ctx, app), }) } - - pub fn button(ctx: &EventCtx, app: &App) -> Widget { - ctx.style() - .btn_back("Browse neighbourhoods") - .hotkey(Key::Escape) - .build_def(ctx) - .hide(app.per_map.consultation.is_some()) - } } impl State for BrowseNeighbourhoods { @@ -88,14 +79,6 @@ impl State for BrowseNeighbourhoods { } match self.left_panel.event(ctx) { Outcome::Clicked(x) => match x.as_ref() { - "Calculate" | "Show impact" => { - return Transition::Push(crate::impact::ShowResults::new_state(ctx, app)); - } - "Plan a route" => { - return Transition::Push(crate::route_planner::RoutePlanner::new_state( - ctx, app, - )); - } "Automatically place filters" => { ctx.loading_screen("automatically filter all neighbourhoods", |ctx, timer| { timer.start_iter( @@ -299,22 +282,6 @@ pub enum Style { Shortcuts, } -fn impact_widget(ctx: &EventCtx, app: &App) -> Widget { - if &app.per_map.impact.map == app.per_map.map.get_name() - && app.per_map.impact.change_key == app.per_map.edits.get_change_key() - { - // Nothing to calculate! - return ctx.style().btn_outline.text("Show impact").build_def(ctx); - } - - Widget::col(vec![ - Line("The app may freeze while calculating this.") - .small() - .into_widget(ctx), - ctx.style().btn_outline.text("Calculate").build_def(ctx), - ]) -} - fn help() -> Vec<&'static str> { vec![ "Basic map navigation: click and drag to pan, swipe or scroll to zoom", @@ -344,11 +311,6 @@ fn advanced_panel(ctx: &EventCtx, app: &App) -> Widget { ), ])]) .section(ctx), - Widget::col(vec![ - "Predict proposal impact".text_widget(ctx), - impact_widget(ctx, app), - ]) - .section(ctx), Widget::col(vec![ ctx.style() .btn_outline diff --git a/apps/ltn/src/components/layers.rs b/apps/ltn/src/components/layers.rs index 01d8ceb671..45396d4560 100644 --- a/apps/ltn/src/components/layers.rs +++ b/apps/ltn/src/components/layers.rs @@ -7,6 +7,7 @@ use widgetry::{ Widget, }; +use crate::components::Mode; use crate::{colors, App, FilterType, Transition}; // Partly copied from ungap/layers.s @@ -181,15 +182,6 @@ fn zoom_enabled_cache_key(ctx: &EventCtx) -> (bool, bool) { (ctx.canvas.is_max_zoom(), ctx.canvas.is_min_zoom()) } -#[derive(PartialEq)] -pub enum Mode { - BrowseNeighbourhoods, - ModifyNeighbourhood, - SelectBoundary, - RoutePlanner, - Impact, -} - impl Mode { fn legend(&self, ctx: &mut EventCtx, cs: &ColorScheme) -> Widget { // TODO Light/dark buildings? Traffic signals? diff --git a/apps/ltn/src/components/mod.rs b/apps/ltn/src/components/mod.rs index d59b0bf96f..93b6f5a0d0 100644 --- a/apps/ltn/src/components/mod.rs +++ b/apps/ltn/src/components/mod.rs @@ -3,6 +3,15 @@ mod layers; mod left_panel; mod top_panel; -pub use layers::{Layers, Mode}; +pub use layers::Layers; pub use left_panel::LeftPanel; pub use top_panel::TopPanel; + +#[derive(PartialEq)] +pub enum Mode { + BrowseNeighbourhoods, + ModifyNeighbourhood, + SelectBoundary, + RoutePlanner, + Impact, +} diff --git a/apps/ltn/src/components/top_panel.rs b/apps/ltn/src/components/top_panel.rs index 0accce364e..b4cd1b5700 100644 --- a/apps/ltn/src/components/top_panel.rs +++ b/apps/ltn/src/components/top_panel.rs @@ -1,18 +1,73 @@ use geom::CornerRadii; +use widgetry::tools::ChooseSomething; use widgetry::tools::PopupMsg; use widgetry::{ - lctrl, CornerRounding, EventCtx, HorizontalAlignment, Key, Line, Outcome, Panel, PanelDims, - VerticalAlignment, Widget, + lctrl, Choice, Color, CornerRounding, EventCtx, HorizontalAlignment, Key, Line, Outcome, Panel, + PanelDims, VerticalAlignment, Widget, }; +use crate::components::Mode; use crate::{App, BrowseNeighbourhoods, Transition}; pub struct TopPanel; impl TopPanel { - pub fn panel(ctx: &mut EventCtx, app: &App) -> Panel { + pub fn panel(ctx: &mut EventCtx, app: &App, mode: Mode) -> Panel { let consultation = app.per_map.consultation.is_some(); + // While we're adjusting a boundary, it's weird to navigate away without explicitly + // confirming or reverting the edits. Just remove the nav bar entirely. + let navbar = if mode != Mode::SelectBoundary { + Widget::row(vec![ + ctx.style() + .btn_outline + .text("Pick area") + .disabled( + mode == Mode::BrowseNeighbourhoods || app.per_map.consultation.is_some(), + ) + .maybe_disabled_tooltip(if mode == Mode::BrowseNeighbourhoods { + None + } else { + Some("This consultation is only about the current area") + }) + .build_def(ctx), + ctx.style() + .btn_outline + .text("Design LTN") + .disabled( + mode == Mode::ModifyNeighbourhood + || app.per_map.current_neighbourhood.is_none(), + ) + .maybe_disabled_tooltip(if mode == Mode::ModifyNeighbourhood { + None + } else { + Some("Pick an area first") + }) + .build_def(ctx), + ctx.style() + .btn_outline + .text("Plan route") + .hotkey(Key::R) + .disabled(mode == Mode::RoutePlanner) + .build_def(ctx), + ctx.style() + .btn_outline + .text("Predict impact") + .disabled(mode == Mode::Impact || app.per_map.consultation.is_some()) + .maybe_disabled_tooltip(if mode == Mode::Impact { + None + } else { + Some("Not supported here yet") + }) + .build_def(ctx), + ]) + .centered_vert() + .padding(16) + .outline((5.0, Color::BLACK)) + } else { + Widget::nothing() + }; + Panel::new_builder( Widget::row(vec![ map_gui::tools::home_btn(ctx), @@ -33,6 +88,7 @@ impl TopPanel { map_gui::tools::change_map_btn(ctx, app) .centered_vert() .hide(consultation), + navbar, Widget::row(vec![ ctx.style() .btn_plain @@ -68,7 +124,7 @@ impl TopPanel { pub fn event Vec<&'static str>>( ctx: &mut EventCtx, - app: &App, + app: &mut App, panel: &mut Panel, help: F, ) -> Option { @@ -113,6 +169,18 @@ impl TopPanel { } })) } + "Pick area" => Some(Transition::Replace(BrowseNeighbourhoods::new_state( + ctx, app, + ))), + "Design LTN" => Some(Transition::Replace(crate::connectivity::Viewer::new_state( + ctx, + app, + app.per_map.current_neighbourhood.unwrap(), + ))), + "Plan route" => Some(Transition::Replace( + crate::route_planner::RoutePlanner::new_state(ctx, app), + )), + "Predict impact" => Some(launch_impact(ctx, app)), _ => unreachable!(), } } else { @@ -120,3 +188,25 @@ impl TopPanel { } } } + +fn launch_impact(ctx: &mut EventCtx, app: &mut App) -> Transition { + if &app.per_map.impact.map == app.per_map.map.get_name() + && app.per_map.impact.change_key == app.per_map.edits.get_change_key() + { + return Transition::Replace(crate::impact::ShowResults::new_state(ctx, app)); + } + + Transition::Push(ChooseSomething::new_state(ctx, + "Impact prediction is experimental. You have to interpret the results carefully. The app may also freeze while calculating this.", + Choice::strings(vec!["Never mind", "I understand the warnings. Predict impact!"]), + Box::new(|choice, ctx, app| { + if choice == "Never mind" { + Transition::Pop + } else { + Transition::Multi(vec![ + Transition::Pop, + Transition::Replace(crate::impact::ShowResults::new_state(ctx, app)), + ]) + } + }))) +} diff --git a/apps/ltn/src/connectivity.rs b/apps/ltn/src/connectivity.rs index f88fd5340e..333a855ca4 100644 --- a/apps/ltn/src/connectivity.rs +++ b/apps/ltn/src/connectivity.rs @@ -26,11 +26,17 @@ pub struct Viewer { } impl Viewer { - pub fn new_state(ctx: &mut EventCtx, app: &App, id: NeighbourhoodID) -> Box> { + pub fn new_state( + ctx: &mut EventCtx, + app: &mut App, + id: NeighbourhoodID, + ) -> Box> { + app.per_map.current_neighbourhood = Some(id); + let neighbourhood = Neighbourhood::new(ctx, app, id); let mut viewer = Viewer { - top_panel: crate::components::TopPanel::panel(ctx, app), + top_panel: crate::components::TopPanel::panel(ctx, app, Mode::ModifyNeighbourhood), left_panel: Panel::empty(ctx), neighbourhood, draw_top_layer: Drawable::empty(ctx), diff --git a/apps/ltn/src/edit/mod.rs b/apps/ltn/src/edit/mod.rs index ed6ba4f6d5..c1617eb607 100644 --- a/apps/ltn/src/edit/mod.rs +++ b/apps/ltn/src/edit/mod.rs @@ -17,8 +17,7 @@ use widgetry::{ }; use crate::{ - after_edit, colors, is_private, App, BrowseNeighbourhoods, FilterType, Neighbourhood, - RoadFilter, Transition, + after_edit, colors, is_private, App, FilterType, Neighbourhood, RoadFilter, Transition, }; pub enum EditMode { @@ -87,24 +86,18 @@ impl EditNeighbourhood { ) -> PanelBuilder { let contents = Widget::col(vec![ app.per_map.alt_proposals.to_widget(ctx, app), - BrowseNeighbourhoods::button(ctx, app), - { - let mut row = Vec::new(); - if app.per_map.consultation.is_none() { - row.push( - ctx.style() - .btn_outline - .text("Adjust boundary") - .hotkey(Key::B) - .build_def(ctx), - ); - } - row.push(crate::route_planner::RoutePlanner::button(ctx)); - Widget::row(row) - }, Line("Editing neighbourhood") .small_heading() .into_widget(ctx), + if app.per_map.consultation.is_none() { + ctx.style() + .btn_outline + .text("Adjust boundary") + .hotkey(Key::B) + .build_def(ctx) + } else { + Widget::nothing() + }, Widget::col(vec![ edit_mode(ctx, app), match app.session.edit_mode { @@ -170,12 +163,6 @@ impl EditNeighbourhood { ) -> EditOutcome { let id = neighbourhood.id; match action { - "Browse neighbourhoods" => { - // Recalculate the state to redraw any changed filters - EditOutcome::Transition(Transition::Replace(BrowseNeighbourhoods::new_state( - ctx, app, - ))) - } "Adjust boundary" => EditOutcome::Transition(Transition::Replace( crate::select_boundary::SelectBoundary::new_state(ctx, app, id), )), @@ -190,9 +177,6 @@ impl EditNeighbourhood { } EditOutcome::Transition(Transition::Recreate) } - "Plan a route" => EditOutcome::Transition(Transition::Push( - crate::route_planner::RoutePlanner::new_state(ctx, app), - )), "Modal filter - no entry" => { app.session.filter_type = FilterType::NoEntry; app.session.edit_mode = EditMode::Filters; diff --git a/apps/ltn/src/impact/ui.rs b/apps/ltn/src/impact/ui.rs index 1bbaa4515a..abf9a3ec5f 100644 --- a/apps/ltn/src/impact/ui.rs +++ b/apps/ltn/src/impact/ui.rs @@ -17,7 +17,7 @@ use widgetry::{ use crate::components::Mode; use crate::impact::{end_of_day, Filters, Impact}; -use crate::{colors, App, BrowseNeighbourhoods, Transition}; +use crate::{colors, App, Transition}; // TODO Share structure or pieces with Ungap's predict mode // ... can't we just produce data of a certain shape, and have a UI pretty tuned for that? @@ -69,7 +69,6 @@ impl ShowResults { } let contents = Widget::col(vec![ - BrowseNeighbourhoods::button(ctx, app), 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_all(vec![ @@ -79,7 +78,7 @@ impl ShowResults { Line(" roads have less. Width of the road shows how much baseline traffic it has."), ]).wrap_to_pct(ctx, 20).into_widget(ctx), Text::from(Line("Click a road to see changed routes through it.")).wrap_to_pct(ctx, 20).into_widget(ctx), - Text::from(Line("Results may be wrong for various reasons. Interpret carefully.")).wrap_to_pct(ctx, 20).into_widget(ctx), + Text::from(Line("Results may be wrong for various reasons. Interpret carefully.").bold_body()).wrap_to_pct(ctx, 20).into_widget(ctx), // TODO Dropdown for the scenario, and explain its source/limitations app.per_map.impact.filters.to_panel(ctx, app), app.per_map @@ -100,7 +99,7 @@ impl ShowResults { .text("Save before/after counts to files (GeoJSON)") .build_def(ctx), ]); - let top_panel = crate::components::TopPanel::panel(ctx, app); + let top_panel = crate::components::TopPanel::panel(ctx, app, Mode::Impact); let left_panel = crate::components::LeftPanel::builder(ctx, &top_panel, contents).build(ctx); @@ -120,11 +119,6 @@ impl State for ShowResults { } match self.left_panel.event(ctx) { Outcome::Clicked(x) => match x.as_ref() { - "Browse neighbourhoods" => { - // Don't just Pop; if we updated the results, the UI won't warn the user about a slow - // loading - return Transition::Replace(BrowseNeighbourhoods::new_state(ctx, app)); - } "Save before/after counts to files (JSON)" => { let path1 = "counts_a.json"; let path2 = "counts_b.json"; diff --git a/apps/ltn/src/route_planner.rs b/apps/ltn/src/route_planner.rs index c36435769e..0fc69fd008 100644 --- a/apps/ltn/src/route_planner.rs +++ b/apps/ltn/src/route_planner.rs @@ -6,12 +6,12 @@ use map_model::{PathV2, PathfinderCache}; use synthpop::{TripEndpoint, TripMode}; use widgetry::mapspace::World; use widgetry::{ - ButtonBuilder, Color, ControlState, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, + ButtonBuilder, Color, ControlState, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, RoundedF64, Spinner, State, Text, Toggle, Widget, }; use crate::components::Mode; -use crate::{colors, App, BrowseNeighbourhoods, Transition}; +use crate::{colors, App, Transition}; pub struct RoutePlanner { top_panel: Panel, @@ -58,7 +58,7 @@ impl RoutePlanner { } let mut rp = RoutePlanner { - top_panel: crate::components::TopPanel::panel(ctx, app), + top_panel: crate::components::TopPanel::panel(ctx, app, Mode::RoutePlanner), left_panel: Panel::empty(ctx), waypoints: InputWaypoints::new_max_2(app), files: TripManagement::new(app), @@ -76,14 +76,6 @@ impl RoutePlanner { 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 fn update_everything(&mut self, ctx: &mut EventCtx, app: &mut App) { self.files.autosave(app); @@ -91,12 +83,6 @@ impl RoutePlanner { let contents = Widget::col(vec![ app.per_map.alt_proposals.to_widget(ctx, app), - BrowseNeighbourhoods::button(ctx, app), - ctx.style() - .btn_back("Analyze neighbourhood") - .hotkey(Key::Escape) - .build_def(ctx) - .hide(app.per_map.consultation.is_none()), Line("Plan a route").small_heading().into_widget(ctx), Widget::col(vec![ self.files.get_panel_widget(ctx), @@ -334,12 +320,6 @@ impl State for RoutePlanner { let panel_outcome = self.left_panel.event(ctx); if let Outcome::Clicked(ref x) = panel_outcome { - if x == "Browse neighbourhoods" { - return Transition::Replace(BrowseNeighbourhoods::new_state(ctx, app)); - } - if x == "Analyze neighbourhood" { - return Transition::Pop; - } if let Some(t) = self.files.on_click(ctx, app, x) { // Bit hacky... if matches!(t, Transition::Keep) { diff --git a/apps/ltn/src/select_boundary.rs b/apps/ltn/src/select_boundary.rs index 2b92751cb9..2c76c94eb9 100644 --- a/apps/ltn/src/select_boundary.rs +++ b/apps/ltn/src/select_boundary.rs @@ -66,7 +66,7 @@ impl SelectBoundary { app.session.edit_mode = EditMode::Filters; } - let top_panel = crate::components::TopPanel::panel(ctx, app); + let top_panel = crate::components::TopPanel::panel(ctx, app, Mode::SelectBoundary); let left_panel = make_panel(ctx, app, id, &top_panel); let mut state = SelectBoundary { top_panel, diff --git a/widgetry/src/widgets/button.rs b/widgetry/src/widgets/button.rs index b8e8210130..6f038feb74 100644 --- a/widgetry/src/widgets/button.rs +++ b/widgetry/src/widgets/button.rs @@ -480,6 +480,12 @@ impl<'b, 'a: 'b, 'c> ButtonBuilder<'a, 'c> { self } + /// Like `disabled_tooltip`, but the tooltip may not exist. + pub fn maybe_disabled_tooltip(mut self, tooltip: Option>) -> Self { + self.disabled_tooltip = tooltip.map(|x| x.into()); + self + } + /// The button's items will be rendered in a vertical column /// /// If the button doesn't have both an image and label, this has no effect.