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 map_gui::tools::ColorNetwork;
|
||||
use widgetry::mapspace::{ToggleZoomed, World};
|
||||
use widgetry::mapspace::ToggleZoomed;
|
||||
use widgetry::tools::PopupMsg;
|
||||
use widgetry::{
|
||||
DrawBaselayer, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, TextExt, Toggle, Widget,
|
||||
};
|
||||
|
||||
use crate::edit::{EditNeighborhood, Tab};
|
||||
use crate::filters::auto::Heuristic;
|
||||
use crate::per_neighborhood::{FilterableObj, Tab};
|
||||
use crate::shortcuts::find_shortcuts;
|
||||
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
||||
|
||||
@ -15,8 +15,8 @@ pub struct Viewer {
|
||||
top_panel: Panel,
|
||||
left_panel: Panel,
|
||||
neighborhood: Neighborhood,
|
||||
world: World<FilterableObj>,
|
||||
draw_top_layer: ToggleZoomed,
|
||||
edit: EditNeighborhood,
|
||||
}
|
||||
|
||||
impl Viewer {
|
||||
@ -27,8 +27,8 @@ impl Viewer {
|
||||
top_panel: crate::components::TopPanel::panel(ctx, app),
|
||||
left_panel: Panel::empty(ctx),
|
||||
neighborhood,
|
||||
world: World::unbounded(),
|
||||
draw_top_layer: ToggleZoomed::empty(ctx),
|
||||
edit: EditNeighborhood::temporary(),
|
||||
};
|
||||
viewer.update(ctx, app);
|
||||
Box::new(viewer)
|
||||
@ -47,10 +47,12 @@ impl Viewer {
|
||||
format!("{} cells are totally disconnected", disconnected_cells)
|
||||
};
|
||||
|
||||
self.left_panel = Tab::Connectivity
|
||||
self.left_panel = self
|
||||
.edit
|
||||
.panel_builder(
|
||||
ctx,
|
||||
app,
|
||||
Tab::Connectivity,
|
||||
&self.top_panel,
|
||||
Widget::col(vec![
|
||||
format!(
|
||||
@ -66,8 +68,8 @@ impl Viewer {
|
||||
)
|
||||
.build(ctx);
|
||||
|
||||
let (world, draw_top_layer) = make_world(ctx, app, &self.neighborhood);
|
||||
self.world = world;
|
||||
let (edit, draw_top_layer) = setup_editing(ctx, app, &self.neighborhood);
|
||||
self.edit = edit;
|
||||
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,
|
||||
app,
|
||||
x.as_ref(),
|
||||
@ -131,16 +133,15 @@ impl State<App> for Viewer {
|
||||
app.session.heuristic = self.left_panel.dropdown_value("heuristic");
|
||||
|
||||
if x != "heuristic" {
|
||||
let (world, draw_top_layer) = make_world(ctx, app, &self.neighborhood);
|
||||
self.world = world;
|
||||
let (edit, draw_top_layer) = setup_editing(ctx, app, &self.neighborhood);
|
||||
self.edit = edit;
|
||||
self.draw_top_layer = draw_top_layer;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let world_outcome = self.world.event(ctx);
|
||||
if crate::per_neighborhood::handle_world_outcome(ctx, app, world_outcome) {
|
||||
if self.edit.event(ctx, app) {
|
||||
self.neighborhood = Neighborhood::new(ctx, app, self.neighborhood.id);
|
||||
self.update(ctx, app);
|
||||
}
|
||||
@ -153,7 +154,7 @@ impl State<App> for Viewer {
|
||||
}
|
||||
|
||||
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);
|
||||
self.draw_top_layer.draw(g);
|
||||
|
||||
@ -173,16 +174,16 @@ impl State<App> for Viewer {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_world(
|
||||
fn setup_editing(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
neighborhood: &Neighborhood,
|
||||
) -> (World<FilterableObj>, ToggleZoomed) {
|
||||
) -> (EditNeighborhood, ToggleZoomed) {
|
||||
let shortcuts = ctx.loading_screen("find shortcuts", |_, 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;
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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> {
|
||||
|
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 connectivity;
|
||||
mod draw_cells;
|
||||
mod edit;
|
||||
mod export;
|
||||
mod filters;
|
||||
mod impact;
|
||||
mod neighborhood;
|
||||
mod partition;
|
||||
mod per_neighborhood;
|
||||
mod route_planner;
|
||||
mod save;
|
||||
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_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 crate::per_neighborhood::{FilterableObj, Tab};
|
||||
use crate::edit::{EditNeighborhood, Tab};
|
||||
use crate::shortcuts::{find_shortcuts, Shortcuts};
|
||||
use crate::{colors, App, Neighborhood, NeighborhoodID, Transition};
|
||||
|
||||
@ -14,7 +14,7 @@ pub struct BrowseShortcuts {
|
||||
current_idx: usize,
|
||||
|
||||
draw_path: ToggleZoomed,
|
||||
world: World<FilterableObj>,
|
||||
edit: EditNeighborhood,
|
||||
neighborhood: Neighborhood,
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ impl BrowseShortcuts {
|
||||
let shortcuts = ctx.loading_screen("find shortcuts", |_, 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 {
|
||||
top_panel: crate::components::TopPanel::panel(ctx, app),
|
||||
@ -39,7 +39,7 @@ impl BrowseShortcuts {
|
||||
current_idx: 0,
|
||||
draw_path: ToggleZoomed::empty(ctx),
|
||||
neighborhood,
|
||||
world,
|
||||
edit,
|
||||
};
|
||||
|
||||
if let Some(req) = start_with_request {
|
||||
@ -63,10 +63,12 @@ impl BrowseShortcuts {
|
||||
self.shortcuts.quiet_and_total_streets(&self.neighborhood);
|
||||
|
||||
if self.shortcuts.paths.is_empty() {
|
||||
self.left_panel = Tab::Shortcuts
|
||||
self.left_panel = self
|
||||
.edit
|
||||
.panel_builder(
|
||||
ctx,
|
||||
app,
|
||||
Tab::Shortcuts,
|
||||
&self.top_panel,
|
||||
percentage_bar(
|
||||
ctx,
|
||||
@ -86,10 +88,12 @@ impl BrowseShortcuts {
|
||||
let controls = self.prev_next_controls(ctx);
|
||||
self.left_panel.replace(ctx, "prev/next controls", controls);
|
||||
} else {
|
||||
self.left_panel = Tab::Shortcuts
|
||||
self.left_panel = self
|
||||
.edit
|
||||
.panel_builder(
|
||||
ctx,
|
||||
app,
|
||||
Tab::Shortcuts,
|
||||
&self.top_panel,
|
||||
Widget::col(vec![
|
||||
percentage_bar(
|
||||
@ -174,7 +178,7 @@ impl State<App> for BrowseShortcuts {
|
||||
self.recalculate(ctx, app);
|
||||
}
|
||||
x => {
|
||||
if let Some(t) = crate::per_neighborhood::handle_action(
|
||||
if let Some(t) = self.edit.handle_panel_action(
|
||||
ctx,
|
||||
app,
|
||||
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
|
||||
// world
|
||||
let world_outcome = self.world.event(ctx);
|
||||
if crate::per_neighborhood::handle_world_outcome(ctx, app, world_outcome) {
|
||||
if self.edit.event(ctx, app) {
|
||||
// Reset state, but if possible, preserve the current individual shortcut.
|
||||
let current_request = self.shortcuts.paths[self.current_idx].get_req().clone();
|
||||
return Transition::Replace(BrowseShortcuts::new_state(
|
||||
@ -226,7 +227,7 @@ impl State<App> for BrowseShortcuts {
|
||||
self.top_panel.draw(g);
|
||||
self.left_panel.draw(g);
|
||||
|
||||
self.world.draw(g);
|
||||
self.edit.world.draw(g);
|
||||
self.draw_path.draw(g);
|
||||
|
||||
g.redraw(&self.neighborhood.fade_irrelevant);
|
||||
|
Loading…
Reference in New Issue
Block a user