1use std::cell::RefCell;
2
3use geom::{Angle, Bounds, Distance, Line, Polygon, Pt2D, Ring, Tessellation};
4use map_model::{Building, BuildingID, Map, OffstreetParking};
5use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Text};
6
7use crate::colors::ColorScheme;
8use crate::options::{CameraAngle, Options};
9use crate::render::{DrawOptions, Renderable, OUTLINE_THICKNESS};
10use crate::{AppLike, ID};
11
12pub struct DrawBuilding {
13 pub id: BuildingID,
14 label: RefCell<Option<Drawable>>,
15}
16
17impl DrawBuilding {
18 pub fn new(
19 ctx: &EventCtx,
20 bldg: &Building,
21 map: &Map,
22 cs: &ColorScheme,
23 opts: &Options,
24 bldg_batch: &mut GeomBatch,
25 outlines_batch: &mut GeomBatch,
26 ) -> DrawBuilding {
27 let bldg_color = if bldg.amenities.is_empty() {
28 cs.residential_building
29 } else {
30 cs.commercial_building
31 };
32
33 match &opts.camera_angle {
34 CameraAngle::TopDown => {
35 bldg_batch.push(bldg_color, bldg.polygon.clone());
36 if opts.show_building_outlines {
37 outlines_batch.push(
38 cs.building_outline,
39 bldg.polygon.to_outline(Distance::meters(0.1)),
40 );
41 }
42
43 let parking_icon = match bldg.parking {
44 OffstreetParking::PublicGarage(_, _) => true,
45 OffstreetParking::Private(_, garage) => garage,
46 };
47 if parking_icon {
48 bldg_batch.append(
51 GeomBatch::load_svg(ctx, "system/assets/map/parking.svg")
52 .scale(0.1)
53 .centered_on(bldg.label_center),
54 );
55 }
56 }
57 CameraAngle::Abstract => {
58 bldg_batch.push(
60 bldg_color,
61 Polygon::rectangle_centered(
62 bldg.polygon.center(),
63 Distance::meters(5.0),
64 Distance::meters(5.0),
65 ),
66 );
67 }
68 x => {
69 let angle = match x {
70 CameraAngle::IsometricNE => Angle::degrees(-45.0),
71 CameraAngle::IsometricNW => Angle::degrees(-135.0),
72 CameraAngle::IsometricSE => Angle::degrees(45.0),
73 CameraAngle::IsometricSW => Angle::degrees(135.0),
74 CameraAngle::TopDown | CameraAngle::Abstract => unreachable!(),
75 };
76
77 let bldg_height_per_level = 3.5;
78 let bldg_rendered_meters = bldg_height_per_level * bldg.levels.powf(0.8);
81 let height = Distance::meters(bldg_rendered_meters);
82
83 let map_bounds = map.get_gps_bounds().to_bounds();
84 let (map_width, map_height) = (map_bounds.width(), map_bounds.height());
85 let map_length = map_width.hypot(map_height);
86
87 let distance = |pt: &Pt2D| {
88 let projection_origin = match x {
91 CameraAngle::IsometricNE => Pt2D::new(0.0, map_height),
92 CameraAngle::IsometricNW => Pt2D::new(map_width, map_height),
93 CameraAngle::IsometricSE => Pt2D::new(0.0, 0.0),
94 CameraAngle::IsometricSW => Pt2D::new(map_width, 0.0),
95 CameraAngle::TopDown | CameraAngle::Abstract => unreachable!(),
96 };
97
98 let abs_pt = Pt2D::new(
99 (pt.x() - projection_origin.x()).abs(),
100 (pt.y() - projection_origin.y()).abs(),
101 );
102
103 let a = f64::hypot(abs_pt.x(), abs_pt.y());
104 let theta = f64::atan(abs_pt.y() / abs_pt.x());
105 let distance = a * f64::sin(theta + std::f64::consts::PI / 4.0);
106 Distance::meters(distance)
107 };
108
109 let closest_pt = bldg
117 .polygon
118 .get_outer_ring()
119 .points()
120 .iter()
121 .min_by(|a, b| distance(a).cmp(&distance(b)))
122 .cloned();
123
124 let distance_from_projection_axis = closest_pt
125 .map(|pt| distance(&pt).inner_meters())
126 .unwrap_or(0.0);
127
128 let scale_factor = map_length;
130 let groundfloor_z = distance_from_projection_axis / scale_factor - 1.0;
131 let roof_z = groundfloor_z - height.inner_meters() / scale_factor;
132
133 if let Ok(roof) = Ring::new(
135 bldg.polygon
136 .get_outer_ring()
137 .points()
138 .iter()
139 .map(|pt| pt.project_away(height, angle))
140 .collect(),
141 ) {
142 bldg_batch.push(Color::BLACK, bldg.polygon.to_outline(Distance::meters(0.3)));
143
144 let wall_z = (groundfloor_z + roof_z) / 2.0;
149
150 let mut wall_beams = Vec::new();
151 for (low, high) in bldg
152 .polygon
153 .get_outer_ring()
154 .points()
155 .iter()
156 .zip(roof.points().iter())
157 {
158 if let Ok(l) = Line::new(*low, *high) {
161 wall_beams.push(l);
162 }
163 }
164 let wall_color = Color::hex("#BBBEC3");
165 for (wall1, wall2) in wall_beams.iter().zip(wall_beams.iter().skip(1)) {
166 bldg_batch.push_with_z(
167 wall_color,
168 Ring::must_new(vec![
169 wall1.pt1(),
170 wall1.pt2(),
171 wall2.pt2(),
172 wall2.pt1(),
173 wall1.pt1(),
174 ])
175 .into_polygon(),
176 wall_z,
177 );
178 }
179 for wall in wall_beams {
180 bldg_batch.push_with_z(
181 Color::BLACK,
182 wall.make_polygons(Distance::meters(0.1)),
183 wall_z,
184 );
185 }
186
187 bldg_batch.push_with_z(bldg_color, roof.clone().into_polygon(), roof_z);
188 bldg_batch.push_with_z(
189 Color::BLACK,
190 roof.to_outline(Distance::meters(0.3)),
191 roof_z,
192 );
193 } else {
194 bldg_batch.push(bldg_color, bldg.polygon.clone());
195 outlines_batch.push(
196 cs.building_outline,
197 bldg.polygon.to_outline(Distance::meters(0.1)),
198 );
199 }
200 }
201 }
202
203 DrawBuilding {
204 id: bldg.id,
205 label: RefCell::new(None),
206 }
207 }
208
209 pub fn clear_rendering(&mut self) {
210 *self.label.borrow_mut() = None;
211 }
212}
213
214impl Renderable for DrawBuilding {
215 fn get_id(&self) -> ID {
216 ID::Building(self.id)
217 }
218
219 fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike, opts: &DrawOptions) {
220 if opts.label_buildings {
221 let mut label = self.label.borrow_mut();
224 if label.is_none() {
225 let mut batch = GeomBatch::new();
226 let b = app.map().get_b(self.id);
227 if let Some(a) = b.amenities.get(0) {
228 let mut txt = Text::from(
229 Line(a.names.get(app.opts().language.as_ref())).fg(Color::BLACK),
230 );
231 if b.amenities.len() > 1 {
232 txt.append(Line(format!(" (+{})", b.amenities.len() - 1)).fg(Color::BLACK));
233 }
234 batch.append(
235 txt.render_autocropped(g)
236 .scale(0.1)
237 .centered_on(b.label_center),
238 );
239 }
240 *label = Some(g.prerender.upload(batch));
241 }
242 g.redraw(label.as_ref().unwrap());
243 }
244 }
245
246 fn get_zorder(&self) -> isize {
248 0
249 }
250
251 fn get_outline(&self, map: &Map) -> Tessellation {
252 map.get_b(self.id).polygon.to_outline(OUTLINE_THICKNESS)
253 }
254
255 fn get_bounds(&self, map: &Map) -> Bounds {
256 map.get_b(self.id).polygon.get_bounds()
257 }
258
259 fn contains_pt(&self, pt: Pt2D, map: &Map) -> bool {
260 map.get_b(self.id).polygon.contains_pt(pt)
261 }
262}