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 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 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 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 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 Widget::custom_col(rows)
259}
260
261pub fn draw_occupants(details: &mut Details, app: &App, id: BuildingID, focus: Option<PersonID>) {
262 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 '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 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 waiting_for_bus: true,
315 on: Traversable::Lane(LaneID::dummy()),
316 },
317 0,
318 );
319 }
320 }
321}