1use std::collections::HashMap;
2
3use crate::ID;
4use geom::{Bounds, CornerRadii, Distance, Polygon, Pt2D, UnitFmt};
5use map_gui::render::{Renderable, OUTLINE_THICKNESS};
6use map_model::{
7 osm, BufferType, Direction, EditCmd, EditRoad, LaneID, LaneSpec, LaneType, MapEdits, Road,
8 RoadID,
9};
10use widgetry::tools::PopupMsg;
11use widgetry::{
12 lctrl, Choice, Color, ControlState, DragDrop, Drawable, EdgeInsets, EventCtx, GeomBatch,
13 GeomBatchStack, GfxCtx, HorizontalAlignment, Image, Key, Line, Outcome, Panel, PersistentSplit,
14 Spinner, StackAxis, State, Text, TextExt, VerticalAlignment, Widget, DEFAULT_CORNER_RADIUS,
15};
16
17use crate::app::{App, Transition};
18use crate::common::Warping;
19use crate::edit::zones::ZoneEditor;
20use crate::edit::{apply_map_edits, can_edit_lane, speed_limit_choices};
21
22pub struct RoadEditor {
26 r: RoadID,
27 selected_lane: Option<LaneID>,
28 hovering_on_lane: Option<LaneID>,
30 top_panel: Panel,
31 main_panel: Panel,
32 fade_irrelevant: Drawable,
33
34 lane_highlights: ((Option<LaneID>, Option<LaneID>), Drawable),
36 draw_drop_position: Drawable,
38
39 num_edit_cmds_originally: usize,
41 redo_stack: Vec<EditCmd>,
42 orig_road_state: EditRoad,
43}
44
45impl RoadEditor {
46 pub fn new_state(ctx: &mut EventCtx, app: &mut App, l: LaneID) -> Box<dyn State<App>> {
48 RoadEditor::create(ctx, app, l.road, Some(l))
49 }
50
51 pub fn new_state_without_lane(
52 ctx: &mut EventCtx,
53 app: &mut App,
54 r: RoadID,
55 ) -> Box<dyn State<App>> {
56 RoadEditor::create(ctx, app, r, None)
57 }
58
59 fn create(
60 ctx: &mut EventCtx,
61 app: &mut App,
62 r: RoadID,
63 selected_lane: Option<LaneID>,
64 ) -> Box<dyn State<App>> {
65 app.primary.current_selection = None;
66
67 let mut editor = RoadEditor {
68 r,
69 selected_lane,
70 top_panel: Panel::empty(ctx),
71 main_panel: Panel::empty(ctx),
72 fade_irrelevant: Drawable::empty(ctx),
73 lane_highlights: ((None, None), Drawable::empty(ctx)),
74 draw_drop_position: Drawable::empty(ctx),
75 hovering_on_lane: None,
76
77 num_edit_cmds_originally: app.primary.map.get_edits().commands.len(),
78 redo_stack: Vec::new(),
79 orig_road_state: app.primary.map.get_r_edit(r),
80 };
81 editor.recalc_all_panels(ctx, app);
82 Box::new(editor)
83 }
84
85 fn lane_for_idx(&self, app: &App, idx: usize) -> LaneID {
86 app.primary.map.get_r(self.r).lanes[idx].id
87 }
88
89 fn modify_current_lane<F: Fn(&mut EditRoad, usize)>(
90 &mut self,
91 ctx: &mut EventCtx,
92 app: &mut App,
93 select_new_lane_offset: Option<isize>,
94 f: F,
95 ) -> Transition {
96 let idx = self.selected_lane.unwrap().offset;
97 let cmd = app.primary.map.edit_road_cmd(self.r, |new| (f)(new, idx));
98
99 if let EditCmd::ChangeRoad { ref new, .. } = cmd {
101 let mut parking = 0;
102 let mut driving = 0;
103 for spec in &new.lanes_ltr {
104 if spec.lt == LaneType::Parking {
105 parking += 1;
106 } else if spec.lt == LaneType::Driving {
107 driving += 1;
108 }
109 }
110 if parking > 0 && driving == 0 {
111 return Transition::Push(PopupMsg::new_state(
112 ctx,
113 "Error",
114 vec!["Parking can't exist without a driving lane to access it."],
115 ));
116 }
117 }
118
119 let mut edits = app.primary.map.get_edits().clone();
120 edits.commands.push(cmd);
121 apply_map_edits(ctx, app, edits);
122 self.redo_stack.clear();
123
124 self.selected_lane = select_new_lane_offset
125 .map(|offset| self.lane_for_idx(app, (idx as isize + offset) as usize));
126 self.recalc_hovering(ctx, app);
127
128 self.recalc_all_panels(ctx, app);
129
130 Transition::Keep
131 }
132
133 fn recalc_all_panels(&mut self, ctx: &mut EventCtx, app: &App) {
134 self.main_panel = make_main_panel(
135 ctx,
136 app,
137 app.primary.map.get_r(self.r),
138 self.selected_lane,
139 self.hovering_on_lane,
140 );
141
142 self.top_panel = make_top_panel(
143 ctx,
144 app,
145 self.num_edit_cmds_originally,
146 self.redo_stack.is_empty(),
147 self.r,
148 self.orig_road_state.clone(),
149 );
150
151 self.recalc_lane_highlights(ctx, app);
152
153 self.fade_irrelevant = fade_irrelevant(app, self.r).upload(ctx);
154 }
155
156 fn recalc_lane_highlights(&mut self, ctx: &mut EventCtx, app: &App) {
157 let drag_drop = self.main_panel.find::<DragDrop<LaneID>>("lane cards");
158 let selected = drag_drop.selected_value().or(self.selected_lane);
159 let hovering = drag_drop.hovering_value().or(self.hovering_on_lane);
160 if (selected, hovering) != self.lane_highlights.0 {
161 self.lane_highlights = build_lane_highlights(ctx, app, selected, hovering);
162 }
163 }
164
165 fn compress_edits(&self, app: &App) -> Option<MapEdits> {
166 if app.primary.map.get_edits().commands.len() > self.num_edit_cmds_originally + 2 {
168 let mut edits = app.primary.map.get_edits().clone();
169 let last_edit = match edits.commands.pop().unwrap() {
170 EditCmd::ChangeRoad { new, .. } => new,
171 _ => unreachable!(),
172 };
173 edits.commands.truncate(self.num_edit_cmds_originally + 1);
174 match edits.commands.last_mut().unwrap() {
175 EditCmd::ChangeRoad { ref mut new, .. } => {
176 *new = last_edit;
177 }
178 _ => unreachable!(),
179 }
180 return Some(edits);
181 }
182 None
183 }
184
185 fn recalc_hovering(&mut self, ctx: &EventCtx, app: &mut App) {
187 app.recalculate_current_selection(ctx);
188 self.hovering_on_lane = match app.primary.current_selection.take() {
189 Some(ID::Lane(l)) if can_edit_lane(app, l) => Some(l),
190 _ => None,
191 };
192 }
193}
194
195impl State<App> for RoadEditor {
196 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
197 ctx.canvas_movement();
198
199 let mut panels_need_recalc = false;
200
201 if let Outcome::Clicked(x) = self.top_panel.event(ctx) {
202 match x.as_ref() {
203 "Finish" => {
204 if let Some(edits) = self.compress_edits(app) {
205 apply_map_edits(ctx, app, edits);
206 }
207 return Transition::Pop;
208 }
209 "Cancel" => {
210 let mut edits = app.primary.map.get_edits().clone();
211 if edits.commands.len() != self.num_edit_cmds_originally {
212 edits.commands.truncate(self.num_edit_cmds_originally);
213 apply_map_edits(ctx, app, edits);
214 }
215 return Transition::Pop;
216 }
217 "Revert" => {
218 let mut edits = app.primary.map.get_edits().clone();
219 edits.commands.push(EditCmd::ChangeRoad {
220 r: self.r,
221 old: app.primary.map.get_r_edit(self.r),
222 new: EditRoad::get_orig_from_osm(
223 app.primary.map.get_r(self.r),
224 app.primary.map.get_config(),
225 ),
226 });
227 apply_map_edits(ctx, app, edits);
228
229 self.redo_stack.clear();
230 self.selected_lane = None;
231 self.recalc_hovering(ctx, app);
232 panels_need_recalc = true;
233 }
234 "undo" => {
235 let mut edits = app.primary.map.get_edits().clone();
236 self.redo_stack.push(edits.commands.pop().unwrap());
237 apply_map_edits(ctx, app, edits);
238
239 self.selected_lane = None;
240 self.recalc_hovering(ctx, app);
241 panels_need_recalc = true;
242 }
243 "redo" => {
244 let mut edits = app.primary.map.get_edits().clone();
245 edits.commands.push(self.redo_stack.pop().unwrap());
246 apply_map_edits(ctx, app, edits);
247
248 self.selected_lane = None;
249 self.recalc_hovering(ctx, app);
250 panels_need_recalc = true;
251 }
252 "jump to road" => {
253 return Transition::Push(Warping::new_state(
254 ctx,
255 app.primary.canonical_point(ID::Road(self.r)).unwrap(),
256 Some(10.0),
257 Some(ID::Road(self.r)),
258 &mut app.primary,
259 ));
260 }
261 "Apply to multiple road segments" => {
262 return Transition::Push(
263 crate::edit::multiple_roads::SelectSegments::new_state(
264 ctx,
265 app,
266 self.r,
267 self.orig_road_state.clone(),
268 app.primary.map.get_r_edit(self.r),
269 self.compress_edits(app)
270 .unwrap_or_else(|| app.primary.map.get_edits().clone()),
271 ),
272 );
273 }
274 _ => unreachable!(),
275 }
276 }
277
278 match self.main_panel.event(ctx) {
279 Outcome::Clicked(x) => {
280 if let Some(idx) = x.strip_prefix("modify Lane #") {
281 self.selected_lane = Some(LaneID::decode_u32(idx.parse().unwrap()));
282 panels_need_recalc = true;
283 } else if x == "delete lane" {
284 return self.modify_current_lane(ctx, app, None, |new, idx| {
285 new.lanes_ltr.remove(idx);
286 });
287 } else if x == "flip direction" {
288 return self.modify_current_lane(ctx, app, Some(0), |new, idx| {
289 new.lanes_ltr[idx].dir = new.lanes_ltr[idx].dir.opposite();
290 });
291 } else if let Some(lt) = x.strip_prefix("change to ") {
292 let lt = if lt == "buffer" {
293 self.main_panel.persistent_split_value("change to buffer")
294 } else {
295 LaneType::from_short_name(lt).unwrap()
296 };
297 let width = LaneSpec::typical_lane_widths(
298 lt,
299 app.primary
300 .map
301 .get_r(self.r)
302 .osm_tags
303 .get(osm::HIGHWAY)
304 .unwrap(),
305 )[0]
306 .0;
307 return self.modify_current_lane(ctx, app, Some(0), |new, idx| {
308 new.lanes_ltr[idx].lt = lt;
309 new.lanes_ltr[idx].width = width;
310 });
311 } else if let Some(lt) = x.strip_prefix("add ") {
312 let lt = if lt == "buffer" {
313 self.main_panel.persistent_split_value("add buffer")
314 } else {
315 LaneType::from_short_name(lt).unwrap()
316 };
317
318 if lt == LaneType::Parking
320 && app
321 .primary
322 .map
323 .get_r(self.r)
324 .lanes
325 .iter()
326 .all(|l| l.lane_type != LaneType::Driving)
327 {
328 return Transition::Push(PopupMsg::new_state(ctx, "Error", vec!["Add a driving lane first. Parking can't exist without a way to access it."]));
329 }
330
331 let mut edits = app.primary.map.get_edits().clone();
332 let old = app.primary.map.get_r_edit(self.r);
333 let mut new = old.clone();
334 let idx = LaneSpec::add_new_lane(
335 &mut new.lanes_ltr,
336 lt,
337 app.primary
338 .map
339 .get_r(self.r)
340 .osm_tags
341 .get(osm::HIGHWAY)
342 .unwrap(),
343 app.primary.map.get_config().driving_side,
344 );
345 edits.commands.push(EditCmd::ChangeRoad {
346 r: self.r,
347 old,
348 new,
349 });
350 apply_map_edits(ctx, app, edits);
351 self.redo_stack.clear();
352
353 self.selected_lane = Some(self.lane_for_idx(app, idx));
354 self.recalc_hovering(ctx, app);
355 panels_need_recalc = true;
356 } else if x == "Access restrictions" {
357 if let Some(edits) = self.compress_edits(app) {
361 apply_map_edits(ctx, app, edits);
362 }
363 return Transition::Replace(ZoneEditor::new_state(ctx, app, self.r));
364 } else {
365 unreachable!()
366 }
367 }
368 Outcome::Changed(x) => match x.as_ref() {
369 "speed limit" => {
370 let speed_limit = self.main_panel.dropdown_value("speed limit");
371
372 let mut edits = app.primary.map.get_edits().clone();
373 let old = app.primary.map.get_r_edit(self.r);
374 let mut new = old.clone();
375 new.speed_limit = speed_limit;
376 edits.commands.push(EditCmd::ChangeRoad {
377 r: self.r,
378 old,
379 new,
380 });
381 apply_map_edits(ctx, app, edits);
382 self.redo_stack.clear();
383
384 self.selected_lane = self
386 .selected_lane
387 .map(|id| self.lane_for_idx(app, id.offset));
388 self.recalc_hovering(ctx, app);
389 panels_need_recalc = true;
390 }
391 "width preset" => {
392 let width = self.main_panel.dropdown_value("width preset");
393 return self.modify_current_lane(ctx, app, Some(0), |new, idx| {
394 new.lanes_ltr[idx].width = width;
395 });
396 }
397 "width custom" => {
398 let width = self.main_panel.spinner("width custom");
399 return self.modify_current_lane(ctx, app, Some(0), |new, idx| {
400 new.lanes_ltr[idx].width = width;
401 });
402 }
403 "lane cards" => {
404 panels_need_recalc = true;
406 }
407 "dragging lane cards" => {
408 let (from, to) = self
409 .main_panel
410 .find::<DragDrop<LaneID>>("lane cards")
411 .get_dragging_state()
412 .unwrap();
413 self.draw_drop_position = draw_drop_position(app, self.r, from, to).upload(ctx);
414 }
415 "change to buffer" => {
416 let lt = self.main_panel.persistent_split_value("change to buffer");
417 app.session.buffer_lane_type = lt;
418 let width = LaneSpec::typical_lane_widths(
419 lt,
420 app.primary
421 .map
422 .get_r(self.r)
423 .osm_tags
424 .get(osm::HIGHWAY)
425 .unwrap(),
426 )[0]
427 .0;
428 return self.modify_current_lane(ctx, app, Some(0), |new, idx| {
429 new.lanes_ltr[idx].lt = lt;
430 new.lanes_ltr[idx].width = width;
431 });
432 }
433 "add buffer" => {
434 app.session.buffer_lane_type =
435 self.main_panel.persistent_split_value("add buffer");
436 }
437 _ => unreachable!(),
438 },
439 Outcome::DragDropReleased(_, old_idx, new_idx) => {
440 self.draw_drop_position = Drawable::empty(ctx);
441
442 if old_idx != new_idx {
443 let mut edits = app.primary.map.get_edits().clone();
444 edits
445 .commands
446 .push(app.primary.map.edit_road_cmd(self.r, |new| {
447 let spec = new.lanes_ltr.remove(old_idx);
448 new.lanes_ltr.insert(new_idx, spec);
449 }));
450 apply_map_edits(ctx, app, edits);
451 self.redo_stack.clear();
452 }
453
454 self.selected_lane = Some(self.lane_for_idx(app, new_idx));
455 self.hovering_on_lane = self.selected_lane;
456 panels_need_recalc = true;
457 }
458 Outcome::Nothing => {}
459 _ => debug!("main_panel had unhandled outcome"),
460 }
461
462 if self
463 .main_panel
464 .find::<DragDrop<LaneID>>("lane cards")
465 .get_dragging_state()
466 .is_some()
467 {
468 self.hovering_on_lane = None;
470 self.recalc_lane_highlights(ctx, app);
473 } else if ctx.redo_mouseover() {
474 let prev_hovering_on_lane = self.hovering_on_lane;
475 self.recalc_hovering(ctx, app);
476 if prev_hovering_on_lane != self.hovering_on_lane {
477 panels_need_recalc = true;
478 }
479 }
480 if let Some(l) = self.hovering_on_lane {
481 if ctx.normal_left_click() {
482 if l.road == self.r {
483 self.selected_lane = Some(l);
484 panels_need_recalc = true;
485 } else {
486 if let Some(edits) = self.compress_edits(app) {
489 apply_map_edits(ctx, app, edits);
490 }
491 return Transition::Replace(RoadEditor::new_state(ctx, app, l));
492 }
493 }
494 } else if self.selected_lane.is_some()
495 && ctx.canvas.get_cursor_in_map_space().is_some()
496 && ctx.normal_left_click()
497 {
498 self.selected_lane = None;
500 self.hovering_on_lane = None;
501 panels_need_recalc = true;
502 }
503
504 if panels_need_recalc {
505 self.recalc_all_panels(ctx, app);
506 }
507
508 Transition::Keep
509 }
510
511 fn draw(&self, g: &mut GfxCtx, _: &App) {
512 g.redraw(&self.fade_irrelevant);
513 g.redraw(&self.lane_highlights.1);
514 g.redraw(&self.draw_drop_position);
515 self.top_panel.draw(g);
516 self.main_panel.draw(g);
517 }
518}
519
520fn make_top_panel(
521 ctx: &mut EventCtx,
522 app: &App,
523 num_edit_cmds_originally: usize,
524 no_redo_cmds: bool,
525 r: RoadID,
526 orig_road_state: EditRoad,
527) -> Panel {
528 let map = &app.primary.map;
529 let current_state = map.get_r_edit(r);
530
531 Panel::new_builder(Widget::col(vec![
532 Widget::row(vec![
533 Line(format!("Edit {}", r)).small_heading().into_widget(ctx),
534 ctx.style()
535 .btn_plain
536 .icon("system/assets/tools/location.svg")
537 .build_widget(ctx, "jump to road"),
538 ctx.style()
539 .btn_plain
540 .text("+ Apply to multiple")
541 .label_color(Color::hex("#4CA7E9"), ControlState::Default)
542 .hotkey(Key::M)
543 .disabled(current_state == orig_road_state)
544 .disabled_tooltip("You have to edit one road segment first, then you can apply the changes to more segments.")
545 .build_widget(ctx, "Apply to multiple road segments"),
546 ]),
547 Widget::row(vec![
548 ctx.style()
549 .btn_solid_primary
550 .text("Finish")
551 .hotkey(Key::Enter)
552 .build_def(ctx),
553 ctx.style()
554 .btn_plain
555 .icon("system/assets/tools/undo.svg")
556 .disabled(map.get_edits().commands.len() == num_edit_cmds_originally)
557 .hotkey(lctrl(Key::Z))
558 .build_widget(ctx, "undo"),
559 ctx.style()
560 .btn_plain
561 .icon("system/assets/tools/redo.svg")
562 .disabled(no_redo_cmds)
563 .hotkey(lctrl(Key::Y))
565 .build_widget(ctx, "redo"),
566 ctx.style()
567 .btn_plain_destructive
568 .text("Revert")
569 .disabled(current_state == EditRoad::get_orig_from_osm(map.get_r(r), map.get_config()))
570 .build_def(ctx),
571 ctx.style()
572 .btn_plain
573 .text("Cancel")
574 .hotkey(Key::Escape)
575 .build_def(ctx),
576 ]),
577 ]))
578 .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
579 .build(ctx)
580}
581
582fn make_main_panel(
583 ctx: &mut EventCtx,
584 app: &App,
585 road: &Road,
586 selected_lane: Option<LaneID>,
587 hovering_on_lane: Option<LaneID>,
588) -> Panel {
589 let map = &app.primary.map;
590
591 let current_lt = selected_lane.map(|l| map.get_l(l).lane_type);
592
593 let current_lts: Vec<LaneType> = road.lanes.iter().map(|l| l.lane_type).collect();
594
595 let lane_types = [
596 (LaneType::Driving, Some(Key::D)),
597 (LaneType::Biking, Some(Key::B)),
598 (LaneType::Bus, Some(Key::T)),
599 (LaneType::Sidewalk, Some(Key::S)),
600 (LaneType::Parking, Some(Key::P)),
601 (LaneType::Construction, Some(Key::C)),
602 ];
603 let moving_lane_idx = 4;
605
606 let mut lane_type_buttons = HashMap::new();
607 for (lane_type, _key) in lane_types {
608 let btn = ctx
609 .style()
610 .btn_outline
611 .icon(lane_type_to_icon(lane_type).unwrap());
612
613 lane_type_buttons.insert(lane_type, btn);
614 }
615
616 let make_buffer_picker = |ctx, prefix, initial_type| {
617 PersistentSplit::widget(
618 ctx,
619 &format!("{} buffer", prefix),
620 initial_type,
621 None,
622 vec![
623 BufferType::Stripes,
624 BufferType::Verge,
625 BufferType::FlexPosts,
626 BufferType::Planters,
627 BufferType::JerseyBarrier,
628 BufferType::Curb,
629 ]
630 .into_iter()
631 .map(|buf| {
632 let lt = LaneType::Buffer(buf);
633 let width =
634 LaneSpec::typical_lane_widths(lt, road.osm_tags.get(osm::HIGHWAY).unwrap())[0]
635 .0;
636 Choice::new(
637 format!("{} ({})", lt.short_name(), width.to_string(&app.opts.units)),
638 lt,
639 )
640 })
641 .collect(),
642 )
643 };
644
645 let add_lane_row = Widget::row(vec![
646 "add new".text_widget(ctx).centered_vert(),
647 Widget::row({
648 let mut row: Vec<Widget> = lane_types
649 .iter()
650 .map(|(lt, key)| {
651 lane_type_buttons
652 .get(lt)
653 .expect("lane_type button should have been cached")
654 .clone()
655 .hotkey(if selected_lane.is_none() {
658 key.map(|k| k.into())
659 } else {
660 None
661 })
662 .build_widget(ctx, format!("add {}", lt.short_name()))
663 .centered_vert()
664 })
665 .collect();
666 row.push(make_buffer_picker(ctx, "add", app.session.buffer_lane_type));
667 row.insert(moving_lane_idx, Widget::vert_separator(ctx, 40.0));
668 row
669 }),
670 ]);
671 let mut drag_drop = DragDrop::new(ctx, "lane cards", StackAxis::Horizontal);
672
673 let road_width = road.get_width();
674
675 for l in &road.lanes {
676 let idx = l.id.offset;
677 let id = l.id;
678 let dir = l.dir;
679 let lt = l.lane_type;
680
681 let mut icon_stack = GeomBatchStack::vertical(vec![
682 Image::from_path(lane_type_to_icon(lt).unwrap())
683 .dims((60.0, 50.0))
684 .build_batch(ctx)
685 .unwrap()
686 .0,
687 ]);
688 icon_stack.set_spacing(20.0);
689
690 if can_reverse(lt) {
691 icon_stack.push(
692 Image::from_path(if dir == Direction::Fwd {
693 "system/assets/edit/forwards.svg"
694 } else {
695 "system/assets/edit/backwards.svg"
696 })
697 .dims((30.0, 30.0))
698 .build_batch(ctx)
699 .unwrap()
700 .0,
701 );
702 }
703 let lane_width = map.get_l(id).width;
704
705 icon_stack.push(Text::from(Line(lane_width.to_string(&app.opts.units))).render(ctx));
706 let icon_batch = icon_stack.batch();
707 let icon_bounds = icon_batch.get_bounds();
708
709 let mut rounding = CornerRadii::zero();
710 if idx == 0 {
711 rounding.top_left = DEFAULT_CORNER_RADIUS;
712 }
713 if idx == road.lanes.len() - 1 {
714 rounding.top_right = DEFAULT_CORNER_RADIUS;
715 }
716
717 let (card_bounds, default_batch, hovering_batch, selected_batch) = {
718 let card_batch = |(icon_batch, is_hovering, is_selected)| -> (GeomBatch, Bounds) {
719 let road_width_px = 700.0;
720 let icon_width = 30.0;
721 let lane_ratio_of_road = lane_width / road_width;
722 let h_padding = ((road_width_px * lane_ratio_of_road - icon_width) / 2.0).max(2.0);
723
724 Image::from_batch(icon_batch, icon_bounds)
725 .bg_color(if is_selected {
728 selected_lane_bg(ctx)
729 } else if is_hovering {
730 selected_lane_bg(ctx).dull(0.3)
731 } else {
732 selected_lane_bg(ctx).dull(0.15)
733 })
734 .color(ctx.style().btn_tab.fg)
735 .dims((30.0, 100.0))
736 .padding(EdgeInsets {
737 top: 32.0,
738 left: h_padding,
739 bottom: 32.0,
740 right: h_padding,
741 })
742 .corner_rounding(rounding)
743 .build_batch(ctx)
744 .unwrap()
745 };
746
747 let (mut default_batch, bounds) = card_batch((icon_batch.clone(), false, false));
748 let border = {
749 let top_left = Pt2D::new(bounds.min_x, bounds.max_y - 2.0);
750 let bottom_right = Pt2D::new(bounds.max_x, bounds.max_y);
751 Polygon::rectangle_two_corners(top_left, bottom_right).unwrap()
752 };
753 default_batch.push(ctx.style().section_outline.1.shade(0.2), border);
754 let (hovering_batch, _) = card_batch((icon_batch.clone(), true, false));
755 let (selected_batch, _) = card_batch((icon_batch, false, true));
756 (bounds, default_batch, hovering_batch, selected_batch)
757 };
758
759 drag_drop.push_card(
760 id,
761 card_bounds.into(),
762 default_batch,
763 hovering_batch,
764 selected_batch,
765 );
766 }
767 drag_drop.set_initial_state(selected_lane, hovering_on_lane);
768
769 let modify_lane = if let Some(l) = selected_lane {
770 let lane = map.get_l(l);
771 Widget::col(vec![
772 Widget::row(vec![
773 "change to".text_widget(ctx).centered_vert(),
774 Widget::row({
775 let mut row: Vec<Widget> = lane_types
776 .iter()
777 .map(|(lt, key)| {
778 let lt = *lt;
779 let mut btn = lane_type_buttons
780 .get(<)
781 .expect("lane_type button should have been cached")
782 .clone()
783 .hotkey(key.map(|k| k.into()));
784
785 if current_lt == Some(lt) {
786 btn = btn.disabled(true);
789 } else if lt == LaneType::Parking
790 && current_lts
791 .iter()
792 .filter(|x| **x == LaneType::Parking)
793 .count()
794 == 2
795 {
796 btn = btn
802 .disabled(true)
803 .disabled_tooltip("This road already has two parking lanes");
804 } else if lt == LaneType::Sidewalk
805 && current_lts.iter().filter(|x| x.is_walkable()).count() == 2
806 {
807 btn = btn
812 .disabled(true)
813 .disabled_tooltip("This road already has two sidewalks");
814 }
815
816 btn.build_widget(ctx, format!("change to {}", lt.short_name()))
817 })
818 .collect();
819 row.push(make_buffer_picker(
820 ctx,
821 "change to",
822 match current_lt {
823 Some(lt @ LaneType::Buffer(_)) => lt,
824 _ => app.session.buffer_lane_type,
825 },
826 ));
827 row.insert(moving_lane_idx, Widget::vert_separator(ctx, 40.0));
828 row
829 }),
830 ]),
831 Widget::row(vec![
832 ctx.style()
833 .btn_solid_destructive
834 .icon("system/assets/tools/trash.svg")
835 .disabled(road.lanes.len() == 1)
836 .hotkey(Key::Backspace)
837 .build_widget(ctx, "delete lane")
838 .centered_vert(),
839 ctx.style()
840 .btn_plain
841 .text("flip direction")
842 .disabled(!can_reverse(lane.lane_type))
843 .hotkey(Key::F)
844 .build_def(ctx)
845 .centered_vert(),
846 Widget::row(vec![
847 Line("Width").secondary().into_widget(ctx).centered_vert(),
848 Widget::dropdown(ctx, "width preset", lane.width, width_choices(app, l)),
849 Spinner::widget_with_custom_rendering(
850 ctx,
851 "width custom",
852 (Distance::meters(0.3), Distance::meters(7.0)),
853 lane.width,
854 Distance::meters(0.1),
855 Box::new(|x| x.to_string(&UnitFmt::metric())),
858 ),
859 ])
860 .section(ctx),
861 ]),
862 ])
863 } else {
864 Widget::nothing()
865 };
866
867 let total_width = {
868 let line1 = Text::from_all(vec![
869 Line("Total width ").secondary(),
870 Line(road_width.to_string(&app.opts.units)),
871 ])
872 .into_widget(ctx);
873 let orig_width = EditRoad::get_orig_from_osm(map.get_r(road.id), map.get_config())
874 .lanes_ltr
875 .into_iter()
876 .map(|spec| spec.width)
877 .sum();
878 let line2 = ctx
879 .style()
880 .btn_plain
881 .btn()
882 .label_styled_text(
883 Text::from(match road_width.cmp(&orig_width) {
884 std::cmp::Ordering::Equal => Line("No change").secondary(),
885 std::cmp::Ordering::Less => Line(format!(
886 "- {}",
887 (orig_width - road_width).to_string(&app.opts.units)
888 ))
889 .fg(Color::GREEN),
890 std::cmp::Ordering::Greater => Line(format!(
891 "+ {}",
892 (road_width - orig_width).to_string(&app.opts.units)
893 ))
894 .fg(Color::RED),
895 }),
896 ControlState::Default,
897 )
898 .disabled(true)
899 .disabled_tooltip("The original road width is an estimate, so any changes might not require major construction.")
900 .build_widget(ctx, "changes to total width")
901 .align_right();
902 Widget::col(vec![line1, line2])
903 };
904
905 let road_settings = Widget::row(vec![
906 total_width,
907 Line("Speed limit")
908 .secondary()
909 .into_widget(ctx)
910 .centered_vert(),
911 Widget::dropdown(
912 ctx,
913 "speed limit",
914 road.speed_limit,
915 speed_limit_choices(app, Some(road.speed_limit)),
916 )
917 .centered_vert(),
918 ctx.style()
919 .btn_outline
920 .text("Access restrictions")
921 .build_def(ctx)
922 .centered_vert(),
923 ]);
924
925 Panel::new_builder(
926 Widget::custom_col(vec![
927 Widget::col(vec![
928 road_settings,
929 Widget::horiz_separator(ctx, 1.0),
930 add_lane_row,
931 ])
932 .section(ctx)
933 .margin_below(16),
934 drag_drop
935 .into_widget(ctx)
936 .bg(ctx.style().text_primary_color.tint(0.3))
937 .margin_left(16),
938 modify_lane.padding(16.0).bg(selected_lane_bg(ctx)),
940 ])
941 .padding_left(16),
942 )
943 .aligned(HorizontalAlignment::Left, VerticalAlignment::Center)
944 .ignore_initial_events()
948 .build_custom(ctx)
949}
950
951fn selected_lane_bg(ctx: &EventCtx) -> Color {
952 ctx.style().btn_tab.bg_disabled
953}
954
955fn build_lane_highlights(
956 ctx: &EventCtx,
957 app: &App,
958 selected_lane: Option<LaneID>,
959 hovered_lane: Option<LaneID>,
960) -> ((Option<LaneID>, Option<LaneID>), Drawable) {
961 let mut batch = GeomBatch::new();
962 let map = &app.primary.map;
963
964 let selected_color = selected_lane_bg(ctx);
965 let hovered_color = app.cs.selected;
966
967 if let Some(hovered_lane) = hovered_lane {
968 batch.push(
969 hovered_color,
970 app.primary.draw_map.get_l(hovered_lane).get_outline(map),
971 );
972 }
973
974 if let Some(selected_lane) = selected_lane {
975 batch.push(
976 selected_color,
977 app.primary.draw_map.get_l(selected_lane).get_outline(map),
978 );
979 }
980
981 ((selected_lane, hovered_lane), ctx.upload(batch))
982}
983
984fn lane_type_to_icon(lt: LaneType) -> Option<&'static str> {
985 match lt {
986 LaneType::Driving => Some("system/assets/edit/driving.svg"),
987 LaneType::Parking => Some("system/assets/edit/parking.svg"),
988 LaneType::Sidewalk | LaneType::Shoulder => Some("system/assets/edit/sidewalk.svg"),
989 LaneType::Biking => Some("system/assets/edit/bike.svg"),
990 LaneType::Bus => Some("system/assets/edit/bus.svg"),
991 LaneType::SharedLeftTurn => Some("system/assets/map/shared_left_turn.svg"),
992 LaneType::Construction => Some("system/assets/edit/construction.svg"),
993 LaneType::Buffer(BufferType::Stripes | BufferType::Verge) => {
994 Some("system/assets/edit/buffer/stripes.svg")
995 }
996 LaneType::Buffer(BufferType::FlexPosts) => Some("system/assets/edit/buffer/flex_posts.svg"),
997 LaneType::Buffer(BufferType::Planters) => Some("system/assets/edit/buffer/planters.svg"),
998 LaneType::Buffer(BufferType::JerseyBarrier) => {
999 Some("system/assets/edit/buffer/jersey_barrier.svg")
1000 }
1001 LaneType::Buffer(BufferType::Curb) => Some("system/assets/edit/buffer/curb.svg"),
1002 LaneType::LightRail | LaneType::Footway | LaneType::SharedUse => None,
1004 }
1005}
1006
1007fn width_choices(app: &App, l: LaneID) -> Vec<Choice<Distance>> {
1008 let lane = app.primary.map.get_l(l);
1009 let mut choices = LaneSpec::typical_lane_widths(
1010 lane.lane_type,
1011 app.primary
1012 .map
1013 .get_r(lane.id.road)
1014 .osm_tags
1015 .get(osm::HIGHWAY)
1016 .unwrap(),
1017 );
1018 if !choices.iter().any(|(x, _)| *x == lane.width) {
1019 choices.push((lane.width, "custom"));
1020 }
1021 choices.sort();
1022 choices
1023 .into_iter()
1024 .map(|(x, label)| Choice::new(format!("{} - {}", x.to_string(&app.opts.units), label), x))
1025 .collect()
1026}
1027
1028fn can_reverse(_: LaneType) -> bool {
1031 true
1032}
1033fn fade_irrelevant(app: &App, r: RoadID) -> GeomBatch {
1038 let map = &app.primary.map;
1039 let road = map.get_r(r);
1040 let mut holes = vec![road.get_thick_polygon()];
1041 for i in [road.src_i, road.dst_i] {
1042 let i = map.get_i(i);
1043 holes.push(i.polygon.clone());
1044 }
1045
1046 match Polygon::convex_hull(holes) {
1048 Ok(hole) => {
1049 let fade_area = Polygon::with_holes(
1050 map.get_boundary_polygon().get_outer_ring().clone(),
1051 vec![hole.into_outer_ring()],
1052 );
1053 GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)])
1054 }
1055 Err(_) => {
1056 GeomBatch::new()
1058 }
1059 }
1060}
1061
1062fn draw_drop_position(app: &App, r: RoadID, from: usize, to: usize) -> GeomBatch {
1063 let mut batch = GeomBatch::new();
1064 if from == to {
1065 return batch;
1066 }
1067 let map = &app.primary.map;
1068 let road = map.get_r(r);
1069 let take_num = if from < to { to + 1 } else { to };
1070 let width = road.lanes.iter().take(take_num).map(|x| x.width).sum();
1071 if let Ok(pl) = road.shift_from_left_side(width) {
1072 batch.push(app.cs.selected, pl.make_polygons(OUTLINE_THICKNESS));
1073 }
1074 batch
1075}