game/ungap/
quick_sketch.rs

1use crate::ID;
2use geom::Distance;
3use map_model::{BufferType, EditCmd, LaneID, LaneSpec, LaneType, RoadID};
4use widgetry::tools::{PopupMsg, URLManager};
5use widgetry::{
6    lctrl, Choice, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, TextExt, Widget,
7};
8
9use crate::app::{App, Transition};
10use crate::common::{share, RouteSketcher};
11use crate::edit::{apply_map_edits, can_edit_lane, LoadEdits, RoadEditor, SaveEdits};
12use crate::sandbox::gameplay::GameplayMode;
13use crate::ungap::{Layers, Tab, TakeLayers};
14
15pub struct QuickSketch {
16    top_panel: Panel,
17    layers: Layers,
18    route_sketcher: RouteSketcher,
19
20    map_edit_key: usize,
21}
22
23impl TakeLayers for QuickSketch {
24    fn take_layers(self) -> Layers {
25        self.layers
26    }
27}
28
29impl QuickSketch {
30    pub fn new_state(ctx: &mut EventCtx, app: &mut App, layers: Layers) -> Box<dyn State<App>> {
31        let mut qs = QuickSketch {
32            top_panel: Panel::empty(ctx),
33            layers,
34            route_sketcher: RouteSketcher::new(app),
35
36            map_edit_key: usize::MAX,
37        };
38        qs.update_top_panel(ctx, app);
39        Box::new(qs)
40    }
41
42    fn update_top_panel(&mut self, ctx: &mut EventCtx, app: &App) {
43        let mut col = Vec::new();
44        if !self.route_sketcher.is_route_started() {
45            col.push("Zoom in and click a road to edit in detail".text_widget(ctx));
46        }
47        col.push(self.route_sketcher.get_widget_to_describe(ctx));
48
49        if self.route_sketcher.is_route_valid() {
50            // We're usually replacing an existing panel, except the very first time.
51            let default_buffer = if self.top_panel.has_widget("buffer type") {
52                self.top_panel.dropdown_value("buffer type")
53            } else {
54                Some(BufferType::FlexPosts)
55            };
56            col.push(Widget::row(vec![
57                "Protect the new bike lanes?"
58                    .text_widget(ctx)
59                    .centered_vert(),
60                Widget::dropdown(
61                    ctx,
62                    "buffer type",
63                    default_buffer,
64                    vec![
65                        // TODO Width / cost summary?
66                        Choice::new("diagonal stripes", Some(BufferType::Stripes)),
67                        Choice::new("flex posts", Some(BufferType::FlexPosts)),
68                        Choice::new("planters", Some(BufferType::Planters)),
69                        // Omit the others for now
70                        Choice::new("no -- just paint", None),
71                    ],
72                ),
73            ]));
74            col.push(
75                Widget::custom_row(vec![ctx
76                    .style()
77                    .btn_solid_primary
78                    .text("Add bike lanes")
79                    .hotkey(Key::Enter)
80                    .disabled(!self.route_sketcher.is_route_valid())
81                    .build_def(ctx)])
82                .evenly_spaced(),
83            );
84        }
85
86        let proposals = proposal_management(ctx, app).section(ctx);
87        self.top_panel = Tab::AddLanes.make_left_panel(
88            ctx,
89            app,
90            Widget::col(vec![Widget::col(col).section(ctx), proposals]),
91        );
92
93        // Also manage the URL here, since this is called for every edit
94        let map = &app.primary.map;
95        let checksum = map.get_edits().get_checksum(map);
96        if share::UploadedProposals::load().md5sums.contains(&checksum) {
97            URLManager::update_url_param("--edits".to_string(), format!("remote/{}", checksum));
98        } else {
99            URLManager::update_url_param("--edits".to_string(), map.get_edits().edits_name.clone());
100        }
101    }
102}
103
104impl State<App> for QuickSketch {
105    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
106        let key = app.primary.map.get_edits_change_key();
107        if self.map_edit_key != key {
108            self.map_edit_key = key;
109            self.update_top_panel(ctx, app);
110        }
111
112        // Only when zoomed in and not drawing a route, click to edit a road in detail
113        if !self.route_sketcher.is_route_started() && ctx.canvas.is_zoomed() {
114            if ctx.redo_mouseover() {
115                app.primary.current_selection =
116                    match app.mouseover_unzoomed_roads_and_intersections(ctx) {
117                        Some(ID::Road(r)) => Some(r),
118                        Some(ID::Lane(l)) => Some(l.road),
119                        _ => None,
120                    }
121                    .and_then(|r| {
122                        if app.primary.map.get_r(r).is_light_rail() {
123                            None
124                        } else {
125                            Some(ID::Road(r))
126                        }
127                    });
128            }
129            if let Some(ID::Road(r)) = app.primary.current_selection {
130                // If it's light rail, a footway, etc, then the first lane should trigger
131                // can_edit_lane
132                if ctx.normal_left_click() && can_edit_lane(app, LaneID { road: r, offset: 0 }) {
133                    return Transition::Push(RoadEditor::new_state_without_lane(ctx, app, r));
134                }
135            }
136        } else {
137            app.primary.current_selection = None;
138        }
139
140        if let Outcome::Clicked(x) = self.top_panel.event(ctx) {
141            match x.as_ref() {
142                "Add bike lanes" => {
143                    let messages = make_quick_changes(
144                        ctx,
145                        app,
146                        self.route_sketcher.all_roads(app),
147                        self.top_panel.dropdown_value("buffer type"),
148                    );
149                    self.route_sketcher = RouteSketcher::new(app);
150                    self.update_top_panel(ctx, app);
151                    return Transition::Push(PopupMsg::new_state(ctx, "Changes made", messages));
152                }
153                "Open a proposal" => {
154                    // Dummy mode, just to allow all edits
155                    // TODO Actually, should we make one to express that only road edits are
156                    // relevant?
157                    let mode = GameplayMode::Freeform(app.primary.map.get_name().clone());
158
159                    // TODO Do we want to do SaveEdits first if unsaved_edits()? We have
160                    // auto-saving... and after loading an old "untitled proposal", it looks
161                    // unsaved.
162                    return Transition::Push(LoadEdits::new_state(ctx, app, mode));
163                }
164                "Save this proposal" => {
165                    return Transition::Push(SaveEdits::new_state(
166                        ctx,
167                        app,
168                        format!("Save \"{}\" as", app.primary.map.get_edits().edits_name),
169                        false,
170                        Some(Transition::Pop),
171                        Box::new(|_, _| {}),
172                    ));
173                }
174                "Share proposal" => {
175                    return Transition::Push(share::ShareProposal::new_state(ctx, app, "--ungap"));
176                }
177                x => {
178                    // TODO More brittle routing of outcomes.
179                    if self.route_sketcher.on_click(x) {
180                        self.update_top_panel(ctx, app);
181                        return Transition::Keep;
182                    }
183
184                    return Tab::AddLanes
185                        .handle_action::<QuickSketch>(ctx, app, x)
186                        .unwrap();
187                }
188            }
189        }
190
191        if self.route_sketcher.event(ctx, app) {
192            self.update_top_panel(ctx, app);
193        }
194
195        if let Some(t) = self.layers.event(ctx, app) {
196            return t;
197        }
198
199        Transition::Keep
200    }
201
202    fn draw(&self, g: &mut GfxCtx, app: &App) {
203        self.top_panel.draw(g);
204        self.layers.draw(g, app);
205        self.route_sketcher.draw(g);
206    }
207}
208
209fn make_quick_changes(
210    ctx: &mut EventCtx,
211    app: &mut App,
212    roads: Vec<RoadID>,
213    buffer_type: Option<BufferType>,
214) -> Vec<String> {
215    // TODO Erasing changes
216
217    let mut edits = app.primary.map.get_edits().clone();
218    let mut changed = 0;
219    let mut unchanged = 0;
220    for r in roads {
221        let old = app.primary.map.get_r_edit(r);
222        let mut new = old.clone();
223        LaneSpec::maybe_add_bike_lanes(
224            &mut new.lanes_ltr,
225            buffer_type,
226            app.primary.map.get_config().driving_side,
227        );
228        if old == new {
229            unchanged += 1;
230        } else {
231            changed += 1;
232            edits.commands.push(EditCmd::ChangeRoad { r, old, new });
233        }
234    }
235    apply_map_edits(ctx, app, edits);
236
237    let mut messages = Vec::new();
238    if changed > 0 {
239        messages.push(format!("Added bike lanes to {} segments", changed));
240    }
241    if unchanged > 0 {
242        messages.push(format!("Didn't modify {} segments -- the road isn't wide enough, or there's already a bike lane", unchanged));
243    }
244    messages
245}
246
247fn proposal_management(ctx: &mut EventCtx, app: &App) -> Widget {
248    let mut col = Vec::new();
249    let edits = app.primary.map.get_edits();
250
251    let total_mileage = {
252        // Look for the new lanes...
253        let mut total = Distance::ZERO;
254        // TODO We're assuming the edits have been compressed.
255        for cmd in &edits.commands {
256            if let EditCmd::ChangeRoad { r, old, new } = cmd {
257                let num_before = old
258                    .lanes_ltr
259                    .iter()
260                    .filter(|spec| spec.lt == LaneType::Biking)
261                    .count();
262                let num_after = new
263                    .lanes_ltr
264                    .iter()
265                    .filter(|spec| spec.lt == LaneType::Biking)
266                    .count();
267                if num_before != num_after {
268                    let multiplier = (num_after as f64) - (num_before) as f64;
269                    total += multiplier * app.primary.map.get_r(*r).length();
270                }
271            }
272        }
273        total
274    };
275    if edits.commands.is_empty() {
276        col.push("Today's network".text_widget(ctx));
277    } else {
278        col.push(Line(&edits.edits_name).into_widget(ctx));
279    }
280    col.push(
281        Line(format!(
282            "{:.1} miles of new bike lanes",
283            total_mileage.to_miles()
284        ))
285        .secondary()
286        .into_widget(ctx),
287    );
288    col.push(Widget::row(vec![
289        ctx.style()
290            .btn_outline
291            .text("Open a proposal")
292            .hotkey(lctrl(Key::O))
293            .build_def(ctx),
294        ctx.style()
295            .btn_outline
296            .icon_text("system/assets/tools/save.svg", "Save this proposal")
297            .hotkey(lctrl(Key::S))
298            .disabled(edits.commands.is_empty())
299            .build_def(ctx),
300    ]));
301    col.push(
302        ctx.style()
303            .btn_outline
304            .text("Share proposal")
305            .disabled(edits.commands.is_empty())
306            .build_def(ctx),
307    );
308
309    Widget::col(col)
310}