map_gui/render/
turn.rs

1use std::collections::{HashMap, HashSet};
2
3use geom::{Angle, ArrowCap, Circle, Distance, PolyLine, Polygon};
4use map_model::{IntersectionID, LaneID, Map, MovementID, TurnPriority, SIDEWALK_THICKNESS};
5use widgetry::{Color, GeomBatch, Prerender};
6
7use crate::colors::ColorScheme;
8use crate::render::{traffic_signal, BIG_ARROW_THICKNESS};
9use crate::AppLike;
10
11const TURN_ICON_ARROW_LENGTH: Distance = Distance::const_meters(1.5);
12
13pub struct DrawMovement {
14    pub id: MovementID,
15    pub hitbox: Polygon,
16}
17
18impl DrawMovement {
19    // Only for traffic signals! Also returns the stuff to draw each movement
20    pub fn for_i(
21        prerender: &Prerender,
22        map: &Map,
23        cs: &ColorScheme,
24        i: IntersectionID,
25        idx: usize,
26    ) -> Vec<(DrawMovement, GeomBatch)> {
27        let signal = map.get_traffic_signal(i);
28        let stage = &signal.stages[idx];
29
30        // TODO Sort by angle here if we want some consistency
31        let mut offset_per_lane: HashMap<LaneID, usize> = HashMap::new();
32        let mut results = Vec::new();
33        for movement in map.get_i(i).movements.values() {
34            let mut batch = GeomBatch::new();
35            // TODO Refactor the slice_start/slice_end stuff from draw_signal_stage.
36            let hitbox = if stage.protected_movements.contains(&movement.id) {
37                if movement.id.crosswalk {
38                    batch = traffic_signal::walk_icon(movement, prerender);
39                    batch.get_bounds().to_circle().to_polygon()
40                } else {
41                    let arrow = movement
42                        .geom
43                        .make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
44                    batch.push(cs.signal_protected_turn, arrow.clone());
45                    batch.push(Color::BLACK, arrow.to_outline(Distance::meters(0.2)));
46                    arrow
47                }
48            } else if stage.yield_movements.contains(&movement.id) {
49                let pl = &movement.geom;
50                // We currently always assume the turn intersects a crosswalk at the beginning and
51                // end, so draw without overlaps if the polyline is long enough.
52                if pl.length() >= 2.0 * SIDEWALK_THICKNESS {
53                    batch.extend(
54                        Color::BLACK,
55                        pl.exact_slice(
56                            SIDEWALK_THICKNESS - Distance::meters(0.1),
57                            pl.length() - SIDEWALK_THICKNESS + Distance::meters(0.1),
58                        )
59                        .dashed_arrow(
60                            BIG_ARROW_THICKNESS,
61                            Distance::meters(1.2),
62                            Distance::meters(0.3),
63                            ArrowCap::Triangle,
64                        ),
65                    );
66                    let arrow = pl
67                        .exact_slice(SIDEWALK_THICKNESS, pl.length() - SIDEWALK_THICKNESS)
68                        .dashed_arrow(
69                            BIG_ARROW_THICKNESS / 2.0,
70                            Distance::meters(1.0),
71                            Distance::meters(0.5),
72                            ArrowCap::Triangle,
73                        );
74                    batch.extend(cs.signal_protected_turn, arrow.clone());
75                } else {
76                    // TODO These turns are often too small to even dash the arrow. So they'll just
77                    // look like solid protected turns...
78                    warn!(
79                        "{:?} is too short to render as a yield movement",
80                        movement.id
81                    );
82                    batch.extend(
83                        cs.signal_protected_turn,
84                        pl.dashed_arrow(
85                            BIG_ARROW_THICKNESS / 2.0,
86                            Distance::meters(1.0),
87                            Distance::meters(0.5),
88                            ArrowCap::Triangle,
89                        ),
90                    );
91                }
92                // Bit weird, but don't use the dashed arrow as the hitbox. The gaps in between
93                // should still be clickable.
94                movement
95                    .geom
96                    .make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle)
97            } else if movement.id.crosswalk {
98                batch = traffic_signal::dont_walk_icon(movement, prerender);
99                batch.get_bounds().to_circle().to_polygon()
100            } else {
101                // Use circular icons for banned turns
102                let offset = movement
103                    .members
104                    .iter()
105                    .map(|t| *offset_per_lane.entry(t.src).or_insert(0))
106                    .max()
107                    .unwrap();
108                let (pl, _) = movement.src_center_and_width(map);
109                let (circle, arrow) = make_circle_geom(offset as f64, pl, movement.angle);
110                let mut seen_lanes = HashSet::new();
111                for t in &movement.members {
112                    if !seen_lanes.contains(&t.src) {
113                        *offset_per_lane.get_mut(&t.src).unwrap() = offset + 1;
114                        seen_lanes.insert(t.src);
115                    }
116                }
117                batch.push(cs.signal_banned_turn.alpha(0.5), circle.clone());
118                batch.push(Color::WHITE, arrow);
119                circle
120            };
121            results.push((
122                DrawMovement {
123                    id: movement.id,
124                    hitbox,
125                },
126                batch,
127            ));
128        }
129        results
130    }
131
132    pub fn draw_selected_movement(
133        &self,
134        app: &dyn AppLike,
135        batch: &mut GeomBatch,
136        next_priority: Option<TurnPriority>,
137    ) {
138        let movement = &app.map().get_i(self.id.parent).movements[&self.id];
139        let pl = &movement.geom;
140
141        let green = Color::hex("#72CE36");
142        match next_priority {
143            Some(TurnPriority::Protected) => {
144                let arrow = pl.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
145                batch.push(green.alpha(0.5), arrow.clone());
146                batch.push(green, arrow.to_outline(Distance::meters(0.1)));
147            }
148            Some(TurnPriority::Yield) => {
149                batch.extend(
150                    // TODO Ideally the inner part would be the lower opacity green, but can't yet
151                    // express that it should cover up the thicker solid blue beneath it
152                    Color::BLACK.alpha(0.8),
153                    pl.dashed_arrow(
154                        BIG_ARROW_THICKNESS,
155                        Distance::meters(1.2),
156                        Distance::meters(0.3),
157                        ArrowCap::Triangle,
158                    ),
159                );
160                batch.extend(
161                    green.alpha(0.8),
162                    pl.exact_slice(Distance::meters(0.1), pl.length() - Distance::meters(0.1))
163                        .dashed_arrow(
164                            BIG_ARROW_THICKNESS / 2.0,
165                            Distance::meters(1.0),
166                            Distance::meters(0.5),
167                            ArrowCap::Triangle,
168                        ),
169                );
170            }
171            Some(TurnPriority::Banned) => {
172                batch.extend(
173                    Color::BLACK.alpha(0.8),
174                    pl.dashed_arrow(
175                        BIG_ARROW_THICKNESS,
176                        Distance::meters(1.2),
177                        Distance::meters(0.3),
178                        ArrowCap::Triangle,
179                    ),
180                );
181                batch.extend(
182                    app.cs().signal_banned_turn.alpha(0.8),
183                    pl.exact_slice(Distance::meters(0.1), pl.length() - Distance::meters(0.1))
184                        .dashed_arrow(
185                            BIG_ARROW_THICKNESS / 2.0,
186                            Distance::meters(1.0),
187                            Distance::meters(0.5),
188                            ArrowCap::Triangle,
189                        ),
190                );
191            }
192            None => {}
193        }
194    }
195}
196
197// Produces (circle, arrow)
198fn make_circle_geom(offset: f64, pl: PolyLine, turn_angle: Angle) -> (Polygon, Polygon) {
199    let height = 2.0 * TURN_ICON_ARROW_LENGTH;
200    // Always extend the pl first to handle short entry lanes
201    let extension = PolyLine::must_new(vec![
202        pl.last_pt(),
203        pl.last_pt()
204            .project_away(Distance::meters(500.0), pl.last_line().angle()),
205    ]);
206    let pl = pl.must_extend(extension);
207    let slice = pl.exact_slice(offset * height, (offset + 1.0) * height);
208    let center = slice.middle();
209    let block = Circle::new(center, TURN_ICON_ARROW_LENGTH).to_polygon();
210
211    let arrow_angle = pl.last_line().angle().opposite() + turn_angle;
212    let arrow = PolyLine::must_new(vec![
213        center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, arrow_angle.opposite()),
214        center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, arrow_angle),
215    ])
216    .make_arrow(Distance::meters(0.5), ArrowCap::Triangle);
217
218    (block, arrow)
219}