1use std::collections::{BTreeSet, VecDeque};
2
3use anyhow::Result;
4
5use abstutil::Timer;
6use geom::{Distance, Line, Polygon, Pt2D};
7use map_gui::options::TrafficSignalStyle;
8use map_gui::render::{traffic_signal, DrawMovement, DrawOptions};
9use map_model::{
10 ControlTrafficSignal, EditIntersectionControl, IntersectionID, MovementID, Stage, StageType,
11 TurnPriority,
12};
13use widgetry::tools::PopupMsg;
14use widgetry::{
15 include_labeled_bytes, lctrl, Color, ControlState, DragDrop, DrawBaselayer, Drawable, EventCtx,
16 GeomBatch, GeomBatchStack, GfxCtx, HorizontalAlignment, Image, Key, Line, Outcome, Panel,
17 RewriteColor, StackAxis, State, Text, TextExt, VerticalAlignment, Widget,
18};
19
20use crate::app::{App, ShowEverything, Transition};
21use crate::common::CommonState;
22use crate::edit::{apply_map_edits, ConfirmDiscard};
23use crate::sandbox::GameplayMode;
24
25mod edits;
26mod gmns;
27mod offsets;
28mod picker;
29mod preview;
30
31pub struct TrafficSignalEditor {
34 side_panel: Panel,
35 top_panel: Panel,
36
37 mode: GameplayMode,
38 members: BTreeSet<IntersectionID>,
39 current_stage: usize,
40
41 movements: Vec<DrawMovement>,
42 movement_selected: Option<(MovementID, Option<TurnPriority>)>,
44 draw_current: Drawable,
45 tooltip: Option<Text>,
46
47 command_stack: Vec<BundleEdits>,
48 redo_stack: Vec<BundleEdits>,
49 original: BundleEdits,
51 warn_changed: bool,
52
53 fade_irrelevant: Drawable,
54}
55
56#[derive(Clone, PartialEq)]
58pub struct BundleEdits {
59 signals: Vec<ControlTrafficSignal>,
60}
61
62impl TrafficSignalEditor {
63 pub fn new_state(
64 ctx: &mut EventCtx,
65 app: &mut App,
66 members: BTreeSet<IntersectionID>,
67 mode: GameplayMode,
68 ) -> Box<dyn State<App>> {
69 app.primary.current_selection = None;
70
71 let original = BundleEdits::get_current(app, &members);
72 let synced = BundleEdits::synchronize(app, &members);
73 let warn_changed = original != synced;
74 synced.apply(app);
75
76 let mut editor = TrafficSignalEditor {
77 side_panel: make_side_panel(ctx, app, &members, 0),
78 top_panel: make_top_panel(ctx, app, false, false),
79 mode,
80 current_stage: 0,
81 movements: Vec::new(),
82 movement_selected: None,
83 draw_current: Drawable::empty(ctx),
84 tooltip: None,
85 command_stack: Vec::new(),
86 redo_stack: Vec::new(),
87 warn_changed,
88 original,
89 fade_irrelevant: fade_irrelevant(app, &members).upload(ctx),
90 members,
91 };
92 editor.recalc_draw_current(ctx, app);
93 Box::new(editor)
94 }
95
96 fn change_stage(&mut self, ctx: &mut EventCtx, app: &App, idx: usize) {
97 if self.current_stage == idx {
98 let mut new = make_side_panel(ctx, app, &self.members, self.current_stage);
99 new.restore(ctx, &self.side_panel);
100 self.side_panel = new;
101 } else {
102 self.current_stage = idx;
103 self.side_panel = make_side_panel(ctx, app, &self.members, self.current_stage);
104 }
105
106 self.recalc_draw_current(ctx, app);
107 }
108
109 fn add_new_edit<F: Fn(&mut ControlTrafficSignal)>(
110 &mut self,
111 ctx: &mut EventCtx,
112 app: &mut App,
113 idx: usize,
114 fxn: F,
115 ) {
116 let mut bundle = BundleEdits::get_current(app, &self.members);
117 self.command_stack.push(bundle.clone());
118 self.redo_stack.clear();
119 for ts in &mut bundle.signals {
120 fxn(ts);
121 }
122 bundle.apply(app);
123
124 self.top_panel = make_top_panel(ctx, app, true, false);
125 self.change_stage(ctx, app, idx);
126 }
127
128 fn recalc_draw_current(&mut self, ctx: &mut EventCtx, app: &App) {
129 let mut batch = GeomBatch::new();
130 let mut movements = Vec::new();
131 for i in &self.members {
132 let stage = &app.primary.map.get_traffic_signal(*i).stages[self.current_stage];
133 for (m, draw) in DrawMovement::for_i(
134 ctx.prerender,
135 &app.primary.map,
136 &app.cs,
137 *i,
138 self.current_stage,
139 ) {
140 if self
141 .movement_selected
142 .map(|(x, _)| x != m.id)
143 .unwrap_or(true)
144 || m.id.crosswalk
145 {
146 batch.append(draw);
147 } else if !stage.protected_movements.contains(&m.id)
148 && !stage.yield_movements.contains(&m.id)
149 {
150 batch.append(draw.color(RewriteColor::Change(
152 app.cs.signal_banned_turn.alpha(0.5),
153 Color::hex("#72CE36"),
154 )));
155 }
156 movements.push(m);
157 }
158 traffic_signal::draw_stage_number(
159 ctx.prerender,
160 app.primary.map.get_i(*i),
161 self.current_stage,
162 &mut batch,
163 );
164 }
165
166 if let Some((selected, next_priority)) = self.movement_selected {
168 for m in &movements {
169 if m.id == selected {
170 m.draw_selected_movement(app, &mut batch, next_priority);
171 break;
172 }
173 }
174 }
175
176 self.draw_current = ctx.upload(batch);
177 self.movements = movements;
178 }
179
180 fn validate_all_members(&self, app: &App) -> Result<()> {
182 for i in &self.members {
183 app.primary
184 .map
185 .get_traffic_signal(*i)
186 .validate(app.primary.map.get_i(*i))?;
187 }
188 Ok(())
189 }
190}
191
192impl State<App> for TrafficSignalEditor {
193 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
194 self.tooltip = None;
195
196 if self.warn_changed {
197 self.warn_changed = false;
198 return Transition::Push(PopupMsg::new_state(
199 ctx,
200 "Note",
201 vec!["Some signals were modified to match the number and duration of stages"],
202 ));
203 }
204
205 ctx.canvas_movement();
206
207 let canonical_signal = app
208 .primary
209 .map
210 .get_traffic_signal(*self.members.iter().next().unwrap());
211 let num_stages = canonical_signal.stages.len();
212
213 match self.side_panel.event(ctx) {
214 Outcome::Clicked(x) => match x.as_ref() {
215 "Edit entire signal" => {
216 return Transition::Push(edits::edit_entire_signal(
217 ctx,
218 app,
219 canonical_signal.id,
220 self.mode.clone(),
221 self.original.clone(),
222 ));
223 }
224 "Tune offsets between signals" => {
225 return Transition::Push(offsets::ShowAbsolute::new_state(
226 ctx,
227 app,
228 self.members.clone(),
229 ));
230 }
231 "Add a new stage" => {
232 self.add_new_edit(ctx, app, num_stages, |ts| {
233 ts.stages.push(Stage::new());
234 });
235 return Transition::Keep;
236 }
237 "change duration" => {
238 return Transition::Push(edits::ChangeDuration::new_state(
239 ctx,
240 app,
241 canonical_signal,
242 self.current_stage,
243 ));
244 }
245 "delete stage" => {
246 let idx = self.current_stage;
247 self.add_new_edit(ctx, app, 0, |ts| {
248 ts.stages.remove(idx);
249 });
250 return Transition::Keep;
251 }
252 "previous stage" => {
253 self.change_stage(ctx, app, self.current_stage - 1);
254 return Transition::Keep;
255 }
256 "next stage" => {
257 self.change_stage(ctx, app, self.current_stage + 1);
258 return Transition::Keep;
259 }
260 x => {
261 if let Some(x) = x.strip_prefix("stage ") {
262 let idx = x.parse::<usize>().unwrap() - 1;
263 self.change_stage(ctx, app, idx);
264 return Transition::Keep;
265 } else {
266 unreachable!()
267 }
268 }
269 },
270 Outcome::DragDropReleased(_, old_idx, new_idx) => {
271 self.add_new_edit(ctx, app, new_idx, |ts| {
272 ts.stages.swap(old_idx, new_idx);
273 });
274 }
275 _ => {}
276 }
277
278 if let Outcome::Clicked(x) = self.top_panel.event(ctx) {
279 match x.as_ref() {
280 "Finish" => {
281 if let Some(bundle) = check_for_missing_turns(app, &self.members) {
282 bundle.apply(app);
283 self.command_stack.push(bundle);
284 self.redo_stack.clear();
285
286 self.top_panel = make_top_panel(ctx, app, true, false);
287 self.change_stage(ctx, app, 0);
288
289 return Transition::Push(PopupMsg::new_state(
290 ctx,
291 "Error: missing turns",
292 vec![
293 "Some turns are missing from this traffic signal",
294 "They've all been added as a new first stage. Please update your \
295 changes to include them.",
296 ],
297 ));
298 } else if let Err(err) = self.validate_all_members(app) {
299 error!("{}", err);
302 return Transition::Push(PopupMsg::new_state(
303 ctx,
304 "Error",
305 vec!["This signal configuration is somehow invalid; check the console logs"]
306 ));
307 } else {
308 let changes = BundleEdits::get_current(app, &self.members);
309 self.original.apply(app);
310 changes.commit(ctx, app);
311 return Transition::Pop;
312 }
313 }
314 "Cancel" => {
315 if BundleEdits::get_current(app, &self.members) == self.original {
316 self.original.apply(app);
317 return Transition::Pop;
318 }
319 let original = self.original.clone();
320 return Transition::Push(ConfirmDiscard::new_state(
321 ctx,
322 Box::new(move |app| {
323 original.apply(app);
324 }),
325 ));
326 }
327 "Edit multiple signals" => {
328 if let Err(err) = self.validate_all_members(app) {
329 error!("{}", err);
330 return Transition::Push(PopupMsg::new_state(
331 ctx,
332 "Error",
333 vec!["This signal configuration is somehow invalid; check the console logs"]
334 ));
335 }
336
337 let changes = check_for_missing_turns(app, &self.members)
340 .unwrap_or_else(|| BundleEdits::get_current(app, &self.members));
341 self.original.apply(app);
342 changes.commit(ctx, app);
343 return Transition::Replace(picker::SignalPicker::new_state(
344 ctx,
345 self.members.clone(),
346 self.mode.clone(),
347 ));
348 }
349 "Export" => {
350 for signal in BundleEdits::get_current(app, &self.members).signals {
351 let ts = signal.export(&app.primary.map);
352 abstio::write_json(
353 format!("traffic_signal_{}.json", ts.intersection_osm_node_id),
354 &ts,
355 );
356 }
357 }
358 "Change crosswalks" => {
359 return Transition::Replace(super::crosswalks::CrosswalkEditor::new_state(
361 ctx,
362 app,
363 *self.members.iter().next().unwrap(),
364 ));
365 }
366 "Preview" => {
367 app.primary
369 .map
370 .recalculate_pathfinding_after_edits(&mut Timer::throwaway());
371
372 return Transition::Push(preview::make_previewer(
373 ctx,
374 app,
375 self.members.clone(),
376 self.current_stage,
377 ));
378 }
379 "undo" => {
380 self.redo_stack
381 .push(BundleEdits::get_current(app, &self.members));
382 self.command_stack.pop().unwrap().apply(app);
383 self.top_panel = make_top_panel(ctx, app, !self.command_stack.is_empty(), true);
384 self.change_stage(ctx, app, 0);
385 return Transition::Keep;
386 }
387 "redo" => {
388 self.command_stack
389 .push(BundleEdits::get_current(app, &self.members));
390 self.redo_stack.pop().unwrap().apply(app);
391 self.top_panel = make_top_panel(ctx, app, true, !self.redo_stack.is_empty());
392 self.change_stage(ctx, app, 0);
393 return Transition::Keep;
394 }
395 _ => unreachable!(),
396 }
397 }
398
399 {
400 if self.current_stage != 0 && ctx.input.pressed(Key::LeftArrow) {
401 self.change_stage(ctx, app, self.current_stage - 1);
402 }
403
404 if self.current_stage != num_stages - 1 && ctx.input.pressed(Key::RightArrow) {
405 self.change_stage(ctx, app, self.current_stage + 1);
406 }
407 }
408
409 if ctx.redo_mouseover() {
410 let old = self.movement_selected;
411
412 self.movement_selected = None;
413 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
414 for m in &self.movements {
415 let signal = app.primary.map.get_traffic_signal(m.id.parent);
416 let i = app.primary.map.get_i(signal.id);
417 if m.hitbox.contains_pt(pt) {
418 let stage = &signal.stages[self.current_stage];
419 let next_priority = match stage.get_priority_of_movement(m.id) {
420 TurnPriority::Banned => {
421 if stage.could_be_protected(m.id, i) {
422 Some(TurnPriority::Protected)
423 } else if m.id.crosswalk {
424 None
425 } else {
426 Some(TurnPriority::Yield)
427 }
428 }
429 TurnPriority::Yield => Some(TurnPriority::Banned),
430 TurnPriority::Protected => {
431 if m.id.crosswalk {
432 Some(TurnPriority::Banned)
433 } else {
434 Some(TurnPriority::Yield)
435 }
436 }
437 };
438 self.movement_selected = Some((m.id, next_priority));
439 break;
440 }
441 }
442 }
443
444 if self.movement_selected != old {
445 self.change_stage(ctx, app, self.current_stage);
446 }
447 }
448
449 if let Some((id, Some(pri))) = self.movement_selected {
450 let signal = app.primary.map.get_traffic_signal(id.parent);
451 let mut txt = Text::new();
452 txt.add_line(Line(format!(
453 "{} {}",
454 match signal.stages[self.current_stage].get_priority_of_movement(id) {
455 TurnPriority::Protected => "Protected",
456 TurnPriority::Yield => "Yielding",
457 TurnPriority::Banned => "Forbidden",
458 },
459 if id.crosswalk { "crosswalk" } else { "turn" },
460 )));
461 txt.add_appended(vec![
462 Line("Click").fg(ctx.style().text_hotkey_color),
463 Line(format!(
464 " to {}",
465 match pri {
466 TurnPriority::Protected => "add it as protected",
467 TurnPriority::Yield => "allow it after yielding",
468 TurnPriority::Banned => "forbid it",
469 }
470 )),
471 ]);
472 self.tooltip = Some(txt);
473 if app.per_obj.left_click(
474 ctx,
475 format!(
476 "toggle from {:?} to {:?}",
477 signal.stages[self.current_stage].get_priority_of_movement(id),
478 pri
479 ),
480 ) {
481 let idx = self.current_stage;
482 let movement = app.primary.map.get_i(id.parent).movements[&id].clone();
483 self.add_new_edit(ctx, app, idx, |ts| {
484 if ts.id == id.parent {
485 ts.stages[idx].edit_movement(&movement, pri);
486 }
487 });
488 return Transition::KeepWithMouseover;
489 }
490 }
491
492 Transition::Keep
493 }
494
495 fn draw_baselayer(&self) -> DrawBaselayer {
496 DrawBaselayer::Custom
497 }
498
499 fn draw(&self, g: &mut GfxCtx, app: &App) {
500 {
501 let mut opts = DrawOptions::new();
502 opts.suppress_traffic_signal_details
503 .extend(self.members.clone());
504 app.draw(g, opts, &ShowEverything::new());
505 }
506 g.redraw(&self.fade_irrelevant);
507 g.redraw(&self.draw_current);
508
509 self.top_panel.draw(g);
510 self.side_panel.draw(g);
511
512 if let Some((id, _)) = self.movement_selected {
513 let osd = if id.crosswalk {
514 Text::from(format!(
515 "Crosswalk across {}",
516 app.primary
517 .map
518 .get_r(id.from.road)
519 .get_name(app.opts.language.as_ref())
520 ))
521 } else {
522 Text::from(format!(
523 "Turn from {} to {}",
524 app.primary
525 .map
526 .get_r(id.from.road)
527 .get_name(app.opts.language.as_ref()),
528 app.primary
529 .map
530 .get_r(id.to.road)
531 .get_name(app.opts.language.as_ref())
532 ))
533 };
534 CommonState::draw_custom_osd(g, app, osd);
535 } else {
536 CommonState::draw_osd(g, app);
537 }
538
539 if let Some(txt) = self.tooltip.clone() {
540 g.draw_mouse_tooltip(txt);
541 }
542 }
543}
544
545fn make_top_panel(ctx: &mut EventCtx, app: &App, can_undo: bool, can_redo: bool) -> Panel {
546 let mut second_row = vec![ctx
547 .style()
548 .btn_outline
549 .text("Change crosswalks")
550 .hotkey(Key::C)
551 .build_def(ctx)];
552 if app.opts.dev {
553 second_row.push(
554 ctx.style()
555 .btn_outline
556 .text("Export")
557 .tooltip(Text::from_multiline(vec![
558 Line(
559 "This will create a JSON file in the directory where A/B Street is running",
560 )
561 .small(),
562 Line(
563 "Contribute this to map how this traffic signal is currently timed in \
564 real life.",
565 )
566 .small(),
567 ]))
568 .build_def(ctx),
569 );
570 }
571
572 Panel::new_builder(Widget::col(vec![
573 Widget::row(vec![
574 Line("Traffic signal editor")
575 .small_heading()
576 .into_widget(ctx),
577 ctx.style()
578 .btn_plain
579 .text("+ Edit multiple")
580 .label_color(Color::hex("#4CA7E9"), ControlState::Default)
581 .hotkey(Key::M)
582 .build_widget(ctx, "Edit multiple signals"),
583 ]),
584 Widget::row(vec![
585 ctx.style()
586 .btn_solid_primary
587 .text("Finish")
588 .hotkey(Key::Enter)
589 .build_def(ctx),
590 ctx.style()
591 .btn_outline
592 .text("Preview")
593 .hotkey(lctrl(Key::P))
594 .build_def(ctx),
595 ctx.style()
596 .btn_plain
597 .icon("system/assets/tools/undo.svg")
598 .disabled(!can_undo)
599 .hotkey(lctrl(Key::Z))
600 .build_widget(ctx, "undo"),
601 ctx.style()
602 .btn_plain
603 .icon("system/assets/tools/redo.svg")
604 .disabled(!can_redo)
605 .hotkey(lctrl(Key::Y))
607 .build_widget(ctx, "redo"),
608 ctx.style()
609 .btn_plain_destructive
610 .text("Cancel")
611 .hotkey(Key::Escape)
612 .build_def(ctx)
613 .align_right(),
614 ]),
615 Widget::row(second_row),
616 ]))
617 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
618 .build(ctx)
619}
620
621fn make_side_panel(
622 ctx: &mut EventCtx,
623 app: &App,
624 members: &BTreeSet<IntersectionID>,
625 selected: usize,
626) -> Panel {
627 let map = &app.primary.map;
628 let canonical_signal = map.get_traffic_signal(*members.iter().next().unwrap());
630
631 let mut txt = Text::new();
632 if members.len() == 1 {
633 let i = *members.iter().next().unwrap();
634 txt.add_line(Line(i.to_string()).big_heading_plain());
635
636 let mut road_names = BTreeSet::new();
637 for r in &app.primary.map.get_i(i).roads {
638 road_names.insert(
639 app.primary
640 .map
641 .get_r(*r)
642 .get_name(app.opts.language.as_ref()),
643 );
644 }
645 for r in road_names {
646 txt.add_line(Line(format!(" {}", r)).secondary());
647 }
648 } else {
649 txt.add_line(Line(format!("{} intersections", members.len())).big_heading_plain());
650 txt.add_line(
651 Line(
652 members
653 .iter()
654 .map(|i| format!("#{}", i.0))
655 .collect::<Vec<_>>()
656 .join(", "),
657 )
658 .secondary(),
659 );
660 }
661 let mut col = vec![txt.into_widget(ctx)];
662
663 col.push(
665 Widget::row(vec![
666 ctx.style()
667 .btn_plain
668 .icon_bytes(include_labeled_bytes!(
669 "../../../../../widgetry/icons/arrow_left.svg"
670 ))
671 .disabled(selected == 0)
672 .build_widget(ctx, "previous stage"),
673 ctx.style()
674 .btn_plain
675 .icon_bytes(include_labeled_bytes!(
676 "../../../../../widgetry/icons/arrow_right.svg"
677 ))
678 .disabled(selected == canonical_signal.stages.len() - 1)
679 .build_widget(ctx, "next stage"),
680 match canonical_signal.stages[selected].stage_type {
681 StageType::Fixed(d) => format!("Stage duration: {}", d),
682 StageType::Variable(min, delay, additional) => format!(
683 "Stage duration: {}, {}, {} (variable)",
684 min, delay, additional
685 ),
686 }
687 .text_widget(ctx)
688 .centered_vert(),
689 ctx.style()
690 .btn_plain
691 .icon("system/assets/tools/pencil.svg")
692 .hotkey(Key::X)
693 .build_widget(ctx, "change duration"),
694 if canonical_signal.stages.len() > 1 {
695 ctx.style()
696 .btn_solid_destructive
697 .icon("system/assets/tools/trash.svg")
698 .build_widget(ctx, "delete stage")
699 } else {
700 Widget::nothing()
701 },
702 ctx.style()
703 .btn_plain
704 .icon("system/assets/speed/plus.svg")
705 .build_widget(ctx, "Add a new stage"),
706 ])
707 .padding(10)
708 .bg(app.cs.inner_panel_bg),
709 );
710
711 let translations = squish_polygons_together(
712 members
713 .iter()
714 .map(|i| app.primary.map.get_i(*i).polygon.clone())
715 .collect(),
716 );
717
718 let mut drag_drop = DragDrop::new(ctx, "stage cards", StackAxis::Horizontal);
719 for idx in 0..canonical_signal.stages.len() {
720 let mut stack = GeomBatchStack::vertical(vec![
721 Text::from(Line(format!(
722 "Stage {}: {}",
723 idx + 1,
724 match canonical_signal.stages[idx].stage_type {
725 StageType::Fixed(d) => format!("{}", d),
726 StageType::Variable(min, _, _) => format!("{} (v)", min),
727 },
728 )))
729 .render(ctx),
730 draw_multiple_signals(ctx, app, members, idx, &translations),
731 ]);
732 stack.set_spacing(10.0);
733 let icon_batch = stack.batch();
734 let icon_bounds = icon_batch.get_bounds();
735 let image = Image::from_batch(icon_batch, icon_bounds)
736 .dims(150.0)
737 .untinted()
738 .padding(16);
739 let (default_batch, bounds) = image.clone().build_batch(ctx).unwrap();
740 let (hovering_batch, _) = image
741 .clone()
742 .bg_color(ctx.style().btn_tab.bg_disabled.dull(0.3))
743 .build_batch(ctx)
744 .unwrap();
745 let (selected_batch, _) = image
746 .bg_color(ctx.style().btn_solid_primary.bg)
747 .build_batch(ctx)
748 .unwrap();
749
750 drag_drop.push_card(
751 idx,
752 bounds.into(),
753 default_batch,
754 hovering_batch,
755 selected_batch,
756 );
757 }
758 drag_drop.set_initial_state(Some(selected), None);
759
760 col.push(drag_drop.into_widget(ctx));
761
762 col.push(Widget::row(vec![
763 format!(
765 "One full cycle lasts {}",
766 canonical_signal.simple_cycle_duration()
767 )
768 .text_widget(ctx)
769 .centered_vert(),
770 if members.len() == 1 {
771 ctx.style()
772 .btn_outline
773 .text("Edit entire signal")
774 .hotkey(Key::E)
775 .build_def(ctx)
776 } else {
777 ctx.style()
778 .btn_outline
779 .text("Tune offsets between signals")
780 .hotkey(Key::O)
781 .build_def(ctx)
782 },
783 ]));
784
785 Panel::new_builder(Widget::col(col))
786 .aligned(HorizontalAlignment::Left, VerticalAlignment::Center)
787 .ignore_initial_events()
789 .build(ctx)
790}
791
792impl BundleEdits {
793 fn apply(&self, app: &mut App) {
794 for s in &self.signals {
795 app.primary.map.incremental_edit_traffic_signal(s.clone());
796 }
797 }
798
799 fn commit(self, ctx: &mut EventCtx, app: &mut App) {
800 if self == BundleEdits::get_current(app, &self.signals.iter().map(|s| s.id).collect()) {
802 return;
803 }
804
805 let mut edits = app.primary.map.get_edits().clone();
806 for signal in self.signals {
808 edits
809 .commands
810 .push(app.primary.map.edit_intersection_cmd(signal.id, |new| {
811 new.control =
812 EditIntersectionControl::TrafficSignal(signal.export(&app.primary.map));
813 }));
814 }
815 apply_map_edits(ctx, app, edits);
816 }
817
818 fn get_current(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits {
819 let signals = members
820 .iter()
821 .map(|i| app.primary.map.get_traffic_signal(*i).clone())
822 .collect();
823 BundleEdits { signals }
824 }
825
826 fn synchronize(app: &App, members: &BTreeSet<IntersectionID>) -> BundleEdits {
829 let map = &app.primary.map;
830 let canonical = map.get_traffic_signal(
832 *members
833 .iter()
834 .max_by_key(|i| map.get_traffic_signal(**i).stages.len())
835 .unwrap(),
836 );
837
838 let mut signals = Vec::new();
839 for i in members {
840 let mut signal = map.get_traffic_signal(*i).clone();
841 for (idx, canonical_stage) in canonical.stages.iter().enumerate() {
842 if signal.stages.len() == idx {
843 signal.stages.push(Stage::new());
844 }
845 signal.stages[idx].stage_type = canonical_stage.stage_type.clone();
846 }
847 signals.push(signal);
848 }
849
850 BundleEdits { signals }
851 }
852}
853
854fn check_for_missing_turns(app: &App, members: &BTreeSet<IntersectionID>) -> Option<BundleEdits> {
856 let mut all_missing = BTreeSet::new();
857 for i in members {
858 all_missing.extend(
859 app.primary
860 .map
861 .get_traffic_signal(*i)
862 .missing_turns(app.primary.map.get_i(*i)),
863 );
864 }
865 if all_missing.is_empty() {
866 return None;
867 }
868
869 let mut bundle = BundleEdits::get_current(app, members);
870 for signal in &mut bundle.signals {
872 let mut stage = Stage::new();
873 for m in &all_missing {
875 if m.parent != signal.id {
876 continue;
877 }
878 if m.crosswalk {
879 stage.protected_movements.insert(*m);
880 } else {
881 stage.yield_movements.insert(*m);
882 }
883 }
884 signal.stages.insert(0, stage);
885 }
886 Some(bundle)
887}
888
889fn draw_multiple_signals(
890 ctx: &mut EventCtx,
891 app: &App,
892 members: &BTreeSet<IntersectionID>,
893 idx: usize,
894 translations: &[(f64, f64)],
895) -> GeomBatch {
896 let mut batch = GeomBatch::new();
897 for (i, (dx, dy)) in members.iter().zip(translations) {
898 let mut piece = GeomBatch::new();
899 piece.push(
900 app.cs.normal_intersection,
901 app.primary.map.get_i(*i).polygon.clone(),
902 );
903 traffic_signal::draw_signal_stage(
904 ctx.prerender,
905 &app.primary.map.get_traffic_signal(*i).stages[idx],
906 idx,
907 *i,
908 None,
909 &mut piece,
910 app,
911 TrafficSignalStyle::Yuwen,
912 );
913 batch.append(piece.translate(*dx, *dy));
914 }
915
916 let square_dims = 150.0;
918 batch = batch.autocrop();
919 let bounds = batch.get_bounds();
920 let zoom = (square_dims / bounds.width()).min(square_dims / bounds.height());
921 batch.scale(zoom)
922}
923
924fn squish_polygons_together(mut polygons: Vec<Polygon>) -> Vec<(f64, f64)> {
926 if polygons.len() == 1 {
927 return vec![(0.0, 0.0)];
928 }
929
930 let step_size = 0.8
933 * polygons.iter().fold(std::f64::MAX, |x, p| {
934 x.min(p.get_bounds().width()).min(p.get_bounds().height())
935 });
936
937 let mut translations: Vec<(f64, f64)> =
938 std::iter::repeat((0.0, 0.0)).take(polygons.len()).collect();
939 let mut indices: VecDeque<usize> = (0..polygons.len()).collect();
941
942 let mut attempts = 0;
943 while !indices.is_empty() {
944 let idx = indices.pop_front().unwrap();
945 let center = Pt2D::center(&polygons.iter().map(|p| p.center()).collect::<Vec<_>>());
946 let angle = Line::must_new(polygons[idx].center(), center).angle();
947 let pt = Pt2D::new(0.0, 0.0).project_away(Distance::meters(step_size), angle);
948
949 let translated = polygons[idx].translate(pt.x(), pt.y());
951 if polygons.iter().enumerate().any(|(i, p)| {
952 i != idx
953 && !translated
954 .intersection(p)
955 .map(|list| list.is_empty())
956 .unwrap_or(true)
957 }) {
958 } 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
975pub fn fade_irrelevant(app: &App, members: &BTreeSet<IntersectionID>) -> GeomBatch {
976 let mut holes = Vec::new();
977 for i in members {
978 let i = app.primary.map.get_i(*i);
979 holes.push(i.polygon.clone());
980 for r in &i.roads {
981 holes.push(app.primary.map.get_r(*r).get_thick_polygon());
982 }
983 }
984 match Polygon::convex_hull(holes) {
986 Ok(hole) => {
987 let fade_area = Polygon::with_holes(
988 app.primary
989 .map
990 .get_boundary_polygon()
991 .get_outer_ring()
992 .clone(),
993 vec![hole.into_outer_ring()],
994 );
995 GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)])
996 }
997 Err(_) => {
998 GeomBatch::new()
1000 }
1001 }
1002}