fifteen_min/
amenities_details.rs

1use geom::Duration;
2use map_gui::tools::draw_isochrone;
3use map_model::{AmenityType, BuildingID};
4use widgetry::table::{Col, Filter, Table};
5use widgetry::tools::open_browser;
6use widgetry::{
7    Color, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State, Text,
8    Transition, VerticalAlignment, Widget,
9};
10
11use crate::isochrone::Isochrone;
12use crate::{render, App};
13
14pub struct ExploreAmenitiesDetails {
15    table: Table<App, Entry, ()>,
16    panel: Panel,
17    draw: Drawable,
18}
19
20struct Entry {
21    bldg: BuildingID,
22    amenity_idx: usize,
23    name: String,
24    amenity_type: String,
25    address: String,
26    duration_away: Duration,
27}
28
29impl ExploreAmenitiesDetails {
30    pub fn new_state(
31        ctx: &mut EventCtx,
32        app: &App,
33        isochrone: &Isochrone,
34        category: AmenityType,
35    ) -> Box<dyn State<App>> {
36        let mut batch = draw_isochrone(
37            &app.map,
38            &isochrone.time_to_reach_building,
39            &isochrone.thresholds,
40            &isochrone.colors,
41        );
42        batch.append(render::draw_star(ctx, app.map.get_b(isochrone.start[0])));
43
44        let mut entries = Vec::new();
45        for b in isochrone.amenities_reachable.get(category) {
46            let bldg = app.map.get_b(*b);
47            for (amenity_idx, amenity) in bldg.amenities.iter().enumerate() {
48                if AmenityType::categorize(&amenity.amenity_type) == Some(category) {
49                    entries.push(Entry {
50                        bldg: bldg.id,
51                        amenity_idx,
52                        name: amenity.names.get(app.opts.language.as_ref()).to_string(),
53                        amenity_type: amenity.amenity_type.clone(),
54                        address: bldg.address.clone(),
55                        duration_away: isochrone.time_to_reach_building[&bldg.id],
56                    });
57                    // Highlight the matching buildings
58                    batch.push(Color::RED, bldg.polygon.clone());
59                }
60            }
61        }
62
63        let mut table: Table<App, Entry, ()> = Table::new(
64            "time_to_reach_table",
65            entries,
66            // The label has extra junk to avoid crashing when one building has two stores,
67            // possibly with the same name in the current language
68            Box::new(|x| format!("{}: {} ({})", x.bldg.0, x.name, x.amenity_idx)),
69            "Time to reach",
70            Filter::empty(),
71        );
72        table.column(
73            "Type",
74            Box::new(|ctx, _, x| Text::from(&x.amenity_type).render(ctx)),
75            Col::Sortable(Box::new(|rows| {
76                rows.sort_by_key(|x| x.amenity_type.clone())
77            })),
78        );
79        table.static_col("Name", Box::new(|x| x.name.clone()));
80        table.static_col("Address", Box::new(|x| x.address.clone()));
81        table.column(
82            "Time to reach",
83            Box::new(|ctx, app, x| {
84                Text::from(x.duration_away.to_string(&app.opts.units)).render(ctx)
85            }),
86            Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.duration_away))),
87        );
88
89        let panel = Panel::new_builder(Widget::col(vec![
90            Widget::row(vec![
91                Line(format!("{} within 15 minutes", category))
92                    .small_heading()
93                    .into_widget(ctx),
94                ctx.style().btn_close_widget(ctx),
95            ]),
96            table.render(ctx, app),
97        ]))
98        .aligned(HorizontalAlignment::Center, VerticalAlignment::TopInset)
99        .build(ctx);
100
101        Box::new(Self {
102            table,
103            panel,
104            draw: ctx.upload(batch),
105        })
106    }
107}
108
109impl State<App> for ExploreAmenitiesDetails {
110    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
111        ctx.canvas_movement();
112
113        match self.panel.event(ctx) {
114            Outcome::Clicked(x) => {
115                if self.table.clicked(&x) {
116                    self.table.replace_render(ctx, app, &mut self.panel)
117                } else if x == "close" {
118                    return Transition::Pop;
119                } else if let Some(idx) = x.split(':').next().and_then(|x| x.parse::<usize>().ok())
120                {
121                    let b = app.map.get_b(BuildingID(idx));
122                    open_browser(b.orig_id.to_string());
123                } else {
124                    unreachable!()
125                }
126            }
127            Outcome::Changed(_) => {
128                self.table.panel_changed(&self.panel);
129                self.table.replace_render(ctx, app, &mut self.panel)
130            }
131            _ => {}
132        }
133
134        Transition::Keep
135    }
136
137    fn draw(&self, g: &mut GfxCtx, app: &App) {
138        g.redraw(&self.draw);
139        self.panel.draw(g);
140        if let Some(x) = self
141            .panel
142            .currently_hovering()
143            .and_then(|x| x.split(':').next())
144            .and_then(|x| x.parse::<usize>().ok())
145        {
146            g.draw_polygon(Color::CYAN, app.map.get_b(BuildingID(x)).polygon.clone());
147        }
148    }
149}