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),
Menu(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>),
UsizePlot(Plot<usize>),
Histogram(Histogram),
@ -1007,6 +1008,9 @@ impl Composite {
pub fn slider(&self, name: &str) -> &Slider {
&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 {
self.sliders.get_mut(name).unwrap()
}
@ -1029,10 +1033,16 @@ impl Composite {
}
}
// TODO This invalidates the dropdown!
pub fn dropdown_value<T: 'static>(&mut self, name: &str) -> T {
pub fn dropdown_value<T: 'static + Clone>(&mut self, name: &str) -> T {
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),
}
}

View File

@ -105,11 +105,31 @@ impl Dropdown {
}
}
// TODO This invalidates the entire widget!
pub fn take_value<T: 'static>(&mut self) -> T {
let data: Box<dyn Any> = self.choices.remove(self.current_idx).data;
// TODO This invalidates the entire widget! Have to call return_value
pub fn take_value<T: 'static>(&mut self) -> Choice<T> {
let c = self.choices.remove(self.current_idx);
let data: Box<dyn Any> = c.data;
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};
pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) {
// Meters
let resolution = 10.0;
#[derive(Clone, PartialEq)]
pub struct HeatmapOptions {
// 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.
let mut counts: Grid<u16> = Grid::new(
(bounds.width() / resolution).ceil() as usize,
(bounds.height() / resolution).ceil() as usize,
(bounds.width() / opts.resolution).ceil() as usize,
(bounds.height() / opts.resolution).ceil() as usize,
0,
);
for pt in pts {
// TODO more careful rounding
let idx = counts.idx(
((pt.x() - bounds.min_x) / resolution) as usize,
((pt.y() - bounds.min_y) / resolution) as usize,
((pt.x() - bounds.min_x) / opts.resolution) as usize,
((pt.y() - bounds.min_y) / opts.resolution) as usize,
);
counts.data[idx] += 1;
}
// Diffusion
let num_passes = 5;
for _ in 0..num_passes {
for _ in 0..opts.num_passes {
// Have to hot-swap! Urgh
let mut copy = counts.data.clone();
for y in 0..counts.height {
@ -41,24 +71,34 @@ pub fn make_heatmap(batch: &mut GeomBatch, bounds: &Bounds, pts: Vec<Pt2D>) {
// Now draw rectangles
let max = *counts.data.iter().max().unwrap();
// TODO Full spectral progression isn't recommended anymore!
// This is in order from low density to high.
let colors = vec![
Color::hex("#0b2c7a"),
Color::hex("#1e9094"),
Color::hex("#0ec441"),
Color::hex("#7bed00"),
Color::hex("#f7d707"),
Color::hex("#e68e1c"),
Color::hex("#c2523c"),
];
let colors = match opts.colors {
HeatmapColors::FullSpectral => vec![
Color::hex("#0b2c7a"),
Color::hex("#1e9094"),
Color::hex("#0ec441"),
Color::hex("#7bed00"),
Color::hex("#f7d707"),
Color::hex("#e68e1c"),
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?
let range = max / ((colors.len() - 1) as u16);
if range == 0 {
// Max is too low, use less colors?
return;
}
let square = Polygon::rectangle(resolution, resolution);
let square = Polygon::rectangle(opts.resolution, opts.resolution);
for y in 0..counts.height {
for x in 0..counts.width {
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)];
batch.push(
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::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::overlays::Overlays;
pub use self::panels::tool_panel;

View File

@ -1,6 +1,8 @@
use crate::app::App;
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::helpers::rotating_color_map;
use crate::helpers::ID;
@ -10,7 +12,7 @@ use abstutil::{prettyprint_usize, Counter};
use ezgui::{
hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, Histogram,
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 map_model::{BusRouteID, IntersectionID};
@ -28,7 +30,7 @@ pub enum Overlays {
Elevation(Colorer, Drawable),
Edits(Colorer),
TripsHistogram(Time, Composite),
PersonDotMap(Time, Drawable),
PopulationMap(Time, Option<HeatmapOptions>, Drawable, Composite),
// These aren't selectable from the main picker
IntersectionDemand(Time, IntersectionID, Drawable, Composite),
@ -94,9 +96,9 @@ impl Overlays {
app.overlay = Overlays::bus_passengers(id, ctx, app);
}
}
Overlays::PersonDotMap(t, _) => {
Overlays::PopulationMap(t, ref opts, _, _) => {
if now != t {
app.overlay = Overlays::person_dot_map(ctx, app);
app.overlay = Overlays::population_map(ctx, app, opts.clone());
}
}
// No updates needed
@ -193,9 +195,24 @@ impl Overlays {
}
}
}
Overlays::PersonDotMap(_, _) => {
// TODO No controls or legend at all?
app.overlay = orig_overlay;
Overlays::PopulationMap(_, ref mut opts, _, ref mut c) => {
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;
}
}
}
}
Overlays::Inactive => {}
}
@ -224,9 +241,10 @@ impl Overlays {
g.redraw(draw);
}
}
Overlays::PersonDotMap(_, ref draw) => {
Overlays::PopulationMap(_, _, ref draw, ref composite) => {
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
g.redraw(draw);
composite.draw(g);
}
}
// 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("bike network").build_def(ctx, hotkey(Key::B)),
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
if let Some(name) = match app.overlay {
Overlays::Inactive => Some("None"),
@ -289,7 +305,7 @@ impl Overlays {
Overlays::BusNetwork(_) => Some("bus network"),
Overlays::Elevation(_, _) => Some("elevation"),
Overlays::Edits(_) => Some("map edits"),
Overlays::PersonDotMap(_, _) => Some("dot map of people"),
Overlays::PopulationMap(_, _, _, _) => Some("population map"),
_ => None,
} {
for btn in &mut choices {
@ -384,9 +400,9 @@ impl Overlays {
}),
)
.maybe_cb(
"dot map of people",
"population map",
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))
}),
);
@ -405,7 +421,7 @@ impl Overlays {
Overlays::BusNetwork(_) => Some("bus network"),
Overlays::Elevation(_, _) => Some("elevation"),
Overlays::Edits(_) => Some("map edits"),
Overlays::PersonDotMap(_, _) => Some("dot map of people"),
Overlays::PopulationMap(_, _, _, _) => Some("population map"),
Overlays::TripsHistogram(_, _) => None,
Overlays::IntersectionDemand(_, _, _, _) => None,
Overlays::BusRoute(_, _, _) => None,
@ -981,7 +997,7 @@ impl Overlays {
// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
// 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();
// 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) {
@ -1007,14 +1023,15 @@ impl Overlays {
// 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 mut batch = GeomBatch::new();
if true {
make_heatmap(&mut batch, app.primary.map.get_bounds(), pts);
if let Some(ref o) = opts {
make_heatmap(&mut batch, app.primary.map.get_bounds(), pts, o);
} else {
for pt in pts {
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,
))
}
// 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
}
}