Convert the new bike routing tool to use World. #763

This one is the most complicated, and it's still not done, but it's not
any buggier than the ad-hoc implementation. I still need to figure out
how to merge the two worlds of waypoints and routes.

This one also required a large, but mechanical, refactor to lift
ToggleZoomed and the concept of unzoomed/zoomed from map_gui to
widgetry.
This commit is contained in:
Dustin Carlino 2021-10-03 12:48:50 -07:00
parent 43f8a6d1e7
commit 41465c341b
48 changed files with 558 additions and 528 deletions

View File

@ -372,11 +372,7 @@ impl HoverOnBuilding {
pub fn key(ctx: &EventCtx, app: &App) -> Option<HoverKey> {
match app.mouseover_unzoomed_buildings(ctx) {
Some(ID::Building(b)) => {
let scale_factor = if ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail {
1.0
} else {
10.0
};
let scale_factor = if ctx.canvas.is_zoomed() { 1.0 } else { 10.0 };
Some((b, scale_factor))
}
_ => None,

View File

@ -11,11 +11,12 @@ use geom::{Bounds, Circle, Distance, Duration, FindClosest, Polygon, Pt2D, Time}
use map_gui::colors::ColorScheme;
use map_gui::options::Options;
use map_gui::render::{unzoomed_agent_radius, AgentCache, DrawMap, DrawOptions, Renderable};
use map_gui::tools::{CameraState, ToggleZoomed};
use map_gui::tools::CameraState;
use map_gui::ID;
use map_model::AreaType;
use map_model::{BufferType, IntersectionID, LaneType, Map, Traversable};
use sim::{AgentID, Analytics, Scenario, Sim, SimCallback, SimFlags, VehicleType};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Cached, Canvas, EventCtx, GfxCtx, Prerender, SharedAppState, State};
use crate::challenges::HighScore;
@ -122,8 +123,7 @@ impl App {
g.clear(self.cs.void_background);
g.redraw(&draw_map.boundary_polygon);
if g.canvas.cam_zoom < self.opts.min_zoom_for_detail {
// Unzoomed mode
if g.canvas.is_unzoomed() {
let layers = show_objs.layers();
if layers.show_areas {
g.redraw(&draw_map.draw_all_areas);
@ -255,7 +255,7 @@ impl App {
unzoomed_roads_and_intersections: bool,
unzoomed_buildings: bool,
) -> Option<ID> {
let unzoomed = ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail;
let unzoomed = ctx.canvas.is_unzoomed();
// Unzoomed mode. Ignore when debugging areas.
if unzoomed && !(debug_mode || unzoomed_roads_and_intersections || unzoomed_buildings) {

View File

@ -1,8 +1,9 @@
use geom::{Circle, Distance, FindClosest, Polygon};
use geom::{Circle, Distance, FindClosest, Pt2D};
use sim::TripEndpoint;
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
use widgetry::{
Color, ControlState, CornerRounding, DragDrop, Drawable, EventCtx, GeomBatch, GfxCtx, Image,
Line, Outcome, StackAxis, Text, Widget,
Color, ControlState, CornerRounding, DragDrop, EventCtx, GeomBatch, GfxCtx, Image, Key, Line,
Outcome, RewriteColor, StackAxis, Text, Widget,
};
use crate::app::App;
@ -11,29 +12,22 @@ use crate::app::App;
/// Panel, since there's probably more stuff there too.
pub struct InputWaypoints {
waypoints: Vec<Waypoint>,
draw_waypoints: Drawable,
hovering_on_waypt: Option<usize>,
draw_hover: Drawable,
// TODO Invariant not captured by these separate fields: when dragging is true,
// hovering_on_waypt is fixed.
dragging: bool,
world: World<WaypointID>,
snap_to_endpts: FindClosest<TripEndpoint>,
}
// TODO Maybe it's been a while and I've forgotten some UI patterns, but this is painfully manual.
// I think we need a draggable map-space thing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct WaypointID(usize);
impl ObjectID for WaypointID {}
struct Waypoint {
at: TripEndpoint,
label: String,
hitbox: Polygon,
}
fn get_waypoint_text(idx: usize) -> char {
char::from_u32('A' as u32 + idx as u32).unwrap()
center: Pt2D,
}
impl InputWaypoints {
pub fn new(ctx: &mut EventCtx, app: &App) -> InputWaypoints {
pub fn new(app: &App) -> InputWaypoints {
let map = &app.primary.map;
let mut snap_to_endpts = FindClosest::new(map.get_bounds());
for i in map.all_intersections() {
@ -47,10 +41,7 @@ impl InputWaypoints {
InputWaypoints {
waypoints: Vec::new(),
draw_waypoints: Drawable::empty(ctx),
hovering_on_waypt: None,
draw_hover: Drawable::empty(ctx),
dragging: false,
world: World::bounded(map.get_bounds()),
snap_to_endpts,
}
}
@ -60,8 +51,7 @@ impl InputWaypoints {
for at in waypoints {
self.waypoints.push(Waypoint::new(app, at));
}
self.update_waypoints_drawable(ctx);
self.update_hover(ctx);
self.rebuild_world(ctx, app);
}
pub fn get_panel_widget(&self, ctx: &mut EventCtx) -> Widget {
@ -142,47 +132,41 @@ impl InputWaypoints {
self.waypoints.iter().map(|w| w.at).collect()
}
/// If the outcome from the panel isn't used by the caller, pass it along here. This handles
/// calling `ctx.canvas_movement` when appropriate. When this returns true, something has
/// changed, so the caller may want to update their view of the route and call
/// `get_panel_widget` again.
/// If the outcome from the panel isn't used by the caller, pass it along here. When this
/// returns true, something has changed, so the caller may want to update their view of the
/// route and call `get_panel_widget` again.
pub fn event(&mut self, ctx: &mut EventCtx, app: &mut App, outcome: Outcome) -> bool {
if self.dragging {
if ctx.redo_mouseover() && self.update_dragging(ctx, app) == Some(true) {
return true;
match self.world.event(ctx) {
WorldOutcome::ClickedFreeSpace(pt) => {
if let Some((at, _)) = self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0)) {
self.waypoints.push(Waypoint::new(app, at));
self.rebuild_world(ctx, app);
return true;
}
return false;
}
if ctx.input.left_mouse_button_released() {
self.dragging = false;
self.update_hover(ctx);
}
} else {
if ctx.redo_mouseover() {
self.update_hover(ctx);
}
if self.hovering_on_waypt.is_none() {
ctx.canvas_movement();
} else if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
// Zooming is OK, but can't start click and drag
ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
}
if self.hovering_on_waypt.is_some() && ctx.input.left_mouse_button_pressed() {
self.dragging = true;
}
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if self.hovering_on_waypt.is_none() && ctx.normal_left_click() {
if let Some((at, _)) =
self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0))
{
self.waypoints.push(Waypoint::new(app, at));
self.update_waypoints_drawable(ctx);
self.update_hover(ctx);
WorldOutcome::Dragging {
obj: WaypointID(idx),
cursor,
..
} => {
if let Some((at, _)) = self
.snap_to_endpts
.closest_pt(cursor, Distance::meters(30.0))
{
if self.waypoints[idx].at != at {
self.waypoints[idx] = Waypoint::new(app, at);
self.rebuild_world(ctx, app);
return true;
}
}
}
WorldOutcome::Keypress("delete", WaypointID(idx)) => {
self.waypoints.remove(idx);
self.rebuild_world(ctx, app);
return true;
}
_ => {}
}
match outcome {
@ -190,7 +174,7 @@ impl InputWaypoints {
if let Some(x) = x.strip_prefix("delete waypoint ") {
let idx = x.parse::<usize>().unwrap();
self.waypoints.remove(idx);
self.update_waypoints_drawable(ctx);
self.rebuild_world(ctx, app);
return true;
} else {
panic!("Unknown InputWaypoints click {}", x);
@ -210,8 +194,7 @@ impl InputWaypoints {
}
pub fn draw(&self, g: &mut GfxCtx) {
g.redraw(&self.draw_waypoints);
g.redraw(&self.draw_hover);
self.world.draw(g);
}
fn get_waypoint_color(&self, idx: usize) -> Color {
@ -219,66 +202,38 @@ impl InputWaypoints {
match idx {
0 => Color::GREEN,
idx if idx == total_waypoints - 1 => Color::RED,
// technically this includes the case where idx >= total_waypoints which should hopefully never happen
_ => [Color::BLUE, Color::ORANGE, Color::PURPLE][idx % 3],
}
}
fn update_waypoints_drawable(&mut self, ctx: &mut EventCtx) {
let mut batch = GeomBatch::new();
for (idx, waypt) in self.waypoints.iter().enumerate() {
let color = self.get_waypoint_color(idx);
let text = get_waypoint_text(idx);
fn rebuild_world(&mut self, ctx: &mut EventCtx, app: &App) {
let mut world = World::bounded(app.primary.map.get_bounds());
let mut geom = GeomBatch::new();
geom.push(color, waypt.hitbox.clone());
geom.append(
Text::from(Line(format!("{}", text)).fg(Color::WHITE))
for (idx, waypoint) in self.waypoints.iter().enumerate() {
let hitbox = Circle::new(waypoint.center, Distance::meters(30.0)).to_polygon();
let color = self.get_waypoint_color(idx);
let mut draw_normal = GeomBatch::new();
draw_normal.push(color, hitbox.clone());
draw_normal.append(
Text::from(Line(get_waypoint_text(idx).to_string()).fg(Color::WHITE))
.render(ctx)
.centered_on(waypt.hitbox.center()),
.centered_on(waypoint.center),
);
batch.append(geom);
}
self.draw_waypoints = ctx.upload(batch);
}
fn update_hover(&mut self, ctx: &EventCtx) {
self.hovering_on_waypt = None;
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
self.hovering_on_waypt = self
.waypoints
.iter()
.position(|waypt| waypt.hitbox.contains_pt(pt));
world
.add(WaypointID(idx))
.hitbox(hitbox.clone())
.draw(draw_normal)
.draw_hover_rewrite(RewriteColor::Change(color, Color::BLUE.alpha(0.5)))
.hotkey(Key::Backspace, "delete")
.draggable()
.build(ctx);
}
let mut batch = GeomBatch::new();
if let Some(idx) = self.hovering_on_waypt {
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
}
self.draw_hover = ctx.upload(batch);
}
// `Some(true)` means to update.
fn update_dragging(&mut self, ctx: &mut EventCtx, app: &App) -> Option<bool> {
let pt = ctx.canvas.get_cursor_in_map_space()?;
let (at, _) = self.snap_to_endpts.closest_pt(pt, Distance::meters(30.0))?;
let mut changed = false;
let idx = self.hovering_on_waypt.unwrap();
if self.waypoints[idx].at != at {
self.waypoints[idx] = Waypoint::new(app, at);
self.update_waypoints_drawable(ctx);
changed = true;
}
let mut batch = GeomBatch::new();
// Show where we're currently snapped
batch.push(Color::BLUE.alpha(0.5), self.waypoints[idx].hitbox.clone());
self.draw_hover = ctx.upload(batch);
Some(changed)
world.initialize_hover(ctx);
world.rebuilt_during_drag(&self.world);
self.world = world;
}
}
@ -296,8 +251,10 @@ impl Waypoint {
}
TripEndpoint::SuddenlyAppear(pos) => (pos.pt(map), pos.to_string()),
};
let hitbox = Circle::new(center, Distance::meters(30.0)).to_polygon();
Waypoint { at, label, hitbox }
Waypoint { at, label, center }
}
}
fn get_waypoint_text(idx: usize) -> char {
char::from_u32('A' as u32 + idx as u32).unwrap()
}

View File

@ -1,7 +1,8 @@
use std::collections::HashSet;
use map_gui::tools::{ColorDiscrete, ToggleZoomed};
use map_gui::tools::ColorDiscrete;
use map_model::{connectivity, LaneID, Map, PathConstraints};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State, TextExt,
VerticalAlignment, Widget,
@ -104,8 +105,8 @@ impl State<App> for Floodfiller {
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.draw.draw(g, app);
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.draw.draw(g);
self.panel.draw(g);
}
}

View File

@ -944,7 +944,7 @@ impl ScreenshotTest {
// Taking screenshots messes with options and doesn't restore them after. It's expected
// whoever's taking screenshots (just Dustin so far) will just quit after taking them.
app.change_color_scheme(ctx, ColorSchemeChoice::DayMode);
app.opts.min_zoom_for_detail = 0.0;
ctx.canvas.settings.min_zoom_for_detail = 0.0;
MapLoader::new_state(
ctx,
app,

View File

@ -1,7 +1,8 @@
use abstutil::Counter;
use map_gui::tools::{ColorLegend, ColorNetwork, ToggleZoomed};
use map_gui::tools::{ColorLegend, ColorNetwork};
use map_gui::ID;
use map_model::{IntersectionID, PathStep, RoadID, Traversable};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, EventCtx, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State, Text,
VerticalAlignment, Widget,
@ -115,7 +116,7 @@ impl State<App> for PathCounter {
self.panel.draw(g);
CommonState::draw_osd(g, app);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());

View File

@ -3,13 +3,14 @@ use std::collections::HashMap;
use abstutil::{prettyprint_usize, Counter, Timer};
use geom::{Duration, Polygon};
use map_gui::colors::ColorSchemeChoice;
use map_gui::tools::{ColorNetwork, ToggleZoomed};
use map_gui::tools::ColorNetwork;
use map_gui::{AppLike, ID};
use map_model::{
DirectedRoadID, Direction, PathRequest, RoadID, RoutingParams, Traversable,
NORMAL_LANE_THICKNESS,
};
use sim::{TripEndpoint, TripMode};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
RoundedF64, Spinner, State, Text, TextExt, TextSpan, VerticalAlignment, Widget,
@ -466,7 +467,7 @@ impl State<App> for AllRoutesExplorer {
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.panel.draw(g);
CommonState::draw_osd(g, app);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}

View File

@ -1,8 +1,9 @@
use abstutil::{prettyprint_usize, Counter};
use collisions::{CollisionDataset, Severity};
use geom::{Circle, Distance, Duration, FindClosest, Polygon, Time};
use map_gui::tools::{ColorNetwork, ToggleZoomed};
use map_gui::tools::ColorNetwork;
use map_gui::ID;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Line, Outcome,
Panel, Slider, State, Text, TextExt, Toggle, VerticalAlignment, Widget,
@ -296,10 +297,10 @@ impl State<App> for CollisionsViewer {
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
match self.dataviz {
Dataviz::Aggregated { ref draw } => {
draw.draw(g, app);
draw.draw(g);
}
Dataviz::Individual {
ref draw_all_circles,

View File

@ -118,6 +118,7 @@ impl State<App> for PolygonEditor {
obj: Obj::Point(idx),
dx,
dy,
..
} => {
self.points[idx] = self.points[idx].offset(dx, dy);
self.rebuild_world(ctx, app);
@ -126,6 +127,7 @@ impl State<App> for PolygonEditor {
obj: Obj::Polygon,
dx,
dy,
..
} => {
for pt in &mut self.points {
*pt = pt.offset(dx, dy);

View File

@ -1,6 +1,7 @@
use abstutil::prettyprint_usize;
use map_gui::tools::{ColorDiscrete, ToggleZoomed};
use map_gui::tools::ColorDiscrete;
use sim::Scenario;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, Text,
VerticalAlignment, Widget,
@ -112,7 +113,7 @@ impl State<App> for ScenarioManager {
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.draw.draw(g, app);
self.draw.draw(g);
self.panel.draw(g);
CommonState::draw_osd(g, app);
}

View File

@ -150,6 +150,7 @@ impl State<App> for StoryMapEditor {
obj: MarkerID(idx),
dx,
dy,
..
} => {
for pt in &mut self.story.markers[idx].pts {
*pt = pt.offset(dx, dy);

View File

@ -4,9 +4,10 @@ use abstutil::{prettyprint_usize, Timer};
use geom::Speed;
use map_gui::options::OptionsPanel;
use map_gui::render::DrawMap;
use map_gui::tools::{grey_out_map, ChooseSomething, ColorLegend, PopupMsg, ToggleZoomed};
use map_gui::tools::{grey_out_map, ChooseSomething, ColorLegend, PopupMsg};
use map_gui::ID;
use map_model::{EditCmd, IntersectionID, LaneID, MapEdits};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
lctrl, Choice, Color, ControlState, EventCtx, GfxCtx, HorizontalAlignment, Image, Key, Line,
Menu, Outcome, Panel, State, Text, TextBox, TextExt, VerticalAlignment, Widget,
@ -330,7 +331,7 @@ impl State<App> for EditMode {
}
}
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if let Some(id) = app.primary.current_selection.clone() {
if app.per_obj.left_click(ctx, "edit this") {
return Transition::Push(Warping::new_state(
@ -369,7 +370,7 @@ impl State<App> for EditMode {
self.tool_panel.draw(g);
self.top_center.draw(g);
self.changelist.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
CommonState::draw_osd(g, app);
}
}

View File

@ -3,9 +3,10 @@ use std::collections::BTreeSet;
use enumset::EnumSet;
use maplit::btreeset;
use map_gui::tools::{ColorDiscrete, ToggleZoomed};
use map_gui::tools::ColorDiscrete;
use map_model::{AccessRestrictions, PathConstraints, RoadID};
use sim::TripMode;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, Text,
VerticalAlignment, Widget,
@ -157,7 +158,7 @@ impl State<App> for ZoneEditor {
fn draw(&self, g: &mut GfxCtx, app: &App) {
// TODO The currently selected road is covered up pretty badly
self.draw.draw(g, app);
self.draw.draw(g);
self.panel.draw(g);
self.selector.draw(g, app, false);
CommonState::draw_osd(g, app);

View File

@ -3,13 +3,14 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
pub use trip::OpenTrip;
use geom::{Circle, Distance, Polygon, Time};
use map_gui::tools::{open_browser, ToggleZoomed, ToggleZoomedBuilder};
use map_gui::tools::open_browser;
use map_gui::ID;
use map_model::{AreaID, BuildingID, BusRouteID, BusStopID, IntersectionID, LaneID, ParkingLotID};
use sim::{
AgentID, AgentType, Analytics, CarID, ParkingSpot, PedestrianID, PersonID, PersonState, TripID,
VehicleType,
};
use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
use widgetry::{
EventCtx, GfxCtx, Key, Line, LinePlot, Outcome, Panel, PlotOptions, Series, Text, TextExt,
Toggle, Widget,
@ -631,9 +632,9 @@ impl InfoPanel {
}
}
pub fn draw(&self, g: &mut GfxCtx, app: &App) {
pub fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw_extra.draw(g, app);
self.draw_extra.draw(g);
if let Some(pt) = g.canvas.get_cursor_in_map_space() {
for (poly, txt) in &self.tooltips {
if poly.contains_pt(pt) {

View File

@ -1,6 +1,7 @@
use geom::{Angle, Distance, FindClosest, PolyLine, Polygon, Pt2D};
use map_gui::tools::{ColorDiscrete, ColorScale, Grid, ToggleZoomed};
use map_gui::tools::{ColorDiscrete, ColorScale, Grid};
use map_gui::ID;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, EventCtx, GeomBatch, GfxCtx, Panel, Text, TextExt, Widget};
use crate::app::App;
@ -29,9 +30,9 @@ impl Layer for SteepStreets {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}
@ -168,7 +169,7 @@ impl Layer for ElevationContours {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
if ctx.redo_mouseover() {
self.tooltip = None;
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if let Some((elevation, _)) = self
.closest_elevation
@ -185,9 +186,9 @@ impl Layer for ElevationContours {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}

View File

@ -2,10 +2,11 @@ use maplit::btreeset;
use abstutil::{prettyprint_usize, Counter};
use geom::{Distance, Time};
use map_gui::tools::{ColorDiscrete, ColorLegend, ColorNetwork, ToggleZoomed};
use map_gui::tools::{ColorDiscrete, ColorLegend, ColorNetwork};
use map_gui::ID;
use map_model::{AmenityType, LaneType};
use sim::AgentType;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, EventCtx, GfxCtx, Line, Panel, Text, Widget};
use crate::app::App;
@ -30,7 +31,7 @@ impl Layer for BikeActivity {
}
// Show a tooltip with count, only when unzoomed
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if ctx.redo_mouseover() || recalc_tooltip {
self.tooltip = None;
if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
@ -51,9 +52,9 @@ impl Layer for BikeActivity {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}
@ -174,9 +175,9 @@ impl Layer for Static {
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Option<LayerOutcome> {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -2,8 +2,9 @@ use std::collections::HashSet;
use abstutil::prettyprint_usize;
use geom::{Circle, Distance, Pt2D, Time};
use map_gui::tools::{make_heatmap, HeatmapOptions, ToggleZoomed};
use map_gui::tools::{make_heatmap, HeatmapOptions};
use sim::PersonState;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Choice, Color, EventCtx, GfxCtx, Line, Outcome, Panel, Text, TextExt, Toggle, Widget,
};
@ -47,9 +48,9 @@ impl Layer for Pandemic {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -3,9 +3,10 @@ use std::collections::BTreeSet;
use abstutil::{prettyprint_usize, Counter};
use geom::{Circle, Distance, Duration, Pt2D, Time};
use map_gui::render::unzoomed_agent_radius;
use map_gui::tools::{ColorLegend, ColorNetwork, ToggleZoomed};
use map_gui::tools::{ColorLegend, ColorNetwork};
use map_model::{BuildingID, OffstreetParking, ParkingLotID, PathRequest, RoadID};
use sim::{ParkingSpot, VehicleType};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{EventCtx, GfxCtx, Line, Outcome, Panel, Text, Toggle, Widget};
use crate::app::App;
@ -61,9 +62,9 @@ impl Layer for Occupancy {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);
@ -315,9 +316,9 @@ impl Layer for Efficiency {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -2,8 +2,9 @@ use std::collections::HashSet;
use abstutil::prettyprint_usize;
use geom::{Circle, Distance, Pt2D, Time};
use map_gui::tools::{make_heatmap, HeatmapOptions, ToggleZoomed};
use map_gui::tools::{make_heatmap, HeatmapOptions};
use sim::PersonState;
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, EventCtx, GfxCtx, Image, Line, Outcome, Panel, Toggle, Widget};
use crate::app::App;
@ -45,9 +46,9 @@ impl Layer for PopulationMap {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -2,8 +2,9 @@ use std::collections::BTreeSet;
use abstutil::prettyprint_usize;
use geom::{Circle, Distance, Pt2D, Time};
use map_gui::tools::{make_heatmap, HeatmapOptions, ToggleZoomed};
use map_gui::tools::{make_heatmap, HeatmapOptions};
use sim::{Problem, TripInfo, TripMode};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, EventCtx, GfxCtx, Line, Outcome, Panel, Slider, Text, TextExt, Toggle, Widget,
};
@ -46,9 +47,9 @@ impl Layer for ProblemMap {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -5,10 +5,11 @@ use maplit::btreeset;
use abstutil::{prettyprint_usize, Counter};
use geom::{Circle, Distance, Duration, Percent, Polygon, Pt2D, Time};
use map_gui::render::unzoomed_agent_radius;
use map_gui::tools::{ColorLegend, ColorNetwork, DivergingScale, ToggleZoomed};
use map_gui::tools::{ColorLegend, ColorNetwork, DivergingScale};
use map_gui::ID;
use map_model::{IntersectionID, Map, Traversable};
use sim::{AgentType, VehicleType};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, EventCtx, GfxCtx, Line, Outcome, Panel, Text, TextExt, Toggle, Widget};
use crate::app::App;
@ -31,9 +32,9 @@ impl Layer for Backpressure {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);
@ -106,7 +107,7 @@ impl Layer for Throughput {
}
// Show a tooltip with count, only when unzoomed
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if ctx.redo_mouseover() || recalc_tooltip {
self.tooltip = None;
match app.mouseover_unzoomed_roads_and_intersections(ctx) {
@ -174,9 +175,9 @@ impl Layer for Throughput {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}
@ -253,7 +254,7 @@ impl Layer for CompareThroughput {
}
// Show a tooltip with count, only when unzoomed
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if ctx.redo_mouseover() || recalc_tooltip {
self.tooltip = None;
match app.mouseover_unzoomed_roads_and_intersections(ctx) {
@ -315,9 +316,9 @@ impl Layer for CompareThroughput {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}
@ -411,9 +412,9 @@ impl Layer for TrafficJams {
<dyn Layer>::simple_event(ctx, &mut self.panel)
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);
@ -546,9 +547,9 @@ impl Layer for Delay {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -1,5 +1,6 @@
use map_gui::tools::{ColorDiscrete, ToggleZoomed};
use map_gui::tools::ColorDiscrete;
use map_model::{PathConstraints, PathStep};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{EventCtx, GfxCtx, Outcome, Panel, Toggle, Widget};
use crate::app::App;
@ -35,9 +36,9 @@ impl Layer for TransitNetwork {
}
None
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g, app);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);

View File

@ -85,9 +85,9 @@ fn run(mut settings: Settings) {
ltn: args.enabled("--ltn"),
};
settings = settings.canvas_settings(setup.opts.canvas_settings.clone());
setup.opts.toggle_day_night_colors = true;
setup.opts.update_from_args(&mut args);
settings = settings.canvas_settings(setup.opts.canvas_settings.clone());
if args.enabled("--dump_raw_events") {
settings = settings.dump_raw_events();

View File

@ -1,9 +1,10 @@
use std::collections::BTreeSet;
use geom::{Distance, Line};
use map_gui::tools::{CityPicker, ColorDiscrete, ToggleZoomed};
use map_gui::tools::{CityPicker, ColorDiscrete};
use map_gui::ID;
use map_model::{IntersectionID, Map, Road, RoadID};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
State, Text, TextExt, VerticalAlignment, Widget,
@ -216,7 +217,7 @@ impl State<App> for Viewer {
fn draw(&self, g: &mut GfxCtx, app: &App) {
self.panel.draw(g);
self.draw_neighborhood.draw(g, app);
self.draw_neighborhood.draw(g);
g.redraw(&self.draw_dynamic_stuff);
if let Some(ID::Road(r)) = app.primary.current_selection {

View File

@ -101,7 +101,7 @@ impl TrafficSignalDemand {
}
draw_all.extend(Color::WHITE, outlines);
}
world.draw_master_batch(ctx.upload(draw_all));
world.draw_master_batch(ctx, draw_all);
world.initialize_hover(ctx);
self.world = world;

View File

@ -411,11 +411,7 @@ impl GameplayState for Tutorial {
// Special things
if tut.interaction() == Task::Camera {
let fire = GeomBatch::load_svg(g, "system/assets/tools/fire.svg")
.scale(if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
0.2
} else {
0.1
})
.scale(if g.canvas.is_unzoomed() { 0.2 } else { 0.1 })
.autocrop()
.centered_on(app.primary.map.get_b(tut.fire_station).polygon.polylabel());
let offset = -fire.get_dims().height / 2.0;

View File

@ -308,7 +308,7 @@ fn make_tool_panel(ctx: &mut EventCtx, app: &App) -> Widget {
.padding(8);
Widget::col(vec![
(if ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail {
(if ctx.canvas.is_zoomed() {
buttons
.clone()
.image_path("system/assets/minimap/zoom_out_fully.svg")

View File

@ -33,7 +33,7 @@ impl RoutePreview {
.and_then(|id| id.agent_id())
{
let now = app.primary.sim.time();
let zoomed = ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail;
let zoomed = ctx.canvas.is_zoomed();
if self
.preview
.as_ref()

View File

@ -161,7 +161,7 @@ impl State<App> for SandboxMode {
// We need to recalculate unzoomed agent mouseover when the mouse is still and time passes
// (since something could move beneath the cursor), or when the mouse moves.
if app.primary.current_selection.is_none()
&& ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail
&& ctx.canvas.is_unzoomed()
&& (ctx.redo_mouseover()
|| self
.recalc_unzoomed_agent

View File

@ -66,7 +66,7 @@ impl State<App> for ExploreMap {
}
// Only when zoomed in, click to edit a road in detail
if ctx.canvas.cam_zoom >= app.opts.min_zoom_for_detail {
if ctx.canvas.is_zoomed() {
if ctx.redo_mouseover() {
app.primary.current_selection =
match app.mouseover_unzoomed_roads_and_intersections(ctx) {

View File

@ -67,7 +67,7 @@ impl Layers {
if ctx.redo_mouseover() && self.elevation && !self.minimized {
let mut label = Text::new().into_widget(ctx);
if ctx.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if let Some((elevation, _)) = app
.session
@ -193,7 +193,7 @@ impl Layers {
pub fn draw(&self, g: &mut GfxCtx, app: &App) {
self.panel.draw(g);
if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if g.canvas.is_unzoomed() {
g.redraw(&self.fade_map);
let mut draw_bike_layer = true;
@ -222,7 +222,7 @@ impl Layers {
if self.elevation {
if let Some((_, ref draw)) = app.session.elevation_contours.value() {
draw.draw(g, app);
draw.draw(g);
}
}
if let Some(ref draw) = self.steep_streets {

View File

@ -50,7 +50,7 @@ impl MagnifyingGlass {
}
pub fn draw(&self, g: &mut GfxCtx, app: &App) {
if g.canvas.cam_zoom >= app.opts.min_zoom_for_detail {
if g.canvas.is_zoomed() {
return;
}

View File

@ -3,10 +3,11 @@ use std::collections::HashSet;
use abstutil::{prettyprint_usize, Counter, Timer};
use geom::{Distance, Duration, Polygon};
use map_gui::load::FileLoader;
use map_gui::tools::{ColorNetwork, ToggleZoomed};
use map_gui::tools::ColorNetwork;
use map_gui::ID;
use map_model::{PathRequest, PathStepV2, RoadID};
use sim::{Scenario, TripEndpoint, TripMode};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{
Color, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, Spinner, State, Text, TextExt, Widget,
};
@ -117,7 +118,7 @@ impl State<App> for ShowGaps {
self.layers.draw(g, app);
let data = app.session.mode_shift.value().unwrap();
data.gaps.draw.draw(g, app);
data.gaps.draw.draw(g);
if let Some(ref txt) = self.tooltip {
g.draw_mouse_tooltip(txt.clone());
}

View File

@ -1,7 +1,8 @@
use map_model::RoutingParams;
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
use widgetry::{Choice, EventCtx, GfxCtx, Outcome, Panel, State, TextExt, Widget};
use self::results::{AltRouteResults, RouteResults};
use self::results::RouteDetails;
use crate::app::{App, Transition};
use crate::common::InputWaypoints;
use crate::ungap::{Layers, Tab, TakeLayers};
@ -15,10 +16,11 @@ pub struct RoutePlanner {
input_panel: Panel,
waypoints: InputWaypoints,
main_route: RouteResults,
main_route: RouteDetails,
files: files::RouteManagement,
alt_routes: Vec<AltRouteResults>,
// TODO We really only need to store preferences and stats, but...
alt_routes: Vec<RouteDetails>,
world: World<RouteID>,
}
impl TakeLayers for RoutePlanner {
@ -27,6 +29,13 @@ impl TakeLayers for RoutePlanner {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum RouteID {
Main,
Alt(usize),
}
impl ObjectID for RouteID {}
impl RoutePlanner {
pub fn new_state(ctx: &mut EventCtx, app: &App, layers: Layers) -> Box<dyn State<App>> {
let mut rp = RoutePlanner {
@ -34,19 +43,29 @@ impl RoutePlanner {
once: true,
input_panel: Panel::empty(ctx),
waypoints: InputWaypoints::new(ctx, app),
main_route: RouteResults::main_route(ctx, app, Vec::new()),
waypoints: InputWaypoints::new(app),
main_route: RouteDetails::main_route(ctx, app, Vec::new()).details,
files: files::RouteManagement::new(app),
alt_routes: Vec::new(),
world: World::bounded(app.primary.map.get_bounds()),
};
rp.update_input_panel(ctx, app);
rp.recalculate_routes(ctx, app);
Box::new(rp)
}
// Use the current session settings to determine "main" and alts
fn recalculate_routes(&mut self, ctx: &mut EventCtx, app: &App) {
// Use the current session settings to determine "main" and alts
self.main_route = RouteResults::main_route(ctx, app, self.waypoints.get_waypoints());
let mut world = World::bounded(app.primary.map.get_bounds());
let main_route = RouteDetails::main_route(ctx, app, self.waypoints.get_waypoints());
self.main_route = main_route.details;
world
.add(RouteID::Main)
.hitbox(main_route.hitbox)
.draw(main_route.draw)
.build(ctx);
// This doesn't depend on the alt routes, so just do it here
self.update_input_panel(ctx, app, main_route.details_widget);
self.alt_routes.clear();
// Just a few fixed variations... all 9 combos seems overwhelming
@ -68,7 +87,7 @@ impl RoutePlanner {
if app.session.routing_preferences == preferences {
continue;
}
let alt = AltRouteResults::new(
let mut alt = RouteDetails::alt_route(
ctx,
app,
self.waypoints.get_waypoints(),
@ -76,18 +95,26 @@ impl RoutePlanner {
preferences,
);
// Dedupe equivalent routes based on their stats, which is usually detailed enough
if alt.results.stats != self.main_route.stats
&& self
.alt_routes
.iter()
.all(|x| alt.results.stats != x.results.stats)
if alt.details.stats != self.main_route.stats
&& self.alt_routes.iter().all(|x| alt.details.stats != x.stats)
{
self.alt_routes.push(alt);
self.alt_routes.push(alt.details);
world
.add(RouteID::Alt(self.alt_routes.len() - 1))
.hitbox(alt.hitbox)
.draw(alt.draw)
.hover_alpha(0.8)
.tooltip(alt.tooltip_for_alt.take().unwrap())
.clickable()
.build(ctx);
}
}
world.initialize_hover(ctx);
self.world = world;
}
fn update_input_panel(&mut self, ctx: &mut EventCtx, app: &App) {
fn update_input_panel(&mut self, ctx: &mut EventCtx, app: &App, main_route: Widget) {
let col = Widget::col(vec![
self.files.get_panel_widget(ctx),
Widget::col(vec![Widget::row(vec![
@ -118,7 +145,7 @@ impl RoutePlanner {
])])
.section(ctx),
self.waypoints.get_panel_widget(ctx).section(ctx),
self.main_route.to_widget(ctx, app).section(ctx),
main_route.section(ctx),
]);
let mut new_panel = Tab::Route.make_left_panel(ctx, app, col);
@ -133,7 +160,6 @@ impl RoutePlanner {
self.waypoints
.overwrite(ctx, app, self.files.current.waypoints.clone());
self.recalculate_routes(ctx, app);
self.update_input_panel(ctx, app);
}
}
@ -148,17 +174,14 @@ impl State<App> for RoutePlanner {
});
}
let mut focused_on_alt_route = false;
for r in &mut self.alt_routes {
r.event(ctx);
focused_on_alt_route |= r.has_focus();
if r.has_focus() && ctx.normal_left_click() {
match self.world.event(ctx) {
WorldOutcome::ClickedObject(RouteID::Alt(idx)) => {
// Switch routes
app.session.routing_preferences = r.results.preferences;
app.session.routing_preferences = self.alt_routes[idx].preferences;
self.recalculate_routes(ctx, app);
self.update_input_panel(ctx, app);
return Transition::Keep;
}
_ => {}
}
let outcome = self.input_panel.event(ctx);
@ -181,7 +204,6 @@ impl State<App> for RoutePlanner {
stressful_roads: self.input_panel.dropdown_value("stressful roads"),
};
self.recalculate_routes(ctx, app);
self.update_input_panel(ctx, app);
return Transition::Keep;
}
}
@ -193,21 +215,12 @@ impl State<App> for RoutePlanner {
{
return t;
}
// Dragging behavior inside here only works if we're not hovering on an alternate route
// TODO But then that prevents dragging some waypoints! Can we give waypoints precedence
// instead?
if !focused_on_alt_route && self.waypoints.event(ctx, app, outcome) {
if self.waypoints.event(ctx, app, outcome) {
// Sync from waypoints to file management
// TODO Maaaybe this directly live in the InputWaypoints system?
self.files.current.waypoints = self.waypoints.get_waypoints();
self.recalculate_routes(ctx, app);
self.update_input_panel(ctx, app);
}
if focused_on_alt_route {
// Still allow zooming
if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
}
}
if let Some(t) = self.layers.event(ctx, app) {
@ -221,10 +234,8 @@ impl State<App> for RoutePlanner {
self.layers.draw(g, app);
self.input_panel.draw(g);
self.waypoints.draw(g);
self.main_route.draw(g, app, &self.input_panel);
for r in &self.alt_routes {
r.draw(g, app);
}
self.main_route.draw(g, &self.input_panel);
self.world.draw(g);
}
}

View File

@ -1,9 +1,10 @@
use std::collections::HashSet;
use geom::{Circle, Distance, Duration, FindClosest, PolyLine};
use map_gui::tools::{PopupMsg, ToggleZoomed};
use geom::{Circle, Distance, Duration, FindClosest, PolyLine, Polygon};
use map_gui::tools::PopupMsg;
use map_model::{Path, PathStep, NORMAL_LANE_THICKNESS};
use sim::{TripEndpoint, TripMode};
use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, LinePlot, Outcome, Panel, PlotOptions,
Series, Text, Widget,
@ -12,26 +13,31 @@ use widgetry::{
use super::RoutingPreferences;
use crate::app::{App, Transition};
pub struct RouteResults {
/// A temporary structure that the caller should unpack and use as needed.
pub struct BuiltRoute {
pub details: RouteDetails,
pub details_widget: Widget,
pub draw: ToggleZoomedBuilder,
pub hitbox: Polygon,
pub tooltip_for_alt: Option<Text>,
}
pub struct RouteDetails {
pub preferences: RoutingPreferences,
pub stats: RouteStats,
// It's tempting to glue together all of the paths. But since some waypoints might force the
// path to double back on itself, rendering the path as a single PolyLine would break.
paths: Vec<(Path, Option<PolyLine>)>,
// Match each polyline to the index in paths
closest_path_segment: FindClosest<usize>,
pub stats: RouteStats,
hover_on_line_plot: Option<(Distance, Drawable)>,
hover_on_route_tooltip: Option<Text>,
draw_route: ToggleZoomed,
draw_high_stress: Drawable,
draw_traffic_signals: Drawable,
draw_unprotected_turns: Drawable,
// Possibly a bit large to stash
elevation_pts: Vec<(Distance, Distance)>,
}
#[derive(PartialEq)]
@ -45,10 +51,10 @@ pub struct RouteStats {
total_down: Distance,
}
impl RouteResults {
impl RouteDetails {
/// "main" is determined by `app.session.routing_preferences`
pub fn main_route(ctx: &mut EventCtx, app: &App, waypoints: Vec<TripEndpoint>) -> RouteResults {
RouteResults::new(
pub fn main_route(ctx: &mut EventCtx, app: &App, waypoints: Vec<TripEndpoint>) -> BuiltRoute {
RouteDetails::new(
ctx,
app,
waypoints,
@ -58,15 +64,41 @@ impl RouteResults {
)
}
pub fn alt_route(
ctx: &mut EventCtx,
app: &App,
waypoints: Vec<TripEndpoint>,
main: &RouteDetails,
preferences: RoutingPreferences,
) -> BuiltRoute {
let mut built = RouteDetails::new(
ctx,
app,
waypoints,
Color::grey(0.3),
Some(Color::CYAN),
preferences,
);
built.tooltip_for_alt = Some(compare_routes(
app,
&main.stats,
&built.details.stats,
preferences,
));
built
}
fn new(
ctx: &mut EventCtx,
app: &App,
waypoints: Vec<TripEndpoint>,
route_color: Color,
// Only used for alts
outline_color: Option<Color>,
preferences: RoutingPreferences,
) -> RouteResults {
) -> BuiltRoute {
let mut draw_route = ToggleZoomed::builder();
let mut hitbox_pieces = Vec::new();
let mut draw_high_stress = GeomBatch::new();
let mut draw_traffic_signals = GeomBatch::new();
let mut draw_unprotected_turns = GeomBatch::new();
@ -135,7 +167,11 @@ impl RouteResults {
draw_route
.unzoomed
.push(route_color.alpha(0.8), shape.clone());
draw_route.zoomed.push(route_color.alpha(0.5), shape);
draw_route
.zoomed
.push(route_color.alpha(0.5), shape.clone());
hitbox_pieces.push(shape);
if let Some(color) = outline_color {
if let Some(outline) =
@ -162,27 +198,39 @@ impl RouteResults {
total_up += dy;
}
}
let stats = RouteStats {
total_distance,
dist_along_high_stress_roads,
total_time,
num_traffic_signals,
num_unprotected_turns,
total_up,
total_down,
};
RouteResults {
preferences,
draw_route: draw_route.build(ctx),
draw_high_stress: ctx.upload(draw_high_stress),
draw_traffic_signals: ctx.upload(draw_traffic_signals),
draw_unprotected_turns: ctx.upload(draw_unprotected_turns),
paths,
closest_path_segment,
hover_on_line_plot: None,
hover_on_route_tooltip: None,
elevation_pts,
stats: RouteStats {
total_distance,
dist_along_high_stress_roads,
total_time,
num_traffic_signals,
num_unprotected_turns,
total_up,
total_down,
let details_widget = make_detail_widget(ctx, app, &stats, elevation_pts);
BuiltRoute {
details: RouteDetails {
preferences,
draw_high_stress: ctx.upload(draw_high_stress),
draw_traffic_signals: ctx.upload(draw_traffic_signals),
draw_unprotected_turns: ctx.upload(draw_unprotected_turns),
paths,
closest_path_segment,
hover_on_line_plot: None,
hover_on_route_tooltip: None,
stats,
},
details_widget,
draw: draw_route,
hitbox: if hitbox_pieces.is_empty() {
// Dummy tiny hitbox
Polygon::rectangle(0.0001, 0.0001)
} else {
Polygon::union_all(hitbox_pieces)
},
tooltip_for_alt: None,
}
}
@ -293,8 +341,7 @@ impl RouteResults {
None
}
pub fn draw(&self, g: &mut GfxCtx, app: &App, panel: &Panel) {
self.draw_route.draw(g, app);
pub fn draw(&self, g: &mut GfxCtx, panel: &Panel) {
if let Some((_, ref draw)) = self.hover_on_line_plot {
g.redraw(draw);
}
@ -311,155 +358,99 @@ impl RouteResults {
g.redraw(&self.draw_unprotected_turns);
}
}
}
pub fn to_widget(&self, ctx: &mut EventCtx, app: &App) -> Widget {
let pct_stressful = if self.stats.total_distance == Distance::ZERO {
0.0
} else {
((self.stats.dist_along_high_stress_roads / self.stats.total_distance) * 100.0).round()
};
fn make_detail_widget(
ctx: &mut EventCtx,
app: &App,
stats: &RouteStats,
elevation_pts: Vec<(Distance, Distance)>,
) -> Widget {
let pct_stressful = if stats.total_distance == Distance::ZERO {
0.0
} else {
((stats.dist_along_high_stress_roads / stats.total_distance) * 100.0).round()
};
let elevation_plot = LinePlot::new_widget(
Widget::col(vec![
Line("Route details").small_heading().into_widget(ctx),
Text::from_all(vec![
Line("Distance: ").secondary(),
Line(stats.total_distance.to_string(&app.opts.units)),
])
.into_widget(ctx),
Widget::row(vec![
Text::from_all(vec![
Line(format!(
" {} or {}%",
stats
.dist_along_high_stress_roads
.to_string(&app.opts.units),
pct_stressful
)),
Line(" along ").secondary(),
])
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text("high-stress roads")
.build_def(ctx),
]),
Text::from_all(vec![
Line("Estimated time: ").secondary(),
Line(stats.total_time.to_string(&app.opts.units)),
])
.into_widget(ctx),
Widget::row(vec![
Line("Traffic signals crossed: ")
.secondary()
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text(stats.num_traffic_signals.to_string())
.build_widget(ctx, "traffic signals"),
]),
Widget::row(vec![
Line("Unprotected left turns onto busy roads: ")
.secondary()
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text(stats.num_unprotected_turns.to_string())
.build_widget(ctx, "unprotected turns"),
]),
Text::from_all(vec![
Line("Elevation change: ").secondary(),
Line(format!(
"{}↑, {}↓",
stats.total_up.to_string(&app.opts.units),
stats.total_down.to_string(&app.opts.units)
)),
])
.into_widget(ctx),
LinePlot::new_widget(
ctx,
"elevation",
vec![Series {
label: "Elevation".to_string(),
color: Color::RED,
pts: self.elevation_pts.clone(),
pts: elevation_pts,
}],
PlotOptions {
filterable: false,
max_x: Some(self.stats.total_distance.round_up_for_axis()),
max_x: Some(stats.total_distance.round_up_for_axis()),
max_y: Some(app.primary.map.max_elevation().round_up_for_axis()),
disabled: HashSet::new(),
},
app.opts.units,
);
Widget::col(vec![
Line("Route details").small_heading().into_widget(ctx),
Text::from_all(vec![
Line("Distance: ").secondary(),
Line(self.stats.total_distance.to_string(&app.opts.units)),
])
.into_widget(ctx),
Widget::row(vec![
Text::from_all(vec![
Line(format!(
" {} or {}%",
self.stats
.dist_along_high_stress_roads
.to_string(&app.opts.units),
pct_stressful
)),
Line(" along ").secondary(),
])
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text("high-stress roads")
.build_def(ctx),
]),
Text::from_all(vec![
Line("Estimated time: ").secondary(),
Line(self.stats.total_time.to_string(&app.opts.units)),
])
.into_widget(ctx),
Widget::row(vec![
Line("Traffic signals crossed: ")
.secondary()
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text(self.stats.num_traffic_signals.to_string())
.build_widget(ctx, "traffic signals"),
]),
Widget::row(vec![
Line("Unprotected left turns onto busy roads: ")
.secondary()
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_plain
.btn()
.label_underlined_text(self.stats.num_unprotected_turns.to_string())
.build_widget(ctx, "unprotected turns"),
]),
Text::from_all(vec![
Line("Elevation change: ").secondary(),
Line(format!(
"{}↑, {}↓",
self.stats.total_up.to_string(&app.opts.units),
self.stats.total_down.to_string(&app.opts.units)
)),
])
.into_widget(ctx),
elevation_plot,
])
}
}
pub struct AltRouteResults {
pub results: RouteResults,
hovering: bool,
tooltip: Text,
}
impl AltRouteResults {
pub fn new(
ctx: &mut EventCtx,
app: &App,
waypoints: Vec<TripEndpoint>,
main: &RouteResults,
preferences: RoutingPreferences,
) -> AltRouteResults {
let results = RouteResults::new(
ctx,
app,
waypoints,
Color::grey(0.3),
Some(Color::CYAN),
preferences,
);
let tooltip = compare_routes(app, &main.stats, &results.stats, preferences);
AltRouteResults {
results,
hovering: false,
tooltip,
}
}
pub fn has_focus(&self) -> bool {
self.hovering
}
pub fn event(&mut self, ctx: &mut EventCtx) {
if ctx.redo_mouseover() {
self.hovering = false;
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
if self
.results
.closest_path_segment
.closest_pt(pt, 10.0 * NORMAL_LANE_THICKNESS)
.is_some()
{
self.hovering = true;
}
}
}
}
pub fn draw(&self, g: &mut GfxCtx, app: &App) {
self.results.draw_route.draw(g, app);
if self.hovering {
g.draw_mouse_tooltip(self.tooltip.clone());
}
}
),
])
}
fn compare_routes(

View File

@ -30,9 +30,6 @@ pub struct Options {
pub color_scheme: ColorSchemeChoice,
/// Automatically change color_scheme based on simulation time to reflect day/night
pub toggle_day_night_colors: bool,
/// Map elements are drawn differently when unzoomed and zoomed. This specifies the canvas zoom
/// level where they switch.
pub min_zoom_for_detail: f64,
/// Draw buildings in different perspectives
pub camera_angle: CameraAngle,
/// Draw building driveways.
@ -80,7 +77,6 @@ impl Options {
traffic_signal_style: TrafficSignalStyle::Brian,
color_scheme: ColorSchemeChoice::DayMode,
toggle_day_night_colors: false,
min_zoom_for_detail: 4.0,
camera_angle: CameraAngle::TopDown,
show_building_driveways: true,
@ -103,7 +99,7 @@ impl Options {
pub fn update_from_args(&mut self, args: &mut CmdArgs) {
self.dev = args.enabled("--dev");
if args.enabled("--lowzoom") {
self.min_zoom_for_detail = 1.0;
self.canvas_settings.min_zoom_for_detail = 1.0;
}
if let Some(x) = args.optional("--color_scheme") {
let mut ok = false;
@ -257,7 +253,7 @@ impl OptionsPanel {
Widget::dropdown(
ctx,
"min zoom",
app.opts().min_zoom_for_detail,
ctx.canvas.settings.min_zoom_for_detail,
vec![
Choice::new("1.0", 1.0),
Choice::new("2.0", 2.0),
@ -347,6 +343,7 @@ impl<A: AppLike> State<A> for OptionsPanel {
ctx.canvas.settings.gui_scroll_speed = self.panel.spinner("gui_scroll_speed");
ctx.canvas.settings.canvas_scroll_speed =
self.panel.spinner("canvas_scroll_speed");
ctx.canvas.settings.min_zoom_for_detail = self.panel.dropdown_value("min zoom");
// Copy the settings into the Options struct, so they're saved.
opts.canvas_settings = ctx.canvas.settings.clone();
@ -398,7 +395,6 @@ impl<A: AppLike> State<A> for OptionsPanel {
opts.toggle_day_night_colors = false;
}
opts.min_zoom_for_detail = self.panel.dropdown_value("min zoom");
opts.units.metric = self.panel.is_checked("metric / imperial units");
let language = self.panel.dropdown_value("language");

View File

@ -37,6 +37,7 @@ impl<T: 'static> SimpleApp<T> {
) -> (SimpleApp<T>, Vec<Box<dyn State<SimpleApp<T>>>>) {
let mut args = CmdArgs::new();
opts.update_from_args(&mut args);
ctx.canvas.settings = opts.canvas_settings.clone();
let map_name = args
.optional_free()
.map(|path| {
@ -169,9 +170,7 @@ impl<T: 'static> SimpleApp<T> {
unzoomed_buildings: bool,
) -> Option<ID> {
// Unzoomed mode. Ignore when debugging areas.
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail
&& !(unzoomed_roads_and_intersections || unzoomed_buildings)
{
if ctx.canvas.is_unzoomed() && !(unzoomed_roads_and_intersections || unzoomed_buildings) {
return None;
}
@ -186,26 +185,22 @@ impl<T: 'static> SimpleApp<T> {
for obj in objects {
match obj.get_id() {
ID::Road(_) => {
if !unzoomed_roads_and_intersections
|| ctx.canvas.cam_zoom >= self.opts.min_zoom_for_detail
{
if !unzoomed_roads_and_intersections || ctx.canvas.is_zoomed() {
continue;
}
}
ID::Intersection(_) => {
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail
&& !unzoomed_roads_and_intersections
{
if ctx.canvas.is_unzoomed() && !unzoomed_roads_and_intersections {
continue;
}
}
ID::Building(_) => {
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail && !unzoomed_buildings {
if ctx.canvas.is_unzoomed() && !unzoomed_buildings {
continue;
}
}
_ => {
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
continue;
}
}
@ -260,7 +255,7 @@ impl<T: 'static> AppLike for SimpleApp<T> {
}
fn draw_with_opts(&self, g: &mut GfxCtx, opts: DrawOptions) {
if g.canvas.cam_zoom < self.opts.min_zoom_for_detail {
if g.canvas.is_unzoomed() {
self.draw_unzoomed(g);
} else {
self.draw_zoomed(g, opts);

View File

@ -3,9 +3,9 @@ use std::collections::HashMap;
use abstutil::Counter;
use geom::{Circle, Distance, Line, Polygon, Pt2D};
use map_model::{BuildingID, BusStopID, IntersectionID, LaneID, Map, ParkingLotID, RoadID};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, EventCtx, Fill, GeomBatch, Line, LinearGradient, Text, Widget};
use crate::tools::ToggleZoomed;
use crate::AppLike;
pub struct ColorDiscrete<'a> {

View File

@ -1,52 +0,0 @@
use widgetry::{Drawable, EventCtx, GeomBatch, GfxCtx};
use crate::AppLike;
/// Draws one of two versions of something, based on whether the app is zoomed in past a threshold.
pub struct ToggleZoomed {
// Some callers access directly for minimaps
pub unzoomed: Drawable,
pub zoomed: Drawable,
}
impl ToggleZoomed {
pub fn new(ctx: &EventCtx, unzoomed: GeomBatch, zoomed: GeomBatch) -> ToggleZoomed {
ToggleZoomed {
unzoomed: ctx.upload(unzoomed),
zoomed: ctx.upload(zoomed),
}
}
pub fn empty(ctx: &EventCtx) -> ToggleZoomed {
ToggleZoomed {
unzoomed: Drawable::empty(ctx),
zoomed: Drawable::empty(ctx),
}
}
pub fn builder() -> ToggleZoomedBuilder {
ToggleZoomedBuilder {
unzoomed: GeomBatch::new(),
zoomed: GeomBatch::new(),
}
}
pub fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike) {
if g.canvas.cam_zoom < app.opts().min_zoom_for_detail {
g.redraw(&self.unzoomed);
} else {
g.redraw(&self.zoomed);
}
}
}
pub struct ToggleZoomedBuilder {
pub unzoomed: GeomBatch,
pub zoomed: GeomBatch,
}
impl ToggleZoomedBuilder {
pub fn build(self, ctx: &EventCtx) -> ToggleZoomed {
ToggleZoomed::new(ctx, self.unzoomed, self.zoomed)
}
}

View File

@ -81,7 +81,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
dragging: false,
panel: Panel::empty(ctx),
zoomed: ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail,
zoomed: ctx.canvas.is_zoomed(),
layer,
zoom_lvl: 0,
@ -98,7 +98,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
}
pub fn recreate_panel(&mut self, ctx: &mut EventCtx, app: &A) {
if ctx.canvas.cam_zoom < app.opts().min_zoom_for_detail {
if ctx.canvas.is_unzoomed() {
self.panel = self.controls.make_unzoomed_panel(ctx, app);
return;
}
@ -271,7 +271,7 @@ impl<A: AppLike + 'static, T: MinimapControls<A>> Minimap<A, T> {
self.recreate_panel(ctx, app);
}
let zoomed = ctx.canvas.cam_zoom >= app.opts().min_zoom_for_detail;
let zoomed = ctx.canvas.is_zoomed();
let layer = self.controls.has_layer(app);
if zoomed != self.zoomed || layer != self.layer {
let just_zoomed_in = zoomed && !self.zoomed;

View File

@ -7,7 +7,6 @@ use widgetry::{lctrl, EventCtx, GfxCtx, Key, Line, Text, Widget};
pub use self::camera::{CameraState, DefaultMap};
pub use self::city_picker::CityPicker;
pub use self::colors::{ColorDiscrete, ColorLegend, ColorNetwork, ColorScale, DivergingScale};
pub use self::draw::{ToggleZoomed, ToggleZoomedBuilder};
pub use self::heatmap::{draw_isochrone, make_heatmap, Grid, HeatmapOptions};
pub use self::icons::{goal_marker, start_marker};
pub use self::minimap::{Minimap, MinimapControls};
@ -27,7 +26,6 @@ mod city_picker;
mod colors;
#[cfg(not(target_arch = "wasm32"))]
mod command;
mod draw;
mod heatmap;
mod icons;
#[cfg(not(target_arch = "wasm32"))]

View File

@ -148,7 +148,7 @@ impl<A: AppLike + 'static> State<A> for CrossStreet {
return Transition::Replace(app.make_warper(
ctx,
pt,
Some(app.opts().min_zoom_for_detail),
Some(ctx.canvas.settings.min_zoom_for_detail),
None,
));
}
@ -172,7 +172,7 @@ impl<A: AppLike + 'static> State<A> for CrossStreet {
return Transition::Replace(app.make_warper(
ctx,
pt,
Some(app.opts().min_zoom_for_detail),
Some(ctx.canvas.settings.min_zoom_for_detail),
Some(ID::Intersection(i)),
));
} else {
@ -273,7 +273,7 @@ impl<A: AppLike + 'static> State<A> for SearchBuildings {
return Transition::Replace(app.make_warper(
ctx,
pt,
Some(app.opts().min_zoom_for_detail),
Some(ctx.canvas.settings.min_zoom_for_detail),
Some(ID::Building(bldgs[0])),
));
}

View File

@ -353,7 +353,7 @@ impl State<App> for Viewer {
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
if g.canvas.is_unzoomed() {
app.draw_unzoomed(g);
} else {
app.draw_zoomed(g, DrawOptions::new());

View File

@ -5,7 +5,7 @@ mod mapper;
fn main() {
let mut options = map_gui::options::Options::load_or_default();
options.min_zoom_for_detail = 2.0;
options.canvas_settings.min_zoom_for_detail = 2.0;
let settings = widgetry::Settings::new("OSM parking mapper")
.read_svg(Box::new(abstio::slurp_bytes))
.canvas_settings(options.canvas_settings.clone());

View File

@ -477,7 +477,7 @@ impl State<App> for Game {
if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
ctx.canvas.cam_zoom = 1.1_f64
.powf(ctx.canvas.cam_zoom.log(1.1) + dy)
.max(app.opts.min_zoom_for_detail)
.max(ctx.canvas.settings.min_zoom_for_detail)
.min(50.0);
ctx.canvas.center_on_map_pt(self.player.get_pos());
}

View File

@ -54,6 +54,10 @@ pub struct CanvasSettings {
// TODO Ideally this would be an f64, but elsewhere we use it in a Spinner. Until we override
// the Display trait to do some rounding, floating point increments render pretty horribly.
pub canvas_scroll_speed: usize,
/// Some map-space elements are drawn differently when unzoomed and zoomed. This specifies the canvas
/// zoom level where they switch. The concept of "unzoomed" and "zoomed" is used by
/// `ToggleZoomed`.
pub min_zoom_for_detail: f64,
}
impl CanvasSettings {
@ -65,6 +69,7 @@ impl CanvasSettings {
keys_to_pan: false,
gui_scroll_speed: 5,
canvas_scroll_speed: 10,
min_zoom_for_detail: 4.0,
}
}
}
@ -353,6 +358,14 @@ impl Canvas {
};
ScreenPt::new(x1, y1)
}
pub fn is_unzoomed(&self) -> bool {
self.cam_zoom < self.settings.min_zoom_for_detail
}
pub fn is_zoomed(&self) -> bool {
self.cam_zoom >= self.settings.min_zoom_for_detail
}
}
const INSET: f64 = 16.0;

View File

@ -0,0 +1,85 @@
mod world;
use crate::{Drawable, EventCtx, GeomBatch, GfxCtx, RewriteColor};
pub use world::{DummyID, ObjectID, World, WorldOutcome};
/// Draws one of two versions of something, based on whether the canvas is zoomed in past a threshold.
pub struct ToggleZoomed {
// Some callers access directly for minimaps
pub unzoomed: Drawable,
pub zoomed: Drawable,
// Draw the same thing whether zoomed or unzoomed
just_unzoomed: bool,
}
impl ToggleZoomed {
pub fn new(ctx: &EventCtx, unzoomed: GeomBatch, zoomed: GeomBatch) -> ToggleZoomed {
ToggleZoomed {
unzoomed: ctx.upload(unzoomed),
zoomed: ctx.upload(zoomed),
just_unzoomed: false,
}
}
pub fn empty(ctx: &EventCtx) -> ToggleZoomed {
ToggleZoomed {
unzoomed: Drawable::empty(ctx),
zoomed: Drawable::empty(ctx),
just_unzoomed: false,
}
}
pub fn builder() -> ToggleZoomedBuilder {
ToggleZoomedBuilder {
unzoomed: GeomBatch::new(),
zoomed: GeomBatch::new(),
just_unzoomed: false,
}
}
pub fn draw(&self, g: &mut GfxCtx) {
if self.just_unzoomed || g.canvas.cam_zoom < g.canvas.settings.min_zoom_for_detail {
g.redraw(&self.unzoomed);
} else {
g.redraw(&self.zoomed);
}
}
}
#[derive(Clone)]
pub struct ToggleZoomedBuilder {
pub unzoomed: GeomBatch,
pub zoomed: GeomBatch,
just_unzoomed: bool,
}
impl ToggleZoomedBuilder {
/// Transforms all colors in both batches.
pub fn color(mut self, transformation: RewriteColor) -> Self {
self.unzoomed = self.unzoomed.color(transformation);
self.zoomed = self.zoomed.color(transformation);
self
}
pub fn build(self, ctx: &EventCtx) -> ToggleZoomed {
if self.just_unzoomed {
assert!(self.zoomed.is_empty());
}
ToggleZoomed {
unzoomed: ctx.upload(self.unzoomed),
zoomed: ctx.upload(self.zoomed),
just_unzoomed: self.just_unzoomed,
}
}
}
// Drawing just one batch means the same thing will appear whether zoomed or unzoomed
impl std::convert::From<GeomBatch> for ToggleZoomedBuilder {
fn from(unzoomed: GeomBatch) -> Self {
Self {
unzoomed,
zoomed: GeomBatch::new(),
just_unzoomed: true,
}
}
}

View File

@ -6,12 +6,14 @@ use aabb_quadtree::{ItemId, QuadTree};
use geom::{Bounds, Circle, Distance, Polygon, Pt2D};
use crate::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, MultiKey, RewriteColor, Text};
use crate::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
use crate::{Color, EventCtx, GeomBatch, GfxCtx, MultiKey, RewriteColor, Text};
// TODO Tests...
// - start drag in screenspace, release in map
// - start drag in mapspace, release in screen
// - reset hovering when we go out of screenspace
// - start dragging one object, and while dragging, hover on top of other objects
/// A `World` manages objects that exist in "map-space", the zoomable and pannable canvas. These
/// objects can be drawn, hovered on, clicked, dragged, etc.
@ -20,7 +22,7 @@ pub struct World<ID: ObjectID> {
objects: HashMap<ID, Object<ID>>,
quadtree: QuadTree<ID>,
draw_master_batches: Vec<Drawable>,
draw_master_batches: Vec<ToggleZoomed>,
hovering: Option<ID>,
// If we're currently dragging, where was the cursor during the last movement, and has the
@ -33,7 +35,13 @@ pub enum WorldOutcome<ID: ObjectID> {
/// A left click occurred while not hovering on any object
ClickedFreeSpace(Pt2D),
/// An object is being dragged. The given offsets are relative to the previous dragging event.
Dragging { obj: ID, dx: f64, dy: f64 },
/// The current position of the cursor is included.
Dragging {
obj: ID,
dx: f64,
dy: f64,
cursor: Pt2D,
},
/// While hovering on an object with a defined hotkey, that key was pressed.
Keypress(&'static str, ID),
/// A hoverable object was clicked
@ -52,8 +60,8 @@ pub struct ObjectBuilder<'a, ID: ObjectID> {
id: ID,
hitbox: Option<Polygon>,
zorder: usize,
draw_normal: Option<GeomBatch>,
draw_hover: Option<GeomBatch>,
draw_normal: Option<ToggleZoomedBuilder>,
draw_hover: Option<ToggleZoomedBuilder>,
tooltip: Option<Text>,
clickable: bool,
draggable: bool,
@ -76,12 +84,12 @@ impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
}
/// Specifies how to draw this object normally (while not hovering on it)
pub fn draw(mut self, batch: GeomBatch) -> Self {
pub fn draw<I: Into<ToggleZoomedBuilder>>(mut self, normal: I) -> Self {
assert!(
self.draw_normal.is_none(),
"already specified how to draw normally"
);
self.draw_normal = Some(batch);
self.draw_normal = Some(normal.into());
self
}
@ -102,23 +110,28 @@ impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
/// Specifies how to draw the object while the cursor is hovering on it. Note that an object
/// isn't considered hoverable unless this is specified!
pub fn draw_hovered(mut self, batch: GeomBatch) -> Self {
pub fn draw_hovered<I: Into<ToggleZoomedBuilder>>(mut self, hovered: I) -> Self {
assert!(
self.draw_hover.is_none(),
"already specified how to draw hovered"
);
self.draw_hover = Some(batch);
self.draw_hover = Some(hovered.into());
self
}
/// Draw the object in a hovered state by transforming the normal drawing.
pub fn draw_hover_rewrite(self, rewrite: RewriteColor) -> Self {
let hovered = self
.draw_normal
.clone()
.expect("first specify how to draw normally")
.color(rewrite);
self.draw_hovered(hovered)
}
/// Draw the object in a hovered state by changing the alpha value of the normal drawing.
pub fn hover_alpha(self, alpha: f32) -> Self {
let batch = self
.draw_normal
.clone()
.expect("first specify how to draw normally")
.color(RewriteColor::ChangeAlpha(alpha));
self.draw_hovered(batch)
self.draw_hover_rewrite(RewriteColor::ChangeAlpha(alpha))
}
/// Draw a tooltip while hovering over this object.
@ -176,11 +189,11 @@ impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
_quadtree_id: quadtree_id,
hitbox,
zorder: self.zorder,
draw_normal: ctx.upload(
self.draw_normal
.expect("didn't specify how to draw normally"),
),
draw_hover: self.draw_hover.take().map(|batch| ctx.upload(batch)),
draw_normal: self
.draw_normal
.expect("didn't specify how to draw normally")
.build(ctx),
draw_hover: self.draw_hover.take().map(|draw| draw.build(ctx)),
tooltip: self.tooltip,
clickable: self.clickable,
draggable: self.draggable,
@ -195,8 +208,8 @@ struct Object<ID: ObjectID> {
_quadtree_id: ItemId,
hitbox: Polygon,
zorder: usize,
draw_normal: Drawable,
draw_hover: Option<Drawable>,
draw_normal: ToggleZoomed,
draw_hover: Option<ToggleZoomed>,
tooltip: Option<Text>,
clickable: bool,
draggable: bool,
@ -265,14 +278,23 @@ impl<ID: ObjectID> World<ID> {
/// If a drag event causes the world to be totally rebuilt, call this with the previous world
/// to preserve the ongoing drag.
///
/// This should be called after `initialize_hover`.
///
/// Important: the rebuilt world must include the same object ID that's currently being dragged
/// from the previous world.
pub fn rebuilt_during_drag(&mut self, prev_world: &World<ID>) {
self.dragging_from = prev_world.dragging_from;
if prev_world.dragging_from.is_some() {
self.dragging_from = prev_world.dragging_from;
self.hovering = prev_world.hovering;
assert!(self.objects.contains_key(self.hovering.as_ref().unwrap()));
}
}
/// Draw something underneath all objects. This is useful for performance, when a large number
/// of objects never change appearance.
pub fn draw_master_batch(&mut self, draw: Drawable) {
self.draw_master_batches.push(draw);
pub fn draw_master_batch<I: Into<ToggleZoomedBuilder>>(&mut self, ctx: &EventCtx, draw: I) {
self.draw_master_batches.push(draw.into().build(ctx));
}
/// Let objects in the world respond to something happening.
@ -306,6 +328,7 @@ impl<ID: ObjectID> World<ID> {
obj: self.hovering.unwrap(),
dx,
dy,
cursor,
};
}
}
@ -393,7 +416,7 @@ impl<ID: ObjectID> World<ID> {
pub fn draw(&self, g: &mut GfxCtx) {
// Always draw master batches first
for draw in &self.draw_master_batches {
g.redraw(draw);
draw.draw(g);
}
let mut objects = Vec::new();
@ -407,7 +430,7 @@ impl<ID: ObjectID> World<ID> {
let obj = &self.objects[&id];
if Some(id) == self.hovering {
if let Some(ref draw) = obj.draw_hover {
g.redraw(draw);
draw.draw(g);
drawn = true;
}
if let Some(ref txt) = obj.tooltip {
@ -415,7 +438,7 @@ impl<ID: ObjectID> World<ID> {
}
}
if !drawn {
g.redraw(&obj.draw_normal);
obj.draw_normal.draw(g);
}
}
}