game/info/
building.rs

1use std::collections::BTreeMap;
2
3use geom::{Angle, Circle, Distance, Speed, Time};
4use map_model::{BuildingID, LaneID, OffstreetParking, Traversable, SIDEWALK_THICKNESS};
5use sim::{DrawPedestrianInput, PedestrianID, PersonID, TripResult, VehicleType};
6use synthpop::TripMode;
7use widgetry::{Color, EventCtx, Line, Text, TextExt, Widget};
8
9use crate::app::App;
10use crate::info::{header_btns, make_table, make_tabs, Details, Tab};
11use crate::render::DrawPedestrian;
12
13pub fn info(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BuildingID) -> Widget {
14    Widget::custom_col(vec![
15        header(ctx, app, details, id, Tab::BldgInfo(id)),
16        info_body(ctx, app, details, id).tab_body(ctx),
17    ])
18}
19
20fn info_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BuildingID) -> Widget {
21    let mut rows = vec![];
22
23    let b = app.primary.map.get_b(id);
24
25    let mut kv = vec![("Address", b.address.clone())];
26    if let Some(ref names) = b.name {
27        kv.push(("Name", names.get(app.opts.language.as_ref()).to_string()));
28    }
29    if app.opts.dev {
30        kv.push(("OSM ID", format!("{}", b.orig_id.inner_id())));
31    }
32
33    let num_spots = b.num_parking_spots();
34    if app.primary.sim.infinite_parking() {
35        kv.push((
36            "Parking",
37            format!(
38                "Unlimited, currently {} cars inside",
39                app.primary.sim.bldg_to_parked_cars(b.id).len()
40            ),
41        ));
42    } else if num_spots > 0 {
43        let free = app.primary.sim.get_free_offstreet_spots(b.id).len();
44        if let OffstreetParking::PublicGarage(ref n, _) = b.parking {
45            kv.push((
46                "Parking",
47                format!("{} / {} public spots available via {}", free, num_spots, n),
48            ));
49        } else {
50            kv.push((
51                "Parking",
52                format!("{} / {} private spots available", free, num_spots),
53            ));
54        }
55    } else {
56        kv.push(("Parking", "None".to_string()));
57    }
58
59    rows.extend(make_table(ctx, kv));
60
61    let mut txt = Text::new();
62
63    if !b.amenities.is_empty() {
64        txt.add_line("");
65        if b.amenities.len() == 1 {
66            txt.add_line("1 amenity:");
67        } else {
68            txt.add_line(format!("{} amenities:", b.amenities.len()));
69        }
70        for a in &b.amenities {
71            txt.add_line(format!(
72                "  {} ({})",
73                a.names.get(app.opts.language.as_ref()),
74                a.amenity_type
75            ));
76        }
77    }
78
79    if !app.primary.sim.infinite_parking() {
80        txt.add_line("");
81        if let Some(pl) = app
82            .primary
83            .sim
84            .walking_path_to_nearest_parking_spot(&app.primary.map, id)
85            .and_then(|path| path.trace(&app.primary.map))
86        {
87            let color = app.cs.parking_trip;
88            // TODO But this color doesn't show up well against the info panel...
89            txt.add_line(Line("Nearest parking").fg(color));
90            txt.append(Line(format!(
91                " is ~{} away by foot",
92                pl.length() / Speed::miles_per_hour(3.0)
93            )));
94
95            details
96                .draw_extra
97                .unzoomed
98                .push(color, pl.make_polygons(Distance::meters(10.0)));
99            details.draw_extra.zoomed.extend(
100                color,
101                pl.dashed_lines(
102                    Distance::meters(0.75),
103                    Distance::meters(1.0),
104                    Distance::meters(0.4),
105                ),
106            );
107        } else {
108            txt.add_line("No nearby parking available")
109        }
110    }
111
112    if !txt.is_empty() {
113        rows.push(txt.into_widget(ctx))
114    }
115
116    if app.opts.dev {
117        rows.push(
118            ctx.style()
119                .btn_outline
120                .text("Open OSM")
121                .build_widget(ctx, format!("open {}", b.orig_id)),
122        );
123
124        if !b.osm_tags.is_empty() {
125            rows.push("Raw OpenStreetMap data".text_widget(ctx));
126            rows.extend(make_table(
127                ctx,
128                b.osm_tags
129                    .inner()
130                    .iter()
131                    .map(|(k, v)| (k, v.to_string()))
132                    .collect(),
133            ));
134        }
135    }
136
137    Widget::col(rows)
138}
139
140pub fn people(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BuildingID) -> Widget {
141    Widget::custom_col(vec![
142        header(ctx, app, details, id, Tab::BldgPeople(id)),
143        people_body(ctx, app, details, id).tab_body(ctx),
144    ])
145}
146
147fn people_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: BuildingID) -> Widget {
148    let mut rows = vec![];
149
150    // Two caveats about these counts:
151    // 1) A person might use multiple modes through the day, but this just picks a single category.
152    // 2) Only people currently in the building currently are counted, whether or not that's their
153    //    home.
154    let mut drivers = 0;
155    let mut cyclists = 0;
156    let mut others = 0;
157
158    let mut ppl: Vec<(Time, Widget)> = Vec::new();
159    for p in app.primary.sim.bldg_to_people(id) {
160        let person = app.primary.sim.get_person(p);
161
162        let mut has_car = false;
163        let mut has_bike = false;
164        for vehicle in &person.vehicles {
165            if vehicle.vehicle_type == VehicleType::Car {
166                has_car = true;
167            } else if vehicle.vehicle_type == VehicleType::Bike {
168                has_bike = true;
169            }
170        }
171        if has_car {
172            drivers += 1;
173        } else if has_bike {
174            cyclists += 1;
175        } else {
176            others += 1;
177        }
178
179        let mut next_trip: Option<(Time, TripMode)> = None;
180        for t in &person.trips {
181            match app.primary.sim.trip_to_agent(*t) {
182                TripResult::TripNotStarted => {
183                    let trip = app.primary.sim.trip_info(*t);
184                    next_trip = Some((trip.departure, trip.mode));
185                    break;
186                }
187                TripResult::Ok(_) | TripResult::ModeChange => {
188                    // TODO What to do here? This is meant for building callers right now
189                    break;
190                }
191                TripResult::TripDone | TripResult::TripCancelled => {}
192                TripResult::TripDoesntExist => unreachable!(),
193            }
194        }
195
196        details
197            .hyperlinks
198            .insert(p.to_string(), Tab::PersonTrips(p, BTreeMap::new()));
199        let widget = Widget::row(vec![
200            ctx.style().btn_outline.text(p.to_string()).build_def(ctx),
201            if let Some((t, mode)) = next_trip {
202                format!(
203                    "Leaving in {} to {}",
204                    t - app.primary.sim.time(),
205                    mode.verb()
206                )
207                .text_widget(ctx)
208            } else {
209                "Staying inside".text_widget(ctx)
210            },
211        ]);
212        ppl.push((
213            next_trip
214                .map(|(t, _)| t)
215                .unwrap_or_else(|| app.primary.sim.get_end_of_day()),
216            widget,
217        ));
218    }
219
220    // Sort by time to next trip
221    ppl.sort_by_key(|(t, _)| *t);
222    if ppl.is_empty() {
223        rows.push("Nobody's inside right now".text_widget(ctx));
224    } else {
225        rows.push(
226            format!(
227                "{} drivers, {} cyclists, {} others",
228                drivers, cyclists, others
229            )
230            .text_widget(ctx),
231        );
232
233        for (_, w) in ppl {
234            rows.push(w);
235        }
236    }
237
238    Widget::col(rows)
239}
240
241fn header(ctx: &EventCtx, app: &App, details: &mut Details, id: BuildingID, tab: Tab) -> Widget {
242    let rows = vec![
243        Widget::row(vec![
244            Line(id.to_string()).small_heading().into_widget(ctx),
245            header_btns(ctx),
246        ]),
247        make_tabs(
248            ctx,
249            &mut details.hyperlinks,
250            tab,
251            vec![("Info", Tab::BldgInfo(id)), ("People", Tab::BldgPeople(id))],
252        ),
253    ];
254
255    draw_occupants(details, app, id, None);
256    // TODO Draw cars parked inside?
257
258    Widget::custom_col(rows)
259}
260
261pub fn draw_occupants(details: &mut Details, app: &App, id: BuildingID, focus: Option<PersonID>) {
262    // TODO Lots of fun ideas here. Have a deterministic simulation based on building ID and time
263    // to have people "realistically" move around. Draw little floor plans.
264
265    let mut ppl = app.primary.sim.bldg_to_people(id);
266    let num_rows_cols = (ppl.len() as f64).sqrt().ceil() as usize;
267
268    let ped_len = sim::pedestrian_body_radius().inner_meters() * 2.0;
269    let separation = ped_len * 1.5;
270
271    let total_width_height = (num_rows_cols as f64) * (ped_len + separation);
272    let top_left = app
273        .primary
274        .map
275        .get_b(id)
276        .label_center
277        .offset(-total_width_height / 2.0, -total_width_height / 2.0);
278
279    // TODO Current thing is inefficient and can easily wind up outside the building.
280
281    'OUTER: for x in 0..num_rows_cols {
282        for y in 0..num_rows_cols {
283            let person = if let Some(p) = ppl.pop() {
284                p
285            } else {
286                break 'OUTER;
287            };
288            let pos = top_left.offset(
289                (x as f64) * (ped_len + separation),
290                (y as f64) * (ped_len + separation),
291            );
292
293            if Some(person) == focus {
294                details.draw_extra.zoomed.push(
295                    Color::YELLOW.alpha(0.8),
296                    Circle::new(pos, SIDEWALK_THICKNESS).to_polygon(),
297                );
298            }
299
300            DrawPedestrian::geometry(
301                &mut details.draw_extra.zoomed,
302                &app.primary.sim,
303                &app.cs,
304                &DrawPedestrianInput {
305                    // Lies
306                    id: PedestrianID(person.0),
307                    person,
308                    pos,
309                    facing: Angle::degrees(90.0),
310                    waiting_for_turn: None,
311                    intent: None,
312                    preparing_bike: false,
313                    // Both hands and feet!
314                    waiting_for_bus: true,
315                    on: Traversable::Lane(LaneID::dummy()),
316                },
317                0,
318            );
319        }
320    }
321}