1 mod edits;
2 mod picker;
3 mod preview;
4
5 use crate::app::{App, ShowEverything};
6 use crate::common::CommonState;
7 use crate::edit::apply_map_edits;
8 use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
9 use crate::options::TrafficSignalStyle;
10 use crate::render::{draw_signal_stage, DrawMovement, DrawOptions, BIG_ARROW_THICKNESS};
11 use crate::sandbox::GameplayMode;
12 use abstutil::Timer;
13 use geom::{ArrowCap, Distance, Duration, Line, Polygon, Pt2D};
14 use map_model::{
15 ControlTrafficSignal, EditCmd, EditIntersection, IntersectionID, Movement, MovementID,
16 PhaseType, Stage, TurnPriority,
17 };
18 use std::collections::{BTreeSet, HashMap, VecDeque};
19 use widgetry::{
20 hotkey, lctrl, Btn, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
21 Line, Outcome, Panel, RewriteColor, Spinner, Text, TextExt, VerticalAlignment, Widget,
22 };
23
24 // Welcome to one of the most overwhelmingly complicated parts of the UI...
25
26 pub struct TrafficSignalEditor {
27 side_panel: Panel,
28 top_panel: Panel,
29
30 mode: GameplayMode,
31 members: BTreeSet<IntersectionID>,
32 current_stage: usize,
33
34 movements: Vec<DrawMovement>,
35 // And the next priority to toggle to
36 movement_selected: Option<(MovementID, Option<TurnPriority>)>,
37 draw_current: Drawable,
38
39 command_stack: Vec<BundleEdits>,
40 redo_stack: Vec<BundleEdits>,
41 // Before synchronizing the number of stages
42 original: BundleEdits,
43 warn_changed: bool,
44
45 fade_irrelevant: Drawable,
46 }
47
48 // For every member intersection, the full state of that signal
49 #[derive(Clone, PartialEq)]
50 pub struct BundleEdits {
51 signals: Vec<ControlTrafficSignal>,
52 }
53
54 impl TrafficSignalEditor {
new( ctx: &mut EventCtx, app: &mut App, members: BTreeSet<IntersectionID>, mode: GameplayMode, ) -> Box<dyn State>55 pub fn new(
56 ctx: &mut EventCtx,
57 app: &mut App,
58 members: BTreeSet<IntersectionID>,
59 mode: GameplayMode,
60 ) -> Box<dyn State> {
61 let map = &app.primary.map;
62 app.primary.current_selection = None;
63
64 let fade_area = {
65 let mut holes = Vec::new();
66 for i in &members {
67 let i = map.get_i(*i);
68 holes.push(i.polygon.clone());
69 for r in &i.roads {
70 holes.push(map.get_r(*r).get_thick_polygon(map));
71 }
72 }
73 // The convex hull illuminates a bit more of the surrounding area, looks better
74 Polygon::with_holes(
75 map.get_boundary_polygon().clone().into_ring(),
76 vec![Polygon::convex_hull(holes).into_ring()],
77 )
78 };
79
80 let mut movements = Vec::new();
81 for i in &members {
82 movements.extend(DrawMovement::for_i(*i, &app.primary.map));
83 }
84
85 let original = BundleEdits::get_current(app, &members);
86 let synced = BundleEdits::synchronize(app, &members);
87 let warn_changed = original != synced;
88 synced.apply(app);
89
90 let mut editor = TrafficSignalEditor {
91 side_panel: make_side_panel(ctx, app, &members, 0, None),
92 top_panel: make_top_panel(ctx, app, false, false),
93 mode,
94 members,
95 current_stage: 0,
96 movements,
97 movement_selected: None,
98 draw_current: ctx.upload(GeomBatch::new()),
99 command_stack: Vec::new(),
100 redo_stack: Vec::new(),
101 warn_changed,
102 original,
103 fade_irrelevant: GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)]).upload(ctx),
104 };
105 editor.draw_current = editor.recalc_draw_current(ctx, app);
106 Box::new(editor)
107 }
108
change_stage(&mut self, ctx: &mut EventCtx, app: &App, idx: usize)109 fn change_stage(&mut self, ctx: &mut EventCtx, app: &App, idx: usize) {
110 let hovering = self.movement_selected.map(|(m, _)| m.parent);
111
112 if self.current_stage == idx {
113 let mut new = make_side_panel(ctx, app, &self.members, self.current_stage, hovering);
114 new.restore(ctx, &self.side_panel);
115 self.side_panel = new;
116 } else {
117 self.current_stage = idx;
118 self.side_panel =
119 make_side_panel(ctx, app, &self.members, self.current_stage, hovering);
120 // TODO Maybe center of previous member
121 self.side_panel
122 .scroll_to_member(ctx, format!("stage {}", idx + 1));
123 }
124
125 self.draw_current = self.recalc_draw_current(ctx, app);
126 }
127
add_new_edit<F: Fn(&mut ControlTrafficSignal)>( &mut self, ctx: &mut EventCtx, app: &mut App, idx: usize, fxn: F, )128 fn add_new_edit<F: Fn(&mut ControlTrafficSignal)>(
129 &mut self,
130 ctx: &mut EventCtx,
131 app: &mut App,
132 idx: usize,
133 fxn: F,
134 ) {
135 let mut bundle = BundleEdits::get_current(app, &self.members);
136 self.command_stack.push(bundle.clone());
137 self.redo_stack.clear();
138 for ts in &mut bundle.signals {
139 fxn(ts);
140 }
141 bundle.apply(app);
142
143 self.top_panel = make_top_panel(ctx, app, true, false);
144 self.change_stage(ctx, app, idx);
145 }
146
recalc_draw_current(&self, ctx: &mut EventCtx, app: &App) -> Drawable147 fn recalc_draw_current(&self, ctx: &mut EventCtx, app: &App) -> Drawable {
148 let mut batch = GeomBatch::new();
149
150 for i in &self.members {
151 let signal = app.primary.map.get_traffic_signal(*i);
152 let mut stage = signal.stages[self.current_stage].clone();
153 if let Some((id, _)) = self.movement_selected {
154 if id.parent == signal.id {
155 stage.edit_movement(&signal.movements[&id], TurnPriority::Banned);
156 }
157 }
158 draw_signal_stage(
159 ctx.prerender,
160 &stage,
161 signal.id,
162 None,
163 &mut batch,
164 app,
165 app.opts.traffic_signal_style.clone(),
166 );
167 }
168
169 for m in &self.movements {
170 let signal = app.primary.map.get_traffic_signal(m.id.parent);
171 if self
172 .movement_selected
173 .as_ref()
174 .map(|(id, _)| *id == m.id)
175 .unwrap_or(false)
176 {
177 draw_selected_movement(
178 app,
179 &mut batch,
180 m,
181 &signal.movements[&m.id],
182 self.movement_selected.unwrap().1,
183 );
184 } else {
185 batch.push(app.cs.signal_turn_block_bg, m.block.clone());
186 let stage = &signal.stages[self.current_stage];
187 let arrow_color = match stage.get_priority_of_movement(m.id) {
188 TurnPriority::Protected => app.cs.signal_protected_turn,
189 TurnPriority::Yield => app.cs.signal_permitted_turn,
190 TurnPriority::Banned => app.cs.signal_banned_turn,
191 };
192 batch.push(arrow_color, m.arrow.clone());
193 }
194 }
195 ctx.upload(batch)
196 }
197 }
198
199 impl State for TrafficSignalEditor {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition200 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
201 if self.warn_changed {
202 self.warn_changed = false;
203 return Transition::Push(PopupMsg::new(
204 ctx,
205 "Note",
206 vec!["Some signals were modified to match the number and duration of stages"],
207 ));
208 }
209
210 ctx.canvas_movement();
211
212 let canonical_signal = app
213 .primary
214 .map
215 .get_traffic_signal(*self.members.iter().next().unwrap());
216 let num_stages = canonical_signal.stages.len();
217
218 match self.side_panel.event(ctx) {
219 Outcome::Clicked(x) => {
220 if x == "Edit multiple signals" {
221 // First commit the current changes, so we enter SignalPicker with clean state.
222 // This UX flow is a little unintuitive.
223 let changes = check_for_missing_turns(app, &self.members)
224 .unwrap_or_else(|| BundleEdits::get_current(app, &self.members));
225 self.original.apply(app);
226 changes.commit(ctx, app);
227 return Transition::Replace(picker::SignalPicker::new(
228 ctx,
229 self.members.clone(),
230 self.mode.clone(),
231 ));
232 }
233 if x == "Edit entire signal" {
234 return Transition::Push(edits::edit_entire_signal(
235 ctx,
236 app,
237 canonical_signal.id,
238 self.mode.clone(),
239 self.original.clone(),
240 ));
241 }
242 if x == "Add new stage" {
243 self.add_new_edit(ctx, app, num_stages, |ts| {
244 ts.stages.push(Stage::new());
245 });
246 return Transition::Keep;
247 }
248 if x == "Apply offset" {
249 let offset = Duration::seconds(self.side_panel.spinner("offset") as f64);
250 self.add_new_edit(ctx, app, self.current_stage, |ts| {
251 ts.offset = offset;
252 });
253 return Transition::Keep;
254 }
255 if x == "Apply offsets" {
256 let offsets: HashMap<IntersectionID, Duration> = self
257 .members
258 .iter()
259 .enumerate()
260 .map(|(idx, i)| {
261 (
262 *i,
263 Duration::seconds(
264 self.side_panel.spinner(&format!("offset {}", idx)) as f64,
265 ),
266 )
267 })
268 .collect();
269 self.add_new_edit(ctx, app, self.current_stage, |ts| {
270 ts.offset = offsets[&ts.id];
271 });
272 return Transition::Keep;
273 }
274 if let Some(x) = x.strip_prefix("change duration of stage ") {
275 let idx = x.parse::<usize>().unwrap() - 1;
276 return Transition::Push(edits::ChangeDuration::new(
277 ctx,
278 canonical_signal.stages[idx].phase_type.clone(),
279 idx,
280 ));
281 }
282 if let Some(x) = x.strip_prefix("delete stage ") {
283 let idx = x.parse::<usize>().unwrap() - 1;
284 self.add_new_edit(ctx, app, 0, |ts| {
285 ts.stages.remove(idx);
286 });
287 return Transition::Keep;
288 }
289 if let Some(x) = x.strip_prefix("move up stage ") {
290 let idx = x.parse::<usize>().unwrap() - 1;
291 self.add_new_edit(ctx, app, idx - 1, |ts| {
292 ts.stages.swap(idx, idx - 1);
293 });
294 return Transition::Keep;
295 }
296 if let Some(x) = x.strip_prefix("move down stage ") {
297 let idx = x.parse::<usize>().unwrap() - 1;
298 self.add_new_edit(ctx, app, idx + 1, |ts| {
299 ts.stages.swap(idx, idx + 1);
300 });
301 return Transition::Keep;
302 }
303 if let Some(x) = x.strip_prefix("stage ") {
304 let idx = x.parse::<usize>().unwrap() - 1;
305 self.change_stage(ctx, app, idx);
306 return Transition::Keep;
307 }
308 unreachable!()
309 }
310 _ => {}
311 }
312
313 match self.top_panel.event(ctx) {
314 Outcome::Clicked(x) => match x.as_ref() {
315 "Finish" => {
316 if let Some(bundle) = check_for_missing_turns(app, &self.members) {
317 bundle.apply(app);
318 self.command_stack.push(bundle.clone());
319 self.redo_stack.clear();
320
321 self.top_panel = make_top_panel(ctx, app, true, false);
322 self.change_stage(ctx, app, 0);
323
324 return Transition::Push(PopupMsg::new(
325 ctx,
326 "Error: missing turns",
327 vec![
328 "Some turns are missing from this traffic signal",
329 "They've all been added as a new first stage. Please update your \
330 changes to include them.",
331 ],
332 ));
333 } else {
334 let changes = BundleEdits::get_current(app, &self.members);
335 self.original.apply(app);
336 changes.commit(ctx, app);
337 return Transition::Pop;
338 }
339 }
340 "Export" => {
341 for signal in BundleEdits::get_current(app, &self.members).signals {
342 let ts = signal.export(&app.primary.map);
343 abstutil::write_json(
344 format!("traffic_signal_data/{}.json", ts.intersection_osm_node_id),
345 &ts,
346 );
347 }
348 }
349 "Preview" => {
350 // Might have to do this first!
351 app.primary
352 .map
353 .recalculate_pathfinding_after_edits(&mut Timer::throwaway());
354
355 return Transition::Push(preview::make_previewer(
356 ctx,
357 app,
358 self.members.clone(),
359 self.current_stage,
360 ));
361 }
362 "undo" => {
363 self.redo_stack
364 .push(BundleEdits::get_current(app, &self.members));
365 self.command_stack.pop().unwrap().apply(app);
366 self.top_panel = make_top_panel(ctx, app, !self.command_stack.is_empty(), true);
367 self.change_stage(ctx, app, 0);
368 return Transition::Keep;
369 }
370 "redo" => {
371 self.command_stack
372 .push(BundleEdits::get_current(app, &self.members));
373 self.redo_stack.pop().unwrap().apply(app);
374 self.top_panel = make_top_panel(ctx, app, true, !self.redo_stack.is_empty());
375 self.change_stage(ctx, app, 0);
376 return Transition::Keep;
377 }
378 _ => unreachable!(),
379 },
380 _ => {}
381 }
382
383 {
384 if self.current_stage != 0 && ctx.input.key_pressed(Key::UpArrow) {
385 self.change_stage(ctx, app, self.current_stage - 1);
386 }
387
388 if self.current_stage != num_stages - 1 && ctx.input.key_pressed(Key::DownArrow) {
389 self.change_stage(ctx, app, self.current_stage + 1);
390 }
391 }
392
393 if ctx.redo_mouseover() {
394 let old = self.movement_selected.clone();
395
396 self.movement_selected = None;
397 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
398 for m in &self.movements {
399 let signal = app.primary.map.get_traffic_signal(m.id.parent);
400 if m.block.contains_pt(pt) {
401 let stage = &signal.stages[self.current_stage];
402 let next_priority = match stage.get_priority_of_movement(m.id) {
403 TurnPriority::Banned => {
404 if stage.could_be_protected(m.id, &signal.movements) {
405 Some(TurnPriority::Protected)
406 } else if m.id.crosswalk {
407 None
408 } else {
409 Some(TurnPriority::Yield)
410 }
411 }
412 TurnPriority::Yield => Some(TurnPriority::Banned),
413 TurnPriority::Protected => {
414 if m.id.crosswalk {
415 Some(TurnPriority::Banned)
416 } else {
417 Some(TurnPriority::Yield)
418 }
419 }
420 };
421 self.movement_selected = Some((m.id, next_priority));
422 break;
423 }
424 }
425 }
426
427 if self.movement_selected != old {
428 self.draw_current = self.recalc_draw_current(ctx, app);
429 self.change_stage(ctx, app, self.current_stage);
430 }
431 }
432
433 if let Some((id, next_priority)) = self.movement_selected {
434 if let Some(pri) = next_priority {
435 let signal = app.primary.map.get_traffic_signal(id.parent);
436 if app.per_obj.left_click(
437 ctx,
438 format!(
439 "toggle from {:?} to {:?}",
440 signal.stages[self.current_stage].get_priority_of_movement(id),
441 pri
442 ),
443 ) {
444 let idx = self.current_stage;
445 let signal = signal.clone();
446 self.add_new_edit(ctx, app, idx, |ts| {
447 if ts.id == id.parent {
448 ts.stages[idx].edit_movement(&signal.movements[&id], pri);
449 }
450 });
451 return Transition::KeepWithMouseover;
452 }
453 }
454 }
455
456 Transition::Keep
457 }
458
draw_baselayer(&self) -> DrawBaselayer459 fn draw_baselayer(&self) -> DrawBaselayer {
460 DrawBaselayer::Custom
461 }
462
draw(&self, g: &mut GfxCtx, app: &App)463 fn draw(&self, g: &mut GfxCtx, app: &App) {
464 {
465 let mut opts = DrawOptions::new();
466 opts.suppress_traffic_signal_details
467 .extend(self.members.clone());
468 app.draw(g, opts, &app.primary.sim, &ShowEverything::new());
469 }
470 g.redraw(&self.fade_irrelevant);
471 g.redraw(&self.draw_current);
472
473 self.top_panel.draw(g);
474 self.side_panel.draw(g);
475
476 if let Some((id, _)) = self.movement_selected {
477 let osd = if id.crosswalk {
478 Text::from(Line(format!(
479 "Crosswalk across {}",
480 app.primary
481 .map
482 .get_r(id.from.id)
483 .get_name(app.opts.language.as_ref())
484 )))
485 } else {
486 Text::from(Line(format!(
487 "Turn from {} to {}",
488 app.primary
489 .map
490 .get_r(id.from.id)
491 .get_name(app.opts.language.as_ref()),
492 app.primary
493 .map
494 .get_r(id.to.id)
495 .get_name(app.opts.language.as_ref())
496 )))
497 };
498 CommonState::draw_custom_osd(g, app, osd);
499 } else {
500 CommonState::draw_osd(g, app);
501 }
502 }
503 }
504
make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool) -> Panel505 fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool) -> Panel {
506 let row = vec![
507 Btn::text_fg("Finish").build_def(ctx, hotkey(Key::Escape)),
508 Btn::text_fg("Preview").build_def(ctx, lctrl(Key::P)),
509 (if can_undo {
510 Btn::svg_def("system/assets/tools/undo.svg").build(ctx, "undo", lctrl(Key::Z))
511 } else {
512 Widget::draw_svg_transform(
513 ctx,
514 "system/assets/tools/undo.svg",
515 RewriteColor::ChangeAll(Color::WHITE.alpha(0.5)),
516 )
517 })
518 .centered_vert(),
519 (if can_redo {
520 Btn::svg_def("system/assets/tools/redo.svg").build(
521 ctx,
522 "redo",
523 // TODO ctrl+shift+Z!
524 lctrl(Key::Y),
525 )
526 } else {
527 Widget::draw_svg_transform(
528 ctx,
529 "system/assets/tools/redo.svg",
530 RewriteColor::ChangeAll(Color::WHITE.alpha(0.5)),
531 )
532 })
533 .centered_vert(),
534 if app.opts.dev {
535 Btn::text_fg("Export")
536 .tooltip(Text::from_multiline(vec![
537 Line("This will create a JSON file in traffic_signal_data/.").small(),
538 Line(
539 "Contribute this to map how this traffic signal is currently timed in \
540 real life.",
541 )
542 .small(),
543 ]))
544 .build_def(ctx, None)
545 } else {
546 Widget::nothing()
547 },
548 ];
549 Panel::new(Widget::col(vec![
550 Line("Traffic signal editor").small_heading().draw(ctx),
551 Widget::row(row),
552 ]))
553 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
554 .build(ctx)
555 }
556
make_side_panel( ctx: &mut EventCtx, app: &App, members: &BTreeSet<IntersectionID>, selected: usize, hovering: Option<IntersectionID>, ) -> Panel557 fn make_side_panel(
558 ctx: &mut EventCtx,
559 app: &App,
560 members: &BTreeSet<IntersectionID>,
561 selected: usize,
562 hovering: Option<IntersectionID>,
563 ) -> Panel {
564 let map = &app.primary.map;
565 // Use any member for stage duration
566 let canonical_signal = map.get_traffic_signal(*members.iter().next().unwrap());
567
568 let mut txt = Text::new();
569 if members.len() == 1 {
570 let i = *members.iter().next().unwrap();
571 txt.add(Line(i.to_string()).big_heading_plain());
572
573 let mut road_names = BTreeSet::new();
574 for r in &app.primary.map.get_i(i).roads {
575 road_names.insert(
576 app.primary
577 .map
578 .get_r(*r)
579 .get_name(app.opts.language.as_ref()),
580 );
581 }
582 for r in road_names {
583 txt.add(Line(format!(" {}", r)));
584 }
585 } else {
586 txt.add(Line(format!("{} intersections", members.len())).big_heading_plain());
587 }
588 {
589 let mut total = Duration::ZERO;
590 for s in &canonical_signal.stages {
591 total += s.phase_type.simple_duration();
592 }
593 // TODO Say "normally" to account for adaptive stages?
594 txt.add(Line(""));
595 txt.add(Line(format!("One full cycle lasts {}", total)));
596 }
597
598 let mut col = vec![
599 txt.draw(ctx),
600 Btn::text_bg2("Edit multiple signals").build_def(ctx, hotkey(Key::M)),
601 ];
602 if members.len() == 1 {
603 col.push(Btn::text_bg2("Edit entire signal").build_def(ctx, hotkey(Key::E)));
604 col.push(Widget::row(vec![
605 "Offset (s):".draw_text(ctx),
606 Spinner::new(
607 ctx,
608 (0, 90),
609 canonical_signal.offset.inner_seconds() as isize,
610 )
611 .named("offset"),
612 Btn::text_bg2("Apply").build(ctx, "Apply offset", None),
613 ]));
614 }
615
616 let translations = squish_polygons_together(
617 members
618 .iter()
619 .map(|i| app.primary.map.get_i(*i).polygon.clone())
620 .collect(),
621 );
622
623 for (idx, canonical_stage) in canonical_signal.stages.iter().enumerate() {
624 col.push(Widget::horiz_separator(ctx, 0.2));
625
626 let unselected_btn = draw_multiple_signals(ctx, app, members, idx, hovering, &translations);
627 let mut selected_btn = unselected_btn.clone();
628 let bbox = unselected_btn.get_bounds().get_rectangle();
629 selected_btn.push(Color::RED, bbox.to_outline(Distance::meters(5.0)).unwrap());
630 let stage_btn = Btn::custom(unselected_btn, selected_btn, bbox).build(
631 ctx,
632 format!("stage {}", idx + 1),
633 None,
634 );
635
636 let stage_col = Widget::col(vec![
637 Widget::row(vec![
638 match canonical_stage.phase_type {
639 PhaseType::Fixed(d) => Line(format!("Stage {}: {}", idx + 1, d)),
640 PhaseType::Adaptive(d) => Line(format!("Stage {}: {} (adaptive)", idx + 1, d)),
641 }
642 .small_heading()
643 .draw(ctx),
644 Btn::svg_def("system/assets/tools/edit.svg").build(
645 ctx,
646 format!("change duration of stage {}", idx + 1),
647 if selected == idx {
648 hotkey(Key::X)
649 } else {
650 None
651 },
652 ),
653 if canonical_signal.stages.len() > 1 {
654 Btn::svg_def("system/assets/tools/delete.svg")
655 .build(ctx, format!("delete stage {}", idx + 1), None)
656 .align_right()
657 } else {
658 Widget::nothing()
659 },
660 ]),
661 Widget::row(vec![
662 stage_btn,
663 Widget::col(vec![
664 if idx == 0 {
665 Btn::text_fg("↑").inactive(ctx)
666 } else {
667 Btn::text_fg("↑").build(ctx, format!("move up stage {}", idx + 1), None)
668 },
669 if idx == canonical_signal.stages.len() - 1 {
670 Btn::text_fg("↓").inactive(ctx)
671 } else {
672 Btn::text_fg("↓").build(ctx, format!("move down stage {}", idx + 1), None)
673 },
674 ])
675 .centered_vert()
676 .align_right(),
677 ]),
678 ])
679 .padding(10);
680
681 if idx == selected {
682 col.push(stage_col.bg(Color::hex("#2A2A2A")));
683 } else {
684 col.push(stage_col);
685 }
686 }
687
688 col.push(Widget::horiz_separator(ctx, 0.2));
689 col.push(Btn::text_fg("Add new stage").build_def(ctx, None));
690
691 // TODO This doesn't even have a way of knowing which spinner corresponds to which
692 // intersection!
693 if members.len() > 1 {
694 col.push("Tune offset (s)".draw_text(ctx));
695 col.push(
696 Widget::row(
697 members
698 .iter()
699 .enumerate()
700 .map(|(idx, i)| {
701 let spinner = Spinner::new(
702 ctx,
703 (0, 90),
704 map.get_traffic_signal(*i).offset.inner_seconds() as isize,
705 )
706 .named(format!("offset {}", idx));
707 if hovering == Some(*i) {
708 spinner.padding(2).outline(2.0, Color::YELLOW)
709 } else {
710 spinner
711 }
712 })
713 .collect(),
714 )
715 .evenly_spaced(),
716 );
717 col.push(Btn::text_bg2("Apply").build(ctx, "Apply offsets", None));
718 }
719
720 Panel::new(Widget::col(col))
721 .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
722 .exact_size_percent(30, 85)
723 .build(ctx)
724 }
725
726 impl BundleEdits {
apply(&self, app: &mut App)727 fn apply(&self, app: &mut App) {
728 for s in &self.signals {
729 app.primary.map.incremental_edit_traffic_signal(s.clone());
730 }
731 }
732
commit(self, ctx: &mut EventCtx, app: &mut App)733 fn commit(self, ctx: &mut EventCtx, app: &mut App) {
734 // Skip if there's no change
735 if self == BundleEdits::get_current(app, &self.signals.iter().map(|s| s.id).collect()) {
736 return;
737 }
738
739 let mut edits = app.primary.map.get_edits().clone();
740 // TODO Can we batch these commands somehow, so undo/redo in edit mode behaves properly?
741 for signal in self.signals {
742 edits.commands.push(EditCmd::ChangeIntersection {
743 i: signal.id,
744 old: app.primary.map.get_i_edit(signal.id),
745 new: EditIntersection::TrafficSignal(signal.export(&app.primary.map)),
746 });
747 }
748 apply_map_edits(ctx, app, edits);
749 }
750
get_current(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits751 fn get_current(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits {
752 let signals = members
753 .iter()
754 .map(|i| app.primary.map.get_traffic_signal(*i).clone())
755 .collect();
756 BundleEdits { signals }
757 }
758
759 // If the intersections haven't been edited together before, the number of stages and the
760 // durations might not match up. Just initially force them to align somehow.
synchronize(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits761 fn synchronize(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits {
762 let map = &app.primary.map;
763 // Pick one of the members with the most stages as canonical.
764 let canonical = map.get_traffic_signal(
765 *members
766 .iter()
767 .max_by_key(|i| map.get_traffic_signal(**i).stages.len())
768 .unwrap(),
769 );
770
771 let mut signals = Vec::new();
772 for i in members {
773 let mut signal = map.get_traffic_signal(*i).clone();
774 for (idx, canonical_stage) in canonical.stages.iter().enumerate() {
775 if signal.stages.len() == idx {
776 signal.stages.push(Stage::new());
777 }
778 signal.stages[idx].phase_type = canonical_stage.phase_type.clone();
779 }
780 signals.push(signal);
781 }
782
783 BundleEdits { signals }
784 }
785 }
786
787 // If None, nothing missing.
check_for_missing_turns(app: &App, members: &BTreeSet<IntersectionID>) -> Option<BundleEdits>788 fn check_for_missing_turns(app: &App, members: &BTreeSet<IntersectionID>) -> Option<BundleEdits> {
789 let mut all_missing = BTreeSet::new();
790 for i in members {
791 all_missing.extend(app.primary.map.get_traffic_signal(*i).missing_turns());
792 }
793 if all_missing.is_empty() {
794 return None;
795 }
796
797 let mut bundle = BundleEdits::get_current(app, members);
798 // Stick all the missing turns in a new stage at the beginning.
799 for signal in &mut bundle.signals {
800 let mut stage = Stage::new();
801 // TODO Could do this more efficiently
802 for m in &all_missing {
803 if m.parent != signal.id {
804 continue;
805 }
806 if m.crosswalk {
807 stage.protected_movements.insert(*m);
808 } else {
809 stage.yield_movements.insert(*m);
810 }
811 }
812 signal.stages.insert(0, stage);
813 }
814 Some(bundle)
815 }
816
draw_multiple_signals( ctx: &mut EventCtx, app: &App, members: &BTreeSet<IntersectionID>, idx: usize, hovering: Option<IntersectionID>, translations: &Vec<(f64, f64)>, ) -> GeomBatch817 fn draw_multiple_signals(
818 ctx: &mut EventCtx,
819 app: &App,
820 members: &BTreeSet<IntersectionID>,
821 idx: usize,
822 hovering: Option<IntersectionID>,
823 translations: &Vec<(f64, f64)>,
824 ) -> GeomBatch {
825 let mut batch = GeomBatch::new();
826 for (i, (dx, dy)) in members.iter().zip(translations) {
827 let mut piece = GeomBatch::new();
828 piece.push(
829 app.cs.normal_intersection,
830 app.primary.map.get_i(*i).polygon.clone(),
831 );
832 draw_signal_stage(
833 ctx.prerender,
834 &app.primary.map.get_traffic_signal(*i).stages[idx],
835 *i,
836 None,
837 &mut piece,
838 app,
839 TrafficSignalStyle::Sidewalks,
840 );
841 if members.len() > 1 && hovering.map(|x| x == *i).unwrap_or(false) {
842 // TODO This makes the side-panel jump a little, because the outline slightly increases
843 // the bounds...
844 if let Ok(p) = app
845 .primary
846 .map
847 .get_i(*i)
848 .polygon
849 .to_outline(Distance::meters(0.1))
850 {
851 piece.push(Color::YELLOW, p);
852 }
853 }
854 batch.append(piece.translate(*dx, *dy));
855 }
856
857 // Make the whole thing fit a fixed width
858 batch = batch.autocrop();
859 let bounds = batch.get_bounds();
860 let zoom = (300.0 / bounds.width()).min(300.0 / bounds.height());
861 batch.scale(zoom)
862 }
863
draw_selected_movement( app: &App, batch: &mut GeomBatch, g: &DrawMovement, m: &Movement, next_priority: Option<TurnPriority>, )864 fn draw_selected_movement(
865 app: &App,
866 batch: &mut GeomBatch,
867 g: &DrawMovement,
868 m: &Movement,
869 next_priority: Option<TurnPriority>,
870 ) {
871 // TODO Refactor this mess. Maybe after things like "dashed with outline" can be expressed more
872 // composably like SVG, using lyon.
873 let block_color = match next_priority {
874 Some(TurnPriority::Protected) => {
875 let green = Color::hex("#72CE36");
876 let arrow = m.geom.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
877 batch.push(green.alpha(0.5), arrow.clone());
878 if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
879 batch.push(green, p);
880 }
881 green
882 }
883 Some(TurnPriority::Yield) => {
884 batch.extend(
885 // TODO Ideally the inner part would be the lower opacity blue, but can't yet
886 // express that it should cover up the thicker solid blue beneath it
887 Color::BLACK.alpha(0.8),
888 m.geom.dashed_arrow(
889 BIG_ARROW_THICKNESS,
890 Distance::meters(1.2),
891 Distance::meters(0.3),
892 ArrowCap::Triangle,
893 ),
894 );
895 batch.extend(
896 app.cs.signal_permitted_turn.alpha(0.8),
897 m.geom
898 .exact_slice(
899 Distance::meters(0.1),
900 m.geom.length() - Distance::meters(0.1),
901 )
902 .dashed_arrow(
903 BIG_ARROW_THICKNESS / 2.0,
904 Distance::meters(1.0),
905 Distance::meters(0.5),
906 ArrowCap::Triangle,
907 ),
908 );
909 app.cs.signal_permitted_turn
910 }
911 Some(TurnPriority::Banned) => {
912 let red = Color::hex("#EB3223");
913 let arrow = m.geom.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
914 batch.push(red.alpha(0.5), arrow.clone());
915 if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
916 batch.push(red, p);
917 }
918 red
919 }
920 None => app.cs.signal_turn_block_bg,
921 };
922 batch.push(block_color, g.block.clone());
923 batch.push(Color::WHITE, g.arrow.clone());
924 }
925
926 // TODO Move to geom?
squish_polygons_together(mut polygons: Vec<Polygon>) -> Vec<(f64, f64)>927 fn squish_polygons_together(mut polygons: Vec<Polygon>) -> Vec<(f64, f64)> {
928 if polygons.len() == 1 {
929 return vec![(0.0, 0.0)];
930 }
931
932 // Can't be too big, or polygons could silently swap places. To be careful, pick something a
933 // bit smaller than the smallest polygon.
934 let step_size = 0.8
935 * polygons.iter().fold(std::f64::MAX, |x, p| {
936 x.min(p.get_bounds().width()).min(p.get_bounds().height())
937 });
938
939 let mut translations: Vec<(f64, f64)> =
940 std::iter::repeat((0.0, 0.0)).take(polygons.len()).collect();
941 // Once a polygon hits another while moving, stop adjusting it. Otherwise, go round-robin.
942 let mut indices: VecDeque<usize> = (0..polygons.len()).collect();
943
944 let mut attempts = 0;
945 while !indices.is_empty() {
946 let idx = indices.pop_front().unwrap();
947 let center = Pt2D::center(&polygons.iter().map(|p| p.center()).collect());
948 let angle = Line::must_new(polygons[idx].center(), center).angle();
949 let pt = Pt2D::new(0.0, 0.0).project_away(Distance::meters(step_size), angle);
950
951 // Do we hit anything if we move this way?
952 let translated = polygons[idx].translate(pt.x(), pt.y());
953 if polygons
954 .iter()
955 .enumerate()
956 .any(|(i, p)| i != idx && !translated.intersection(p).is_empty())
957 {
958 // Stop moving this polygon
959 } else {
960 translations[idx].0 += pt.x();
961 translations[idx].1 += pt.y();
962 polygons[idx] = translated;
963 indices.push_back(idx);
964 }
965
966 attempts += 1;
967 if attempts == 100 {
968 break;
969 }
970 }
971
972 translations
973 }
974