game/edit/traffic_signals/
offsets.rs

1use std::collections::BTreeSet;
2
3use maplit::btreeset;
4
5use geom::{Distance, Duration};
6use map_model::IntersectionID;
7use widgetry::{
8    Color, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Panel, RewriteColor,
9    SimpleState, Spinner, State, Text, TextExt, VerticalAlignment, Widget,
10};
11
12use crate::app::{App, Transition};
13use crate::common::CommonState;
14use crate::edit::traffic_signals::fade_irrelevant;
15
16pub struct ShowAbsolute {
17    members: BTreeSet<IntersectionID>,
18    labels: Drawable,
19}
20
21impl ShowAbsolute {
22    pub fn new_state(
23        ctx: &mut EventCtx,
24        app: &App,
25        members: BTreeSet<IntersectionID>,
26    ) -> Box<dyn State<App>> {
27        let mut batch = fade_irrelevant(app, &members);
28        for i in &members {
29            batch.append(
30                Text::from(
31                    app.primary
32                        .map
33                        .get_traffic_signal(*i)
34                        .offset
35                        .to_string(&app.opts.units),
36                )
37                .bg(Color::PURPLE)
38                .render_autocropped(ctx)
39                .color(RewriteColor::ChangeAlpha(0.8))
40                .scale(0.3)
41                .centered_on(app.primary.map.get_i(*i).polygon.center()),
42            );
43        }
44
45        let panel = Panel::new_builder(Widget::col(vec![
46            Widget::row(vec![
47                Line(format!("Tuning offset for {} signals", members.len()))
48                    .small_heading()
49                    .into_widget(ctx),
50                ctx.style().btn_close_widget(ctx),
51            ]),
52            "Select an intersection as the base".text_widget(ctx),
53        ]))
54        .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
55        .build(ctx);
56        <dyn SimpleState<_>>::new_state(
57            panel,
58            Box::new(ShowAbsolute {
59                members,
60                labels: ctx.upload(batch),
61            }),
62        )
63    }
64}
65
66impl SimpleState<App> for ShowAbsolute {
67    fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &mut Panel) -> Transition {
68        match x {
69            "close" => {
70                // TODO Bit confusing UX, because all the offset changes won't show up in the
71                // undo stack. Could maybe do ConsumeState.
72                Transition::Pop
73            }
74            _ => unreachable!(),
75        }
76    }
77
78    fn on_mouseover(&mut self, ctx: &mut EventCtx, app: &mut App) {
79        app.primary.current_selection = app
80            .mouseover_unzoomed_intersections(ctx)
81            .filter(|id| self.members.contains(&id.as_intersection()));
82    }
83
84    fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
85        ctx.canvas_movement();
86        if let Some(i) = app.click_on_intersection(ctx, "select base intersection") {
87            return Transition::Replace(ShowRelative::new_state(ctx, app, i, self.members.clone()));
88        }
89
90        Transition::Keep
91    }
92
93    fn draw(&self, g: &mut GfxCtx, app: &App) {
94        CommonState::draw_osd(g, app);
95
96        g.redraw(&self.labels);
97    }
98}
99
100struct ShowRelative {
101    base: IntersectionID,
102    members: BTreeSet<IntersectionID>,
103    labels: Drawable,
104}
105
106impl ShowRelative {
107    pub fn new_state(
108        ctx: &mut EventCtx,
109        app: &App,
110        base: IntersectionID,
111        members: BTreeSet<IntersectionID>,
112    ) -> Box<dyn State<App>> {
113        let base_offset = app.primary.map.get_traffic_signal(base).offset;
114        let mut batch = fade_irrelevant(app, &members);
115        for i in &members {
116            if *i == base {
117                batch.push(
118                    Color::BLUE.alpha(0.8),
119                    app.primary.map.get_i(*i).polygon.clone(),
120                );
121            } else {
122                let offset = app.primary.map.get_traffic_signal(*i).offset - base_offset;
123                batch.append(
124                    Text::from(offset.to_string(&app.opts.units))
125                        .bg(Color::PURPLE)
126                        .render_autocropped(ctx)
127                        .color(RewriteColor::ChangeAlpha(0.8))
128                        .scale(0.3)
129                        .centered_on(app.primary.map.get_i(*i).polygon.center()),
130                );
131            }
132        }
133
134        let panel = Panel::new_builder(Widget::col(vec![
135            Widget::row(vec![
136                Line(format!("Tuning offset for {} signals", members.len()))
137                    .small_heading()
138                    .into_widget(ctx),
139                ctx.style().btn_close_widget(ctx),
140            ]),
141            "Select a second intersection to tune offset between the two".text_widget(ctx),
142        ]))
143        .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
144        .build(ctx);
145        <dyn SimpleState<_>>::new_state(
146            panel,
147            Box::new(ShowRelative {
148                base,
149                members,
150                labels: ctx.upload(batch),
151            }),
152        )
153    }
154}
155
156impl SimpleState<App> for ShowRelative {
157    fn on_click(
158        &mut self,
159        ctx: &mut EventCtx,
160        app: &mut App,
161        x: &str,
162        _: &mut Panel,
163    ) -> Transition {
164        match x {
165            "close" => Transition::Replace(ShowAbsolute::new_state(ctx, app, self.members.clone())),
166            _ => unreachable!(),
167        }
168    }
169
170    fn on_mouseover(&mut self, ctx: &mut EventCtx, app: &mut App) {
171        app.primary.current_selection = app.mouseover_unzoomed_intersections(ctx).filter(|id| {
172            let i = id.as_intersection();
173            self.members.contains(&i) && i != self.base
174        });
175    }
176
177    fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
178        ctx.canvas_movement();
179        if let Some(i) = app.click_on_intersection(ctx, "select second intersection") {
180            return Transition::Push(TuneRelative::new_state(
181                ctx,
182                app,
183                self.base,
184                i,
185                self.members.clone(),
186            ));
187        }
188
189        Transition::Keep
190    }
191
192    fn draw(&self, g: &mut GfxCtx, app: &App) {
193        CommonState::draw_osd(g, app);
194
195        g.redraw(&self.labels);
196    }
197}
198
199struct TuneRelative {
200    i1: IntersectionID,
201    i2: IntersectionID,
202    members: BTreeSet<IntersectionID>,
203    labels: Drawable,
204}
205
206impl TuneRelative {
207    pub fn new_state(
208        ctx: &mut EventCtx,
209        app: &App,
210        i1: IntersectionID,
211        i2: IntersectionID,
212        members: BTreeSet<IntersectionID>,
213    ) -> Box<dyn State<App>> {
214        let mut batch = fade_irrelevant(app, &btreeset! {i1, i2});
215        let map = &app.primary.map;
216        // TODO Colors aren't clear. Show directionality.
217        batch.push(Color::BLUE.alpha(0.8), map.get_i(i1).polygon.clone());
218        batch.push(Color::RED.alpha(0.8), map.get_i(i2).polygon.clone());
219        let path = map
220            .simple_path_btwn(i1, i2)
221            .map(|pair| pair.0)
222            .unwrap_or_else(Vec::new);
223        let mut dist_btwn = Distance::ZERO;
224        let mut car_dt = Duration::ZERO;
225        for r in path {
226            let r = map.get_r(r);
227            // TODO Glue polylines together and do dashed_lines
228            batch.push(app.cs.route, r.get_thick_polygon());
229            dist_btwn += r.length();
230            car_dt += r.length() / r.speed_limit;
231        }
232
233        let offset1 = map.get_traffic_signal(i1).offset;
234        let offset2 = map.get_traffic_signal(i2).offset;
235        let panel = Panel::new_builder(Widget::col(vec![
236            Widget::row(vec![
237                Line(format!("Tuning offset between {} and {}", i1, i2))
238                    .small_heading()
239                    .into_widget(ctx),
240                ctx.style().btn_close_widget(ctx),
241            ]),
242            Text::from_multiline(vec![
243                Line(format!("Distance: {}", dist_btwn)),
244                Line(format!(
245                    "  about {} for a car if there's no congestion",
246                    car_dt
247                )),
248                // TODO We could calculate a full path and incorporate incline
249                Line(format!(
250                    "  about {} for a bike",
251                    dist_btwn / map_model::MAX_BIKE_SPEED
252                )),
253                Line(format!(
254                    "  about {} for a pedestrian",
255                    dist_btwn / map_model::MAX_WALKING_SPEED
256                )),
257            ])
258            .into_widget(ctx),
259            Widget::row(vec![
260                "Offset:".text_widget(ctx).centered_vert(),
261                Spinner::widget(
262                    ctx,
263                    "offset",
264                    (Duration::ZERO, Duration::seconds(90.0)),
265                    offset2 - offset1,
266                    Duration::seconds(1.0),
267                ),
268            ]),
269            ctx.style()
270                .btn_outline
271                .text("Update offset")
272                .hotkey(Key::Enter)
273                .build_def(ctx),
274        ]))
275        .build(ctx);
276        <dyn SimpleState<_>>::new_state(
277            panel,
278            Box::new(TuneRelative {
279                i1,
280                i2,
281                members,
282                labels: ctx.upload(batch),
283            }),
284        )
285    }
286}
287
288impl SimpleState<App> for TuneRelative {
289    fn on_click(
290        &mut self,
291        ctx: &mut EventCtx,
292        app: &mut App,
293        x: &str,
294        panel: &mut Panel,
295    ) -> Transition {
296        match x {
297            "close" => Transition::Pop,
298            "Update offset" => {
299                let mut ts = app.primary.map.get_traffic_signal(self.i2).clone();
300                let relative = panel.spinner("offset");
301                let offset1 = app.primary.map.get_traffic_signal(self.i1).offset;
302                ts.offset = offset1 + relative;
303                app.primary.map.incremental_edit_traffic_signal(ts);
304                Transition::Multi(vec![
305                    Transition::Pop,
306                    Transition::Replace(ShowRelative::new_state(
307                        ctx,
308                        app,
309                        self.i1,
310                        self.members.clone(),
311                    )),
312                ])
313            }
314            _ => unreachable!(),
315        }
316    }
317
318    fn other_event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
319        ctx.canvas_movement();
320        Transition::Keep
321    }
322
323    fn draw(&self, g: &mut GfxCtx, _: &App) {
324        g.redraw(&self.labels);
325    }
326}