1use std::marker::PhantomData;
2
3use geom::{Distance, Pt2D, Ring, Time};
4use widgetry::{
5 ControlState, Drawable, EventCtx, Filler, GfxCtx, HorizontalAlignment, Line, Outcome, Panel,
6 ScreenDims, ScreenPt, Spinner, Transition, VerticalAlignment, Widget,
7};
8
9use crate::AppLike;
10
11static MINIMAP_WIDTH: f64 = 400.0;
12static MINIMAP_HEIGHT: f64 = 300.0;
13
14pub struct Minimap<A: AppLike, T: MinimapControls<A>> {
16 controls: T,
17 time: Time,
18 app_type: PhantomData<A>,
19
20 dragging: bool,
21 panel: Panel,
22 zoomed: bool,
24 layer: bool,
25
26 zoom_lvl: usize,
28 base_zoom: f64,
29 zoom: f64,
30 offset_x: f64,
31 offset_y: f64,
32}
33
34pub trait MinimapControls<A: AppLike> {
36 fn has_zorder(&self, app: &A) -> bool;
39 fn has_layer(&self, _: &A) -> bool {
42 false
43 }
44
45 fn draw_extra(&self, _: &mut GfxCtx, _: &A) {}
47
48 fn make_unzoomed_panel(&self, ctx: &mut EventCtx, _: &A) -> Panel {
50 Panel::empty(ctx)
51 }
52 fn make_legend(&self, _: &mut EventCtx, _: &A) -> Widget {
55 Widget::nothing()
56 }
57 fn make_zoomed_side_panel(&self, _: &mut EventCtx, _: &A) -> Widget {
59 Widget::nothing()
60 }
61
62 fn panel_clicked(&self, _: &mut EventCtx, _: &mut A, _: &str) -> Option<Transition<A>> {
64 unreachable!()
65 }
66 fn panel_changed(&self, _: &mut EventCtx, _: &mut A, _: &Panel) {}
68}
69
70impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
71 pub fn new(ctx: &mut EventCtx, app: &A, controls: T) -> Minimap<A, T> {
72 let bounds = app.map().get_bounds();
75 let base_zoom = 0.15 * ctx.canvas.window_width / bounds.width().min(bounds.height());
76 let layer = controls.has_layer(app);
77 let mut m = Minimap {
78 controls,
79 time: Time::START_OF_DAY,
80 app_type: PhantomData,
81
82 dragging: false,
83 panel: Panel::empty(ctx),
84 zoomed: ctx.canvas.is_zoomed(),
85 layer,
86
87 zoom_lvl: 0,
88 base_zoom,
89 zoom: base_zoom,
90 offset_x: 0.0,
91 offset_y: 0.0,
92 };
93 m.recreate_panel(ctx, app);
94 if m.zoomed {
95 m.recenter(ctx, app);
96 }
97 m
98 }
99
100 pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &A) {
101 if ctx.canvas.is_unzoomed() {
102 self.panel = self.controls.make_unzoomed_panel(ctx, app);
103 return;
104 }
105
106 let zoom_col = {
107 let mut col = vec![ctx
108 .style()
109 .btn_plain
110 .icon("system/assets/speed/plus.svg")
111 .build_widget(ctx, "zoom in")
112 .centered_horiz()
113 .margin_below(10)];
114
115 let level_btn = ctx
116 .style()
117 .btn_plain
118 .icon("system/assets/speed/zoom_level_rect.svg")
119 .padding_top(0.0)
120 .padding_bottom(0.0);
121
122 for i in (0..=3).rev() {
123 let level_btn = if self.zoom_lvl < i {
124 level_btn
125 .clone()
126 .image_color(ctx.style().btn_outline.fg_disabled, ControlState::Default)
127 } else {
128 level_btn.clone()
129 };
130 col.push(
131 level_btn
132 .build_widget(ctx, format!("zoom to level {}", i + 1))
133 .centered_horiz()
134 .margin_below(10),
135 );
136 }
137 col.push(
138 ctx.style()
139 .btn_plain
140 .icon("system/assets/speed/minus.svg")
141 .build_widget(ctx, "zoom out")
142 .centered_horiz(),
143 );
144 Widget::custom_col(vec![
149 Widget::custom_col(col)
150 .padding(10)
151 .bg(app.cs().inner_panel_bg),
152 if self.controls.has_zorder(app) {
153 Widget::col(vec![
154 Line("Z-order:").small().into_widget(ctx),
155 Spinner::widget(
156 ctx,
157 "zorder",
158 app.draw_map().zorder_range,
159 app.draw_map().show_zorder,
160 1,
161 ),
162 ])
163 .margin_above(10)
164 } else {
165 Widget::nothing()
166 },
167 ])
168 .margin_above(26)
169 };
170
171 let minimap_widget =
172 Filler::fixed_dims(ScreenDims::new(MINIMAP_WIDTH, MINIMAP_HEIGHT)).named("minimap");
173
174 let minimap_controls = {
175 let buttons = ctx.style().btn_plain.btn().padding(4);
176 Widget::col(vec![
177 buttons
178 .clone()
179 .image_path("system/assets/minimap/up.svg")
180 .build_widget(ctx, "pan up")
181 .centered_horiz(),
182 Widget::row(vec![
183 buttons
184 .clone()
185 .image_path("system/assets/minimap/left.svg")
186 .build_widget(ctx, "pan left")
187 .centered_vert(),
188 minimap_widget,
189 buttons
190 .clone()
191 .image_path("system/assets/minimap/right.svg")
192 .build_widget(ctx, "pan right")
193 .centered_vert(),
194 ]),
195 buttons
196 .clone()
197 .image_path("system/assets/minimap/down.svg")
198 .build_widget(ctx, "pan down")
199 .centered_horiz(),
200 ])
201 };
202
203 let controls = if app.opts().minimal_controls {
204 minimap_controls.padding(16).bg(app.cs().panel_bg)
205 } else {
206 Widget::row(vec![
207 self.controls.make_zoomed_side_panel(ctx, app),
208 Widget::col(vec![
209 Widget::row(vec![minimap_controls, zoom_col]),
210 self.controls.make_legend(ctx, app),
211 ])
212 .padding(16)
213 .bg(app.cs().panel_bg),
214 ])
215 };
216
217 self.panel = Panel::new_builder(controls)
218 .aligned(
219 HorizontalAlignment::Right,
220 VerticalAlignment::BottomAboveOSD,
221 )
222 .build_custom(ctx);
223 }
224
225 fn map_to_minimap_pct(&self, pt: Pt2D) -> (f64, f64) {
226 let inner_rect = self.panel.rect_of("minimap");
227 let pct_x = (pt.x() * self.zoom - self.offset_x) / inner_rect.width();
228 let pct_y = (pt.y() * self.zoom - self.offset_y) / inner_rect.height();
229 (pct_x, pct_y)
230 }
231
232 pub fn set_zoom(&mut self, ctx: &mut EventCtx, app: &A, zoom_lvl: usize) {
233 let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.center_to_map_pt());
235
236 let zoom_speed: f64 = 2.0;
237 self.zoom_lvl = zoom_lvl;
238 self.zoom = self.base_zoom * zoom_speed.powi(self.zoom_lvl as i32);
239 self.recreate_panel(ctx, app);
240
241 let map_center = ctx.canvas.center_to_map_pt();
243 let inner_rect = self.panel.rect_of("minimap");
244 self.offset_x = map_center.x() * self.zoom - pct_x * inner_rect.width();
245 self.offset_y = map_center.y() * self.zoom - pct_y * inner_rect.height();
246 }
247
248 fn recenter(&mut self, ctx: &EventCtx, app: &A) {
249 let map_center = ctx.canvas.center_to_map_pt();
251 let rect = self.panel.rect_of("minimap");
252 let off_x = map_center.x() * self.zoom - rect.width() / 2.0;
253 let off_y = map_center.y() * self.zoom - rect.height() / 2.0;
254
255 let bounds = app.map().get_bounds();
257 let max_x = bounds.max_x * self.zoom - rect.width();
261 let max_y = bounds.max_y * self.zoom - rect.height();
262 if max_x >= 0.0 && max_y >= 0.0 {
263 self.offset_x = off_x.clamp(0.0, max_x);
264 self.offset_y = off_y.clamp(0.0, max_y);
265 }
266 }
267
268 pub fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Option<Transition<A>> {
269 if self.time != app.sim_time() {
270 self.time = app.sim_time();
271 self.recreate_panel(ctx, app);
272 }
273
274 let zoomed = ctx.canvas.is_zoomed();
275 let layer = self.controls.has_layer(app);
276 if zoomed != self.zoomed || layer != self.layer {
277 let just_zoomed_in = zoomed && !self.zoomed;
278
279 self.zoomed = zoomed;
280 self.layer = layer;
281 self.recreate_panel(ctx, app);
282
283 if just_zoomed_in {
284 self.recenter(ctx, app);
285 }
286 } else if self.zoomed && !self.dragging {
287 let mut ok = true;
290 for pt in [
291 ScreenPt::new(0.0, 0.0),
292 ScreenPt::new(ctx.canvas.window_width, ctx.canvas.window_height),
293 ] {
294 let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.screen_to_map(pt));
295 if !(0.0..=1.0).contains(&pct_x) || pct_y < 0.0 || pct_y > 1.0 {
296 ok = false;
297 break;
298 }
299 }
300 if !ok {
301 self.recenter(ctx, app);
302 }
303 }
304 if ctx.input.is_window_resized() {
305 let map_bounds = app.map().get_bounds();
309 self.base_zoom = (MINIMAP_WIDTH / map_bounds.width())
311 .max(MINIMAP_HEIGHT / map_bounds.height())
312 .max(0.001);
313 self.zoom = self.base_zoom;
314 if self.zoomed {
315 self.recenter(ctx, app);
316 }
317 }
318
319 let pan_speed = 100.0;
320 match self.panel.event(ctx) {
321 Outcome::Clicked(x) => match x {
322 x if x == "pan up" => {
323 self.offset_y -= pan_speed * self.zoom;
324 return Some(Transition::KeepWithMouseover);
325 }
326 x if x == "pan down" => {
327 self.offset_y += pan_speed * self.zoom;
328 return Some(Transition::KeepWithMouseover);
329 }
330 x if x == "pan left" => {
331 self.offset_x -= pan_speed * self.zoom;
332 return Some(Transition::KeepWithMouseover);
333 }
334 x if x == "pan right" => {
335 self.offset_x += pan_speed * self.zoom;
336 return Some(Transition::KeepWithMouseover);
337 }
338 x if x == "zoom in" => {
341 if self.zoom_lvl != 3 {
342 self.set_zoom(ctx, app, self.zoom_lvl + 1);
343 }
344 }
345 x if x == "zoom out" => {
346 if self.zoom_lvl != 0 {
347 self.set_zoom(ctx, app, self.zoom_lvl - 1);
348 }
349 }
350 x if x == "zoom to level 1" => {
351 self.set_zoom(ctx, app, 0);
352 }
353 x if x == "zoom to level 2" => {
354 self.set_zoom(ctx, app, 1);
355 }
356 x if x == "zoom to level 3" => {
357 self.set_zoom(ctx, app, 2);
358 }
359 x if x == "zoom to level 4" => {
360 self.set_zoom(ctx, app, 3);
361 }
362 x => {
363 if let Some(transition) = self.controls.panel_clicked(ctx, app, &x) {
364 return Some(transition);
365 }
366 }
367 },
368 Outcome::Changed(_) => {
369 self.controls.panel_changed(ctx, app, &self.panel);
370 if self.panel.has_widget("zorder") {
371 app.mut_draw_map().show_zorder = self.panel.spinner("zorder");
372 }
373 self.recreate_panel(ctx, app);
374 }
375 _ => {}
376 }
377
378 if self.zoomed {
379 let inner_rect = self.panel.rect_of("minimap");
380
381 let mut pt = ctx.canvas.get_cursor();
384 if self.dragging {
385 if ctx.input.left_mouse_button_released() {
386 self.dragging = false;
387 }
388 pt.x = pt.x.clamp(inner_rect.x1, inner_rect.x2);
390 pt.y = pt.y.clamp(inner_rect.y1, inner_rect.y2);
391 } else if inner_rect.contains(pt) && ctx.input.left_mouse_button_pressed() {
392 self.dragging = true;
393 } else {
394 return None;
395 }
396
397 let percent_x = (pt.x - inner_rect.x1) / inner_rect.width();
398 let percent_y = (pt.y - inner_rect.y1) / inner_rect.height();
399
400 let map_pt = Pt2D::new(
401 (self.offset_x + percent_x * inner_rect.width()) / self.zoom,
402 (self.offset_y + percent_y * inner_rect.height()) / self.zoom,
403 );
404 ctx.canvas.center_on_map_pt(map_pt);
405 }
406
407 None
408 }
409
410 pub fn draw(&self, g: &mut GfxCtx, app: &A) {
411 self.draw_with_extra_layers(g, app, Vec::new());
412 }
413
414 pub fn draw_with_extra_layers(&self, g: &mut GfxCtx, app: &A, extra: Vec<&Drawable>) {
415 self.panel.draw(g);
416 if !self.zoomed {
417 return;
418 }
419
420 let inner_rect = self.panel.rect_of("minimap").clone();
421
422 let mut map_bounds = *app.map().get_bounds();
423 map_bounds.min_x = (map_bounds.min_x + self.offset_x) / self.zoom;
425 map_bounds.min_y = (map_bounds.min_y + self.offset_y) / self.zoom;
426 map_bounds.max_x = map_bounds.min_x + inner_rect.width() / self.zoom;
427 map_bounds.max_y = map_bounds.min_y + inner_rect.height() / self.zoom;
428
429 g.fork(
430 Pt2D::new(map_bounds.min_x, map_bounds.min_y),
431 ScreenPt::new(inner_rect.x1, inner_rect.y1),
432 self.zoom,
433 None,
434 );
435 g.enable_clipping(inner_rect);
436 let draw_map = app.draw_map();
437 g.redraw(&draw_map.boundary_polygon);
438 g.redraw(&draw_map.draw_all_areas);
439 g.redraw(&draw_map.draw_all_unzoomed_parking_lots);
440 g.redraw(&draw_map.draw_all_unzoomed_roads_and_intersections);
441 if app.cs().show_buildings_in_minimap {
442 g.redraw(&draw_map.draw_all_buildings);
443 }
444 for draw in extra {
445 g.redraw(draw);
446 }
447 self.controls.draw_extra(g, app);
448
449 let (x1, y1) = {
451 let pt = g.canvas.screen_to_map(ScreenPt::new(0.0, 0.0));
452 (pt.x(), pt.y())
453 };
454 let (x2, y2) = {
455 let pt = g
456 .canvas
457 .screen_to_map(ScreenPt::new(g.canvas.window_width, g.canvas.window_height));
458 (pt.x(), pt.y())
459 };
460 if let Ok(rect) = Ring::new(vec![
463 Pt2D::new(x1, y1),
464 Pt2D::new(x2, y1),
465 Pt2D::new(x2, y2),
466 Pt2D::new(x1, y2),
467 Pt2D::new(x1, y1),
468 ]) {
469 if let Some(color) = app.cs().minimap_cursor_bg {
470 g.draw_polygon(color, rect.clone().into_polygon());
471 }
472 g.draw_polygon(
473 app.cs().minimap_cursor_border,
474 rect.to_outline(Distance::meters(10.0)),
475 );
476 }
477 g.disable_clipping();
478 g.unfork();
479 }
480
481 pub fn get_panel(&self) -> &Panel {
482 &self.panel
483 }
484
485 pub fn mut_panel(&mut self) -> &mut Panel {
486 &mut self.panel
487 }
488}