Start a new UI for routing. Just managing waypoints

This commit is contained in:
Dustin Carlino 2021-08-23 13:44:21 -07:00
parent fbd173408e
commit 8f485667fd
2 changed files with 268 additions and 0 deletions

View File

@ -2,6 +2,7 @@ mod labels;
mod layers;
mod magnifying;
mod quick_sketch;
mod route;
mod share;
use std::collections::HashMap;
@ -240,6 +241,12 @@ impl State<App> for ExploreMap {
ctx, app,
));
}
"Plan a route" => {
app.primary.current_selection = None;
return Transition::Push(crate::ungap::route::RoutePlanner::new_state(
ctx, app,
));
}
_ => unreachable!(),
}
}
@ -461,6 +468,11 @@ fn make_top_panel(ctx: &mut EventCtx, app: &App) -> Panel {
.icon_text("system/assets/tools/pencil.svg", "Create new bike lanes")
.hotkey(Key::S)
.build_def(ctx),
ctx.style()
.btn_outline
.text("Plan a route")
.hotkey(Key::R)
.build_def(ctx),
]))
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
.build(ctx)

256
game/src/ungap/route.rs Normal file
View File

@ -0,0 +1,256 @@
use rand::seq::SliceRandom;
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
use geom::{Circle, Distance, FindClosest, Polygon};
use sim::TripEndpoint;
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
State, Text, TextExt, VerticalAlignment, Widget,
};
use crate::app::{App, Transition};
pub struct RoutePlanner {
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>,
}
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) -> 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 {
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,
};
rp.update_input_panel(ctx);
rp.update_drawable(ctx);
Box::new(rp)
}
fn update_input_panel(&mut self, ctx: &mut EventCtx) {
let mut col = vec![Widget::row(vec![
Line("Plan a route").small_heading().into_widget(ctx),
ctx.style().btn_close_widget(ctx),
])];
for (idx, waypt) in self.waypoints.iter().enumerate() {
col.push(Widget::row(vec![
format!("{}) {}", waypt.order, waypt.label).text_widget(ctx),
// TODO Circular outline style?
ctx.style()
.btn_outline
.text("X")
.build_widget(ctx, &format!("delete waypoint {}", idx)),
]));
}
col.push(
ctx.style()
.btn_plain
.text("Add waypoint")
.hotkey(Key::A)
.build_def(ctx),
);
self.input_panel = Panel::new_builder(Widget::col(col))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx);
}
fn update_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(&self, ctx: &mut EventCtx, app: &App) -> Waypoint {
// Just pick a random place, then let the user drag the marker around
// TODO Repeat if it matches an existing
let at = TripEndpoint::Bldg(
app.primary
.map
.all_buildings()
.choose(&mut XorShiftRng::from_entropy())
.unwrap()
.id,
);
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);
self.update_drawable(ctx);
}
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(())
}
}
impl State<App> for RoutePlanner {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
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 Outcome::Clicked(x) = self.input_panel.event(ctx) {
match x.as_ref() {
"close" => {
return Transition::Pop;
}
"Add waypoint" => {
self.waypoints.push(self.make_new_waypt(ctx, app));
self.update_input_panel(ctx);
self.update_drawable(ctx);
}
x => {
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);
self.update_drawable(ctx);
} else {
unreachable!()
}
}
}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.input_panel.draw(g);
g.redraw(&self.draw_waypoints);
g.redraw(&self.draw_hover);
}
}
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,
}
}
}
// 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.