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
15const 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 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 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 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 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}