mirror of
https://github.com/a-b-street/abstreet.git
synced 2025-01-04 12:36:46 +03:00
filter commuter patterns by mode. optionally exclude borders.
This commit is contained in:
parent
f9a606fab7
commit
9e4dbc9c98
@ -1,6 +1,7 @@
|
|||||||
use crate::app::{App, ShowEverything};
|
use crate::app::{App, ShowEverything};
|
||||||
use crate::common::ColorLegend;
|
use crate::common::ColorLegend;
|
||||||
use crate::game::{DrawBaselayer, State, Transition};
|
use crate::game::{DrawBaselayer, State, Transition};
|
||||||
|
use crate::helpers::checkbox_per_mode;
|
||||||
use crate::render::DrawOptions;
|
use crate::render::DrawOptions;
|
||||||
use abstutil::{prettyprint_usize, Counter, MultiMap};
|
use abstutil::{prettyprint_usize, Counter, MultiMap};
|
||||||
use ezgui::{
|
use ezgui::{
|
||||||
@ -10,7 +11,7 @@ use ezgui::{
|
|||||||
use geom::{Polygon, Time};
|
use geom::{Polygon, Time};
|
||||||
use map_model::{BuildingID, BuildingType, IntersectionID, LaneID, RoadID, TurnType};
|
use map_model::{BuildingID, BuildingType, IntersectionID, LaneID, RoadID, TurnType};
|
||||||
use maplit::hashset;
|
use maplit::hashset;
|
||||||
use sim::{DontDrawAgents, TripEndpoint, TripInfo};
|
use sim::{DontDrawAgents, TripEndpoint, TripInfo, TripMode};
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
|
|
||||||
pub struct CommuterPatterns {
|
pub struct CommuterPatterns {
|
||||||
@ -19,6 +20,7 @@ pub struct CommuterPatterns {
|
|||||||
blocks: Vec<Block>,
|
blocks: Vec<Block>,
|
||||||
current_block: Option<BlockID>,
|
current_block: Option<BlockID>,
|
||||||
draw_current_block: Option<Drawable>,
|
draw_current_block: Option<Drawable>,
|
||||||
|
filter: Filter,
|
||||||
|
|
||||||
// Indexed by BlockID
|
// Indexed by BlockID
|
||||||
trips_from_block: Vec<Vec<TripInfo>>,
|
trips_from_block: Vec<Vec<TripInfo>>,
|
||||||
@ -38,9 +40,14 @@ struct Block {
|
|||||||
shape: Polygon,
|
shape: Polygon,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Filters {
|
#[derive(PartialEq)]
|
||||||
|
struct Filter {
|
||||||
|
// If false, then trips to this block
|
||||||
|
from_block: bool,
|
||||||
|
include_borders: bool,
|
||||||
depart_from: Time,
|
depart_from: Time,
|
||||||
depart_until: Time,
|
depart_until: Time,
|
||||||
|
modes: BTreeSet<TripMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockID = usize;
|
type BlockID = usize;
|
||||||
@ -83,6 +90,13 @@ impl CommuterPatterns {
|
|||||||
draw_current_block: None,
|
draw_current_block: None,
|
||||||
trips_from_block,
|
trips_from_block,
|
||||||
trips_to_block,
|
trips_to_block,
|
||||||
|
filter: Filter {
|
||||||
|
from_block: true,
|
||||||
|
include_borders: true,
|
||||||
|
depart_from: Time::START_OF_DAY,
|
||||||
|
depart_until: app.primary.sim.get_end_of_day(),
|
||||||
|
modes: TripMode::all().into_iter().collect(),
|
||||||
|
},
|
||||||
|
|
||||||
draw_all_blocks: ctx.upload(all_blocks),
|
draw_all_blocks: ctx.upload(all_blocks),
|
||||||
composite: make_panel(ctx, app),
|
composite: make_panel(ctx, app),
|
||||||
@ -90,27 +104,43 @@ impl CommuterPatterns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For all trips from (or to) the base block, how many of them go to all other blocks?
|
// For all trips from (or to) the base block, how many of them go to all other blocks?
|
||||||
fn count_per_block(&self, base: &Block, from: bool, filter: Filters) -> Vec<(&Block, usize)> {
|
fn count_per_block(&self, base: &Block) -> Vec<(&Block, usize)> {
|
||||||
let candidates = if from {
|
let candidates = if self.filter.from_block {
|
||||||
&self.trips_from_block[base.id]
|
&self.trips_from_block[base.id]
|
||||||
} else {
|
} else {
|
||||||
&self.trips_to_block[base.id]
|
&self.trips_to_block[base.id]
|
||||||
};
|
};
|
||||||
let mut count: Counter<BlockID> = Counter::new();
|
let mut count: Counter<BlockID> = Counter::new();
|
||||||
for trip in candidates {
|
for trip in candidates {
|
||||||
if trip.departure < filter.depart_from || trip.departure > filter.depart_until {
|
if trip.departure < self.filter.depart_from || trip.departure > self.filter.depart_until
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if from {
|
if !self.filter.modes.contains(&trip.mode) {
|
||||||
count.inc(match trip.end {
|
continue;
|
||||||
TripEndpoint::Bldg(b) => self.bldg_to_block[&b],
|
}
|
||||||
TripEndpoint::Border(i, _) => self.border_to_block[&i],
|
if self.filter.from_block {
|
||||||
});
|
match trip.end {
|
||||||
|
TripEndpoint::Bldg(b) => {
|
||||||
|
count.inc(self.bldg_to_block[&b]);
|
||||||
|
}
|
||||||
|
TripEndpoint::Border(i, _) => {
|
||||||
|
if self.filter.include_borders {
|
||||||
|
count.inc(self.border_to_block[&i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
count.inc(match trip.start {
|
match trip.start {
|
||||||
TripEndpoint::Bldg(b) => self.bldg_to_block[&b],
|
TripEndpoint::Bldg(b) => {
|
||||||
TripEndpoint::Border(i, _) => self.border_to_block[&i],
|
count.inc(self.bldg_to_block[&b]);
|
||||||
});
|
}
|
||||||
|
TripEndpoint::Border(i, _) => {
|
||||||
|
if self.filter.include_borders {
|
||||||
|
count.inc(self.border_to_block[&i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +166,8 @@ impl State for CommuterPatterns {
|
|||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Or if a filter changed!
|
let old_block = self.current_block;
|
||||||
if ctx.redo_mouseover() {
|
if ctx.redo_mouseover() {
|
||||||
let old_block = self.current_block;
|
|
||||||
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
|
||||||
self.current_block = self
|
self.current_block = self
|
||||||
.blocks
|
.blocks
|
||||||
@ -148,108 +177,118 @@ impl State for CommuterPatterns {
|
|||||||
} else {
|
} else {
|
||||||
self.current_block = None;
|
self.current_block = None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if old_block != self.current_block {
|
let mut filter = Filter {
|
||||||
if let Some(id) = self.current_block {
|
from_block: self.composite.is_checked("from / to this block"),
|
||||||
let block = &self.blocks[id];
|
include_borders: self.composite.is_checked("include borders"),
|
||||||
|
depart_from: app
|
||||||
|
.primary
|
||||||
|
.sim
|
||||||
|
.get_end_of_day()
|
||||||
|
.percent_of(self.composite.area_slider("depart from").get_percent()),
|
||||||
|
depart_until: app
|
||||||
|
.primary
|
||||||
|
.sim
|
||||||
|
.get_end_of_day()
|
||||||
|
.percent_of(self.composite.area_slider("depart until").get_percent()),
|
||||||
|
modes: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
for m in TripMode::all() {
|
||||||
|
if self.composite.is_checked(m.ongoing_verb()) {
|
||||||
|
filter.modes.insert(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show the members of this block
|
if old_block != self.current_block || filter != self.filter {
|
||||||
let mut batch = GeomBatch::new();
|
self.filter = filter;
|
||||||
let mut building_counts: Vec<(&str, u32)> = vec![
|
if let Some(id) = self.current_block {
|
||||||
("Residential", 0),
|
let block = &self.blocks[id];
|
||||||
("Residential/Commercial", 0),
|
|
||||||
("Commercial", 0),
|
// Show the members of this block
|
||||||
("Vacant", 0), /* maybe "Empty Building" would be better, but currently
|
let mut batch = GeomBatch::new();
|
||||||
* it causes an undesireable lineheight change */
|
let mut building_counts: Vec<(&str, u32)> = vec![
|
||||||
];
|
("Residential", 0),
|
||||||
for b in &block.bldgs {
|
("Residential/Commercial", 0),
|
||||||
let building = app.primary.map.get_b(*b);
|
("Commercial", 0),
|
||||||
batch.push(Color::PURPLE, building.polygon.clone());
|
("Vacant", 0), /* maybe "Empty Building" would be better, but currently
|
||||||
match building.bldg_type {
|
* it causes an undesireable lineheight change */
|
||||||
BuildingType::Residential(_) => building_counts[0].1 += 1,
|
];
|
||||||
BuildingType::ResidentialCommercial(_) => building_counts[1].1 += 1,
|
for b in &block.bldgs {
|
||||||
BuildingType::Commercial => building_counts[2].1 += 1,
|
let building = app.primary.map.get_b(*b);
|
||||||
BuildingType::Empty => building_counts[3].1 += 1,
|
batch.push(Color::PURPLE, building.polygon.clone());
|
||||||
}
|
match building.bldg_type {
|
||||||
|
BuildingType::Residential(_) => building_counts[0].1 += 1,
|
||||||
|
BuildingType::ResidentialCommercial(_) => building_counts[1].1 += 1,
|
||||||
|
BuildingType::Commercial => building_counts[2].1 += 1,
|
||||||
|
BuildingType::Empty => building_counts[3].1 += 1,
|
||||||
}
|
}
|
||||||
for i in &block.borders {
|
|
||||||
batch.push(Color::PURPLE, app.primary.map.get_i(*i).polygon.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let non_empty_building_counts: Vec<(&str, u32)> = building_counts
|
|
||||||
.into_iter()
|
|
||||||
.filter(|building_count| building_count.1 > 0)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let block_buildings_description = if non_empty_building_counts.len() > 0 {
|
|
||||||
non_empty_building_counts
|
|
||||||
.into_iter()
|
|
||||||
.map(|building_count| {
|
|
||||||
format!("{}: {}", building_count.0, building_count.1)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ")
|
|
||||||
} else {
|
|
||||||
"Empty".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let from = self.composite.is_checked("from / to this block");
|
|
||||||
let filter =
|
|
||||||
Filters {
|
|
||||||
depart_from: app.primary.sim.get_end_of_day().percent_of(
|
|
||||||
self.composite.area_slider("depart from").get_percent(),
|
|
||||||
),
|
|
||||||
depart_until: app.primary.sim.get_end_of_day().percent_of(
|
|
||||||
self.composite.area_slider("depart until").get_percent(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let others = self.count_per_block(block, from, filter);
|
|
||||||
let mut total_trips = 0;
|
|
||||||
if !others.is_empty() {
|
|
||||||
let max_cnt = others.iter().map(|(_, cnt)| *cnt).max().unwrap() as f64;
|
|
||||||
for (other, cnt) in others {
|
|
||||||
total_trips += cnt;
|
|
||||||
let pct = (cnt as f64) / max_cnt;
|
|
||||||
// TODO Use app.cs.good_to_bad_red or some other color gradient
|
|
||||||
batch.push(
|
|
||||||
app.cs.good_to_bad_red.eval(pct).alpha(0.8),
|
|
||||||
other.shape.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.draw_current_block = Some(ctx.upload(batch));
|
|
||||||
|
|
||||||
self.composite.replace(
|
|
||||||
ctx,
|
|
||||||
"current",
|
|
||||||
format!("Selected: {}", block_buildings_description)
|
|
||||||
.draw_text(ctx)
|
|
||||||
.named("current")
|
|
||||||
.margin_below(10),
|
|
||||||
);
|
|
||||||
|
|
||||||
let new_scale = ColorLegend::gradient(
|
|
||||||
ctx,
|
|
||||||
&app.cs.good_to_bad_red,
|
|
||||||
vec![
|
|
||||||
"0".to_string(),
|
|
||||||
format!("{} trips", prettyprint_usize(total_trips)),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.named("scale");
|
|
||||||
self.composite.replace(ctx, "scale", new_scale);
|
|
||||||
} else {
|
|
||||||
self.draw_current_block = None;
|
|
||||||
self.composite.replace(
|
|
||||||
ctx,
|
|
||||||
"current",
|
|
||||||
"None selected"
|
|
||||||
.draw_text(ctx)
|
|
||||||
.named("current")
|
|
||||||
.margin_below(10),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
for i in &block.borders {
|
||||||
|
batch.push(Color::PURPLE, app.primary.map.get_i(*i).polygon.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let non_empty_building_counts: Vec<(&str, u32)> = building_counts
|
||||||
|
.into_iter()
|
||||||
|
.filter(|building_count| building_count.1 > 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let block_buildings_description = if non_empty_building_counts.len() > 0 {
|
||||||
|
non_empty_building_counts
|
||||||
|
.into_iter()
|
||||||
|
.map(|building_count| format!("{}: {}", building_count.0, building_count.1))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
} else {
|
||||||
|
"Empty".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let others = self.count_per_block(block);
|
||||||
|
let mut total_trips = 0;
|
||||||
|
if !others.is_empty() {
|
||||||
|
let max_cnt = others.iter().map(|(_, cnt)| *cnt).max().unwrap() as f64;
|
||||||
|
for (other, cnt) in others {
|
||||||
|
total_trips += cnt;
|
||||||
|
let pct = (cnt as f64) / max_cnt;
|
||||||
|
// TODO Use app.cs.good_to_bad_red or some other color gradient
|
||||||
|
batch.push(
|
||||||
|
app.cs.good_to_bad_red.eval(pct).alpha(0.8),
|
||||||
|
other.shape.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.draw_current_block = Some(ctx.upload(batch));
|
||||||
|
|
||||||
|
self.composite.replace(
|
||||||
|
ctx,
|
||||||
|
"current",
|
||||||
|
format!("Selected: {}", block_buildings_description)
|
||||||
|
.draw_text(ctx)
|
||||||
|
.named("current")
|
||||||
|
.margin_below(10),
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_scale = ColorLegend::gradient(
|
||||||
|
ctx,
|
||||||
|
&app.cs.good_to_bad_red,
|
||||||
|
vec![
|
||||||
|
"0".to_string(),
|
||||||
|
format!("{} trips", prettyprint_usize(total_trips)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.named("scale");
|
||||||
|
self.composite.replace(ctx, "scale", new_scale);
|
||||||
|
} else {
|
||||||
|
self.draw_current_block = None;
|
||||||
|
self.composite.replace(
|
||||||
|
ctx,
|
||||||
|
"current",
|
||||||
|
"None selected"
|
||||||
|
.draw_text(ctx)
|
||||||
|
.named("current")
|
||||||
|
.margin_below(10),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,6 +514,7 @@ fn make_panel(ctx: &mut EventCtx, app: &App) -> Composite {
|
|||||||
.align_right(),
|
.align_right(),
|
||||||
]),
|
]),
|
||||||
Checkbox::text(ctx, "from / to this block", hotkey(Key::Space), true),
|
Checkbox::text(ctx, "from / to this block", hotkey(Key::Space), true),
|
||||||
|
Checkbox::text(ctx, "include borders", None, true),
|
||||||
Widget::row(vec![
|
Widget::row(vec![
|
||||||
"Departing from:".draw_text(ctx).margin_right(20),
|
"Departing from:".draw_text(ctx).margin_right(20),
|
||||||
AreaSlider::new(ctx, 0.25 * ctx.canvas.window_width, 0.0).named("depart from"),
|
AreaSlider::new(ctx, 0.25 * ctx.canvas.window_width, 0.0).named("depart from"),
|
||||||
@ -483,6 +523,7 @@ fn make_panel(ctx: &mut EventCtx, app: &App) -> Composite {
|
|||||||
"Departing until:".draw_text(ctx).margin_right(20),
|
"Departing until:".draw_text(ctx).margin_right(20),
|
||||||
AreaSlider::new(ctx, 0.25 * ctx.canvas.window_width, 1.0).named("depart until"),
|
AreaSlider::new(ctx, 0.25 * ctx.canvas.window_width, 1.0).named("depart until"),
|
||||||
]),
|
]),
|
||||||
|
checkbox_per_mode(ctx, app, &TripMode::all().into_iter().collect()),
|
||||||
"None selected".draw_text(ctx).named("current"),
|
"None selected".draw_text(ctx).named("current"),
|
||||||
ColorLegend::gradient(ctx, &app.cs.good_to_bad_red, vec!["0", "0"]).named("scale"),
|
ColorLegend::gradient(ctx, &app.cs.good_to_bad_red, vec!["0", "0"]).named("scale"),
|
||||||
]))
|
]))
|
||||||
|
Loading…
Reference in New Issue
Block a user