From d3c4b0b26a46eca596b31ea96229831cf622ef05 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Wed, 20 May 2020 14:01:19 -0700 Subject: [PATCH] start grouping uber-turns together, to make editing clusters of signals sane. very basic first steps. --- game/src/edit/cluster_traffic_signals.rs | 101 +++++++++++++++++++++++ game/src/edit/mod.rs | 2 + game/src/render/mod.rs | 2 +- game/src/render/turn.rs | 61 +++++++++++++- game/src/sandbox/uber_turns.rs | 7 ++ map_model/src/lib.rs | 2 +- map_model/src/pathfind/driving.rs | 4 +- map_model/src/pathfind/uber_turns.rs | 95 ++++++++++++++++++++- 8 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 game/src/edit/cluster_traffic_signals.rs diff --git a/game/src/edit/cluster_traffic_signals.rs b/game/src/edit/cluster_traffic_signals.rs new file mode 100644 index 0000000000..62daafc95c --- /dev/null +++ b/game/src/edit/cluster_traffic_signals.rs @@ -0,0 +1,101 @@ +use crate::app::{App, ShowEverything}; +use crate::game::{DrawBaselayer, State, Transition}; +use crate::render::{DrawOptions, DrawUberTurnGroup, BIG_ARROW_THICKNESS}; +use ezgui::{ + hotkey, Btn, Composite, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Outcome, + VerticalAlignment, Widget, +}; +use geom::ArrowCap; +use map_model::{IntersectionCluster, IntersectionID}; +use std::collections::BTreeSet; + +pub struct ClusterTrafficSignalEditor { + composite: Composite, + + members: BTreeSet, + groups: Vec, + group_selected: Option, +} + +impl ClusterTrafficSignalEditor { + pub fn new(ctx: &mut EventCtx, app: &mut App, ic: &IntersectionCluster) -> Box { + app.primary.current_selection = None; + Box::new(ClusterTrafficSignalEditor { + composite: Composite::new( + Widget::row(vec![ + Btn::text_fg("Finish").build_def(ctx, hotkey(Key::Escape)) + ]) + .bg(app.cs.panel_bg), + ) + .aligned(HorizontalAlignment::Center, VerticalAlignment::Top) + .build(ctx), + groups: DrawUberTurnGroup::new(ic, &app.primary.map), + group_selected: None, + members: ic.members.clone(), + }) + } +} + +impl State for ClusterTrafficSignalEditor { + fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition { + match self.composite.event(ctx) { + Some(Outcome::Clicked(x)) => match x.as_ref() { + "Finish" => { + return Transition::Pop; + } + _ => unreachable!(), + }, + None => {} + } + + ctx.canvas_movement(); + if ctx.redo_mouseover() { + self.group_selected = None; + if let Some(pt) = ctx.canvas.get_cursor_in_map_space() { + for (idx, g) in self.groups.iter().enumerate() { + if g.block.contains_pt(pt) { + self.group_selected = Some(idx); + break; + } + } + } + } + + 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.members.clone()); + app.draw(g, opts, &app.primary.sim, &ShowEverything::new()); + } + + let mut batch = GeomBatch::new(); + for (idx, g) in self.groups.iter().enumerate() { + if Some(idx) == self.group_selected { + batch.push(app.cs.selected, g.block.clone()); + // Overwrite the original thing + batch.push( + app.cs.selected, + g.group + .geom + .make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle) + .unwrap(), + ); + } else { + batch.push(app.cs.signal_turn_block_bg, g.block.clone()); + } + let arrow_color = app.cs.signal_protected_turn; + batch.push(arrow_color, g.arrow.clone()); + } + batch.draw(g); + + self.composite.draw(g); + } +} diff --git a/game/src/edit/mod.rs b/game/src/edit/mod.rs index cc271248c9..18831859b8 100644 --- a/game/src/edit/mod.rs +++ b/game/src/edit/mod.rs @@ -1,8 +1,10 @@ mod bulk; +mod cluster_traffic_signals; mod lanes; mod stop_signs; mod traffic_signals; +pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor; pub use self::lanes::LaneEditor; pub use self::stop_signs::StopSignEditor; pub use self::traffic_signals::TrafficSignalEditor; diff --git a/game/src/render/mod.rs b/game/src/render/mod.rs index 1640e226de..ed00f45a3c 100644 --- a/game/src/render/mod.rs +++ b/game/src/render/mod.rs @@ -22,7 +22,7 @@ pub use crate::render::map::{AgentCache, AgentColorScheme, DrawMap}; pub use crate::render::pedestrian::{DrawPedCrowd, DrawPedestrian}; pub use crate::render::road::DrawRoad; pub use crate::render::traffic_signal::{draw_signal_phase, make_signal_diagram}; -pub use crate::render::turn::DrawTurnGroup; +pub use crate::render::turn::{DrawTurnGroup, DrawUberTurnGroup}; use ezgui::{GfxCtx, Prerender}; use geom::{Distance, Polygon, Pt2D}; use map_model::{IntersectionID, Map}; diff --git a/game/src/render/turn.rs b/game/src/render/turn.rs index fb39b94b69..37ae18b125 100644 --- a/game/src/render/turn.rs +++ b/game/src/render/turn.rs @@ -1,5 +1,5 @@ use geom::{ArrowCap, Distance, PolyLine, Polygon}; -use map_model::{IntersectionID, LaneID, Map, TurnGroupID}; +use map_model::{IntersectionCluster, IntersectionID, LaneID, Map, TurnGroupID, UberTurnGroup}; use std::collections::{HashMap, HashSet}; const TURN_ICON_ARROW_LENGTH: Distance = Distance::const_meters(1.5); @@ -61,3 +61,62 @@ impl DrawTurnGroup { draw } } + +// TODO Share some code with DrawTurnGroup? +pub struct DrawUberTurnGroup { + pub group: UberTurnGroup, + pub block: Polygon, + pub arrow: Polygon, +} + +impl DrawUberTurnGroup { + pub fn new(ic: &IntersectionCluster, map: &Map) -> Vec { + let mut offset_per_lane: HashMap = HashMap::new(); + let mut draw = Vec::new(); + for group in ic.uber_turn_groups(map) { + // TODO Right now they have one lane, but probably changing this soon + let offset = group + .members + .iter() + .map(|ut| *offset_per_lane.entry(ut.entry()).or_insert(0)) + .max() + .unwrap() as f64; + let (pl, width) = group.src_center_and_width(map); + let slice = if pl.length() >= (offset + 1.0) * TURN_ICON_ARROW_LENGTH { + pl.exact_slice( + offset * TURN_ICON_ARROW_LENGTH, + (offset + 1.0) * TURN_ICON_ARROW_LENGTH, + ) + } else { + pl + }; + let block = slice.make_polygons(width); + + // TODO need to shrink the entire pl + let arrow = { + let center = slice.middle(); + PolyLine::new(vec![ + center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, geom::Angle::new_degs(90.0)), + center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, geom::Angle::new_degs(270.0)), + ]) + .make_arrow(Distance::meters(0.5), ArrowCap::Triangle) + .unwrap() + }; + + let mut seen_lanes = HashSet::new(); + for ut in &group.members { + if !seen_lanes.contains(&ut.entry()) { + *offset_per_lane.get_mut(&ut.entry()).unwrap() += 1; + seen_lanes.insert(ut.entry()); + } + } + + draw.push(DrawUberTurnGroup { + group, + block, + arrow, + }); + } + draw + } +} diff --git a/game/src/sandbox/uber_turns.rs b/game/src/sandbox/uber_turns.rs index 43ec9a1983..d93453e231 100644 --- a/game/src/sandbox/uber_turns.rs +++ b/game/src/sandbox/uber_turns.rs @@ -1,5 +1,6 @@ use crate::app::{App, ShowEverything}; use crate::common::CommonState; +use crate::edit::ClusterTrafficSignalEditor; use crate::game::{msg, DrawBaselayer, State, Transition}; use crate::helpers::ID; use crate::render::{DrawOptions, BIG_ARROW_THICKNESS}; @@ -176,6 +177,7 @@ impl UberTurnViewer { Btn::text_fg("X").build_def(ctx, hotkey(Key::Escape)), ]), Checkbox::text(ctx, "legal / illegal movements", None, legal_turns), + Btn::text_fg("Edit").build_def(ctx, None), ]) .padding(10) .bg(app.cs.panel_bg), @@ -198,6 +200,11 @@ impl State for UberTurnViewer { "X" => { return Transition::Pop; } + "Edit" => { + return Transition::Replace(ClusterTrafficSignalEditor::new( + ctx, app, &self.ic, + )); + } "previous uber-turn" => { return Transition::Replace(UberTurnViewer::new( ctx, diff --git a/map_model/src/lib.rs b/map_model/src/lib.rs index 1b902ba54a..32393868b6 100644 --- a/map_model/src/lib.rs +++ b/map_model/src/lib.rs @@ -24,7 +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::uber_turns::{IntersectionCluster, UberTurn, UberTurnGroup}; pub use crate::pathfind::{Path, PathConstraints, PathRequest, PathStep}; pub use crate::road::{DirectedRoadID, Road, RoadID}; pub use crate::stop_signs::{ControlStopSign, RoadWithStopSign}; diff --git a/map_model/src/pathfind/driving.rs b/map_model/src/pathfind/driving.rs index 1d1f6dd945..b31a76f7a1 100644 --- a/map_model/src/pathfind/driving.rs +++ b/map_model/src/pathfind/driving.rs @@ -144,7 +144,7 @@ fn make_input_graph( .iter() .all(|t| constraints.can_use(map.get_l(t.dst), map)) { - uber_turn_entrances.insert(ut.path[0].src, idx); + uber_turn_entrances.insert(ut.entry(), idx); } else { // Similar to the hack below for unused lanes if idx == uber_turns.len() - 1 { @@ -185,7 +185,7 @@ fn make_input_graph( input_graph.add_edge(from, nodes.get(Node::UberTurn(*idx)), sum_cost.max(1)); input_graph.add_edge( nodes.get(Node::UberTurn(*idx)), - nodes.get(Node::Lane(ut.path.last().unwrap().dst)), + nodes.get(Node::Lane(ut.exit())), // The cost is already captured for entering the uber-turn 1, ); diff --git a/map_model/src/pathfind/uber_turns.rs b/map_model/src/pathfind/uber_turns.rs index 5601370587..2baf163e68 100644 --- a/map_model/src/pathfind/uber_turns.rs +++ b/map_model/src/pathfind/uber_turns.rs @@ -1,5 +1,6 @@ -use crate::{IntersectionID, Map, TurnID}; -use geom::PolyLine; +use crate::{IntersectionID, LaneID, Map, TurnID}; +use abstutil::MultiMap; +use geom::{Distance, PolyLine, Pt2D}; use petgraph::graphmap::UnGraphMap; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -154,6 +155,13 @@ fn trace_back(end: TurnID, preds: &BTreeMap) -> Vec { } impl UberTurn { + pub fn entry(&self) -> LaneID { + self.path[0].src + } + pub fn exit(&self) -> LaneID { + self.path.last().unwrap().dst + } + pub fn geom(&self, map: &Map) -> PolyLine { let mut pl = map.get_t(self.path[0]).geom.clone(); let mut first = true; @@ -168,3 +176,86 @@ impl UberTurn { pl } } + +pub struct UberTurnGroup { + pub members: Vec, + pub geom: PolyLine, +} + +impl IntersectionCluster { + pub fn uber_turn_groups(&self, map: &Map) -> Vec { + let mut groups: MultiMap<(LaneID, LaneID), usize> = MultiMap::new(); + for (idx, ut) in self.uber_turns.iter().enumerate() { + groups.insert((ut.entry(), ut.exit()), idx); + } + + let mut result = Vec::new(); + for (_, member_indices) in groups.consume() { + let mut members = Vec::new(); + let mut polylines = Vec::new(); + for idx in member_indices { + polylines.push(self.uber_turns[idx].geom(map)); + members.push(self.uber_turns[idx].clone()); + } + result.push(UberTurnGroup { + members, + geom: group_geom(polylines), + }); + } + result + } +} + +impl UberTurnGroup { + // TODO Share code with TurnGroup + // Polyline points FROM intersection + pub fn src_center_and_width(&self, map: &Map) -> (PolyLine, Distance) { + let sample_entry = self.members[0].entry(); + let r = map.get_parent(sample_entry); + let dir = r.is_forwards(sample_entry); + // Points towards the intersection + let pl = if dir { + r.get_current_center(map) + } else { + r.get_current_center(map).reversed() + }; + + // TODO Poorly expressed. We just want the first leftmost value, and the last rightmost. + let mut leftmost = Distance::meters(99999.0); + let mut rightmost = Distance::ZERO; + let mut left = Distance::ZERO; + let mut right = Distance::ZERO; + + for l in r.lanes_on_side(dir) { + right += map.get_l(l).width; + + if self.members.iter().any(|ut| ut.entry() == l) { + leftmost = leftmost.min(left); + rightmost = rightmost.max(right); + } + + left += map.get_l(l).width; + } + + let pl = map.right_shift(pl, (leftmost + rightmost) / 2.0).unwrap(); + // Flip direction, so we point away from the intersection + (pl.reversed(), rightmost - leftmost) + } +} + +fn group_geom(mut polylines: Vec) -> PolyLine { + let num_pts = polylines[0].points().len(); + for pl in &polylines { + if num_pts != pl.points().len() { + return polylines.remove(0); + } + } + + let mut pts = Vec::new(); + for idx in 0..num_pts { + pts.push(Pt2D::center( + &polylines.iter().map(|pl| pl.points()[idx]).collect(), + )); + } + PolyLine::new(pts) +}