map_gui/render/
traffic_signal.rs

1use std::collections::BTreeSet;
2
3use geom::{Angle, ArrowCap, Circle, Distance, Duration, Line, PolyLine, Pt2D};
4use map_model::{
5    Intersection, IntersectionID, Movement, Stage, StageType, TurnPriority, SIDEWALK_THICKNESS,
6};
7use widgetry::{Color, GeomBatch, Line, Prerender, RewriteColor, Text};
8
9use crate::options::TrafficSignalStyle;
10use crate::render::intersection::make_crosswalk;
11use crate::render::BIG_ARROW_THICKNESS;
12use crate::AppLike;
13
14pub fn draw_signal_stage(
15    prerender: &Prerender,
16    stage: &Stage,
17    idx: usize,
18    i: IntersectionID,
19    time_left: Option<Duration>,
20    batch: &mut GeomBatch,
21    app: &dyn AppLike,
22    signal_style: TrafficSignalStyle,
23) {
24    let i = app.map().get_i(i);
25
26    match signal_style {
27        TrafficSignalStyle::Brian => {
28            let mut dont_walk = BTreeSet::new();
29            let mut crossed_roads = BTreeSet::new();
30            for m in i.movements.keys() {
31                if m.crosswalk {
32                    dont_walk.insert(m);
33                    // TODO This is incorrect; some crosswalks hop over intermediate roads. How do
34                    // we detect or plumb that?
35                    crossed_roads.insert((m.from.road, m.parent));
36                    crossed_roads.insert((m.to.road, m.parent));
37                }
38            }
39
40            let (yellow_light, percent) = if let Some(t) = time_left {
41                if stage.stage_type.simple_duration() > Duration::ZERO {
42                    (
43                        t <= Duration::seconds(5.0),
44                        (t / stage.stage_type.simple_duration()) as f32,
45                    )
46                } else {
47                    (true, 1.0)
48                }
49            } else {
50                (false, 1.0)
51            };
52            let arrow_body_color = if yellow_light {
53                // The warning color for fixed is yellow, for anything else its orange to clue the
54                // user into it possibly extending.
55                if let StageType::Fixed(_) = stage.stage_type {
56                    Color::YELLOW
57                } else {
58                    Color::ORANGE
59                }
60            } else {
61                app.cs().signal_protected_turn.alpha(percent)
62            };
63
64            for m in &stage.yield_movements {
65                assert!(!m.crosswalk);
66                let pl = &i.movements[m].geom;
67                // TODO Make dashed_arrow draw the last polygon without an awkward overlap. Then we
68                // can just make one call here and control the outline thickness just using
69                // to_outline.
70                if let Ok(slice) = pl.maybe_exact_slice(
71                    SIDEWALK_THICKNESS - Distance::meters(0.1),
72                    pl.length() - SIDEWALK_THICKNESS + Distance::meters(0.1),
73                ) {
74                    batch.extend(
75                        Color::BLACK,
76                        slice.dashed_arrow(
77                            BIG_ARROW_THICKNESS,
78                            Distance::meters(1.2),
79                            Distance::meters(0.3),
80                            ArrowCap::Triangle,
81                        ),
82                    );
83                }
84                if let Ok(slice) =
85                    pl.maybe_exact_slice(SIDEWALK_THICKNESS, pl.length() - SIDEWALK_THICKNESS)
86                {
87                    batch.extend(
88                        arrow_body_color,
89                        slice.dashed_arrow(
90                            BIG_ARROW_THICKNESS / 2.0,
91                            Distance::meters(1.0),
92                            Distance::meters(0.5),
93                            ArrowCap::Triangle,
94                        ),
95                    );
96                }
97            }
98
99            for m in &stage.protected_movements {
100                if !m.crosswalk {
101                    // TODO Maybe less if shoulders meet
102                    let slice_start = if crossed_roads.contains(&(m.from.road, m.parent)) {
103                        SIDEWALK_THICKNESS
104                    } else {
105                        Distance::ZERO
106                    };
107                    let slice_end = if crossed_roads.contains(&(m.to.road, m.parent)) {
108                        SIDEWALK_THICKNESS
109                    } else {
110                        Distance::ZERO
111                    };
112
113                    let pl = &i.movements[m].geom;
114                    if let Ok(pl) = pl.maybe_exact_slice(slice_start, pl.length() - slice_end) {
115                        let arrow = pl.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
116                        batch.push(arrow_body_color, arrow.clone());
117                        batch.push(Color::BLACK, arrow.to_outline(Distance::meters(0.2)));
118                    }
119                } else {
120                    batch.append(
121                        walk_icon(&i.movements[m], prerender)
122                            .color(RewriteColor::ChangeAlpha(percent)),
123                    );
124                    dont_walk.remove(m);
125                }
126            }
127
128            for m in dont_walk {
129                batch.append(dont_walk_icon(&i.movements[m], prerender));
130            }
131
132            draw_stage_number(prerender, i, idx, batch);
133        }
134        TrafficSignalStyle::Yuwen => {
135            for m in &stage.yield_movements {
136                assert!(!m.crosswalk);
137                let arrow = i.movements[m]
138                    .geom
139                    .make_arrow(BIG_ARROW_THICKNESS * 2.0, ArrowCap::Triangle);
140                batch.push(app.cs().signal_permitted_turn.alpha(0.3), arrow.clone());
141                batch.push(
142                    app.cs().signal_permitted_turn,
143                    arrow.to_outline(BIG_ARROW_THICKNESS / 2.0),
144                );
145            }
146            for m in &stage.protected_movements {
147                if m.crosswalk {
148                    // TODO This only works on the side panel. On the full map, the crosswalks are
149                    // always drawn, so this awkwardly doubles some of them.
150                    make_crosswalk(
151                        batch,
152                        app.map().get_t(i.movements[m].members[0]),
153                        app.map(),
154                        app.cs(),
155                    );
156                } else {
157                    batch.push(
158                        app.cs().signal_protected_turn,
159                        i.movements[m]
160                            .geom
161                            .make_arrow(BIG_ARROW_THICKNESS * 2.0, ArrowCap::Triangle),
162                    );
163                }
164            }
165            if let Some(t) = time_left {
166                draw_time_left(app, prerender, stage, i, idx, t, batch);
167            }
168        }
169        TrafficSignalStyle::IndividualTurnArrows => {
170            for turn in &i.turns {
171                if turn.between_sidewalks() {
172                    continue;
173                }
174                match stage.get_priority_of_turn(turn.id, i) {
175                    TurnPriority::Protected => {
176                        batch.push(
177                            app.cs().signal_protected_turn,
178                            turn.geom
179                                .make_arrow(BIG_ARROW_THICKNESS * 2.0, ArrowCap::Triangle),
180                        );
181                    }
182                    TurnPriority::Yield => {
183                        batch.push(
184                            app.cs().signal_permitted_turn,
185                            turn.geom
186                                .make_arrow(BIG_ARROW_THICKNESS * 2.0, ArrowCap::Triangle)
187                                .to_outline(BIG_ARROW_THICKNESS / 2.0),
188                        );
189                    }
190                    TurnPriority::Banned => {}
191                }
192            }
193            if let Some(t) = time_left {
194                draw_time_left(app, prerender, stage, i, idx, t, batch);
195            }
196        }
197    }
198}
199
200pub fn draw_stage_number(
201    prerender: &Prerender,
202    i: &Intersection,
203    idx: usize,
204    batch: &mut GeomBatch,
205) {
206    let radius = Distance::meters(1.0);
207    let center = i.polygon.polylabel();
208    batch.push(
209        Color::hex("#5B5B5B"),
210        Circle::new(center, radius).to_polygon(),
211    );
212    batch.append(
213        Text::from(Line(format!("{}", idx + 1)).fg(Color::WHITE))
214            .render_autocropped(prerender)
215            .scale(0.075)
216            .centered_on(center),
217    );
218}
219
220fn draw_time_left(
221    app: &dyn AppLike,
222    prerender: &Prerender,
223    stage: &Stage,
224    i: &Intersection,
225    idx: usize,
226    time_left: Duration,
227    batch: &mut GeomBatch,
228) {
229    let radius = Distance::meters(2.0);
230    let center = i.polygon.center();
231    let duration = stage.stage_type.simple_duration();
232    let percent = if duration > Duration::ZERO {
233        time_left / duration
234    } else {
235        1.0
236    };
237    batch.push(
238        app.cs().signal_box,
239        Circle::new(center, 1.2 * radius).to_polygon(),
240    );
241    batch.push(
242        app.cs().signal_spinner.alpha(0.3),
243        Circle::new(center, radius).to_polygon(),
244    );
245    batch.push(
246        app.cs().signal_spinner,
247        Circle::new(center, radius).to_partial_tessellation(percent),
248    );
249    batch.append(
250        Text::from(format!("{}", idx + 1))
251            .render_autocropped(prerender)
252            .scale(0.1)
253            .centered_on(center),
254    );
255}
256
257pub fn walk_icon(movement: &Movement, prerender: &Prerender) -> GeomBatch {
258    let (center, angle) = crosswalk_icon(&movement.geom);
259    GeomBatch::load_svg(prerender, "system/assets/map/walk.svg")
260        .scale(0.07)
261        .centered_on(center)
262        .rotate(angle)
263}
264pub fn dont_walk_icon(movement: &Movement, prerender: &Prerender) -> GeomBatch {
265    let (center, angle) = crosswalk_icon(&movement.geom);
266    GeomBatch::load_svg(prerender, "system/assets/map/dont_walk.svg")
267        .scale(0.07)
268        .centered_on(center)
269        .rotate(angle)
270}
271
272// TODO Kind of a hack to know that the second point is a better center.
273// Returns (center, angle)
274fn crosswalk_icon(geom: &PolyLine) -> (Pt2D, Angle) {
275    let l = Line::must_new(geom.points()[1], geom.points()[2]);
276    (
277        l.dist_along(Distance::meters(1.0))
278            .unwrap_or_else(|_| l.pt1()),
279        l.angle().shortest_rotation_towards(Angle::degrees(90.0)),
280    )
281}