expose some settings for interactively tuning the dot / heat map of people. this way is buggy (controls constantly being recreated), but a start.

This commit is contained in:
Dustin Carlino 2020-03-21 17:42:17 -07:00
parent 4ad75f99a1
commit acbeb2b499
5 changed files with 202 additions and 51 deletions

View File

@ -33,7 +33,8 @@ enum WidgetType {
Slider(String), Slider(String),
Menu(String), Menu(String),
Filler(String), Filler(String),
// TODO Sadness. Can't have some kind of wildcard generic here? // TODO Sadness. Can't have some kind of wildcard generic here? I think this goes away when
// WidgetType becomes a trait.
DurationPlot(Plot<Duration>), DurationPlot(Plot<Duration>),
UsizePlot(Plot<usize>), UsizePlot(Plot<usize>),
Histogram(Histogram), Histogram(Histogram),
@ -1007,6 +1008,9 @@ impl Composite {
pub fn slider(&self, name: &str) -> &Slider { pub fn slider(&self, name: &str) -> &Slider {
&self.sliders[name] &self.sliders[name]
} }
pub fn maybe_slider(&self, name: &str) -> Option<&Slider> {
self.sliders.get(name)
}
pub fn slider_mut(&mut self, name: &str) -> &mut Slider { pub fn slider_mut(&mut self, name: &str) -> &mut Slider {
self.sliders.get_mut(name).unwrap() self.sliders.get_mut(name).unwrap()
} }
@ -1029,10 +1033,16 @@ impl Composite {
} }
} }
// TODO This invalidates the dropdown! pub fn dropdown_value<T: 'static + Clone>(&mut self, name: &str) -> T {
pub fn dropdown_value<T: 'static>(&mut self, name: &str) -> T {
match self.find_mut(name).widget { match self.find_mut(name).widget {
WidgetType::Dropdown(ref mut dropdown) => dropdown.take_value(), WidgetType::Dropdown(ref mut dropdown) => {
// Amusing little pattern here.
// TODO I think this entire hack goes away when WidgetImpl is just a trait.
let choice: Choice<T> = dropdown.take_value();
let value = choice.data.clone();
dropdown.return_value(choice);
value
}
_ => panic!("{} isn't a dropdown", name), _ => panic!("{} isn't a dropdown", name),
} }
} }

View File

@ -105,11 +105,31 @@ impl Dropdown {
} }
} }
// TODO This invalidates the entire widget! // TODO This invalidates the entire widget! Have to call return_value
pub fn take_value<T: 'static>(&mut self) -> T { pub fn take_value<T: 'static>(&mut self) -> Choice<T> {
let data: Box<dyn Any> = self.choices.remove(self.current_idx).data; let c = self.choices.remove(self.current_idx);
let data: Box<dyn Any> = c.data;
let boxed: Box<T> = data.downcast().unwrap(); let boxed: Box<T> = data.downcast().unwrap();
*boxed Choice {
label: c.label,
data: *boxed,
hotkey: c.hotkey,
active: c.active,
tooltip: c.tooltip,
}
}
pub fn return_value<T: 'static>(&mut self, c: Choice<T>) {
let data: Box<dyn Any> = Box::new(c.data);
self.choices.insert(
self.current_idx,
Choice {
label: c.label,
data,
hotkey: c.hotkey,
active: c.active,
tooltip: c.tooltip,
},
);
} }
} }

View File

@ -1,28 +1,58 @@
use ezgui::{Color, GeomBatch}; use ezgui::{Choice, Color, GeomBatch};
use geom::{Bounds, Polygon, Pt2D}; use geom::{Bounds, Polygon, Pt2D};
pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) { #[derive(Clone, PartialEq)]
// Meters pub struct HeatmapOptions {
let resolution = 10.0; // In meters
pub resolution: f64,
pub num_passes: usize,
pub colors: HeatmapColors,
}
impl HeatmapOptions {
pub fn new() -> HeatmapOptions {
HeatmapOptions {
resolution: 10.0,
num_passes: 5,
colors: HeatmapColors::FullSpectral,
}
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum HeatmapColors {
FullSpectral,
SingleHue,
}
impl HeatmapColors {
pub fn choices() -> Vec<Choice<HeatmapColors>> {
vec![
Choice::new("full spectral", HeatmapColors::FullSpectral),
Choice::new("single hue", HeatmapColors::SingleHue),
]
}
}
pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>, opts: &HeatmapOptions) {
// u8 is not quite enough -- one building could totally have more than 256 people. // u8 is not quite enough -- one building could totally have more than 256 people.
let mut counts: Grid<u16> = Grid::new( let mut counts: Grid<u16> = Grid::new(
(bounds.width() / resolution).ceil() as usize, (bounds.width() / opts.resolution).ceil() as usize,
(bounds.height() / resolution).ceil() as usize, (bounds.height() / opts.resolution).ceil() as usize,
0, 0,
); );
for pt in pts { for pt in pts {
// TODO more careful rounding // TODO more careful rounding
let idx = counts.idx( let idx = counts.idx(
((pt.x() - bounds.min_x) / resolution) as usize, ((pt.x() - bounds.min_x) / opts.resolution) as usize,
((pt.y() - bounds.min_y) / resolution) as usize, ((pt.y() - bounds.min_y) / opts.resolution) as usize,
); );
counts.data[idx] += 1; counts.data[idx] += 1;
} }
// Diffusion // Diffusion
let num_passes = 5; for _ in 0..opts.num_passes {
for _ in 0..num_passes {
// Have to hot-swap! Urgh // Have to hot-swap! Urgh
let mut copy = counts.data.clone(); let mut copy = counts.data.clone();
for y in 0..counts.height { for y in 0..counts.height {
@ -41,9 +71,9 @@ pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) {
// Now draw rectangles // Now draw rectangles
let max = *counts.data.iter().max().unwrap(); let max = *counts.data.iter().max().unwrap();
// TODO Full spectral progression isn't recommended anymore!
// This is in order from low density to high. // This is in order from low density to high.
let colors = vec![ let colors = match opts.colors {
HeatmapColors::FullSpectral => vec![
Color::hex("#0b2c7a"), Color::hex("#0b2c7a"),
Color::hex("#1e9094"), Color::hex("#1e9094"),
Color::hex("#0ec441"), Color::hex("#0ec441"),
@ -51,14 +81,24 @@ pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) {
Color::hex("#f7d707"), Color::hex("#f7d707"),
Color::hex("#e68e1c"), Color::hex("#e68e1c"),
Color::hex("#c2523c"), Color::hex("#c2523c"),
]; ],
HeatmapColors::SingleHue => vec![
Color::hex("#FFEBD6"),
Color::hex("#F5CBAE"),
Color::hex("#EBA988"),
Color::hex("#E08465"),
Color::hex("#D65D45"),
Color::hex("#CC3527"),
Color::hex("#C40A0A"),
],
};
// TODO Off by 1? // TODO Off by 1?
let range = max / ((colors.len() - 1) as u16); let range = max / ((colors.len() - 1) as u16);
if range == 0 { if range == 0 {
// Max is too low, use less colors? // Max is too low, use less colors?
return; return;
} }
let square = Polygon::rectangle(resolution, resolution); let square = Polygon::rectangle(opts.resolution, opts.resolution);
for y in 0..counts.height { for y in 0..counts.height {
for x in 0..counts.width { for x in 0..counts.width {
let idx = counts.idx(x, y); let idx = counts.idx(x, y);
@ -68,7 +108,7 @@ pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) {
let color = colors[((cnt / range) as usize).min(colors.len() - 1)]; let color = colors[((cnt / range) as usize).min(colors.len() - 1)];
batch.push( batch.push(
color, color,
square.translate((x as f64) * resolution, (y as f64) * resolution), square.translate((x as f64) * opts.resolution, (y as f64) * opts.resolution),
); );
} }
} }

View File

@ -12,7 +12,7 @@ mod warp;
pub use self::bus_explorer::ShowBusRoute; pub use self::bus_explorer::ShowBusRoute;
pub use self::colors::{ColorLegend, Colorer}; pub use self::colors::{ColorLegend, Colorer};
pub use self::heatmap::make_heatmap; pub use self::heatmap::{make_heatmap, HeatmapColors, HeatmapOptions};
pub use self::minimap::Minimap; pub use self::minimap::Minimap;
pub use self::overlays::Overlays; pub use self::overlays::Overlays;
pub use self::panels::tool_panel; pub use self::panels::tool_panel;

View File

@ -1,6 +1,8 @@
use crate::app::App; use crate::app::App;
use crate::colors; use crate::colors;
use crate::common::{make_heatmap, ColorLegend, Colorer, ShowBusRoute, Warping}; use crate::common::{
make_heatmap, ColorLegend, Colorer, HeatmapColors, HeatmapOptions, ShowBusRoute, Warping,
};
use crate::game::Transition; use crate::game::Transition;
use crate::helpers::rotating_color_map; use crate::helpers::rotating_color_map;
use crate::helpers::ID; use crate::helpers::ID;
@ -10,7 +12,7 @@ use abstutil::{prettyprint_usize, Counter};
use ezgui::{ use ezgui::{
hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, Histogram, hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, Histogram,
HorizontalAlignment, JustDraw, Key, Line, Outcome, Plot, PlotOptions, RewriteColor, Series, HorizontalAlignment, JustDraw, Key, Line, Outcome, Plot, PlotOptions, RewriteColor, Series,
Text, TextExt, VerticalAlignment, Widget, Slider, Text, TextExt, VerticalAlignment, Widget,
}; };
use geom::{Circle, Distance, Duration, PolyLine, Polygon, Pt2D, Statistic, Time}; use geom::{Circle, Distance, Duration, PolyLine, Polygon, Pt2D, Statistic, Time};
use map_model::{BusRouteID, IntersectionID}; use map_model::{BusRouteID, IntersectionID};
@ -28,7 +30,7 @@ pub enum Overlays {
Elevation(Colorer, Drawable), Elevation(Colorer, Drawable),
Edits(Colorer), Edits(Colorer),
TripsHistogram(Time, Composite), TripsHistogram(Time, Composite),
PersonDotMap(Time, Drawable), PopulationMap(Time, Option<HeatmapOptions>, Drawable, Composite),
// These aren't selectable from the main picker // These aren't selectable from the main picker
IntersectionDemand(Time, IntersectionID, Drawable, Composite), IntersectionDemand(Time, IntersectionID, Drawable, Composite),
@ -94,9 +96,9 @@ impl Overlays {
app.overlay = Overlays::bus_passengers(id, ctx, app); app.overlay = Overlays::bus_passengers(id, ctx, app);
} }
} }
Overlays::PersonDotMap(t, _) => { Overlays::PopulationMap(t, ref opts, _, _) => {
if now != t { if now != t {
app.overlay = Overlays::person_dot_map(ctx, app); app.overlay = Overlays::population_map(ctx, app, opts.clone());
} }
} }
// No updates needed // No updates needed
@ -193,10 +195,25 @@ impl Overlays {
} }
} }
} }
Overlays::PersonDotMap(_, _) => { Overlays::PopulationMap(_, ref mut opts, _, ref mut c) => {
// TODO No controls or legend at all? c.align_above(ctx, minimap);
match c.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
app.overlay = Overlays::Inactive;
}
_ => unreachable!(),
},
None => {
let new_opts = heatmap_options(c);
if *opts != new_opts {
app.overlay = Overlays::population_map(ctx, app, new_opts);
} else {
app.overlay = orig_overlay; app.overlay = orig_overlay;
} }
}
}
}
Overlays::Inactive => {} Overlays::Inactive => {}
} }
@ -224,9 +241,10 @@ impl Overlays {
g.redraw(draw); g.redraw(draw);
} }
} }
Overlays::PersonDotMap(_, ref draw) => { Overlays::PopulationMap(_, _, ref draw, ref composite) => {
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL { if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
g.redraw(draw); g.redraw(draw);
composite.draw(g);
} }
} }
// All of these shouldn't care about zoom // All of these shouldn't care about zoom
@ -274,10 +292,8 @@ impl Overlays {
Btn::text_fg("throughput").build_def(ctx, hotkey(Key::T)), Btn::text_fg("throughput").build_def(ctx, hotkey(Key::T)),
Btn::text_fg("bike network").build_def(ctx, hotkey(Key::B)), Btn::text_fg("bike network").build_def(ctx, hotkey(Key::B)),
Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)), Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)),
Btn::text_fg("population map").build_def(ctx, hotkey(Key::X)),
]; ];
if app.opts.dev {
choices.push(Btn::text_fg("dot map of people").build_def(ctx, hotkey(Key::X)));
}
// TODO Grey out the inactive SVGs, and add the green checkmark // TODO Grey out the inactive SVGs, and add the green checkmark
if let Some(name) = match app.overlay { if let Some(name) = match app.overlay {
Overlays::Inactive => Some("None"), Overlays::Inactive => Some("None"),
@ -289,7 +305,7 @@ impl Overlays {
Overlays::BusNetwork(_) => Some("bus network"), Overlays::BusNetwork(_) => Some("bus network"),
Overlays::Elevation(_, _) => Some("elevation"), Overlays::Elevation(_, _) => Some("elevation"),
Overlays::Edits(_) => Some("map edits"), Overlays::Edits(_) => Some("map edits"),
Overlays::PersonDotMap(_, _) => Some("dot map of people"), Overlays::PopulationMap(_, _, _, _) => Some("population map"),
_ => None, _ => None,
} { } {
for btn in &mut choices { for btn in &mut choices {
@ -384,9 +400,9 @@ impl Overlays {
}), }),
) )
.maybe_cb( .maybe_cb(
"dot map of people", "population map",
Box::new(|ctx, app| { Box::new(|ctx, app| {
app.overlay = Overlays::person_dot_map(ctx, app); app.overlay = Overlays::population_map(ctx, app, None);
Some(maybe_unzoom(ctx, app)) Some(maybe_unzoom(ctx, app))
}), }),
); );
@ -405,7 +421,7 @@ impl Overlays {
Overlays::BusNetwork(_) => Some("bus network"), Overlays::BusNetwork(_) => Some("bus network"),
Overlays::Elevation(_, _) => Some("elevation"), Overlays::Elevation(_, _) => Some("elevation"),
Overlays::Edits(_) => Some("map edits"), Overlays::Edits(_) => Some("map edits"),
Overlays::PersonDotMap(_, _) => Some("dot map of people"), Overlays::PopulationMap(_, _, _, _) => Some("population map"),
Overlays::TripsHistogram(_, _) => None, Overlays::TripsHistogram(_, _) => None,
Overlays::IntersectionDemand(_, _, _, _) => None, Overlays::IntersectionDemand(_, _, _, _) => None,
Overlays::BusRoute(_, _, _) => None, Overlays::BusRoute(_, _, _) => None,
@ -981,7 +997,7 @@ impl Overlays {
// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to // TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
// return this kind of data instead! // return this kind of data instead!
fn person_dot_map(ctx: &EventCtx, app: &App) -> Overlays { fn population_map(ctx: &mut EventCtx, app: &App, opts: Option<HeatmapOptions>) -> Overlays {
let mut pts = Vec::new(); let mut pts = Vec::new();
// Faster to grab all agent positions than individually map trips to agent positions. // Faster to grab all agent positions than individually map trips to agent positions.
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) { for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
@ -1007,14 +1023,15 @@ impl Overlays {
// It's quite silly to produce triangles for the same circle over and over again. ;) // It's quite silly to produce triangles for the same circle over and over again. ;)
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon(); let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
let mut batch = GeomBatch::new(); let mut batch = GeomBatch::new();
if true { if let Some(ref o) = opts {
make_heatmap(&mut batch, app.primary.map.get_bounds(), pts); make_heatmap(&mut batch, app.primary.map.get_bounds(), pts, o);
} else { } else {
for pt in pts { for pt in pts {
batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y())); batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
} }
} }
Overlays::PersonDotMap(app.primary.sim.time(), ctx.upload(batch)) let controls = population_controls(ctx, opts.as_ref());
Overlays::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls)
} }
} }
@ -1030,3 +1047,67 @@ fn maybe_unzoom(ctx: &EventCtx, app: &mut App) -> Transition {
&mut app.primary, &mut app.primary,
)) ))
} }
// This function sounds more ominous than it should.
fn population_controls(ctx: &mut EventCtx, opts: Option<&HeatmapOptions>) -> Composite {
let mut col = vec![
Widget::row(vec![
Line("Population").roboto_bold().draw(ctx),
Btn::text_fg("X")
.build(ctx, "close", hotkey(Key::Escape))
.align_right(),
]),
Widget::checkbox(ctx, "Show heatmap", None, opts.is_some()),
];
if let Some(ref o) = opts {
// TODO Display the value...
col.push(Widget::row(vec![
"Resolution (meters)".draw_text(ctx),
Widget::slider("resolution"),
]));
col.push(Widget::row(vec![
"Diffusion (num of passes)".draw_text(ctx),
Widget::slider("passes"),
]));
let mut resolution = Slider::horizontal(ctx, 100.0, 25.0);
// 1 to 100m
resolution.set_percent(ctx, (o.resolution - 1.0) / 99.0);
let mut passes = Slider::horizontal(ctx, 100.0, 25.0);
// 0 to 10
passes.set_percent(ctx, (o.num_passes as f64) / 10.0);
col.push(Widget::dropdown(
ctx,
"Colors",
o.colors,
HeatmapColors::choices(),
));
Composite::new(Widget::col(col).bg(colors::PANEL_BG))
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
.slider("resolution", resolution)
.slider("passes", passes)
.build(ctx)
} else {
Composite::new(Widget::col(col).bg(colors::PANEL_BG))
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
.build(ctx)
}
}
fn heatmap_options(c: &mut Composite) -> Option<HeatmapOptions> {
if c.is_checked("Show heatmap") {
// Did we just change?
if c.maybe_slider("resolution").is_some() {
Some(HeatmapOptions {
resolution: 1.0 + c.slider("resolution").get_percent() * 99.0,
num_passes: (c.slider("passes").get_percent() * 10.0) as usize,
colors: c.dropdown_value("Colors"),
})
} else {
Some(HeatmapOptions::new())
}
} else {
None
}
}