fifteen_min/
from_amenity.rs

1use abstutil::prettyprint_usize;
2use map_gui::tools::draw_isochrone;
3use map_gui::ID;
4use map_model::AmenityType;
5use widgetry::tools::{ChooseSomething, ColorLegend, URLManager};
6use widgetry::{
7    Cached, Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, State,
8    Text, Transition, Widget,
9};
10
11use crate::common::{HoverKey, HoverOnBuilding};
12use crate::isochrone::{BorderIsochrone, Isochrone, Options};
13use crate::{common, render, App};
14
15// It could be useful in the future, but it's kind of noisy right now
16const SHOW_BORDER_ISOCHRONE: bool = false;
17
18pub struct FromAmenity {
19    panel: Panel,
20    draw_unwalkable_roads: Drawable,
21
22    amenity_type: AmenityType,
23    draw: Drawable,
24    isochrone: Isochrone,
25    hovering_on_bldg: Cached<HoverKey, HoverOnBuilding>,
26}
27
28impl FromAmenity {
29    pub fn random_amenity(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
30        Self::new_state(ctx, app, AmenityType::Cafe)
31    }
32
33    pub fn new_state(
34        ctx: &mut EventCtx,
35        app: &App,
36        amenity_type: AmenityType,
37    ) -> Box<dyn State<App>> {
38        map_gui::tools::update_url_map_name(app);
39
40        let draw_unwalkable_roads = render::draw_unwalkable_roads(ctx, app);
41
42        // For a category, find all matching stores
43        let mut stores = Vec::new();
44        for b in app.map.all_buildings() {
45            if b.has_amenity(amenity_type) {
46                stores.push(b.id);
47            }
48        }
49        let isochrone = Isochrone::new(ctx, app, stores, app.session.clone());
50
51        let mut batch = GeomBatch::new();
52
53        if SHOW_BORDER_ISOCHRONE {
54            // Draw an isochrone showing the map boundary
55            let mut borders = Vec::new();
56            for i in app.map.all_intersections() {
57                if i.is_border() {
58                    borders.push(i.id);
59                }
60            }
61            let border_isochrone = BorderIsochrone::new(ctx, app, borders, app.session.clone());
62
63            batch.append(draw_isochrone(
64                &app.map,
65                &border_isochrone.time_to_reach_building,
66                &border_isochrone.thresholds,
67                &border_isochrone.colors,
68            ));
69        }
70
71        batch.append(draw_isochrone(
72            &app.map,
73            &isochrone.time_to_reach_building,
74            &isochrone.thresholds,
75            &isochrone.colors,
76        ));
77        for &start in &isochrone.start {
78            batch.append(render::draw_star(ctx, app.map.get_b(start)));
79        }
80
81        let panel = build_panel(ctx, app, amenity_type, &isochrone);
82
83        Box::new(Self {
84            panel,
85            draw_unwalkable_roads,
86
87            amenity_type,
88            draw: ctx.upload(batch),
89            isochrone,
90            hovering_on_bldg: Cached::new(),
91        })
92    }
93}
94
95impl State<App> for FromAmenity {
96    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
97        // Allow panning and zooming
98        if ctx.canvas_movement() {
99            URLManager::update_url_cam(ctx, app.map.get_gps_bounds());
100        }
101
102        if ctx.redo_mouseover() {
103            let isochrone = &self.isochrone;
104            self.hovering_on_bldg
105                .update(HoverOnBuilding::key(ctx, app), |key| {
106                    HoverOnBuilding::value(ctx, app, key, isochrone)
107                });
108            // Also update this to conveniently get an outline drawn. Note we don't want to do this
109            // inside the callback above, because it doesn't run when the key becomes None.
110            app.current_selection = self.hovering_on_bldg.key().map(|(b, _)| ID::Building(b));
111        }
112
113        match self.panel.event(ctx) {
114            Outcome::Clicked(x) => {
115                if x == "explore matching amenities" {
116                    return Transition::Push(
117                        crate::amenities_details::ExploreAmenitiesDetails::new_state(
118                            ctx,
119                            app,
120                            &self.isochrone,
121                            self.amenity_type,
122                        ),
123                    );
124                } else if x == "change amenity type" {
125                    return Transition::Push(ChooseSomething::new_state(
126                        ctx,
127                        "Search from all amenities of what type?",
128                        app.map
129                            .get_available_amenity_types()
130                            .into_iter()
131                            .map(|at| Choice::new(at.to_string(), at))
132                            .collect(),
133                        Box::new(move |choice, ctx, app| {
134                            Transition::Multi(vec![
135                                Transition::Pop,
136                                Transition::Replace(Self::new_state(ctx, app, choice)),
137                            ])
138                        }),
139                    ));
140                }
141
142                return common::on_click(ctx, app, &x);
143            }
144            Outcome::Changed(_) => {
145                app.session = Options {
146                    movement: common::options_from_controls(&self.panel),
147                    thresholds: Options::default_thresholds(),
148                };
149                return Transition::Replace(Self::new_state(ctx, app, self.amenity_type));
150            }
151            _ => {}
152        }
153
154        Transition::Keep
155    }
156
157    fn draw(&self, g: &mut GfxCtx, _: &App) {
158        g.redraw(&self.draw);
159        g.redraw(&self.draw_unwalkable_roads);
160        self.panel.draw(g);
161        if let Some(hover) = self.hovering_on_bldg.value() {
162            g.draw_mouse_tooltip(hover.tooltip.clone());
163            g.redraw(&hover.drawn_route);
164        }
165    }
166}
167
168fn build_panel(
169    ctx: &mut EventCtx,
170    app: &App,
171    amenity_type: AmenityType,
172    isochrone: &Isochrone,
173) -> Panel {
174    let contents = vec![
175        Line(format!("What's within 15 minutes of all {}", amenity_type)).into_widget(ctx),
176        Widget::row(vec![
177            Line("Change amenity type:").into_widget(ctx),
178            ctx.style()
179                .btn_outline
180                .text(amenity_type.to_string())
181                .build_widget(ctx, "change amenity type"),
182        ]),
183        ctx.style()
184            .btn_outline
185            .text(format!("{} matching amenities", isochrone.start.len()))
186            .build_widget(ctx, "explore matching amenities"),
187        Text::from_all(vec![
188            Line("Estimated population: ").secondary(),
189            Line(prettyprint_usize(isochrone.population)),
190        ])
191        .into_widget(ctx),
192        Text::from_all(vec![
193            Line("Estimated street parking spots: ").secondary(),
194            Line(prettyprint_usize(isochrone.onstreet_parking_spots)),
195        ])
196        .into_widget(ctx),
197        ColorLegend::categories(
198            ctx,
199            vec![
200                (Color::GREEN, "0 mins"),
201                (Color::ORANGE, "5"),
202                (Color::RED, "10"),
203            ],
204            "15",
205        ),
206        if SHOW_BORDER_ISOCHRONE {
207            ColorLegend::row(
208                ctx,
209                Color::rgb(0, 0, 0).alpha(0.3),
210                "< 15 mins from border (amenity could exist off map)",
211            )
212        } else {
213            Widget::nothing()
214        },
215    ];
216
217    common::build_panel(
218        ctx,
219        app,
220        common::Mode::StartFromAmenity,
221        Widget::col(contents),
222    )
223}