use std::marker::PhantomData;
use geom::{Distance, Pt2D, Ring, Time};
use widgetry::{
ControlState, Drawable, EventCtx, Filler, GfxCtx, HorizontalAlignment, Line, Outcome, Panel,
ScreenDims, ScreenPt, Spinner, Transition, VerticalAlignment, Widget,
};
use crate::AppLike;
static MINIMAP_WIDTH: f64 = 400.0;
static MINIMAP_HEIGHT: f64 = 300.0;
pub struct Minimap<A: AppLike, T: MinimapControls<A>> {
controls: T,
time: Time,
app_type: PhantomData<A>,
dragging: bool,
panel: Panel,
zoomed: bool,
layer: bool,
zoom_lvl: usize,
base_zoom: f64,
zoom: f64,
offset_x: f64,
offset_y: f64,
}
pub trait MinimapControls<A: AppLike> {
fn has_zorder(&self, app: &A) -> bool;
fn has_layer(&self, _: &A) -> bool {
false
}
fn draw_extra(&self, _: &mut GfxCtx, _: &A) {}
fn make_unzoomed_panel(&self, ctx: &mut EventCtx, _: &A) -> Panel {
Panel::empty(ctx)
}
fn make_legend(&self, _: &mut EventCtx, _: &A) -> Widget {
Widget::nothing()
}
fn make_zoomed_side_panel(&self, _: &mut EventCtx, _: &A) -> Widget {
Widget::nothing()
}
fn panel_clicked(&self, _: &mut EventCtx, _: &mut A, _: &str) -> Option<Transition<A>> {
unreachable!()
}
fn panel_changed(&self, _: &mut EventCtx, _: &mut A, _: &Panel) {}
}
impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
pub fn new(ctx: &mut EventCtx, app: &A, controls: T) -> Minimap<A, T> {
let bounds = app.map().get_bounds();
let base_zoom = 0.15 * ctx.canvas.window_width / bounds.width().min(bounds.height());
let layer = controls.has_layer(app);
let mut m = Minimap {
controls,
time: Time::START_OF_DAY,
app_type: PhantomData,
dragging: false,
panel: Panel::empty(ctx),
zoomed: ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail,
layer,
zoom_lvl: 0,
base_zoom,
zoom: base_zoom,
offset_x: 0.0,
offset_y: 0.0,
};
m.recreate_panel(ctx, app);
if m.zoomed {
m.recenter(ctx, app);
}
m
}
pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &A) {
if ctx.canvas.cam_zoom < app.opts().min_zoom_for_detail {
self.panel = self.controls.make_unzoomed_panel(ctx, app);
return;
}
let zoom_col = {
let mut col = vec![ctx
.style()
.btn_plain
.icon("system/assets/speed/plus.svg")
.build_widget(ctx, "zoom in")
.centered_horiz()
.margin_below(10)];
let level_btn = ctx
.style()
.btn_plain
.icon("system/assets/speed/zoom_level_rect.svg")
.padding_top(0.0)
.padding_bottom(0.0);
for i in (0..=3).rev() {
let level_btn = if self.zoom_lvl < i {
level_btn
.clone()
.image_color(ctx.style().btn_outline.fg_disabled, ControlState::Default)
} else {
level_btn.clone()
};
col.push(
level_btn
.build_widget(ctx, format!("zoom to level {}", i + 1))
.centered_horiz()
.margin_below(10),
);
}
col.push(
ctx.style()
.btn_plain
.icon("system/assets/speed/minus.svg")
.build_widget(ctx, "zoom out")
.centered_horiz(),
);
Widget::custom_col(vec![
Widget::custom_col(col)
.padding(10)
.bg(app.cs().inner_panel_bg),
if self.controls.has_zorder(app) {
Widget::col(vec![
Line("Z-order:").small().into_widget(ctx),
Spinner::widget(
ctx,
"zorder",
app.draw_map().zorder_range,
app.draw_map().show_zorder,
1,
),
])
.margin_above(10)
} else {
Widget::nothing()
},
])
.margin_above(26)
};
let minimap_widget =
Filler::fixed_dims(ScreenDims::new(MINIMAP_WIDTH, MINIMAP_HEIGHT)).named("minimap");
let minimap_controls = {
let buttons = ctx.style().btn_plain.btn().padding(4);
Widget::col(vec![
buttons
.clone()
.image_path("system/assets/minimap/up.svg")
.build_widget(ctx, "pan up")
.centered_horiz(),
Widget::row(vec![
buttons
.clone()
.image_path("system/assets/minimap/left.svg")
.build_widget(ctx, "pan left")
.centered_vert(),
minimap_widget,
buttons
.clone()
.image_path("system/assets/minimap/right.svg")
.build_widget(ctx, "pan right")
.centered_vert(),
]),
buttons
.clone()
.image_path("system/assets/minimap/down.svg")
.build_widget(ctx, "pan down")
.centered_horiz(),
])
};
let controls = if app.opts().minimal_controls {
minimap_controls.padding(16).bg(app.cs().panel_bg)
} else {
Widget::row(vec![
self.controls.make_zoomed_side_panel(ctx, app),
Widget::col(vec![
Widget::row(vec![minimap_controls, zoom_col]),
self.controls.make_legend(ctx, app),
])
.padding(16)
.bg(app.cs().panel_bg),
])
};
self.panel = Panel::new_builder(controls)
.aligned(
HorizontalAlignment::Right,
VerticalAlignment::BottomAboveOSD,
)
.build_custom(ctx);
}
fn map_to_minimap_pct(&self, pt: Pt2D) -> (f64, f64) {
let inner_rect = self.panel.rect_of("minimap");
let pct_x = (pt.x() * self.zoom - self.offset_x) / inner_rect.width();
let pct_y = (pt.y() * self.zoom - self.offset_y) / inner_rect.height();
(pct_x, pct_y)
}
pub fn set_zoom(&mut self, ctx: &mut EventCtx, app: &A, zoom_lvl: usize) {
let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.center_to_map_pt());
let zoom_speed: f64 = 2.0;
self.zoom_lvl = zoom_lvl;
self.zoom = self.base_zoom * zoom_speed.powi(self.zoom_lvl as i32);
self.recreate_panel(ctx, app);
let map_center = ctx.canvas.center_to_map_pt();
let inner_rect = self.panel.rect_of("minimap");
self.offset_x = map_center.x() * self.zoom - pct_x * inner_rect.width();
self.offset_y = map_center.y() * self.zoom - pct_y * inner_rect.height();
}
fn recenter(&mut self, ctx: &EventCtx, app: &A) {
let map_center = ctx.canvas.center_to_map_pt();
let rect = self.panel.rect_of("minimap");
let off_x = map_center.x() * self.zoom - rect.width() / 2.0;
let off_y = map_center.y() * self.zoom - rect.height() / 2.0;
let bounds = app.map().get_bounds();
let max_x = bounds.max_x * self.zoom - rect.width();
let max_y = bounds.max_y * self.zoom - rect.height();
if max_x >= 0.0 && max_y >= 0.0 {
self.offset_x = off_x.clamp(0.0, max_x);
self.offset_y = off_y.clamp(0.0, max_y);
}
}
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Option<Transition<A>> {
if self.time != app.sim_time() {
self.time = app.sim_time();
self.recreate_panel(ctx, app);
}
let zoomed = ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail;
let layer = self.controls.has_layer(app);
if zoomed != self.zoomed || layer != self.layer {
let just_zoomed_in = zoomed && !self.zoomed;
self.zoomed = zoomed;
self.layer = layer;
self.recreate_panel(ctx, app);
if just_zoomed_in {
self.recenter(ctx, app);
}
} else if self.zoomed && !self.dragging {
let mut ok = true;
for &pt in &[
ScreenPt::new(0.0, 0.0),
ScreenPt::new(ctx.canvas.window_width, ctx.canvas.window_height),
] {
let (pct_x, pct_y) = self.map_to_minimap_pct(ctx.canvas.screen_to_map(pt));
if !(0.0..=1.0).contains(&pct_x) || pct_y < 0.0 || pct_y > 1.0 {
ok = false;
break;
}
}
if !ok {
self.recenter(ctx, app);
}
}
if ctx.input.is_window_resized() {
let map_bounds = app.map().get_bounds();
self.base_zoom = (MINIMAP_WIDTH / map_bounds.width())
.max(MINIMAP_HEIGHT / map_bounds.height())
.max(0.001);
self.zoom = self.base_zoom;
if self.zoomed {
self.recenter(ctx, app);
}
}
let pan_speed = 100.0;
match self.panel.event(ctx) {
Outcome::Clicked(x) => match x {
x if x == "pan up" => {
self.offset_y -= pan_speed * self.zoom;
return Some(Transition::KeepWithMouseover);
}
x if x == "pan down" => {
self.offset_y += pan_speed * self.zoom;
return Some(Transition::KeepWithMouseover);
}
x if x == "pan left" => {
self.offset_x -= pan_speed * self.zoom;
return Some(Transition::KeepWithMouseover);
}
x if x == "pan right" => {
self.offset_x += pan_speed * self.zoom;
return Some(Transition::KeepWithMouseover);
}
x if x == "zoom in" => {
if self.zoom_lvl != 3 {
self.set_zoom(ctx, app, self.zoom_lvl + 1);
}
}
x if x == "zoom out" => {
if self.zoom_lvl != 0 {
self.set_zoom(ctx, app, self.zoom_lvl - 1);
}
}
x if x == "zoom to level 1" => {
self.set_zoom(ctx, app, 0);
}
x if x == "zoom to level 2" => {
self.set_zoom(ctx, app, 1);
}
x if x == "zoom to level 3" => {
self.set_zoom(ctx, app, 2);
}
x if x == "zoom to level 4" => {
self.set_zoom(ctx, app, 3);
}
x => {
if let Some(transition) = self.controls.panel_clicked(ctx, app, &x) {
return Some(transition);
}
}
},
Outcome::Changed(_) => {
self.controls.panel_changed(ctx, app, &self.panel);
if self.panel.has_widget("zorder") {
app.mut_draw_map().show_zorder = self.panel.spinner("zorder");
}
self.recreate_panel(ctx, app);
}
_ => {}
}
if self.zoomed {
let inner_rect = self.panel.rect_of("minimap");
let mut pt = ctx.canvas.get_cursor();
if self.dragging {
if ctx.input.left_mouse_button_released() {
self.dragging = false;
}
pt.x = pt.x.clamp(inner_rect.x1, inner_rect.x2);
pt.y = pt.y.clamp(inner_rect.y1, inner_rect.y2);
} else if inner_rect.contains(pt) && ctx.input.left_mouse_button_pressed() {
self.dragging = true;
} else {
return None;
}
let percent_x = (pt.x - inner_rect.x1) / inner_rect.width();
let percent_y = (pt.y - inner_rect.y1) / inner_rect.height();
let map_pt = Pt2D::new(
(self.offset_x + percent_x * inner_rect.width()) / self.zoom,
(self.offset_y + percent_y * inner_rect.height()) / self.zoom,
);
ctx.canvas.center_on_map_pt(map_pt);
}
None
}
pub fn draw(&self, g: &mut GfxCtx, app: &A) {
self.draw_with_extra_layers(g, app, Vec::new());
}
pub fn draw_with_extra_layers(&self, g: &mut GfxCtx, app: &A, extra: Vec<&Drawable>) {
self.panel.draw(g);
if !self.zoomed {
return;
}
let inner_rect = self.panel.rect_of("minimap").clone();
let mut map_bounds = *app.map().get_bounds();
map_bounds.min_x = (map_bounds.min_x + self.offset_x) / self.zoom;
map_bounds.min_y = (map_bounds.min_y + self.offset_y) / self.zoom;
map_bounds.max_x = map_bounds.min_x + inner_rect.width() / self.zoom;
map_bounds.max_y = map_bounds.min_y + inner_rect.height() / self.zoom;
g.fork(
Pt2D::new(map_bounds.min_x, map_bounds.min_y),
ScreenPt::new(inner_rect.x1, inner_rect.y1),
self.zoom,
None,
);
g.enable_clipping(inner_rect);
let draw_map = app.draw_map();
g.redraw(&draw_map.boundary_polygon);
g.redraw(&draw_map.draw_all_areas);
g.redraw(&draw_map.draw_all_unzoomed_parking_lots);
g.redraw(&draw_map.draw_all_unzoomed_roads_and_intersections);
g.redraw(&draw_map.draw_all_buildings);
for draw in extra {
g.redraw(draw);
}
self.controls.draw_extra(g, app);
let (x1, y1) = {
let pt = g.canvas.screen_to_map(ScreenPt::new(0.0, 0.0));
(pt.x(), pt.y())
};
let (x2, y2) = {
let pt = g
.canvas
.screen_to_map(ScreenPt::new(g.canvas.window_width, g.canvas.window_height));
(pt.x(), pt.y())
};
if let Ok(rect) = Ring::new(vec![
Pt2D::new(x1, y1),
Pt2D::new(x2, y1),
Pt2D::new(x2, y2),
Pt2D::new(x1, y2),
Pt2D::new(x1, y1),
]) {
if let Some(color) = app.cs().minimap_cursor_bg {
g.draw_polygon(color, rect.clone().into_polygon());
}
g.draw_polygon(
app.cs().minimap_cursor_border,
rect.to_outline(Distance::meters(10.0)),
);
}
g.disable_clipping();
g.unfork();
}
pub fn get_panel(&self) -> &Panel {
&self.panel
}
pub fn mut_panel(&mut self) -> &mut Panel {
&mut self.panel
}
}