Split waypoint input management into its own component. Better

organization, insulates from the upcoming complexity increase for
viewing the route result, and paves the way for using this component in
something like a better tool for spawning a simulation agent to visit
waypoints. #743
This commit is contained in:
Dustin Carlino 2021-09-01 11:50:37 -07:00
parent 0587fa44d7
commit 470ac4f0a9
3 changed files with 265 additions and 209 deletions

View File

@ -12,6 +12,7 @@ use widgetry::{
pub use self::route_sketcher::RouteSketcher;
pub use self::select::RoadSelector;
pub use self::warp::{warp_to_id, Warping};
pub use self::waypoints::InputWaypoints;
use crate::app::App;
use crate::app::Transition;
use crate::info::{ContextualActions, InfoPanel, Tab};
@ -20,6 +21,7 @@ use crate::sandbox::TimeWarpScreen;
mod route_sketcher;
mod select;
mod warp;
mod waypoints;
// TODO This is now just used in two modes...
pub struct CommonState {

View File

@ -0,0 +1,239 @@
use geom::{Circle, Distance, FindClosest, Polygon};
use sim::TripEndpoint;
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Image, Line, Outcome, Text, TextExt, Widget,
};
use crate::app::App;
/// Click to add waypoints, drag them, see the list on a panel and delete them. The caller owns the
/// Panel, since there's probably more stuff there too.
pub struct InputWaypoints {
waypoints: Vec<Waypoint>,
draw_waypoints: Drawable,
hovering_on_waypt: Option<usize>,
draw_hover: Drawable,
// TODO Invariant not captured by these separate fields: when dragging is true,
// hovering_on_waypt is fixed.
dragging: bool,
snap_to_endpts: FindClosest<TripEndpoint>,
}
// TODO Maybe it's been a while and I've forgotten some UI patterns, but this is painfully manual.
// I think we need a draggable map-space thing.
struct Waypoint {
// TODO Different colors would also be helpful
order: char,
at: TripEndpoint,
label: String,
geom: GeomBatch,
hitbox: Polygon,
}
impl InputWaypoints {
pub fn new(ctx: &mut EventCtx, app: &App) -> InputWaypoints {
let map = &app.primary.map;
let mut snap_to_endpts = FindClosest::new(map.get_bounds());
for i in map.all_intersections() {
if i.is_border() {
snap_to_endpts.add(TripEndpoint::Border(i.id), i.polygon.points());
}
}
for b in map.all_buildings() {
snap_to_endpts.add(TripEndpoint::Bldg(b.id), b.polygon.points());
}
InputWaypoints {
waypoints: Vec::new(),
draw_waypoints: Drawable::empty(ctx),
hovering_on_waypt: None,
draw_hover: Drawable::empty(ctx),
dragging: false,
snap_to_endpts,
}
}
pub fn get_panel_widget(&self, ctx: &mut EventCtx) -> Widget {
let mut col = Vec::new();
for (idx, waypt) in self.waypoints.iter().enumerate() {
col.push(Widget::row(vec![
format!("{}) {}", waypt.order, waypt.label)
.text_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain_destructive
.text("X")
.build_widget(ctx, &format!("delete waypoint {}", idx)),
]));
}
col.push(Widget::row(vec![
Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
Text::from_all(vec![
Line("Click").fg(ctx.style().text_hotkey_color),
Line(" to add a waypoint, "),
Line("drag").fg(ctx.style().text_hotkey_color),
Line(" a waypoint to move it"),
])
.into_widget(ctx),
]));
Widget::col(col)
}
pub fn get_waypoints(&self) -> Vec<TripEndpoint> {
self.waypoints.iter().map(|w| w.at).collect()
}
/// If the outcome from the panel isn't used by the caller, pass it along here. This handles
/// calling `ctx.canvas_movement` when appropriate. When this returns true, something has
/// changed, so the caller may want to update their view of the route and call
/// `get_panel_widget` again.
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App, outcome: Outcome) -> bool {
if self.dragging {
if ctx.redo_mouseover() {
if self.update_dragging(ctx, app) == Some(true) {
return true;
}
}
if ctx.input.left_mouse_button_released() {
self.dragging = false;
self.update_hover(ctx);
}
} else {
if ctx.redo_mouseover() {
self.update_hover(ctx);
}
if self.hovering_on_waypt.is_none() {
ctx.canvas_movement();
} else if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
// Zooming is OK, but can't start click and drag
ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
}
if self.hovering_on_waypt.is_some() && ctx.input.left_mouse_button_pressed() {
self.dragging = true;
}
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if self.hovering_on_waypt.is_none() && ctx.normal_left_click() {
if let Some((at, _)) =
self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0))
{
self.waypoints
.push(Waypoint::new(ctx, app, at, self.waypoints.len()));
self.update_waypoints_drawable(ctx);
self.update_hover(ctx);
return true;
}
}
}
}
if let Outcome::Clicked(x) = outcome {
if let Some(x) = x.strip_prefix("delete waypoint ") {
let idx = x.parse::<usize>().unwrap();
self.waypoints.remove(idx);
// Recalculate labels, in case we deleted in the middle
for (idx, waypt) in self.waypoints.iter_mut().enumerate() {
*waypt = Waypoint::new(ctx, app, waypt.at, idx);
}
self.update_waypoints_drawable(ctx);
return true;
} else {
panic!("Unknown InputWaypoints click {}", x);
}
}
false
}
pub fn draw(&self, g: &mut GfxCtx) {
g.redraw(&self.draw_waypoints);
g.redraw(&self.draw_hover);
}
fn update_waypoints_drawable(&mut self, ctx: &mut EventCtx) {
let mut batch = GeomBatch::new();
for waypt in &self.waypoints {
batch.append(waypt.geom.clone());
}
self.draw_waypoints = ctx.upload(batch);
}
fn update_hover(&mut self, ctx: &EventCtx) {
self.hovering_on_waypt = None;
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
self.hovering_on_waypt = self
.waypoints
.iter()
.position(|waypt| waypt.hitbox.contains_pt(pt));
}
let mut batch = GeomBatch::new();
if let Some(idx) = self.hovering_on_waypt {
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
}
self.draw_hover = ctx.upload(batch);
}
// `Some(true)` means to update.
fn update_dragging(&mut self, ctx: &mut EventCtx, app: &App) -> Option<bool> {
let pt = ctx.canvas.get_cursor_in_map_space()?;
let (at, _) = self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0))?;
let mut changed = false;
let idx = self.hovering_on_waypt.unwrap();
if self.waypoints[idx].at != at {
self.waypoints[idx] = Waypoint::new(ctx, app, at, idx);
self.update_waypoints_drawable(ctx);
changed = true;
}
let mut batch = GeomBatch::new();
// Show where we're currently snapped
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
self.draw_hover = ctx.upload(batch);
Some(changed)
}
}
impl Waypoint {
fn new(ctx: &mut EventCtx, app: &App, at: TripEndpoint, idx: usize) -> Waypoint {
let order = char::from_u32('A' as u32 + idx as u32).unwrap();
let map = &app.primary.map;
let (center, label) = match at {
TripEndpoint::Bldg(b) => {
let b = map.get_b(b);
(b.polygon.center(), b.address.clone())
}
TripEndpoint::Border(i) => {
let i = map.get_i(i);
(i.polygon.center(), i.name(app.opts.language.as_ref(), map))
}
TripEndpoint::SuddenlyAppear(pos) => (pos.pt(map), pos.to_string()),
};
let circle = Circle::new(center, Distance::meters(30.0)).to_polygon();
let mut geom = GeomBatch::new();
geom.push(Color::RED, circle.clone());
geom.append(
Text::from(Line(format!("{}", order)).fg(Color::WHITE))
.render(ctx)
.centered_on(center),
);
let hitbox = circle;
Waypoint {
order,
at,
label,
geom,
hitbox,
}
}
}

View File

@ -1,30 +1,23 @@
use std::collections::HashSet;
use geom::{Circle, Distance, Duration, FindClosest, Polygon, Pt2D};
use geom::{Distance, Duration};
use map_model::{PathStep, NORMAL_LANE_THICKNESS};
use sim::{TripEndpoint, TripMode};
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Image, Line, LinePlot,
Outcome, Panel, PlotOptions, Series, State, Text, TextExt, VerticalAlignment, Widget,
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, LinePlot, Outcome,
Panel, PlotOptions, Series, State, Text, VerticalAlignment, Widget,
};
use crate::app::{App, Transition};
use crate::common::InputWaypoints;
use crate::ungap::{Layers, Tab, TakeLayers};
pub struct RoutePlanner {
layers: Layers,
once: bool,
// All of this manages the waypoint input
input_panel: Panel,
waypoints: Vec<Waypoint>,
draw_waypoints: Drawable,
hovering_on_waypt: Option<usize>,
draw_hover: Drawable,
// TODO Invariant not captured by these separate fields: when dragging is true,
// hovering_on_waypt is fixed.
dragging: bool,
snap_to_endpts: FindClosest<TripEndpoint>,
waypoints: InputWaypoints,
// Routing
draw_route: Drawable,
@ -37,133 +30,30 @@ impl TakeLayers for RoutePlanner {
}
}
// TODO Maybe it's been a while and I've forgotten some UI patterns, but this is painfully manual.
// I think we need a draggable map-space thing.
struct Waypoint {
// TODO Different colors would also be helpful
order: char,
at: TripEndpoint,
label: String,
geom: GeomBatch,
hitbox: Polygon,
}
impl RoutePlanner {
pub fn new_state(ctx: &mut EventCtx, app: &App, layers: Layers) -> Box<dyn State<App>> {
let map = &app.primary.map;
let mut snap_to_endpts = FindClosest::new(map.get_bounds());
for i in map.all_intersections() {
if i.is_border() {
snap_to_endpts.add(TripEndpoint::Border(i.id), i.polygon.points());
}
}
for b in map.all_buildings() {
snap_to_endpts.add(TripEndpoint::Bldg(b.id), b.polygon.points());
}
let mut rp = RoutePlanner {
layers,
once: true,
input_panel: Panel::empty(ctx),
waypoints: Vec::new(),
draw_waypoints: Drawable::empty(ctx),
hovering_on_waypt: None,
draw_hover: Drawable::empty(ctx),
dragging: false,
snap_to_endpts,
waypoints: InputWaypoints::new(ctx, app),
draw_route: Drawable::empty(ctx),
results_panel: Panel::empty(ctx),
};
rp.update_input_panel(ctx, app);
rp.update_waypoints_drawable(ctx);
rp.update_route(ctx, app);
Box::new(rp)
}
fn update_input_panel(&mut self, ctx: &mut EventCtx, app: &App) {
let mut col = vec![Tab::Route.make_header(ctx, app)];
for (idx, waypt) in self.waypoints.iter().enumerate() {
col.push(Widget::row(vec![
format!("{}) {}", waypt.order, waypt.label)
.text_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain_destructive
.text("X")
.build_widget(ctx, &format!("delete waypoint {}", idx)),
]));
}
col.push(Widget::row(vec![
Image::from_path("system/assets/tools/mouse.svg").into_widget(ctx),
Text::from_all(vec![
Line("Click").fg(ctx.style().text_hotkey_color),
Line(" to add a waypoint, "),
Line("drag").fg(ctx.style().text_hotkey_color),
Line(" a waypoint to move it"),
])
.into_widget(ctx),
]));
self.input_panel = Panel::new_builder(Widget::col(col))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx);
}
fn update_waypoints_drawable(&mut self, ctx: &mut EventCtx) {
let mut batch = GeomBatch::new();
for waypt in &self.waypoints {
batch.append(waypt.geom.clone());
}
self.draw_waypoints = ctx.upload(batch);
}
fn make_new_waypt(&mut self, ctx: &mut EventCtx, app: &App, pt: Pt2D) {
if let Some((at, _)) = self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0)) {
self.waypoints
.push(Waypoint::new(ctx, app, at, self.waypoints.len()));
}
}
fn update_hover(&mut self, ctx: &EventCtx) {
self.hovering_on_waypt = None;
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
self.hovering_on_waypt = self
.waypoints
.iter()
.position(|waypt| waypt.hitbox.contains_pt(pt));
}
let mut batch = GeomBatch::new();
if let Some(idx) = self.hovering_on_waypt {
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
}
self.draw_hover = ctx.upload(batch);
}
// Just use Option for early return
fn update_dragging(&mut self, ctx: &mut EventCtx, app: &App) -> Option<()> {
let pt = ctx.canvas.get_cursor_in_map_space()?;
let (at, _) = self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0))?;
let idx = self.hovering_on_waypt.unwrap();
if self.waypoints[idx].at != at {
self.waypoints[idx] = Waypoint::new(ctx, app, at, idx);
self.update_input_panel(ctx, app);
self.update_waypoints_drawable(ctx);
self.update_route(ctx, app);
}
let mut batch = GeomBatch::new();
// Show where we're currently snapped
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
self.draw_hover = ctx.upload(batch);
Some(())
self.input_panel = Panel::new_builder(Widget::col(vec![
Tab::Route.make_header(ctx, app),
self.waypoints.get_panel_widget(ctx),
]))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx);
}
fn update_route(&mut self, ctx: &mut EventCtx, app: &App) {
@ -180,9 +70,9 @@ impl RoutePlanner {
let mut elevation_pts: Vec<(Distance, Distance)> = Vec::new();
let mut current_dist = Distance::ZERO;
for pair in self.waypoints.windows(2) {
for pair in self.waypoints.get_waypoints().windows(2) {
if let Some((path, draw_path)) =
TripEndpoint::path_req(pair[0].at, pair[1].at, TripMode::Bike, map)
TripEndpoint::path_req(pair[0], pair[1], TripMode::Bike, map)
.and_then(|req| map.pathfind(req).ok())
.and_then(|path| {
path.trace(&app.primary.map)
@ -309,56 +199,17 @@ impl State<App> for RoutePlanner {
});
}
if self.dragging {
if ctx.redo_mouseover() {
self.update_dragging(ctx, app);
}
if ctx.input.left_mouse_button_released() {
self.dragging = false;
self.update_hover(ctx);
}
} else {
if ctx.redo_mouseover() {
self.update_hover(ctx);
}
if self.hovering_on_waypt.is_none() {
ctx.canvas_movement();
} else if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
// Zooming is OK, but can't start click and drag
ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
}
if self.hovering_on_waypt.is_some() && ctx.input.left_mouse_button_pressed() {
self.dragging = true;
}
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if self.hovering_on_waypt.is_none() && ctx.normal_left_click() {
self.make_new_waypt(ctx, app, pt);
self.update_input_panel(ctx, app);
self.update_waypoints_drawable(ctx);
self.update_route(ctx, app);
self.update_hover(ctx);
}
}
}
if let Outcome::Clicked(x) = self.input_panel.event(ctx) {
if let Some(x) = x.strip_prefix("delete waypoint ") {
let idx = x.parse::<usize>().unwrap();
self.waypoints.remove(idx);
// Recalculate labels, in case we deleted in the middle
for (idx, waypt) in self.waypoints.iter_mut().enumerate() {
*waypt = Waypoint::new(ctx, app, waypt.at, idx);
}
self.update_input_panel(ctx, app);
self.update_waypoints_drawable(ctx);
self.update_route(ctx, app);
} else {
match self.input_panel.event(ctx) {
// TODO Inverting control is hard. Who should try to handle the outcome first?
Outcome::Clicked(x) if !x.starts_with("delete waypoint ") => {
return Tab::Route.handle_action::<RoutePlanner>(ctx, app, &x);
}
outcome => {
if self.waypoints.event(ctx, app, outcome) {
self.update_input_panel(ctx, app);
self.update_route(ctx, app);
}
}
}
if let Some(t) = self.layers.event(ctx, app) {
@ -371,45 +222,9 @@ impl State<App> for RoutePlanner {
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.layers.draw(g, app);
self.input_panel.draw(g);
g.redraw(&self.draw_waypoints);
g.redraw(&self.draw_hover);
self.waypoints.draw(g);
self.results_panel.draw(g);
g.redraw(&self.draw_route);
}
}
impl Waypoint {
fn new(ctx: &mut EventCtx, app: &App, at: TripEndpoint, idx: usize) -> Waypoint {
let order = char::from_u32('A' as u32 + idx as u32).unwrap();
let map = &app.primary.map;
let (center, label) = match at {
TripEndpoint::Bldg(b) => {
let b = map.get_b(b);
(b.polygon.center(), b.address.clone())
}
TripEndpoint::Border(i) => {
let i = map.get_i(i);
(i.polygon.center(), i.name(app.opts.language.as_ref(), map))
}
TripEndpoint::SuddenlyAppear(pos) => (pos.pt(map), pos.to_string()),
};
let circle = Circle::new(center, Distance::meters(30.0)).to_polygon();
let mut geom = GeomBatch::new();
geom.push(Color::RED, circle.clone());
geom.append(
Text::from(Line(format!("{}", order)).fg(Color::WHITE))
.render(ctx)
.centered_on(center),
);
let hitbox = circle;
Waypoint {
order,
at,
label,
geom,
hitbox,
}
}
}