game/edit/traffic_signals/
offsets.rs1use 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 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 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 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 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}