1use std::collections::{BTreeMap, HashSet};
2
3use anyhow::Result;
4
5use abstutil::{prettyprint_usize, Timer};
6use geom::{Distance, FindClosest, PolyLine, Polygon};
7use map_gui::tools::CityPicker;
8use map_gui::{SimpleApp, ID};
9use map_model::{osm, RoadID};
10use osm::WayID;
11use widgetry::tools::{open_browser, ColorLegend, PopupMsg};
12use widgetry::{
13 Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Menu,
14 Outcome, Panel, State, Text, TextExt, Toggle, Transition, VerticalAlignment, Widget,
15};
16
17type App = SimpleApp<()>;
18
19const FAKE_PARKING_TAG: &str = "abst:parking_source";
20
21pub struct ParkingMapper {
22 panel: Panel,
23 draw_layer: Drawable,
24 show: Show,
25 selected: Option<(HashSet<RoadID>, Drawable)>,
26
27 data: BTreeMap<WayID, Value>,
28}
29
30#[derive(Clone, Copy, PartialEq, Debug)]
31enum Show {
32 ToDo,
33 Done,
34 DividedHighways,
35 UnmappedDividedHighways,
36 OverlappingStuff,
37}
38
39#[derive(PartialEq, Clone)]
40pub enum Value {
41 BothSides,
42 NoStopping,
43 RightOnly,
44 LeftOnly,
45 Complicated,
46}
47
48impl ParkingMapper {
49 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
50 ParkingMapper::make(ctx, app, Show::ToDo, BTreeMap::new())
51 }
52
53 fn make(
54 ctx: &mut EventCtx,
55 app: &App,
56 show: Show,
57 data: BTreeMap<WayID, Value>,
58 ) -> Box<dyn State<App>> {
59 let map = &app.map;
60
61 let color = match show {
62 Show::ToDo => Color::RED,
63 Show::Done => Color::BLUE,
64 Show::DividedHighways | Show::UnmappedDividedHighways => Color::RED,
65 Show::OverlappingStuff => Color::RED,
66 }
67 .alpha(0.5);
68 let mut batch = GeomBatch::new();
69 let mut done = HashSet::new();
70 let mut todo = HashSet::new();
71 for r in map.all_roads() {
72 if r.is_light_rail() {
73 continue;
74 }
75 if r.osm_tags.contains_key(FAKE_PARKING_TAG)
76 && !data.contains_key(&r.orig_id.osm_way_id)
77 {
78 todo.insert(r.orig_id.osm_way_id);
79 if show == Show::ToDo {
80 batch.push(color, map.get_r(r.id).get_thick_polygon());
81 }
82 } else {
83 done.insert(r.orig_id.osm_way_id);
84 if show == Show::Done {
85 batch.push(color, map.get_r(r.id).get_thick_polygon());
86 }
87 }
88 }
89 if show == Show::DividedHighways {
90 for r in find_divided_highways(app) {
91 batch.push(color, map.get_r(r).get_thick_polygon());
92 }
93 }
94 if show == Show::UnmappedDividedHighways {
95 for r in find_divided_highways(app) {
96 let r = map.get_r(r);
97 if !r.osm_tags.is("dual_carriageway", "yes") {
98 batch.push(color, r.get_thick_polygon());
99 }
100 }
101 }
102 if show == Show::OverlappingStuff {
103 ctx.loading_screen(
104 "find buildings and parking lots overlapping roads",
105 |_, timer| {
106 for poly in find_overlapping_stuff(app, timer) {
107 batch.push(color, poly);
108 }
109 },
110 );
111 }
112
113 for i in map.all_intersections() {
115 let is_todo = i.roads.iter().any(|id| {
116 let r = map.get_r(*id);
117 r.osm_tags.contains_key(FAKE_PARKING_TAG)
118 && !data.contains_key(&r.orig_id.osm_way_id)
119 });
120 if matches!((show, is_todo), (Show::ToDo, true) | (Show::Done, false)) {
121 batch.push(color, i.polygon.clone());
122 }
123 }
124
125 Box::new(ParkingMapper {
126 draw_layer: ctx.upload(batch),
127 show,
128 panel: Panel::new_builder(Widget::col(vec![
129 map_gui::tools::app_header(ctx, app, "Parking mapper"),
130 format!(
131 "{} / {} ways done (you've mapped {})",
132 prettyprint_usize(done.len()),
133 prettyprint_usize(done.len() + todo.len()),
134 data.len()
135 )
136 .text_widget(ctx),
137 Widget::row(vec![
138 Widget::dropdown(
139 ctx,
140 "Show",
141 show,
142 vec![
143 Choice::new("missing tags", Show::ToDo),
144 Choice::new("already mapped", Show::Done),
145 Choice::new("divided highways", Show::DividedHighways).tooltip(
146 "Roads divided in OSM often have the wrong number of lanes tagged",
147 ),
148 Choice::new("unmapped divided highways", Show::UnmappedDividedHighways),
149 Choice::new(
150 "buildings and parking lots overlapping roads",
151 Show::OverlappingStuff,
152 )
153 .tooltip("Roads often have the wrong number of lanes tagged"),
154 ],
155 ),
156 ColorLegend::row(
157 ctx,
158 color,
159 match show {
160 Show::ToDo => "TODO",
161 Show::Done => "done",
162 Show::DividedHighways => "divided highways",
163 Show::UnmappedDividedHighways => "unmapped divided highways",
164 Show::OverlappingStuff => {
165 "buildings and parking lots overlapping roads"
166 }
167 },
168 ),
169 ]),
170 Toggle::checkbox(ctx, "max 3 days parking (default in Seattle)", None, false),
171 ctx.style()
172 .btn_outline
173 .text("Generate OsmChange file")
174 .build_def(ctx),
175 "Select a road".text_widget(ctx).named("info"),
176 ]))
177 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
178 .build(ctx),
179 selected: None,
180 data,
181 })
182 }
183}
184
185impl State<App> for ParkingMapper {
186 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
187 let map = &app.map;
188
189 ctx.canvas_movement();
190 if ctx.redo_mouseover() {
191 let mut maybe_r = match app.mouseover_unzoomed_roads_and_intersections(ctx) {
192 Some(ID::Road(r)) => Some(r),
193 Some(ID::Lane(l)) => Some(l.road),
194 _ => None,
195 };
196 if let Some(r) = maybe_r {
197 if map.get_r(r).is_light_rail() {
198 maybe_r = None;
199 }
200 }
201 if let Some(id) = maybe_r {
202 if self
203 .selected
204 .as_ref()
205 .map(|(ids, _)| !ids.contains(&id))
206 .unwrap_or(true)
207 {
208 let road = map.get_r(id);
210 let way = road.orig_id.osm_way_id;
211 let mut ids = HashSet::new();
212 let mut batch = GeomBatch::new();
213 for r in map.all_roads() {
214 if r.orig_id.osm_way_id == way {
215 ids.insert(r.id);
216 batch.push(Color::CYAN.alpha(0.5), r.get_thick_polygon());
217 }
218 }
219
220 self.selected = Some((ids, ctx.upload(batch)));
221
222 let mut txt = Text::new();
223 txt.add_line(format!("Click to map parking for OSM way {}", way));
224 txt.add_appended(vec![
225 Line("Shortcut: press "),
226 Key::N.txt(ctx),
227 Line(" to indicate no parking"),
228 ]);
229 txt.add_appended(vec![
230 Line("Press "),
231 Key::S.txt(ctx),
232 Line(" to open Bing StreetSide here"),
233 ]);
234 txt.add_appended(vec![
235 Line("Press "),
236 Key::E.txt(ctx),
237 Line(" to edit OpenStreetMap for this way"),
238 ]);
239 for (k, v) in road.osm_tags.inner() {
240 if k.starts_with("abst:") {
241 continue;
242 }
243 if k.contains("parking") {
244 if !road.osm_tags.contains_key(FAKE_PARKING_TAG) {
245 txt.add_line(format!("{} = {}", k, v));
246 }
247 } else if k == "sidewalk" {
248 txt.add_line(Line(format!("{} = {}", k, v)).secondary());
249 } else {
250 txt.add_line(Line(format!("{} = {}", k, v)).secondary());
251 }
252 }
253 self.panel.replace(ctx, "info", txt.into_widget(ctx));
254 }
255 } else if self.selected.is_some() {
256 self.selected = None;
257 self.panel
258 .replace(ctx, "info", "Select a road".text_widget(ctx));
259 }
260 }
261 if self.selected.is_some() && ctx.normal_left_click() {
262 return Transition::Push(ChangeWay::new_state(
263 ctx,
264 app,
265 &self.selected.as_ref().unwrap().0,
266 self.show,
267 self.data.clone(),
268 ));
269 }
270 if self.selected.is_some() && ctx.input.pressed(Key::N) {
271 let osm_way_id = map
272 .get_r(*self.selected.as_ref().unwrap().0.iter().next().unwrap())
273 .orig_id
274 .osm_way_id;
275 let mut new_data = self.data.clone();
276 new_data.insert(osm_way_id, Value::NoStopping);
277 return Transition::Replace(ParkingMapper::make(ctx, app, self.show, new_data));
278 }
279 if self.selected.is_some() && ctx.input.pressed(Key::S) {
280 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
281 let gps = pt.to_gps(map.get_gps_bounds());
282 open_browser(format!(
283 "https://www.bing.com/maps?cp={}~{}&style=x",
284 gps.y(),
285 gps.x()
286 ));
287 }
288 }
289 if let Some((ref roads, _)) = self.selected {
290 if ctx.input.pressed(Key::E) {
291 open_browser(format!(
292 "https://www.openstreetmap.org/edit?way={}",
293 map.get_r(*roads.iter().next().unwrap())
294 .orig_id
295 .osm_way_id
296 .0
297 ));
298 }
299 }
300
301 match self.panel.event(ctx) {
302 Outcome::Clicked(x) => match x.as_ref() {
303 "Generate OsmChange file" => {
304 if self.data.is_empty() {
305 return Transition::Push(PopupMsg::new_state(
306 ctx,
307 "No changes yet",
308 vec!["Map some parking first"],
309 ));
310 }
311 return match ctx.loading_screen("generate OsmChange file", |_, timer| {
312 generate_osmc(
313 &self.data,
314 self.panel
315 .is_checked("max 3 days parking (default in Seattle)"),
316 timer,
317 )
318 }) {
319 Ok(()) => Transition::Push(PopupMsg::new_state(
320 ctx,
321 "Diff generated",
322 vec!["diff.osc created. Load it in JOSM, verify, and upload!"],
323 )),
324 Err(err) => Transition::Push(PopupMsg::new_state(
325 ctx,
326 "Error",
327 vec![format!("{}", err)],
328 )),
329 };
330 }
331 "Home" => {
332 return Transition::Clear(vec![map_gui::tools::TitleScreen::new_state(
333 ctx,
334 app,
335 map_gui::tools::Executable::ParkingMapper,
336 Box::new(|ctx, app, _| Self::new_state(ctx, app)),
337 )]);
338 }
339 "change map" => {
340 return Transition::Push(CityPicker::new_state(
341 ctx,
342 app,
343 Box::new(|ctx, app| {
344 Transition::Multi(vec![
345 Transition::Pop,
346 Transition::Replace(ParkingMapper::make(
347 ctx,
348 app,
349 Show::ToDo,
350 BTreeMap::new(),
351 )),
352 ])
353 }),
354 ));
355 }
356 _ => unreachable!(),
357 },
358 Outcome::Changed(_) => {
359 return Transition::Replace(ParkingMapper::make(
360 ctx,
361 app,
362 self.panel.dropdown_value("Show"),
363 self.data.clone(),
364 ));
365 }
366 _ => {}
367 }
368
369 Transition::Keep
370 }
371
372 fn draw(&self, g: &mut GfxCtx, _: &App) {
373 g.redraw(&self.draw_layer);
374 if let Some((_, ref roads)) = self.selected {
375 g.redraw(roads);
376 }
377 self.panel.draw(g);
378 }
379}
380
381struct ChangeWay {
382 panel: Panel,
383 draw: Drawable,
384 osm_way_id: WayID,
385 data: BTreeMap<WayID, Value>,
386 show: Show,
387}
388
389impl ChangeWay {
390 fn new_state(
391 ctx: &mut EventCtx,
392 app: &App,
393 selected: &HashSet<RoadID>,
394 show: Show,
395 data: BTreeMap<WayID, Value>,
396 ) -> Box<dyn State<App>> {
397 let map = &app.map;
398 let osm_way_id = map
399 .get_r(*selected.iter().next().unwrap())
400 .orig_id
401 .osm_way_id;
402
403 let mut batch = GeomBatch::new();
404 let thickness = Distance::meters(2.0);
405 for id in selected {
406 let r = map.get_r(*id);
407 batch.push(
408 Color::GREEN,
409 r.center_pts
410 .must_shift_right(r.get_half_width())
411 .make_polygons(thickness),
412 );
413 batch.push(
414 Color::BLUE,
415 r.center_pts
416 .must_shift_left(r.get_half_width())
417 .make_polygons(thickness),
418 );
419 }
420
421 Box::new(ChangeWay {
422 panel: Panel::new_builder(Widget::col(vec![
423 Widget::row(vec![
424 Line("What kind of parking does this road have?")
425 .small_heading()
426 .into_widget(ctx),
427 ctx.style().btn_close_widget(ctx),
428 ]),
429 Menu::widget(
430 ctx,
431 vec![
432 Choice::new("none -- no stopping or parking", Value::NoStopping),
433 Choice::new("both sides", Value::BothSides),
434 Choice::new("just on the green side", Value::RightOnly),
435 Choice::new("just on the blue side", Value::LeftOnly),
436 Choice::new(
437 "it changes at some point along the road",
438 Value::Complicated,
439 ),
440 Choice::new("loading zone on one or both sides", Value::Complicated),
441 ],
442 )
443 .named("menu"),
444 ]))
445 .build(ctx),
446 draw: ctx.upload(batch),
447 osm_way_id,
448 data,
449 show,
450 })
451 }
452}
453
454impl State<App> for ChangeWay {
455 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
456 ctx.canvas_movement();
457 match self.panel.event(ctx) {
458 Outcome::Clicked(x) => match x.as_ref() {
459 "close" => Transition::Pop,
460 _ => {
461 let value = self.panel.take_menu_choice::<Value>("menu");
462 if value == Value::Complicated {
463 Transition::Replace(PopupMsg::new_state(
464 ctx,
465 "Complicated road",
466 vec![
467 "You'll have to manually split the way in ID or JOSM and apply \
468 the appropriate parking tags to each section.",
469 ],
470 ))
471 } else {
472 self.data.insert(self.osm_way_id, value);
473 Transition::Multi(vec![
474 Transition::Pop,
475 Transition::Replace(ParkingMapper::make(
476 ctx,
477 app,
478 self.show,
479 self.data.clone(),
480 )),
481 ])
482 }
483 }
484 },
485 _ => {
486 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
487 return Transition::Pop;
488 }
489 Transition::Keep
490 }
491 }
492 }
493
494 fn draw(&self, g: &mut GfxCtx, _: &App) {
495 g.redraw(&self.draw);
496 self.panel.draw(g);
497 }
498}
499
500fn generate_osmc(data: &BTreeMap<WayID, Value>, in_seattle: bool, timer: &mut Timer) -> Result<()> {
501 use std::io::Write;
502
503 use fs_err::File;
504
505 use abstutil::Tags;
506
507 let mut modified_ways = Vec::new();
508 timer.start_iter("fetch latest OSM data per modified way", data.len());
509 for (way, value) in data {
510 timer.next();
511 if value == &Value::Complicated {
512 continue;
513 }
514
515 let url = format!("https://api.openstreetmap.org/api/0.6/way/{}", way.0);
516 info!("Fetching {}", url);
517 let resp = reqwest::blocking::get(&url)?.text()?;
518 let mut tree = xmltree::Element::parse(resp.as_bytes())?
519 .take_child("way")
520 .unwrap();
521 let mut osm_tags = Tags::empty();
522 let mut other_children = Vec::new();
523 for node in tree.children.drain(..) {
524 if let Some(elem) = node.as_element() {
525 if elem.name == "tag" {
526 osm_tags.insert(elem.attributes["k"].clone(), elem.attributes["v"].clone());
527 continue;
528 }
529 }
530 other_children.push(node);
531 }
532
533 osm_tags.remove("parking:lane:left");
535 osm_tags.remove("parking:lane:right");
536 osm_tags.remove("parking_lane_both");
537 match value {
538 Value::BothSides => {
539 osm_tags.insert("parking_lane_both", "parallel");
540 if in_seattle {
541 osm_tags.insert("parking:condition:both:maxstay", "3 days");
542 }
543 }
544 Value::NoStopping => {
545 osm_tags.insert("parking_lane_both", "no_stopping");
546 }
547 Value::RightOnly => {
548 osm_tags.insert("parking:lane:right", "parallel");
549 osm_tags.insert("parking:lane:left", "no_stopping");
550 if in_seattle {
551 osm_tags.insert("parking:condition:right:maxstay", "3 days");
552 }
553 }
554 Value::LeftOnly => {
555 osm_tags.insert("parking:lane:left", "parallel");
556 osm_tags.insert("parking:lane:right", "no_stopping");
557 if in_seattle {
558 osm_tags.insert("parking:condition:left:maxstay", "3 days");
559 }
560 }
561 Value::Complicated => unreachable!(),
562 }
563
564 tree.children = other_children;
565 for (k, v) in osm_tags.inner() {
566 let mut new_elem = xmltree::Element::new("tag");
567 new_elem.attributes.insert("k".to_string(), k.to_string());
568 new_elem.attributes.insert("v".to_string(), v.to_string());
569 tree.children.push(xmltree::XMLNode::Element(new_elem));
570 }
571
572 tree.attributes.remove("timestamp");
573 tree.attributes.remove("changeset");
574 tree.attributes.remove("user");
575 tree.attributes.remove("uid");
576 tree.attributes.remove("visible");
577
578 let mut bytes: Vec<u8> = Vec::new();
579 tree.write(&mut bytes)?;
580 let out = String::from_utf8(bytes)?;
581 let stripped = out.trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
582 modified_ways.push(stripped.to_string());
583 }
584
585 let mut f = File::create("diff.osc")?;
586 writeln!(f, "<osmChange version=\"0.6\" generator=\"abst\"><modify>")?;
587 for w in modified_ways {
588 writeln!(f, " {}", w)?;
589 }
590 writeln!(f, "</modify></osmChange>")?;
591 info!("Wrote diff.osc");
592 Ok(())
593}
594
595fn find_divided_highways(app: &App) -> HashSet<RoadID> {
596 let map = &app.map;
597 let mut closest: FindClosest<RoadID> = FindClosest::new();
598 let mut oneways = Vec::new();
601 for r in map.all_roads() {
602 if r.oneway_for_driving().is_some() {
603 closest.add(r.id, r.center_pts.points());
604 oneways.push(r.id);
605 }
606 }
607
608 let mut found = HashSet::new();
609 for r1 in oneways {
610 let r1 = map.get_r(r1);
611 for dist in [Distance::ZERO, r1.length() / 2.0, r1.length()] {
612 let (pt, angle) = r1.center_pts.must_dist_along(dist);
613 for (r2, _, _) in closest.all_close_pts(pt, Distance::meters(250.0)) {
614 if r1.id != r2
615 && PolyLine::must_new(vec![
616 pt.project_away(Distance::meters(100.0), angle.rotate_degs(90.0)),
617 pt.project_away(Distance::meters(100.0), angle.rotate_degs(-90.0)),
618 ])
619 .intersection(&map.get_r(r2).center_pts)
620 .is_some()
621 && r1.get_name(app.opts.language.as_ref())
622 == map.get_r(r2).get_name(app.opts.language.as_ref())
623 {
624 found.insert(r1.id);
625 found.insert(r2);
626 }
627 }
628 }
629 }
630 found
631}
632
633fn find_overlapping_stuff(app: &App, timer: &mut Timer) -> Vec<Polygon> {
635 let map = &app.map;
636 let mut closest: FindClosest<RoadID> = FindClosest::new();
637 for r in map.all_roads() {
638 if r.osm_tags.contains_key("tunnel") {
639 continue;
640 }
641 closest.add(r.id, r.center_pts.points());
642 }
643
644 let mut polygons = Vec::new();
645
646 timer.start_iter("check buildings", map.all_buildings().len());
647 for b in map.all_buildings() {
648 timer.next();
649 for (r, _, _) in closest.all_close_pts(b.label_center, Distance::meters(500.0)) {
650 if !b
651 .polygon
652 .intersection(&map.get_r(r).get_thick_polygon())
653 .map(|list| list.is_empty())
654 .unwrap_or(true)
655 {
656 polygons.push(b.polygon.clone());
657 }
658 }
659 }
660
661 timer.start_iter("check parking lots", map.all_parking_lots().len());
662 for pl in map.all_parking_lots() {
663 timer.next();
664 for (r, _, _) in closest.all_close_pts(pl.polygon.center(), Distance::meters(500.0)) {
665 if !pl
666 .polygon
667 .intersection(&map.get_r(r).get_thick_polygon())
668 .map(|list| list.is_empty())
669 .unwrap_or(true)
670 {
671 polygons.push(pl.polygon.clone());
672 }
673 }
674 }
675
676 polygons
677}