1use std::collections::BTreeMap;
2
3use rand::seq::SliceRandom;
4use rand::{Rng, SeedableRng};
5use rand_xorshift::XorShiftRng;
6
7use geom::{Angle, Duration, Time};
8use map_model::Map;
9use sim::{
10 AgentID, CarID, ParkingSpot, PedestrianID, Person, PersonID, PersonState, TripID, TripResult,
11 VehicleType,
12};
13use synthpop::{TripEndpoint, TripMode};
14use widgetry::{
15 include_labeled_bytes, Color, ControlState, CornerRounding, EdgeInsets, EventCtx, GeomBatch,
16 Image, Key, Line, RewriteColor, Text, TextExt, TextSpan, Widget,
17};
18
19use crate::app::App;
20use crate::info::{building, header_btns, make_table, make_tabs, trip, Details, OpenTrip, Tab};
21
22pub fn trips(
23 ctx: &mut EventCtx,
24 app: &App,
25 details: &mut Details,
26 id: PersonID,
27 open_trips: &mut BTreeMap<TripID, OpenTrip>,
28 is_paused: bool,
29) -> Widget {
30 Widget::custom_col(vec![
31 header(
32 ctx,
33 app,
34 details,
35 id,
36 Tab::PersonTrips(id, open_trips.clone()),
37 is_paused,
38 ),
39 trips_body(ctx, app, details, id, open_trips).tab_body(ctx),
40 ])
41}
42
43fn trips_body(
44 ctx: &mut EventCtx,
45 app: &App,
46 details: &mut Details,
47 id: PersonID,
48 open_trips: &mut BTreeMap<TripID, OpenTrip>,
49) -> Widget {
50 let mut rows = vec![];
51
52 let map = &app.primary.map;
53 let sim = &app.primary.sim;
54 let person = sim.get_person(id);
55
56 if !open_trips.is_empty() {
60 details.draw_extra.unzoomed.push(
61 app.cs.fade_map_dark,
62 app.primary.map.get_boundary_polygon().clone(),
63 );
64 }
65
66 let mut wheres_waldo = true;
68 for (idx, t) in person.trips.iter().enumerate() {
69 let (trip_status, color, maybe_info) = match sim.trip_to_agent(*t) {
70 TripResult::TripNotStarted => {
71 if wheres_waldo {
72 wheres_waldo = false;
73 rows.push(current_status(ctx, person, map));
74 }
75 if sim.time() > sim.trip_info(*t).departure {
76 (
77 "delayed start",
78 Color::YELLOW,
79 open_trips
80 .get_mut(t)
81 .map(|open_trip| trip::future(ctx, app, *t, open_trip, details)),
82 )
83 } else {
84 (
85 "future",
86 Color::hex("#4CA7E9"),
87 open_trips
88 .get_mut(t)
89 .map(|open_trip| trip::future(ctx, app, *t, open_trip, details)),
90 )
91 }
92 }
93 TripResult::Ok(a) => {
94 assert!(wheres_waldo);
95 wheres_waldo = false;
96 (
97 "ongoing",
98 Color::hex("#7FFA4D"),
99 open_trips
100 .get_mut(t)
101 .map(|open_trip| trip::ongoing(ctx, app, *t, a, open_trip, details)),
102 )
103 }
104 TripResult::ModeChange => {
105 assert!(wheres_waldo);
107 wheres_waldo = false;
108 (
109 "ongoing",
110 Color::hex("#7FFA4D"),
111 open_trips.get(t).map(|_| Widget::nothing()),
112 )
113 }
114 TripResult::TripDone => {
115 assert!(wheres_waldo);
116 (
117 "finished",
118 Color::hex("#A3A3A3"),
119 if open_trips.contains_key(t) {
120 Some(trip::finished(ctx, app, id, open_trips, *t, details))
121 } else {
122 None
123 },
124 )
125 }
126 TripResult::TripCancelled => {
127 (
129 "cancelled",
130 app.cs.signal_banned_turn,
131 open_trips
132 .get_mut(t)
133 .map(|open_trip| trip::cancelled(ctx, app, *t, open_trip, details)),
134 )
135 }
136 TripResult::TripDoesntExist => unreachable!(),
137 };
138 let trip = sim.trip_info(*t);
139
140 let (row_btn, _hitbox) = Widget::custom_row(vec![
141 format!("Trip {} ", idx + 1)
142 .batch_text(ctx)
143 .centered_vert()
144 .margin_right(21),
145 Widget::row(vec![
146 GeomBatch::load_svg(
147 ctx.prerender,
148 match trip.mode {
149 TripMode::Walk => "system/assets/meters/pedestrian.svg",
150 TripMode::Bike => "system/assets/meters/bike.svg",
151 TripMode::Drive => "system/assets/meters/car.svg",
152 TripMode::Transit => "system/assets/meters/bus.svg",
153 },
154 )
155 .scale(0.75)
157 .autocrop()
159 .color(RewriteColor::ChangeAll(color))
160 .batch(),
161 Line(trip_status)
164 .small()
165 .fg(color)
166 .batch(ctx)
167 .container()
168 .padding_bottom(2),
169 ])
170 .centered()
171 .corner_rounding(CornerRounding::FullyRounded)
172 .outline((1.0, color))
173 .bg(color.alpha(0.2))
174 .padding(EdgeInsets {
175 top: 5.0,
176 bottom: 5.0,
177 left: 10.0,
178 right: 10.0,
179 })
180 .margin_right(21),
181 if trip.modified {
182 Line("modified").batch(ctx).centered_vert().margin_right(15)
183 } else {
184 Widget::nothing()
185 },
186 if trip_status == "finished" {
187 if let Some(before) = app
188 .has_prebaked()
189 .and_then(|_| app.prebaked().finished_trip_time(*t))
190 {
191 let (after, _, _) = app.primary.sim.finished_trip_details(*t).unwrap();
192 Text::from(cmp_duration_shorter(after, before))
193 .batch(ctx)
194 .centered_vert()
195 } else {
196 Widget::nothing()
197 }
198 } else {
199 Widget::nothing()
200 },
201 {
202 let mut icon = Image::from_bytes(include_labeled_bytes!(
203 "../../../../widgetry/icons/arrow_drop_down.svg"
204 ))
205 .build_batch(ctx)
206 .expect("invalid svg")
207 .0
208 .scale(1.5);
209
210 if !open_trips.contains_key(t) {
211 icon = icon.rotate(Angle::degrees(180.0));
212 }
213
214 icon.batch().container().align_right().margin_right(10)
215 },
216 ])
217 .centered()
218 .outline(ctx.style().section_outline)
219 .padding(16)
220 .bg(app.cs.inner_panel_bg)
221 .into_geom(ctx, Some(0.3));
222 rows.push(
223 ctx.style()
224 .btn_solid
225 .btn()
226 .custom_batch(row_btn.clone(), ControlState::Default)
227 .custom_batch(
228 row_btn.color(RewriteColor::Change(
229 app.cs.inner_panel_bg,
230 ctx.style().btn_outline.bg_hover,
231 )),
232 ControlState::Hovered,
233 )
234 .build_widget(
235 ctx,
236 format!(
237 "{} {}",
238 if open_trips.contains_key(t) {
239 "hide"
240 } else {
241 "show"
242 },
243 t
244 ),
245 )
246 .margin_above(if idx == 0 { 0 } else { 16 }),
247 );
248
249 if let Some(info) = maybe_info {
250 rows.push(
251 info.outline(ctx.style().section_outline)
252 .bg(app.cs.inner_panel_bg)
253 .padding(16),
254 );
255
256 let mut new_trips = open_trips.clone();
257 new_trips.remove(t);
258 details
259 .hyperlinks
260 .insert(format!("hide {}", t), Tab::PersonTrips(id, new_trips));
261 } else {
262 let mut new_trips = open_trips.clone();
263 new_trips.insert(*t, OpenTrip::new());
264 details
265 .hyperlinks
266 .insert(format!("show {}", t), Tab::PersonTrips(id, new_trips));
267 }
268 }
269 if wheres_waldo {
270 rows.push(current_status(ctx, person, map));
271 }
272
273 Widget::col(rows)
274}
275
276pub fn bio(
277 ctx: &mut EventCtx,
278 app: &App,
279 details: &mut Details,
280 id: PersonID,
281 is_paused: bool,
282) -> Widget {
283 Widget::custom_col(vec![
284 header(ctx, app, details, id, Tab::PersonBio(id), is_paused),
285 bio_body(ctx, app, details, id).tab_body(ctx),
286 ])
287}
288
289fn bio_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: PersonID) -> Widget {
290 let mut rows = vec![];
291 let person = app.primary.sim.get_person(id);
292 let mut rng = XorShiftRng::seed_from_u64(id.0 as u64);
293
294 let mut svg_data = Vec::new();
295 svg_face::generate_face(&mut svg_data, &mut rng).unwrap();
296 let batch = GeomBatch::load_svg_bytes_uncached(&svg_data).autocrop();
297 let dims = batch.get_dims();
298 let batch = batch.scale((200.0 / dims.width).min(200.0 / dims.height));
299 rows.push(batch.into_widget(ctx).centered_horiz());
300
301 let nickname = petname::Petnames::default().generate(&mut rng, 2, " ");
302 let age = rng.gen_range(5..100);
303
304 let mut table = vec![("Nickname", nickname), ("Age", age.to_string())];
305 if app.opts.dev {
306 table.push(("Debug ID", format!("{:?}", person.orig_id)));
307 }
308 rows.extend(make_table(ctx, table));
309 if let Some(p) = app.primary.sim.get_pandemic_model() {
319 let status = if p.is_sane(id) {
321 "Susceptible".to_string()
322 } else if p.is_exposed(id) {
323 format!("Exposed at {}", p.get_time(id).unwrap().ampm_tostring())
324 } else if p.is_infectious(id) {
325 format!("Infected at {}", p.get_time(id).unwrap().ampm_tostring())
326 } else if p.is_recovered(id) {
327 format!("Recovered at {}", p.get_time(id).unwrap().ampm_tostring())
328 } else if p.is_dead(id) {
329 format!("Dead at {}", p.get_time(id).unwrap().ampm_tostring())
330 } else {
331 "Other (hospitalized or quarantined)".to_string()
333 };
334 rows.push(
335 Text::from_all(vec![
336 Line("Pandemic model state: ").secondary(),
337 Line(status),
338 ])
339 .into_widget(ctx),
340 );
341 }
342
343 let mut has_bike = false;
344 for v in &person.vehicles {
345 if v.vehicle_type == VehicleType::Bike {
346 has_bike = true;
347 } else if app.primary.sim.lookup_parked_car(v.id).is_some() {
348 rows.push(
349 ctx.style()
350 .btn_outline
351 .text(format!("Owner of {} (parked)", v.id))
352 .build_def(ctx),
353 );
354 details
355 .hyperlinks
356 .insert(format!("Owner of {} (parked)", v.id), Tab::ParkedCar(v.id));
357 } else if let PersonState::Trip(t) = person.state {
358 match app.primary.sim.trip_to_agent(t) {
359 TripResult::Ok(AgentID::Car(x)) if x == v.id => {
360 rows.push(format!("Owner of {} (currently driving)", v.id).text_widget(ctx));
361 }
362 _ => {
363 rows.push(format!("Owner of {} (off-map)", v.id).text_widget(ctx));
364 }
365 }
366 } else {
367 rows.push(format!("Owner of {} (off-map)", v.id).text_widget(ctx));
368 }
369 }
370 if has_bike {
371 rows.push("Owns a bike".text_widget(ctx));
372 }
373
374 if app.opts.dev {
376 if let Some(AgentID::Car(car)) = app.primary.sim.person_to_agent(id) {
377 rows.push(
378 Text::from(format!("State: {:?}", app.primary.sim.debug_car_ui(car)))
379 .wrap_to_pct(ctx, 20)
380 .into_widget(ctx),
381 );
382 }
383 }
384
385 Widget::col(rows)
386}
387
388pub fn schedule(
389 ctx: &mut EventCtx,
390 app: &App,
391 details: &mut Details,
392 id: PersonID,
393 is_paused: bool,
394) -> Widget {
395 Widget::custom_col(vec![
396 header(ctx, app, details, id, Tab::PersonSchedule(id), is_paused),
397 schedule_body(ctx, app, id).tab_body(ctx),
398 ])
399}
400
401fn schedule_body(ctx: &mut EventCtx, app: &App, id: PersonID) -> Widget {
402 let mut rows = vec![];
403 let person = app.primary.sim.get_person(id);
404 let mut rng = XorShiftRng::seed_from_u64(id.0 as u64);
405
406 let mut last_t = Time::START_OF_DAY;
408 for t in &person.trips {
409 let trip = app.primary.sim.trip_info(*t);
410 let at = match trip.start {
411 TripEndpoint::Building(b) => {
412 let b = app.primary.map.get_b(b);
413 if b.amenities.is_empty() {
414 b.address.clone()
415 } else {
416 let list = b
417 .amenities
418 .iter()
419 .map(|a| a.names.get(app.opts.language.as_ref()))
420 .collect::<Vec<_>>();
421 format!("{} (at {})", list.choose(&mut rng).unwrap(), b.address)
422 }
423 }
424 TripEndpoint::Border(_) => "off-map".to_string(),
425 TripEndpoint::SuddenlyAppear(_) => "suddenly appear".to_string(),
426 };
427 rows.push(
428 Text::from(format!(" Spends {} at {}", trip.departure - last_t, at)).into_widget(ctx),
429 );
430 last_t = trip.departure;
432 }
433 let last_trip = app.primary.sim.trip_info(*person.trips.last().unwrap());
435 let at = match last_trip.end {
436 TripEndpoint::Building(b) => {
437 let b = app.primary.map.get_b(b);
438 if b.amenities.is_empty() {
439 b.address.clone()
440 } else {
441 let list = b
442 .amenities
443 .iter()
444 .map(|a| a.names.get(app.opts.language.as_ref()))
445 .collect::<Vec<_>>();
446 format!("{} (at {})", list.choose(&mut rng).unwrap(), b.address)
447 }
448 }
449 TripEndpoint::Border(_) => "off-map".to_string(),
450 TripEndpoint::SuddenlyAppear(_) => "suddenly disappear".to_string(),
451 };
452 rows.push(
453 Text::from(format!(
454 " Spends {} at {}",
455 app.primary.sim.get_end_of_day() - last_trip.departure,
456 at
457 ))
458 .into_widget(ctx),
459 );
460
461 Widget::col(rows)
462}
463
464pub fn crowd(ctx: &EventCtx, app: &App, details: &mut Details, members: &[PedestrianID]) -> Widget {
465 let header = Widget::custom_col(vec![
466 Line("Pedestrian crowd").small_heading().into_widget(ctx),
467 header_btns(ctx),
468 ]);
469 Widget::custom_col(vec![
470 header,
471 crowd_body(ctx, app, details, members).tab_body(ctx),
472 ])
473}
474
475fn crowd_body(
476 ctx: &EventCtx,
477 app: &App,
478 details: &mut Details,
479 members: &[PedestrianID],
480) -> Widget {
481 let mut rows = vec![];
482 for (idx, id) in members.iter().enumerate() {
483 let person = app
484 .primary
485 .sim
486 .agent_to_person(AgentID::Pedestrian(*id))
487 .unwrap();
488 rows.push(Widget::row(vec![
490 format!("{})", idx + 1).text_widget(ctx).centered_vert(),
491 ctx.style()
492 .btn_outline
493 .text(person.to_string())
494 .build_def(ctx),
495 ]));
496 details.hyperlinks.insert(
497 person.to_string(),
498 Tab::PersonTrips(
499 person,
500 OpenTrip::single(
501 app.primary
502 .sim
503 .agent_to_trip(AgentID::Pedestrian(*id))
504 .unwrap(),
505 ),
506 ),
507 );
508 }
509
510 Widget::col(rows)
511}
512
513pub fn parked_car(
514 ctx: &mut EventCtx,
515 app: &App,
516 details: &mut Details,
517 id: CarID,
518 is_paused: bool,
519) -> Widget {
520 let header = Widget::row(vec![
521 Line(format!("Parked car #{}", id.id))
522 .small_heading()
523 .into_widget(ctx),
524 Widget::row(vec![
525 if is_paused {
528 ctx.style()
529 .btn_plain
530 .icon("system/assets/tools/location.svg")
531 .hotkey(Key::F)
532 .build_widget(ctx, "follow (run the simulation)")
533 } else {
534 ctx.style()
536 .btn_plain
537 .icon("system/assets/tools/location.svg")
538 .image_color(Color::hex("#7FFA4D"), ControlState::Default)
539 .hotkey(Key::F)
540 .build_widget(ctx, "unfollow (pause the simulation)")
541 },
542 ctx.style().btn_close_widget(ctx),
543 ])
544 .align_right(),
545 ]);
546
547 Widget::custom_col(vec![
548 header,
549 parked_car_body(ctx, app, details, id).tab_body(ctx),
550 ])
551}
552
553fn parked_car_body(ctx: &mut EventCtx, app: &App, details: &mut Details, id: CarID) -> Widget {
554 let mut rows = vec![];
556
557 let p = app.primary.sim.get_owner_of_car(id).unwrap();
558 rows.push(
559 ctx.style()
560 .btn_outline
561 .text(format!("Owned by {}", p))
562 .build_def(ctx),
563 );
564 details.hyperlinks.insert(
565 format!("Owned by {}", p),
566 Tab::PersonTrips(p, BTreeMap::new()),
567 );
568
569 if let Some(p) = app.primary.sim.lookup_parked_car(id) {
570 match p.spot {
571 ParkingSpot::Onstreet(_, _) | ParkingSpot::Lot(_, _) => {
572 ctx.canvas.center_on_map_pt(
573 app.primary
574 .sim
575 .canonical_pt_for_agent(AgentID::Car(id), &app.primary.map)
576 .unwrap(),
577 );
578 }
579 ParkingSpot::Offstreet(b, _) => {
580 ctx.canvas
581 .center_on_map_pt(app.primary.map.get_b(b).polygon.center());
582 rows.push(
583 format!("Parked inside {}", app.primary.map.get_b(b).address).text_widget(ctx),
584 );
585 }
586 }
587
588 rows.push(
589 format!(
590 "Parked here for {}",
591 app.primary.sim.time() - p.parked_since
592 )
593 .text_widget(ctx),
594 );
595 } else {
596 rows.push("No longer parked".text_widget(ctx));
597 }
598
599 Widget::col(rows)
600}
601
602fn header(
603 ctx: &mut EventCtx,
604 app: &App,
605 details: &mut Details,
606 id: PersonID,
607 tab: Tab,
608 is_paused: bool,
609) -> Widget {
610 let mut rows = vec![];
611
612 let (current_trip, (descr, maybe_icon)) = match app.primary.sim.get_person(id).state {
613 PersonState::Inside(b) => {
614 ctx.canvas
615 .center_on_map_pt(app.primary.map.get_b(b).label_center);
616 building::draw_occupants(details, app, b, Some(id));
617 (None, ("indoors", Some("system/assets/tools/home.svg")))
618 }
619 PersonState::Trip(t) => (
620 Some(t),
621 if let Some(a) = app.primary.sim.trip_to_agent(t).ok() {
622 if let Some(pt) = app.primary.sim.canonical_pt_for_agent(a, &app.primary.map) {
623 ctx.canvas.center_on_map_pt(pt);
624 }
625 match a {
626 AgentID::Pedestrian(_) => {
627 ("walking", Some("system/assets/meters/pedestrian.svg"))
628 }
629 AgentID::Car(c) => match c.vehicle_type {
630 VehicleType::Car => ("driving", Some("system/assets/meters/car.svg")),
631 VehicleType::Bike => ("biking", Some("system/assets/meters/bike.svg")),
632 VehicleType::Bus | VehicleType::Train => unreachable!(),
633 },
634 AgentID::BusPassenger(_, _) => {
635 ("riding a bus", Some("system/assets/meters/bus.svg"))
636 }
637 }
638 } else {
639 ("...", None)
641 },
642 ),
643 PersonState::OffMap => (None, ("off map", None)),
644 };
645
646 rows.push(Widget::custom_row(vec![
647 Line(format!("{}", id)).small_heading().into_widget(ctx),
648 if let Some(icon) = maybe_icon {
649 let batch = GeomBatch::load_svg(ctx, icon)
650 .color(RewriteColor::ChangeAll(Color::hex("#A3A3A3")))
651 .autocrop();
652 let y_factor = 20.0 / batch.get_dims().height;
653 batch.scale(y_factor).into_widget(ctx).margin_left(28)
654 } else {
655 Widget::nothing()
656 }
657 .centered_vert(),
658 Line(descr.to_string())
659 .small_heading()
660 .fg(Color::hex("#A3A3A3"))
661 .into_widget(ctx)
662 .margin_horiz(10),
663 Widget::row(vec![
664 if is_paused {
667 ctx.style()
668 .btn_plain
669 .icon("system/assets/tools/location.svg")
670 .hotkey(Key::F)
671 .build_widget(ctx, "follow (run the simulation)")
672 } else {
673 ctx.style()
675 .btn_plain
676 .icon("system/assets/tools/location.svg")
677 .image_color(Color::hex("#7FFA4D"), ControlState::Default)
678 .hotkey(Key::F)
679 .build_widget(ctx, "unfollow (pause the simulation)")
680 },
681 ctx.style().btn_close_widget(ctx),
682 ])
683 .align_right(),
684 ]));
685
686 let open_trips = if let Some(t) = current_trip {
687 OpenTrip::single(t)
688 } else {
689 BTreeMap::new()
690 };
691 let mut tabs = vec![
692 ("Trips", Tab::PersonTrips(id, open_trips)),
693 ("Bio", Tab::PersonBio(id)),
694 ];
695 if app.opts.dev {
696 tabs.push(("Schedule", Tab::PersonSchedule(id)));
697 }
698 rows.push(make_tabs(ctx, &mut details.hyperlinks, tab, tabs));
699
700 Widget::col(rows)
701}
702
703fn current_status(ctx: &EventCtx, person: &Person, map: &Map) -> Widget {
704 (match person.state {
705 PersonState::Inside(b) => {
706 format!("Currently inside {}", map.get_b(b).address).text_widget(ctx)
708 }
709 PersonState::Trip(_) => unreachable!(),
710 PersonState::OffMap => "Currently outside the map boundaries".text_widget(ctx),
711 })
712 .margin_vert(16)
713}
714
715fn cmp_duration_shorter(after: Duration, before: Duration) -> TextSpan {
717 if after.epsilon_eq(before) {
718 Line("no change").small()
719 } else if after < before {
720 Line(format!("{} faster", before - after))
721 .small()
722 .fg(Color::GREEN)
723 } else if after > before {
724 Line(format!("{} slower", after - before))
725 .small()
726 .fg(Color::RED)
727 } else {
728 unreachable!()
729 }
730}