1use std::collections::BTreeSet;
2
3use crate::App;
4use abstutil::{prettyprint_usize, Counter, MultiMap, Timer};
5use geom::Percent;
6use map_gui::tools::grey_out_map;
7use map_model::connectivity::Spot;
8use map_model::{AmenityType, BuildingID};
9use widgetry::tools::{ColorLegend, PopupMsg, URLManager};
10use widgetry::{
11 Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel,
12 SimpleState, State, Text, TextExt, Toggle, Transition, Widget,
13};
14
15use crate::isochrone::Options;
16use crate::{common, render};
17
18pub struct ScoreHomes;
21
22impl ScoreHomes {
23 pub fn new_state(
24 ctx: &mut EventCtx,
25 app: &App,
26 amenities: Vec<AmenityType>,
27 ) -> Box<dyn State<App>> {
28 let amenities_present = app.map.get_available_amenity_types();
29 let mut toggles = Vec::new();
30 let mut missing = Vec::new();
31 for at in AmenityType::all() {
32 if amenities_present.contains(&at) {
33 toggles.push(Toggle::switch(
34 ctx,
35 &at.to_string(),
36 None,
37 amenities.contains(&at),
38 ));
39 } else {
40 missing.push(at.to_string());
41 }
42 }
43
44 let panel = Panel::new_builder(Widget::col(vec![
45 Widget::row(vec![Line("Calculate acces scores")
46 .small_heading()
47 .into_widget(ctx)]),
48 "Select the types of businesses you want within a 15 minute walkshed.".text_widget(ctx),
50 Widget::row(vec![
51 ctx.style().btn_outline.text("Enable all").build_def(ctx),
52 ctx.style().btn_outline.text("Disable all").build_def(ctx),
53 ]),
54 Widget::custom_row(toggles).flex_wrap(ctx, Percent::int(50)),
55 ctx.style()
56 .btn_solid_primary
57 .text("Calculate")
58 .hotkey(Key::Enter)
59 .build_def(ctx),
60 Text::from(
61 Line(format!(
62 "These amenities aren't present in this map: {}",
63 missing.join(", ")
64 ))
65 .secondary(),
66 )
67 .wrap_to_pct(ctx, 50)
68 .into_widget(ctx),
69 ]))
70 .build(ctx);
71
72 <dyn SimpleState<_>>::new_state(panel, Box::new(ScoreHomes))
73 }
74}
75
76impl SimpleState<App> for ScoreHomes {
77 fn on_click(
78 &mut self,
79 ctx: &mut EventCtx,
80 app: &mut App,
81 x: &str,
82 panel: &mut Panel,
83 ) -> Transition<App> {
84 match x {
85 "Enable all" => {
86 return Transition::Replace(Self::new_state(
87 ctx,
88 app,
89 app.map.get_available_amenity_types().into_iter().collect(),
90 ));
91 }
92 "Disable all" => {
93 return Transition::Replace(Self::new_state(ctx, app, Vec::new()));
94 }
95 "Calculate" => {
96 let amenities: Vec<AmenityType> = AmenityType::all()
97 .into_iter()
98 .filter(|at| panel.maybe_is_checked(&at.to_string()).unwrap_or(false))
99 .collect();
100 if amenities.is_empty() {
101 return Transition::Push(PopupMsg::new_state(
102 ctx,
103 "No amenities selected",
104 vec!["Please select at least one amenity that you want in your walkshd"],
105 ));
106 }
107
108 return Transition::Multi(vec![
109 Transition::Pop,
110 Transition::Replace(Results::new_state(ctx, app, amenities)),
111 ]);
112 }
113 _ => unreachable!(),
114 }
115 }
116
117 fn draw(&self, g: &mut GfxCtx, app: &App) {
118 grey_out_map(g, app);
119 }
120
121 fn draw_baselayer(&self) -> DrawBaselayer {
122 DrawBaselayer::PreviousState
123 }
124}
125
126fn score_houses_by_one_match(
129 app: &App,
130 amenities: Vec<AmenityType>,
131 timer: &mut Timer,
132) -> (Counter<BuildingID>, MultiMap<AmenityType, BuildingID>) {
133 let mut satisfied_per_bldg: Counter<BuildingID> = Counter::new();
134 let mut amenities_reachable = MultiMap::new();
135
136 let map = &app.map;
137 let movement_opts = &app.session.movement;
138 for (category, stores, times) in
139 timer.parallelize("find houses close to amenities", amenities, |category| {
140 let mut stores = BTreeSet::new();
142 let mut spots = Vec::new();
143 for b in map.all_buildings() {
144 if b.has_amenity(category) {
145 stores.insert(b.id);
146 spots.push(Spot::Building(b.id));
147 }
148 }
149 (
150 category,
151 stores,
152 movement_opts.clone().times_from(map, spots),
153 )
154 })
155 {
156 amenities_reachable.set(category, stores);
157 for (b, _) in times {
158 satisfied_per_bldg.inc(b);
159 }
160 }
161
162 (satisfied_per_bldg, amenities_reachable)
163}
164
165struct Results {
168 panel: Panel,
169 draw_houses: Drawable,
170 amenities: Vec<AmenityType>,
171 amenities_reachable: MultiMap<AmenityType, BuildingID>,
172 draw_unwalkable_roads: Drawable,
173 hovering_on_category: common::HoverOnCategory,
174}
175
176impl Results {
177 fn new_state(
178 ctx: &mut EventCtx,
179 app: &App,
180 amenities: Vec<AmenityType>,
181 ) -> Box<dyn State<App>> {
182 let draw_unwalkable_roads = render::draw_unwalkable_roads(ctx, app);
183
184 assert!(!amenities.is_empty());
185 let (scores, amenities_reachable) = ctx.loading_screen("search for houses", |_, timer| {
186 score_houses_by_one_match(app, amenities.clone(), timer)
187 });
188
189 let mut batch = GeomBatch::new();
190 let mut matches_all = 0;
191
192 for (b, count) in scores.consume() {
193 if count == amenities.len() {
194 matches_all += 1;
195 }
196 let color = app
197 .cs
198 .good_to_bad_red
199 .eval((count as f64) / (amenities.len() as f64));
200 batch.push(color, app.map.get_b(b).polygon.clone());
201 }
202
203 let panel = build_panel(ctx, app, &amenities, &amenities_reachable, matches_all);
204
205 Box::new(Self {
206 draw_unwalkable_roads,
207 panel,
208 draw_houses: ctx.upload(batch),
209 amenities,
210 amenities_reachable,
211 hovering_on_category: common::HoverOnCategory::new(Color::YELLOW),
212 })
213 }
214}
215
216impl State<App> for Results {
217 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
218 if ctx.canvas_movement() {
220 URLManager::update_url_cam(ctx, app.map.get_gps_bounds());
221 }
222
223 if ctx.redo_mouseover() {
224 self.hovering_on_category.update_on_mouse_move(
225 ctx,
226 app,
227 &self.panel,
228 &self.amenities_reachable,
229 );
230 }
231
232 match self.panel.event(ctx) {
233 Outcome::Clicked(x) => {
234 if x == "change scoring criteria" {
235 return Transition::Push(ScoreHomes::new_state(
236 ctx,
237 app,
238 self.amenities.clone(),
239 ));
240 } else if x.starts_with("businesses: ") {
241 return Transition::Keep;
243 }
244 return common::on_click(ctx, app, &x);
245 }
246 Outcome::Changed(_) => {
247 app.session = Options {
248 movement: common::options_from_controls(&self.panel),
249 thresholds: Options::default_thresholds(),
250 };
251 return Transition::Replace(Self::new_state(ctx, app, self.amenities.clone()));
252 }
253 _ => {}
254 }
255
256 Transition::Keep
257 }
258
259 fn draw(&self, g: &mut GfxCtx, _: &App) {
260 g.redraw(&self.draw_unwalkable_roads);
261 g.redraw(&self.draw_houses);
262 self.hovering_on_category.draw(g);
263 self.panel.draw(g);
264 }
265}
266
267fn build_panel(
268 ctx: &mut EventCtx,
269 app: &App,
270 amenities: &Vec<AmenityType>,
271 amenities_reachable: &MultiMap<AmenityType, BuildingID>,
272 matches_all: usize,
273) -> Panel {
274 let contents = vec![
275 "What homes are within 15 minutes away?".text_widget(ctx),
276 "Containing at least 1 of each:".text_widget(ctx),
277 Widget::custom_row(
278 amenities_reachable
279 .borrow()
280 .iter()
281 .map(|(amenity, buildings)| {
282 ctx.style()
283 .btn_outline
284 .text(format!("{}: {}", amenity, buildings.len()))
285 .build_widget(ctx, format!("businesses: {}", amenity))
286 .margin_right(4)
287 .margin_below(4)
288 })
289 .collect(),
290 )
291 .flex_wrap(ctx, Percent::int(30)),
292 format!(
293 "{} houses match all categories",
294 prettyprint_usize(matches_all)
295 )
296 .text_widget(ctx),
297 Line("Darker is better; more categories")
298 .secondary()
299 .into_widget(ctx),
300 ColorLegend::gradient_with_width(
301 ctx,
302 &app.cs.good_to_bad_red,
303 vec!["0", &amenities.len().to_string()],
304 150.0,
305 ),
306 ctx.style()
307 .btn_outline
308 .text("change scoring criteria")
309 .build_def(ctx),
310 ];
311
312 common::build_panel(ctx, app, common::Mode::ScoreHomes, Widget::col(contents))
313}