mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-24 15:02:59 +03:00
Prepare for editing one-ways by better hiding the API of editing filters.
This commit is contained in:
parent
df854db143
commit
2c7ebbcc13
@ -1,13 +1,13 @@
|
|||||||
use geom::{ArrowCap, Distance, PolyLine};
|
use geom::{ArrowCap, Distance, PolyLine};
|
||||||
use map_gui::tools::ColorNetwork;
|
use map_gui::tools::ColorNetwork;
|
||||||
use widgetry::mapspace::{ToggleZoomed, World};
|
use widgetry::mapspace::ToggleZoomed;
|
||||||
use widgetry::tools::PopupMsg;
|
use widgetry::tools::PopupMsg;
|
||||||
use widgetry::{
|
use widgetry::{
|
||||||
DrawBaselayer, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, TextExt, Toggle, Widget,
|
DrawBaselayer, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, TextExt, Toggle, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::edit::{EditNeighborhood, Tab};
|
||||||
use crate::filters::auto::Heuristic;
|
use crate::filters::auto::Heuristic;
|
||||||
use crate::per_neighborhood::{FilterableObj, Tab};
|
|
||||||
use crate::shortcuts::find_shortcuts;
|
use crate::shortcuts::find_shortcuts;
|
||||||
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ pub struct Viewer {
|
|||||||
top_panel: Panel,
|
top_panel: Panel,
|
||||||
left_panel: Panel,
|
left_panel: Panel,
|
||||||
neighborhood: Neighborhood,
|
neighborhood: Neighborhood,
|
||||||
world: World<FilterableObj>,
|
|
||||||
draw_top_layer: ToggleZoomed,
|
draw_top_layer: ToggleZoomed,
|
||||||
|
edit: EditNeighborhood,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Viewer {
|
impl Viewer {
|
||||||
@ -27,8 +27,8 @@ impl Viewer {
|
|||||||
top_panel: crate::components::TopPanel::panel(ctx, app),
|
top_panel: crate::components::TopPanel::panel(ctx, app),
|
||||||
left_panel: Panel::empty(ctx),
|
left_panel: Panel::empty(ctx),
|
||||||
neighborhood,
|
neighborhood,
|
||||||
world: World::unbounded(),
|
|
||||||
draw_top_layer: ToggleZoomed::empty(ctx),
|
draw_top_layer: ToggleZoomed::empty(ctx),
|
||||||
|
edit: EditNeighborhood::temporary(),
|
||||||
};
|
};
|
||||||
viewer.update(ctx, app);
|
viewer.update(ctx, app);
|
||||||
Box::new(viewer)
|
Box::new(viewer)
|
||||||
@ -47,10 +47,12 @@ impl Viewer {
|
|||||||
format!("{} cells are totally disconnected", disconnected_cells)
|
format!("{} cells are totally disconnected", disconnected_cells)
|
||||||
};
|
};
|
||||||
|
|
||||||
self.left_panel = Tab::Connectivity
|
self.left_panel = self
|
||||||
|
.edit
|
||||||
.panel_builder(
|
.panel_builder(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
|
Tab::Connectivity,
|
||||||
&self.top_panel,
|
&self.top_panel,
|
||||||
Widget::col(vec![
|
Widget::col(vec![
|
||||||
format!(
|
format!(
|
||||||
@ -66,8 +68,8 @@ impl Viewer {
|
|||||||
)
|
)
|
||||||
.build(ctx);
|
.build(ctx);
|
||||||
|
|
||||||
let (world, draw_top_layer) = make_world(ctx, app, &self.neighborhood);
|
let (edit, draw_top_layer) = setup_editing(ctx, app, &self.neighborhood);
|
||||||
self.world = world;
|
self.edit = edit;
|
||||||
self.draw_top_layer = draw_top_layer;
|
self.draw_top_layer = draw_top_layer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +100,7 @@ impl State<App> for Viewer {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(t) = crate::per_neighborhood::handle_action(
|
} else if let Some(t) = self.edit.handle_panel_action(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
x.as_ref(),
|
x.as_ref(),
|
||||||
@ -131,16 +133,15 @@ impl State<App> for Viewer {
|
|||||||
app.session.heuristic = self.left_panel.dropdown_value("heuristic");
|
app.session.heuristic = self.left_panel.dropdown_value("heuristic");
|
||||||
|
|
||||||
if x != "heuristic" {
|
if x != "heuristic" {
|
||||||
let (world, draw_top_layer) = make_world(ctx, app, &self.neighborhood);
|
let (edit, draw_top_layer) = setup_editing(ctx, app, &self.neighborhood);
|
||||||
self.world = world;
|
self.edit = edit;
|
||||||
self.draw_top_layer = draw_top_layer;
|
self.draw_top_layer = draw_top_layer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let world_outcome = self.world.event(ctx);
|
if self.edit.event(ctx, app) {
|
||||||
if crate::per_neighborhood::handle_world_outcome(ctx, app, world_outcome) {
|
|
||||||
self.neighborhood = Neighborhood::new(ctx, app, self.neighborhood.id);
|
self.neighborhood = Neighborhood::new(ctx, app, self.neighborhood.id);
|
||||||
self.update(ctx, app);
|
self.update(ctx, app);
|
||||||
}
|
}
|
||||||
@ -153,7 +154,7 @@ impl State<App> for Viewer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, g: &mut GfxCtx, app: &App) {
|
fn draw(&self, g: &mut GfxCtx, app: &App) {
|
||||||
crate::draw_with_layering(g, app, |g| self.world.draw(g));
|
crate::draw_with_layering(g, app, |g| self.edit.world.draw(g));
|
||||||
g.redraw(&self.neighborhood.fade_irrelevant);
|
g.redraw(&self.neighborhood.fade_irrelevant);
|
||||||
self.draw_top_layer.draw(g);
|
self.draw_top_layer.draw(g);
|
||||||
|
|
||||||
@ -173,16 +174,16 @@ impl State<App> for Viewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_world(
|
fn setup_editing(
|
||||||
ctx: &mut EventCtx,
|
ctx: &mut EventCtx,
|
||||||
app: &App,
|
app: &App,
|
||||||
neighborhood: &Neighborhood,
|
neighborhood: &Neighborhood,
|
||||||
) -> (World<FilterableObj>, ToggleZoomed) {
|
) -> (EditNeighborhood, ToggleZoomed) {
|
||||||
let shortcuts = ctx.loading_screen("find shortcuts", |_, timer| {
|
let shortcuts = ctx.loading_screen("find shortcuts", |_, timer| {
|
||||||
find_shortcuts(app, neighborhood, timer)
|
find_shortcuts(app, neighborhood, timer)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut world = crate::per_neighborhood::make_world(ctx, app, neighborhood, &shortcuts);
|
let mut edit = EditNeighborhood::new(ctx, app, neighborhood, &shortcuts);
|
||||||
let map = &app.map;
|
let map = &app.map;
|
||||||
|
|
||||||
// The world is drawn in between areas and roads, but some things need to be drawn on top of
|
// The world is drawn in between areas and roads, but some things need to be drawn on top of
|
||||||
@ -191,7 +192,7 @@ fn make_world(
|
|||||||
|
|
||||||
let render_cells = crate::draw_cells::RenderCells::new(map, neighborhood);
|
let render_cells = crate::draw_cells::RenderCells::new(map, neighborhood);
|
||||||
if app.session.draw_cells_as_areas {
|
if app.session.draw_cells_as_areas {
|
||||||
world.draw_master_batch(ctx, render_cells.draw());
|
edit.world.draw_master_batch(ctx, render_cells.draw());
|
||||||
|
|
||||||
let mut colorer = ColorNetwork::no_fading(app);
|
let mut colorer = ColorNetwork::no_fading(app);
|
||||||
colorer.ranked_roads(shortcuts.count_per_road.clone(), &app.cs.good_to_bad_red);
|
colorer.ranked_roads(shortcuts.count_per_road.clone(), &app.cs.good_to_bad_red);
|
||||||
@ -301,7 +302,7 @@ fn make_world(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(world, draw_top_layer.build(ctx))
|
(edit, draw_top_layer.build(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help() -> Vec<&'static str> {
|
fn help() -> Vec<&'static str> {
|
||||||
|
131
apps/ltn/src/edit/filters.rs
Normal file
131
apps/ltn/src/edit/filters.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
use geom::Distance;
|
||||||
|
use map_model::PathConstraints;
|
||||||
|
use widgetry::mapspace::{World, WorldOutcome};
|
||||||
|
use widgetry::tools::open_browser;
|
||||||
|
use widgetry::{lctrl, EventCtx, Image, Key, Line, Text, TextExt, Widget};
|
||||||
|
|
||||||
|
use super::Obj;
|
||||||
|
use crate::shortcuts::Shortcuts;
|
||||||
|
use crate::{after_edit, colors, App, DiagonalFilter, Neighborhood};
|
||||||
|
|
||||||
|
pub fn widget(ctx: &mut EventCtx, app: &App) -> Widget {
|
||||||
|
Widget::col(vec![
|
||||||
|
Widget::row(vec![
|
||||||
|
Image::from_path("system/assets/tools/pencil.svg")
|
||||||
|
.into_widget(ctx)
|
||||||
|
.centered_vert(),
|
||||||
|
Text::from(Line(
|
||||||
|
"Click a road or intersection to add or remove a modal filter",
|
||||||
|
))
|
||||||
|
.wrap_to_pct(ctx, 15)
|
||||||
|
.into_widget(ctx),
|
||||||
|
]),
|
||||||
|
crate::components::FreehandFilters::button(ctx),
|
||||||
|
Widget::row(vec![
|
||||||
|
format!(
|
||||||
|
"{} filters added",
|
||||||
|
app.session.modal_filters.roads.len()
|
||||||
|
+ app.session.modal_filters.intersections.len()
|
||||||
|
)
|
||||||
|
.text_widget(ctx)
|
||||||
|
.centered_vert(),
|
||||||
|
ctx.style()
|
||||||
|
.btn_plain
|
||||||
|
.icon("system/assets/tools/undo.svg")
|
||||||
|
.disabled(app.session.modal_filters.previous_version.is_none())
|
||||||
|
.hotkey(lctrl(Key::Z))
|
||||||
|
.build_widget(ctx, "undo"),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates clickable objects for managing filters on roads and intersections. Everything is
|
||||||
|
/// invisible; the caller is responsible for drawing things.
|
||||||
|
pub fn make_world(
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &App,
|
||||||
|
neighborhood: &Neighborhood,
|
||||||
|
shortcuts: &Shortcuts,
|
||||||
|
) -> World<Obj> {
|
||||||
|
let map = &app.map;
|
||||||
|
let mut world = World::bounded(map.get_bounds());
|
||||||
|
|
||||||
|
for r in &neighborhood.orig_perimeter.interior {
|
||||||
|
let road = map.get_r(*r);
|
||||||
|
world
|
||||||
|
.add(Obj::InteriorRoad(*r))
|
||||||
|
.hitbox(road.get_thick_polygon())
|
||||||
|
.drawn_in_master_batch()
|
||||||
|
.hover_outline(colors::OUTLINE, Distance::meters(5.0))
|
||||||
|
.tooltip(Text::from(format!(
|
||||||
|
"{} shortcuts cross {}",
|
||||||
|
shortcuts.count_per_road.get(*r),
|
||||||
|
road.get_name(app.opts.language.as_ref()),
|
||||||
|
)))
|
||||||
|
.hotkey(lctrl(Key::D), "debug")
|
||||||
|
.clickable()
|
||||||
|
.build(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in &neighborhood.interior_intersections {
|
||||||
|
world
|
||||||
|
.add(Obj::InteriorIntersection(*i))
|
||||||
|
.hitbox(map.get_i(*i).polygon.clone())
|
||||||
|
.drawn_in_master_batch()
|
||||||
|
.hover_outline(colors::OUTLINE, Distance::meters(5.0))
|
||||||
|
.tooltip(Text::from(format!(
|
||||||
|
"{} shortcuts cross this intersection",
|
||||||
|
shortcuts.count_per_intersection.get(*i)
|
||||||
|
)))
|
||||||
|
.clickable()
|
||||||
|
.hotkey(lctrl(Key::D), "debug")
|
||||||
|
.build(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
world.initialize_hover(ctx);
|
||||||
|
world
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_world_outcome(ctx: &mut EventCtx, app: &mut App, outcome: WorldOutcome<Obj>) -> bool {
|
||||||
|
let map = &app.map;
|
||||||
|
match outcome {
|
||||||
|
WorldOutcome::ClickedObject(Obj::InteriorRoad(r)) => {
|
||||||
|
let road = map.get_r(r);
|
||||||
|
// Filtering a road that's already marked bike-only doesn't make sense. Likewise for
|
||||||
|
// one-ways.
|
||||||
|
if !PathConstraints::Car.can_use_road(road, map) || road.is_oneway() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.session.modal_filters.before_edit();
|
||||||
|
if app.session.modal_filters.roads.remove(&r).is_none() {
|
||||||
|
// Place the filter on the part of the road that was clicked
|
||||||
|
// These calls shouldn't fail -- since we clicked a road, the cursor must be in
|
||||||
|
// map-space. And project_pt returns a point that's guaranteed to be on the
|
||||||
|
// polyline.
|
||||||
|
let cursor_pt = ctx.canvas.get_cursor_in_map_space().unwrap();
|
||||||
|
let pt_on_line = road.center_pts.project_pt(cursor_pt);
|
||||||
|
let (distance, _) = road.center_pts.dist_along_of_point(pt_on_line).unwrap();
|
||||||
|
|
||||||
|
app.session.modal_filters.roads.insert(r, distance);
|
||||||
|
}
|
||||||
|
after_edit(ctx, app);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
WorldOutcome::ClickedObject(Obj::InteriorIntersection(i)) => {
|
||||||
|
app.session.modal_filters.before_edit();
|
||||||
|
DiagonalFilter::cycle_through_alternatives(app, i);
|
||||||
|
after_edit(ctx, app);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
WorldOutcome::Keypress("debug", Obj::InteriorIntersection(i)) => {
|
||||||
|
open_browser(app.map.get_i(i).orig_id.to_string());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
WorldOutcome::Keypress("debug", Obj::InteriorRoad(r)) => {
|
||||||
|
open_browser(app.map.get_r(r).orig_id.osm_way_id.to_string());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
181
apps/ltn/src/edit/mod.rs
Normal file
181
apps/ltn/src/edit/mod.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
mod filters;
|
||||||
|
|
||||||
|
use map_model::{IntersectionID, RoadID};
|
||||||
|
use widgetry::mapspace::{ObjectID, World};
|
||||||
|
use widgetry::{EventCtx, Key, Line, Panel, PanelBuilder, Widget, DEFAULT_CORNER_RADIUS};
|
||||||
|
|
||||||
|
use crate::shortcuts::Shortcuts;
|
||||||
|
use crate::{after_edit, App, BrowseNeighborhoods, Neighborhood, Transition};
|
||||||
|
|
||||||
|
// TODO This is only used for styling now
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum Tab {
|
||||||
|
Connectivity,
|
||||||
|
Shortcuts,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab {
|
||||||
|
fn make_buttons(self, ctx: &mut EventCtx, app: &App) -> Widget {
|
||||||
|
let mut row = Vec::new();
|
||||||
|
for (tab, label, key) in [
|
||||||
|
(Tab::Connectivity, "Connectivity", Key::F1),
|
||||||
|
(Tab::Shortcuts, "Shortcuts", Key::F2),
|
||||||
|
] {
|
||||||
|
// TODO Match the TabController styling
|
||||||
|
row.push(
|
||||||
|
ctx.style()
|
||||||
|
.btn_tab
|
||||||
|
.text(label)
|
||||||
|
.corner_rounding(geom::CornerRadii {
|
||||||
|
top_left: DEFAULT_CORNER_RADIUS,
|
||||||
|
top_right: DEFAULT_CORNER_RADIUS,
|
||||||
|
bottom_left: 0.0,
|
||||||
|
bottom_right: 0.0,
|
||||||
|
})
|
||||||
|
.hotkey(key)
|
||||||
|
// We abuse "disabled" to denote "currently selected"
|
||||||
|
.disabled(self == tab)
|
||||||
|
.build_def(ctx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if app.session.consultation.is_none() {
|
||||||
|
// TODO The 3rd doesn't really act like a tab
|
||||||
|
row.push(
|
||||||
|
ctx.style()
|
||||||
|
.btn_tab
|
||||||
|
.text("Adjust boundary")
|
||||||
|
.corner_rounding(geom::CornerRadii {
|
||||||
|
top_left: DEFAULT_CORNER_RADIUS,
|
||||||
|
top_right: DEFAULT_CORNER_RADIUS,
|
||||||
|
bottom_left: 0.0,
|
||||||
|
bottom_right: 0.0,
|
||||||
|
})
|
||||||
|
.hotkey(Key::B)
|
||||||
|
.build_def(ctx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget::row(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditNeighborhood {
|
||||||
|
// Only pub for drawing
|
||||||
|
pub world: World<Obj>,
|
||||||
|
// True if we're editing filters, false if we're editing one-ways. (An enum is overkill)
|
||||||
|
filters: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Obj {
|
||||||
|
InteriorRoad(RoadID),
|
||||||
|
InteriorIntersection(IntersectionID),
|
||||||
|
}
|
||||||
|
impl ObjectID for Obj {}
|
||||||
|
|
||||||
|
impl EditNeighborhood {
|
||||||
|
pub fn temporary() -> Self {
|
||||||
|
Self {
|
||||||
|
world: World::unbounded(),
|
||||||
|
filters: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO filters!
|
||||||
|
pub fn new(
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &App,
|
||||||
|
neighborhood: &Neighborhood,
|
||||||
|
shortcuts: &Shortcuts,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
world: filters::make_world(ctx, app, neighborhood, shortcuts),
|
||||||
|
filters: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn panel_builder(
|
||||||
|
&self,
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &App,
|
||||||
|
tab: Tab,
|
||||||
|
top_panel: &Panel,
|
||||||
|
per_tab_contents: Widget,
|
||||||
|
) -> PanelBuilder {
|
||||||
|
let contents = Widget::col(vec![
|
||||||
|
app.session.alt_proposals.to_widget(ctx, app),
|
||||||
|
BrowseNeighborhoods::button(ctx, app),
|
||||||
|
Line("Editing neighborhood")
|
||||||
|
.small_heading()
|
||||||
|
.into_widget(ctx),
|
||||||
|
if self.filters {
|
||||||
|
filters::widget(ctx, app)
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
.section(ctx),
|
||||||
|
tab.make_buttons(ctx, app),
|
||||||
|
per_tab_contents,
|
||||||
|
crate::route_planner::RoutePlanner::button(ctx),
|
||||||
|
]);
|
||||||
|
crate::components::LeftPanel::builder(ctx, top_panel, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If true, the neighborhood has changed and the caller should recalculate stuff, including
|
||||||
|
/// the panel
|
||||||
|
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> bool {
|
||||||
|
let outcome = self.world.event(ctx);
|
||||||
|
if self.filters {
|
||||||
|
filters::handle_world_outcome(ctx, app, outcome)
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_panel_action(
|
||||||
|
&self,
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &mut App,
|
||||||
|
action: &str,
|
||||||
|
neighborhood: &Neighborhood,
|
||||||
|
panel: &Panel,
|
||||||
|
) -> Option<Transition> {
|
||||||
|
let id = neighborhood.id;
|
||||||
|
match action {
|
||||||
|
"Browse neighborhoods" => {
|
||||||
|
// Recalculate the state to redraw any changed filters
|
||||||
|
Some(Transition::Replace(BrowseNeighborhoods::new_state(
|
||||||
|
ctx, app,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
"Adjust boundary" => Some(Transition::Replace(
|
||||||
|
crate::select_boundary::SelectBoundary::new_state(ctx, app, id),
|
||||||
|
)),
|
||||||
|
"Connectivity" => Some(Transition::Replace(crate::connectivity::Viewer::new_state(
|
||||||
|
ctx, app, id,
|
||||||
|
))),
|
||||||
|
"Shortcuts" => Some(Transition::Replace(
|
||||||
|
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, id, None),
|
||||||
|
)),
|
||||||
|
// Overkill to force all mode-specific code into the module
|
||||||
|
"Create filters along a shape" => Some(Transition::Push(
|
||||||
|
crate::components::FreehandFilters::new_state(
|
||||||
|
ctx,
|
||||||
|
neighborhood,
|
||||||
|
panel.center_of("Create filters along a shape"),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
"undo" => {
|
||||||
|
let prev = app.session.modal_filters.previous_version.take().unwrap();
|
||||||
|
app.session.modal_filters = prev;
|
||||||
|
after_edit(ctx, app);
|
||||||
|
// TODO Ideally, preserve panel state (checkboxes and dropdowns)
|
||||||
|
Some(Transition::Recreate)
|
||||||
|
}
|
||||||
|
"Plan a route" => Some(Transition::Push(
|
||||||
|
crate::route_planner::RoutePlanner::new_state(ctx, app),
|
||||||
|
)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,12 +22,12 @@ mod colors;
|
|||||||
mod components;
|
mod components;
|
||||||
mod connectivity;
|
mod connectivity;
|
||||||
mod draw_cells;
|
mod draw_cells;
|
||||||
|
mod edit;
|
||||||
mod export;
|
mod export;
|
||||||
mod filters;
|
mod filters;
|
||||||
mod impact;
|
mod impact;
|
||||||
mod neighborhood;
|
mod neighborhood;
|
||||||
mod partition;
|
mod partition;
|
||||||
mod per_neighborhood;
|
|
||||||
mod route_planner;
|
mod route_planner;
|
||||||
mod save;
|
mod save;
|
||||||
mod select_boundary;
|
mod select_boundary;
|
||||||
|
@ -1,263 +0,0 @@
|
|||||||
use geom::Distance;
|
|
||||||
use map_model::{IntersectionID, PathConstraints, RoadID};
|
|
||||||
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
|
|
||||||
use widgetry::tools::open_browser;
|
|
||||||
use widgetry::{
|
|
||||||
lctrl, EventCtx, Image, Key, Line, Panel, PanelBuilder, Text, TextExt, Widget,
|
|
||||||
DEFAULT_CORNER_RADIUS,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::shortcuts::Shortcuts;
|
|
||||||
use crate::{
|
|
||||||
after_edit, colors, App, BrowseNeighborhoods, DiagonalFilter, Neighborhood, Transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO This is only used for styling now
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum Tab {
|
|
||||||
Connectivity,
|
|
||||||
Shortcuts,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tab {
|
|
||||||
pub fn panel_builder(
|
|
||||||
self,
|
|
||||||
ctx: &mut EventCtx,
|
|
||||||
app: &App,
|
|
||||||
top_panel: &Panel,
|
|
||||||
per_tab_contents: Widget,
|
|
||||||
) -> PanelBuilder {
|
|
||||||
let contents = Widget::col(vec![
|
|
||||||
app.session.alt_proposals.to_widget(ctx, app),
|
|
||||||
BrowseNeighborhoods::button(ctx, app),
|
|
||||||
Line("Editing neighborhood")
|
|
||||||
.small_heading()
|
|
||||||
.into_widget(ctx),
|
|
||||||
Widget::col(vec![
|
|
||||||
Widget::row(vec![
|
|
||||||
Image::from_path("system/assets/tools/pencil.svg")
|
|
||||||
.into_widget(ctx)
|
|
||||||
.centered_vert(),
|
|
||||||
Text::from(Line(
|
|
||||||
"Click a road or intersection to add or remove a modal filter",
|
|
||||||
))
|
|
||||||
.wrap_to_pct(ctx, 15)
|
|
||||||
.into_widget(ctx),
|
|
||||||
]),
|
|
||||||
crate::components::FreehandFilters::button(ctx),
|
|
||||||
Widget::row(vec![
|
|
||||||
format!(
|
|
||||||
"{} filters added",
|
|
||||||
app.session.modal_filters.roads.len()
|
|
||||||
+ app.session.modal_filters.intersections.len()
|
|
||||||
)
|
|
||||||
.text_widget(ctx)
|
|
||||||
.centered_vert(),
|
|
||||||
ctx.style()
|
|
||||||
.btn_plain
|
|
||||||
.icon("system/assets/tools/undo.svg")
|
|
||||||
.disabled(app.session.modal_filters.previous_version.is_none())
|
|
||||||
.hotkey(lctrl(Key::Z))
|
|
||||||
.build_widget(ctx, "undo"),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
.section(ctx),
|
|
||||||
self.make_buttons(ctx, app),
|
|
||||||
per_tab_contents,
|
|
||||||
crate::route_planner::RoutePlanner::button(ctx),
|
|
||||||
]);
|
|
||||||
crate::components::LeftPanel::builder(ctx, top_panel, contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_buttons(self, ctx: &mut EventCtx, app: &App) -> Widget {
|
|
||||||
let mut row = Vec::new();
|
|
||||||
for (tab, label, key) in [
|
|
||||||
(Tab::Connectivity, "Connectivity", Key::F1),
|
|
||||||
(Tab::Shortcuts, "Shortcuts", Key::F2),
|
|
||||||
] {
|
|
||||||
// TODO Match the TabController styling
|
|
||||||
row.push(
|
|
||||||
ctx.style()
|
|
||||||
.btn_tab
|
|
||||||
.text(label)
|
|
||||||
.corner_rounding(geom::CornerRadii {
|
|
||||||
top_left: DEFAULT_CORNER_RADIUS,
|
|
||||||
top_right: DEFAULT_CORNER_RADIUS,
|
|
||||||
bottom_left: 0.0,
|
|
||||||
bottom_right: 0.0,
|
|
||||||
})
|
|
||||||
.hotkey(key)
|
|
||||||
// We abuse "disabled" to denote "currently selected"
|
|
||||||
.disabled(self == tab)
|
|
||||||
.build_def(ctx),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if app.session.consultation.is_none() {
|
|
||||||
// TODO The 3rd doesn't really act like a tab
|
|
||||||
row.push(
|
|
||||||
ctx.style()
|
|
||||||
.btn_tab
|
|
||||||
.text("Adjust boundary")
|
|
||||||
.corner_rounding(geom::CornerRadii {
|
|
||||||
top_left: DEFAULT_CORNER_RADIUS,
|
|
||||||
top_right: DEFAULT_CORNER_RADIUS,
|
|
||||||
bottom_left: 0.0,
|
|
||||||
bottom_right: 0.0,
|
|
||||||
})
|
|
||||||
.hotkey(Key::B)
|
|
||||||
.build_def(ctx),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget::row(row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_action(
|
|
||||||
ctx: &mut EventCtx,
|
|
||||||
app: &mut App,
|
|
||||||
action: &str,
|
|
||||||
neighborhood: &Neighborhood,
|
|
||||||
panel: &Panel,
|
|
||||||
) -> Option<Transition> {
|
|
||||||
let id = neighborhood.id;
|
|
||||||
match action {
|
|
||||||
"Browse neighborhoods" => {
|
|
||||||
// Recalculate the state to redraw any changed filters
|
|
||||||
Some(Transition::Replace(BrowseNeighborhoods::new_state(
|
|
||||||
ctx, app,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
"Adjust boundary" => Some(Transition::Replace(
|
|
||||||
crate::select_boundary::SelectBoundary::new_state(ctx, app, id),
|
|
||||||
)),
|
|
||||||
"Connectivity" => Some(Transition::Replace(crate::connectivity::Viewer::new_state(
|
|
||||||
ctx, app, id,
|
|
||||||
))),
|
|
||||||
"Shortcuts" => Some(Transition::Replace(
|
|
||||||
crate::shortcut_viewer::BrowseShortcuts::new_state(ctx, app, id, None),
|
|
||||||
)),
|
|
||||||
"Create filters along a shape" => Some(Transition::Push(
|
|
||||||
crate::components::FreehandFilters::new_state(
|
|
||||||
ctx,
|
|
||||||
neighborhood,
|
|
||||||
panel.center_of("Create filters along a shape"),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
"undo" => {
|
|
||||||
let prev = app.session.modal_filters.previous_version.take().unwrap();
|
|
||||||
app.session.modal_filters = prev;
|
|
||||||
after_edit(ctx, app);
|
|
||||||
// TODO Ideally, preserve panel state (checkboxes and dropdowns)
|
|
||||||
Some(Transition::Recreate)
|
|
||||||
}
|
|
||||||
"Plan a route" => Some(Transition::Push(
|
|
||||||
crate::route_planner::RoutePlanner::new_state(ctx, app),
|
|
||||||
)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum FilterableObj {
|
|
||||||
InteriorRoad(RoadID),
|
|
||||||
InteriorIntersection(IntersectionID),
|
|
||||||
}
|
|
||||||
impl ObjectID for FilterableObj {}
|
|
||||||
|
|
||||||
/// Creates clickable objects for managing filters on roads and intersections. Everything is
|
|
||||||
/// invisible; the caller is responsible for drawing things.
|
|
||||||
pub fn make_world(
|
|
||||||
ctx: &mut EventCtx,
|
|
||||||
app: &App,
|
|
||||||
neighborhood: &Neighborhood,
|
|
||||||
shortcuts: &Shortcuts,
|
|
||||||
) -> World<FilterableObj> {
|
|
||||||
let map = &app.map;
|
|
||||||
let mut world = World::bounded(map.get_bounds());
|
|
||||||
|
|
||||||
for r in &neighborhood.orig_perimeter.interior {
|
|
||||||
let road = map.get_r(*r);
|
|
||||||
world
|
|
||||||
.add(FilterableObj::InteriorRoad(*r))
|
|
||||||
.hitbox(road.get_thick_polygon())
|
|
||||||
.drawn_in_master_batch()
|
|
||||||
.hover_outline(colors::OUTLINE, Distance::meters(5.0))
|
|
||||||
.tooltip(Text::from(format!(
|
|
||||||
"{} shortcuts cross {}",
|
|
||||||
shortcuts.count_per_road.get(*r),
|
|
||||||
road.get_name(app.opts.language.as_ref()),
|
|
||||||
)))
|
|
||||||
.hotkey(lctrl(Key::D), "debug")
|
|
||||||
.clickable()
|
|
||||||
.build(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in &neighborhood.interior_intersections {
|
|
||||||
world
|
|
||||||
.add(FilterableObj::InteriorIntersection(*i))
|
|
||||||
.hitbox(map.get_i(*i).polygon.clone())
|
|
||||||
.drawn_in_master_batch()
|
|
||||||
.hover_outline(colors::OUTLINE, Distance::meters(5.0))
|
|
||||||
.tooltip(Text::from(format!(
|
|
||||||
"{} shortcuts cross this intersection",
|
|
||||||
shortcuts.count_per_intersection.get(*i)
|
|
||||||
)))
|
|
||||||
.clickable()
|
|
||||||
.hotkey(lctrl(Key::D), "debug")
|
|
||||||
.build(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
world.initialize_hover(ctx);
|
|
||||||
world
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If true, the neighborhood has changed and the caller should recalculate stuff, including the
|
|
||||||
/// panel
|
|
||||||
pub fn handle_world_outcome(
|
|
||||||
ctx: &mut EventCtx,
|
|
||||||
app: &mut App,
|
|
||||||
outcome: WorldOutcome<FilterableObj>,
|
|
||||||
) -> bool {
|
|
||||||
let map = &app.map;
|
|
||||||
match outcome {
|
|
||||||
WorldOutcome::ClickedObject(FilterableObj::InteriorRoad(r)) => {
|
|
||||||
let road = map.get_r(r);
|
|
||||||
// Filtering a road that's already marked bike-only doesn't make sense. Likewise for
|
|
||||||
// one-ways.
|
|
||||||
if !PathConstraints::Car.can_use_road(road, map) || road.is_oneway() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.session.modal_filters.before_edit();
|
|
||||||
if app.session.modal_filters.roads.remove(&r).is_none() {
|
|
||||||
// Place the filter on the part of the road that was clicked
|
|
||||||
// These calls shouldn't fail -- since we clicked a road, the cursor must be in
|
|
||||||
// map-space. And project_pt returns a point that's guaranteed to be on the
|
|
||||||
// polyline.
|
|
||||||
let cursor_pt = ctx.canvas.get_cursor_in_map_space().unwrap();
|
|
||||||
let pt_on_line = road.center_pts.project_pt(cursor_pt);
|
|
||||||
let (distance, _) = road.center_pts.dist_along_of_point(pt_on_line).unwrap();
|
|
||||||
|
|
||||||
app.session.modal_filters.roads.insert(r, distance);
|
|
||||||
}
|
|
||||||
after_edit(ctx, app);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
WorldOutcome::ClickedObject(FilterableObj::InteriorIntersection(i)) => {
|
|
||||||
app.session.modal_filters.before_edit();
|
|
||||||
DiagonalFilter::cycle_through_alternatives(app, i);
|
|
||||||
after_edit(ctx, app);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
WorldOutcome::Keypress("debug", FilterableObj::InteriorIntersection(i)) => {
|
|
||||||
open_browser(app.map.get_i(i).orig_id.to_string());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
WorldOutcome::Keypress("debug", FilterableObj::InteriorRoad(r)) => {
|
|
||||||
open_browser(app.map.get_r(r).orig_id.osm_way_id.to_string());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
use map_gui::tools::percentage_bar;
|
use map_gui::tools::percentage_bar;
|
||||||
use map_model::{PathRequest, NORMAL_LANE_THICKNESS};
|
use map_model::{PathRequest, NORMAL_LANE_THICKNESS};
|
||||||
use widgetry::mapspace::{ToggleZoomed, World};
|
use widgetry::mapspace::ToggleZoomed;
|
||||||
use widgetry::{EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, Text, TextExt, Widget};
|
use widgetry::{EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, Text, TextExt, Widget};
|
||||||
|
|
||||||
use crate::per_neighborhood::{FilterableObj, Tab};
|
use crate::edit::{EditNeighborhood, Tab};
|
||||||
use crate::shortcuts::{find_shortcuts, Shortcuts};
|
use crate::shortcuts::{find_shortcuts, Shortcuts};
|
||||||
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ pub struct BrowseShortcuts {
|
|||||||
current_idx: usize,
|
current_idx: usize,
|
||||||
|
|
||||||
draw_path: ToggleZoomed,
|
draw_path: ToggleZoomed,
|
||||||
world: World<FilterableObj>,
|
edit: EditNeighborhood,
|
||||||
neighborhood: Neighborhood,
|
neighborhood: Neighborhood,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ impl BrowseShortcuts {
|
|||||||
let shortcuts = ctx.loading_screen("find shortcuts", |_, timer| {
|
let shortcuts = ctx.loading_screen("find shortcuts", |_, timer| {
|
||||||
find_shortcuts(app, &neighborhood, timer)
|
find_shortcuts(app, &neighborhood, timer)
|
||||||
});
|
});
|
||||||
let world = crate::per_neighborhood::make_world(ctx, app, &neighborhood, &shortcuts);
|
let edit = EditNeighborhood::new(ctx, app, &neighborhood, &shortcuts);
|
||||||
|
|
||||||
let mut state = BrowseShortcuts {
|
let mut state = BrowseShortcuts {
|
||||||
top_panel: crate::components::TopPanel::panel(ctx, app),
|
top_panel: crate::components::TopPanel::panel(ctx, app),
|
||||||
@ -39,7 +39,7 @@ impl BrowseShortcuts {
|
|||||||
current_idx: 0,
|
current_idx: 0,
|
||||||
draw_path: ToggleZoomed::empty(ctx),
|
draw_path: ToggleZoomed::empty(ctx),
|
||||||
neighborhood,
|
neighborhood,
|
||||||
world,
|
edit,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(req) = start_with_request {
|
if let Some(req) = start_with_request {
|
||||||
@ -63,10 +63,12 @@ impl BrowseShortcuts {
|
|||||||
self.shortcuts.quiet_and_total_streets(&self.neighborhood);
|
self.shortcuts.quiet_and_total_streets(&self.neighborhood);
|
||||||
|
|
||||||
if self.shortcuts.paths.is_empty() {
|
if self.shortcuts.paths.is_empty() {
|
||||||
self.left_panel = Tab::Shortcuts
|
self.left_panel = self
|
||||||
|
.edit
|
||||||
.panel_builder(
|
.panel_builder(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
|
Tab::Shortcuts,
|
||||||
&self.top_panel,
|
&self.top_panel,
|
||||||
percentage_bar(
|
percentage_bar(
|
||||||
ctx,
|
ctx,
|
||||||
@ -86,10 +88,12 @@ impl BrowseShortcuts {
|
|||||||
let controls = self.prev_next_controls(ctx);
|
let controls = self.prev_next_controls(ctx);
|
||||||
self.left_panel.replace(ctx, "prev/next controls", controls);
|
self.left_panel.replace(ctx, "prev/next controls", controls);
|
||||||
} else {
|
} else {
|
||||||
self.left_panel = Tab::Shortcuts
|
self.left_panel = self
|
||||||
|
.edit
|
||||||
.panel_builder(
|
.panel_builder(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
|
Tab::Shortcuts,
|
||||||
&self.top_panel,
|
&self.top_panel,
|
||||||
Widget::col(vec![
|
Widget::col(vec![
|
||||||
percentage_bar(
|
percentage_bar(
|
||||||
@ -174,7 +178,7 @@ impl State<App> for BrowseShortcuts {
|
|||||||
self.recalculate(ctx, app);
|
self.recalculate(ctx, app);
|
||||||
}
|
}
|
||||||
x => {
|
x => {
|
||||||
if let Some(t) = crate::per_neighborhood::handle_action(
|
if let Some(t) = self.edit.handle_panel_action(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
x,
|
x,
|
||||||
@ -205,10 +209,7 @@ impl State<App> for BrowseShortcuts {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Bit weird to allow this while showing individual paths, since we don't draw the
|
if self.edit.event(ctx, app) {
|
||||||
// world
|
|
||||||
let world_outcome = self.world.event(ctx);
|
|
||||||
if crate::per_neighborhood::handle_world_outcome(ctx, app, world_outcome) {
|
|
||||||
// Reset state, but if possible, preserve the current individual shortcut.
|
// Reset state, but if possible, preserve the current individual shortcut.
|
||||||
let current_request = self.shortcuts.paths[self.current_idx].get_req().clone();
|
let current_request = self.shortcuts.paths[self.current_idx].get_req().clone();
|
||||||
return Transition::Replace(BrowseShortcuts::new_state(
|
return Transition::Replace(BrowseShortcuts::new_state(
|
||||||
@ -226,7 +227,7 @@ impl State<App> for BrowseShortcuts {
|
|||||||
self.top_panel.draw(g);
|
self.top_panel.draw(g);
|
||||||
self.left_panel.draw(g);
|
self.left_panel.draw(g);
|
||||||
|
|
||||||
self.world.draw(g);
|
self.edit.world.draw(g);
|
||||||
self.draw_path.draw(g);
|
self.draw_path.draw(g);
|
||||||
|
|
||||||
g.redraw(&self.neighborhood.fade_irrelevant);
|
g.redraw(&self.neighborhood.fade_irrelevant);
|
||||||
|
Loading…
Reference in New Issue
Block a user