1use abstutil::prettyprint_usize;
2use map_gui::tools::{MinimapControls, Navigator};
3use widgetry::{
4 ControlState, EventCtx, GfxCtx, HorizontalAlignment, Image, Key, Line, Panel, ScreenDims, Text,
5 VerticalAlignment, Widget,
6};
7
8use crate::app::App;
9use crate::app::Transition;
10use crate::common::Warping;
11use crate::layer::PickLayer;
12
13pub struct MinimapController;
14
15impl MinimapControls<App> for MinimapController {
16 fn has_zorder(&self, app: &App) -> bool {
17 app.opts.dev
18 }
19 fn has_layer(&self, app: &App) -> bool {
20 app.primary.layer.is_some()
21 }
22
23 fn draw_extra(&self, g: &mut GfxCtx, app: &App) {
24 if let Some(ref l) = app.primary.layer {
25 l.draw_minimap(g);
26 }
27
28 let mut cache = app.primary.agents.borrow_mut();
29 cache.draw_unzoomed_agents(g, &app.primary.map, &app.primary.sim, &app.cs, &app.opts);
30 }
31
32 fn make_unzoomed_panel(&self, ctx: &mut EventCtx, app: &App) -> Panel {
33 let unzoomed_agents = &app.primary.agents.borrow().unzoomed_agents;
34 let is_enabled = [
35 unzoomed_agents.cars(),
36 unzoomed_agents.bikes(),
37 unzoomed_agents.buses_and_trains(),
38 unzoomed_agents.peds(),
39 ];
40 Panel::new_builder(Widget::row(vec![
41 make_tool_panel(ctx, app).align_right(),
42 Widget::col(make_agent_toggles(ctx, app, is_enabled))
43 .bg(app.cs.panel_bg)
44 .padding(16),
45 ]))
46 .aligned(
47 HorizontalAlignment::Right,
48 VerticalAlignment::BottomAboveOSD,
49 )
50 .build_custom(ctx)
51 }
52 fn make_legend(&self, ctx: &mut EventCtx, app: &App) -> Widget {
53 let unzoomed_agents = &app.primary.agents.borrow().unzoomed_agents;
54 let is_enabled = [
55 unzoomed_agents.cars(),
56 unzoomed_agents.bikes(),
57 unzoomed_agents.buses_and_trains(),
58 unzoomed_agents.peds(),
59 ];
60
61 Widget::custom_row(make_agent_toggles(ctx, app, is_enabled))
62 .margin_left(26)
64 }
65
66 fn make_zoomed_side_panel(&self, ctx: &mut EventCtx, app: &App) -> Widget {
67 make_tool_panel(ctx, app)
68 }
69
70 fn panel_clicked(&self, ctx: &mut EventCtx, app: &mut App, action: &str) -> Option<Transition> {
71 match action {
72 "search" => {
73 return Some(Transition::Push(Navigator::new_state(ctx, app)));
74 }
75 "zoom out fully" => {
76 return Some(Transition::Push(Warping::new_state(
77 ctx,
78 app.primary.map.get_bounds().get_rectangle().center(),
79 Some(ctx.canvas.min_zoom()),
80 None,
81 &mut app.primary,
82 )));
83 }
84 "zoom in fully" => {
85 return Some(Transition::Push(Warping::new_state(
86 ctx,
87 ctx.canvas.center_to_map_pt(),
88 Some(10.0),
89 None,
90 &mut app.primary,
91 )));
92 }
93 "change layers" => {
94 return Some(Transition::Push(PickLayer::pick(ctx, app)));
95 }
96 "more data" => Some(Transition::Push(app.session.dash_tab.launch(ctx, app))),
97 _ => unreachable!(),
98 }
99 }
100 fn panel_changed(&self, _: &mut EventCtx, app: &mut App, panel: &Panel) {
101 if panel.has_widget("Car") {
102 app.primary
103 .agents
104 .borrow_mut()
105 .unzoomed_agents
106 .update(panel);
107 }
108 }
109}
110
111fn make_agent_toggles(ctx: &mut EventCtx, app: &App, is_enabled: [bool; 4]) -> Vec<Widget> {
114 use widgetry::{include_labeled_bytes, Color, GeomBatchStack, RewriteColor, Toggle};
115 let [is_car_enabled, is_bike_enabled, is_bus_enabled, is_pedestrian_enabled] = is_enabled;
116
117 pub fn colored_checkbox(
118 ctx: &EventCtx,
119 action: &str,
120 is_enabled: bool,
121 color: Color,
122 icon: &str,
123 label: &str,
124 tooltip: Text,
125 ) -> Widget {
126 let buttons = ctx
127 .style()
128 .btn_plain
129 .btn()
130 .label_text(label)
131 .padding(4.0)
132 .tooltip(tooltip)
133 .image_color(RewriteColor::NoOp, ControlState::Default);
134
135 let icon_batch = Image::from_path(icon)
136 .build_batch(ctx)
137 .expect("invalid svg")
138 .0;
139 let false_btn = {
140 let checkbox = Image::from_bytes(include_labeled_bytes!(
141 "../../../../widgetry/icons/checkbox_no_border_unchecked.svg"
142 ))
143 .color(RewriteColor::Change(Color::BLACK, color.alpha(0.3)));
144 let mut row = GeomBatchStack::horizontal(vec![
145 checkbox.build_batch(ctx).expect("invalid svg").0,
146 icon_batch.clone(),
147 ]);
148 row.set_spacing(8.0);
149
150 let row_batch = row.batch();
151 let bounds = row_batch.get_bounds();
152 buttons.clone().image_batch(row_batch, bounds)
153 };
154
155 let true_btn = {
159 let checkbox = Image::from_bytes(include_labeled_bytes!(
160 "../../../../widgetry/icons/checkbox_no_border_checked.svg"
161 ))
162 .color(RewriteColor::Change(Color::BLACK, color));
163
164 let mut row = GeomBatchStack::horizontal(vec![
165 checkbox.build_batch(ctx).expect("invalid svg").0,
166 icon_batch,
167 ]);
168 row.set_spacing(8.0);
169
170 let row_batch = row.batch();
171 let bounds = row_batch.get_bounds();
172 buttons.image_batch(row_batch, bounds)
173 };
174
175 Toggle::new_widget(
176 is_enabled,
177 false_btn.build(ctx, action),
178 true_btn.build(ctx, action),
179 )
180 .named(action)
181 .container()
182 .force_width(137.0)
184 }
185
186 let counts = app.primary.sim.num_commuters_vehicles();
187
188 let pedestrian_details = {
189 let tooltip = Text::from_multiline(vec![
190 Line("Pedestrians"),
191 Line(format!(
192 "Walking commuters: {}",
193 prettyprint_usize(counts.walking_commuters)
194 ))
195 .secondary(),
196 Line(format!(
197 "To/from public transit: {}",
198 prettyprint_usize(counts.walking_to_from_transit)
199 ))
200 .secondary(),
201 Line(format!(
202 "To/from a car: {}",
203 prettyprint_usize(counts.walking_to_from_car)
204 ))
205 .secondary(),
206 Line(format!(
207 "To/from a bike: {}",
208 prettyprint_usize(counts.walking_to_from_bike)
209 ))
210 .secondary(),
211 ]);
212
213 let count = prettyprint_usize(
214 counts.walking_commuters
215 + counts.walking_to_from_transit
216 + counts.walking_to_from_car
217 + counts.walking_to_from_bike,
218 );
219
220 colored_checkbox(
221 ctx,
222 "Walk",
223 is_pedestrian_enabled,
224 app.cs.unzoomed_pedestrian,
225 "system/assets/meters/pedestrian.svg",
226 &count,
227 tooltip,
228 )
229 };
230
231 let bike_details = {
232 let tooltip = Text::from_multiline(vec![
233 Line("Cyclists"),
234 Line(prettyprint_usize(counts.cyclists)).secondary(),
235 ]);
236
237 colored_checkbox(
238 ctx,
239 "Bike",
240 is_bike_enabled,
241 app.cs.unzoomed_bike,
242 "system/assets/meters/bike.svg",
243 &prettyprint_usize(counts.cyclists),
244 tooltip,
245 )
246 };
247
248 let car_details = {
249 let tooltip = Text::from_multiline(vec![
250 Line("Cars"),
251 Line(format!(
252 "Single-occupancy vehicles: {}",
253 prettyprint_usize(counts.sov_drivers)
254 ))
255 .secondary(),
256 ]);
257 colored_checkbox(
258 ctx,
259 "Car",
260 is_car_enabled,
261 app.cs.unzoomed_car,
262 "system/assets/meters/car.svg",
263 &prettyprint_usize(counts.sov_drivers),
264 tooltip,
265 )
266 };
267
268 let bus_details = {
269 let tooltip = Text::from_multiline(vec![
270 Line("Public transit"),
271 Line(format!(
272 "{} passengers on {} buses",
273 prettyprint_usize(counts.bus_riders),
274 prettyprint_usize(counts.buses)
275 ))
276 .secondary(),
277 Line(format!(
278 "{} passengers on {} trains",
279 prettyprint_usize(counts.train_riders),
280 prettyprint_usize(counts.trains)
281 ))
282 .secondary(),
283 ]);
284
285 colored_checkbox(
286 ctx,
287 "Bus",
288 is_bus_enabled,
289 app.cs.unzoomed_bus,
290 "system/assets/meters/bus.svg",
291 &prettyprint_usize(counts.bus_riders + counts.train_riders),
292 tooltip,
293 )
294 };
295
296 vec![car_details, bike_details, bus_details, pedestrian_details]
297}
298
299fn make_tool_panel(ctx: &mut EventCtx, app: &App) -> Widget {
300 let buttons = ctx
301 .style()
302 .btn_floating
303 .btn()
304 .image_dims(ScreenDims::square(20.0))
305 .bg_color(app.cs.inner_panel_bg, ControlState::Default)
308 .padding(8);
309
310 Widget::col(vec![
311 (if ctx.canvas.is_zoomed() {
312 buttons
313 .clone()
314 .image_path("system/assets/minimap/zoom_out_fully.svg")
315 .build_widget(ctx, "zoom out fully")
316 } else {
317 buttons
318 .clone()
319 .image_path("system/assets/minimap/zoom_in_fully.svg")
320 .build_widget(ctx, "zoom in fully")
321 }),
322 buttons
323 .clone()
324 .image_path("system/assets/tools/layers.svg")
325 .hotkey(Key::L)
326 .build_widget(ctx, "change layers"),
327 buttons
328 .clone()
329 .image_path("system/assets/tools/search.svg")
330 .hotkey(Key::K)
331 .build_widget(ctx, "search"),
332 buttons
333 .image_path("system/assets/meters/trip_histogram.svg")
334 .hotkey(Key::Q)
335 .build_widget(ctx, "more data"),
336 ])
337}