parking_mapper/
mapper.rs

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        // Nicer display
114        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                    // Select all roads part of this way
209                    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        // Fill out the tags.
534        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    // TODO Consider not even filtering by oneway. I keep finding mistakes where people split a
599    // road, but didn't mark one side oneway!
600    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
633// TODO Lots of false positives here... why?
634fn 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}