start a tool to understand popular destinations. remove dot map,

superceded by live population map
This commit is contained in:
Dustin Carlino 2020-05-12 12:54:35 -07:00
parent 9ce3061e17
commit 3145326207
13 changed files with 202 additions and 137 deletions

View File

@ -93,6 +93,10 @@ impl<T: Ord + PartialEq + Clone> Counter<T> {
list.into_iter().map(|(t, _)| t).collect() list.into_iter().map(|(t, _)| t).collect()
} }
pub fn max(&self) -> usize {
*self.map.values().max().unwrap()
}
pub fn compare(mut self, mut other: Counter<T>) -> Vec<(T, usize, usize)> { pub fn compare(mut self, mut other: Counter<T>) -> Vec<(T, usize, usize)> {
for key in self.map.keys() { for key in self.map.keys() {
other.map.entry(key.clone()).or_insert(0); other.map.entry(key.clone()).or_insert(0);
@ -106,6 +110,9 @@ impl<T: Ord + PartialEq + Clone> Counter<T> {
.collect() .collect()
} }
pub fn borrow(&self) -> &BTreeMap<T, usize> {
&self.map
}
pub fn consume(self) -> BTreeMap<T, usize> { pub fn consume(self) -> BTreeMap<T, usize> {
self.map self.map
} }

View File

@ -21,8 +21,8 @@ first make sure your .osm has been clipped:
[the instructions](dev.md#building-map-data). You'll need Rust, osmconvert, [the instructions](dev.md#building-map-data). You'll need Rust, osmconvert,
gdal, etc. gdal, etc.
2. Use http://geojson.io/ to draw a polygon around the region you want to 2. Use [geojson.io](http://geojson.io/) to draw a polygon around the region you
simulate. want to simulate.
3. Create a new directory: `mkdir -p data/input/your_city/polygons` 3. Create a new directory: `mkdir -p data/input/your_city/polygons`

View File

@ -278,6 +278,7 @@ impl App {
&ShowEverything::new(), &ShowEverything::new(),
false, false,
false, false,
false,
); );
} }
@ -291,10 +292,11 @@ impl App {
show_objs: &dyn ShowObject, show_objs: &dyn ShowObject,
debug_mode: bool, debug_mode: bool,
unzoomed_roads_and_intersections: bool, unzoomed_roads_and_intersections: bool,
unzoomed_buildings: bool,
) -> Option<ID> { ) -> Option<ID> {
// Unzoomed mode. Ignore when debugging areas and extra shapes. // Unzoomed mode. Ignore when debugging areas and extra shapes.
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail
&& !(debug_mode || unzoomed_roads_and_intersections) && !(debug_mode || unzoomed_roads_and_intersections || unzoomed_buildings)
{ {
return None; return None;
} }
@ -332,6 +334,11 @@ impl App {
continue; continue;
} }
} }
ID::Building(_) => {
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail && !unzoomed_buildings {
continue;
}
}
_ => { _ => {
if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail { if ctx.canvas.cam_zoom < self.opts.min_zoom_for_detail {
continue; continue;

View File

@ -110,7 +110,7 @@ impl State for DebugMode {
if ctx.redo_mouseover() { if ctx.redo_mouseover() {
app.primary.current_selection = app.primary.current_selection =
app.calculate_current_selection(ctx, &app.primary.sim, self, true, false); app.calculate_current_selection(ctx, &app.primary.sim, self, true, false, false);
} }
match self.composite.event(ctx) { match self.composite.event(ctx) {
@ -179,8 +179,14 @@ impl State for DebugMode {
} }
"unhide everything" => { "unhide everything" => {
self.hidden.clear(); self.hidden.clear();
app.primary.current_selection = app.primary.current_selection = app.calculate_current_selection(
app.calculate_current_selection(ctx, &app.primary.sim, self, true, false); ctx,
&app.primary.sim,
self,
true,
false,
false,
);
self.reset_info(ctx); self.reset_info(ctx);
} }
"search OSM metadata" => { "search OSM metadata" => {

View File

@ -28,7 +28,7 @@ struct Block {
} }
impl BlockMap { impl BlockMap {
pub fn new(ctx: &mut EventCtx, app: &App, scenario: Scenario) -> BlockMap { pub fn new(ctx: &mut EventCtx, app: &App, scenario: Scenario) -> Box<dyn State> {
let mut bldg_to_block = HashMap::new(); let mut bldg_to_block = HashMap::new();
let mut blocks = Vec::new(); let mut blocks = Vec::new();
@ -64,7 +64,7 @@ impl BlockMap {
all_blocks.push(Color::YELLOW.alpha(0.5), block.shape.clone()); all_blocks.push(Color::YELLOW.alpha(0.5), block.shape.clone());
} }
BlockMap { Box::new(BlockMap {
bldg_to_block, bldg_to_block,
blocks, blocks,
scenario, scenario,
@ -89,7 +89,7 @@ impl BlockMap {
) )
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx), .build(ctx),
} })
} }
fn count_per_block(&self, base: &Block, from: bool, map: &Map) -> Vec<(&Block, usize)> { fn count_per_block(&self, base: &Block, from: bool, map: &Map) -> Vec<(&Block, usize)> {

View File

@ -0,0 +1,152 @@
use crate::app::{App, ShowEverything};
use crate::common::{make_heatmap, HeatmapOptions};
use crate::game::{State, Transition};
use crate::helpers::ID;
use abstutil::Counter;
use ezgui::{
hotkey, Btn, Checkbox, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, Text, VerticalAlignment, Widget,
};
use map_model::BuildingID;
use sim::{DontDrawAgents, Scenario, TripEndpoint};
pub struct PopularDestinations {
per_bldg: Counter<BuildingID>,
composite: Composite,
opts: Option<HeatmapOptions>,
draw: Drawable,
}
impl PopularDestinations {
pub fn new(ctx: &mut EventCtx, app: &App, scenario: &Scenario) -> Box<dyn State> {
let mut per_bldg = Counter::new();
for p in &scenario.people {
for trip in &p.trips {
if let TripEndpoint::Bldg(b) = trip.trip.end(&app.primary.map) {
per_bldg.inc(b);
}
}
}
PopularDestinations::make(ctx, app, per_bldg, None)
}
fn make(
ctx: &mut EventCtx,
app: &App,
per_bldg: Counter<BuildingID>,
opts: Option<HeatmapOptions>,
) -> Box<dyn State> {
let map = &app.primary.map;
let mut batch = GeomBatch::new();
let controls = if let Some(ref o) = opts {
let mut pts = Vec::new();
for (b, cnt) in per_bldg.borrow() {
let pt = map.get_b(*b).label_center;
for _ in 0..*cnt {
pts.push(pt);
}
}
// TODO Er, the heatmap actually looks terrible.
Widget::col(o.to_controls(ctx, make_heatmap(&mut batch, map.get_bounds(), pts, o)))
} else {
let max = per_bldg.max();
let gradient = colorous::REDS;
for (b, cnt) in per_bldg.borrow() {
let c = gradient.eval_rational(*cnt, max);
batch.push(
Color::rgb(c.r as usize, c.g as usize, c.b as usize),
map.get_b(*b).polygon.clone(),
);
}
Widget::nothing()
};
Box::new(PopularDestinations {
per_bldg,
draw: ctx.upload(batch),
composite: Composite::new(
Widget::col(vec![
Widget::row(vec![
Line("Most popular destinations")
.small_heading()
.draw(ctx)
.margin_right(10),
Btn::text_fg("X")
.build_def(ctx, hotkey(Key::Escape))
.align_right(),
]),
Checkbox::text(ctx, "Show heatmap", None, opts.is_some()),
controls,
])
.padding(10)
.bg(app.cs.panel_bg),
)
.aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
.build(ctx),
opts,
})
}
}
impl State for PopularDestinations {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
if ctx.redo_mouseover() {
app.primary.current_selection = app.calculate_current_selection(
ctx,
&DontDrawAgents {},
&ShowEverything::new(),
false,
false,
true,
);
if let Some(ID::Building(_)) = app.primary.current_selection {
} else {
app.primary.current_selection = None;
}
}
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
return Transition::Pop;
}
_ => unreachable!(),
},
None => {}
}
let opts = if self.composite.is_checked("Show heatmap") {
Some(HeatmapOptions::from_controls(&self.composite))
} else {
None
};
if self.opts != opts {
return Transition::Replace(PopularDestinations::make(
ctx,
app,
self.per_bldg.clone(),
opts,
));
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, app: &App) {
g.redraw(&self.draw);
self.composite.draw(g);
if let Some(ID::Building(b)) = app.primary.current_selection {
let mut txt = Text::new();
txt.add(Line(format!(
"{} trips to here",
abstutil::prettyprint_usize(self.per_bldg.get(b))
)));
for (name, amenity) in &app.primary.map.get_b(b).amenities {
txt.add(Line(format!("- {} ({})", name, amenity)));
}
g.draw_mouse_tooltip(txt);
}
}
}

View File

@ -210,6 +210,7 @@ impl State for ParkingMapper {
&ShowEverything::new(), &ShowEverything::new(),
false, false,
true, true,
false,
) { ) {
Some(ID::Road(r)) => Some(r), Some(ID::Road(r)) => Some(r),
Some(ID::Lane(l)) => Some(map.get_l(l).parent), Some(ID::Lane(l)) => Some(map.get_l(l).parent),

View File

@ -1,4 +1,5 @@
mod blocks; mod blocks;
mod destinations;
pub mod mapping; pub mod mapping;
mod polygon; mod polygon;
mod scenario; mod scenario;

View File

@ -1,15 +1,16 @@
use crate::app::App; use crate::app::App;
use crate::common::{tool_panel, Colorer, CommonState, ContextualActions, Warping}; use crate::common::{tool_panel, Colorer, CommonState, ContextualActions, Warping};
use crate::devtools::blocks::BlockMap; use crate::devtools::blocks::BlockMap;
use crate::devtools::destinations::PopularDestinations;
use crate::game::{State, Transition, WizardState}; use crate::game::{State, Transition, WizardState};
use crate::helpers::ID; use crate::helpers::ID;
use crate::managed::{WrappedComposite, WrappedOutcome}; use crate::managed::{WrappedComposite, WrappedOutcome};
use abstutil::{prettyprint_usize, Counter, MultiMap}; use abstutil::{prettyprint_usize, Counter, MultiMap};
use ezgui::{ use ezgui::{
hotkey, lctrl, Btn, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, hotkey, lctrl, Choice, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line,
HorizontalAlignment, Key, Line, Outcome, Slider, Text, VerticalAlignment, Widget, Outcome, Text,
}; };
use geom::{Distance, Line, PolyLine, Polygon}; use geom::{Distance, PolyLine};
use map_model::{BuildingID, IntersectionID, Map}; use map_model::{BuildingID, IntersectionID, Map};
use sim::{ use sim::{
DrivingGoal, IndividTrip, PersonSpec, Scenario, SidewalkPOI, SidewalkSpot, SpawnTrip, DrivingGoal, IndividTrip, PersonSpec, Scenario, SidewalkPOI, SidewalkSpot, SpawnTrip,
@ -104,7 +105,7 @@ impl ScenarioManager {
], ],
vec![ vec![
(hotkey(Key::B), "block map"), (hotkey(Key::B), "block map"),
(hotkey(Key::D), "dot map"), (hotkey(Key::D), "popular destinations"),
(lctrl(Key::P), "stop showing paths"), (lctrl(Key::P), "stop showing paths"),
], ],
), ),
@ -128,15 +129,11 @@ impl State for ScenarioManager {
"X" => { "X" => {
return Transition::Pop; return Transition::Pop;
} }
"dot map" => {
return Transition::Push(Box::new(DotMap::new(ctx, app, &self.scenario)));
}
"block map" => { "block map" => {
return Transition::Push(Box::new(BlockMap::new( return Transition::Push(BlockMap::new(ctx, app, self.scenario.clone()));
ctx, }
app, "popular destinations" => {
self.scenario.clone(), return Transition::Push(PopularDestinations::new(ctx, app, &self.scenario));
)));
} }
// TODO Inactivate this sometimes // TODO Inactivate this sometimes
"stop showing paths" => { "stop showing paths" => {
@ -465,109 +462,6 @@ fn show_demand(
batch.upload(ctx) batch.upload(ctx)
} }
struct DotMap {
composite: Composite,
lines: Vec<Line>,
draw: Option<(f64, Drawable)>,
}
impl DotMap {
fn new(ctx: &mut EventCtx, app: &App, scenario: &Scenario) -> DotMap {
let map = &app.primary.map;
let lines = scenario
.people
.iter()
.flat_map(|p| {
p.trips.iter().filter_map(|trip| {
let (start, end) = match &trip.trip {
SpawnTrip::VehicleAppearing { start, goal, .. } => {
(start.pt(map), goal.pt(map))
}
SpawnTrip::FromBorder { dr, goal, .. } => {
(map.get_i(dr.src_i(map)).polygon.center(), goal.pt(map))
}
SpawnTrip::UsingParkedCar(b, goal) => {
(map.get_b(*b).polygon.center(), goal.pt(map))
}
SpawnTrip::UsingBike(start, goal) => {
(start.sidewalk_pos.pt(map), goal.pt(map))
}
SpawnTrip::JustWalking(start, goal) => {
(start.sidewalk_pos.pt(map), goal.sidewalk_pos.pt(map))
}
SpawnTrip::UsingTransit(start, goal, _, _, _) => {
(start.sidewalk_pos.pt(map), goal.sidewalk_pos.pt(map))
}
SpawnTrip::Remote { .. } => unimplemented!(),
};
Line::maybe_new(start, end)
})
})
.collect();
DotMap {
composite: Composite::new(
Widget::col(vec![
Widget::row(vec![
Line("Dot map of all trips").small_heading().draw(ctx),
Btn::text_fg("X")
.build_def(ctx, hotkey(Key::Escape))
.align_right(),
]),
Slider::horizontal(ctx, 150.0, 25.0, 0.0).named("time slider"),
])
.padding(10)
.bg(app.cs.panel_bg),
)
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx),
lines,
draw: None,
}
}
}
impl State for DotMap {
fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
ctx.canvas_movement();
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"X" => {
return Transition::Pop;
}
_ => unreachable!(),
},
None => {}
}
let pct = self.composite.slider("time slider").get_percent();
if self.draw.as_ref().map(|(p, _)| pct != *p).unwrap_or(true) {
let mut batch = GeomBatch::new();
let radius = Distance::meters(5.0);
for l in &self.lines {
// Circles are too expensive. :P
batch.push(
Color::RED,
Polygon::rectangle_centered(l.percent_along(pct), radius, radius),
);
}
self.draw = Some((pct, batch.upload(ctx)));
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
if let Some((_, ref d)) = self.draw {
g.redraw(d);
}
self.composite.draw(g);
}
}
struct Actions<'a> { struct Actions<'a> {
demand: &'a mut Option<Drawable>, demand: &'a mut Option<Drawable>,
scenario: &'a Scenario, scenario: &'a Scenario,

View File

@ -58,6 +58,7 @@ impl State for BulkSelect {
&ShowEverything::new(), &ShowEverything::new(),
false, false,
true, true,
false,
); );
if let Some(ID::Intersection(_)) = app.primary.current_selection { if let Some(ID::Intersection(_)) = app.primary.current_selection {
} else { } else {
@ -296,6 +297,7 @@ impl State for PaintSelect {
&ShowEverything::new(), &ShowEverything::new(),
false, false,
true, true,
false,
); );
if let Some(ID::Road(_)) = app.primary.current_selection { if let Some(ID::Road(_)) = app.primary.current_selection {
} else { } else {

View File

@ -100,6 +100,7 @@ impl State for EditMode {
&ShowEverything::new(), &ShowEverything::new(),
false, false,
true, true,
false,
); );
if let Some(ID::Lane(l)) = app.primary.current_selection { if let Some(ID::Lane(l)) = app.primary.current_selection {
if !can_edit_lane(&self.mode, l, app) { if !can_edit_lane(&self.mode, l, app) {

View File

@ -47,7 +47,7 @@ pub fn info(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BuildingID
txt.add(Line(format!("{} amenities:", b.amenities.len()))); txt.add(Line(format!("{} amenities:", b.amenities.len())));
} }
for (name, amenity) in &b.amenities { for (name, amenity) in &b.amenities {
txt.add(Line(format!("- {} (a {})", name, amenity))); txt.add(Line(format!("- {} ({})", name, amenity)));
} }
} }

View File

@ -132,11 +132,7 @@ pub fn main_menu(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
txt txt
}) })
.build_def(ctx, hotkey(Key::M)), .build_def(ctx, hotkey(Key::M)),
if app.opts.dev { Btn::text_bg2("Internal Dev Tools").build_def(ctx, hotkey(Key::D)),
Btn::text_bg2("Internal Dev Tools").build_def(ctx, hotkey(Key::D))
} else {
Widget::nothing()
},
]) ])
.centered(), .centered(),
Widget::col(vec![ Widget::col(vec![
@ -146,7 +142,7 @@ pub fn main_menu(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
.centered(), .centered(),
]; ];
let mut c = WrappedComposite::new( let c = WrappedComposite::new(
Composite::new(Widget::col(col).evenly_spaced()) Composite::new(Widget::col(col).evenly_spaced())
.exact_size_percent(90, 85) .exact_size_percent(90, 85)
.build(ctx), .build(ctx),
@ -219,13 +215,11 @@ pub fn main_menu(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
crate::devtools::mapping::ParkingMapper::new(ctx, app, true, BTreeMap::new()), crate::devtools::mapping::ParkingMapper::new(ctx, app, true, BTreeMap::new()),
)) ))
}), }),
); )
if app.opts.dev { .cb(
c = c.cb(
"Internal Dev Tools", "Internal Dev Tools",
Box::new(|ctx, app| Some(Transition::Push(DevToolsMode::new(ctx, app)))), Box::new(|ctx, app| Some(Transition::Push(DevToolsMode::new(ctx, app)))),
); );
}
ManagedGUIState::fullscreen(c) ManagedGUIState::fullscreen(c)
} }