map_gui/render/
building.rs

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                    // Might need to scale down more for some buildings, but so far, this works
49                    // everywhere.
50                    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                // TODO The hitbox needs to change too
59                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                // In downtown areas, really tall buildings look kind of ridculous next to
79                // everything else. So we artificially compress the number of levels a bit.
80                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                    // some normalization so we can compute the distance to the corner of the
89                    // screen from which the orthographic projection is based.
90                    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                // Things closer to the isometric axis should appear in front of things farther
110                // away, so we give them a higher z-index.
111                //
112                // Naively, we compute the entire building's distance as the distance from its
113                // closest point. This is simple and usually works, but will likely fail on more
114                // complex building arrangements, e.g. if a building were tightly encircled by a
115                // large building.
116                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                // smaller z renders above larger
129                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                // TODO Some buildings have holes in them
134                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                    // In actuality, the z of the walls should start at groundfloor_z and end at
145                    // roof_z, but since we aren't dealing with actual 3d geometries, we have to
146                    // pick one value. Anecdotally, picking a value between the two seems to
147                    // usually looks right, but probably breaks down in certain overlap scenarios.
148                    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                        // Sometimes building height is 0!
159                        // https://www.openstreetmap.org/way/390547658
160                        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            // Labels are expensive to compute up-front, so do it lazily, since we don't really
222            // zoom in on all buildings in a single session anyway
223            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    // Some buildings cover up tunnels
247    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}