1use std::str::FromStr;
8
9use abstutil::prettyprint_usize;
10use geom::{Distance, FindClosest, Percent};
11use map_gui::ID;
12use map_model::{AmenityType, Building, BuildingID};
13use widgetry::tools::{ColorLegend, URLManager};
14use widgetry::{
15 Cached, Color, Drawable, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, Text, Transition,
16 Widget,
17};
18
19use crate::common::{HoverKey, HoverOnBuilding, HoverOnCategory};
20use crate::isochrone::{Isochrone, Options};
21use crate::{common, render, App};
22
23pub struct SingleStart {
25 panel: Panel,
26 snap_to_buildings: FindClosest<BuildingID>,
27 draw_unwalkable_roads: Drawable,
28
29 highlight_start: Drawable,
30 isochrone: Isochrone,
31 hovering_on_bldg: Cached<HoverKey, HoverOnBuilding>,
32 hovering_on_category: HoverOnCategory,
33}
34
35impl SingleStart {
36 pub fn random_start(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
38 let bldgs = app.map.all_buildings();
39 let start = bldgs[bldgs.len() / 2].id;
40 Self::new_state(ctx, app, start)
41 }
42
43 pub fn new_state(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Box<dyn State<App>> {
44 map_gui::tools::update_url_map_name(app);
45
46 let draw_unwalkable_roads = render::draw_unwalkable_roads(ctx, app);
47
48 let mut snap_to_buildings = FindClosest::new();
49 for b in app.map.all_buildings() {
50 snap_to_buildings.add_polygon(b.id, &b.polygon);
51 }
52
53 let start = app.map.get_b(start);
54 let isochrone = Isochrone::new(ctx, app, vec![start.id], app.session.clone());
55 let highlight_start = render::draw_star(ctx, start);
56 let contents = panel_contents(ctx, start, &isochrone);
57 let panel = common::build_panel(ctx, app, common::Mode::SingleStart, contents);
58
59 Box::new(Self {
60 panel,
61 snap_to_buildings,
62 highlight_start: ctx.upload(highlight_start),
63 isochrone,
64 hovering_on_bldg: Cached::new(),
65 hovering_on_category: HoverOnCategory::new(Color::RED),
66 draw_unwalkable_roads,
67 })
68 }
69
70 fn change_start(&mut self, ctx: &mut EventCtx, app: &App, b: BuildingID) {
71 if self.isochrone.start[0] == b {
72 return;
73 }
74
75 let start = app.map.get_b(b);
76 self.isochrone = Isochrone::new(ctx, app, vec![start.id], app.session.clone());
77 let star = render::draw_star(ctx, start);
78 self.highlight_start = ctx.upload(star);
79 let contents = panel_contents(ctx, start, &self.isochrone);
80 self.panel.replace(ctx, "contents", contents);
81 self.hovering_on_bldg.clear();
84 }
85}
86
87impl State<App> for SingleStart {
88 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
89 if ctx.canvas_movement() {
91 URLManager::update_url_cam(ctx, app.map.get_gps_bounds());
92 }
93
94 if ctx.redo_mouseover() {
95 let isochrone = &self.isochrone;
96 self.hovering_on_bldg
97 .update(HoverOnBuilding::key(ctx, app), |key| {
98 HoverOnBuilding::value(ctx, app, key, isochrone)
99 });
100 app.current_selection = self.hovering_on_bldg.key().map(|(b, _)| ID::Building(b));
103
104 self.hovering_on_category.update_on_mouse_move(
105 ctx,
106 app,
107 &self.panel,
108 &self.isochrone.amenities_reachable,
109 );
110
111 if ctx.is_key_down(Key::LeftControl) {
112 if let Some(cursor) = ctx.canvas.get_cursor_in_map_space() {
113 if let Some((b, _)) = self
114 .snap_to_buildings
115 .closest_pt(cursor, Distance::meters(30.0))
116 {
117 self.change_start(ctx, app, b);
118 }
119 }
120 }
121 }
122
123 if let Some(cursor) = ctx.canvas.get_cursor_in_map_space() {
126 if ctx.normal_left_click() {
127 if let Some((b, _)) = self
128 .snap_to_buildings
129 .closest_pt(cursor, Distance::meters(30.0))
130 {
131 self.change_start(ctx, app, b);
132 }
133 }
134 }
135
136 match self.panel.event(ctx) {
137 Outcome::Clicked(x) => {
138 if let Some(category) = x.strip_prefix("businesses: ") {
139 return Transition::Push(
140 crate::amenities_details::ExploreAmenitiesDetails::new_state(
141 ctx,
142 app,
143 &self.isochrone,
144 AmenityType::from_str(category).unwrap(),
145 ),
146 );
147 } else {
148 return common::on_click(ctx, app, &x);
149 }
150 }
151 Outcome::Changed(_) => {
152 app.session = Options {
153 movement: common::options_from_controls(&self.panel),
154 thresholds: Options::default_thresholds(),
155 };
156 self.draw_unwalkable_roads = render::draw_unwalkable_roads(ctx, app);
157 self.isochrone =
158 Isochrone::new(ctx, app, vec![self.isochrone.start[0]], app.session.clone());
159 let contents =
160 panel_contents(ctx, app.map.get_b(self.isochrone.start[0]), &self.isochrone);
161 self.panel.replace(ctx, "contents", contents);
162 }
163 _ => {}
164 }
165
166 Transition::Keep
167 }
168
169 fn draw(&self, g: &mut GfxCtx, _: &App) {
170 self.isochrone.draw.draw(g);
171 g.redraw(&self.highlight_start);
172 g.redraw(&self.draw_unwalkable_roads);
173 self.panel.draw(g);
174 if let Some(hover) = self.hovering_on_bldg.value() {
175 g.draw_mouse_tooltip(hover.tooltip.clone());
176 g.redraw(&hover.drawn_route);
177 }
178 self.hovering_on_category.draw(g);
179 }
180}
181
182fn panel_contents(ctx: &mut EventCtx, start: &Building, isochrone: &Isochrone) -> Widget {
183 Widget::col(vec![
184 Text::from_all(vec![
185 Line("Click").fg(ctx.style().text_hotkey_color),
186 Line(" a building or hold ").secondary(),
187 Line(Key::LeftControl.describe()).fg(ctx.style().text_hotkey_color),
188 Line(" to change the start point"),
189 ])
190 .into_widget(ctx),
191 Text::from_all(vec![
192 Line("Starting from: ").secondary(),
193 Line(&start.address),
194 ])
195 .into_widget(ctx),
196 Text::from_all(vec![
197 Line("Estimated population: ").secondary(),
198 Line(prettyprint_usize(isochrone.population)),
199 ])
200 .into_widget(ctx),
201 Text::from_all(vec![
202 Line("Estimated street parking spots: ").secondary(),
203 Line(prettyprint_usize(isochrone.onstreet_parking_spots)),
204 ])
205 .into_widget(ctx),
206 ColorLegend::categories(
207 ctx,
208 vec![
209 (Color::GREEN, "0 mins"),
210 (Color::ORANGE, "5"),
211 (Color::RED, "10"),
212 ],
213 "15",
214 ),
215 Widget::custom_row(
216 isochrone
217 .amenities_reachable
218 .borrow()
219 .iter()
220 .map(|(amenity, buildings)| {
221 ctx.style()
222 .btn_outline
223 .text(format!("{}: {}", amenity, buildings.len()))
224 .build_widget(ctx, format!("businesses: {}", amenity))
225 .margin_right(4)
226 .margin_below(4)
227 })
228 .collect(),
229 )
230 .flex_wrap(ctx, Percent::int(30)),
231 ])
232}