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 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 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 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 s.update(ctx, app);
389
390 s
391 }
392
393 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}