mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-18 20:02:18 +03:00
Start a new UI for routing. Just managing waypoints
This commit is contained in:
parent
fbd173408e
commit
8f485667fd
@ -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
256
game/src/ungap/route.rs
Normal 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.
|
Loading…
Reference in New Issue
Block a user