mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-29 17:34:58 +03:00
reorganizing info panel code, splitting into modules
This commit is contained in:
parent
fb8072d7b7
commit
e22727ee98
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
||||
mod bus_explorer;
|
||||
mod colors;
|
||||
mod heatmap;
|
||||
mod info;
|
||||
mod minimap;
|
||||
mod navigate;
|
||||
mod overlays;
|
||||
@ -20,6 +19,7 @@ pub use self::warp::Warping;
|
||||
use crate::app::App;
|
||||
use crate::game::Transition;
|
||||
use crate::helpers::{list_names, ID};
|
||||
use crate::info::{InfoPanel, InfoTab};
|
||||
use crate::sandbox::SpeedControls;
|
||||
use ezgui::{
|
||||
lctrl, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, ScreenDims, ScreenPt, ScreenRectangle,
|
||||
@ -30,7 +30,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
pub struct CommonState {
|
||||
turn_cycler: turn_cycler::TurnCyclerState,
|
||||
info_panel: Option<info::InfoPanel>,
|
||||
info_panel: Option<InfoPanel>,
|
||||
}
|
||||
|
||||
impl CommonState {
|
||||
@ -68,9 +68,9 @@ impl CommonState {
|
||||
{
|
||||
app.per_obj.info_panel_open = true;
|
||||
let actions = app.per_obj.consume();
|
||||
self.info_panel = Some(info::InfoPanel::new(
|
||||
self.info_panel = Some(InfoPanel::new(
|
||||
id.clone(),
|
||||
info::Tab::Nil,
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
actions,
|
||||
@ -287,14 +287,7 @@ impl CommonState {
|
||||
|
||||
// Meant to be used for launching from other states
|
||||
pub fn launch_info_panel(&mut self, id: ID, ctx: &mut EventCtx, app: &mut App) {
|
||||
self.info_panel = Some(info::InfoPanel::new(
|
||||
id,
|
||||
info::Tab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
None,
|
||||
));
|
||||
self.info_panel = Some(InfoPanel::new(id, InfoTab::Nil, ctx, app, Vec::new(), None));
|
||||
app.per_obj.info_panel_open = true;
|
||||
}
|
||||
|
||||
|
129
game/src/info/agents.rs
Normal file
129
game/src/info/agents.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use crate::app::App;
|
||||
use crate::info::trip::trip_details;
|
||||
use crate::info::{make_table, TripDetails};
|
||||
use crate::render::Renderable;
|
||||
use ezgui::{Color, EventCtx, GeomBatch, Line, Text, Widget};
|
||||
use sim::{AgentID, CarID, PedestrianID, VehicleType};
|
||||
|
||||
pub fn car_info(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
id: CarID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
batch: &mut GeomBatch,
|
||||
) -> (Vec<Widget>, Option<TripDetails>) {
|
||||
let mut rows = vec![];
|
||||
|
||||
let label = match id.1 {
|
||||
VehicleType::Car => "Car",
|
||||
VehicleType::Bike => "Bike",
|
||||
VehicleType::Bus => "Bus",
|
||||
};
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("{} #{}", label, id.0)).roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let (kv, extra) = app.primary.sim.car_properties(id, &app.primary.map);
|
||||
rows.extend(make_table(ctx, kv));
|
||||
if !extra.is_empty() {
|
||||
let mut txt = Text::from(Line(""));
|
||||
for line in extra {
|
||||
txt.add(Line(line));
|
||||
}
|
||||
rows.push(txt.draw(ctx));
|
||||
}
|
||||
|
||||
let trip = if id.1 == VehicleType::Bus {
|
||||
None
|
||||
} else {
|
||||
app.primary.sim.agent_to_trip(AgentID::Car(id))
|
||||
};
|
||||
let details = trip.map(|t| {
|
||||
let (more, details) = trip_details(
|
||||
ctx,
|
||||
app,
|
||||
t,
|
||||
app.primary.sim.progress_along_path(AgentID::Car(id)),
|
||||
);
|
||||
rows.push(more);
|
||||
details
|
||||
});
|
||||
|
||||
if let Some(b) = app.primary.sim.get_owner_of_car(id) {
|
||||
// TODO Mention this, with a warp tool
|
||||
batch.push(
|
||||
app.cs
|
||||
.get_def("something associated with something else", Color::PURPLE),
|
||||
app.primary.draw_map.get_b(b).get_outline(&app.primary.map),
|
||||
);
|
||||
}
|
||||
|
||||
(rows, details)
|
||||
}
|
||||
|
||||
pub fn ped_info(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
id: PedestrianID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> (Vec<Widget>, Option<TripDetails>) {
|
||||
let mut rows = vec![];
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Pedestrian #{}", id.0))
|
||||
.roboto_bold()
|
||||
.draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let (kv, extra) = app.primary.sim.ped_properties(id, &app.primary.map);
|
||||
rows.extend(make_table(ctx, kv));
|
||||
if !extra.is_empty() {
|
||||
let mut txt = Text::from(Line(""));
|
||||
for line in extra {
|
||||
txt.add(Line(line));
|
||||
}
|
||||
rows.push(txt.draw(ctx));
|
||||
}
|
||||
|
||||
let (more, details) = trip_details(
|
||||
ctx,
|
||||
app,
|
||||
app.primary
|
||||
.sim
|
||||
.agent_to_trip(AgentID::Pedestrian(id))
|
||||
.unwrap(),
|
||||
app.primary.sim.progress_along_path(AgentID::Pedestrian(id)),
|
||||
);
|
||||
rows.push(more);
|
||||
|
||||
(rows, Some(details))
|
||||
}
|
||||
|
||||
// TODO Embedded panel is perfect here
|
||||
pub fn crowd_info(
|
||||
ctx: &EventCtx,
|
||||
_app: &App,
|
||||
members: Vec<PedestrianID>,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line("Pedestrian crowd").roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add(Line(format!("Crowd of {}", members.len())));
|
||||
rows.push(txt.draw(ctx));
|
||||
|
||||
rows
|
||||
}
|
145
game/src/info/building.rs
Normal file
145
game/src/info/building.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use crate::app::App;
|
||||
use crate::colors;
|
||||
use crate::helpers::ID;
|
||||
use crate::info::{make_table, person, InfoTab};
|
||||
use ezgui::{hotkey, Btn, EventCtx, GeomBatch, Key, Line, Text, TextExt, Widget};
|
||||
use map_model::BuildingID;
|
||||
|
||||
pub fn info(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: BuildingID,
|
||||
tab: InfoTab,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
batch: &mut GeomBatch,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
let b = app.primary.map.get_b(id);
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Building #{}", id.0)).roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
// Properties
|
||||
{
|
||||
let mut kv = Vec::new();
|
||||
|
||||
kv.push(("Address".to_string(), b.just_address(&app.primary.map)));
|
||||
if let Some(name) = b.just_name() {
|
||||
kv.push(("Name".to_string(), name.to_string()));
|
||||
}
|
||||
|
||||
if let Some(ref p) = b.parking {
|
||||
kv.push((
|
||||
"Parking".to_string(),
|
||||
format!("{} spots via {}", p.num_stalls, p.name),
|
||||
));
|
||||
} else {
|
||||
kv.push(("Parking".to_string(), "None".to_string()));
|
||||
}
|
||||
|
||||
if app.opts.dev {
|
||||
kv.push((
|
||||
"Dist along sidewalk".to_string(),
|
||||
b.front_path.sidewalk.dist_along().to_string(),
|
||||
));
|
||||
|
||||
for (k, v) in &b.osm_tags {
|
||||
kv.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
rows.extend(make_table(ctx, kv));
|
||||
}
|
||||
|
||||
let mut txt = Text::new();
|
||||
let trip_lines = app.primary.sim.count_trips_involving_bldg(id).describe();
|
||||
if !trip_lines.is_empty() {
|
||||
txt.add(Line(""));
|
||||
for line in trip_lines {
|
||||
txt.add(Line(line));
|
||||
}
|
||||
}
|
||||
|
||||
let cars = app.primary.sim.get_parked_cars_by_owner(id);
|
||||
if !cars.is_empty() {
|
||||
txt.add(Line(""));
|
||||
txt.add(Line(format!(
|
||||
"{} parked cars owned by this building",
|
||||
cars.len()
|
||||
)));
|
||||
// TODO Jump to it or see status
|
||||
for p in cars {
|
||||
txt.add(Line(format!("- {}", p.vehicle.id)));
|
||||
}
|
||||
}
|
||||
|
||||
if !b.amenities.is_empty() {
|
||||
txt.add(Line(""));
|
||||
if b.amenities.len() > 1 {
|
||||
txt.add(Line(format!("{} amenities:", b.amenities.len())));
|
||||
}
|
||||
for (name, amenity) in &b.amenities {
|
||||
txt.add(Line(format!("- {} (a {})", name, amenity)));
|
||||
}
|
||||
}
|
||||
|
||||
if !txt.is_empty() {
|
||||
rows.push(txt.draw(ctx))
|
||||
}
|
||||
|
||||
match tab {
|
||||
InfoTab::Nil => {
|
||||
let num = app.primary.sim.bldg_to_people(id).len();
|
||||
if num > 0 {
|
||||
rows.push(
|
||||
Btn::text_bg1(format!("{} people inside", num))
|
||||
.build(ctx, "examine people inside", None)
|
||||
.margin(5),
|
||||
);
|
||||
}
|
||||
}
|
||||
InfoTab::BldgPeople(ppl, idx) => {
|
||||
let mut inner = vec![
|
||||
// TODO Keys are weird! But left/right for speed
|
||||
Widget::row(vec![
|
||||
Btn::text_fg("<")
|
||||
.build(ctx, "previous", hotkey(Key::UpArrow))
|
||||
.margin(5),
|
||||
format!("Occupant {}/{}", idx + 1, ppl.len()).draw_text(ctx),
|
||||
Btn::text_fg(">")
|
||||
.build(ctx, "next", hotkey(Key::DownArrow))
|
||||
.margin(5),
|
||||
Btn::text_fg("X")
|
||||
.build(ctx, "close occupants panel", None)
|
||||
.align_right(),
|
||||
])
|
||||
.centered(),
|
||||
];
|
||||
inner.extend(person::info(ctx, app, ppl[idx], None, Vec::new()));
|
||||
rows.push(Widget::col(inner).bg(colors::INNER_PANEL_BG));
|
||||
}
|
||||
}
|
||||
|
||||
for p in app.primary.sim.get_parked_cars_by_owner(id) {
|
||||
batch.push(
|
||||
app.cs.get("something associated with something else"),
|
||||
app.primary
|
||||
.draw_map
|
||||
.get_obj(
|
||||
ID::Car(p.vehicle.id),
|
||||
app,
|
||||
&mut app.primary.draw_map.agents.borrow_mut(),
|
||||
ctx.prerender,
|
||||
)
|
||||
.unwrap()
|
||||
.get_outline(&app.primary.map),
|
||||
);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
55
game/src/info/bus_stop.rs
Normal file
55
game/src/info/bus_stop.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::app::App;
|
||||
use ezgui::{EventCtx, Line, Text, Widget};
|
||||
use geom::Time;
|
||||
use map_model::BusStopID;
|
||||
use sim::CarID;
|
||||
|
||||
pub fn info(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: BusStopID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
let sim = &app.primary.sim;
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line("Bus stop").roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add(Line(format!(
|
||||
"On {}",
|
||||
app.primary.map.get_parent(id.sidewalk).get_name()
|
||||
)));
|
||||
let all_arrivals = &sim.get_analytics().bus_arrivals;
|
||||
for r in app.primary.map.get_routes_serving_stop(id) {
|
||||
txt.add(Line(format!("- Route {}", r.name)).roboto_bold());
|
||||
let arrivals: Vec<(Time, CarID)> = all_arrivals
|
||||
.iter()
|
||||
.filter(|(_, _, route, stop)| r.id == *route && id == *stop)
|
||||
.map(|(t, car, _, _)| (*t, *car))
|
||||
.collect();
|
||||
if let Some((t, _)) = arrivals.last() {
|
||||
// TODO Button to jump to the bus
|
||||
txt.add(Line(format!(" Last bus arrived {} ago", sim.time() - *t)));
|
||||
} else {
|
||||
txt.add(Line(" No arrivals yet"));
|
||||
}
|
||||
// TODO Kind of inefficient...
|
||||
if let Some(hgram) = sim
|
||||
.get_analytics()
|
||||
.bus_passenger_delays(sim.time(), r.id)
|
||||
.remove(&id)
|
||||
{
|
||||
txt.add(Line(format!(" Waiting: {}", hgram.describe())));
|
||||
}
|
||||
}
|
||||
rows.push(txt.draw(ctx));
|
||||
|
||||
rows
|
||||
}
|
57
game/src/info/debug.rs
Normal file
57
game/src/info/debug.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::app::App;
|
||||
use crate::info::make_table;
|
||||
use crate::render::ExtraShapeID;
|
||||
use ezgui::{EventCtx, Line, Widget};
|
||||
use map_model::AreaID;
|
||||
|
||||
pub fn area(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: AreaID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Area #{}", id.0)).roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let a = app.primary.map.get_a(id);
|
||||
let mut kv = Vec::new();
|
||||
for (k, v) in &a.osm_tags {
|
||||
kv.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
rows.extend(make_table(ctx, kv));
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
pub fn extra_shape(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: ExtraShapeID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Extra GIS shape #{}", id.0))
|
||||
.roboto_bold()
|
||||
.draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let es = app.primary.draw_map.get_es(id);
|
||||
let mut kv = Vec::new();
|
||||
for (k, v) in &es.attributes {
|
||||
kv.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
rows.extend(make_table(ctx, kv));
|
||||
|
||||
rows
|
||||
}
|
130
game/src/info/intersection.rs
Normal file
130
game/src/info/intersection.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use crate::app::App;
|
||||
use crate::helpers::rotating_color_map;
|
||||
use crate::info::throughput;
|
||||
use abstutil::prettyprint_usize;
|
||||
use ezgui::{EventCtx, Line, Plot, PlotOptions, Series, Text, Widget};
|
||||
use geom::{Duration, Statistic, Time};
|
||||
use map_model::{IntersectionID, IntersectionType};
|
||||
use sim::Analytics;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub fn info(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: IntersectionID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
let i = app.primary.map.get_i(id);
|
||||
|
||||
let label = match i.intersection_type {
|
||||
IntersectionType::StopSign => format!("Intersection #{} (Stop signs)", id.0),
|
||||
IntersectionType::TrafficSignal => format!("Intersection #{} (Traffic signals)", id.0),
|
||||
IntersectionType::Border => format!("Border #{}", id.0),
|
||||
IntersectionType::Construction => format!("Intersection #{} (under construction)", id.0),
|
||||
};
|
||||
rows.push(Widget::row(vec![
|
||||
Line(label).roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
|
||||
let mut txt = Text::from(Line("Connecting"));
|
||||
let mut road_names = BTreeSet::new();
|
||||
for r in &i.roads {
|
||||
road_names.insert(app.primary.map.get_r(*r).get_name());
|
||||
}
|
||||
for r in road_names {
|
||||
// TODO The spacing is ignored, so use -
|
||||
txt.add(Line(format!("- {}", r)));
|
||||
}
|
||||
|
||||
rows.extend(action_btns);
|
||||
|
||||
let trip_lines = app.primary.sim.count_trips_involving_border(id).describe();
|
||||
if !trip_lines.is_empty() {
|
||||
txt.add(Line(""));
|
||||
for line in trip_lines {
|
||||
txt.add(Line(line));
|
||||
}
|
||||
}
|
||||
|
||||
txt.add(Line("Throughput").roboto_bold());
|
||||
txt.add(Line(format!(
|
||||
"Since midnight: {} agents crossed",
|
||||
prettyprint_usize(
|
||||
app.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.thruput_stats
|
||||
.count_per_intersection
|
||||
.get(id)
|
||||
)
|
||||
)));
|
||||
txt.add(Line(format!("In 20 minute buckets:")));
|
||||
rows.push(txt.draw(ctx));
|
||||
|
||||
rows.push(
|
||||
throughput(ctx, app, move |a, t| {
|
||||
a.throughput_intersection(t, id, Duration::minutes(20))
|
||||
})
|
||||
.margin(10),
|
||||
);
|
||||
|
||||
if app.primary.map.get_i(id).is_traffic_signal() {
|
||||
let mut txt = Text::from(Line(""));
|
||||
txt.add(Line("Delay").roboto_bold());
|
||||
txt.add(Line(format!("In 20 minute buckets:")));
|
||||
rows.push(txt.draw(ctx));
|
||||
|
||||
rows.push(delay(ctx, app, id, Duration::minutes(20)).margin(10));
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn delay(ctx: &EventCtx, app: &App, i: IntersectionID, bucket: Duration) -> Widget {
|
||||
let get_data = |a: &Analytics, t: Time| {
|
||||
let mut series: Vec<(Statistic, Vec<(Time, Duration)>)> = Statistic::all()
|
||||
.into_iter()
|
||||
.map(|stat| (stat, Vec::new()))
|
||||
.collect();
|
||||
for (t, distrib) in a.intersection_delays_bucketized(t, i, bucket) {
|
||||
for (stat, pts) in series.iter_mut() {
|
||||
if distrib.count() == 0 {
|
||||
pts.push((t, Duration::ZERO));
|
||||
} else {
|
||||
pts.push((t, distrib.select(*stat)));
|
||||
}
|
||||
}
|
||||
}
|
||||
series
|
||||
};
|
||||
|
||||
let mut all_series = Vec::new();
|
||||
for (idx, (stat, pts)) in get_data(app.primary.sim.get_analytics(), app.primary.sim.time())
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
all_series.push(Series {
|
||||
label: stat.to_string(),
|
||||
color: rotating_color_map(idx),
|
||||
pts,
|
||||
});
|
||||
}
|
||||
if app.has_prebaked().is_some() {
|
||||
for (idx, (stat, pts)) in get_data(app.prebaked(), Time::END_OF_DAY)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
all_series.push(Series {
|
||||
label: format!("{} (baseline)", stat),
|
||||
color: rotating_color_map(idx).alpha(0.3),
|
||||
pts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Plot::new_duration(ctx, all_series, PlotOptions::new())
|
||||
}
|
126
game/src/info/lane.rs
Normal file
126
game/src/info/lane.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use crate::app::App;
|
||||
use crate::info::{make_table, throughput};
|
||||
use abstutil::prettyprint_usize;
|
||||
use ezgui::{EventCtx, Line, Text, TextExt, Widget};
|
||||
use geom::Duration;
|
||||
use map_model::LaneID;
|
||||
|
||||
pub fn info(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: LaneID,
|
||||
header_btns: Widget,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
let map = &app.primary.map;
|
||||
|
||||
let l = map.get_l(id);
|
||||
let r = map.get_r(l.parent);
|
||||
|
||||
let label = if l.is_sidewalk() { "Sidewalk" } else { "Lane" };
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("{} #{}", label, id.0)).roboto_bold().draw(ctx),
|
||||
header_btns,
|
||||
]));
|
||||
rows.push(format!("@ {}", r.get_name()).draw_text(ctx));
|
||||
rows.extend(action_btns);
|
||||
|
||||
// Properties
|
||||
{
|
||||
let mut kv = Vec::new();
|
||||
|
||||
if !l.is_sidewalk() {
|
||||
kv.push(("Type".to_string(), l.lane_type.describe().to_string()));
|
||||
}
|
||||
|
||||
if l.is_parking() {
|
||||
kv.push((
|
||||
"Parking".to_string(),
|
||||
format!("{} spots, parallel parking", l.number_parking_spots()),
|
||||
));
|
||||
} else {
|
||||
kv.push(("Speed limit".to_string(), r.get_speed_limit().to_string()));
|
||||
}
|
||||
|
||||
kv.push(("Length".to_string(), l.length().describe_rounded()));
|
||||
|
||||
if app.opts.dev {
|
||||
kv.push(("Parent".to_string(), r.id.to_string()));
|
||||
|
||||
if l.is_driving() {
|
||||
kv.push((
|
||||
"Parking blackhole redirect".to_string(),
|
||||
format!("{:?}", l.parking_blackhole),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(types) = l.get_turn_restrictions(r) {
|
||||
kv.push(("Turn restrictions".to_string(), format!("{:?}", types)));
|
||||
}
|
||||
for (restriction, to) in &r.turn_restrictions {
|
||||
kv.push((
|
||||
format!("Restriction from this road to {}", to),
|
||||
format!("{:?}", restriction),
|
||||
));
|
||||
}
|
||||
|
||||
// TODO Simplify and expose everywhere after there's better data
|
||||
kv.push((
|
||||
"Elevation change".to_string(),
|
||||
format!(
|
||||
"{} to {}",
|
||||
map.get_i(l.src_i).elevation,
|
||||
map.get_i(l.dst_i).elevation
|
||||
),
|
||||
));
|
||||
kv.push((
|
||||
"Incline / grade".to_string(),
|
||||
format!("{:.1}%", l.percent_grade(map) * 100.0),
|
||||
));
|
||||
kv.push((
|
||||
"Elevation details".to_string(),
|
||||
format!(
|
||||
"{} over {}",
|
||||
map.get_i(l.dst_i).elevation - map.get_i(l.src_i).elevation,
|
||||
l.length()
|
||||
),
|
||||
));
|
||||
|
||||
for (k, v) in &r.osm_tags {
|
||||
kv.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
rows.extend(make_table(ctx, kv));
|
||||
}
|
||||
|
||||
if !l.is_parking() {
|
||||
let mut txt = Text::from(Line(""));
|
||||
txt.add(Line("Throughput (entire road)").roboto_bold());
|
||||
txt.add(Line(format!(
|
||||
"Since midnight: {} agents crossed",
|
||||
prettyprint_usize(
|
||||
app.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.thruput_stats
|
||||
.count_per_road
|
||||
.get(r.id)
|
||||
)
|
||||
)));
|
||||
txt.add(Line(format!("In 20 minute buckets:")));
|
||||
rows.push(txt.draw(ctx));
|
||||
|
||||
let r = app.primary.map.get_l(id).parent;
|
||||
rows.push(
|
||||
throughput(ctx, app, move |a, t| {
|
||||
a.throughput_road(t, r, Duration::minutes(20))
|
||||
})
|
||||
.margin(10),
|
||||
);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
540
game/src/info/mod.rs
Normal file
540
game/src/info/mod.rs
Normal file
@ -0,0 +1,540 @@
|
||||
mod agents;
|
||||
mod building;
|
||||
mod bus_stop;
|
||||
mod debug;
|
||||
mod intersection;
|
||||
mod lane;
|
||||
mod person;
|
||||
mod trip;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::colors;
|
||||
use crate::common::Warping;
|
||||
use crate::game::{msg, State, Transition, WizardState};
|
||||
use crate::helpers::ID;
|
||||
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
||||
use crate::sandbox::{SandboxMode, SpeedControls};
|
||||
use ezgui::{
|
||||
hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
|
||||
Line, Outcome, Plot, PlotOptions, Series, Text, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Circle, Distance, Time};
|
||||
use map_model::BuildingID;
|
||||
use sim::{AgentID, Analytics, PersonID, TripID, TripMode, TripResult, VehicleType};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
pub struct InfoPanel {
|
||||
pub id: ID,
|
||||
tab: InfoTab,
|
||||
time: Time,
|
||||
composite: Composite,
|
||||
|
||||
also_draw: Drawable,
|
||||
trip_details: Option<TripDetails>,
|
||||
|
||||
actions: Vec<(Key, String)>,
|
||||
}
|
||||
|
||||
// TODO Safer to expand out ID cases here
|
||||
#[derive(Clone)]
|
||||
pub enum InfoTab {
|
||||
Nil,
|
||||
// If we're live updating, the people inside could change! We're choosing to freeze the list
|
||||
// here.
|
||||
BldgPeople(Vec<PersonID>, usize),
|
||||
}
|
||||
|
||||
pub struct TripDetails {
|
||||
id: TripID,
|
||||
unzoomed: Drawable,
|
||||
zoomed: Drawable,
|
||||
markers: HashMap<String, ID>,
|
||||
}
|
||||
|
||||
impl InfoPanel {
|
||||
pub fn new(
|
||||
id: ID,
|
||||
tab: InfoTab,
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
mut actions: Vec<(Key, String)>,
|
||||
maybe_speed: Option<&mut SpeedControls>,
|
||||
) -> InfoPanel {
|
||||
if maybe_speed.map(|s| s.is_paused()).unwrap_or(false)
|
||||
&& id.agent_id().is_some()
|
||||
&& actions
|
||||
.get(0)
|
||||
.map(|(_, a)| a != "follow agent")
|
||||
.unwrap_or(true)
|
||||
{
|
||||
actions.insert(0, (Key::F, "follow agent".to_string()));
|
||||
}
|
||||
|
||||
let action_btns = actions
|
||||
.iter()
|
||||
.map(|(key, label)| {
|
||||
let mut txt = Text::new();
|
||||
txt.append(Line(key.describe()).fg(ezgui::HOTKEY_COLOR));
|
||||
txt.append(Line(format!(" - {}", label)));
|
||||
Btn::text_bg(label, txt, colors::SECTION_BG, colors::HOVERING)
|
||||
.build_def(ctx, hotkey(*key))
|
||||
.margin(5)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut batch = GeomBatch::new();
|
||||
// TODO Handle transitions between peds and crowds better
|
||||
if let Some(obj) = app.primary.draw_map.get_obj(
|
||||
id.clone(),
|
||||
app,
|
||||
&mut app.primary.draw_map.agents.borrow_mut(),
|
||||
ctx.prerender,
|
||||
) {
|
||||
// Different selection styles for different objects.
|
||||
match id {
|
||||
ID::Car(_) | ID::Pedestrian(_) | ID::PedCrowd(_) => {
|
||||
// Some objects are much wider/taller than others
|
||||
let multiplier = match id {
|
||||
ID::Car(c) => {
|
||||
if c.1 == VehicleType::Bike {
|
||||
3.0
|
||||
} else {
|
||||
0.75
|
||||
}
|
||||
}
|
||||
ID::Pedestrian(_) => 3.0,
|
||||
ID::PedCrowd(_) => 0.75,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
// Make a circle to cover the object.
|
||||
let bounds = obj.get_outline(&app.primary.map).get_bounds();
|
||||
let radius = multiplier * Distance::meters(bounds.width().max(bounds.height()));
|
||||
batch.push(
|
||||
app.cs.get_def("current object", Color::WHITE).alpha(0.5),
|
||||
Circle::new(bounds.center(), radius).to_polygon(),
|
||||
);
|
||||
batch.push(
|
||||
app.cs.get("current object"),
|
||||
Circle::outline(bounds.center(), radius, Distance::meters(0.3)),
|
||||
);
|
||||
|
||||
// TODO And actually, don't cover up the agent. The Renderable API isn't quite
|
||||
// conducive to doing this yet.
|
||||
}
|
||||
_ => {
|
||||
batch.push(
|
||||
app.cs.get_def("perma selected thing", Color::BLUE),
|
||||
obj.get_outline(&app.primary.map),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let header_btns = Widget::row(vec![
|
||||
Btn::svg_def("../data/system/assets/tools/location.svg")
|
||||
.build(ctx, "jump to object", hotkey(Key::J))
|
||||
.margin(5),
|
||||
Btn::text_fg("X").build(ctx, "close info", hotkey(Key::Escape)),
|
||||
])
|
||||
.align_right();
|
||||
let (col, trip_details) = match id.clone() {
|
||||
ID::Road(_) => unreachable!(),
|
||||
ID::Lane(id) => (lane::info(ctx, app, id, header_btns, action_btns), None),
|
||||
ID::Intersection(id) => (
|
||||
intersection::info(ctx, app, id, header_btns, action_btns),
|
||||
None,
|
||||
),
|
||||
ID::Turn(_) => unreachable!(),
|
||||
ID::Building(id) => (
|
||||
building::info(
|
||||
ctx,
|
||||
app,
|
||||
id,
|
||||
tab.clone(),
|
||||
header_btns,
|
||||
action_btns,
|
||||
&mut batch,
|
||||
),
|
||||
None,
|
||||
),
|
||||
ID::Car(id) => agents::car_info(ctx, app, id, header_btns, action_btns, &mut batch),
|
||||
ID::Pedestrian(id) => agents::ped_info(ctx, app, id, header_btns, action_btns),
|
||||
ID::PedCrowd(members) => (
|
||||
agents::crowd_info(ctx, app, members, header_btns, action_btns),
|
||||
None,
|
||||
),
|
||||
ID::BusStop(id) => (bus_stop::info(ctx, app, id, header_btns, action_btns), None),
|
||||
ID::Area(id) => (debug::area(ctx, app, id, header_btns, action_btns), None),
|
||||
ID::ExtraShape(id) => (
|
||||
debug::extra_shape(ctx, app, id, header_btns, action_btns),
|
||||
None,
|
||||
),
|
||||
ID::Trip(id) => trip::info(ctx, app, id, action_btns),
|
||||
ID::Person(id) => (
|
||||
person::info(ctx, app, id, Some(header_btns), action_btns),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
// Follow the agent. When the sim is paused, this lets the player naturally pan away,
|
||||
// because the InfoPanel isn't being updated.
|
||||
if let Some(pt) = id
|
||||
.agent_id()
|
||||
.and_then(|a| app.primary.sim.canonical_pt_for_agent(a, &app.primary.map))
|
||||
{
|
||||
ctx.canvas.center_on_map_pt(pt);
|
||||
}
|
||||
|
||||
InfoPanel {
|
||||
id,
|
||||
tab,
|
||||
actions,
|
||||
trip_details,
|
||||
time: app.primary.sim.time(),
|
||||
composite: Composite::new(Widget::col(col).bg(colors::PANEL_BG).padding(10))
|
||||
.aligned(
|
||||
HorizontalAlignment::Percent(0.02),
|
||||
VerticalAlignment::Percent(0.2),
|
||||
)
|
||||
.max_size_percent(35, 60)
|
||||
.build(ctx),
|
||||
also_draw: batch.upload(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// (Are we done, optional transition)
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
app: &mut App,
|
||||
maybe_speed: Option<&mut SpeedControls>,
|
||||
) -> (bool, Option<Transition>) {
|
||||
// Can click on the map to cancel
|
||||
if ctx.canvas.get_cursor_in_map_space().is_some()
|
||||
&& app.primary.current_selection.is_none()
|
||||
&& app.per_obj.left_click(ctx, "stop showing info")
|
||||
{
|
||||
return (true, None);
|
||||
}
|
||||
|
||||
// Live update?
|
||||
if app.primary.sim.time() != self.time {
|
||||
if let Some(a) = self.id.agent_id() {
|
||||
if let Some(ref details) = self.trip_details {
|
||||
match app.primary.sim.trip_to_agent(details.id) {
|
||||
TripResult::Ok(a2) => {
|
||||
if a != a2 {
|
||||
if !app.primary.sim.does_agent_exist(a) {
|
||||
*self = InfoPanel::new(
|
||||
ID::from_agent(a2),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
return (
|
||||
false,
|
||||
Some(Transition::Push(msg(
|
||||
"The trip is transitioning to a new mode",
|
||||
vec![format!(
|
||||
"{} is now {}, following them instead",
|
||||
agent_name(a),
|
||||
agent_name(a2)
|
||||
)],
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
return (true, Some(Transition::Push(trip_transition(a, a2))));
|
||||
}
|
||||
}
|
||||
TripResult::TripDone => {
|
||||
*self = InfoPanel::new(
|
||||
ID::Trip(details.id),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
return (
|
||||
false,
|
||||
Some(Transition::Push(msg(
|
||||
"Trip complete",
|
||||
vec![format!(
|
||||
"{} has finished their trip. Say goodbye!",
|
||||
agent_name(a)
|
||||
)],
|
||||
))),
|
||||
);
|
||||
}
|
||||
TripResult::TripDoesntExist => unreachable!(),
|
||||
// Just wait a moment for trip_transition to kick in...
|
||||
TripResult::ModeChange => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO Detect crowds changing here maybe
|
||||
|
||||
let preserve_scroll = self.composite.preserve_scroll();
|
||||
*self = InfoPanel::new(
|
||||
self.id.clone(),
|
||||
self.tab.clone(),
|
||||
ctx,
|
||||
app,
|
||||
self.actions.clone(),
|
||||
maybe_speed,
|
||||
);
|
||||
self.composite.restore_scroll(ctx, preserve_scroll);
|
||||
return (false, None);
|
||||
}
|
||||
|
||||
match self.composite.event(ctx) {
|
||||
Some(Outcome::Clicked(action)) => {
|
||||
if action == "close info" {
|
||||
(true, None)
|
||||
} else if action == "jump to object" {
|
||||
(
|
||||
false,
|
||||
Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
self.id.canonical_point(&app.primary).unwrap(),
|
||||
Some(10.0),
|
||||
Some(self.id.clone()),
|
||||
&mut app.primary,
|
||||
))),
|
||||
)
|
||||
} else if action == "follow agent" {
|
||||
maybe_speed.unwrap().resume_realtime(ctx);
|
||||
(false, None)
|
||||
} else if let Some(_) = strip_prefix_usize(&action, "examine trip phase ") {
|
||||
// Don't do anything! Just using buttons for convenient tooltips.
|
||||
(false, None)
|
||||
} else if let Some(id) = self
|
||||
.trip_details
|
||||
.as_ref()
|
||||
.and_then(|d| d.markers.get(&action))
|
||||
{
|
||||
(
|
||||
false,
|
||||
Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
id.canonical_point(&app.primary).unwrap(),
|
||||
Some(10.0),
|
||||
None,
|
||||
&mut app.primary,
|
||||
))),
|
||||
)
|
||||
} else if action == "examine people inside" {
|
||||
let ppl = match self.id {
|
||||
ID::Building(b) => app.primary.sim.bldg_to_people(b),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let preserve_scroll = self.composite.preserve_scroll();
|
||||
*self = InfoPanel::new(
|
||||
self.id.clone(),
|
||||
InfoTab::BldgPeople(ppl, 0),
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
self.composite.restore_scroll(ctx, preserve_scroll);
|
||||
return (false, None);
|
||||
} else if action == "previous" {
|
||||
let tab = match self.tab.clone() {
|
||||
InfoTab::BldgPeople(ppl, idx) => {
|
||||
InfoTab::BldgPeople(ppl, if idx != 0 { idx - 1 } else { idx })
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let preserve_scroll = self.composite.preserve_scroll();
|
||||
*self = InfoPanel::new(self.id.clone(), tab, ctx, app, Vec::new(), maybe_speed);
|
||||
self.composite.restore_scroll(ctx, preserve_scroll);
|
||||
return (false, None);
|
||||
} else if action == "next" {
|
||||
let tab = match self.tab.clone() {
|
||||
InfoTab::BldgPeople(ppl, idx) => InfoTab::BldgPeople(
|
||||
ppl.clone(),
|
||||
if idx != ppl.len() - 1 { idx + 1 } else { idx },
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let preserve_scroll = self.composite.preserve_scroll();
|
||||
*self = InfoPanel::new(self.id.clone(), tab, ctx, app, Vec::new(), maybe_speed);
|
||||
self.composite.restore_scroll(ctx, preserve_scroll);
|
||||
return (false, None);
|
||||
} else if action == "close occupants panel" {
|
||||
let preserve_scroll = self.composite.preserve_scroll();
|
||||
*self = InfoPanel::new(
|
||||
self.id.clone(),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
self.composite.restore_scroll(ctx, preserve_scroll);
|
||||
return (false, None);
|
||||
} else if let Some(idx) = strip_prefix_usize(&action, "examine Trip #") {
|
||||
*self = InfoPanel::new(
|
||||
ID::Trip(TripID(idx)),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
return (false, None);
|
||||
} else if let Some(idx) = strip_prefix_usize(&action, "examine Person #") {
|
||||
*self = InfoPanel::new(
|
||||
ID::Person(PersonID(idx)),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
return (false, None);
|
||||
} else if let Some(idx) = strip_prefix_usize(&action, "examine Building #") {
|
||||
*self = InfoPanel::new(
|
||||
ID::Building(BuildingID(idx)),
|
||||
InfoTab::Nil,
|
||||
ctx,
|
||||
app,
|
||||
Vec::new(),
|
||||
maybe_speed,
|
||||
);
|
||||
return (false, None);
|
||||
} else {
|
||||
app.primary.current_selection = Some(self.id.clone());
|
||||
(true, Some(Transition::ApplyObjectAction(action)))
|
||||
}
|
||||
}
|
||||
None => (false, None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
self.composite.draw(g);
|
||||
if let Some(ref details) = self.trip_details {
|
||||
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
|
||||
g.redraw(&details.unzoomed);
|
||||
} else {
|
||||
g.redraw(&details.zoomed);
|
||||
}
|
||||
}
|
||||
g.redraw(&self.also_draw);
|
||||
}
|
||||
}
|
||||
|
||||
fn make_table(ctx: &EventCtx, rows: Vec<(String, String)>) -> Vec<Widget> {
|
||||
rows.into_iter()
|
||||
.map(|(k, v)| {
|
||||
Widget::row(vec![
|
||||
Line(k).roboto_bold().draw(ctx),
|
||||
// TODO not quite...
|
||||
v.draw_text(ctx).centered_vert().align_right(),
|
||||
])
|
||||
})
|
||||
.collect()
|
||||
|
||||
// Attempt two
|
||||
/*let mut keys = Text::new();
|
||||
let mut values = Text::new();
|
||||
for (k, v) in rows {
|
||||
keys.add(Line(k).roboto_bold());
|
||||
values.add(Line(v));
|
||||
}
|
||||
vec![Widget::row(vec![
|
||||
keys.draw(ctx),
|
||||
values.draw(ctx).centered_vert().bg(Color::GREEN),
|
||||
])]*/
|
||||
}
|
||||
|
||||
fn throughput<F: Fn(&Analytics, Time) -> BTreeMap<TripMode, Vec<(Time, usize)>>>(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
get_data: F,
|
||||
) -> Widget {
|
||||
let mut series = get_data(app.primary.sim.get_analytics(), app.primary.sim.time())
|
||||
.into_iter()
|
||||
.map(|(m, pts)| Series {
|
||||
label: m.to_string(),
|
||||
color: color_for_mode(m, app),
|
||||
pts,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if app.has_prebaked().is_some() {
|
||||
// TODO Ahh these colors don't show up differently at all.
|
||||
for (m, pts) in get_data(app.prebaked(), Time::END_OF_DAY) {
|
||||
series.push(Series {
|
||||
label: format!("{} (baseline)", m),
|
||||
color: color_for_mode(m, app).alpha(0.3),
|
||||
pts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Plot::new_usize(ctx, series, PlotOptions::new())
|
||||
}
|
||||
|
||||
fn color_for_mode(m: TripMode, app: &App) -> Color {
|
||||
match m {
|
||||
TripMode::Walk => app.cs.get("unzoomed pedestrian"),
|
||||
TripMode::Bike => app.cs.get("unzoomed bike"),
|
||||
TripMode::Transit => app.cs.get("unzoomed bus"),
|
||||
TripMode::Drive => app.cs.get("unzoomed car"),
|
||||
}
|
||||
}
|
||||
|
||||
fn trip_transition(from: AgentID, to: AgentID) -> Box<dyn State> {
|
||||
WizardState::new(Box::new(move |wiz, ctx, _| {
|
||||
let orig = format!("keep following {}", agent_name(from));
|
||||
let change = format!("follow {} instead", agent_name(to));
|
||||
|
||||
let id = if wiz
|
||||
.wrap(ctx)
|
||||
.choose_string("The trip is transitioning to a new mode", || {
|
||||
vec![orig.clone(), change.clone()]
|
||||
})?
|
||||
== orig
|
||||
{
|
||||
ID::from_agent(from)
|
||||
} else {
|
||||
ID::from_agent(to)
|
||||
};
|
||||
Some(Transition::PopWithData(Box::new(move |state, app, ctx| {
|
||||
state
|
||||
.downcast_mut::<SandboxMode>()
|
||||
.unwrap()
|
||||
.controls
|
||||
.common
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.launch_info_panel(id, ctx, app);
|
||||
})))
|
||||
}))
|
||||
}
|
||||
|
||||
fn agent_name(a: AgentID) -> String {
|
||||
match a {
|
||||
AgentID::Car(c) => match c.1 {
|
||||
VehicleType::Car => format!("Car #{}", c.0),
|
||||
VehicleType::Bike => format!("Bike #{}", c.0),
|
||||
VehicleType::Bus => format!("Bus #{}", c.0),
|
||||
},
|
||||
AgentID::Pedestrian(p) => format!("Pedestrian #{}", p.0),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Can't easily use this in the other few cases, which use a match...
|
||||
fn strip_prefix_usize(x: &String, prefix: &str) -> Option<usize> {
|
||||
if x.starts_with(prefix) {
|
||||
// If it starts with the prefix, insist on there being a valid number there
|
||||
Some(x[prefix.len()..].parse::<usize>().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
70
game/src/info/person.rs
Normal file
70
game/src/info/person.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use crate::app::App;
|
||||
use ezgui::{Btn, EventCtx, Line, TextExt, Widget};
|
||||
use sim::{PersonID, PersonState};
|
||||
|
||||
pub fn info(
|
||||
ctx: &EventCtx,
|
||||
app: &App,
|
||||
id: PersonID,
|
||||
// If None, then the panel is embedded
|
||||
header_btns: Option<Widget>,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> Vec<Widget> {
|
||||
let mut rows = vec![];
|
||||
|
||||
// Header
|
||||
let standalone = header_btns.is_some();
|
||||
if let Some(btns) = header_btns {
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Person #{}", id.0)).roboto_bold().draw(ctx),
|
||||
btns,
|
||||
]));
|
||||
} else {
|
||||
rows.push(Line(format!("Person #{}", id.0)).roboto_bold().draw(ctx));
|
||||
}
|
||||
rows.extend(action_btns);
|
||||
|
||||
let person = app.primary.sim.get_person(id);
|
||||
|
||||
// TODO Redundant to say they're inside when the panel is embedded. But... if the person leaves
|
||||
// while we have the panel open, then it IS relevant.
|
||||
if standalone {
|
||||
// TODO Point out where the person is now, relative to schedule...
|
||||
rows.push(match person.state {
|
||||
// TODO not the best tooltip, but easy to parse :(
|
||||
PersonState::Inside(b) => Btn::text_bg1(format!(
|
||||
"Currently inside {}",
|
||||
app.primary.map.get_b(b).just_address(&app.primary.map)
|
||||
))
|
||||
.build(ctx, format!("examine Building #{}", b.0), None),
|
||||
PersonState::Trip(t) => format!("Currently doing Trip #{}", t.0).draw_text(ctx),
|
||||
PersonState::OffMap => "Currently outside the map boundaries".draw_text(ctx),
|
||||
PersonState::Limbo => "Currently in limbo -- they broke out of the Matrix! Woops. (A \
|
||||
bug occurred)"
|
||||
.draw_text(ctx),
|
||||
});
|
||||
}
|
||||
|
||||
rows.push(Line("Schedule").roboto_bold().draw(ctx));
|
||||
for t in &person.trips {
|
||||
// TODO Still maybe unsafe? Check if trip has actually started or not
|
||||
// TODO Say where the trip goes, no matter what?
|
||||
let start_time = app.primary.sim.trip_start_time(*t);
|
||||
if app.primary.sim.time() < start_time {
|
||||
rows.push(
|
||||
format!("{}: Trip #{} will start", start_time.ampm_tostring(), t.0).draw_text(ctx),
|
||||
);
|
||||
} else {
|
||||
rows.push(Widget::row(vec![
|
||||
format!("{}: ", start_time.ampm_tostring()).draw_text(ctx),
|
||||
Btn::text_bg1(format!("Trip #{}", t.0))
|
||||
.build(ctx, format!("examine Trip #{}", t.0), None)
|
||||
.margin(5),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO All the colorful side info
|
||||
|
||||
rows
|
||||
}
|
361
game/src/info/trip.rs
Normal file
361
game/src/info/trip.rs
Normal file
@ -0,0 +1,361 @@
|
||||
use crate::app::App;
|
||||
use crate::colors;
|
||||
use crate::helpers::ID;
|
||||
use crate::info::{make_table, TripDetails};
|
||||
use crate::render::dashed_lines;
|
||||
use ezgui::{
|
||||
hotkey, Btn, Color, EventCtx, GeomBatch, Key, Line, Plot, PlotOptions, RewriteColor, Series,
|
||||
Text, Widget,
|
||||
};
|
||||
use geom::{Angle, Distance, Duration, Polygon, Pt2D, Time};
|
||||
use map_model::{Map, Path, PathStep};
|
||||
use sim::{TripEnd, TripID, TripPhaseType, TripStart};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn info(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
id: TripID,
|
||||
action_btns: Vec<Widget>,
|
||||
) -> (Vec<Widget>, Option<TripDetails>) {
|
||||
let mut rows = vec![];
|
||||
|
||||
rows.push(Widget::row(vec![
|
||||
Line(format!("Trip #{}", id.0)).roboto_bold().draw(ctx),
|
||||
// No jump-to-object button; this is probably a finished trip.
|
||||
Btn::text_fg("X")
|
||||
.build(ctx, "close info", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]));
|
||||
rows.extend(action_btns);
|
||||
|
||||
let (more, details) = trip_details(ctx, app, id, None);
|
||||
rows.push(more);
|
||||
|
||||
(rows, Some(details))
|
||||
}
|
||||
|
||||
pub fn trip_details(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
trip: TripID,
|
||||
progress_along_path: Option<f64>,
|
||||
) -> (Widget, TripDetails) {
|
||||
let map = &app.primary.map;
|
||||
let phases = app.primary.sim.get_analytics().get_trip_phases(trip, map);
|
||||
let (trip_start, trip_end) = app.primary.sim.trip_endpoints(trip);
|
||||
|
||||
let mut unzoomed = GeomBatch::new();
|
||||
let mut zoomed = GeomBatch::new();
|
||||
let mut markers = HashMap::new();
|
||||
|
||||
let trip_start_time = phases[0].start_time;
|
||||
let trip_end_time = phases.last().as_ref().and_then(|p| p.end_time);
|
||||
|
||||
let start_tooltip = match trip_start {
|
||||
TripStart::Bldg(b) => {
|
||||
let bldg = map.get_b(b);
|
||||
|
||||
markers.insert("jump to start".to_string(), ID::Building(b));
|
||||
|
||||
unzoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/start_pos.svg",
|
||||
bldg.label_center,
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
zoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/start_pos.svg",
|
||||
bldg.label_center,
|
||||
0.5,
|
||||
Angle::ZERO,
|
||||
);
|
||||
|
||||
let mut txt = Text::from(Line("jump to start"));
|
||||
txt.add(Line(bldg.just_address(map)));
|
||||
txt.add(Line(phases[0].start_time.ampm_tostring()));
|
||||
txt
|
||||
}
|
||||
TripStart::Border(i) => {
|
||||
let i = map.get_i(i);
|
||||
|
||||
markers.insert("jump to start".to_string(), ID::Intersection(i.id));
|
||||
|
||||
unzoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/start_pos.svg",
|
||||
i.polygon.center(),
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
zoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/start_pos.svg",
|
||||
i.polygon.center(),
|
||||
0.5,
|
||||
Angle::ZERO,
|
||||
);
|
||||
|
||||
let mut txt = Text::from(Line("jump to start"));
|
||||
txt.add(Line(i.name(map)));
|
||||
txt.add(Line(phases[0].start_time.ampm_tostring()));
|
||||
txt
|
||||
}
|
||||
};
|
||||
let start_btn = Btn::svg(
|
||||
"../data/system/assets/timeline/start_pos.svg",
|
||||
RewriteColor::Change(Color::WHITE, colors::HOVERING),
|
||||
)
|
||||
.tooltip(start_tooltip)
|
||||
.build(ctx, "jump to start", None);
|
||||
|
||||
let goal_tooltip = match trip_end {
|
||||
TripEnd::Bldg(b) => {
|
||||
let bldg = map.get_b(b);
|
||||
|
||||
markers.insert("jump to goal".to_string(), ID::Building(b));
|
||||
|
||||
unzoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/goal_pos.svg",
|
||||
bldg.label_center,
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
zoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/goal_pos.svg",
|
||||
bldg.label_center,
|
||||
0.5,
|
||||
Angle::ZERO,
|
||||
);
|
||||
|
||||
let mut txt = Text::from(Line("jump to goal"));
|
||||
txt.add(Line(bldg.just_address(map)));
|
||||
if let Some(t) = trip_end_time {
|
||||
txt.add(Line(t.ampm_tostring()));
|
||||
}
|
||||
txt
|
||||
}
|
||||
TripEnd::Border(i) => {
|
||||
let i = map.get_i(i);
|
||||
|
||||
markers.insert("jump to goal".to_string(), ID::Intersection(i.id));
|
||||
|
||||
unzoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/goal_pos.svg",
|
||||
i.polygon.center(),
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
zoomed.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/goal_pos.svg",
|
||||
i.polygon.center(),
|
||||
0.5,
|
||||
Angle::ZERO,
|
||||
);
|
||||
|
||||
let mut txt = Text::from(Line("jump to goal"));
|
||||
txt.add(Line(i.name(map)));
|
||||
if let Some(t) = trip_end_time {
|
||||
txt.add(Line(t.ampm_tostring()));
|
||||
}
|
||||
txt
|
||||
}
|
||||
TripEnd::ServeBusRoute(_) => unreachable!(),
|
||||
};
|
||||
let goal_btn = Btn::svg(
|
||||
"../data/system/assets/timeline/goal_pos.svg",
|
||||
RewriteColor::Change(Color::WHITE, colors::HOVERING),
|
||||
)
|
||||
.tooltip(goal_tooltip)
|
||||
.build(ctx, "jump to goal", None);
|
||||
|
||||
let total_duration_so_far =
|
||||
trip_end_time.unwrap_or_else(|| app.primary.sim.time()) - phases[0].start_time;
|
||||
|
||||
let total_width = 0.3 * ctx.canvas.window_width;
|
||||
let mut timeline = Vec::new();
|
||||
let num_phases = phases.len();
|
||||
let mut elevation = Vec::new();
|
||||
for (idx, p) in phases.into_iter().enumerate() {
|
||||
let color = match p.phase_type {
|
||||
TripPhaseType::Driving => Color::hex("#D63220"),
|
||||
TripPhaseType::Walking => Color::hex("#DF8C3D"),
|
||||
TripPhaseType::Biking => app.cs.get("bike lane"),
|
||||
TripPhaseType::Parking => Color::hex("#4E30A6"),
|
||||
TripPhaseType::WaitingForBus(_) => app.cs.get("bus stop marking"),
|
||||
TripPhaseType::RidingBus(_) => app.cs.get("bus lane"),
|
||||
TripPhaseType::Aborted | TripPhaseType::Finished => unreachable!(),
|
||||
}
|
||||
.alpha(0.7);
|
||||
|
||||
let mut txt = Text::from(Line(&p.phase_type.describe(&app.primary.map)));
|
||||
txt.add(Line(format!(
|
||||
"- Started at {}",
|
||||
p.start_time.ampm_tostring()
|
||||
)));
|
||||
let duration = if let Some(t2) = p.end_time {
|
||||
let d = t2 - p.start_time;
|
||||
txt.add(Line(format!("- Ended at {} (duration: {})", t2, d)));
|
||||
d
|
||||
} else {
|
||||
let d = app.primary.sim.time() - p.start_time;
|
||||
txt.add(Line(format!("- Ongoing (duration so far: {})", d)));
|
||||
d
|
||||
};
|
||||
// TODO Problems when this is really low?
|
||||
let percent_duration = if total_duration_so_far == Duration::ZERO {
|
||||
0.0
|
||||
} else {
|
||||
duration / total_duration_so_far
|
||||
};
|
||||
txt.add(Line(format!(
|
||||
"- {}% of trip duration",
|
||||
(100.0 * percent_duration) as usize
|
||||
)));
|
||||
|
||||
let phase_width = total_width * percent_duration;
|
||||
let rect = Polygon::rectangle(phase_width, 15.0);
|
||||
let mut normal = GeomBatch::from(vec![(color, rect.clone())]);
|
||||
if idx == num_phases - 1 {
|
||||
if let Some(p) = progress_along_path {
|
||||
normal.add_svg(
|
||||
ctx.prerender,
|
||||
"../data/system/assets/timeline/current_pos.svg",
|
||||
Pt2D::new(p * phase_width, 7.5),
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
}
|
||||
}
|
||||
normal.add_svg(
|
||||
ctx.prerender,
|
||||
match p.phase_type {
|
||||
TripPhaseType::Driving => "../data/system/assets/timeline/driving.svg",
|
||||
TripPhaseType::Walking => "../data/system/assets/timeline/walking.svg",
|
||||
TripPhaseType::Biking => "../data/system/assets/timeline/biking.svg",
|
||||
TripPhaseType::Parking => "../data/system/assets/timeline/parking.svg",
|
||||
TripPhaseType::WaitingForBus(_) => {
|
||||
"../data/system/assets/timeline/waiting_for_bus.svg"
|
||||
}
|
||||
TripPhaseType::RidingBus(_) => "../data/system/assets/timeline/riding_bus.svg",
|
||||
TripPhaseType::Aborted | TripPhaseType::Finished => unreachable!(),
|
||||
},
|
||||
// TODO Hardcoded layouting...
|
||||
Pt2D::new(0.5 * phase_width, -20.0),
|
||||
1.0,
|
||||
Angle::ZERO,
|
||||
);
|
||||
|
||||
let mut hovered = GeomBatch::from(vec![(color.alpha(1.0), rect.clone())]);
|
||||
for (c, p) in normal.clone().consume().into_iter().skip(1) {
|
||||
hovered.push(c, p);
|
||||
}
|
||||
|
||||
timeline.push(
|
||||
Btn::custom(normal, hovered, rect)
|
||||
.build(ctx, format!("examine trip phase {}", idx + 1), None)
|
||||
.centered_vert(),
|
||||
);
|
||||
|
||||
// TODO Could really cache this between live updates
|
||||
if let Some((dist, ref path)) = p.path {
|
||||
if app.opts.dev
|
||||
&& (p.phase_type == TripPhaseType::Walking || p.phase_type == TripPhaseType::Biking)
|
||||
{
|
||||
elevation.push(make_elevation(
|
||||
ctx,
|
||||
color,
|
||||
p.phase_type == TripPhaseType::Walking,
|
||||
path,
|
||||
map,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(trace) = path.trace(map, dist, None) {
|
||||
unzoomed.push(color, trace.make_polygons(Distance::meters(10.0)));
|
||||
zoomed.extend(
|
||||
color,
|
||||
dashed_lines(
|
||||
&trace,
|
||||
Distance::meters(0.75),
|
||||
Distance::meters(1.0),
|
||||
Distance::meters(0.4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeline.insert(0, start_btn.margin(5));
|
||||
timeline.push(goal_btn.margin(5));
|
||||
|
||||
let mut table = vec![
|
||||
("Trip start".to_string(), trip_start_time.ampm_tostring()),
|
||||
("Duration".to_string(), total_duration_so_far.to_string()),
|
||||
];
|
||||
if let Some(t) = trip_end_time {
|
||||
table.push(("Trip end".to_string(), t.ampm_tostring()));
|
||||
}
|
||||
let mut col = vec![Widget::row(timeline).evenly_spaced().margin_above(25)];
|
||||
col.extend(make_table(ctx, table));
|
||||
col.extend(elevation);
|
||||
if let Some(p) = app.primary.sim.trip_to_person(trip) {
|
||||
col.push(
|
||||
Btn::text_bg1(format!("Trip by Person #{}", p.0))
|
||||
.build(ctx, format!("examine Person #{}", p.0), None)
|
||||
.margin(5),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
Widget::col(col),
|
||||
TripDetails {
|
||||
id: trip,
|
||||
unzoomed: unzoomed.upload(ctx),
|
||||
zoomed: zoomed.upload(ctx),
|
||||
markers,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn make_elevation(ctx: &EventCtx, color: Color, walking: bool, path: &Path, map: &Map) -> Widget {
|
||||
let mut pts: Vec<(Distance, Distance)> = Vec::new();
|
||||
let mut dist = Distance::ZERO;
|
||||
for step in path.get_steps() {
|
||||
if let PathStep::Turn(t) = step {
|
||||
pts.push((dist, map.get_i(t.parent).elevation));
|
||||
}
|
||||
dist += step.as_traversable().length(map);
|
||||
}
|
||||
// TODO Plot needs to support Distance as both X and Y axis. :P
|
||||
// TODO Show roughly where we are in the trip; use distance covered by current path for this
|
||||
Plot::new_usize(
|
||||
ctx,
|
||||
vec![Series {
|
||||
label: if walking {
|
||||
"Elevation for walking"
|
||||
} else {
|
||||
"Elevation for biking"
|
||||
}
|
||||
.to_string(),
|
||||
color,
|
||||
pts: pts
|
||||
.into_iter()
|
||||
.map(|(x, y)| {
|
||||
(
|
||||
Time::START_OF_DAY + Duration::seconds(x.inner_meters()),
|
||||
y.inner_meters() as usize,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}],
|
||||
PlotOptions::new(),
|
||||
)
|
||||
.bg(colors::PANEL_BG)
|
||||
}
|
@ -8,6 +8,7 @@ mod devtools;
|
||||
mod edit;
|
||||
mod game;
|
||||
mod helpers;
|
||||
mod info;
|
||||
mod managed;
|
||||
mod obj_actions;
|
||||
mod options;
|
||||
|
@ -543,7 +543,8 @@ impl TripManager {
|
||||
}
|
||||
}
|
||||
|
||||
// This will be None for parked cars
|
||||
// This will be None for parked cars. Buses technically do have trips. Should always work for
|
||||
// pedestrians.
|
||||
pub fn agent_to_trip(&self, id: AgentID) -> Option<TripID> {
|
||||
self.active_trip_mode.get(&id).cloned()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user