osm_viewer/
viewer.rs

1use std::collections::BTreeSet;
2
3use abstutil::{prettyprint_usize, Counter};
4use geom::ArrowCap;
5use map_gui::options::OptionsPanel;
6use map_gui::render::{DrawOptions, BIG_ARROW_THICKNESS};
7use map_gui::tools::{CityPicker, Minimap, MinimapControls, Navigator};
8use map_gui::{SimpleApp, ID};
9use widgetry::tools::{open_browser, PopupMsg, URLManager};
10use widgetry::{
11    lctrl, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
12    Line, Outcome, Panel, State, Text, TextExt, Toggle, Transition, VerticalAlignment, Widget,
13};
14
15type App = SimpleApp<()>;
16
17pub struct Viewer {
18    top_panel: Panel,
19    fixed_object_outline: Option<Drawable>,
20    minimap: Minimap<App, MinimapController>,
21    businesses: Option<BusinessSearch>,
22}
23
24impl Viewer {
25    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
26        map_gui::tools::update_url_map_name(app);
27
28        let mut viewer = Viewer {
29            fixed_object_outline: None,
30            minimap: Minimap::new(ctx, app, MinimapController),
31            businesses: None,
32            top_panel: Panel::empty(ctx),
33        };
34        viewer.recalculate_top_panel(ctx, app, None);
35        Box::new(viewer)
36    }
37
38    // widgetry panels have a bug currently and don't detect changes to the dimensions of contents,
39    // so we can't use replace() without messing up scrollbars.
40    fn recalculate_top_panel(
41        &mut self,
42        ctx: &mut EventCtx,
43        app: &App,
44        biz_search_panel: Option<Widget>,
45    ) {
46        let top_panel = Panel::new_builder(Widget::col(vec![
47            map_gui::tools::app_header(ctx, app, "OpenStreetMap viewer"),
48            Widget::row(vec![
49                ctx.style()
50                    .btn_plain
51                    .icon("system/assets/tools/settings.svg")
52                    .build_widget(ctx, "settings"),
53                ctx.style()
54                    .btn_plain
55                    .icon("system/assets/tools/search.svg")
56                    .hotkey(lctrl(Key::F))
57                    .build_widget(ctx, "search"),
58                ctx.style().btn_plain.text("About").build_def(ctx),
59            ]),
60            Widget::horiz_separator(ctx, 1.0),
61            self.calculate_tags(ctx, app),
62            Widget::horiz_separator(ctx, 1.0),
63            if let Some(ref b) = self.businesses {
64                biz_search_panel.unwrap_or_else(|| b.render(ctx).named("Search for businesses"))
65            } else {
66                ctx.style()
67                    .btn_outline
68                    .text("Search for businesses")
69                    .hotkey(Key::Tab)
70                    .build_def(ctx)
71            },
72        ]))
73        .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
74        .build(ctx);
75        self.top_panel = top_panel;
76    }
77
78    fn calculate_tags(&self, ctx: &EventCtx, app: &App) -> Widget {
79        let mut col = Vec::new();
80        if self.fixed_object_outline.is_some() {
81            col.push("Click something else to examine it".text_widget(ctx));
82        } else {
83            col.push("Click to examine".text_widget(ctx));
84        }
85
86        match app.current_selection {
87            Some(ID::Lane(l)) => {
88                let r = app.map.get_parent(l);
89                col.push(
90                    Widget::row(vec![
91                        ctx.style()
92                            .btn_outline
93                            .text(format!("Open OSM way {}", r.orig_id.osm_way_id.0))
94                            .build_widget(ctx, format!("open {}", r.orig_id.osm_way_id)),
95                        ctx.style().btn_outline.text("Edit OSM way").build_widget(
96                            ctx,
97                            format!(
98                                "open https://www.openstreetmap.org/edit?way={}",
99                                r.orig_id.osm_way_id.0
100                            ),
101                        ),
102                    ])
103                    .evenly_spaced(),
104                );
105
106                let tags = &r.osm_tags;
107                for (k, v) in tags.inner() {
108                    if k.starts_with("abst:") {
109                        continue;
110                    }
111                    if tags.contains_key("abst:parking_source")
112                        && (k == "parking:lane:right"
113                            || k == "parking:lane:left"
114                            || k == "parking:lane:both")
115                    {
116                        continue;
117                    }
118                    col.push(Widget::row(vec![
119                        ctx.style().btn_plain.text(k).build_widget(
120                            ctx,
121                            format!("open https://wiki.openstreetmap.org/wiki/Key:{}", k),
122                        ),
123                        Line(v).into_widget(ctx).align_right(),
124                    ]));
125                }
126            }
127            Some(ID::Intersection(i)) => {
128                let i = app.map.get_i(i);
129                col.push(
130                    ctx.style()
131                        .btn_outline
132                        .text(format!("Open OSM node {}", i.orig_id.0))
133                        .build_widget(ctx, format!("open {}", i.orig_id)),
134                );
135            }
136            Some(ID::Building(b)) => {
137                let b = app.map.get_b(b);
138                col.push(
139                    ctx.style()
140                        .btn_outline
141                        .text(format!("Open OSM ID {}", b.orig_id.inner_id()))
142                        .build_widget(ctx, format!("open {}", b.orig_id)),
143                );
144
145                let mut txt = Text::new();
146                txt.add_line(format!("Address: {}", b.address));
147                if let Some(ref names) = b.name {
148                    txt.add_line(format!(
149                        "Name: {}",
150                        names.get(app.opts.language.as_ref()).to_string()
151                    ));
152                }
153                if !b.amenities.is_empty() {
154                    txt.add_line("");
155                    if b.amenities.len() == 1 {
156                        txt.add_line("1 amenity:");
157                    } else {
158                        txt.add_line(format!("{} amenities:", b.amenities.len()));
159                    }
160                    for a in &b.amenities {
161                        txt.add_line(format!(
162                            "  {} ({})",
163                            a.names.get(app.opts.language.as_ref()),
164                            a.amenity_type
165                        ));
166                    }
167                }
168                col.push(txt.into_widget(ctx));
169
170                if !b.osm_tags.is_empty() {
171                    for (k, v) in b.osm_tags.inner() {
172                        if k.starts_with("abst:") {
173                            continue;
174                        }
175                        col.push(Widget::row(vec![
176                            ctx.style().btn_plain.text(k).build_widget(
177                                ctx,
178                                format!("open https://wiki.openstreetmap.org/wiki/Key:{}", k),
179                            ),
180                            Line(v).into_widget(ctx).align_right(),
181                        ]));
182                    }
183                }
184            }
185            Some(ID::ParkingLot(pl)) => {
186                let pl = app.map.get_pl(pl);
187                col.push(
188                    ctx.style()
189                        .btn_outline
190                        .text(format!("Open OSM ID {}", pl.osm_id.inner_id()))
191                        .build_widget(ctx, format!("open {}", pl.osm_id)),
192                );
193
194                col.push(
195                    format!(
196                        "Estimated parking spots: {}",
197                        prettyprint_usize(pl.capacity())
198                    )
199                    .text_widget(ctx),
200                );
201            }
202            _ => {
203                col = vec!["Zoom in and select something to begin".text_widget(ctx)];
204            }
205        }
206        Widget::col(col)
207    }
208}
209
210impl State<App> for Viewer {
211    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
212        if ctx.canvas_movement() {
213            URLManager::update_url_cam(ctx, app.map.get_gps_bounds());
214        }
215
216        if ctx.redo_mouseover() {
217            let old_id = app.current_selection.clone();
218            app.recalculate_current_selection(ctx);
219
220            if self.fixed_object_outline.is_none() && old_id != app.current_selection {
221                let biz_search = self.top_panel.take("Search for businesses");
222                self.recalculate_top_panel(ctx, app, Some(biz_search));
223            }
224
225            let maybe_amenity = ctx
226                .canvas
227                .get_cursor_in_screen_space()
228                .and_then(|_| self.top_panel.currently_hovering().cloned());
229            if let Some(ref mut b) = self.businesses {
230                b.hovering_on_amenity(ctx, app, maybe_amenity);
231            }
232        }
233
234        if ctx.canvas.get_cursor_in_map_space().is_some() && ctx.normal_left_click() {
235            if let Some(id) = app.current_selection.clone() {
236                // get_obj must succeed, because we can only click static map elements.
237                let outline = app.draw_map.get_obj(id).get_outline(&app.map);
238                let mut batch = GeomBatch::from(vec![(app.cs.perma_selected_object, outline)]);
239
240                if let Some(ID::Lane(l)) = app.current_selection {
241                    for turn in app.map.get_turns_from_lane(l) {
242                        batch.push(
243                            Color::RED,
244                            turn.geom
245                                .make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle),
246                        );
247                    }
248                }
249
250                self.fixed_object_outline = Some(ctx.upload(batch));
251            } else {
252                self.fixed_object_outline = None;
253            }
254            let biz_search = self.top_panel.take("Search for businesses");
255            self.recalculate_top_panel(ctx, app, Some(biz_search));
256        }
257
258        if let Some(t) = self.minimap.event(ctx, app) {
259            return t;
260        }
261
262        match self.top_panel.event(ctx) {
263            Outcome::Clicked(x) => match x.as_ref() {
264                "Home" => {
265                    return Transition::Clear(vec![map_gui::tools::TitleScreen::new_state(
266                        ctx,
267                        app,
268                        map_gui::tools::Executable::OSMViewer,
269                        Box::new(|ctx, app, _| Self::new_state(ctx, app)),
270                    )]);
271                }
272                "change map" => {
273                    return Transition::Push(CityPicker::new_state(
274                        ctx,
275                        app,
276                        Box::new(|ctx, app| {
277                            Transition::Multi(vec![
278                                Transition::Pop,
279                                Transition::Replace(Viewer::new_state(ctx, app)),
280                            ])
281                        }),
282                    ));
283                }
284                "settings" => {
285                    return Transition::Push(OptionsPanel::new_state(ctx, app));
286                }
287                "search" => {
288                    return Transition::Push(Navigator::new_state(ctx, app));
289                }
290                "About" => {
291                    return Transition::Push(PopupMsg::new_state(
292                        ctx,
293                        "About this OSM viewer",
294                        vec![
295                            "If you have an idea about what this viewer should do, get in touch \
296                             at abstreet.org!",
297                            "",
298                            "Note major liberties have been taken with inferring where sidewalks \
299                             and crosswalks exist.",
300                            "Separate footpaths, tram lines, etc are not imported yet.",
301                        ],
302                    ));
303                }
304                "Search for businesses" => {
305                    self.businesses = Some(BusinessSearch::new(ctx, app));
306                    self.recalculate_top_panel(ctx, app, None);
307                }
308                "Hide business search" => {
309                    self.businesses = None;
310                    self.recalculate_top_panel(ctx, app, None);
311                }
312                x => {
313                    if let Some(url) = x.strip_prefix("open ") {
314                        open_browser(url);
315                    } else {
316                        unreachable!()
317                    }
318                }
319            },
320            Outcome::Changed(_) => {
321                let b = self.businesses.as_mut().unwrap();
322                // Update state from checkboxes
323                b.show.clear();
324                for amenity in b.counts.borrow().keys() {
325                    if self.top_panel.is_checked(amenity) {
326                        b.show.insert(amenity.clone());
327                    }
328                }
329                b.update(ctx, app);
330
331                return Transition::KeepWithMouseover;
332            }
333            _ => {}
334        }
335
336        Transition::Keep
337    }
338
339    fn draw_baselayer(&self) -> DrawBaselayer {
340        DrawBaselayer::Custom
341    }
342
343    fn draw(&self, g: &mut GfxCtx, app: &App) {
344        if g.canvas.is_unzoomed() {
345            app.draw_unzoomed(g);
346        } else {
347            app.draw_zoomed(g, DrawOptions::new());
348        }
349
350        self.top_panel.draw(g);
351        self.minimap.draw(g, app);
352        if let Some(ref d) = self.fixed_object_outline {
353            g.redraw(d);
354        }
355        if let Some(ref b) = self.businesses {
356            g.redraw(&b.highlight);
357            if let Some((_, ref d)) = b.hovering_on_amenity {
358                g.redraw(d);
359            }
360        }
361    }
362}
363
364struct BusinessSearch {
365    counts: Counter<String>,
366    show: BTreeSet<String>,
367    highlight: Drawable,
368    hovering_on_amenity: Option<(String, Drawable)>,
369}
370
371impl BusinessSearch {
372    fn new(ctx: &mut EventCtx, app: &App) -> BusinessSearch {
373        let mut counts = Counter::new();
374        for b in app.map.all_buildings() {
375            for a in &b.amenities {
376                counts.inc(a.amenity_type.clone());
377            }
378        }
379        let show = counts.borrow().keys().cloned().collect();
380        let mut s = BusinessSearch {
381            counts,
382            show,
383            highlight: Drawable::empty(ctx),
384            hovering_on_amenity: None,
385        };
386
387        // Initialize highlight
388        s.update(ctx, app);
389
390        s
391    }
392
393    // Updates the highlighted buildings
394    fn update(&mut self, ctx: &mut EventCtx, app: &App) {
395        let mut batch = GeomBatch::new();
396        for b in app.map.all_buildings() {
397            if b.amenities
398                .iter()
399                .any(|a| self.show.contains(&a.amenity_type))
400            {
401                batch.push(Color::RED, b.polygon.clone());
402            }
403        }
404        self.highlight = ctx.upload(batch);
405    }
406
407    fn hovering_on_amenity(&mut self, ctx: &mut EventCtx, app: &App, amenity: Option<String>) {
408        if amenity.is_none() {
409            self.hovering_on_amenity = None;
410            return;
411        }
412
413        let amenity = amenity.unwrap();
414        if self
415            .hovering_on_amenity
416            .as_ref()
417            .map(|(current, _)| current == &amenity)
418            .unwrap_or(false)
419        {
420            return;
421        }
422
423        let mut batch = GeomBatch::new();
424        if self.counts.get(amenity.clone()) > 0 {
425            for b in app.map.all_buildings() {
426                if b.amenities.iter().any(|a| a.amenity_type == amenity) {
427                    batch.push(Color::BLUE, b.polygon.clone());
428                }
429            }
430        }
431        self.hovering_on_amenity = Some((amenity, ctx.upload(batch)));
432    }
433
434    fn render(&self, ctx: &mut EventCtx) -> Widget {
435        let mut col = Vec::new();
436        col.push(
437            ctx.style()
438                .btn_outline
439                .text("Hide business search")
440                .hotkey(Key::Tab)
441                .build_def(ctx),
442        );
443        col.push(
444            format!("{} businesses total", prettyprint_usize(self.counts.sum())).text_widget(ctx),
445        );
446        for (amenity, cnt) in self.counts.borrow() {
447            col.push(Toggle::custom_checkbox(
448                ctx,
449                amenity,
450                vec![Line(format!("{}: {}", amenity, prettyprint_usize(*cnt)))],
451                None,
452                self.show.contains(amenity),
453            ));
454        }
455        Widget::col(col)
456    }
457}
458
459struct MinimapController;
460
461impl MinimapControls<App> for MinimapController {
462    fn has_zorder(&self, _: &App) -> bool {
463        true
464    }
465
466    fn make_legend(&self, _: &mut EventCtx, _: &App) -> Widget {
467        Widget::nothing()
468    }
469}