1use maplit::btreeset;
2
3use crate::ID;
4use abstutil::{prettyprint_usize, Timer};
5use geom::Speed;
6use map_gui::options::OptionsPanel;
7use map_gui::render::DrawMap;
8use map_gui::tools::grey_out_map;
9use map_model::{EditCmd, IntersectionID, LaneID, MapEdits};
10use widgetry::mapspace::ToggleZoomed;
11use widgetry::tools::{ChooseSomething, ColorLegend, PopupMsg};
12use widgetry::{
13 lctrl, Choice, Color, ControlState, EventCtx, GfxCtx, HorizontalAlignment, Image, Key, Line,
14 Menu, Outcome, Panel, State, Text, TextBox, TextExt, VerticalAlignment, Widget,
15};
16
17pub use self::roads::RoadEditor;
18pub use self::routes::RouteEditor;
19pub use self::stop_signs::StopSignEditor;
20pub use self::traffic_signals::TrafficSignalEditor;
21pub use self::validate::check_sidewalk_connectivity;
22use crate::app::{App, Transition};
23use crate::common::{tool_panel, CommonState, Warping};
24use crate::debug::DebugMode;
25use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen};
26
27mod crosswalks;
28mod multiple_roads;
29mod roads;
30mod routes;
31mod stop_signs;
32mod traffic_signals;
33mod validate;
34mod zones;
35
36pub struct EditMode {
37 tool_panel: Panel,
38 top_center: Panel,
39 changelist: Panel,
40 orig_edits: MapEdits,
41 orig_dirty: bool,
42
43 mode: GameplayMode,
45
46 map_edit_key: usize,
47
48 draw: ToggleZoomed,
49}
50
51impl EditMode {
52 pub fn new_state(ctx: &mut EventCtx, app: &mut App, mode: GameplayMode) -> Box<dyn State<App>> {
53 let orig_dirty = app.primary.dirty_from_edits;
54 assert!(app.primary.suspended_sim.is_none());
55 app.primary.suspended_sim = Some(app.primary.clear_sim());
56 let layer = crate::layer::map::Static::edits(ctx, app);
57 Box::new(EditMode {
58 tool_panel: tool_panel(ctx),
59 top_center: make_topcenter(ctx, app),
60 changelist: make_changelist(ctx, app),
61 orig_edits: app.primary.map.get_edits().clone(),
62 orig_dirty,
63 mode,
64 map_edit_key: app.primary.map.get_edits_change_key(),
65 draw: layer.draw,
66 })
67 }
68
69 fn quit(&self, ctx: &mut EventCtx, app: &mut App) -> Transition {
70 let old_sim = app.primary.suspended_sim.take().unwrap();
71
72 if app.primary.map.get_edits() == &self.orig_edits {
74 app.primary.sim = old_sim;
75 app.primary.dirty_from_edits = self.orig_dirty;
76 ctx.loading_screen("apply edits", |_, timer| {
78 app.primary.map.recalculate_pathfinding_after_edits(timer);
79 });
80 return Transition::Pop;
81 }
82
83 ctx.loading_screen("apply edits", move |ctx, timer| {
84 app.primary.map.recalculate_pathfinding_after_edits(timer);
85 if GameplayMode::FixTrafficSignals == self.mode {
86 app.primary.sim = old_sim;
87 app.primary.dirty_from_edits = true;
88 app.primary
89 .sim
90 .handle_live_edited_traffic_signals(&app.primary.map);
91 Transition::Pop
92 } else if app.primary.current_flags.live_map_edits {
93 app.primary.sim = old_sim;
94 app.primary.dirty_from_edits = true;
95 app.primary
96 .sim
97 .handle_live_edited_traffic_signals(&app.primary.map);
98 let (trips, parked_cars) =
99 app.primary.sim.handle_live_edits(&app.primary.map, timer);
100 if trips == 0 && parked_cars == 0 {
101 Transition::Pop
102 } else {
103 Transition::Replace(PopupMsg::new_state(
104 ctx,
105 "Map changes complete",
106 vec![
107 format!(
108 "Your edits interrupted {} trips and displaced {} parked cars",
109 prettyprint_usize(trips),
110 prettyprint_usize(parked_cars)
111 ),
112 "Simulation results won't be finalized unless you restart from \
113 midnight with your changes"
114 .to_string(),
115 ],
116 ))
117 }
118 } else {
119 Transition::Multi(vec![
120 Transition::Pop,
121 Transition::Replace(SandboxMode::async_new(
122 app,
123 self.mode.clone(),
124 Box::new(move |ctx, app| {
125 vec![Transition::Push(TimeWarpScreen::new_state(
126 ctx,
127 app,
128 old_sim.time(),
129 None,
130 ))]
131 }),
132 )),
133 ])
134 }
135 })
136 }
137}
138
139impl State<App> for EditMode {
140 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
141 {
142 let key = app.primary.map.get_edits_change_key();
145 if self.map_edit_key != key {
146 self.map_edit_key = key;
147 self.changelist = make_changelist(ctx, app);
148 let layer = crate::layer::map::Static::edits(ctx, app);
149 self.draw = layer.draw;
150 }
151 }
152
153 if let Some(t) = CommonState::debug_actions(ctx, app) {
154 return t;
155 }
156
157 ctx.canvas_movement();
158 if ctx.redo_mouseover() {
160 app.primary.current_selection = app.mouseover_unzoomed_roads_and_intersections(ctx);
161 if match app.primary.current_selection {
162 Some(ID::Lane(l)) => !self.mode.can_edit_roads() || !can_edit_lane(app, l),
163 Some(ID::Intersection(i)) => {
164 !self.mode.can_edit_stop_signs()
165 && app.primary.map.maybe_get_stop_sign(i).is_some()
166 }
167 Some(ID::Road(_)) => false,
168 _ => true,
169 } {
170 app.primary.current_selection = None;
171 }
172 }
173
174 if app.opts.dev && ctx.input.pressed(lctrl(Key::D)) {
175 return Transition::Push(DebugMode::new_state(ctx, app));
176 }
177
178 if let Outcome::Clicked(x) = self.top_center.event(ctx) {
179 match x.as_ref() {
180 "finish editing" => {
181 return self.quit(ctx, app);
182 }
183 "Fix sidewalk direction errors" => {
184 let new_fixes = validate::fix_sidewalk_direction(&app.primary.map);
185 let msg = if new_fixes.is_empty() {
186 format!("No sidewalk direction errors found")
187 } else {
188 let count = new_fixes.len();
189 let mut edits = app.primary.map.get_edits().clone();
190 edits.commands.extend(new_fixes);
191 apply_map_edits(ctx, app, edits);
192 format!("Fixed {count} sidewalk directions")
193 };
194 return Transition::Push(PopupMsg::new_state(
195 ctx,
196 "Fix sidewalk directions",
197 vec![msg],
198 ));
199 }
200 _ => unreachable!(),
201 }
202 }
203 if let Outcome::Clicked(x) = self.changelist.event(ctx) {
204 match x.as_ref() {
205 "manage proposals" => {
206 let mode = self.mode.clone();
207 return Transition::Push(ChooseSomething::new_state(
208 ctx,
209 "Manage proposals",
210 vec![
211 Choice::string("rename current proposal"),
212 Choice::string("open a saved proposal").multikey(lctrl(Key::L)),
213 Choice::string("create a blank proposal"),
214 Choice::string("save this proposal as..."),
215 Choice::string("share proposal"),
217 Choice::string("delete this proposal and remove all edits")
218 .fg(ctx.style().text_destructive_color),
219 ],
220 Box::new(move |choice, ctx, app| match choice.as_ref() {
221 "rename current proposal" => {
222 let old_name = app.primary.map.get_edits().edits_name.clone();
223 Transition::Replace(SaveEdits::new_state(
224 ctx,
225 app,
226 format!("Rename \"{}\"", old_name),
227 false,
228 Some(Transition::Pop),
229 Box::new(move |_, app| {
230 abstio::delete_file(abstio::path_edits(
231 app.primary.map.get_name(),
232 &old_name,
233 ));
234 }),
235 ))
236 }
237 "open a saved proposal" => {
238 if app.primary.map.unsaved_edits() {
239 Transition::Multi(vec![
240 Transition::Replace(LoadEdits::new_state(ctx, app, mode)),
241 Transition::Push(SaveEdits::new_state(
242 ctx,
243 app,
244 "Do you want to save your proposal first?",
245 true,
246 Some(Transition::Multi(vec![
247 Transition::Pop,
248 Transition::Pop,
249 ])),
250 Box::new(|_, _| {}),
251 )),
252 ])
253 } else {
254 Transition::Replace(LoadEdits::new_state(ctx, app, mode))
255 }
256 }
257 "create a blank proposal" => {
258 if app.primary.map.unsaved_edits() {
259 Transition::Replace(SaveEdits::new_state(
260 ctx,
261 app,
262 "Do you want to save your proposal first?",
263 true,
264 Some(Transition::Pop),
265 Box::new(|ctx, app| {
266 apply_map_edits(ctx, app, app.primary.map.new_edits());
267 }),
268 ))
269 } else {
270 apply_map_edits(ctx, app, app.primary.map.new_edits());
271 Transition::Pop
272 }
273 }
274 "save this proposal as..." => {
275 Transition::Replace(SaveEdits::new_state(
276 ctx,
277 app,
278 format!(
279 "Save \"{}\" as",
280 app.primary.map.get_edits().edits_name
281 ),
282 false,
283 Some(Transition::Pop),
284 Box::new(|_, _| {}),
285 ))
286 }
287 "share proposal" => {
288 Transition::Replace(crate::common::share::ShareProposal::new_state(
292 ctx, app, "--dev",
293 ))
294 }
295 "delete this proposal and remove all edits" => {
296 abstio::delete_file(abstio::path_edits(
297 app.primary.map.get_name(),
298 &app.primary.map.get_edits().edits_name,
299 ));
300 apply_map_edits(ctx, app, app.primary.map.new_edits());
301 Transition::Pop
302 }
303 _ => unreachable!(),
304 }),
305 ));
306 }
307 "load proposal" => {}
308 "undo" => {
309 let mut edits = app.primary.map.get_edits().clone();
310 let maybe_id = cmd_to_id(&edits.commands.pop().unwrap());
311 apply_map_edits(ctx, app, edits);
312 if let Some(id) = maybe_id {
313 return Transition::Push(Warping::new_state(
314 ctx,
315 app.primary.canonical_point(id.clone()).unwrap(),
316 Some(10.0),
317 Some(id),
318 &mut app.primary,
319 ));
320 }
321 }
322 x => {
323 let idx = x["change #".len()..].parse::<usize>().unwrap();
324 if let Some(id) = cmd_to_id(&app.primary.map.get_edits().commands[idx - 1]) {
325 return Transition::Push(Warping::new_state(
326 ctx,
327 app.primary.canonical_point(id.clone()).unwrap(),
328 Some(10.0),
329 Some(id),
330 &mut app.primary,
331 ));
332 }
333 }
334 }
335 }
336
337 if ctx.input.pressed(lctrl(Key::L)) {
339 if app.primary.map.unsaved_edits() {
340 return Transition::Multi(vec![
341 Transition::Push(LoadEdits::new_state(ctx, app, self.mode.clone())),
342 Transition::Push(SaveEdits::new_state(
343 ctx,
344 app,
345 "Do you want to save your proposal first?",
346 true,
347 Some(Transition::Multi(vec![Transition::Pop, Transition::Pop])),
348 Box::new(|_, _| {}),
349 )),
350 ]);
351 } else {
352 return Transition::Push(LoadEdits::new_state(ctx, app, self.mode.clone()));
353 }
354 }
355
356 if ctx.canvas.is_unzoomed() {
357 if let Some(id) = app.primary.current_selection.clone() {
358 if app.per_obj.left_click(ctx, "edit this") {
359 return Transition::Push(Warping::new_state(
360 ctx,
361 app.primary.canonical_point(id).unwrap(),
362 Some(10.0),
363 None,
364 &mut app.primary,
365 ));
366 }
367 }
368 } else {
369 if let Some(ID::Intersection(id)) = app.primary.current_selection {
370 if let Some(state) = maybe_edit_intersection(ctx, app, id, &self.mode) {
371 return Transition::Push(state);
372 }
373 }
374 if let Some(ID::Lane(l)) = app.primary.current_selection {
375 if app.per_obj.left_click(ctx, "edit lane") {
376 return Transition::Push(RoadEditor::new_state(ctx, app, l));
377 }
378 }
379 }
380
381 match self.tool_panel.event(ctx) {
382 Outcome::Clicked(x) => match x.as_ref() {
383 "back" => self.quit(ctx, app),
384 "settings" => Transition::Push(OptionsPanel::new_state(ctx, app)),
385 _ => unreachable!(),
386 },
387 _ => Transition::Keep,
388 }
389 }
390
391 fn draw(&self, g: &mut GfxCtx, app: &App) {
392 self.tool_panel.draw(g);
393 self.top_center.draw(g);
394 self.changelist.draw(g);
395 self.draw.draw(g);
396 CommonState::draw_osd(g, app);
397 }
398}
399
400pub struct SaveEdits {
401 panel: Panel,
402 current_name: String,
403 cancel: Option<Transition>,
404 on_success: Box<dyn Fn(&mut EventCtx, &mut App)>,
405 reset: bool,
406}
407
408impl SaveEdits {
409 pub fn new_state<I: Into<String>>(
410 ctx: &mut EventCtx,
411 app: &App,
412 title: I,
413 discard: bool,
414 cancel: Option<Transition>,
415 on_success: Box<dyn Fn(&mut EventCtx, &mut App)>,
416 ) -> Box<dyn State<App>> {
417 let initial_name = if app.primary.map.unsaved_edits() {
418 String::new()
419 } else {
420 format!("copy of {}", app.primary.map.get_edits().edits_name)
421 };
422 let mut save = SaveEdits {
423 current_name: initial_name.clone(),
424 panel: Panel::new_builder(Widget::col(vec![
425 Line(title).small_heading().into_widget(ctx),
426 Widget::row(vec![
427 "Name:".text_widget(ctx).centered_vert(),
428 TextBox::default_widget(ctx, "filename", initial_name),
429 ]),
430 Widget::placeholder(ctx, "warning"),
433 Widget::row(vec![
434 if discard {
435 ctx.style()
436 .btn_solid_destructive
437 .text("Discard proposal")
438 .build_def(ctx)
439 } else {
440 Widget::nothing()
441 },
442 if cancel.is_some() {
443 ctx.style()
444 .btn_outline
445 .text("Cancel")
446 .hotkey(Key::Escape)
447 .build_def(ctx)
448 } else {
449 Widget::nothing()
450 },
451 ctx.style()
452 .btn_solid_primary
453 .text("Save")
454 .disabled(true)
455 .build_def(ctx),
456 ])
457 .align_right(),
458 ]))
459 .build(ctx),
460 cancel,
461 on_success,
462 reset: discard,
463 };
464 save.recalc_btn(ctx, app);
465 Box::new(save)
466 }
467
468 fn recalc_btn(&mut self, ctx: &mut EventCtx, app: &App) {
469 if self.current_name.is_empty() {
470 self.panel.replace(
471 ctx,
472 "Save",
473 ctx.style()
474 .btn_solid_primary
475 .text("Save")
476 .disabled(true)
477 .build_def(ctx),
478 );
479 self.panel
480 .replace(ctx, "warning", Text::new().into_widget(ctx));
481 } else if abstio::file_exists(abstio::path_edits(
482 app.primary.map.get_name(),
483 &self.current_name,
484 )) {
485 self.panel.replace(
486 ctx,
487 "Save",
488 ctx.style()
489 .btn_solid_primary
490 .text("Save")
491 .disabled(true)
492 .build_def(ctx),
493 );
494 self.panel.replace(
495 ctx,
496 "warning",
497 Line("A proposal with this name already exists")
498 .fg(Color::hex("#FF5E5E"))
499 .into_widget(ctx),
500 );
501 } else {
502 self.panel.replace(
503 ctx,
504 "Save",
505 ctx.style()
506 .btn_solid_primary
507 .text("Save")
508 .hotkey(Key::Enter)
509 .build_def(ctx),
510 );
511 self.panel
512 .replace(ctx, "warning", Text::new().into_widget(ctx));
513 }
514 }
515}
516
517impl State<App> for SaveEdits {
518 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
519 if let Outcome::Clicked(x) = self.panel.event(ctx) {
520 match x.as_ref() {
521 "Save" => {
522 let mut edits = app.primary.map.get_edits().clone();
523 edits.edits_name = self.current_name.clone();
524 app.primary
525 .map
526 .must_apply_edits(edits, &mut Timer::throwaway());
527 app.primary.map.save_edits();
528 if self.reset {
529 apply_map_edits(ctx, app, app.primary.map.new_edits());
530 }
531 (self.on_success)(ctx, app);
532 return Transition::Pop;
533 }
534 "Discard proposal" => {
535 apply_map_edits(ctx, app, app.primary.map.new_edits());
536 return Transition::Pop;
537 }
538 "Cancel" => {
539 return self.cancel.take().unwrap();
540 }
541 _ => unreachable!(),
542 }
543 }
544 let name = self.panel.text_box("filename");
545 if name != self.current_name {
546 self.current_name = name;
547 self.recalc_btn(ctx, app);
548 }
549
550 Transition::Keep
551 }
552
553 fn draw(&self, g: &mut GfxCtx, app: &App) {
554 grey_out_map(g, app);
555 self.panel.draw(g);
556 }
557}
558
559pub struct LoadEdits {
560 panel: Panel,
561 mode: GameplayMode,
562}
563
564impl LoadEdits {
565 pub fn new_state(ctx: &mut EventCtx, app: &App, mode: GameplayMode) -> Box<dyn State<App>> {
567 let current_edits_name = &app.primary.map.get_edits().edits_name;
568
569 let mut your_proposals =
570 abstio::list_all_objects(abstio::path_all_edits(app.primary.map.get_name()))
571 .into_iter()
572 .map(|name| Choice::new(name.clone(), ()).active(&name != current_edits_name))
573 .collect::<Vec<_>>();
574 your_proposals.sort_by_key(|x| (x.label.starts_with("Untitled Proposal"), x.label.clone()));
577
578 let your_edits = vec![
579 Line("Your proposals").small_heading().into_widget(ctx),
580 Menu::widget(ctx, your_proposals),
581 ];
582 let mut proposals = vec![Line("Community proposals").small_heading().into_widget(ctx)];
585 for name in abstio::list_all_objects(abstio::path("system/proposals")) {
587 let path = abstio::path(format!("system/proposals/{}.json", name));
588 if let Ok(edits) =
589 MapEdits::load_from_file(&app.primary.map, path.clone(), &mut Timer::throwaway())
590 {
591 proposals.push(
592 ctx.style()
593 .btn_outline
594 .text(edits.get_title())
595 .build_widget(ctx, &path),
596 );
597 }
598 }
599
600 Box::new(LoadEdits {
601 mode,
602 panel: Panel::new_builder(Widget::col(vec![
603 Widget::row(vec![
604 Line("Load proposal").small_heading().into_widget(ctx),
605 ctx.style().btn_close_widget(ctx),
606 ]),
607 ctx.style()
608 .btn_outline
609 .text("Start over with blank proposal")
610 .build_def(ctx),
611 Widget::row(vec![Widget::col(your_edits), Widget::col(proposals)]).evenly_spaced(),
612 ]))
613 .exact_size_percent(50, 50)
614 .build(ctx),
615 })
616 }
617}
618
619impl State<App> for LoadEdits {
620 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
621 match self.panel.event(ctx) {
622 Outcome::Clicked(x) => {
623 match x.as_ref() {
624 "close" => Transition::Pop,
625 "Start over with blank proposal" => {
626 apply_map_edits(ctx, app, app.primary.map.new_edits());
627 Transition::Pop
628 }
629 path => {
630 let path = if path.ends_with(".json") {
633 path.to_string()
634 } else {
635 abstio::path_edits(app.primary.map.get_name(), path)
636 };
637
638 match MapEdits::load_from_file(
639 &app.primary.map,
640 path.clone(),
641 &mut Timer::throwaway(),
642 )
643 .and_then(|edits| {
644 if self.mode.allows(&edits) {
645 Ok(edits)
646 } else {
647 Err(anyhow!(
648 "The current gameplay mode restricts edits. This proposal has \
649 a banned command."
650 ))
651 }
652 }) {
653 Ok(edits) => {
654 apply_map_edits(ctx, app, edits);
655 app.primary
656 .sim
657 .handle_live_edited_traffic_signals(&app.primary.map);
658 Transition::Pop
659 }
660 Err(err) => {
663 println!("Can't load {}: {}", path, err);
664 Transition::Multi(vec![
665 Transition::Replace(LoadEdits::new_state(
666 ctx,
667 app,
668 self.mode.clone(),
669 )),
670 Transition::Push(PopupMsg::new_state(
674 ctx,
675 "Error",
676 vec![format!("Can't load {}", path), err.to_string()],
677 )),
678 ])
679 }
680 }
681 }
682 }
683 }
684 _ => Transition::Keep,
685 }
686 }
687
688 fn draw(&self, g: &mut GfxCtx, app: &App) {
689 grey_out_map(g, app);
690 self.panel.draw(g);
691 }
692}
693
694fn make_topcenter(ctx: &mut EventCtx, app: &App) -> Panel {
695 Panel::new_builder(Widget::col(vec![
696 Line("Editing map")
697 .small_heading()
698 .into_widget(ctx)
699 .centered_horiz(),
700 ctx.style()
701 .btn_solid_primary
702 .text(format!(
703 "Finish & resume from {}",
704 app.primary
705 .suspended_sim
706 .as_ref()
707 .unwrap()
708 .time()
709 .ampm_tostring()
710 ))
711 .hotkey(Key::Escape)
712 .build_widget(ctx, "finish editing"),
713 if app.opts.dev {
714 ctx.style()
715 .btn_outline
716 .text("Fix sidewalk direction errors")
717 .tooltip(Text::from_multiline(vec![
718 Line("Sidewalk directions must match the side of the road in a certain way."),
719 Line("It's easy to get this wrong; this tool will automatically fix things."),
720 ]))
721 .build_def(ctx)
722 } else {
723 Widget::nothing()
724 },
725 ]))
726 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
727 .build(ctx)
728}
729
730pub fn apply_map_edits(ctx: &mut EventCtx, app: &mut App, edits: MapEdits) {
731 ctx.loading_screen("apply map edits", |ctx, timer| {
732 if !app.store_unedited_map_in_secondary && app.primary.unedited_map.is_none() {
733 timer.start("save unedited map");
734 assert!(app.primary.map.get_edits().commands.is_empty());
735 app.primary.unedited_map = Some(app.primary.map.clone());
736 timer.stop("save unedited map");
737 }
738 if app.store_unedited_map_in_secondary && app.secondary.is_none() {
739 timer.start("save unedited map for toggling");
740 assert!(app.primary.map.get_edits().commands.is_empty());
741 let mut per_map = crate::app::PerMap::map_loaded(
742 app.primary.map.clone(),
743 app.primary.sim.clone(),
744 app.primary.current_flags.clone(),
745 &app.opts,
746 &app.cs,
747 ctx,
748 timer,
749 );
750 per_map.is_secondary = true;
752 app.secondary = Some(per_map);
753 timer.stop("save unedited map for toggling");
754 }
755
756 timer.start("edit map");
757 let effects = app.primary.map.must_apply_edits(edits, timer);
758 timer.stop("edit map");
759
760 if !effects.changed_roads.is_empty() || !effects.changed_intersections.is_empty() {
761 app.primary
762 .draw_map
763 .draw_all_unzoomed_roads_and_intersections = DrawMap::regenerate_unzoomed_layer(
764 ctx,
765 &app.primary.map,
766 &app.cs,
767 &app.opts,
768 timer,
769 );
770 }
771
772 for r in effects.changed_roads {
773 let road = app.primary.map.get_r(r);
774 app.primary.draw_map.recreate_road(road, &app.primary.map);
775 }
776
777 for i in effects.changed_intersections {
778 app.primary
779 .draw_map
780 .recreate_intersection(i, &app.primary.map);
781 }
782
783 for pl in effects.changed_parking_lots {
784 app.primary.draw_map.get_pl(pl).clear_rendering();
785 }
786
787 if app.primary.layer.as_ref().and_then(|l| l.name()) == Some("map edits") {
788 app.primary.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app)));
789 }
790 app.primary.map.save_edits();
795 });
796}
797
798pub fn can_edit_lane(app: &App, l: LaneID) -> bool {
799 let map = &app.primary.map;
800 let lane = map.get_l(l);
801 if lane.is_light_rail() || lane.is_footway() {
802 return false;
803 }
804 let r = map.get_parent(l);
805 if r.is_service() && r.lanes.iter().all(|l| !l.is_bus()) {
807 return false;
808 }
809
810 true
811}
812
813pub fn speed_limit_choices(app: &App, preset: Option<Speed>) -> Vec<Choice<Speed>> {
814 let mut speeds = (10..=70)
816 .step_by(5)
817 .map(|mph| Speed::miles_per_hour(mph as f64))
818 .collect::<Vec<_>>();
819 if let Some(preset) = preset {
820 if !speeds.contains(&preset) {
821 speeds.push(preset);
822 speeds.sort();
823 }
824 }
825 speeds
826 .into_iter()
827 .map(|x| Choice::new(x.to_string(&app.opts.units), x))
828 .collect()
829}
830
831pub fn maybe_edit_intersection(
832 ctx: &mut EventCtx,
833 app: &mut App,
834 id: IntersectionID,
835 mode: &GameplayMode,
836) -> Option<Box<dyn State<App>>> {
837 if app.primary.map.maybe_get_stop_sign(id).is_some()
838 && mode.can_edit_stop_signs()
839 && app.per_obj.left_click(ctx, "edit stop signs")
840 {
841 return Some(StopSignEditor::new_state(ctx, app, id, mode.clone()));
842 }
843
844 if app.primary.map.maybe_get_traffic_signal(id).is_some()
845 && app.per_obj.left_click(ctx, "edit traffic signal")
846 {
847 return Some(TrafficSignalEditor::new_state(
848 ctx,
849 app,
850 btreeset! {id},
851 mode.clone(),
852 ));
853 }
854
855 if app.primary.map.get_i(id).is_closed()
856 && app.per_obj.left_click(ctx, "re-open closed intersection")
857 {
858 let mut edits = app.primary.map.get_edits().clone();
861 edits
862 .commands
863 .push(app.primary.map.edit_intersection_cmd(id, |new| {
864 new.control = edits.original_intersections[&id].control.clone();
865 }));
866 apply_map_edits(ctx, app, edits);
867 }
868
869 None
870}
871
872fn make_changelist(ctx: &mut EventCtx, app: &App) -> Panel {
873 let edits = app.primary.map.get_edits();
876 let mut col = vec![
877 Widget::row(vec![
878 ctx.style()
879 .btn_outline
880 .popup(&edits.edits_name)
881 .hotkey(lctrl(Key::P))
882 .build_widget(ctx, "manage proposals"),
883 "autosaved"
884 .text_widget(ctx)
885 .container()
886 .padding(10)
887 .bg(Color::hex("#5D9630")),
888 ]),
889 ColorLegend::row(
890 ctx,
891 app.cs.edits_layer,
892 format!(
893 "{} roads, {} intersections changed",
894 edits.original_roads.len(),
895 edits.original_intersections.len()
896 ),
897 ),
898 ];
899
900 if edits.commands.len() > 5 {
901 col.push(format!("{} more...", edits.commands.len() - 5).text_widget(ctx));
902 }
903 for idx in edits.commands.len().max(5) - 5..edits.commands.len() {
904 let (summary, details) = edits.commands[idx].describe(&app.primary.map);
905 let mut txt = Text::from(format!("{}) {}", idx + 1, summary));
906 for line in details {
907 txt.add_line(Line(line).secondary());
908 }
909 let btn = ctx
910 .style()
911 .btn_plain
912 .btn()
913 .label_styled_text(txt, ControlState::Default)
914 .build_widget(ctx, format!("change #{}", idx + 1));
915 if idx == edits.commands.len() - 1 {
916 col.push(
917 Widget::row(vec![
918 btn,
919 ctx.style()
920 .btn_close()
921 .hotkey(lctrl(Key::Z))
922 .build_widget(ctx, "undo"),
923 ])
924 .padding(16)
925 .outline(ctx.style().btn_outline.outline),
926 );
927 } else {
928 col.push(btn);
929 }
930 }
931
932 Panel::new_builder(Widget::col(col))
933 .aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
934 .build(ctx)
935}
936
937fn cmd_to_id(cmd: &EditCmd) -> Option<ID> {
939 match cmd {
940 EditCmd::ChangeRoad { r, .. } => Some(ID::Road(*r)),
941 EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)),
942 EditCmd::ChangeRouteSchedule { .. } => None,
943 }
944}
945
946pub struct ConfirmDiscard {
947 panel: Panel,
948 discard: Box<dyn Fn(&mut App)>,
949}
950
951impl ConfirmDiscard {
952 pub fn new_state(ctx: &mut EventCtx, discard: Box<dyn Fn(&mut App)>) -> Box<dyn State<App>> {
953 Box::new(ConfirmDiscard {
954 discard,
955 panel: Panel::new_builder(Widget::col(vec![
956 Widget::row(vec![
957 Image::from_path("system/assets/tools/alert.svg")
958 .untinted()
959 .into_widget(ctx)
960 .container()
961 .padding_top(6),
962 Line("Alert").small_heading().into_widget(ctx),
963 ctx.style().btn_close_widget(ctx),
964 ]),
965 "Are you sure you want to discard changes you made?".text_widget(ctx),
966 Widget::row(vec![
967 ctx.style()
968 .btn_outline
969 .text("Cancel")
970 .hotkey(Key::Escape)
971 .build_def(ctx),
972 ctx.style()
973 .btn_solid_destructive
974 .text("Yes, discard")
975 .build_def(ctx),
976 ])
977 .align_right(),
978 ]))
979 .build(ctx),
980 })
981 }
982}
983
984impl State<App> for ConfirmDiscard {
985 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
986 match self.panel.event(ctx) {
987 Outcome::Clicked(x) => match x.as_ref() {
988 "close" | "Cancel" => Transition::Pop,
989 "Yes, discard" => {
990 (self.discard)(app);
991 Transition::Multi(vec![Transition::Pop, Transition::Pop])
992 }
993 _ => unreachable!(),
994 },
995 _ => Transition::Keep,
996 }
997 }
998
999 fn draw(&self, g: &mut GfxCtx, _: &App) {
1000 self.panel.draw(g);
1001 }
1002}