convert_osm/
extract.rs

1use std::collections::HashSet;
2
3use abstutil::{MultiMap, Tags, Timer};
4use geom::{Distance, FindClosest, GPSBounds, HashablePt2D, LonLat, Polygon, Pt2D, Ring};
5use osm2streets::osm::{OsmID, RelationID, WayID};
6use osm2streets::{osm, NamePerLanguage};
7use raw_map::{
8    Amenity, AreaType, CrossingType, ExtraPOI, ExtraPOIType, RawArea, RawBuilding, RawMap,
9    RawParkingLot,
10};
11
12use crate::Options;
13use streets_reader::osm_reader::glue_multipolygon;
14use streets_reader::OsmExtract;
15
16pub struct Extract {
17    pub osm: OsmExtract,
18    pub doc: streets_reader::osm_reader::Document,
19    pub bus_routes_on_roads: MultiMap<WayID, String>,
20    /// Crossings located at these points, which should be on a Road's center line
21    pub crossing_nodes: HashSet<(HashablePt2D, CrossingType)>,
22    /// Some kind of barrier nodes at these points.
23    pub barrier_nodes: Vec<(osm::NodeID, HashablePt2D)>,
24    pub extra_pois: Vec<ExtraPOI>,
25}
26
27pub fn extract_osm(
28    map: &mut RawMap,
29    osm_input_path: &str,
30    clip_pts: Option<Vec<LonLat>>,
31    opts: &Options,
32    timer: &mut Timer,
33) -> Extract {
34    let osm_input_bytes = fs_err::read(osm_input_path).unwrap();
35    let mut doc = streets_reader::osm_reader::Document::read(
36        &osm_input_bytes,
37        clip_pts.as_ref().map(|pts| GPSBounds::from(pts.clone())),
38        timer,
39    )
40    .unwrap();
41    // If GPSBounds aren't provided above, they'll be computed in the Document
42    map.streets.gps_bounds = doc.gps_bounds.clone().unwrap();
43
44    timer.start("clip OSM document to boundary");
45    if let Some(pts) = clip_pts {
46        map.streets.boundary_polygon = Ring::deduping_new(map.streets.gps_bounds.convert(&pts))
47            .unwrap()
48            .into_polygon();
49        doc.clip(&map.streets.boundary_polygon, timer);
50    } else {
51        map.streets.boundary_polygon = map.streets.gps_bounds.to_bounds().get_rectangle();
52        // No need to clip the Document in this case.
53    }
54    timer.stop("clip OSM document to boundary");
55
56    streets_reader::detect_country_code(&mut map.streets);
57
58    let mut out = OsmExtract::new();
59    let mut amenity_points = Vec::new();
60    let mut bus_routes_on_roads: MultiMap<WayID, String> = MultiMap::new();
61    let mut crossing_nodes = HashSet::new();
62    let mut barrier_nodes = Vec::new();
63    let mut extra_pois = Vec::new();
64
65    timer.start_iter("processing OSM nodes", doc.nodes.len());
66    for (id, node) in &doc.nodes {
67        timer.next();
68        out.handle_node(*id, node);
69        for amenity in get_bldg_amenities(&node.tags) {
70            amenity_points.push((node.pt, amenity));
71        }
72        if node.tags.is(osm::HIGHWAY, "crossing") {
73            // TODO Look for crossing:signals:* too.
74            // https://wiki.openstreetmap.org/wiki/Tag:crossing=traffic%20signals?uselang=en
75            let kind = if node.tags.is("crossing", "traffic_signals") {
76                CrossingType::Signalized
77            } else {
78                CrossingType::Unsignalized
79            };
80            crossing_nodes.insert((node.pt.to_hashable(), kind));
81        }
82        // TODO Any kind of barrier?
83        if node.tags.is("barrier", "bollard") {
84            barrier_nodes.push((*id, node.pt.to_hashable()));
85        }
86
87        if node.tags.is("railway", "station") {
88            if let Some(network) = node.tags.get("network") {
89                if let Some(name) = node.tags.get("name") {
90                    // network can be ; separated, like
91                    // https://www.openstreetmap.org/node/3663368460
92                    if network.contains("London Underground") {
93                        extra_pois.push(ExtraPOI {
94                            pt: node.pt,
95                            kind: ExtraPOIType::LondonUndergroundStation(name.to_string()),
96                        });
97                    } else if network.contains("National Rail") {
98                        extra_pois.push(ExtraPOI {
99                            pt: node.pt,
100                            kind: ExtraPOIType::NationalRailStation(name.to_string()),
101                        });
102                    }
103                }
104            }
105        }
106    }
107
108    let mut coastline_groups: Vec<(WayID, Vec<Pt2D>)> = Vec::new();
109    let mut memorial_areas: Vec<Polygon> = Vec::new();
110    let mut amenity_areas: Vec<(Polygon, Amenity)> = Vec::new();
111    timer.start_iter("processing OSM ways", doc.ways.len());
112    for (id, way) in &mut doc.ways {
113        timer.next();
114        let id = *id;
115
116        if out.handle_way(id, &way, &opts.map_config) {
117            continue;
118        } else if way.tags.is(osm::HIGHWAY, "service") {
119            // If we got here, is_road didn't interpret it as a normal road
120            map.parking_aisles.push((id, way.pts.clone()));
121        } else if way.tags.is("natural", "coastline") && !way.tags.is("place", "island") {
122            coastline_groups.push((id, way.pts.clone()));
123            continue;
124        }
125
126        // All the other cases we care about are areas.
127        let mut deduped = way.pts.clone();
128        deduped.dedup();
129        let polygon = if let Ok(ring) = Ring::new(deduped) {
130            ring.into_polygon()
131        } else {
132            continue;
133        };
134
135        if is_bldg(&way.tags) {
136            map.buildings.insert(
137                OsmID::Way(id),
138                RawBuilding {
139                    polygon,
140                    public_garage_name: None,
141                    num_parking_spots: 0,
142                    amenities: get_bldg_amenities(&way.tags),
143                    osm_tags: way.tags.clone(),
144                },
145            );
146        } else if let Some(at) = get_area_type(&way.tags) {
147            map.areas.push(RawArea {
148                area_type: at,
149                osm_id: OsmID::Way(id),
150                polygon,
151                osm_tags: way.tags.clone(),
152            });
153        } else if way.tags.is("amenity", "parking") {
154            map.parking_lots.push(RawParkingLot {
155                osm_id: OsmID::Way(id),
156                polygon,
157                osm_tags: way.tags.clone(),
158            });
159        } else if way.tags.is("historic", "memorial") {
160            memorial_areas.push(polygon);
161        } else if way.tags.contains_key("amenity") {
162            let amenity = Amenity {
163                names: NamePerLanguage::new(&way.tags).unwrap_or_else(NamePerLanguage::unnamed),
164                amenity_type: way.tags.get("amenity").unwrap().clone(),
165                osm_tags: way.tags.clone(),
166            };
167            amenity_areas.push((polygon, amenity));
168        }
169    }
170
171    let boundary = map.streets.boundary_polygon.get_outer_ring();
172
173    timer.start_iter("processing OSM relations", doc.relations.len());
174    for (id, rel) in &doc.relations {
175        timer.next();
176        let id = *id;
177
178        if out.handle_relation(id, rel) {
179            continue;
180        } else if let Some(area_type) = get_area_type(&rel.tags) {
181            if rel.tags.is("type", "multipolygon") {
182                for polygon in
183                    glue_multipolygon(id, doc.get_multipolygon_members(id, rel), Some(&boundary))
184                {
185                    map.areas.push(RawArea {
186                        area_type,
187                        osm_id: OsmID::Relation(id),
188                        polygon,
189                        osm_tags: rel.tags.clone(),
190                    });
191                }
192            }
193        } else if is_bldg(&rel.tags) {
194            match doc.multipoly_geometry(id, rel) {
195                Ok(polygons) => {
196                    for polygon in polygons {
197                        map.buildings.insert(
198                            OsmID::Relation(id),
199                            RawBuilding {
200                                polygon,
201                                public_garage_name: None,
202                                num_parking_spots: 0,
203                                amenities: get_bldg_amenities(&rel.tags),
204                                osm_tags: rel.tags.clone(),
205                            },
206                        );
207                    }
208                }
209                Err(err) => println!("Skipping building {}: {}", id, err),
210            }
211        } else if rel.tags.is("amenity", "parking") {
212            for polygon in
213                glue_multipolygon(id, doc.get_multipolygon_members(id, rel), Some(&boundary))
214            {
215                map.parking_lots.push(RawParkingLot {
216                    osm_id: OsmID::Relation(id),
217                    polygon,
218                    osm_tags: rel.tags.clone(),
219                });
220            }
221        } else if rel.tags.is("type", "multipolygon") && rel.tags.contains_key("amenity") {
222            let amenity = Amenity {
223                names: NamePerLanguage::new(&rel.tags).unwrap_or_else(NamePerLanguage::unnamed),
224                amenity_type: rel.tags.get("amenity").unwrap().clone(),
225                osm_tags: rel.tags.clone(),
226            };
227            for (role, member) in &rel.members {
228                if role != "outer" {
229                    continue;
230                }
231                if let OsmID::Way(w) = member {
232                    // Sometimes the way is just the building, so we can directly update it
233                    if let Some(b) = map.buildings.get_mut(member) {
234                        b.amenities.push(amenity.clone());
235                    } else if let Ok(ring) = Ring::new(doc.ways[w].pts.clone()) {
236                        // Otherwise, match geometrically later on
237                        amenity_areas.push((ring.into_polygon(), amenity.clone()));
238                    }
239                }
240            }
241        } else if rel.tags.is("type", "route") && rel.tags.is("route", "bus") {
242            if let Some(name) = rel.tags.get("name") {
243                for (role, member) in &rel.members {
244                    if let OsmID::Way(w) = member {
245                        if role.is_empty() {
246                            bus_routes_on_roads.insert(*w, name.to_string());
247                        }
248                    }
249                }
250            }
251        }
252    }
253
254    // Special case the coastline.
255    for polygon in glue_multipolygon(RelationID(-1), coastline_groups, Some(&boundary)) {
256        let mut osm_tags = Tags::empty();
257        osm_tags.insert("water", "ocean");
258        // Put it at the beginning, so that it's naturally beneath island areas
259        map.areas.insert(
260            0,
261            RawArea {
262                area_type: AreaType::Water,
263                osm_id: OsmID::Relation(RelationID(-1)),
264                polygon,
265                osm_tags,
266            },
267        );
268    }
269
270    // Berlin has lots of "buildings" mapped in the Holocaust-Mahnmal. Filter them out.
271    timer.start_iter("match buildings to memorial areas", memorial_areas.len());
272    for area in memorial_areas {
273        timer.next();
274        map.buildings
275            .retain(|_, b| !area.contains_pt(b.polygon.center()));
276    }
277
278    let mut closest_bldg: FindClosest<OsmID> = FindClosest::new();
279    for (id, b) in &map.buildings {
280        closest_bldg.add_polygon(*id, &b.polygon);
281    }
282
283    timer.start_iter("match building amenities", amenity_points.len());
284    for (pt, amenity) in amenity_points {
285        timer.next();
286        if let Some((id, _)) = closest_bldg.closest_pt(pt, Distance::meters(50.0)) {
287            let b = map.buildings.get_mut(&id).unwrap();
288            if b.polygon.contains_pt(pt) {
289                b.amenities.push(amenity);
290            }
291        }
292    }
293
294    timer.start_iter("match buildings to amenity areas", amenity_areas.len());
295    for (poly, amenity) in amenity_areas {
296        timer.next();
297        for b in closest_bldg.all_points_inside(&poly) {
298            map.buildings
299                .get_mut(&b)
300                .unwrap()
301                .amenities
302                .push(amenity.clone());
303        }
304    }
305
306    // Hack to fix z-ordering for Green Lake (and probably other places). Put water and islands
307    // last. I think the more proper fix is interpreting "inner" roles in relations.
308    map.areas.sort_by_key(|a| match a.area_type {
309        AreaType::Island => 2,
310        AreaType::Water => 1,
311        _ => 0,
312    });
313
314    timer.start("find service roads crossing parking lots");
315    // TODO Something's crashing in one map, no time to investigate
316    if map.name != abstio::MapName::new("au", "melbourne", "maribyrnong") {
317        find_parking_aisles(map, &mut out.roads);
318    }
319    timer.stop("find service roads crossing parking lots");
320
321    Extract {
322        osm: out,
323        doc,
324        bus_routes_on_roads,
325        crossing_nodes,
326        barrier_nodes,
327        extra_pois,
328    }
329}
330
331fn is_bldg(tags: &Tags) -> bool {
332    // Sorry, the towers at Gasworks don't count. :)
333    tags.contains_key("building") && !tags.contains_key("abandoned:man_made")
334}
335
336fn get_bldg_amenities(tags: &Tags) -> Vec<Amenity> {
337    let mut amenities = Vec::new();
338    for key in ["amenity", "shop", "craft", "office", "tourism", "leisure"] {
339        if let Some(amenity) = tags.get(key) {
340            amenities.push(Amenity {
341                names: NamePerLanguage::new(tags).unwrap_or_else(NamePerLanguage::unnamed),
342                amenity_type: amenity.clone(),
343                osm_tags: tags.clone(),
344            });
345        }
346    }
347    amenities
348}
349
350fn get_area_type(tags: &Tags) -> Option<AreaType> {
351    if tags.is_any("leisure", vec!["garden", "park", "golf_course"]) {
352        return Some(AreaType::Park);
353    }
354    if tags.is_any("natural", vec!["wood", "scrub"]) {
355        return Some(AreaType::Park);
356    }
357    if tags.is_any(
358        "landuse",
359        vec![
360            "cemetery",
361            "flowerbed",
362            "forest",
363            "grass",
364            "meadow",
365            "recreation_ground",
366            "village_green",
367        ],
368    ) || tags.is("amenity", "graveyard")
369    {
370        return Some(AreaType::Park);
371    }
372
373    if tags.is("natural", "water") || tags.is("waterway", "riverbank") {
374        return Some(AreaType::Water);
375    }
376
377    if tags.is("place", "island") {
378        return Some(AreaType::Island);
379    }
380
381    None
382}
383
384// Look for any service roads that collide with parking lots, and treat them as parking aisles
385// instead.
386fn find_parking_aisles(map: &mut RawMap, roads: &mut Vec<(WayID, Vec<Pt2D>, Tags)>) {
387    let mut closest: FindClosest<usize> = FindClosest::new();
388    for (idx, lot) in map.parking_lots.iter().enumerate() {
389        closest.add_polygon(idx, &lot.polygon);
390    }
391    let mut keep_roads = Vec::new();
392    let mut parking_aisles = Vec::new();
393    for (id, pts, osm_tags) in roads.drain(..) {
394        if !osm_tags.is(osm::HIGHWAY, "service") {
395            keep_roads.push((id, pts, osm_tags));
396            continue;
397        }
398        // TODO This code is repeated later in make/parking_lots.rs, but oh well.
399
400        // Use the center of all the aisle points to match it to lots
401        let candidates: Vec<usize> = closest
402            .all_close_pts(Pt2D::center(&pts), Distance::meters(500.0))
403            .into_iter()
404            .map(|(idx, _, _)| idx)
405            .collect();
406        if service_road_crosses_parking_lot(map, &pts, candidates) {
407            parking_aisles.push((id, pts));
408        } else {
409            keep_roads.push((id, pts, osm_tags));
410        }
411    }
412    roads.extend(keep_roads);
413    for (id, pts) in parking_aisles {
414        map.parking_aisles.push((id, pts));
415    }
416}
417
418fn service_road_crosses_parking_lot(map: &RawMap, pts: &[Pt2D], candidates: Vec<usize>) -> bool {
419    if let Ok((polylines, rings)) = Ring::split_points(pts) {
420        for pl in polylines {
421            for idx in &candidates {
422                if map.parking_lots[*idx].polygon.clip_polyline(&pl).is_some() {
423                    return true;
424                }
425            }
426        }
427        for ring in rings {
428            for idx in &candidates {
429                if map.parking_lots[*idx].polygon.clip_ring(&ring).is_some() {
430                    return true;
431                }
432            }
433        }
434    }
435    false
436}