map_model/make/
buildings.rs

1use std::collections::{BTreeMap, HashSet};
2
3use rand::{Rng, SeedableRng};
4use rand_xorshift::XorShiftRng;
5
6use abstutil::{Tags, Timer};
7use geom::{Distance, HashablePt2D, Line};
8use raw_map::RawBuilding;
9
10use crate::make::{match_points_to_lanes, trim_path};
11use crate::{
12    osm, Amenity, Building, BuildingID, BuildingType, LaneID, Map, NamePerLanguage,
13    OffstreetParking,
14};
15
16/// Finalize importing of buildings, mostly by matching them to the nearest sidewalk.
17pub fn make_all_buildings(
18    input: &BTreeMap<osm::OsmID, RawBuilding>,
19    map: &Map,
20    keep_bldg_tags: bool,
21    timer: &mut Timer,
22) -> Vec<Building> {
23    timer.start("convert buildings");
24    let mut center_per_bldg: BTreeMap<osm::OsmID, HashablePt2D> = BTreeMap::new();
25    let mut query: HashSet<HashablePt2D> = HashSet::new();
26    timer.start_iter("get building center points", input.len());
27    for (id, b) in input {
28        timer.next();
29        let center = b.polygon.center().to_hashable();
30        center_per_bldg.insert(*id, center);
31        query.insert(center);
32    }
33
34    let sidewalk_buffer = Distance::meters(7.5);
35    let sidewalk_pts = match_points_to_lanes(
36        map,
37        query,
38        |l| l.is_walkable(),
39        // Don't put connections too close to intersections
40        sidewalk_buffer,
41        // Try not to skip any buildings, but more than 1km from a sidewalk is a little much
42        Distance::meters(1000.0),
43        timer,
44    );
45
46    let mut results = Vec::new();
47    timer.start_iter("match buildings to sidewalks", center_per_bldg.len());
48    for (orig_id, bldg_center) in center_per_bldg {
49        timer.next();
50        if let Some(sidewalk_pos) = sidewalk_pts.get(&bldg_center) {
51            let b = &input[&orig_id];
52            let sidewalk_line = match Line::new(bldg_center.to_pt2d(), sidewalk_pos.pt(map)) {
53                Ok(l) => trim_path(&b.polygon, l),
54                Err(_) => {
55                    warn!(
56                        "Skipping building {} because front path has 0 length",
57                        orig_id
58                    );
59                    continue;
60                }
61            };
62
63            let id = BuildingID(results.len());
64
65            let mut rng = XorShiftRng::seed_from_u64(orig_id.inner_id() as u64);
66            // TODO is it worth using height or building:height as an alternative if not tagged?
67            let levels = b
68                .osm_tags
69                .get("building:levels")
70                .and_then(|x| x.parse::<f64>().ok())
71                .unwrap_or(1.0);
72
73            results.push(Building {
74                id,
75                polygon: b.polygon.clone(),
76                levels,
77                address: get_address(&b.osm_tags, sidewalk_pos.lane(), map),
78                name: NamePerLanguage::new(&b.osm_tags),
79                orig_id,
80                label_center: b.polygon.polylabel(),
81                amenities: if keep_bldg_tags {
82                    b.amenities.clone()
83                } else {
84                    b.amenities
85                        .iter()
86                        .map(|a| {
87                            let mut a = a.clone();
88                            a.osm_tags = Tags::empty();
89                            a
90                        })
91                        .collect()
92                },
93                bldg_type: classify_bldg(
94                    &b.osm_tags,
95                    &b.amenities,
96                    levels,
97                    b.polygon.area(),
98                    &mut rng,
99                ),
100                parking: if let Some(n) = b.public_garage_name.clone() {
101                    OffstreetParking::PublicGarage(n, b.num_parking_spots)
102                } else {
103                    OffstreetParking::Private(
104                        b.num_parking_spots,
105                        b.osm_tags.is("building", "parking") || b.osm_tags.is("amenity", "parking"),
106                    )
107                },
108                osm_tags: if keep_bldg_tags {
109                    b.osm_tags.clone()
110                } else {
111                    Tags::empty()
112                },
113
114                sidewalk_pos: *sidewalk_pos,
115                driveway_geom: sidewalk_line.to_polyline(),
116            });
117        }
118    }
119
120    info!(
121        "Discarded {} buildings that weren't close enough to a sidewalk",
122        input.len() - results.len()
123    );
124    timer.stop("convert buildings");
125
126    results
127}
128
129// If the house number is missing, just omit it. (In the past, we showed "???" but this was a
130// confusing UX)
131fn get_address(tags: &Tags, sidewalk: LaneID, map: &Map) -> String {
132    let street = tags
133        .get("addr:street")
134        .cloned()
135        .unwrap_or_else(|| map.get_parent(sidewalk).get_name(None));
136    match tags.get("addr:housenumber") {
137        Some(num) => format!("{} {}", num, street),
138        None => street,
139    }
140}
141
142fn classify_bldg(
143    tags: &Tags,
144    amenities: &[Amenity],
145    levels: f64,
146    ground_area_sq_meters: f64,
147    rng: &mut XorShiftRng,
148) -> BuildingType {
149    // used: top values from https://taginfo.openstreetmap.org/keys/building#values (>100k uses)
150
151    let mut commercial = false;
152
153    let area_sq_meters = levels * ground_area_sq_meters;
154
155    // These are produced by get_bldg_amenities in convert_osm/src/osm_reader.rs.
156    // TODO: is it safe to assume all amenities are commercial?
157    // TODO: consider converting amenities to an enum - maybe with a catchall case for the long
158    //       tail of rarely used enums.
159    if !amenities.is_empty() {
160        commercial = true;
161    }
162
163    if tags.is("ruins", "yes") {
164        if commercial {
165            return BuildingType::Commercial(0);
166        }
167        return BuildingType::Empty;
168    }
169
170    let mut residents: usize = 0;
171    let mut workers: usize = 0;
172
173    #[allow(clippy::if_same_then_else)] // false positive (remove after addressing TODO below)
174    if tags.is_any(
175        "building",
176        vec![
177            "office",
178            "industrial",
179            "commercial",
180            "retail",
181            "warehouse",
182            "civic",
183            "public",
184        ],
185    ) {
186        // 1 person per 10 square meters
187        // TODO: Hone in this parameter. Space per person varies with (among other things):
188        //  - building type. e.g. office vs. warehouse
189        //  - regional/cultural norms
190        workers = (area_sq_meters / 10.0) as usize;
191    } else if tags.is_any(
192        "building",
193        vec!["school", "university", "construction", "church"],
194    ) {
195        // TODO: special handling in future
196        return BuildingType::Empty;
197    } else if tags.is_any(
198        "building",
199        vec![
200            "garage",
201            "garages",
202            "shed",
203            "roof",
204            "greenhouse",
205            "farm_auxiliary",
206            "barn",
207            "service",
208        ],
209    ) {
210        return BuildingType::Empty;
211    } else if tags.is_any(
212        "building",
213        vec!["house", "detached", "semidetached_house", "farm"],
214    ) {
215        residents = rng.gen_range(0..3);
216    } else if tags.is_any("building", vec!["hut", "static_caravan", "cabin"]) {
217        residents = rng.gen_range(0..2);
218    } else if tags.is_any("building", vec!["apartments", "terrace", "residential"]) {
219        // 1 person per 10 square meters
220        // TODO: Hone in this parameter. Space per person varies with (among other things):
221        //  - building type. e.g. apartment vs single family
222        //  - regional/cultural norms
223        residents = (area_sq_meters / 10.0) as usize;
224    } else {
225        residents = rng.gen_range(0..2);
226    }
227
228    if commercial && workers == 0 {
229        // TODO: Come up with a better measure
230        workers = (residents as f64 / 3.0) as usize;
231    }
232
233    if commercial {
234        if residents > 0 {
235            return BuildingType::ResidentialCommercial(residents, workers);
236        }
237        return BuildingType::Commercial(workers);
238    }
239    BuildingType::Residential {
240        num_residents: residents,
241        num_housing_units: 1,
242    }
243}