mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-28 03:35:51 +03:00
The simulation analytics tracks problems per trip. The previous measurements for delay at intersections is one case, and the new cyclist-crossing-large-interection event is another. I removed the lane speed measurement, since it's kind of redundant with the delay. Consolidated the reporting in the trip info panel / drawn on the route.
This commit is contained in:
parent
6f5a7b980a
commit
6380bd74ec
@ -5,7 +5,10 @@ use maplit::btreemap;
|
||||
use geom::{Distance, Duration, Percent, Polygon, Pt2D};
|
||||
use map_gui::ID;
|
||||
use map_model::{Map, Path, PathStep};
|
||||
use sim::{AgentID, PersonID, TripEndpoint, TripID, TripMode, TripPhase, TripPhaseType};
|
||||
use sim::{
|
||||
AgentID, Analytics, PersonID, Problem, TripEndpoint, TripID, TripInfo, TripMode, TripPhase,
|
||||
TripPhaseType,
|
||||
};
|
||||
use widgetry::{
|
||||
Color, ControlState, DrawWithTooltips, EventCtx, GeomBatch, Line, LinePlot, PlotOptions,
|
||||
RewriteColor, Series, Text, TextExt, Widget,
|
||||
@ -115,30 +118,13 @@ pub fn ongoing(
|
||||
]),
|
||||
]));
|
||||
}
|
||||
if trip.mode != TripMode::Drive {
|
||||
// TODO Terminology?
|
||||
col.push(Widget::custom_row(vec![
|
||||
Line("Risks")
|
||||
.secondary()
|
||||
.into_widget(ctx)
|
||||
.container()
|
||||
.force_width_pct(ctx, col_width),
|
||||
Text::from_all(vec![
|
||||
Line(
|
||||
app.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.large_intersection_crossings
|
||||
.get(&id)
|
||||
.map(|x| x.len())
|
||||
.unwrap_or(0)
|
||||
.to_string(),
|
||||
),
|
||||
Line(" crossings at large intersections").secondary(),
|
||||
])
|
||||
.into_widget(ctx),
|
||||
]));
|
||||
}
|
||||
col.push(describe_problems(
|
||||
ctx,
|
||||
app.primary.sim.get_analytics(),
|
||||
id,
|
||||
&trip,
|
||||
col_width,
|
||||
));
|
||||
{
|
||||
col.push(Widget::custom_row(vec![
|
||||
Widget::custom_row(vec![Line("Purpose").secondary().into_widget(ctx)])
|
||||
@ -355,33 +341,17 @@ pub fn finished(
|
||||
Line(trip.purpose.to_string()).secondary().into_widget(ctx),
|
||||
]));
|
||||
}
|
||||
// TODO Duplicating some code from ongoing() until we decide how to consolidate things.
|
||||
if trip.mode != TripMode::Drive {
|
||||
let analytics = if open_trips[&id].show_after {
|
||||
col.push(describe_problems(
|
||||
ctx,
|
||||
if open_trips[&id].show_after {
|
||||
app.primary.sim.get_analytics()
|
||||
} else {
|
||||
app.prebaked()
|
||||
};
|
||||
col.push(Widget::custom_row(vec![
|
||||
Line("Risks")
|
||||
.secondary()
|
||||
.into_widget(ctx)
|
||||
.container()
|
||||
.force_width_pct(ctx, col_width),
|
||||
Text::from_all(vec![
|
||||
Line(
|
||||
analytics
|
||||
.large_intersection_crossings
|
||||
.get(&id)
|
||||
.map(|x| x.len())
|
||||
.unwrap_or(0)
|
||||
.to_string(),
|
||||
),
|
||||
Line(" crossings at large intersections").secondary(),
|
||||
])
|
||||
.into_widget(ctx),
|
||||
]));
|
||||
}
|
||||
},
|
||||
id,
|
||||
&trip,
|
||||
col_width,
|
||||
));
|
||||
|
||||
col.push(make_trip_details(
|
||||
ctx,
|
||||
@ -428,94 +398,82 @@ pub fn cancelled(
|
||||
Widget::col(col)
|
||||
}
|
||||
|
||||
/// Highlights intersections which were "slow" on the map
|
||||
fn highlight_slow_intersections(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
|
||||
if let Some(delays) = app
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.trip_intersection_delays
|
||||
.get(&id)
|
||||
{
|
||||
for (id, time) in delays {
|
||||
let intersection = app.primary.map.get_i(id.parent);
|
||||
let (normal_delay_time, slow_delay_time) = if intersection.is_traffic_signal() {
|
||||
(30, 120)
|
||||
} else {
|
||||
(5, 30)
|
||||
};
|
||||
let (fg_color, bg_color) = if *time < normal_delay_time {
|
||||
(Color::WHITE, app.cs.normal_slow_intersection)
|
||||
} else if *time < slow_delay_time {
|
||||
(Color::BLACK, app.cs.slow_intersection)
|
||||
} else {
|
||||
(Color::WHITE, app.cs.very_slow_intersection)
|
||||
};
|
||||
|
||||
let duration = Duration::seconds(*time as f64);
|
||||
details.unzoomed.append(
|
||||
Text::from(Line(format!("{}", duration)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.centered_on(intersection.polygon.center()),
|
||||
);
|
||||
details.zoomed.append(
|
||||
Text::from(Line(format!("{}", duration)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.scale(0.4)
|
||||
.centered_on(intersection.polygon.center()),
|
||||
);
|
||||
fn describe_problems(
|
||||
ctx: &mut EventCtx,
|
||||
analytics: &Analytics,
|
||||
id: TripID,
|
||||
trip: &TripInfo,
|
||||
col_width: Percent,
|
||||
) -> Widget {
|
||||
if trip.mode == TripMode::Bike {
|
||||
let mut count = 0;
|
||||
let empty = Vec::new();
|
||||
for problem in analytics.problems_per_trip.get(&id).unwrap_or(&empty) {
|
||||
if let Problem::LargeIntersectionCrossing(_) = problem {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Widget::custom_row(vec![
|
||||
Line("Risk exposure")
|
||||
.secondary()
|
||||
.into_widget(ctx)
|
||||
.container()
|
||||
.force_width_pct(ctx, col_width),
|
||||
Text::from_all(vec![
|
||||
Line(count.to_string()),
|
||||
// TODO Singular/plural
|
||||
Line(" crossings at large intersections").secondary(),
|
||||
])
|
||||
.into_widget(ctx),
|
||||
])
|
||||
} else {
|
||||
Widget::nothing()
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlights lanes which were "slow" on the map
|
||||
fn highlight_slow_lanes(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
|
||||
if let Some(lane_speeds) = app
|
||||
fn draw_problems(ctx: &EventCtx, app: &App, details: &mut Details, id: TripID) {
|
||||
let empty = Vec::new();
|
||||
for problem in app
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.lane_speed_percentage
|
||||
.problems_per_trip
|
||||
.get(&id)
|
||||
.unwrap_or(&empty)
|
||||
{
|
||||
for (id, speed_percent) in lane_speeds.iter() {
|
||||
let lane = app.primary.map.get_l(*id);
|
||||
let (fg_color, bg_color) = if speed_percent > &95 {
|
||||
(Color::WHITE, app.cs.normal_slow_intersection)
|
||||
} else if speed_percent > &60 {
|
||||
(Color::BLACK, app.cs.slow_intersection)
|
||||
} else {
|
||||
(Color::WHITE, app.cs.very_slow_intersection)
|
||||
};
|
||||
details.unzoomed.push(
|
||||
bg_color,
|
||||
lane.lane_center_pts.make_polygons(Distance::meters(10.0)),
|
||||
);
|
||||
details.zoomed.extend(
|
||||
bg_color,
|
||||
lane.lane_center_pts.dashed_lines(
|
||||
Distance::meters(0.75),
|
||||
Distance::meters(1.0),
|
||||
Distance::meters(0.4),
|
||||
),
|
||||
);
|
||||
let (pt, _) = lane
|
||||
.lane_center_pts
|
||||
.must_dist_along(lane.lane_center_pts.length() / 2.0);
|
||||
details.unzoomed.append(
|
||||
Text::from(Line(format!("{}s", speed_percent)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.centered_on(pt),
|
||||
);
|
||||
details.zoomed.append(
|
||||
Text::from(Line(format!("{}s", speed_percent)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.scale(0.4)
|
||||
.centered_on(pt),
|
||||
);
|
||||
match problem {
|
||||
Problem::IntersectionDelay(i, delay) => {
|
||||
let i = app.primary.map.get_i(*i);
|
||||
// TODO These thresholds don't match what we use as thresholds in the simulation.
|
||||
let (slow, slower) = if i.is_traffic_signal() {
|
||||
(Duration::seconds(30.0), Duration::minutes(2))
|
||||
} else {
|
||||
(Duration::seconds(5.0), Duration::seconds(30.0))
|
||||
};
|
||||
let (fg_color, bg_color) = if *delay < slow {
|
||||
(Color::WHITE, app.cs.slow_intersection)
|
||||
} else if *delay < slower {
|
||||
(Color::BLACK, app.cs.slower_intersection)
|
||||
} else {
|
||||
(Color::WHITE, app.cs.slowest_intersection)
|
||||
};
|
||||
details.unzoomed.append(
|
||||
Text::from(Line(format!("{}", delay)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.centered_on(i.polygon.center()),
|
||||
);
|
||||
details.zoomed.append(
|
||||
Text::from(Line(format!("{}", delay)).fg(fg_color))
|
||||
.bg(bg_color)
|
||||
.render(ctx)
|
||||
.scale(0.4)
|
||||
.centered_on(i.polygon.center()),
|
||||
);
|
||||
}
|
||||
Problem::LargeIntersectionCrossing(_) => {
|
||||
// TODO Maybe a caution icon?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -802,8 +760,7 @@ fn make_trip_details(
|
||||
col.push("Map edits have disconnected the path taken before".text_widget(ctx));
|
||||
}
|
||||
col.extend(elevation);
|
||||
highlight_slow_intersections(ctx, app, details, trip_id);
|
||||
highlight_slow_lanes(ctx, app, details, trip_id);
|
||||
draw_problems(ctx, app, details, trip_id);
|
||||
Widget::col(col)
|
||||
}
|
||||
|
||||
|
@ -99,10 +99,10 @@ pub struct ColorScheme {
|
||||
pub signal_spinner: Color,
|
||||
pub signal_turn_block_bg: Color,
|
||||
|
||||
// Timeline delay highlighting
|
||||
pub very_slow_intersection: Color,
|
||||
// Problems encountered on a trip
|
||||
pub slowest_intersection: Color,
|
||||
pub slower_intersection: Color,
|
||||
pub slow_intersection: Color,
|
||||
pub normal_slow_intersection: Color,
|
||||
|
||||
// Other static elements
|
||||
pub void_background: Color,
|
||||
@ -233,10 +233,10 @@ impl ColorScheme {
|
||||
signal_spinner: hex("#F2994A"),
|
||||
signal_turn_block_bg: Color::grey(0.6),
|
||||
|
||||
// Timeline delay highlighting
|
||||
very_slow_intersection: Color::RED,
|
||||
slow_intersection: Color::YELLOW,
|
||||
normal_slow_intersection: Color::GREEN,
|
||||
// Problems encountered on a trip
|
||||
slowest_intersection: Color::RED,
|
||||
slower_intersection: Color::YELLOW,
|
||||
slow_intersection: Color::GREEN,
|
||||
|
||||
// Other static elements
|
||||
void_background: Color::BLACK,
|
||||
|
@ -6,12 +6,11 @@ use abstutil::Counter;
|
||||
use geom::{Duration, Time};
|
||||
use map_model::{
|
||||
BusRouteID, BusStopID, CompressedMovementID, IntersectionID, LaneID, Map, MovementID,
|
||||
ParkingLotID, Path, PathRequest, RoadID, Traversable, TurnID, TurnType,
|
||||
ParkingLotID, Path, PathRequest, RoadID, Traversable, TurnType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AgentID, AgentType, AlertLocation, CarID, Event, ParkingSpot, TripID, TripMode, TripPhaseType,
|
||||
VehicleType,
|
||||
};
|
||||
|
||||
/// As a simulation runs, different pieces emit Events. The Analytics object listens to these,
|
||||
@ -45,17 +44,8 @@ pub struct Analytics {
|
||||
/// Finish time, ID, mode, trip duration if successful (or None if cancelled)
|
||||
pub finished_trips: Vec<(Time, TripID, TripMode, Option<Duration>)>,
|
||||
|
||||
/// Records how long was spent waiting at each turn (Intersection) for a given trip
|
||||
/// Over a certain threshold
|
||||
/// TripID, [(TurnID, Time Waiting In Seconds)]
|
||||
pub trip_intersection_delays: BTreeMap<TripID, BTreeMap<TurnID, u8>>,
|
||||
/// Records the average speed/maximum speed for each lane
|
||||
/// If it is over a certain threshold (<95% of max speed)
|
||||
/// TripID, [(LaneID, Percent of maximum speed as an integer (0-100)]
|
||||
pub lane_speed_percentage: BTreeMap<TripID, BTreeMap<LaneID, u8>>,
|
||||
/// Record every instance of somebody on foot or a bike crossing an intersection with more than
|
||||
/// 4 connecting roads.
|
||||
pub large_intersection_crossings: BTreeMap<TripID, Vec<(Time, IntersectionID)>>,
|
||||
/// Record different problems that each trip encounters.
|
||||
pub problems_per_trip: BTreeMap<TripID, Vec<Problem>>,
|
||||
|
||||
// TODO This subsumes finished_trips
|
||||
pub trip_log: Vec<(Time, TripID, Option<PathRequest>, TripPhaseType)>,
|
||||
@ -74,6 +64,14 @@ pub struct Analytics {
|
||||
record_anything: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum Problem {
|
||||
/// A vehicle waited >30s, or a pedestrian waited >15s.
|
||||
IntersectionDelay(IntersectionID, Duration),
|
||||
/// A cyclist crossed an intersection with >4 connecting roads.
|
||||
LargeIntersectionCrossing(IntersectionID),
|
||||
}
|
||||
|
||||
impl Analytics {
|
||||
pub fn new(record_anything: bool) -> Analytics {
|
||||
Analytics {
|
||||
@ -86,9 +84,7 @@ impl Analytics {
|
||||
passengers_alighting: BTreeMap::new(),
|
||||
started_trips: BTreeMap::new(),
|
||||
finished_trips: Vec::new(),
|
||||
trip_intersection_delays: BTreeMap::new(),
|
||||
lane_speed_percentage: BTreeMap::new(),
|
||||
large_intersection_crossings: BTreeMap::new(),
|
||||
problems_per_trip: BTreeMap::new(),
|
||||
trip_log: Vec::new(),
|
||||
intersection_delays: BTreeMap::new(),
|
||||
parking_lane_changes: BTreeMap::new(),
|
||||
@ -197,24 +193,17 @@ impl Analytics {
|
||||
|
||||
// Intersection delay
|
||||
if let Event::IntersectionDelayMeasured(trip_id, turn_id, agent, delay) = ev {
|
||||
match agent {
|
||||
AgentID::Car(_) => {
|
||||
if delay > Duration::seconds(30.0) {
|
||||
self.trip_intersection_delays
|
||||
.entry(trip_id)
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(turn_id, delay.inner_seconds() as u8);
|
||||
}
|
||||
}
|
||||
AgentID::Pedestrian(_) => {
|
||||
if delay > Duration::seconds(15.0) {
|
||||
self.trip_intersection_delays
|
||||
.entry(trip_id)
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(turn_id, delay.inner_seconds() as u8);
|
||||
}
|
||||
}
|
||||
AgentID::BusPassenger(_, _) => {}
|
||||
let threshold = match agent {
|
||||
AgentID::Car(_) => Duration::seconds(30.0),
|
||||
AgentID::Pedestrian(_) => Duration::seconds(15.0),
|
||||
// Don't record for riders
|
||||
AgentID::BusPassenger(_, _) => Duration::hours(24),
|
||||
};
|
||||
if delay > threshold {
|
||||
self.problems_per_trip
|
||||
.entry(trip_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(Problem::IntersectionDelay(turn_id.parent, delay));
|
||||
}
|
||||
|
||||
// SharedSidewalkCorner are always no-conflict, immediate turns; they're not
|
||||
@ -230,17 +219,6 @@ impl Analytics {
|
||||
}
|
||||
}
|
||||
|
||||
// Lane Speed
|
||||
if let Event::LaneSpeedPercentage(trip_id, lane_id, avg_speed, max_speed) = ev {
|
||||
let speed_percent: u8 = ((avg_speed / max_speed) * 100.0) as u8;
|
||||
if speed_percent < 95 {
|
||||
self.lane_speed_percentage
|
||||
.entry(trip_id)
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(lane_id, speed_percent);
|
||||
}
|
||||
}
|
||||
|
||||
// Parking spot changes
|
||||
if let Event::CarReachedParkingSpot(_, spot) = ev {
|
||||
if let ParkingSpot::Onstreet(l, _) = spot {
|
||||
@ -271,26 +249,14 @@ impl Analytics {
|
||||
|
||||
// Safety metrics
|
||||
if let Event::AgentEntersTraversable(a, Some(trip), Traversable::Turn(t), _) = ev {
|
||||
if match a {
|
||||
AgentID::Pedestrian(_) => true,
|
||||
AgentID::Car(c) => c.1 == VehicleType::Bike,
|
||||
_ => false,
|
||||
} {
|
||||
//
|
||||
// - If a pedestrian never enters a crosswalk at a large intersection, don't record.
|
||||
// - Note one intersection will get counted multiple times if a pedestrian uses
|
||||
// several crosswalks there.
|
||||
// - Defining a "large intersection" is tricky. If a road is split into two
|
||||
// one-ways, should we count it as two roads? If we haven't consolidated some
|
||||
// crazy intersection, we won't see it.
|
||||
if map.get_t(t).turn_type != TurnType::SharedSidewalkCorner
|
||||
&& map.get_i(t.parent).roads.len() > 4
|
||||
{
|
||||
self.large_intersection_crossings
|
||||
.entry(trip)
|
||||
.or_insert_with(Vec::new)
|
||||
.push((time, t.parent));
|
||||
}
|
||||
if a.to_type() == AgentType::Bike && map.get_i(t.parent).roads.len() > 4 {
|
||||
// Defining a "large intersection" is tricky. If a road is split into two one-ways,
|
||||
// should we count it as two roads? If we haven't consolidated some crazy
|
||||
// intersection, we won't see it.
|
||||
self.problems_per_trip
|
||||
.entry(trip)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(Problem::LargeIntersectionCrossing(t.parent));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use geom::{Duration, Speed};
|
||||
use geom::Duration;
|
||||
use map_model::{
|
||||
BuildingID, BusRouteID, BusStopID, IntersectionID, LaneID, Map, Path, PathRequest, Traversable,
|
||||
TurnID,
|
||||
@ -51,8 +51,6 @@ pub enum Event {
|
||||
},
|
||||
TripCancelled(TripID, TripMode),
|
||||
TripPhaseStarting(TripID, PersonID, Option<PathRequest>, TripPhaseType),
|
||||
/// TripID, LaneID (Where the delay was encountered), Average Speed, Max Speed
|
||||
LaneSpeedPercentage(TripID, LaneID, Speed, Speed),
|
||||
|
||||
/// Just use for parking replanning. Not happy about copying the full path in here, but the way
|
||||
/// to plumb info into Analytics is Event.
|
||||
|
@ -30,7 +30,7 @@ pub use crate::render::{
|
||||
UnzoomedAgent,
|
||||
};
|
||||
|
||||
pub use self::analytics::{Analytics, TripPhase};
|
||||
pub use self::analytics::{Analytics, Problem, TripPhase};
|
||||
pub(crate) use self::cap::CapSimState;
|
||||
pub(crate) use self::events::Event;
|
||||
pub use self::events::{AlertLocation, TripPhaseType};
|
||||
|
@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use abstutil::{deserialize_hashmap, serialize_hashmap, FixedMap, IndexableKey};
|
||||
use geom::{Distance, Duration, PolyLine, Speed, Time};
|
||||
use geom::{Distance, Duration, PolyLine, Time};
|
||||
use map_model::{IntersectionID, LaneID, Map, Path, Position, Traversable};
|
||||
|
||||
use crate::mechanics::car::{Car, CarState};
|
||||
@ -272,23 +272,7 @@ impl DrivingSimState {
|
||||
transit: &mut TransitSimState,
|
||||
) -> bool {
|
||||
match car.state {
|
||||
CarState::Crossing(time_int, dist_int) => {
|
||||
if let Some((trip, _)) = car.trip_and_person {
|
||||
if let Traversable::Lane(lane) = car.router.head() {
|
||||
let time_to_cross = now - time_int.start;
|
||||
if time_to_cross > Duration::ZERO {
|
||||
let avg_speed = Speed::from_dist_time(dist_int.length(), time_to_cross);
|
||||
let max_speed = car.router.head().max_speed_along(
|
||||
car.vehicle.max_speed,
|
||||
car.vehicle.vehicle_type.to_constraints(),
|
||||
ctx.map,
|
||||
);
|
||||
self.events
|
||||
.push(Event::LaneSpeedPercentage(trip, lane, avg_speed, max_speed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CarState::Crossing(_, _) => {
|
||||
car.state = CarState::Queued { blocked_since: now };
|
||||
if car.router.last_step() {
|
||||
// Immediately run update_car_with_distances.
|
||||
|
Loading…
Reference in New Issue
Block a user