game/layer/
parking.rs

1use std::collections::BTreeSet;
2
3use abstutil::{prettyprint_usize, Counter};
4use geom::{Circle, Distance, Duration, Pt2D, Time};
5use map_gui::tools::ColorNetwork;
6use map_model::{BuildingID, OffstreetParking, ParkingLotID, PathRequest, RoadID};
7use sim::{ParkingSpot, VehicleType};
8use widgetry::mapspace::ToggleZoomed;
9use widgetry::tools::ColorLegend;
10use widgetry::{EventCtx, GfxCtx, Line, Outcome, Panel, Text, Toggle, Widget};
11
12use crate::app::App;
13use crate::layer::{header, Layer, LayerOutcome, PANEL_PLACEMENT};
14use crate::render::unzoomed_agent_radius;
15
16pub struct Occupancy {
17    time: Time,
18    onstreet: bool,
19    garages: bool,
20    lots: bool,
21    private_bldgs: bool,
22    looking_for_parking: bool,
23    draw: ToggleZoomed,
24    panel: Panel,
25}
26
27impl Layer for Occupancy {
28    fn name(&self) -> Option<&'static str> {
29        Some("parking occupancy")
30    }
31    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
32        if app.primary.sim.time() != self.time {
33            *self = Occupancy::new(
34                ctx,
35                app,
36                self.onstreet,
37                self.garages,
38                self.lots,
39                self.private_bldgs,
40                self.looking_for_parking,
41            );
42        }
43
44        match self.panel.event(ctx) {
45            Outcome::Clicked(x) => match x.as_ref() {
46                "close" => {
47                    return Some(LayerOutcome::Close);
48                }
49                _ => unreachable!(),
50            },
51            Outcome::Changed(_) => {
52                *self = Occupancy::new(
53                    ctx,
54                    app,
55                    self.panel.is_checked("On-street spots"),
56                    self.panel.is_checked("Public garages"),
57                    self.panel.is_checked("Parking lots"),
58                    self.panel.is_checked("Private buildings"),
59                    self.panel.is_checked("Cars looking for parking"),
60                );
61            }
62            _ => {}
63        }
64        None
65    }
66    fn draw(&self, g: &mut GfxCtx, _: &App) {
67        self.panel.draw(g);
68        self.draw.draw(g);
69    }
70    fn draw_minimap(&self, g: &mut GfxCtx) {
71        g.redraw(&self.draw.unzoomed);
72    }
73}
74
75impl Occupancy {
76    pub fn new(
77        ctx: &mut EventCtx,
78        app: &App,
79        onstreet: bool,
80        garages: bool,
81        lots: bool,
82        private_bldgs: bool,
83        looking_for_parking: bool,
84    ) -> Occupancy {
85        let mut total_ppl = 0;
86        let mut has_car = 0;
87        for p in app.primary.sim.get_all_people() {
88            total_ppl += 1;
89            if p.vehicles
90                .iter()
91                .any(|v| v.vehicle_type == VehicleType::Car)
92            {
93                has_car += 1;
94            }
95        }
96
97        if app.primary.sim.infinite_parking() {
98            let panel = Panel::new_builder(Widget::col(vec![
99                header(ctx, "Parking occupancy"),
100                Text::from_multiline(vec![
101                    Line(format!(
102                        "{:.0}% of the population owns a car",
103                        if total_ppl == 0 {
104                            0.0
105                        } else {
106                            100.0 * (has_car as f64) / (total_ppl as f64)
107                        }
108                    )),
109                    Line(""),
110                    Line("Parking simulation disabled."),
111                    Line("Every building has unlimited capacity.").secondary(),
112                ])
113                .into_widget(ctx),
114            ]))
115            .aligned_pair(PANEL_PLACEMENT)
116            .build(ctx);
117            return Occupancy {
118                time: app.primary.sim.time(),
119                onstreet: false,
120                garages: false,
121                lots: false,
122                private_bldgs: false,
123                looking_for_parking: false,
124                draw: ToggleZoomed::empty(ctx),
125                panel,
126            };
127        }
128
129        let mut filled_spots = Counter::new();
130        let mut avail_spots = Counter::new();
131        let mut keys = BTreeSet::new();
132
133        let mut public_filled = 0;
134        let mut public_avail = 0;
135        let mut private_filled = 0;
136        let mut private_avail = 0;
137
138        let (all_filled_spots, all_avail_spots) = app.primary.sim.get_all_parking_spots();
139
140        for (input, public_counter, private_counter, spots) in vec![
141            (
142                all_filled_spots,
143                &mut public_filled,
144                &mut private_filled,
145                &mut filled_spots,
146            ),
147            (
148                all_avail_spots,
149                &mut public_avail,
150                &mut private_avail,
151                &mut avail_spots,
152            ),
153        ] {
154            for spot in input {
155                match spot {
156                    ParkingSpot::Onstreet(_, _) => {
157                        if !onstreet {
158                            continue;
159                        }
160                        *public_counter += 1;
161                    }
162                    ParkingSpot::Offstreet(b, _) => {
163                        if let OffstreetParking::PublicGarage(_, _) =
164                            app.primary.map.get_b(b).parking
165                        {
166                            if !garages {
167                                continue;
168                            }
169                            *public_counter += 1;
170                        } else {
171                            if !private_bldgs {
172                                continue;
173                            }
174                            *private_counter += 1;
175                        }
176                    }
177                    ParkingSpot::Lot(_, _) => {
178                        if !lots {
179                            continue;
180                        }
181                        *public_counter += 1;
182                    }
183                }
184
185                let loc = Loc::new(spot);
186                keys.insert(loc);
187                spots.inc(loc);
188            }
189        }
190
191        let panel = Panel::new_builder(Widget::col(vec![
192            header(ctx, "Parking occupancy"),
193            Text::from_multiline(vec![
194                Line(format!(
195                    "{:.0}% of the population owns a car",
196                    if total_ppl == 0 {
197                        0.0
198                    } else {
199                        100.0 * (has_car as f64) / (total_ppl as f64)
200                    }
201                )),
202                Line(format!(
203                    "{} / {} public spots filled",
204                    prettyprint_usize(public_filled),
205                    prettyprint_usize(public_filled + public_avail)
206                )),
207                Line(format!(
208                    "{} / {} private spots filled",
209                    prettyprint_usize(private_filled),
210                    prettyprint_usize(private_filled + private_avail)
211                )),
212            ])
213            .into_widget(ctx),
214            Widget::row(vec![
215                Toggle::switch(ctx, "On-street spots", None, onstreet),
216                Toggle::switch(ctx, "Parking lots", None, lots),
217            ])
218            .evenly_spaced(),
219            Widget::row(vec![
220                Toggle::switch(ctx, "Public garages", None, garages),
221                Toggle::switch(ctx, "Private buildings", None, private_bldgs),
222            ])
223            .evenly_spaced(),
224            Toggle::colored_checkbox(
225                ctx,
226                "Cars looking for parking",
227                app.cs.parking_trip,
228                looking_for_parking,
229            ),
230            ColorLegend::gradient(ctx, &app.cs.good_to_bad_red, vec!["0%", "100%"]),
231        ]))
232        .aligned_pair(PANEL_PLACEMENT)
233        .build(ctx);
234
235        let mut colorer = ColorNetwork::new(app);
236        for loc in keys {
237            let open = avail_spots.get(loc);
238            let closed = filled_spots.get(loc);
239            let percent = (closed as f64) / ((open + closed) as f64);
240            let color = app.cs.good_to_bad_red.eval(percent);
241            match loc {
242                Loc::Road(r) => colorer.add_r(r, color),
243                Loc::Bldg(b) => colorer.add_b(b, color),
244                Loc::Lot(pl) => colorer.add_pl(pl, color),
245            }
246        }
247
248        if looking_for_parking {
249            // A bit of copied code from draw_unzoomed_agents
250            let car_circle = Circle::new(
251                Pt2D::new(0.0, 0.0),
252                unzoomed_agent_radius(Some(VehicleType::Car)),
253            )
254            .to_polygon();
255            for a in app.primary.sim.get_unzoomed_agents(&app.primary.map) {
256                if a.parking {
257                    colorer.draw.unzoomed.push(
258                        app.cs.parking_trip.alpha(0.8),
259                        car_circle.translate(a.pos.x(), a.pos.y()),
260                    );
261                }
262            }
263        }
264
265        Occupancy {
266            time: app.primary.sim.time(),
267            onstreet,
268            garages,
269            lots,
270            private_bldgs,
271            looking_for_parking,
272            draw: colorer.build(ctx),
273            panel,
274        }
275    }
276}
277
278#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
279enum Loc {
280    Road(RoadID),
281    Bldg(BuildingID),
282    Lot(ParkingLotID),
283}
284
285impl Loc {
286    fn new(spot: ParkingSpot) -> Loc {
287        match spot {
288            ParkingSpot::Onstreet(l, _) => Loc::Road(l.road),
289            ParkingSpot::Offstreet(b, _) => Loc::Bldg(b),
290            ParkingSpot::Lot(pl, _) => Loc::Lot(pl),
291        }
292    }
293}
294
295pub struct Efficiency {
296    time: Time,
297    draw: ToggleZoomed,
298    panel: Panel,
299}
300
301impl Layer for Efficiency {
302    fn name(&self) -> Option<&'static str> {
303        Some("parking efficiency")
304    }
305    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
306        if app.primary.sim.time() != self.time {
307            *self = Efficiency::new(ctx, app);
308        }
309
310        if let Outcome::Clicked(x) = self.panel.event(ctx) {
311            match x.as_ref() {
312                "close" => {
313                    return Some(LayerOutcome::Close);
314                }
315                _ => unreachable!(),
316            }
317        }
318        None
319    }
320    fn draw(&self, g: &mut GfxCtx, _: &App) {
321        self.panel.draw(g);
322        self.draw.draw(g);
323    }
324    fn draw_minimap(&self, g: &mut GfxCtx) {
325        g.redraw(&self.draw.unzoomed);
326    }
327}
328
329impl Efficiency {
330    pub fn new(ctx: &mut EventCtx, app: &App) -> Efficiency {
331        let panel = Panel::new_builder(Widget::col(vec![
332            header(ctx, "Parking efficiency"),
333            Text::from(Line("How far away are people parked? (minutes)").secondary())
334                .wrap_to_pct(ctx, 15)
335                .into_widget(ctx),
336            ColorLegend::gradient(
337                ctx,
338                &app.cs.good_to_bad_red,
339                // TODO Show a nonproportional scale? Most should be < 1 min, a few < 5 mins,
340                // rarely more than that.
341                vec!["0", "3", "6", "10+"],
342            ),
343        ]))
344        .aligned_pair(PANEL_PLACEMENT)
345        .build(ctx);
346
347        let map = &app.primary.map;
348        // TODO This is going to spam constantly while the sim is running! Probably cache per car.
349        let draw = ctx.loading_screen("measure parking efficiency", |ctx, timer| {
350            let mut draw = ToggleZoomed::builder();
351
352            timer.start("gather requests");
353            let requests: Vec<PathRequest> = app
354                .primary
355                .sim
356                .all_parked_car_positions(map)
357                .into_iter()
358                .map(|(start, end)| PathRequest::walking(start, end))
359                .collect();
360            timer.stop("gather requests");
361            for (car_pt, time) in timer
362                .parallelize("calculate paths", requests, |req| {
363                    let car_pt = req.start.pt(map);
364                    // TODO Walking paths should really return some indication of "zero length
365                    // path" for this
366                    if req.start == req.end {
367                        Some((car_pt, Duration::ZERO))
368                    } else {
369                        map.pathfind(req).ok().map(|path| {
370                            (
371                                car_pt,
372                                path.estimate_duration(map, Some(map_model::MAX_WALKING_SPEED)),
373                            )
374                        })
375                    }
376                })
377                .into_iter()
378                .flatten()
379            {
380                let color = app
381                    .cs
382                    .good_to_bad_red
383                    .eval((time / Duration::minutes(10)).min(1.0));
384                // TODO Actual car shapes? At least cache the circle?
385                draw.unzoomed.push(
386                    color,
387                    Circle::new(car_pt, Distance::meters(5.0)).to_polygon(),
388                );
389                draw.zoomed.push(
390                    color.alpha(0.5),
391                    Circle::new(car_pt, Distance::meters(2.0)).to_polygon(),
392                );
393            }
394            draw.build(ctx)
395        });
396
397        Efficiency {
398            time: app.primary.sim.time(),
399            draw,
400            panel,
401        }
402    }
403}