game/layer/
problems.rs

1use std::collections::BTreeSet;
2use std::fmt::Write;
3
4use anyhow::Result;
5
6use abstutil::prettyprint_usize;
7use geom::{Circle, Distance, Pt2D, Time};
8use map_gui::tools::{checkbox_per_mode, make_heatmap, HeatmapOptions};
9use map_model::Traversable;
10use sim::{Problem, ProblemType, TripInfo};
11use synthpop::TripMode;
12use widgetry::mapspace::ToggleZoomed;
13use widgetry::tools::PopupMsg;
14use widgetry::{
15    Color, EventCtx, GfxCtx, Line, Outcome, Panel, PanelDims, Slider, Text, TextExt, Toggle,
16    Transition, Widget,
17};
18
19use super::problems_diff::ProblemTypes;
20use crate::app::App;
21use crate::layer::{header, problems_diff, Layer, LayerOutcome, PANEL_PLACEMENT};
22
23pub struct ProblemMap {
24    time: Time,
25    opts: Options,
26    draw: ToggleZoomed,
27    panel: Panel,
28}
29
30impl Layer for ProblemMap {
31    fn name(&self) -> Option<&'static str> {
32        Some("problem map")
33    }
34    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
35        if app.primary.sim.time() != self.time {
36            let mut new = ProblemMap::new(ctx, app, self.opts.clone());
37            new.panel.restore(ctx, &self.panel);
38            *self = new;
39        }
40
41        match self.panel.event(ctx) {
42            Outcome::Clicked(x) => match x.as_ref() {
43                "close" => {
44                    return Some(LayerOutcome::Close);
45                }
46                "Export to CSV" => {
47                    return Some(LayerOutcome::Transition(Transition::Push(
48                        match export_raw_problems(app) {
49                            Ok(path) => PopupMsg::new_state(
50                                ctx,
51                                "Data exported",
52                                vec![format!("Data exported to {path}")],
53                            ),
54                            Err(err) => {
55                                PopupMsg::new_state(ctx, "Export failed", vec![err.to_string()])
56                            }
57                        },
58                    )));
59                }
60                _ => unreachable!(),
61            },
62            Outcome::Changed(x) => {
63                if x == "Compare before proposal" {
64                    return Some(LayerOutcome::Replace(Box::new(
65                        problems_diff::RelativeProblemMap::new(ctx, app, self.opts.types.clone()),
66                    )));
67                }
68
69                let new_opts = self.options(app);
70                if self.opts != new_opts {
71                    *self = ProblemMap::new(ctx, app, new_opts);
72                }
73            }
74            _ => {}
75        }
76        None
77    }
78    fn draw(&self, g: &mut GfxCtx, _: &App) {
79        self.panel.draw(g);
80        self.draw.draw(g);
81    }
82    fn draw_minimap(&self, g: &mut GfxCtx) {
83        g.redraw(&self.draw.unzoomed);
84    }
85}
86
87impl ProblemMap {
88    pub fn new(ctx: &mut EventCtx, app: &App, opts: Options) -> ProblemMap {
89        let mut pts = Vec::new();
90        for (trip, problems) in &app.primary.sim.get_analytics().problems_per_trip {
91            for (time, problem) in problems {
92                if opts.show(app.primary.sim.trip_info(*trip), *time, problem) {
93                    pts.push(problem.point(&app.primary.map));
94                }
95            }
96        }
97        let num_pts = pts.len();
98
99        let mut draw = ToggleZoomed::builder();
100        let legend = if let Some(ref o) = opts.heatmap {
101            Some(make_heatmap(
102                ctx,
103                &mut draw.unzoomed,
104                app.primary.map.get_bounds(),
105                pts,
106                o,
107            ))
108        } else {
109            let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(10.0)).to_polygon();
110            // TODO Different colors per problem type
111            for pt in pts {
112                draw.unzoomed
113                    .push(Color::PURPLE.alpha(0.8), circle.translate(pt.x(), pt.y()));
114            }
115            None
116        };
117        let controls = make_controls(ctx, app, &opts, legend, num_pts);
118        ProblemMap {
119            time: app.primary.sim.time(),
120            opts,
121            draw: draw.build(ctx),
122            panel: controls,
123        }
124    }
125
126    fn options(&self, app: &App) -> Options {
127        let heatmap = if self.panel.is_checked("Show heatmap") {
128            Some(HeatmapOptions::from_controls(&self.panel))
129        } else {
130            None
131        };
132        let mut modes = BTreeSet::new();
133        for m in TripMode::all() {
134            if self.panel.is_checked(m.ongoing_verb()) {
135                modes.insert(m);
136            }
137        }
138        let end_of_day = app.primary.sim.get_end_of_day();
139        Options {
140            heatmap,
141            modes,
142            time1: end_of_day.percent_of(self.panel.slider("time1").get_percent()),
143            time2: end_of_day.percent_of(self.panel.slider("time2").get_percent()),
144            types: ProblemTypes::from_controls(&self.panel),
145        }
146    }
147}
148
149#[derive(Clone, PartialEq)]
150pub struct Options {
151    // If None, just a dot map
152    heatmap: Option<HeatmapOptions>,
153    modes: BTreeSet<TripMode>,
154    time1: Time,
155    time2: Time,
156    pub types: ProblemTypes,
157}
158
159impl Options {
160    pub fn new(app: &App) -> Self {
161        Self {
162            heatmap: Some(HeatmapOptions::new()),
163            modes: TripMode::all().into_iter().collect(),
164            time1: Time::START_OF_DAY,
165            time2: app.primary.sim.get_end_of_day(),
166            types: ProblemTypes::new(),
167        }
168    }
169
170    fn show(&self, trip: TripInfo, time: Time, problem: &Problem) -> bool {
171        if !self.modes.contains(&trip.mode) || time < self.time1 || time > self.time2 {
172            return false;
173        }
174        self.types.show(problem)
175    }
176}
177
178fn make_controls(
179    ctx: &mut EventCtx,
180    app: &App,
181    opts: &Options,
182    legend: Option<Widget>,
183    num_problems: usize,
184) -> Panel {
185    let mut col = vec![
186        header(ctx, "Problems encountered"),
187        Text::from_all(vec![
188            Line("Matching problems: ").secondary(),
189            Line(prettyprint_usize(num_problems)),
190        ])
191        .into_widget(ctx),
192    ];
193    if app.has_prebaked().is_some() {
194        col.push(Toggle::switch(ctx, "Compare before proposal", None, false));
195    }
196
197    // TODO You can't drag the sliders, since we don't remember that we're dragging a particular
198    // slider when we recreate it here. Use panel.replace?
199    let end_of_day = app.primary.sim.get_end_of_day();
200    col.push(Widget::row(vec![
201        "Happening between:".text_widget(ctx).margin_right(20),
202        Slider::area(
203            ctx,
204            0.15 * ctx.canvas.window_width,
205            opts.time1.to_percent(end_of_day),
206            "time1",
207        )
208        .align_right(),
209    ]));
210    col.push(Widget::row(vec![
211        "and:".text_widget(ctx).margin_right(20),
212        Slider::area(
213            ctx,
214            0.15 * ctx.canvas.window_width,
215            opts.time2.to_percent(end_of_day),
216            "time2",
217        )
218        .align_right(),
219    ]));
220    col.push(checkbox_per_mode(ctx, app, &opts.modes));
221    col.push(opts.types.to_controls(ctx));
222
223    col.push(Toggle::choice(
224        ctx,
225        "Show heatmap",
226        "Heatmap",
227        "Points",
228        None,
229        opts.heatmap.is_some(),
230    ));
231    if let Some(ref o) = opts.heatmap {
232        col.push(Line("Heatmap Options").small_heading().into_widget(ctx));
233        col.extend(o.to_controls(ctx, legend.unwrap()));
234    }
235    col.push(ctx.style().btn_plain.text("Export to CSV").build_def(ctx));
236
237    Panel::new_builder(Widget::col(col))
238        .aligned_pair(PANEL_PLACEMENT)
239        // TODO Tune and use more widely
240        .dims_height(PanelDims::MaxPercent(0.6))
241        // TODO Not sure why needed -- if you leave the mouse on the right spot,
242        // Outcome::Changed(time1) happens?
243        .ignore_initial_events()
244        .build(ctx)
245}
246
247fn export_raw_problems(app: &App) -> Result<String> {
248    let map = &app.primary.map;
249    let path = format!(
250        "problems_{}_{}.csv",
251        map.get_name().as_filename(),
252        app.primary.sim.time().as_filename()
253    );
254    let mut out = String::new();
255    writeln!(out, "trip_id,time,problem_type,longitude,latitude,osm_url")?;
256    for (trip, problems) in &app.primary.sim.get_analytics().problems_per_trip {
257        for (time, problem) in problems {
258            let pt = problem.point(map).to_gps(map.get_gps_bounds());
259            let osm_url = match problem {
260                Problem::IntersectionDelay(i, _) | Problem::ComplexIntersectionCrossing(i) => {
261                    map.get_i(*i).orig_id.to_string()
262                }
263                Problem::OvertakeDesired(on) | Problem::PedestrianOvercrowding(on) => match on {
264                    Traversable::Lane(l) => map.get_r(l.road).orig_id.to_string(),
265                    Traversable::Turn(t) => map.get_i(t.parent).orig_id.to_string(),
266                },
267                Problem::ArterialIntersectionCrossing(t) => map.get_i(t.parent).orig_id.to_string(),
268            };
269            writeln!(
270                out,
271                "{},{},{:?},{},{},{}",
272                trip.0,
273                time.inner_seconds(),
274                ProblemType::from(problem),
275                pt.x(),
276                pt.y(),
277                osm_url,
278            )?;
279        }
280    }
281
282    abstio::write_file(path, out)
283}