From a229bf9385137d7ba28d85d646fa896e05ddbc19 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Thu, 12 May 2022 13:33:33 +0100 Subject: [PATCH] Add a layer showing pedestrian density. --- apps/game/src/layer/mod.rs | 4 ++ apps/game/src/layer/traffic.rs | 82 +++++++++++++++++++++++++++++++++- map_gui/src/tools/colors.rs | 1 + sim/src/mechanics/walking.rs | 62 ++++++++++++++++++------- sim/src/sim/queries.rs | 13 +++++- 5 files changed, 144 insertions(+), 18 deletions(-) diff --git a/apps/game/src/layer/mod.rs b/apps/game/src/layer/mod.rs index a3c9674106..5feb6d7cc2 100644 --- a/apps/game/src/layer/mod.rs +++ b/apps/game/src/layer/mod.rs @@ -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 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))); } diff --git a/apps/game/src/layer/traffic.rs b/apps/game/src/layer/traffic.rs index da5efc4537..c96c94b6a7 100644 --- a/apps/game/src/layer/traffic.rs +++ b/apps/game/src/layer/traffic.rs @@ -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 { + 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", diff --git a/map_gui/src/tools/colors.rs b/map_gui/src/tools/colors.rs index 0d68553978..e201d3fdc5 100644 --- a/map_gui/src/tools/colors.rs +++ b/map_gui/src/tools/colors.rs @@ -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 diff --git a/sim/src/mechanics/walking.rs b/sim/src/mechanics/walking.rs index f28a50f92c..df39f4e932 100644 --- a/sim/src/mechanics/walking.rs +++ b/sim/src/mechanics/walking.rs @@ -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, BTreeMap) { + 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, ) -> 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() +} diff --git a/sim/src/sim/queries.rs b/sim/src/sim/queries.rs index 555b88ef5e..804b397b39 100644 --- a/sim/src/sim/queries.rs +++ b/sim/src/sim/queries.rs @@ -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> { &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, BTreeMap) { + self.walking.get_pedestrian_density(map) + } } // Drawing