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 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 vec!["0", "3", "6", "10+"],
342 ),
343 ]))
344 .aligned_pair(PANEL_PLACEMENT)
345 .build(ctx);
346
347 let map = &app.primary.map;
348 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 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 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}