1use std::str::FromStr;
2
3use abstutil::MultiMap;
4use geom::Distance;
5use map_gui::tools::{CityPicker, Navigator};
6use map_gui::ID;
7use map_model::connectivity::WalkingOptions;
8use map_model::{AmenityType, BuildingID};
9use widgetry::tools::{ColorLegend, PopupMsg};
10use widgetry::{
11 lctrl, Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Panel,
12 Text, Toggle, Transition, VerticalAlignment, Widget,
13};
14
15use crate::isochrone::{Isochrone, MovementOptions, Options};
16use crate::App;
17
18pub enum Mode {
19 SingleStart,
20 StartFromAmenity,
21 ScoreHomes,
22}
23
24pub fn build_panel(ctx: &mut EventCtx, app: &App, mode: Mode, contents: Widget) -> Panel {
25 fn current_mode(ctx: &mut EventCtx, name: &str) -> Widget {
26 ctx.style()
27 .btn_solid_primary
28 .text(name)
29 .disabled(true)
30 .build_def(ctx)
31 }
32
33 let rows = vec![
34 map_gui::tools::app_header(ctx, app, "15-minute neighborhood explorer"),
35 Widget::row(vec![
36 ctx.style().btn_outline.text("About").build_def(ctx),
37 ctx.style()
38 .btn_outline
39 .text("Sketch bus route (experimental)")
40 .build_def(ctx),
41 ctx.style()
42 .btn_plain
43 .icon("system/assets/tools/search.svg")
44 .hotkey(lctrl(Key::F))
45 .build_widget(ctx, "search"),
46 ]),
47 Widget::horiz_separator(ctx, 1.0).margin_above(10),
48 Widget::row(vec![
49 if matches!(mode, Mode::SingleStart { .. }) {
50 current_mode(ctx, "Start from a building")
51 } else {
52 ctx.style()
53 .btn_outline
54 .text("Start from a building")
55 .build_def(ctx)
56 },
57 if matches!(mode, Mode::StartFromAmenity { .. }) {
58 current_mode(ctx, "Start from an amenity")
59 } else {
60 ctx.style()
61 .btn_outline
62 .text("Start from an amenity")
63 .build_def(ctx)
64 },
65 if matches!(mode, Mode::ScoreHomes { .. }) {
66 current_mode(ctx, "Score homes by access")
67 } else {
68 ctx.style()
69 .btn_outline
70 .text("Score homes by access")
71 .build_def(ctx)
72 },
73 ]),
74 contents.named("contents"),
75 Widget::horiz_separator(ctx, 1.0).margin_above(10),
76 options_to_controls(ctx, &app.session),
77 ];
78
79 Panel::new_builder(Widget::col(rows))
80 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
81 .build(ctx)
82}
83
84pub fn on_click(ctx: &mut EventCtx, app: &App, x: &str) -> Transition<App> {
85 match x {
86 "Sketch bus route (experimental)" => {
87 return Transition::Push(crate::bus::BusExperiment::new_state(ctx, app));
88 }
89 "Home" => {
90 return Transition::Clear(vec![map_gui::tools::TitleScreen::new_state(
91 ctx,
92 app,
93 map_gui::tools::Executable::FifteenMin,
94 Box::new(|ctx, app, _| crate::single_start::SingleStart::random_start(ctx, app)),
95 )]);
96 }
97 "change map" => {
98 return Transition::Push(CityPicker::new_state(
99 ctx,
100 app,
101 Box::new(|ctx, app| {
102 Transition::Multi(vec![
103 Transition::Pop,
104 Transition::Replace(crate::single_start::SingleStart::random_start(
105 ctx, app,
106 )),
107 ])
108 }),
109 ));
110 }
111 "About" => {
112 return Transition::Push(PopupMsg::new_state(
113 ctx,
114 "15-minute neighborhood explorer",
115 vec![
116 "What if you could access most of your daily needs with a 15-minute \
117 walk or bike ride from your house?",
118 "Wouldn't it be nice to not rely on a climate unfriendly motor \
119 vehicle and get stuck in traffic for these simple errands?",
120 "Different cities around the world are talking about what design and \
121 policy changes could lead to 15-minute neighborhoods.",
122 "This tool lets you see what commercial amenities are near you right \
123 now, using data from OpenStreetMap.",
124 "",
125 "Note that sidewalks and crosswalks are assumed on most roads.",
126 "Especially around North Seattle, many roads lack sidewalks and \
127 aren't safe for some people to use.",
128 "We're working to improve the accuracy of the map.",
129 ],
130 ));
131 }
132 "search" => {
133 return Transition::Push(Navigator::new_state(ctx, app));
134 }
135 "Start from a building" => {
136 return Transition::Replace(crate::single_start::SingleStart::random_start(ctx, app));
137 }
138 "Start from an amenity" => {
139 return Transition::Replace(crate::from_amenity::FromAmenity::random_amenity(ctx, app));
140 }
141 "Score homes by access" => {
142 return Transition::Push(crate::score_homes::ScoreHomes::new_state(
143 ctx,
144 app,
145 Vec::new(),
146 ));
147 }
148 _ => panic!("Unhandled click {x}"),
149 }
150}
151
152fn options_to_controls(ctx: &mut EventCtx, opts: &Options) -> Widget {
153 let mut rows = vec![Toggle::choice(
154 ctx,
155 "walking / biking",
156 "walking",
157 "biking",
158 None,
159 match opts.movement {
160 MovementOptions::Walking(_) => true,
161 MovementOptions::Biking => false,
162 },
163 )];
164 match opts.movement {
165 MovementOptions::Walking(ref opts) => {
166 rows.push(Toggle::switch(
167 ctx,
168 "Allow walking on the shoulder of the road without a sidewalk",
169 None,
170 opts.allow_shoulders,
171 ));
172 rows.push(Widget::dropdown(
173 ctx,
174 "speed",
175 opts.walking_speed,
176 WalkingOptions::common_speeds()
177 .into_iter()
178 .map(|(label, speed)| Choice::new(label, speed))
179 .collect(),
180 ));
181
182 rows.push(ColorLegend::row(ctx, Color::BLUE, "unwalkable roads"));
183 }
184 MovementOptions::Biking => {}
185 }
186 Widget::col(rows).section(ctx)
187}
188
189pub fn options_from_controls(panel: &Panel) -> MovementOptions {
190 if panel.is_checked("walking / biking") {
191 MovementOptions::Walking(WalkingOptions {
192 allow_shoulders: panel
193 .maybe_is_checked("Allow walking on the shoulder of the road without a sidewalk")
194 .unwrap_or(true),
195 walking_speed: panel
196 .maybe_dropdown_value("speed")
197 .unwrap_or_else(WalkingOptions::default_speed),
198 })
199 } else {
200 MovementOptions::Biking
201 }
202}
203
204pub struct HoverOnBuilding {
205 pub tooltip: Text,
206 pub drawn_route: Drawable,
207}
208pub type HoverKey = (BuildingID, f64);
210
211impl HoverOnBuilding {
212 pub fn key(ctx: &EventCtx, app: &App) -> Option<HoverKey> {
213 match app.mouseover_unzoomed_buildings(ctx) {
214 Some(ID::Building(b)) => {
215 let scale_factor = if ctx.canvas.is_zoomed() { 1.0 } else { 10.0 };
216 Some((b, scale_factor))
217 }
218 _ => None,
219 }
220 }
221
222 pub fn value(
223 ctx: &mut EventCtx,
224 app: &App,
225 key: HoverKey,
226 isochrone: &Isochrone,
227 ) -> HoverOnBuilding {
228 debug!("Calculating route for {:?}", key);
229
230 let (hover_id, scale_factor) = key;
231 let mut batch = GeomBatch::new();
232 if let Some(polyline) = isochrone
233 .path_to(&app.map, hover_id)
234 .and_then(|path| path.trace(&app.map))
235 {
236 let dashed_lines = polyline.dashed_lines(
237 Distance::meters(0.75 * scale_factor),
238 Distance::meters(1.0 * scale_factor),
239 Distance::meters(0.4 * scale_factor),
240 );
241 batch.extend(Color::BLACK, dashed_lines);
242 }
243
244 HoverOnBuilding {
245 tooltip: if let Some(time) = isochrone.time_to_reach_building.get(&hover_id) {
246 Text::from(format!("{} away", time))
247 } else {
248 Text::from("This is more than 15 minutes away")
249 },
250 drawn_route: ctx.upload(batch),
251 }
252 }
253}
254
255pub struct HoverOnCategory {
256 state: Option<(AmenityType, Drawable)>,
258 color: Color,
259}
260
261impl HoverOnCategory {
262 pub fn new(color: Color) -> Self {
263 Self { state: None, color }
264 }
265
266 pub fn update_on_mouse_move(
267 &mut self,
268 ctx: &EventCtx,
269 app: &App,
270 panel: &Panel,
271 amenities_reachable: &MultiMap<AmenityType, BuildingID>,
272 ) {
273 let key = panel
274 .currently_hovering()
275 .and_then(|x| x.strip_prefix("businesses: "));
276 if let Some(category) = key {
277 let category = AmenityType::from_str(category).unwrap();
278 if self
279 .state
280 .as_ref()
281 .map(|(cat, _)| *cat != category)
282 .unwrap_or(true)
283 {
284 let mut batch = GeomBatch::new();
285 for b in amenities_reachable.get(category) {
286 batch.push(self.color, app.map.get_b(*b).polygon.clone());
287 }
288 self.state = Some((category, ctx.upload(batch)));
289 }
290 } else {
291 self.state = None;
292 }
293 }
294
295 pub fn draw(&self, g: &mut GfxCtx) {
296 if let Some((_, ref draw)) = self.state {
297 g.redraw(draw);
298 }
299 }
300}