1use geom::Duration;
2use map_gui::tools::draw_isochrone;
3use map_model::{AmenityType, BuildingID};
4use widgetry::table::{Col, Filter, Table};
5use widgetry::tools::open_browser;
6use widgetry::{
7 Color, Drawable, EventCtx, GfxCtx, HorizontalAlignment, Line, Outcome, Panel, State, Text,
8 Transition, VerticalAlignment, Widget,
9};
10
11use crate::isochrone::Isochrone;
12use crate::{render, App};
13
14pub struct ExploreAmenitiesDetails {
15 table: Table<App, Entry, ()>,
16 panel: Panel,
17 draw: Drawable,
18}
19
20struct Entry {
21 bldg: BuildingID,
22 amenity_idx: usize,
23 name: String,
24 amenity_type: String,
25 address: String,
26 duration_away: Duration,
27}
28
29impl ExploreAmenitiesDetails {
30 pub fn new_state(
31 ctx: &mut EventCtx,
32 app: &App,
33 isochrone: &Isochrone,
34 category: AmenityType,
35 ) -> Box<dyn State<App>> {
36 let mut batch = draw_isochrone(
37 &app.map,
38 &isochrone.time_to_reach_building,
39 &isochrone.thresholds,
40 &isochrone.colors,
41 );
42 batch.append(render::draw_star(ctx, app.map.get_b(isochrone.start[0])));
43
44 let mut entries = Vec::new();
45 for b in isochrone.amenities_reachable.get(category) {
46 let bldg = app.map.get_b(*b);
47 for (amenity_idx, amenity) in bldg.amenities.iter().enumerate() {
48 if AmenityType::categorize(&amenity.amenity_type) == Some(category) {
49 entries.push(Entry {
50 bldg: bldg.id,
51 amenity_idx,
52 name: amenity.names.get(app.opts.language.as_ref()).to_string(),
53 amenity_type: amenity.amenity_type.clone(),
54 address: bldg.address.clone(),
55 duration_away: isochrone.time_to_reach_building[&bldg.id],
56 });
57 batch.push(Color::RED, bldg.polygon.clone());
59 }
60 }
61 }
62
63 let mut table: Table<App, Entry, ()> = Table::new(
64 "time_to_reach_table",
65 entries,
66 Box::new(|x| format!("{}: {} ({})", x.bldg.0, x.name, x.amenity_idx)),
69 "Time to reach",
70 Filter::empty(),
71 );
72 table.column(
73 "Type",
74 Box::new(|ctx, _, x| Text::from(&x.amenity_type).render(ctx)),
75 Col::Sortable(Box::new(|rows| {
76 rows.sort_by_key(|x| x.amenity_type.clone())
77 })),
78 );
79 table.static_col("Name", Box::new(|x| x.name.clone()));
80 table.static_col("Address", Box::new(|x| x.address.clone()));
81 table.column(
82 "Time to reach",
83 Box::new(|ctx, app, x| {
84 Text::from(x.duration_away.to_string(&app.opts.units)).render(ctx)
85 }),
86 Col::Sortable(Box::new(|rows| rows.sort_by_key(|x| x.duration_away))),
87 );
88
89 let panel = Panel::new_builder(Widget::col(vec![
90 Widget::row(vec![
91 Line(format!("{} within 15 minutes", category))
92 .small_heading()
93 .into_widget(ctx),
94 ctx.style().btn_close_widget(ctx),
95 ]),
96 table.render(ctx, app),
97 ]))
98 .aligned(HorizontalAlignment::Center, VerticalAlignment::TopInset)
99 .build(ctx);
100
101 Box::new(Self {
102 table,
103 panel,
104 draw: ctx.upload(batch),
105 })
106 }
107}
108
109impl State<App> for ExploreAmenitiesDetails {
110 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
111 ctx.canvas_movement();
112
113 match self.panel.event(ctx) {
114 Outcome::Clicked(x) => {
115 if self.table.clicked(&x) {
116 self.table.replace_render(ctx, app, &mut self.panel)
117 } else if x == "close" {
118 return Transition::Pop;
119 } else if let Some(idx) = x.split(':').next().and_then(|x| x.parse::<usize>().ok())
120 {
121 let b = app.map.get_b(BuildingID(idx));
122 open_browser(b.orig_id.to_string());
123 } else {
124 unreachable!()
125 }
126 }
127 Outcome::Changed(_) => {
128 self.table.panel_changed(&self.panel);
129 self.table.replace_render(ctx, app, &mut self.panel)
130 }
131 _ => {}
132 }
133
134 Transition::Keep
135 }
136
137 fn draw(&self, g: &mut GfxCtx, app: &App) {
138 g.redraw(&self.draw);
139 self.panel.draw(g);
140 if let Some(x) = self
141 .panel
142 .currently_hovering()
143 .and_then(|x| x.split(':').next())
144 .and_then(|x| x.parse::<usize>().ok())
145 {
146 g.draw_polygon(Color::CYAN, app.map.get_b(BuildingID(x)).polygon.clone());
147 }
148 }
149}