game/edit/
roads.rs

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
22// TODO Future bug alert: osm_tags.get(osm::HIGHWAY) is brittle, because it'll break for railways.
23// Plumb through road.highway instead.
24
25pub struct RoadEditor {
26    r: RoadID,
27    selected_lane: Option<LaneID>,
28    // This is only for hovering on a lane in the map, not for hovering on a lane card.
29    hovering_on_lane: Option<LaneID>,
30    top_panel: Panel,
31    main_panel: Panel,
32    fade_irrelevant: Drawable,
33
34    // (cache_key: (selected, hovering), Drawable)
35    lane_highlights: ((Option<LaneID>, Option<LaneID>), Drawable),
36    // This gets updated during dragging, and is always cleared out when drag-and-drop ends.
37    draw_drop_position: Drawable,
38
39    // Undo/redo management
40    num_edit_cmds_originally: usize,
41    redo_stack: Vec<EditCmd>,
42    orig_road_state: EditRoad,
43}
44
45impl RoadEditor {
46    /// Always starts focused on a certain lane.
47    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        // Special check here -- this invalid state can be reached in many ways.
100        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        // Compress all of the edits, unless there were 0 or 1 changes
167        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    // Lane IDs may change with every edit. So immediately after an edit, recalculate mouseover.
186    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                    // Special check here
319                    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                    // The RoadEditor maintains an undo/redo stack for a single road, but the
358                    // ZoneEditor usually operates on multiple roads. So before we switch over to
359                    // it, compress and save the current edits.
360                    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                    // Keep selecting the same lane, if one was selected
385                    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                    // hovering index changed
405                    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            // Even if we drag the lane card into map-space, don't hover on anything in the map.
469            self.hovering_on_lane = None;
470            // Don't rebuild the panel -- that'll destroy the DragDrop we just started! But do
471            // update the outlines
472            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                    // Switch to editing another road, first compressing the edits here if
487                    // needed.
488                    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            // Deselect the current lane
499            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                // TODO ctrl+shift+Z!
564                .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    // All the buffer lanes are grouped into a PersistentSplit
604    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                        // When we're modifying an existing lane, hotkeys change the lane, not add
656                        // new lanes.
657                        .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                    // TODO: For selected/hover, rather than change the entire card's background, let's
726                    // just add an outline to match the styling of the corresponding lane in the map
727                    .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(&lt)
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                                // If the selected lane is already this type, we can't change it. Hopefully no need to
787                                // explain this.
788                                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                                // Max 2 parking lanes per road.
797                                //
798                                // (I've seen cases in Ballard with angled parking in a median and also parking on both
799                                // shoulders. If this happens to be mapped as two adjacent one-way roads, it could
800                                // work. But the simulation layer doesn't understand 3 lanes on one road.)
801                                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                                // Max 2 sidewalks or shoulders per road.
808                                //
809                                // (You could imagine some exceptions in reality, but this assumption of max 2 is
810                                // deeply baked into the map model and everything on top of it.)
811                                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                        // Even if the user's settings are set to feet, our step size is in meters, so
856                        // just render in meters.
857                        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            // We use a sort of "tab" metaphor for the selected lane above and this "edit" section
939            modify_lane.padding(16.0).bg(selected_lane_bg(ctx)),
940        ])
941        .padding_left(16),
942    )
943    .aligned(HorizontalAlignment::Left, VerticalAlignment::Center)
944    // If we're hovering on a lane card, we'll immediately produce Outcome::Changed. Since this
945    // usually happens in recalc_all_panels, that's fine -- we'll look up the current lane card
946    // there anyway.
947    .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        // Don't allow creating these yet
1003        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
1028// TODO We need to automatically fix the direction of sidewalks and parking as we initially place
1029// them or shift them around. Until then, allow fixing in the UI manually.
1030fn can_reverse(_: LaneType) -> bool {
1031    true
1032}
1033/*fn can_reverse(lt: LaneType) -> bool {
1034    lt == LaneType::Driving || lt == LaneType::Biking || lt == LaneType::Bus
1035}*/
1036
1037fn 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    // The convex hull illuminates a bit more of the surrounding area, looks better
1047    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            // Just give up and don't fade anything
1057            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}