Add a layer showing pedestrian density.

This commit is contained in:
Dustin Carlino 2022-05-12 13:33:33 +01:00
parent 16e539a77c
commit a229bf9385
5 changed files with 144 additions and 18 deletions

View File

@ -109,6 +109,7 @@ impl PickLayer {
btn("throughput", Key::T),
btn("traffic jams", Key::J),
btn("cycling activity", Key::B),
btn("pedestrian crowding", Key::C),
]),
Widget::col(vec![
"Map".text_widget(ctx),
@ -178,6 +179,9 @@ impl State<App> for PickLayer {
"delay" => {
app.primary.layer = Some(Box::new(traffic::Delay::new(ctx, app)));
}
"pedestrian crowding" => {
app.primary.layer = Some(Box::new(traffic::PedestrianCrowding::new(ctx, app)));
}
"steep streets" => {
app.primary.layer = Some(Box::new(elevation::SteepStreets::new(ctx, app)));
}

View File

@ -6,7 +6,7 @@ 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};
use map_gui::tools::{ColorDiscrete, ColorLegend, ColorNetwork, DivergingScale};
use map_gui::ID;
use map_model::{IntersectionID, Map, Traversable};
use sim::{AgentType, VehicleType};
@ -622,6 +622,86 @@ impl Delay {
}
}
pub struct PedestrianCrowding {
time: Time,
draw: ToggleZoomed,
panel: Panel,
}
impl Layer for PedestrianCrowding {
fn name(&self) -> Option<&'static str> {
Some("pedestrian crowding")
}
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
if app.primary.sim.time() != self.time {
*self = Self::new(ctx, app);
}
if let Outcome::Clicked(x) = self.panel.event(ctx) {
match x.as_ref() {
"close" => {
return Some(LayerOutcome::Close);
}
_ => unreachable!(),
}
}
None
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
self.draw.draw(g);
}
fn draw_minimap(&self, g: &mut GfxCtx) {
g.redraw(&self.draw.unzoomed);
}
}
impl PedestrianCrowding {
pub fn new(ctx: &mut EventCtx, app: &App) -> Self {
let categories = vec![
("< 1", app.cs.good_to_bad_red.eval(0.0)),
("1 - 1.5", app.cs.good_to_bad_red.eval(0.3)),
("1.5 - 2.0", app.cs.good_to_bad_red.eval(0.6)),
("> 2", app.cs.good_to_bad_red.eval(1.0)),
];
let mut colorer = ColorDiscrete::new(app, categories);
fn bucket(x: f64) -> &'static str {
if x < 1.0 {
"< 1"
} else if x <= 1.5 {
"1 - 1.5"
} else if x <= 2.0 {
"1.5 - 2.0"
} else {
"> 2"
}
}
let (roads, intersections) = app.primary.sim.get_pedestrian_density(&app.primary.map);
for (r, density) in roads {
colorer.add_r(r, bucket(density));
}
for (i, density) in intersections {
colorer.add_i(i, bucket(density));
}
let (draw, legend) = colorer.build(ctx);
Self {
time: app.primary.sim.time(),
draw,
panel: Panel::new_builder(Widget::col(vec![
header(ctx, "Pedestrian crowding"),
"(people / m^2)".text_widget(ctx),
legend,
]))
.aligned_pair(PANEL_PLACEMENT)
.build(ctx),
}
}
}
fn export_throughput(app: &App) -> Result<(String, String)> {
let path1 = format!(
"road_throughput_{}_{}.csv",

View File

@ -8,6 +8,7 @@ use widgetry::{Color, EventCtx, Fill, GeomBatch, Line, LinearGradient, Text, Wid
use crate::AppLike;
// TODO Tooltips would almost be nice, for cases like pedestrian crowding
pub struct ColorDiscrete<'a> {
map: &'a Map,
// pub so callers can add stuff in before building

View File

@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use abstutil::{deserialize_multimap, serialize_multimap, FixedMap, IndexableKey, MultiMap};
use geom::{Distance, Duration, Line, PolyLine, Speed, Time};
use map_model::{
BuildingID, DrivingSide, Map, ParkingLotID, Path, PathConstraints, PathStep, TransitRouteID,
Traversable,
BuildingID, DrivingSide, IntersectionID, Map, ParkingLotID, Path, PathConstraints, PathStep,
RoadID, TransitRouteID, Traversable,
};
use crate::sim::Ctx;
@ -625,6 +625,35 @@ impl WalkingSimState {
}
}
}
pub fn get_pedestrian_density(
&self,
map: &Map,
) -> (BTreeMap<RoadID, f64>, BTreeMap<IntersectionID, f64>) {
let mut roads = BTreeMap::new();
let mut intersections = BTreeMap::new();
for (traversable, peds) in self.peds_per_traversable.borrow() {
if peds.is_empty() {
continue;
}
let density = (peds.len() as f64) / area(map, *traversable);
match traversable {
Traversable::Lane(l) => {
let entry = roads.entry(l.road).or_insert(0.0);
if *entry < density {
*entry = density;
}
}
Traversable::Turn(t) => {
let entry = intersections.entry(t.parent).or_insert(0.0);
if *entry < density {
*entry = density;
}
}
}
}
(roads, intersections)
}
}
#[derive(Serialize, Deserialize, Clone)]
@ -995,20 +1024,9 @@ fn crowdedness_penalty(
traversable: Traversable,
peds_per_traversable: &MultiMap<Traversable, PedestrianID>,
) -> f64 {
// 1) How many people are in the space
let num_people = peds_per_traversable.get(traversable).len();
// 2) The length of the sidewalk or crosswalk
let len = traversable.get_polyline(map).length();
// 3) The width of the space
let width = match traversable {
// Sidewalk
Traversable::Lane(l) => map.get_l(l).width,
// For crosswalks, the thinner of the two sidewalks being connected
Traversable::Turn(t) => map.get_l(t.src).width.min(map.get_l(t.dst).width),
};
// Person per area, assuming everyone's equally spread out
let people_per_sq_m = (num_people as f64) / (width.inner_meters() * len.inner_meters());
// Assume everyone's equally spread out
let people_per_sq_m = (num_people as f64) / area(map, traversable);
// Based on eyeballing images from
// https://www.gkstill.com/Support/crowd-density/CrowdDensity-1.html, let's use a fixed
// threshold of 1.5 people per square meter as "crowded" and slow them down by half.
@ -1018,3 +1036,17 @@ fn crowdedness_penalty(
}
0.5
}
// In m^2
fn area(map: &Map, traversable: Traversable) -> f64 {
// The length of the sidewalk or crosswalk
let len = traversable.get_polyline(map).length();
// The width of the space
let width = match traversable {
// Sidewalk
Traversable::Lane(l) => map.get_l(l).width,
// For crosswalks, the thinner of the two sidewalks being connected
Traversable::Turn(t) => map.get_l(t.src).width.min(map.get_l(t.dst).width),
};
width.inner_meters() * len.inner_meters()
}

View File

@ -7,8 +7,8 @@ use std::collections::{BTreeMap, BTreeSet};
use abstutil::Counter;
use geom::{Distance, Duration, PolyLine, Pt2D, Time};
use map_model::{
BuildingID, IntersectionID, Lane, LaneID, Map, Path, Position, TransitRouteID, TransitStopID,
Traversable, TurnID,
BuildingID, IntersectionID, Lane, LaneID, Map, Path, Position, RoadID, TransitRouteID,
TransitStopID, Traversable, TurnID,
};
use synthpop::{OrigPersonID, Scenario, TripMode};
@ -452,6 +452,15 @@ impl Sim {
pub fn get_highlighted_people(&self) -> &Option<BTreeSet<PersonID>> {
&self.highlighted_people
}
/// Returns people / m^2. Roads have up to two sidewalks and intersections have many crossings
/// -- take the max density along any one.
pub fn get_pedestrian_density(
&self,
map: &Map,
) -> (BTreeMap<RoadID, f64>, BTreeMap<IntersectionID, f64>) {
self.walking.get_pedestrian_density(map)
}
}
// Drawing