mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-01 02:33:54 +03:00
start to split layers code into modules. this should become a trait, but not quite ready for that
This commit is contained in:
parent
fb7cfc5513
commit
35491be909
@ -1,6 +1,6 @@
|
||||
use crate::colors::ColorScheme;
|
||||
use crate::common::Layers;
|
||||
use crate::helpers::ID;
|
||||
use crate::layer::Layers;
|
||||
use crate::options::Options;
|
||||
use crate::render::{
|
||||
AgentCache, AgentColorScheme, DrawMap, DrawOptions, Renderable, MIN_ZOOM_FOR_DETAIL,
|
||||
|
@ -1,999 +0,0 @@
|
||||
use crate::app::App;
|
||||
use crate::colors::HeatmapColors;
|
||||
use crate::common::{make_heatmap, ColorLegend, Colorer, HeatmapOptions, ShowBusRoute, Warping};
|
||||
use crate::game::Transition;
|
||||
use crate::helpers::ID;
|
||||
use crate::managed::{ManagedGUIState, WrappedComposite};
|
||||
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
||||
use abstutil::{prettyprint_usize, Counter};
|
||||
use ezgui::{
|
||||
hotkey, Btn, Checkbox, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, Histogram,
|
||||
HorizontalAlignment, Key, Line, Outcome, Spinner, Text, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Circle, Distance, Duration, PolyLine, Pt2D, Time};
|
||||
use map_model::{BusRouteID, IntersectionID};
|
||||
use sim::{GetDrawAgents, ParkingSpot, PersonState};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// TODO Good ideas in
|
||||
// https://towardsdatascience.com/top-10-map-types-in-data-visualization-b3a80898ea70
|
||||
|
||||
pub enum Layers {
|
||||
Inactive,
|
||||
ParkingOccupancy(Time, Colorer),
|
||||
WorstDelay(Time, Colorer),
|
||||
TrafficJams(Time, Colorer),
|
||||
CumulativeThroughput(Time, Colorer),
|
||||
BikeNetwork(Colorer),
|
||||
BusNetwork(Colorer),
|
||||
Elevation(Colorer, Drawable),
|
||||
Edits(Colorer),
|
||||
TripsHistogram(Time, Composite),
|
||||
PopulationMap(Time, PopulationOptions, Drawable, Composite),
|
||||
|
||||
// These aren't selectable from the main picker; they're particular to some object.
|
||||
// TODO They should become something else, like an info panel tab.
|
||||
IntersectionDemand(Time, IntersectionID, Drawable, Composite),
|
||||
BusRoute(Time, BusRouteID, ShowBusRoute),
|
||||
}
|
||||
|
||||
impl Layers {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Layers::Inactive => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Since Layers is embedded in UI, we have to do this slight trick
|
||||
pub fn update(ctx: &mut EventCtx, app: &mut App, minimap: &Composite) -> Option<Transition> {
|
||||
let now = app.primary.sim.time();
|
||||
match app.layer {
|
||||
Layers::ParkingOccupancy(t, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::parking_occupancy(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::WorstDelay(t, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::worst_delay(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::TrafficJams(t, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::traffic_jams(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::CumulativeThroughput(t, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::cumulative_throughput(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::IntersectionDemand(t, i, _, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::intersection_demand(i, ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::TripsHistogram(t, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::trips_histogram(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::BusRoute(t, id, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::show_bus_route(id, ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(t, ref opts, _, _) => {
|
||||
if now != t {
|
||||
app.layer = Layers::population_map(ctx, app, opts.clone());
|
||||
}
|
||||
}
|
||||
// No updates needed
|
||||
Layers::Inactive
|
||||
| Layers::BikeNetwork(_)
|
||||
| Layers::BusNetwork(_)
|
||||
| Layers::Elevation(_, _)
|
||||
| Layers::Edits(_) => {}
|
||||
};
|
||||
|
||||
match app.layer {
|
||||
Layers::ParkingOccupancy(_, ref mut c)
|
||||
| Layers::BikeNetwork(ref mut c)
|
||||
| Layers::BusNetwork(ref mut c)
|
||||
| Layers::Elevation(ref mut c, _)
|
||||
| Layers::WorstDelay(_, ref mut c)
|
||||
| Layers::TrafficJams(_, ref mut c)
|
||||
| Layers::CumulativeThroughput(_, ref mut c)
|
||||
| Layers::Edits(ref mut c) => {
|
||||
c.legend.align_above(ctx, minimap);
|
||||
if c.event(ctx) {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
}
|
||||
Layers::BusRoute(_, _, ref mut c) => {
|
||||
c.colorer.legend.align_above(ctx, minimap);
|
||||
if c.colorer.event(ctx) {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
}
|
||||
Layers::IntersectionDemand(_, i, _, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"intersection demand" => {
|
||||
let id = ID::Intersection(i);
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
id.canonical_point(&app.primary).unwrap(),
|
||||
Some(10.0),
|
||||
Some(id.clone()),
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
"X" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Layers::TripsHistogram(_, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"X" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(_, ref mut opts, _, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"close" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {
|
||||
let new_opts = population_options(c);
|
||||
if *opts != new_opts {
|
||||
app.layer = Layers::population_map(ctx, app, new_opts);
|
||||
// Immediately fix the alignment. TODO Do this for all of them, in a
|
||||
// more uniform way
|
||||
if let Layers::PopulationMap(_, _, _, ref mut c) = app.layer {
|
||||
c.align_above(ctx, minimap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Layers::Inactive => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Draw both controls and, if zoomed, the layer contents
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
match self {
|
||||
Layers::Inactive => {}
|
||||
Layers::ParkingOccupancy(_, ref c)
|
||||
| Layers::BikeNetwork(ref c)
|
||||
| Layers::BusNetwork(ref c)
|
||||
| Layers::WorstDelay(_, ref c)
|
||||
| Layers::TrafficJams(_, ref c)
|
||||
| Layers::CumulativeThroughput(_, ref c)
|
||||
| Layers::Edits(ref c) => {
|
||||
c.draw(g);
|
||||
}
|
||||
Layers::Elevation(ref c, ref draw) => {
|
||||
c.draw(g);
|
||||
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
|
||||
g.redraw(draw);
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(_, _, ref draw, ref composite) => {
|
||||
composite.draw(g);
|
||||
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
|
||||
g.redraw(draw);
|
||||
}
|
||||
}
|
||||
// All of these shouldn't care about zoom
|
||||
Layers::TripsHistogram(_, ref composite) => {
|
||||
composite.draw(g);
|
||||
}
|
||||
Layers::IntersectionDemand(_, _, ref draw, ref legend) => {
|
||||
g.redraw(draw);
|
||||
legend.draw(g);
|
||||
}
|
||||
Layers::BusRoute(_, _, ref s) => {
|
||||
s.draw(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just draw contents and do it always
|
||||
pub fn draw_minimap(&self, g: &mut GfxCtx) {
|
||||
match self {
|
||||
Layers::Inactive => {}
|
||||
Layers::ParkingOccupancy(_, ref c)
|
||||
| Layers::BikeNetwork(ref c)
|
||||
| Layers::BusNetwork(ref c)
|
||||
| Layers::WorstDelay(_, ref c)
|
||||
| Layers::TrafficJams(_, ref c)
|
||||
| Layers::CumulativeThroughput(_, ref c)
|
||||
| Layers::Edits(ref c) => {
|
||||
g.redraw(&c.unzoomed);
|
||||
}
|
||||
Layers::Elevation(ref c, ref draw) => {
|
||||
g.redraw(&c.unzoomed);
|
||||
g.redraw(draw);
|
||||
}
|
||||
Layers::PopulationMap(_, _, ref draw, _) => {
|
||||
g.redraw(draw);
|
||||
}
|
||||
Layers::TripsHistogram(_, _) => {}
|
||||
Layers::IntersectionDemand(_, _, _, _) => {}
|
||||
Layers::BusRoute(_, _, ref s) => {
|
||||
s.draw(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_layers(ctx: &mut EventCtx, app: &App) -> Option<Transition> {
|
||||
let mut col = vec![Widget::row(vec![
|
||||
Line("Layers").small_heading().draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
])];
|
||||
|
||||
col.extend(vec![
|
||||
Btn::text_fg("None").build_def(ctx, hotkey(Key::N)),
|
||||
Btn::text_fg("map edits").build_def(ctx, hotkey(Key::E)),
|
||||
Btn::text_fg("worst traffic jams").build_def(ctx, hotkey(Key::J)),
|
||||
Btn::text_fg("elevation").build_def(ctx, hotkey(Key::S)),
|
||||
Btn::text_fg("parking occupancy").build_def(ctx, hotkey(Key::P)),
|
||||
Btn::text_fg("delay").build_def(ctx, hotkey(Key::D)),
|
||||
Btn::text_fg("throughput").build_def(ctx, hotkey(Key::T)),
|
||||
Btn::text_fg("bike network").build_def(ctx, hotkey(Key::B)),
|
||||
Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)),
|
||||
Btn::text_fg("population map").build_def(ctx, hotkey(Key::X)),
|
||||
]);
|
||||
if let Some(name) = match app.layer {
|
||||
Layers::Inactive => Some("None"),
|
||||
Layers::ParkingOccupancy(_, _) => Some("parking occupancy"),
|
||||
Layers::WorstDelay(_, _) => Some("delay"),
|
||||
Layers::TrafficJams(_, _) => Some("worst traffic jams"),
|
||||
Layers::CumulativeThroughput(_, _) => Some("throughput"),
|
||||
Layers::BikeNetwork(_) => Some("bike network"),
|
||||
Layers::BusNetwork(_) => Some("bus network"),
|
||||
Layers::Elevation(_, _) => Some("elevation"),
|
||||
Layers::Edits(_) => Some("map edits"),
|
||||
Layers::PopulationMap(_, _, _, _) => Some("population map"),
|
||||
_ => None,
|
||||
} {
|
||||
for btn in &mut col {
|
||||
if btn.is_btn(name) {
|
||||
*btn = Btn::text_bg2(name).inactive(ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let c = WrappedComposite::new(
|
||||
Composite::new(
|
||||
Widget::col(col.into_iter().map(|w| w.margin_below(15)).collect())
|
||||
.bg(app.cs.panel_bg)
|
||||
.outline(2.0, Color::WHITE)
|
||||
.padding(10),
|
||||
)
|
||||
.max_size_percent(35, 70)
|
||||
.build(ctx),
|
||||
)
|
||||
.cb("close", Box::new(|_, _| Some(Transition::Pop)))
|
||||
.maybe_cb(
|
||||
"None",
|
||||
Box::new(|_, app| {
|
||||
app.layer = Layers::Inactive;
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"parking occupancy",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::parking_occupancy(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"delay",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::worst_delay(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"worst traffic jams",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::traffic_jams(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"throughput",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::cumulative_throughput(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"bike network",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::bike_network(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"bus network",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::bus_network(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"elevation",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::elevation(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"map edits",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::map_edits(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"population map",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = Layers::population_map(
|
||||
ctx,
|
||||
app,
|
||||
PopulationOptions {
|
||||
pandemic: false,
|
||||
heatmap: Some(HeatmapOptions::new()),
|
||||
},
|
||||
);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
);
|
||||
Some(Transition::Push(ManagedGUIState::over_map(c)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Layers {
|
||||
fn parking_occupancy(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let (filled_spots, avail_spots) = app.primary.sim.get_all_parking_spots();
|
||||
|
||||
// TODO Some kind of Scale abstraction that maps intervals of some quantity (percent,
|
||||
// duration) to these 4 colors
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Parking occupancy (per road)",
|
||||
vec![
|
||||
format!("{} spots filled", prettyprint_usize(filled_spots.len())),
|
||||
format!("{} spots available ", prettyprint_usize(avail_spots.len())),
|
||||
],
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0%", "40%", "70%", "90%", "100%"],
|
||||
);
|
||||
|
||||
let lane = |spot| match spot {
|
||||
ParkingSpot::Onstreet(l, _) => l,
|
||||
ParkingSpot::Offstreet(b, _) => app
|
||||
.primary
|
||||
.map
|
||||
.get_b(b)
|
||||
.parking
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.driving_pos
|
||||
.lane(),
|
||||
};
|
||||
|
||||
let mut filled = Counter::new();
|
||||
let mut avail = Counter::new();
|
||||
let mut keys = HashSet::new();
|
||||
for spot in filled_spots {
|
||||
let l = lane(spot);
|
||||
keys.insert(l);
|
||||
filled.inc(l);
|
||||
}
|
||||
for spot in avail_spots {
|
||||
let l = lane(spot);
|
||||
keys.insert(l);
|
||||
avail.inc(l);
|
||||
}
|
||||
|
||||
for l in keys {
|
||||
let open = avail.get(l);
|
||||
let closed = filled.get(l);
|
||||
let percent = (closed as f64) / ((open + closed) as f64);
|
||||
let color = if percent < 0.4 {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if percent < 0.7 {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if percent < 0.9 {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_l(l, color, &app.primary.map);
|
||||
}
|
||||
|
||||
Layers::ParkingOccupancy(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
fn worst_delay(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO explain more
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Delay (minutes)",
|
||||
Vec::new(),
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0", "1", "5", "15", "longer"],
|
||||
);
|
||||
|
||||
let (per_road, per_intersection) = app.primary.sim.worst_delay(&app.primary.map);
|
||||
for (r, d) in per_road {
|
||||
let color = if d < Duration::minutes(1) {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if d < Duration::minutes(5) {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if d < Duration::minutes(15) {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_r(r, color, &app.primary.map);
|
||||
}
|
||||
for (i, d) in per_intersection {
|
||||
let color = if d < Duration::minutes(1) {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if d < Duration::minutes(5) {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if d < Duration::minutes(15) {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_i(i, color);
|
||||
}
|
||||
|
||||
Layers::WorstDelay(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn traffic_jams(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let jams = app.primary.sim.delayed_intersections(Duration::minutes(5));
|
||||
|
||||
// TODO Silly colors. Weird way of presenting this information. Epicenter + radius?
|
||||
let others = Color::hex("#7FFA4D");
|
||||
let early = Color::hex("#F4DA22");
|
||||
let earliest = Color::hex("#EB5757");
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
format!("{} traffic jams", jams.len()),
|
||||
Vec::new(),
|
||||
vec![
|
||||
("longest lasting", earliest),
|
||||
("recent problems", early),
|
||||
("others", others),
|
||||
],
|
||||
);
|
||||
|
||||
for (idx, (i, _)) in jams.into_iter().enumerate() {
|
||||
if idx == 0 {
|
||||
colorer.add_i(i, earliest);
|
||||
} else if idx <= 5 {
|
||||
colorer.add_i(i, early);
|
||||
} else {
|
||||
colorer.add_i(i, others);
|
||||
}
|
||||
}
|
||||
|
||||
Layers::TrafficJams(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
fn cumulative_throughput(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Throughput (percentiles)",
|
||||
Vec::new(),
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0", "50", "90", "99", "100"],
|
||||
);
|
||||
|
||||
let stats = &app.primary.sim.get_analytics().thruput_stats;
|
||||
|
||||
// TODO If there are many duplicate counts, arbitrarily some will look heavier! Find the
|
||||
// disribution of counts instead.
|
||||
// TODO Actually display the counts at these percentiles
|
||||
// TODO Dump the data in debug mode
|
||||
{
|
||||
let roads = stats.count_per_road.sorted_asc();
|
||||
let p50_idx = ((roads.len() as f64) * 0.5) as usize;
|
||||
let p90_idx = ((roads.len() as f64) * 0.9) as usize;
|
||||
let p99_idx = ((roads.len() as f64) * 0.99) as usize;
|
||||
for (idx, r) in roads.into_iter().enumerate() {
|
||||
let color = if idx < p50_idx {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if idx < p90_idx {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if idx < p99_idx {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_r(*r, color, &app.primary.map);
|
||||
}
|
||||
}
|
||||
// TODO dedupe
|
||||
{
|
||||
let intersections = stats.count_per_intersection.sorted_asc();
|
||||
let p50_idx = ((intersections.len() as f64) * 0.5) as usize;
|
||||
let p90_idx = ((intersections.len() as f64) * 0.9) as usize;
|
||||
let p99_idx = ((intersections.len() as f64) * 0.99) as usize;
|
||||
for (idx, i) in intersections.into_iter().enumerate() {
|
||||
let color = if idx < p50_idx {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if idx < p90_idx {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if idx < p99_idx {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_i(*i, color);
|
||||
}
|
||||
}
|
||||
|
||||
Layers::CumulativeThroughput(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
fn bike_network(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Number and total distance
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
"Bike network",
|
||||
Vec::new(),
|
||||
vec![("bike lanes", app.cs.unzoomed_bike)],
|
||||
);
|
||||
for l in app.primary.map.all_lanes() {
|
||||
if l.is_biking() {
|
||||
colorer.add_l(l.id, app.cs.unzoomed_bike, &app.primary.map);
|
||||
}
|
||||
}
|
||||
Layers::BikeNetwork(colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
fn bus_network(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Same color for both?
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
"Bus network",
|
||||
Vec::new(),
|
||||
vec![
|
||||
("bus lanes", app.cs.bus_layer),
|
||||
("bus stops", app.cs.bus_layer),
|
||||
],
|
||||
);
|
||||
for l in app.primary.map.all_lanes() {
|
||||
if l.is_bus() {
|
||||
colorer.add_l(l.id, app.cs.bus_layer, &app.primary.map);
|
||||
}
|
||||
}
|
||||
for bs in app.primary.map.all_bus_stops().keys() {
|
||||
colorer.add_bs(*bs, app.cs.bus_layer);
|
||||
}
|
||||
|
||||
Layers::BusNetwork(colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
fn elevation(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Two passes because we have to construct the text first :(
|
||||
let mut max = 0.0_f64;
|
||||
for l in app.primary.map.all_lanes() {
|
||||
let pct = l.percent_grade(&app.primary.map).abs();
|
||||
max = max.max(pct);
|
||||
}
|
||||
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Elevation change",
|
||||
vec![format!("Steepest road: {:.0}%", max * 100.0)],
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["flat", "1%", "5%", "15%", "steeper"],
|
||||
);
|
||||
|
||||
let mut max = 0.0_f64;
|
||||
for l in app.primary.map.all_lanes() {
|
||||
let pct = l.percent_grade(&app.primary.map).abs();
|
||||
max = max.max(pct);
|
||||
|
||||
let color = if pct < 0.01 {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if pct < 0.05 {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if pct < 0.15 {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_l(l.id, color, &app.primary.map);
|
||||
}
|
||||
|
||||
let arrow_color = Color::BLACK;
|
||||
let mut batch = GeomBatch::new();
|
||||
// Time for uphill arrows!
|
||||
// TODO Draw V's, not arrows.
|
||||
// TODO Or try gradient colors.
|
||||
for r in app.primary.map.all_roads() {
|
||||
let (mut pl, _) = r.get_thick_polyline(&app.primary.map).unwrap();
|
||||
let e1 = app.primary.map.get_i(r.src_i).elevation;
|
||||
let e2 = app.primary.map.get_i(r.dst_i).elevation;
|
||||
if (e1 - e2).abs() / pl.length() < 0.01 {
|
||||
// Don't bother with ~flat roads
|
||||
continue;
|
||||
}
|
||||
if e1 > e2 {
|
||||
pl = pl.reversed();
|
||||
}
|
||||
|
||||
let arrow_len = Distance::meters(5.0);
|
||||
let btwn = Distance::meters(10.0);
|
||||
let thickness = Distance::meters(1.0);
|
||||
let len = pl.length();
|
||||
|
||||
let mut dist = arrow_len;
|
||||
while dist + arrow_len <= len {
|
||||
let (pt, angle) = pl.dist_along(dist);
|
||||
batch.push(
|
||||
arrow_color,
|
||||
PolyLine::new(vec![
|
||||
pt.project_away(arrow_len / 2.0, angle.opposite()),
|
||||
pt.project_away(arrow_len / 2.0, angle),
|
||||
])
|
||||
.make_arrow(thickness)
|
||||
.unwrap(),
|
||||
);
|
||||
dist += btwn;
|
||||
}
|
||||
}
|
||||
|
||||
Layers::Elevation(colorer.build_unzoomed(ctx, app), batch.upload(ctx))
|
||||
}
|
||||
|
||||
pub fn trips_histogram(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
if app.has_prebaked().is_none() {
|
||||
return Layers::Inactive;
|
||||
}
|
||||
|
||||
let now = app.primary.sim.time();
|
||||
Layers::TripsHistogram(
|
||||
now,
|
||||
Composite::new(
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
{
|
||||
let mut txt = Text::from(Line("Are trips "));
|
||||
txt.append(Line("faster").fg(Color::GREEN));
|
||||
txt.append(Line(", "));
|
||||
txt.append(Line("slower").fg(Color::RED));
|
||||
txt.append(Line(", or "));
|
||||
txt.append(Line("the same").fg(Color::YELLOW));
|
||||
txt.append(Line("?"));
|
||||
txt.draw(ctx)
|
||||
}
|
||||
.margin(10),
|
||||
Btn::text_fg("X").build_def(ctx, None).align_right(),
|
||||
]),
|
||||
Histogram::new(
|
||||
app.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.trip_time_deltas(now, app.prebaked()),
|
||||
ctx,
|
||||
),
|
||||
])
|
||||
.bg(app.cs.panel_bg),
|
||||
)
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn intersection_demand(i: IntersectionID, ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let mut batch = GeomBatch::new();
|
||||
|
||||
let mut total_demand = 0;
|
||||
let mut demand_per_group: Vec<(&PolyLine, usize)> = Vec::new();
|
||||
for g in app.primary.map.get_traffic_signal(i).turn_groups.values() {
|
||||
let demand = app
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.thruput_stats
|
||||
.demand
|
||||
.get(&g.id)
|
||||
.cloned()
|
||||
.unwrap_or(0);
|
||||
if demand > 0 {
|
||||
total_demand += demand;
|
||||
demand_per_group.push((&g.geom, demand));
|
||||
}
|
||||
}
|
||||
|
||||
for (pl, demand) in demand_per_group {
|
||||
let percent = (demand as f64) / (total_demand as f64);
|
||||
batch.push(
|
||||
Color::RED,
|
||||
pl.make_arrow(percent * Distance::meters(5.0)).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let col = vec![
|
||||
Widget::row(vec![
|
||||
"intersection demand".draw_text(ctx),
|
||||
Btn::svg_def("../data/system/assets/tools/location.svg")
|
||||
.build(ctx, "intersection demand", None)
|
||||
.margin(5),
|
||||
Btn::text_fg("X").build_def(ctx, None).align_right(),
|
||||
]),
|
||||
ColorLegend::row(ctx, Color::RED, "current demand"),
|
||||
];
|
||||
|
||||
Layers::IntersectionDemand(
|
||||
app.primary.sim.time(),
|
||||
i,
|
||||
batch.upload(ctx),
|
||||
Composite::new(Widget::col(col).bg(app.cs.panel_bg))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn show_bus_route(id: BusRouteID, ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
Layers::BusRoute(app.primary.sim.time(), id, ShowBusRoute::new(id, ctx, app))
|
||||
}
|
||||
|
||||
pub fn map_edits(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let edits = app.primary.map.get_edits();
|
||||
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
format!("Map edits ({})", edits.edits_name),
|
||||
vec![
|
||||
format!("{} lane types changed", edits.original_lts.len()),
|
||||
format!("{} lanes reversed", edits.reversed_lanes.len()),
|
||||
format!(
|
||||
"{} intersections changed",
|
||||
edits.original_intersections.len()
|
||||
),
|
||||
],
|
||||
vec![("modified lane/intersection", app.cs.edits_layer)],
|
||||
);
|
||||
|
||||
for l in edits.original_lts.keys().chain(&edits.reversed_lanes) {
|
||||
colorer.add_l(*l, app.cs.edits_layer, &app.primary.map);
|
||||
}
|
||||
for i in edits.original_intersections.keys() {
|
||||
colorer.add_i(*i, app.cs.edits_layer);
|
||||
}
|
||||
|
||||
Layers::Edits(colorer.build_both(ctx, app))
|
||||
}
|
||||
|
||||
// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
|
||||
// return this kind of data instead!
|
||||
fn population_map(ctx: &mut EventCtx, app: &App, opts: PopulationOptions) -> Layers {
|
||||
// Only display infected people if this is enabled.
|
||||
let maybe_pandemic = if opts.pandemic {
|
||||
app.primary.sim.get_pandemic_model()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut pts = Vec::new();
|
||||
// Faster to grab all agent positions than individually map trips to agent positions.
|
||||
if let Some(ref model) = maybe_pandemic {
|
||||
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
|
||||
if let Some(p) = a.person {
|
||||
if model.infected.contains_key(&p) {
|
||||
pts.push(a.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
|
||||
pts.push(a.pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Many people are probably in the same building. If we're building a heatmap, we
|
||||
// absolutely care about these repeats! If we're just drawing the simple dot map, avoid
|
||||
// drawing repeat circles.
|
||||
let mut seen_bldgs = HashSet::new();
|
||||
let mut repeat_pts = Vec::new();
|
||||
for person in app.primary.sim.get_all_people() {
|
||||
match person.state {
|
||||
// Already covered above
|
||||
PersonState::Trip(_) => {}
|
||||
PersonState::Inside(b) => {
|
||||
if maybe_pandemic
|
||||
.as_ref()
|
||||
.map(|m| !m.infected.contains_key(&person.id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let pt = app.primary.map.get_b(b).polygon.center();
|
||||
if seen_bldgs.contains(&b) {
|
||||
repeat_pts.push(pt);
|
||||
} else {
|
||||
seen_bldgs.insert(b);
|
||||
pts.push(pt);
|
||||
}
|
||||
}
|
||||
PersonState::OffMap | PersonState::Limbo => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut batch = GeomBatch::new();
|
||||
let colors_and_labels = if let Some(ref o) = opts.heatmap {
|
||||
pts.extend(repeat_pts);
|
||||
Some(make_heatmap(
|
||||
&mut batch,
|
||||
app.primary.map.get_bounds(),
|
||||
pts,
|
||||
o,
|
||||
))
|
||||
} else {
|
||||
// It's quite silly to produce triangles for the same circle over and over again. ;)
|
||||
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
|
||||
for pt in pts {
|
||||
batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
|
||||
}
|
||||
None
|
||||
};
|
||||
let controls = population_controls(ctx, app, &opts, colors_and_labels);
|
||||
Layers::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PopulationOptions {
|
||||
pandemic: bool,
|
||||
// If None, just a dot map
|
||||
heatmap: Option<HeatmapOptions>,
|
||||
}
|
||||
|
||||
// This function sounds more ominous than it should.
|
||||
fn population_controls(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
opts: &PopulationOptions,
|
||||
colors_and_labels: Option<(Vec<Color>, Vec<String>)>,
|
||||
) -> Composite {
|
||||
let (total_ppl, ppl_in_bldg, ppl_off_map) = app.primary.sim.num_ppl();
|
||||
|
||||
let mut col = vec![
|
||||
Widget::row(vec![
|
||||
Widget::draw_svg(ctx, "../data/system/assets/tools/layers.svg").margin_right(10),
|
||||
Line(format!("Population: {}", prettyprint_usize(total_ppl))).draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
Widget::row(vec![
|
||||
Widget::draw_svg(ctx, "../data/system/assets/tools/home.svg").margin_right(10),
|
||||
Line(prettyprint_usize(ppl_in_bldg)).small().draw(ctx),
|
||||
]),
|
||||
Line(format!("Off-map: {}", prettyprint_usize(ppl_off_map)))
|
||||
.small()
|
||||
.draw(ctx),
|
||||
])
|
||||
.centered(),
|
||||
if app.primary.sim.get_pandemic_model().is_some() {
|
||||
Checkbox::text(ctx, "Show pandemic model", None, opts.pandemic)
|
||||
} else {
|
||||
Widget::nothing()
|
||||
},
|
||||
];
|
||||
|
||||
if opts.pandemic {
|
||||
let model = app.primary.sim.get_pandemic_model().unwrap();
|
||||
col.push(
|
||||
format!(
|
||||
"Pandemic model: {} S ({:.1}%), {} E ({:.1}%), {} I ({:.1}%), {} R ({:.1}%)",
|
||||
prettyprint_usize(model.count_sane()),
|
||||
(model.count_sane() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_exposed()),
|
||||
(model.count_exposed() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_infected()),
|
||||
(model.count_infected() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_recovered()),
|
||||
(model.count_recovered() as f64) / (total_ppl as f64) * 100.0
|
||||
)
|
||||
.draw_text(ctx),
|
||||
);
|
||||
assert_eq!(total_ppl, model.count_total());
|
||||
}
|
||||
|
||||
col.push(Checkbox::text(
|
||||
ctx,
|
||||
"Show heatmap",
|
||||
None,
|
||||
opts.heatmap.is_some(),
|
||||
));
|
||||
if let Some(ref o) = opts.heatmap {
|
||||
// TODO Display the value...
|
||||
col.push(Widget::row(vec![
|
||||
"Resolution (meters)".draw_text(ctx).margin(5),
|
||||
Spinner::new(ctx, (1, 100), o.resolution)
|
||||
.named("resolution")
|
||||
.align_right()
|
||||
.centered_vert(),
|
||||
]));
|
||||
col.push(Widget::row(vec![
|
||||
"Radius (resolution multiplier)".draw_text(ctx).margin(5),
|
||||
Spinner::new(ctx, (0, 10), o.radius)
|
||||
.named("radius")
|
||||
.align_right()
|
||||
.centered_vert(),
|
||||
]));
|
||||
|
||||
col.push(Widget::row(vec![
|
||||
"Color scheme".draw_text(ctx).margin(5),
|
||||
Widget::dropdown(ctx, "Colors", o.colors, HeatmapColors::choices()),
|
||||
]));
|
||||
|
||||
// Legend for the heatmap colors
|
||||
let (colors, labels) = colors_and_labels.unwrap();
|
||||
col.push(ColorLegend::scale(ctx, colors, labels));
|
||||
}
|
||||
|
||||
Composite::new(Widget::col(col).padding(5).bg(app.cs.panel_bg))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
fn population_options(c: &mut Composite) -> PopulationOptions {
|
||||
let heatmap = if c.is_checked("Show heatmap") {
|
||||
// Did we just change?
|
||||
if c.has_widget("resolution") {
|
||||
Some(HeatmapOptions {
|
||||
resolution: c.spinner("resolution"),
|
||||
radius: c.spinner("radius"),
|
||||
colors: c.dropdown_value("Colors"),
|
||||
})
|
||||
} else {
|
||||
Some(HeatmapOptions::new())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
PopulationOptions {
|
||||
pandemic: c.has_widget("Show pandemic model") && c.is_checked("Show pandemic model"),
|
||||
heatmap,
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::common::{navigate, shortcuts, Layers, Warping};
|
||||
use crate::common::{navigate, shortcuts, Warping};
|
||||
use crate::game::Transition;
|
||||
use crate::layer::Layers;
|
||||
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
||||
use abstutil::clamp;
|
||||
use ezgui::{
|
||||
|
@ -1,17 +1,13 @@
|
||||
mod bus_explorer;
|
||||
mod colors;
|
||||
mod heatmap;
|
||||
mod layers;
|
||||
mod minimap;
|
||||
mod navigate;
|
||||
mod panels;
|
||||
mod shortcuts;
|
||||
mod warp;
|
||||
|
||||
pub use self::bus_explorer::ShowBusRoute;
|
||||
pub use self::colors::{ColorLegend, Colorer};
|
||||
pub use self::heatmap::{make_heatmap, HeatmapOptions};
|
||||
pub use self::layers::Layers;
|
||||
pub use self::minimap::Minimap;
|
||||
pub use self::panels::tool_panel;
|
||||
pub use self::warp::Warping;
|
||||
|
@ -6,10 +6,11 @@ pub use self::lanes::LaneEditor;
|
||||
pub use self::stop_signs::StopSignEditor;
|
||||
pub use self::traffic_signals::TrafficSignalEditor;
|
||||
use crate::app::{App, ShowEverything};
|
||||
use crate::common::{tool_panel, Colorer, CommonState, Layers, Warping};
|
||||
use crate::common::{tool_panel, Colorer, CommonState, Warping};
|
||||
use crate::debug::DebugMode;
|
||||
use crate::game::{msg, State, Transition, WizardState};
|
||||
use crate::helpers::ID;
|
||||
use crate::layer::Layers;
|
||||
use crate::managed::{WrappedComposite, WrappedOutcome};
|
||||
use crate::render::{DrawIntersection, DrawLane, DrawRoad, MIN_ZOOM_FOR_DETAIL};
|
||||
use crate::sandbox::{GameplayMode, SandboxMode};
|
||||
@ -76,7 +77,7 @@ impl State for EditMode {
|
||||
if self.once {
|
||||
self.once = false;
|
||||
// apply_map_edits will do the job later
|
||||
app.layer = Layers::map_edits(ctx, app);
|
||||
app.layer = crate::layer::map::edits(ctx, app);
|
||||
}
|
||||
{
|
||||
let edits = app.primary.map.get_edits();
|
||||
@ -429,7 +430,7 @@ pub fn apply_map_edits(ctx: &mut EventCtx, app: &mut App, edits: MapEdits) {
|
||||
}
|
||||
|
||||
if let Layers::Edits(_) = app.layer {
|
||||
app.layer = Layers::map_edits(ctx, app);
|
||||
app.layer = crate::layer::map::edits(ctx, app);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::common::Colorer;
|
||||
use crate::layer::Layers;
|
||||
use ezgui::{Color, EventCtx, GeomBatch, GfxCtx, Line, Text};
|
||||
use geom::{Circle, Distance, Pt2D};
|
||||
use map_model::{BusRouteID, PathConstraints, PathRequest, PathStep};
|
||||
@ -11,7 +12,7 @@ pub struct ShowBusRoute {
|
||||
}
|
||||
|
||||
impl ShowBusRoute {
|
||||
pub fn new(id: BusRouteID, ctx: &mut EventCtx, app: &App) -> ShowBusRoute {
|
||||
pub fn new(ctx: &mut EventCtx, app: &App, id: BusRouteID) -> Layers {
|
||||
let map = &app.primary.map;
|
||||
let route = app.primary.map.get_br(id);
|
||||
|
||||
@ -62,11 +63,15 @@ impl ShowBusRoute {
|
||||
));
|
||||
}
|
||||
|
||||
ShowBusRoute {
|
||||
colorer: colorer.build_both(ctx, app),
|
||||
labels,
|
||||
bus_locations,
|
||||
}
|
||||
Layers::BusRoute(
|
||||
app.primary.sim.time(),
|
||||
id,
|
||||
ShowBusRoute {
|
||||
colorer: colorer.build_both(ctx, app),
|
||||
labels,
|
||||
bus_locations,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
79
game/src/layer/elevation.rs
Normal file
79
game/src/layer/elevation.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use crate::app::App;
|
||||
use crate::common::Colorer;
|
||||
use crate::layer::Layers;
|
||||
use ezgui::{Color, EventCtx, GeomBatch};
|
||||
use geom::{Distance, PolyLine};
|
||||
|
||||
pub fn new(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Two passes because we have to construct the text first :(
|
||||
let mut max = 0.0_f64;
|
||||
for l in app.primary.map.all_lanes() {
|
||||
let pct = l.percent_grade(&app.primary.map).abs();
|
||||
max = max.max(pct);
|
||||
}
|
||||
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Elevation change",
|
||||
vec![format!("Steepest road: {:.0}%", max * 100.0)],
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["flat", "1%", "5%", "15%", "steeper"],
|
||||
);
|
||||
|
||||
let mut max = 0.0_f64;
|
||||
for l in app.primary.map.all_lanes() {
|
||||
let pct = l.percent_grade(&app.primary.map).abs();
|
||||
max = max.max(pct);
|
||||
|
||||
let color = if pct < 0.01 {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if pct < 0.05 {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if pct < 0.15 {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_l(l.id, color, &app.primary.map);
|
||||
}
|
||||
|
||||
let arrow_color = Color::BLACK;
|
||||
let mut batch = GeomBatch::new();
|
||||
// Time for uphill arrows!
|
||||
// TODO Draw V's, not arrows.
|
||||
// TODO Or try gradient colors.
|
||||
for r in app.primary.map.all_roads() {
|
||||
let (mut pl, _) = r.get_thick_polyline(&app.primary.map).unwrap();
|
||||
let e1 = app.primary.map.get_i(r.src_i).elevation;
|
||||
let e2 = app.primary.map.get_i(r.dst_i).elevation;
|
||||
if (e1 - e2).abs() / pl.length() < 0.01 {
|
||||
// Don't bother with ~flat roads
|
||||
continue;
|
||||
}
|
||||
if e1 > e2 {
|
||||
pl = pl.reversed();
|
||||
}
|
||||
|
||||
let arrow_len = Distance::meters(5.0);
|
||||
let btwn = Distance::meters(10.0);
|
||||
let thickness = Distance::meters(1.0);
|
||||
let len = pl.length();
|
||||
|
||||
let mut dist = arrow_len;
|
||||
while dist + arrow_len <= len {
|
||||
let (pt, angle) = pl.dist_along(dist);
|
||||
batch.push(
|
||||
arrow_color,
|
||||
PolyLine::new(vec![
|
||||
pt.project_away(arrow_len / 2.0, angle.opposite()),
|
||||
pt.project_away(arrow_len / 2.0, angle),
|
||||
])
|
||||
.make_arrow(thickness)
|
||||
.unwrap(),
|
||||
);
|
||||
dist += btwn;
|
||||
}
|
||||
}
|
||||
|
||||
Layers::Elevation(colorer.build_unzoomed(ctx, app), batch.upload(ctx))
|
||||
}
|
70
game/src/layer/map.rs
Normal file
70
game/src/layer/map.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use crate::app::App;
|
||||
use crate::common::Colorer;
|
||||
use crate::layer::Layers;
|
||||
use ezgui::EventCtx;
|
||||
|
||||
pub fn bike_network(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Number and total distance
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
"Bike network",
|
||||
Vec::new(),
|
||||
vec![("bike lanes", app.cs.unzoomed_bike)],
|
||||
);
|
||||
for l in app.primary.map.all_lanes() {
|
||||
if l.is_biking() {
|
||||
colorer.add_l(l.id, app.cs.unzoomed_bike, &app.primary.map);
|
||||
}
|
||||
}
|
||||
Layers::BikeNetwork(colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn bus_network(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO Same color for both?
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
"Bus network",
|
||||
Vec::new(),
|
||||
vec![
|
||||
("bus lanes", app.cs.bus_layer),
|
||||
("bus stops", app.cs.bus_layer),
|
||||
],
|
||||
);
|
||||
for l in app.primary.map.all_lanes() {
|
||||
if l.is_bus() {
|
||||
colorer.add_l(l.id, app.cs.bus_layer, &app.primary.map);
|
||||
}
|
||||
}
|
||||
for bs in app.primary.map.all_bus_stops().keys() {
|
||||
colorer.add_bs(*bs, app.cs.bus_layer);
|
||||
}
|
||||
|
||||
Layers::BusNetwork(colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn edits(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let edits = app.primary.map.get_edits();
|
||||
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
format!("Map edits ({})", edits.edits_name),
|
||||
vec![
|
||||
format!("{} lane types changed", edits.original_lts.len()),
|
||||
format!("{} lanes reversed", edits.reversed_lanes.len()),
|
||||
format!(
|
||||
"{} intersections changed",
|
||||
edits.original_intersections.len()
|
||||
),
|
||||
],
|
||||
vec![("modified lane/intersection", app.cs.edits_layer)],
|
||||
);
|
||||
|
||||
for l in edits.original_lts.keys().chain(&edits.reversed_lanes) {
|
||||
colorer.add_l(*l, app.cs.edits_layer, &app.primary.map);
|
||||
}
|
||||
for i in edits.original_intersections.keys() {
|
||||
colorer.add_i(*i, app.cs.edits_layer);
|
||||
}
|
||||
|
||||
Layers::Edits(colorer.build_both(ctx, app))
|
||||
}
|
383
game/src/layer/mod.rs
Normal file
383
game/src/layer/mod.rs
Normal file
@ -0,0 +1,383 @@
|
||||
pub mod bus;
|
||||
mod elevation;
|
||||
pub mod map;
|
||||
mod parking;
|
||||
mod population;
|
||||
pub mod traffic;
|
||||
pub mod trips;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::common::{Colorer, HeatmapOptions, Warping};
|
||||
use crate::game::Transition;
|
||||
use crate::helpers::ID;
|
||||
use crate::managed::{ManagedGUIState, WrappedComposite};
|
||||
use crate::render::MIN_ZOOM_FOR_DETAIL;
|
||||
use ezgui::{
|
||||
hotkey, Btn, Color, Composite, Drawable, EventCtx, GfxCtx, Key, Line, Outcome, Widget,
|
||||
};
|
||||
use geom::Time;
|
||||
use map_model::{BusRouteID, IntersectionID};
|
||||
|
||||
// TODO Good ideas in
|
||||
// https://towardsdatascience.com/top-10-map-types-in-data-visualization-b3a80898ea70
|
||||
|
||||
pub enum Layers {
|
||||
Inactive,
|
||||
ParkingOccupancy(Time, Colorer),
|
||||
WorstDelay(Time, Colorer),
|
||||
TrafficJams(Time, Colorer),
|
||||
CumulativeThroughput(Time, Colorer),
|
||||
BikeNetwork(Colorer),
|
||||
BusNetwork(Colorer),
|
||||
Elevation(Colorer, Drawable),
|
||||
Edits(Colorer),
|
||||
TripsHistogram(Time, Composite),
|
||||
PopulationMap(Time, population::PopulationOptions, Drawable, Composite),
|
||||
|
||||
// These aren't selectable from the main picker; they're particular to some object.
|
||||
// TODO They should become something else, like an info panel tab.
|
||||
IntersectionDemand(Time, IntersectionID, Drawable, Composite),
|
||||
BusRoute(Time, BusRouteID, bus::ShowBusRoute),
|
||||
}
|
||||
|
||||
impl Layers {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Layers::Inactive => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Since Layers is embedded in UI, we have to do this slight trick
|
||||
pub fn update(ctx: &mut EventCtx, app: &mut App, minimap: &Composite) -> Option<Transition> {
|
||||
let now = app.primary.sim.time();
|
||||
match app.layer {
|
||||
Layers::ParkingOccupancy(t, _) => {
|
||||
if now != t {
|
||||
app.layer = parking::new(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::WorstDelay(t, _) => {
|
||||
if now != t {
|
||||
app.layer = traffic::delay(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::TrafficJams(t, _) => {
|
||||
if now != t {
|
||||
app.layer = traffic::traffic_jams(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::CumulativeThroughput(t, _) => {
|
||||
if now != t {
|
||||
app.layer = traffic::throughput(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::IntersectionDemand(t, i, _, _) => {
|
||||
if now != t {
|
||||
app.layer = traffic::intersection_demand(ctx, app, i);
|
||||
}
|
||||
}
|
||||
Layers::TripsHistogram(t, _) => {
|
||||
if now != t {
|
||||
app.layer = trips::trips_histogram(ctx, app);
|
||||
}
|
||||
}
|
||||
Layers::BusRoute(t, id, _) => {
|
||||
if now != t {
|
||||
app.layer = bus::ShowBusRoute::new(ctx, app, id);
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(t, ref opts, _, _) => {
|
||||
if now != t {
|
||||
app.layer = population::new(ctx, app, opts.clone());
|
||||
}
|
||||
}
|
||||
// No updates needed
|
||||
Layers::Inactive
|
||||
| Layers::BikeNetwork(_)
|
||||
| Layers::BusNetwork(_)
|
||||
| Layers::Elevation(_, _)
|
||||
| Layers::Edits(_) => {}
|
||||
};
|
||||
|
||||
match app.layer {
|
||||
Layers::ParkingOccupancy(_, ref mut c)
|
||||
| Layers::BikeNetwork(ref mut c)
|
||||
| Layers::BusNetwork(ref mut c)
|
||||
| Layers::Elevation(ref mut c, _)
|
||||
| Layers::WorstDelay(_, ref mut c)
|
||||
| Layers::TrafficJams(_, ref mut c)
|
||||
| Layers::CumulativeThroughput(_, ref mut c)
|
||||
| Layers::Edits(ref mut c) => {
|
||||
c.legend.align_above(ctx, minimap);
|
||||
if c.event(ctx) {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
}
|
||||
Layers::BusRoute(_, _, ref mut c) => {
|
||||
c.colorer.legend.align_above(ctx, minimap);
|
||||
if c.colorer.event(ctx) {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
}
|
||||
Layers::IntersectionDemand(_, i, _, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"intersection demand" => {
|
||||
let id = ID::Intersection(i);
|
||||
return Some(Transition::Push(Warping::new(
|
||||
ctx,
|
||||
id.canonical_point(&app.primary).unwrap(),
|
||||
Some(10.0),
|
||||
Some(id.clone()),
|
||||
&mut app.primary,
|
||||
)));
|
||||
}
|
||||
"X" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Layers::TripsHistogram(_, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"X" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(_, ref mut opts, _, ref mut c) => {
|
||||
c.align_above(ctx, minimap);
|
||||
match c.event(ctx) {
|
||||
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
||||
"close" => {
|
||||
app.layer = Layers::Inactive;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => {
|
||||
let new_opts = population::population_options(c);
|
||||
if *opts != new_opts {
|
||||
app.layer = population::new(ctx, app, new_opts);
|
||||
// Immediately fix the alignment. TODO Do this for all of them, in a
|
||||
// more uniform way
|
||||
if let Layers::PopulationMap(_, _, _, ref mut c) = app.layer {
|
||||
c.align_above(ctx, minimap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Layers::Inactive => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Draw both controls and, if zoomed, the layer contents
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
match self {
|
||||
Layers::Inactive => {}
|
||||
Layers::ParkingOccupancy(_, ref c)
|
||||
| Layers::BikeNetwork(ref c)
|
||||
| Layers::BusNetwork(ref c)
|
||||
| Layers::WorstDelay(_, ref c)
|
||||
| Layers::TrafficJams(_, ref c)
|
||||
| Layers::CumulativeThroughput(_, ref c)
|
||||
| Layers::Edits(ref c) => {
|
||||
c.draw(g);
|
||||
}
|
||||
Layers::Elevation(ref c, ref draw) => {
|
||||
c.draw(g);
|
||||
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
|
||||
g.redraw(draw);
|
||||
}
|
||||
}
|
||||
Layers::PopulationMap(_, _, ref draw, ref composite) => {
|
||||
composite.draw(g);
|
||||
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL {
|
||||
g.redraw(draw);
|
||||
}
|
||||
}
|
||||
// All of these shouldn't care about zoom
|
||||
Layers::TripsHistogram(_, ref composite) => {
|
||||
composite.draw(g);
|
||||
}
|
||||
Layers::IntersectionDemand(_, _, ref draw, ref legend) => {
|
||||
g.redraw(draw);
|
||||
legend.draw(g);
|
||||
}
|
||||
Layers::BusRoute(_, _, ref s) => {
|
||||
s.draw(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just draw contents and do it always
|
||||
pub fn draw_minimap(&self, g: &mut GfxCtx) {
|
||||
match self {
|
||||
Layers::Inactive => {}
|
||||
Layers::ParkingOccupancy(_, ref c)
|
||||
| Layers::BikeNetwork(ref c)
|
||||
| Layers::BusNetwork(ref c)
|
||||
| Layers::WorstDelay(_, ref c)
|
||||
| Layers::TrafficJams(_, ref c)
|
||||
| Layers::CumulativeThroughput(_, ref c)
|
||||
| Layers::Edits(ref c) => {
|
||||
g.redraw(&c.unzoomed);
|
||||
}
|
||||
Layers::Elevation(ref c, ref draw) => {
|
||||
g.redraw(&c.unzoomed);
|
||||
g.redraw(draw);
|
||||
}
|
||||
Layers::PopulationMap(_, _, ref draw, _) => {
|
||||
g.redraw(draw);
|
||||
}
|
||||
Layers::TripsHistogram(_, _) => {}
|
||||
Layers::IntersectionDemand(_, _, _, _) => {}
|
||||
Layers::BusRoute(_, _, ref s) => {
|
||||
s.draw(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_layers(ctx: &mut EventCtx, app: &App) -> Option<Transition> {
|
||||
let mut col = vec![Widget::row(vec![
|
||||
Line("Layers").small_heading().draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
])];
|
||||
|
||||
col.extend(vec![
|
||||
Btn::text_fg("None").build_def(ctx, hotkey(Key::N)),
|
||||
Btn::text_fg("map edits").build_def(ctx, hotkey(Key::E)),
|
||||
Btn::text_fg("worst traffic jams").build_def(ctx, hotkey(Key::J)),
|
||||
Btn::text_fg("elevation").build_def(ctx, hotkey(Key::S)),
|
||||
Btn::text_fg("parking occupancy").build_def(ctx, hotkey(Key::P)),
|
||||
Btn::text_fg("delay").build_def(ctx, hotkey(Key::D)),
|
||||
Btn::text_fg("throughput").build_def(ctx, hotkey(Key::T)),
|
||||
Btn::text_fg("bike network").build_def(ctx, hotkey(Key::B)),
|
||||
Btn::text_fg("bus network").build_def(ctx, hotkey(Key::U)),
|
||||
Btn::text_fg("population map").build_def(ctx, hotkey(Key::X)),
|
||||
]);
|
||||
if let Some(name) = match app.layer {
|
||||
Layers::Inactive => Some("None"),
|
||||
Layers::ParkingOccupancy(_, _) => Some("parking occupancy"),
|
||||
Layers::WorstDelay(_, _) => Some("delay"),
|
||||
Layers::TrafficJams(_, _) => Some("worst traffic jams"),
|
||||
Layers::CumulativeThroughput(_, _) => Some("throughput"),
|
||||
Layers::BikeNetwork(_) => Some("bike network"),
|
||||
Layers::BusNetwork(_) => Some("bus network"),
|
||||
Layers::Elevation(_, _) => Some("elevation"),
|
||||
Layers::Edits(_) => Some("map edits"),
|
||||
Layers::PopulationMap(_, _, _, _) => Some("population map"),
|
||||
_ => None,
|
||||
} {
|
||||
for btn in &mut col {
|
||||
if btn.is_btn(name) {
|
||||
*btn = Btn::text_bg2(name).inactive(ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let c = WrappedComposite::new(
|
||||
Composite::new(
|
||||
Widget::col(col.into_iter().map(|w| w.margin_below(15)).collect())
|
||||
.bg(app.cs.panel_bg)
|
||||
.outline(2.0, Color::WHITE)
|
||||
.padding(10),
|
||||
)
|
||||
.max_size_percent(35, 70)
|
||||
.build(ctx),
|
||||
)
|
||||
.cb("close", Box::new(|_, _| Some(Transition::Pop)))
|
||||
.maybe_cb(
|
||||
"None",
|
||||
Box::new(|_, app| {
|
||||
app.layer = Layers::Inactive;
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"parking occupancy",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = parking::new(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"delay",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = traffic::delay(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"worst traffic jams",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = traffic::traffic_jams(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"throughput",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = traffic::throughput(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"bike network",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = map::bike_network(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"bus network",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = map::bus_network(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"elevation",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = elevation::new(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"map edits",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = map::edits(ctx, app);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
)
|
||||
.maybe_cb(
|
||||
"population map",
|
||||
Box::new(|ctx, app| {
|
||||
app.layer = population::new(
|
||||
ctx,
|
||||
app,
|
||||
population::PopulationOptions {
|
||||
pandemic: false,
|
||||
heatmap: Some(HeatmapOptions::new()),
|
||||
},
|
||||
);
|
||||
Some(Transition::Pop)
|
||||
}),
|
||||
);
|
||||
Some(Transition::Push(ManagedGUIState::over_map(c)))
|
||||
}
|
||||
}
|
69
game/src/layer/parking.rs
Normal file
69
game/src/layer/parking.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use crate::app::App;
|
||||
use crate::common::Colorer;
|
||||
use crate::layer::Layers;
|
||||
use abstutil::{prettyprint_usize, Counter};
|
||||
use ezgui::EventCtx;
|
||||
use sim::ParkingSpot;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn new(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let (filled_spots, avail_spots) = app.primary.sim.get_all_parking_spots();
|
||||
|
||||
// TODO Some kind of Scale abstraction that maps intervals of some quantity (percent,
|
||||
// duration) to these 4 colors
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Parking occupancy (per road)",
|
||||
vec![
|
||||
format!("{} spots filled", prettyprint_usize(filled_spots.len())),
|
||||
format!("{} spots available ", prettyprint_usize(avail_spots.len())),
|
||||
],
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0%", "40%", "70%", "90%", "100%"],
|
||||
);
|
||||
|
||||
let lane = |spot| match spot {
|
||||
ParkingSpot::Onstreet(l, _) => l,
|
||||
ParkingSpot::Offstreet(b, _) => app
|
||||
.primary
|
||||
.map
|
||||
.get_b(b)
|
||||
.parking
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.driving_pos
|
||||
.lane(),
|
||||
};
|
||||
|
||||
let mut filled = Counter::new();
|
||||
let mut avail = Counter::new();
|
||||
let mut keys = HashSet::new();
|
||||
for spot in filled_spots {
|
||||
let l = lane(spot);
|
||||
keys.insert(l);
|
||||
filled.inc(l);
|
||||
}
|
||||
for spot in avail_spots {
|
||||
let l = lane(spot);
|
||||
keys.insert(l);
|
||||
avail.inc(l);
|
||||
}
|
||||
|
||||
for l in keys {
|
||||
let open = avail.get(l);
|
||||
let closed = filled.get(l);
|
||||
let percent = (closed as f64) / ((open + closed) as f64);
|
||||
let color = if percent < 0.4 {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if percent < 0.7 {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if percent < 0.9 {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_l(l, color, &app.primary.map);
|
||||
}
|
||||
|
||||
Layers::ParkingOccupancy(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
208
game/src/layer/population.rs
Normal file
208
game/src/layer/population.rs
Normal file
@ -0,0 +1,208 @@
|
||||
use crate::app::App;
|
||||
use crate::colors::HeatmapColors;
|
||||
use crate::common::{make_heatmap, ColorLegend, HeatmapOptions};
|
||||
use crate::layer::Layers;
|
||||
use abstutil::prettyprint_usize;
|
||||
use ezgui::{
|
||||
hotkey, Btn, Checkbox, Color, Composite, EventCtx, GeomBatch, HorizontalAlignment, Key, Line,
|
||||
Spinner, TextExt, VerticalAlignment, Widget,
|
||||
};
|
||||
use geom::{Circle, Distance, Pt2D};
|
||||
use sim::{GetDrawAgents, PersonState};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// TODO Disable drawing unzoomed agents... or alternatively, implement this by asking Sim to
|
||||
// return this kind of data instead!
|
||||
pub fn new(ctx: &mut EventCtx, app: &App, opts: PopulationOptions) -> Layers {
|
||||
// Only display infected people if this is enabled.
|
||||
let maybe_pandemic = if opts.pandemic {
|
||||
app.primary.sim.get_pandemic_model()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut pts = Vec::new();
|
||||
// Faster to grab all agent positions than individually map trips to agent positions.
|
||||
if let Some(ref model) = maybe_pandemic {
|
||||
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
|
||||
if let Some(p) = a.person {
|
||||
if model.infected.contains_key(&p) {
|
||||
pts.push(a.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
|
||||
pts.push(a.pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Many people are probably in the same building. If we're building a heatmap, we
|
||||
// absolutely care about these repeats! If we're just drawing the simple dot map, avoid
|
||||
// drawing repeat circles.
|
||||
let mut seen_bldgs = HashSet::new();
|
||||
let mut repeat_pts = Vec::new();
|
||||
for person in app.primary.sim.get_all_people() {
|
||||
match person.state {
|
||||
// Already covered above
|
||||
PersonState::Trip(_) => {}
|
||||
PersonState::Inside(b) => {
|
||||
if maybe_pandemic
|
||||
.as_ref()
|
||||
.map(|m| !m.infected.contains_key(&person.id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let pt = app.primary.map.get_b(b).polygon.center();
|
||||
if seen_bldgs.contains(&b) {
|
||||
repeat_pts.push(pt);
|
||||
} else {
|
||||
seen_bldgs.insert(b);
|
||||
pts.push(pt);
|
||||
}
|
||||
}
|
||||
PersonState::OffMap | PersonState::Limbo => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut batch = GeomBatch::new();
|
||||
let colors_and_labels = if let Some(ref o) = opts.heatmap {
|
||||
pts.extend(repeat_pts);
|
||||
Some(make_heatmap(
|
||||
&mut batch,
|
||||
app.primary.map.get_bounds(),
|
||||
pts,
|
||||
o,
|
||||
))
|
||||
} else {
|
||||
// It's quite silly to produce triangles for the same circle over and over again. ;)
|
||||
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
|
||||
for pt in pts {
|
||||
batch.push(Color::RED.alpha(0.8), circle.translate(pt.x(), pt.y()));
|
||||
}
|
||||
None
|
||||
};
|
||||
let controls = population_controls(ctx, app, &opts, colors_and_labels);
|
||||
Layers::PopulationMap(app.primary.sim.time(), opts, ctx.upload(batch), controls)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PopulationOptions {
|
||||
pub pandemic: bool,
|
||||
// If None, just a dot map
|
||||
pub heatmap: Option<HeatmapOptions>,
|
||||
}
|
||||
|
||||
// This function sounds more ominous than it should.
|
||||
fn population_controls(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
opts: &PopulationOptions,
|
||||
colors_and_labels: Option<(Vec<Color>, Vec<String>)>,
|
||||
) -> Composite {
|
||||
let (total_ppl, ppl_in_bldg, ppl_off_map) = app.primary.sim.num_ppl();
|
||||
|
||||
let mut col = vec![
|
||||
Widget::row(vec![
|
||||
Widget::draw_svg(ctx, "../data/system/assets/tools/layers.svg").margin_right(10),
|
||||
Line(format!("Population: {}", prettyprint_usize(total_ppl))).draw(ctx),
|
||||
Btn::plaintext("X")
|
||||
.build(ctx, "close", hotkey(Key::Escape))
|
||||
.align_right(),
|
||||
]),
|
||||
Widget::row(vec![
|
||||
Widget::row(vec![
|
||||
Widget::draw_svg(ctx, "../data/system/assets/tools/home.svg").margin_right(10),
|
||||
Line(prettyprint_usize(ppl_in_bldg)).small().draw(ctx),
|
||||
]),
|
||||
Line(format!("Off-map: {}", prettyprint_usize(ppl_off_map)))
|
||||
.small()
|
||||
.draw(ctx),
|
||||
])
|
||||
.centered(),
|
||||
if app.primary.sim.get_pandemic_model().is_some() {
|
||||
Checkbox::text(ctx, "Show pandemic model", None, opts.pandemic)
|
||||
} else {
|
||||
Widget::nothing()
|
||||
},
|
||||
];
|
||||
|
||||
if opts.pandemic {
|
||||
let model = app.primary.sim.get_pandemic_model().unwrap();
|
||||
col.push(
|
||||
format!(
|
||||
"Pandemic model: {} S ({:.1}%), {} E ({:.1}%), {} I ({:.1}%), {} R ({:.1}%)",
|
||||
prettyprint_usize(model.count_sane()),
|
||||
(model.count_sane() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_exposed()),
|
||||
(model.count_exposed() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_infected()),
|
||||
(model.count_infected() as f64) / (total_ppl as f64) * 100.0,
|
||||
prettyprint_usize(model.count_recovered()),
|
||||
(model.count_recovered() as f64) / (total_ppl as f64) * 100.0
|
||||
)
|
||||
.draw_text(ctx),
|
||||
);
|
||||
assert_eq!(total_ppl, model.count_total());
|
||||
}
|
||||
|
||||
col.push(Checkbox::text(
|
||||
ctx,
|
||||
"Show heatmap",
|
||||
None,
|
||||
opts.heatmap.is_some(),
|
||||
));
|
||||
if let Some(ref o) = opts.heatmap {
|
||||
// TODO Display the value...
|
||||
col.push(Widget::row(vec![
|
||||
"Resolution (meters)".draw_text(ctx).margin(5),
|
||||
Spinner::new(ctx, (1, 100), o.resolution)
|
||||
.named("resolution")
|
||||
.align_right()
|
||||
.centered_vert(),
|
||||
]));
|
||||
col.push(Widget::row(vec![
|
||||
"Radius (resolution multiplier)".draw_text(ctx).margin(5),
|
||||
Spinner::new(ctx, (0, 10), o.radius)
|
||||
.named("radius")
|
||||
.align_right()
|
||||
.centered_vert(),
|
||||
]));
|
||||
|
||||
col.push(Widget::row(vec![
|
||||
"Color scheme".draw_text(ctx).margin(5),
|
||||
Widget::dropdown(ctx, "Colors", o.colors, HeatmapColors::choices()),
|
||||
]));
|
||||
|
||||
// Legend for the heatmap colors
|
||||
let (colors, labels) = colors_and_labels.unwrap();
|
||||
col.push(ColorLegend::scale(ctx, colors, labels));
|
||||
}
|
||||
|
||||
Composite::new(Widget::col(col).padding(5).bg(app.cs.panel_bg))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx)
|
||||
}
|
||||
|
||||
pub fn population_options(c: &mut Composite) -> PopulationOptions {
|
||||
let heatmap = if c.is_checked("Show heatmap") {
|
||||
// Did we just change?
|
||||
if c.has_widget("resolution") {
|
||||
Some(HeatmapOptions {
|
||||
resolution: c.spinner("resolution"),
|
||||
radius: c.spinner("radius"),
|
||||
colors: c.dropdown_value("Colors"),
|
||||
})
|
||||
} else {
|
||||
Some(HeatmapOptions::new())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
PopulationOptions {
|
||||
pandemic: c.has_widget("Show pandemic model") && c.is_checked("Show pandemic model"),
|
||||
heatmap,
|
||||
}
|
||||
}
|
185
game/src/layer/traffic.rs
Normal file
185
game/src/layer/traffic.rs
Normal file
@ -0,0 +1,185 @@
|
||||
use crate::app::App;
|
||||
use crate::common::{ColorLegend, Colorer};
|
||||
use crate::layer::Layers;
|
||||
use ezgui::{
|
||||
Btn, Color, Composite, EventCtx, GeomBatch, HorizontalAlignment, TextExt, VerticalAlignment,
|
||||
Widget,
|
||||
};
|
||||
use geom::{Distance, Duration, PolyLine};
|
||||
use map_model::IntersectionID;
|
||||
|
||||
pub fn delay(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
// TODO explain more
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Delay (minutes)",
|
||||
Vec::new(),
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0", "1", "5", "15", "longer"],
|
||||
);
|
||||
|
||||
let (per_road, per_intersection) = app.primary.sim.worst_delay(&app.primary.map);
|
||||
for (r, d) in per_road {
|
||||
let color = if d < Duration::minutes(1) {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if d < Duration::minutes(5) {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if d < Duration::minutes(15) {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_r(r, color, &app.primary.map);
|
||||
}
|
||||
for (i, d) in per_intersection {
|
||||
let color = if d < Duration::minutes(1) {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if d < Duration::minutes(5) {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if d < Duration::minutes(15) {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_i(i, color);
|
||||
}
|
||||
|
||||
Layers::WorstDelay(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn traffic_jams(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let jams = app.primary.sim.delayed_intersections(Duration::minutes(5));
|
||||
|
||||
// TODO Silly colors. Weird way of presenting this information. Epicenter + radius?
|
||||
let others = Color::hex("#7FFA4D");
|
||||
let early = Color::hex("#F4DA22");
|
||||
let earliest = Color::hex("#EB5757");
|
||||
let mut colorer = Colorer::discrete(
|
||||
ctx,
|
||||
format!("{} traffic jams", jams.len()),
|
||||
Vec::new(),
|
||||
vec![
|
||||
("longest lasting", earliest),
|
||||
("recent problems", early),
|
||||
("others", others),
|
||||
],
|
||||
);
|
||||
|
||||
for (idx, (i, _)) in jams.into_iter().enumerate() {
|
||||
if idx == 0 {
|
||||
colorer.add_i(i, earliest);
|
||||
} else if idx <= 5 {
|
||||
colorer.add_i(i, early);
|
||||
} else {
|
||||
colorer.add_i(i, others);
|
||||
}
|
||||
}
|
||||
|
||||
Layers::TrafficJams(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn throughput(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
let mut colorer = Colorer::scaled(
|
||||
ctx,
|
||||
"Throughput (percentiles)",
|
||||
Vec::new(),
|
||||
app.cs.good_to_bad.to_vec(),
|
||||
vec!["0", "50", "90", "99", "100"],
|
||||
);
|
||||
|
||||
let stats = &app.primary.sim.get_analytics().thruput_stats;
|
||||
|
||||
// TODO If there are many duplicate counts, arbitrarily some will look heavier! Find the
|
||||
// disribution of counts instead.
|
||||
// TODO Actually display the counts at these percentiles
|
||||
// TODO Dump the data in debug mode
|
||||
{
|
||||
let roads = stats.count_per_road.sorted_asc();
|
||||
let p50_idx = ((roads.len() as f64) * 0.5) as usize;
|
||||
let p90_idx = ((roads.len() as f64) * 0.9) as usize;
|
||||
let p99_idx = ((roads.len() as f64) * 0.99) as usize;
|
||||
for (idx, r) in roads.into_iter().enumerate() {
|
||||
let color = if idx < p50_idx {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if idx < p90_idx {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if idx < p99_idx {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_r(*r, color, &app.primary.map);
|
||||
}
|
||||
}
|
||||
// TODO dedupe
|
||||
{
|
||||
let intersections = stats.count_per_intersection.sorted_asc();
|
||||
let p50_idx = ((intersections.len() as f64) * 0.5) as usize;
|
||||
let p90_idx = ((intersections.len() as f64) * 0.9) as usize;
|
||||
let p99_idx = ((intersections.len() as f64) * 0.99) as usize;
|
||||
for (idx, i) in intersections.into_iter().enumerate() {
|
||||
let color = if idx < p50_idx {
|
||||
app.cs.good_to_bad[0]
|
||||
} else if idx < p90_idx {
|
||||
app.cs.good_to_bad[1]
|
||||
} else if idx < p99_idx {
|
||||
app.cs.good_to_bad[2]
|
||||
} else {
|
||||
app.cs.good_to_bad[3]
|
||||
};
|
||||
colorer.add_i(*i, color);
|
||||
}
|
||||
}
|
||||
|
||||
Layers::CumulativeThroughput(app.primary.sim.time(), colorer.build_unzoomed(ctx, app))
|
||||
}
|
||||
|
||||
pub fn intersection_demand(ctx: &mut EventCtx, app: &App, i: IntersectionID) -> Layers {
|
||||
let mut batch = GeomBatch::new();
|
||||
|
||||
let mut total_demand = 0;
|
||||
let mut demand_per_group: Vec<(&PolyLine, usize)> = Vec::new();
|
||||
for g in app.primary.map.get_traffic_signal(i).turn_groups.values() {
|
||||
let demand = app
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.thruput_stats
|
||||
.demand
|
||||
.get(&g.id)
|
||||
.cloned()
|
||||
.unwrap_or(0);
|
||||
if demand > 0 {
|
||||
total_demand += demand;
|
||||
demand_per_group.push((&g.geom, demand));
|
||||
}
|
||||
}
|
||||
|
||||
for (pl, demand) in demand_per_group {
|
||||
let percent = (demand as f64) / (total_demand as f64);
|
||||
batch.push(
|
||||
Color::RED,
|
||||
pl.make_arrow(percent * Distance::meters(5.0)).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let col = vec![
|
||||
Widget::row(vec![
|
||||
"intersection demand".draw_text(ctx),
|
||||
Btn::svg_def("../data/system/assets/tools/location.svg")
|
||||
.build(ctx, "intersection demand", None)
|
||||
.margin(5),
|
||||
Btn::text_fg("X").build_def(ctx, None).align_right(),
|
||||
]),
|
||||
ColorLegend::row(ctx, Color::RED, "current demand"),
|
||||
];
|
||||
|
||||
Layers::IntersectionDemand(
|
||||
app.primary.sim.time(),
|
||||
i,
|
||||
batch.upload(ctx),
|
||||
Composite::new(Widget::col(col).bg(app.cs.panel_bg))
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx),
|
||||
)
|
||||
}
|
45
game/src/layer/trips.rs
Normal file
45
game/src/layer/trips.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use crate::app::App;
|
||||
use crate::layer::Layers;
|
||||
use ezgui::{
|
||||
Btn, Color, Composite, EventCtx, Histogram, HorizontalAlignment, Line, Text, VerticalAlignment,
|
||||
Widget,
|
||||
};
|
||||
|
||||
pub fn trips_histogram(ctx: &mut EventCtx, app: &App) -> Layers {
|
||||
if app.has_prebaked().is_none() {
|
||||
return Layers::Inactive;
|
||||
}
|
||||
|
||||
let now = app.primary.sim.time();
|
||||
Layers::TripsHistogram(
|
||||
now,
|
||||
Composite::new(
|
||||
Widget::col(vec![
|
||||
Widget::row(vec![
|
||||
{
|
||||
let mut txt = Text::from(Line("Are trips "));
|
||||
txt.append(Line("faster").fg(Color::GREEN));
|
||||
txt.append(Line(", "));
|
||||
txt.append(Line("slower").fg(Color::RED));
|
||||
txt.append(Line(", or "));
|
||||
txt.append(Line("the same").fg(Color::YELLOW));
|
||||
txt.append(Line("?"));
|
||||
txt.draw(ctx)
|
||||
}
|
||||
.margin(10),
|
||||
Btn::text_fg("X").build_def(ctx, None).align_right(),
|
||||
]),
|
||||
Histogram::new(
|
||||
app.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.trip_time_deltas(now, app.prebaked()),
|
||||
ctx,
|
||||
),
|
||||
])
|
||||
.bg(app.cs.panel_bg),
|
||||
)
|
||||
.aligned(HorizontalAlignment::Right, VerticalAlignment::Center)
|
||||
.build(ctx),
|
||||
)
|
||||
}
|
@ -9,6 +9,7 @@ mod edit;
|
||||
mod game;
|
||||
mod helpers;
|
||||
mod info;
|
||||
mod layer;
|
||||
mod managed;
|
||||
mod options;
|
||||
mod pregame;
|
||||
|
@ -1,5 +1,4 @@
|
||||
use crate::app::App;
|
||||
use crate::common::Layers;
|
||||
use crate::game::Transition;
|
||||
use crate::managed::{WrappedComposite, WrappedOutcome};
|
||||
use crate::sandbox::gameplay::{challenge_controller, FinalScore, GameplayMode, GameplayState};
|
||||
@ -43,7 +42,7 @@ impl GameplayState for FixTrafficSignals {
|
||||
) -> (Option<Transition>, bool) {
|
||||
// Once is never...
|
||||
if self.once {
|
||||
app.layer = Layers::trips_histogram(ctx, app);
|
||||
app.layer = crate::layer::trips::trips_histogram(ctx, app);
|
||||
self.once = false;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ mod speed;
|
||||
|
||||
use self::misc_tools::{RoutePreview, ShowTrafficSignal, TurnExplorer};
|
||||
use crate::app::App;
|
||||
use crate::common::{tool_panel, CommonState, ContextualActions, Layers, Minimap};
|
||||
use crate::common::{tool_panel, CommonState, ContextualActions, Minimap};
|
||||
use crate::debug::DebugMode;
|
||||
use crate::edit::{
|
||||
apply_map_edits, can_edit_lane, save_edits_as, EditMode, LaneEditor, StopSignEditor,
|
||||
@ -13,6 +13,7 @@ use crate::edit::{
|
||||
};
|
||||
use crate::game::{State, Transition, WizardState};
|
||||
use crate::helpers::{cmp_duration_shorter, ID};
|
||||
use crate::layer::Layers;
|
||||
use crate::managed::{WrappedComposite, WrappedOutcome};
|
||||
use crate::pregame::main_menu;
|
||||
use crate::render::AgentColorScheme;
|
||||
@ -417,7 +418,7 @@ impl AgentMeter {
|
||||
)));
|
||||
}
|
||||
"compare trips to baseline" => {
|
||||
app.layer = Layers::trips_histogram(ctx, app);
|
||||
app.layer = crate::layer::trips::trips_histogram(ctx, app);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
@ -491,7 +492,7 @@ impl ContextualActions for Actions {
|
||||
Transition::Push(ShowTrafficSignal::new(ctx, app, i))
|
||||
}
|
||||
(ID::Intersection(i), "show current demand") => {
|
||||
app.layer = Layers::intersection_demand(i, ctx, app);
|
||||
app.layer = crate::layer::traffic::intersection_demand(ctx, app, i);
|
||||
Transition::Keep
|
||||
}
|
||||
(ID::Intersection(i), "edit traffic signal") => {
|
||||
@ -519,8 +520,11 @@ impl ContextualActions for Actions {
|
||||
),
|
||||
(ID::Car(c), "show route") => {
|
||||
*close_panel = false;
|
||||
app.layer =
|
||||
Layers::show_bus_route(app.primary.sim.bus_route_id(c).unwrap(), ctx, app);
|
||||
app.layer = crate::layer::bus::ShowBusRoute::new(
|
||||
ctx,
|
||||
app,
|
||||
app.primary.sim.bus_route_id(c).unwrap(),
|
||||
);
|
||||
Transition::Keep
|
||||
}
|
||||
(_, "follow") => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::common::{Layers, Warping};
|
||||
use crate::common::Warping;
|
||||
use crate::game::{msg, State, Transition};
|
||||
use crate::helpers::ID;
|
||||
use crate::sandbox::{GameplayMode, SandboxMode};
|
||||
@ -470,7 +470,7 @@ impl State for TimeWarpScreen {
|
||||
Duration::seconds(0.033),
|
||||
) {
|
||||
let id = ID::Intersection(problems[0].0);
|
||||
app.layer = Layers::traffic_jams(ctx, app);
|
||||
app.layer = crate::layer::traffic::traffic_jams(ctx, app);
|
||||
return Transition::Replace(Warping::new(
|
||||
ctx,
|
||||
id.canonical_point(&app.primary).unwrap(),
|
||||
|
Loading…
Reference in New Issue
Block a user