tool to visualize uber-turns

This commit is contained in:
Dustin Carlino 2020-05-18 17:44:46 -07:00
parent ac459fd8e8
commit e2e4ddab2f
5 changed files with 286 additions and 37 deletions

View File

@ -173,16 +173,12 @@ impl State for TurnExplorer {
return Transition::Pop;
}
"previous turn" => {
if self.idx != 0 {
self.idx -= 1;
self.composite = TurnExplorer::make_panel(ctx, app, self.l, self.idx);
}
self.idx -= 1;
self.composite = TurnExplorer::make_panel(ctx, app, self.l, self.idx);
}
"next turn" => {
if self.idx != app.primary.map.get_turns_from_lane(self.l).len() {
self.idx += 1;
self.composite = TurnExplorer::make_panel(ctx, app, self.l, self.idx);
}
self.idx += 1;
self.composite = TurnExplorer::make_panel(ctx, app, self.l, self.idx);
}
_ => unreachable!(),
},

View File

@ -2,6 +2,7 @@ mod dashboards;
pub mod gameplay;
mod misc_tools;
mod speed;
mod uber_turns;
use self::misc_tools::{RoutePreview, ShowTrafficSignal, TurnExplorer};
use crate::app::App;
@ -384,6 +385,7 @@ impl ContextualActions for Actions {
actions.push((Key::F, "explore traffic signal details".to_string()));
actions.push((Key::C, "show current demand".to_string()));
actions.push((Key::E, "edit traffic signal".to_string()));
actions.push((Key::U, "explore uber-turns".to_string()));
}
if app.primary.map.get_i(i).is_stop_sign() {
actions.push((Key::E, "edit stop sign".to_string()));
@ -435,6 +437,9 @@ impl ContextualActions for Actions {
Box::new(EditMode::new(ctx, app, self.gameplay.clone())),
Box::new(StopSignEditor::new(i, ctx, app)),
),
(ID::Intersection(i), "explore uber-turns") => {
Transition::Push(uber_turns::UberTurnPicker::new(ctx, app, i))
}
(ID::Lane(l), "explore turns from this lane") => {
Transition::Push(TurnExplorer::new(ctx, app, l))
}

View File

@ -0,0 +1,222 @@
use crate::app::{App, ShowEverything};
use crate::common::CommonState;
use crate::game::{msg, DrawBaselayer, State, Transition};
use crate::helpers::ID;
use crate::render::{DrawOptions, BIG_ARROW_THICKNESS};
use ezgui::{
hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
Line, Outcome, Text, VerticalAlignment, Widget,
};
use geom::{ArrowCap, Polygon};
use map_model::{IntersectionCluster, IntersectionID};
use sim::DontDrawAgents;
use std::collections::HashSet;
pub struct UberTurnPicker {
members: HashSet<IntersectionID>,
composite: Composite,
}
impl UberTurnPicker {
pub fn new(ctx: &mut EventCtx, app: &App, i: IntersectionID) -> Box<dyn State> {
let mut members = HashSet::new();
members.insert(i);
Box::new(UberTurnPicker {
members,
composite: Composite::new(
Widget::col(vec![
Widget::row(vec![
Line("Select multiple intersections")
.small_heading()
.draw(ctx),
Btn::text_fg("X")
.build_def(ctx, hotkey(Key::Escape))
.align_right(),
]),
Btn::text_fg("View uber-turns").build_def(ctx, None),
])
.padding(10)
.bg(app.cs.panel_bg),
)
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx),
})
}
}
impl State for UberTurnPicker {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
if ctx.redo_mouseover() {
app.recalculate_current_selection(ctx);
}
if let Some(ID::Intersection(i)) = app.primary.current_selection {
if !self.members.contains(&i) && app.per_obj.left_click(ctx, "add this intersection") {
self.members.insert(i);
} else if self.members.contains(&i)
&& app.per_obj.left_click(ctx, "remove this intersection")
{
self.members.remove(&i);
}
}
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
return Transition::Pop;
}
"View uber-turns" => {
if self.members.len() < 2 {
return Transition::Push(msg(
"Error",
vec!["Select at least two intersections"],
));
}
return Transition::Replace(UberTurnViewer::new(
ctx,
app,
IntersectionCluster::new(self.members.clone(), &app.primary.map),
0,
));
}
_ => unreachable!(),
},
None => {}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.composite.draw(g);
CommonState::draw_osd(g, app, &app.primary.current_selection);
let mut batch = GeomBatch::new();
for i in &self.members {
batch.push(
Color::RED.alpha(0.8),
app.primary.map.get_i(*i).polygon.clone(),
);
}
let draw = g.upload(batch);
g.redraw(&draw);
}
}
struct UberTurnViewer {
composite: Composite,
draw: Drawable,
ic: IntersectionCluster,
idx: usize,
}
impl UberTurnViewer {
pub fn new(
ctx: &mut EventCtx,
app: &mut App,
ic: IntersectionCluster,
idx: usize,
) -> Box<dyn State> {
app.primary.current_selection = None;
let mut batch = GeomBatch::new();
for i in &ic.members {
batch.push(
Color::BLUE.alpha(0.5),
app.primary.map.get_i(*i).polygon.clone(),
);
}
batch.push(
Color::RED,
ic.uber_turns[idx]
.geom(&app.primary.map)
.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle)
.unwrap(),
);
Box::new(UberTurnViewer {
draw: ctx.upload(batch),
composite: Composite::new(
Widget::col(vec![Widget::row(vec![
Line("Uber-turn viewer").small_heading().draw(ctx).margin(5),
Widget::draw_batch(
ctx,
GeomBatch::from(vec![(Color::WHITE, Polygon::rectangle(2.0, 50.0))]),
)
.margin(5),
if idx == 0 {
Btn::text_fg("<").inactive(ctx)
} else {
Btn::text_fg("<").build(ctx, "previous uber-turn", hotkey(Key::LeftArrow))
}
.margin(5),
Text::from(Line(format!("{}/{}", idx, ic.uber_turns.len())).secondary())
.draw(ctx)
.margin(5)
.centered_vert(),
if idx == ic.uber_turns.len() - 1 {
Btn::text_fg(">").inactive(ctx)
} else {
Btn::text_fg(">").build(ctx, "next uber-turn", hotkey(Key::RightArrow))
}
.margin(5),
Btn::text_fg("X").build_def(ctx, hotkey(Key::Escape)),
])])
.padding(10)
.bg(app.cs.panel_bg),
)
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx),
ic,
idx,
})
}
}
impl State for UberTurnViewer {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
return Transition::Pop;
}
"previous uber-turn" => {
return Transition::Replace(UberTurnViewer::new(
ctx,
app,
self.ic.clone(),
self.idx - 1,
));
}
"next uber-turn" => {
return Transition::Replace(UberTurnViewer::new(
ctx,
app,
self.ic.clone(),
self.idx + 1,
));
}
_ => unreachable!(),
},
None => {}
}
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::Custom
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
let mut opts = DrawOptions::new();
opts.suppress_traffic_signal_details
.extend(self.ic.members.clone());
app.draw(g, opts, &DontDrawAgents {}, &ShowEverything::new());
self.composite.draw(g);
g.redraw(&self.draw);
}
}

View File

@ -24,6 +24,7 @@ pub use crate::intersection::{Intersection, IntersectionID, IntersectionType};
pub use crate::lane::{Lane, LaneID, LaneType, PARKING_SPOT_LENGTH};
pub use crate::make::RoadSpec;
pub use crate::map::Map;
pub use crate::pathfind::uber_turns::{IntersectionCluster, UberTurn};
pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep};
pub use crate::road::{DirectedRoadID, Road, RoadID};
pub use crate::stop_signs::{ControlStopSign, RoadWithStopSign};

View File

@ -1,14 +1,17 @@
use crate::{IntersectionID, Map, TurnID};
use geom::PolyLine;
use petgraph::graphmap::UnGraphMap;
use std::collections::{HashMap, HashSet};
// This only applies to VehiclePathfinder; walking through these intersections is nothing special.
// TODO I haven't seen any cases yet with "interior" intersections. Some stuff might break.
#[derive(Clone)]
pub struct IntersectionCluster {
pub members: HashSet<IntersectionID>,
pub uber_turns: Vec<UberTurn>,
}
#[derive(Clone)]
pub struct UberTurn {
pub path: Vec<TurnID>,
}
@ -27,11 +30,43 @@ pub fn find(map: &Map) -> Vec<IntersectionCluster> {
}
for intersections in petgraph::algo::kosaraju_scc(&graph) {
let members: HashSet<IntersectionID> = intersections.iter().cloned().collect();
let mut ic = IntersectionCluster::new(members, map);
// Filter out the restricted ones!
// TODO Could be more efficient, but eh
let orig_num = ic.uber_turns.len();
ic.uber_turns.retain(|ut| {
let mut ok = true;
for pair in ut.path.windows(2) {
let r1 = map.get_l(pair[0].src).parent;
let r2 = map.get_l(pair[0].dst).parent;
let r3 = map.get_l(pair[1].dst).parent;
if all_restrictions.contains(&(r1, r2, r3)) {
ok = false;
break;
}
}
ok
});
println!(
"Cluster {:?} has {} uber-turns ({} filtered out):",
intersections,
ic.uber_turns.len(),
orig_num - ic.uber_turns.len()
);
clusters.push(ic);
}
clusters
}
impl IntersectionCluster {
pub fn new(members: HashSet<IntersectionID>, map: &Map) -> IntersectionCluster {
// Find all entrances and exits through this group of intersections
let mut entrances = Vec::new();
let mut exits = HashSet::new();
for i in &intersections {
for i in &members {
for turn in map.get_turns_in_intersection(*i) {
if turn.between_sidewalks() {
continue;
@ -50,30 +85,6 @@ pub fn find(map: &Map) -> Vec<IntersectionCluster> {
for entrance in entrances {
uber_turns.extend(flood(entrance, map, &exits));
}
let orig_num = uber_turns.len();
// Filter out the restricted ones!
// TODO Could be more efficient, but eh
uber_turns.retain(|ut| {
let mut ok = true;
for pair in ut.path.windows(2) {
let r1 = map.get_l(pair[0].src).parent;
let r2 = map.get_l(pair[0].dst).parent;
let r3 = map.get_l(pair[1].dst).parent;
if all_restrictions.contains(&(r1, r2, r3)) {
ok = false;
break;
}
}
ok
});
println!(
"Cluster {:?} has {} uber-turns ({} filtered out):",
members,
uber_turns.len(),
orig_num - uber_turns.len()
);
/*for ut in &uber_turns {
print!("- {}", ut.path[0].src);
for t in &ut.path {
@ -82,13 +93,11 @@ pub fn find(map: &Map) -> Vec<IntersectionCluster> {
println!("");
}*/
clusters.push(IntersectionCluster {
IntersectionCluster {
members,
uber_turns,
});
}
}
clusters
}
fn flood(start: TurnID, map: &Map, exits: &HashSet<TurnID>) -> Vec<UberTurn> {
@ -133,3 +142,19 @@ fn trace_back(end: TurnID, preds: &HashMap<TurnID, TurnID>) -> Vec<TurnID> {
}
}
}
impl UberTurn {
pub fn geom(&self, map: &Map) -> PolyLine {
let mut pl = map.get_t(self.path[0]).geom.clone();
let mut first = true;
for pair in self.path.windows(2) {
if !first {
pl = pl.extend(map.get_t(pair[0]).geom.clone());
first = false;
}
pl = pl.extend(map.get_l(pair[0].dst).lane_center_pts.clone());
pl = pl.extend(map.get_t(pair[1]).geom.clone());
}
pl
}
}