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 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 Choice::new("diagonal stripes", Some(BufferType::Stripes)),
67 Choice::new("flex posts", Some(BufferType::FlexPosts)),
68 Choice::new("planters", Some(BufferType::Planters)),
69 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 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 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 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 let mode = GameplayMode::Freeform(app.primary.map.get_name().clone());
158
159 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 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 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 let mut total = Distance::ZERO;
254 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}