game/sandbox/
minimap.rs

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            // nudge to left-align with the map edge
63            .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
111/// `is_enabled`: are (car, bike, bus, pedestrian) toggles enabled
112/// returns Widgets for (car, bike, bus, pedestrian)
113fn 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        // For typical checkboxes buttons, the checkbox *is* the image, but for the agent toggles
156        // we need both a checkbox *and* an additional icon. To do that, we combine the checkbox
157        // and icon into a single batch, and use that combined batch as the button's image.
158        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        // avoid horizontal resize jitter as numbers fluctuate
183        .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        // the default transparent button background is jarring for these buttons which are floating
306        // in a transparent panel.
307        .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}