convert_osm/
lib.rs

1#[macro_use]
2extern crate log;
3
4use std::collections::{HashMap, HashSet};
5
6use anyhow::Result;
7
8use abstio::MapName;
9use abstutil::{Tags, Timer};
10use geom::{Distance, HashablePt2D, LonLat, PolyLine, Polygon};
11use osm2streets::{osm, MapConfig, Road, RoadID};
12use raw_map::{CrossingType, ExtraRoadData, RawMap};
13
14mod elevation;
15mod extract;
16mod gtfs;
17mod parking;
18
19/// Configures the creation of a `RawMap` from OSM and other input data.
20pub struct Options {
21    pub map_config: MapConfig,
22
23    pub onstreet_parking: OnstreetParking,
24    pub public_offstreet_parking: PublicOffstreetParking,
25    pub private_offstreet_parking: PrivateOffstreetParking,
26    /// If provided, read polygons from this GeoJSON file and add them to the RawMap as buildings.
27    pub extra_buildings: Option<String>,
28    /// Configure public transit using this URL to a static GTFS feed in .zip format.
29    pub gtfs_url: Option<String>,
30    /// Path to a GeoTIFF file in EPSG:4326 to use for elevation data
31    pub elevation_geotiff: Option<String>,
32    /// Only include crosswalks that match a `highway=crossing` OSM node.
33    pub filter_crosswalks: bool,
34}
35
36impl Options {
37    pub fn default() -> Self {
38        Self {
39            map_config: MapConfig::default(),
40            onstreet_parking: OnstreetParking::JustOSM,
41            public_offstreet_parking: PublicOffstreetParking::None,
42            private_offstreet_parking: PrivateOffstreetParking::FixedPerBldg(1),
43            extra_buildings: None,
44            gtfs_url: None,
45            elevation_geotiff: None,
46            filter_crosswalks: false,
47        }
48    }
49}
50
51/// What roads will have on-street parking lanes? Data from
52/// <https://wiki.openstreetmap.org/wiki/Key:parking:lane> is always used if available.
53pub enum OnstreetParking {
54    /// If not tagged, there won't be parking.
55    JustOSM,
56    /// If OSM data is missing, then try to match data from
57    /// <http://data-seattlecitygis.opendata.arcgis.com/datasets/blockface>. This is Seattle specific.
58    Blockface(String),
59}
60
61/// How many spots are available in public parking garages?
62pub enum PublicOffstreetParking {
63    None,
64    /// Pull data from
65    /// <https://data-seattlecitygis.opendata.arcgis.com/datasets/public-garages-or-parking-lots>, a
66    /// Seattle-specific data source.
67    Gis(String),
68}
69
70/// If a building doesn't have anything from public_offstreet_parking and isn't tagged as a garage
71/// in OSM, how many private spots should it have?
72pub enum PrivateOffstreetParking {
73    FixedPerBldg(usize),
74    // TODO Based on the number of residents?
75}
76
77/// Create a RawMap from OSM and other input data.
78pub fn convert(
79    osm_input_path: String,
80    name: MapName,
81    clip_path: Option<String>,
82    opts: Options,
83    timer: &mut Timer,
84) -> RawMap {
85    timer.start("create RawMap from input data");
86
87    let mut map = RawMap::blank(name);
88    // Note that DrivingSide is still incorrect. It'll be set in extract_osm, before Road::new
89    // happens in split_ways.
90    map.streets.config = opts.map_config.clone();
91
92    let clip_pts = clip_path.map(|path| LonLat::read_geojson_polygon(&path).unwrap());
93    timer.start("extract all from OSM");
94    let extract = extract::extract_osm(&mut map, &osm_input_path, clip_pts, &opts, timer);
95    timer.stop("extract all from OSM");
96    let pt_to_road =
97        streets_reader::split_ways::split_up_roads(&mut map.streets, extract.osm, timer);
98
99    // Cul-de-sacs aren't supported yet.
100    map.streets.retain_roads(|r| r.src_i != r.dst_i);
101
102    map.bus_routes_on_roads = extract.bus_routes_on_roads;
103    map.extra_pois = extract.extra_pois;
104
105    clip_map(&mut map, timer);
106
107    for i in map.streets.intersections.keys() {
108        map.elevation_per_intersection.insert(*i, Distance::ZERO);
109    }
110    for r in map.streets.roads.keys() {
111        map.extra_road_data.insert(*r, ExtraRoadData::default());
112    }
113
114    // Remember OSM tags for all roads. Do this before apply_parking, which looks at tags
115    timer.start("preserve OSM tags");
116    let mut way_ids = HashSet::new();
117    for r in map.streets.roads.values() {
118        for id in &r.osm_ids {
119            way_ids.insert(*id);
120        }
121    }
122    for (id, way) in extract.doc.ways {
123        if way_ids.contains(&id) {
124            map.osm_tags.insert(id, way.tags);
125        }
126    }
127    timer.stop("preserve OSM tags");
128
129    parking::apply_parking(&mut map, &opts, timer);
130
131    timer.start("use barrier and crossing nodes");
132    use_barrier_nodes(&mut map, extract.barrier_nodes, &pt_to_road);
133    use_crossing_nodes(&mut map, &extract.crossing_nodes, &pt_to_road);
134    timer.stop("use barrier and crossing nodes");
135
136    if opts.filter_crosswalks {
137        filter_crosswalks(&mut map, extract.crossing_nodes, pt_to_road, timer);
138    }
139
140    if let Some(ref path) = opts.elevation_geotiff {
141        timer.start("add elevation data");
142        if let Err(err) = elevation::add_data(&mut map, path, timer) {
143            error!("No elevation data: {}", err);
144        }
145        timer.stop("add elevation data");
146    }
147    if let Some(ref path) = opts.extra_buildings {
148        add_extra_buildings(&mut map, path).unwrap();
149    }
150
151    if opts.gtfs_url.is_some() {
152        gtfs::import(&mut map).unwrap();
153    }
154
155    timer.start("Add census data");
156    if let Err(err) = add_census(&mut map) {
157        error!("Skipping census data: {err}");
158    }
159    timer.stop("Add census data");
160
161    if map.name == MapName::new("gb", "bristol", "east") {
162        bristol_hack(&mut map);
163    }
164
165    timer.stop("create RawMap from input data");
166
167    map
168}
169
170fn add_extra_buildings(map: &mut RawMap, path: &str) -> Result<()> {
171    let require_in_bounds = true;
172    let mut id = -1;
173    for (polygon, _) in Polygon::from_geojson_bytes(
174        &abstio::slurp_file(path)?,
175        &map.streets.gps_bounds,
176        require_in_bounds,
177    )? {
178        // Add these as new buildings, generating a new dummy OSM ID.
179        map.buildings.insert(
180            osm::OsmID::Way(osm::WayID(id)),
181            raw_map::RawBuilding {
182                polygon,
183                osm_tags: Tags::empty(),
184                public_garage_name: None,
185                num_parking_spots: 1,
186                amenities: Vec::new(),
187            },
188        );
189        // We could use new_osm_way_id, but faster to just assume we're the only place introducing
190        // new OSM IDs.
191        id -= -1;
192    }
193    Ok(())
194}
195
196// We're using Bristol for a project that requires an unusual LTN neighborhood boundary. Insert a
197// fake road where a bridge crosses another road, to force blockfinding to trace along there.
198fn bristol_hack(map: &mut RawMap) {
199    let mut tags = Tags::empty();
200    tags.insert("highway", "service");
201    tags.insert("name", "Fake road");
202    tags.insert("oneway", "yes");
203    tags.insert("sidewalk", "none");
204    tags.insert("lanes", "1");
205    // TODO The LTN pathfinding tool will try to use this road. Discourage that heavily. It'd be
206    // safer to mark this as under construction, but then blockfinding wouldn't treat it as a
207    // boundary.
208    tags.insert("maxspeed", "1 mph");
209    tags.insert("bicycle", "no");
210
211    let src_i = map
212        .streets
213        .intersections
214        .values()
215        .find(|i| i.osm_ids.contains(&osm::NodeID(364061012)))
216        .unwrap()
217        .id;
218    let dst_i = map
219        .streets
220        .intersections
221        .values()
222        .find(|i| i.osm_ids.contains(&osm::NodeID(1215755208)))
223        .unwrap()
224        .id;
225
226    let id = map.streets.next_road_id();
227    map.streets.insert_road(Road::new(
228        id,
229        Vec::new(),
230        src_i,
231        dst_i,
232        PolyLine::must_new(vec![
233            map.streets.intersections[&src_i].polygon.center(),
234            map.streets.intersections[&dst_i].polygon.center(),
235        ]),
236        tags,
237        &map.streets.config,
238    ));
239    map.extra_road_data.insert(id, ExtraRoadData::default());
240}
241
242fn clip_map(map: &mut RawMap, timer: &mut Timer) {
243    let boundary_polygon = map.streets.boundary_polygon.clone();
244
245    map.buildings = timer.retain_parallelized(
246        "clip buildings to boundary",
247        std::mem::take(&mut map.buildings),
248        |b| {
249            b.polygon
250                .get_outer_ring()
251                .points()
252                .iter()
253                .all(|pt| boundary_polygon.contains_pt(*pt))
254        },
255    );
256
257    map.areas = timer
258        .parallelize(
259            "clip areas to boundary",
260            std::mem::take(&mut map.areas),
261            |orig_area| {
262                let mut result = Vec::new();
263                // If clipping fails, giving up on some areas is fine
264                if let Ok(list) = map
265                    .streets
266                    .boundary_polygon
267                    .intersection(&orig_area.polygon)
268                {
269                    for polygon in list {
270                        let mut area = orig_area.clone();
271                        area.polygon = polygon;
272                        result.push(area);
273                    }
274                }
275                result
276            },
277        )
278        .into_iter()
279        .flatten()
280        .collect();
281
282    // TODO Don't touch parking lots. It'll be visually obvious if a clip intersects one of these.
283    // The boundary should be manually adjusted.
284}
285
286fn use_barrier_nodes(
287    map: &mut RawMap,
288    barrier_nodes: Vec<(osm::NodeID, HashablePt2D)>,
289    pt_to_road: &HashMap<HashablePt2D, RoadID>,
290) {
291    // An OSM node likely only maps to one intersection
292    let mut node_to_intersection = HashMap::new();
293    for i in map.streets.intersections.values() {
294        for node in &i.osm_ids {
295            node_to_intersection.insert(*node, i.id);
296        }
297    }
298
299    for (node, pt) in barrier_nodes {
300        // Many barriers are on footpaths or roads that we don't retain
301        if let Some(road) = pt_to_road.get(&pt).and_then(|r| map.streets.roads.get(r)) {
302            // Filters on roads that're already car-free are redundant
303            if road.is_driveable() {
304                map.extra_road_data
305                    .get_mut(&road.id)
306                    .unwrap()
307                    .barrier_nodes
308                    .push(pt.to_pt2d());
309            }
310        } else if let Some(i) = node_to_intersection.get(&node) {
311            let roads = &map.streets.intersections[i].roads;
312            if roads.len() == 2 {
313                // Arbitrarily put the barrier on one of the roads
314                map.extra_road_data
315                    .get_mut(&roads[0])
316                    .unwrap()
317                    .barrier_nodes
318                    .push(pt.to_pt2d());
319            } else {
320                // TODO Look for real examples at non-2-way intersections to understand what to do.
321                // If there's a barrier in the middle of a 4-way, does that disconnect all
322                // movements?
323                warn!(
324                    "There's a barrier at {i}, but there are {} roads connected",
325                    roads.len()
326                );
327            }
328        }
329    }
330}
331
332fn use_crossing_nodes(
333    map: &mut RawMap,
334    crossing_nodes: &HashSet<(HashablePt2D, CrossingType)>,
335    pt_to_road: &HashMap<HashablePt2D, RoadID>,
336) {
337    for (pt, kind) in crossing_nodes {
338        // Some crossings are on footpaths or roads that we don't retain
339        if let Some(road) = pt_to_road
340            .get(pt)
341            .and_then(|r| map.extra_road_data.get_mut(r))
342        {
343            road.crossing_nodes.push((pt.to_pt2d(), *kind));
344        }
345    }
346}
347
348fn filter_crosswalks(
349    map: &mut RawMap,
350    crosswalks: HashSet<(HashablePt2D, CrossingType)>,
351    pt_to_road: HashMap<HashablePt2D, RoadID>,
352    timer: &mut Timer,
353) {
354    // Normally we assume every road has a crosswalk, but since this map is configured to use OSM
355    // crossing nodes, let's reverse that assumption.
356    for road in map.extra_road_data.values_mut() {
357        road.crosswalk_forward = false;
358        road.crosswalk_backward = false;
359    }
360
361    // Match each crosswalk node to a road
362    timer.start_iter("filter crosswalks", crosswalks.len());
363    for (pt, _) in crosswalks {
364        timer.next();
365        // Some crossing nodes are outside the map boundary or otherwise not on a road that we
366        // retained
367        if let Some(road) = pt_to_road.get(&pt).and_then(|r| map.streets.roads.get(r)) {
368            // Crossings aren't right at an intersection. Where is this point along the center
369            // line?
370            if let Some((dist, _)) = road.reference_line.dist_along_of_point(pt.to_pt2d()) {
371                let pct = dist / road.reference_line.length();
372                // Don't throw away any crossings. If it occurs in the first half of the road, snap
373                // to the first intersection. If there's a mid-block crossing mapped, that'll
374                // likely not be correctly interpreted, unless an intersection is there anyway.
375                let data = map.extra_road_data.get_mut(&road.id).unwrap();
376                if pct <= 0.5 {
377                    data.crosswalk_backward = true;
378                } else {
379                    data.crosswalk_forward = true;
380                }
381
382                // TODO Some crosswalks incorrectly snap to the intersection near a short service
383                // road, which later gets trimmed. So the crosswalk effectively disappears.
384            }
385        }
386    }
387}
388
389fn add_census(map: &mut RawMap) -> Result<()> {
390    // TODO Fixed to one area right now. Assumes the file exists.
391    if map.name.city.country != "gb" {
392        return Ok(());
393    }
394    let input_path = "data/input/shared/popgetter/england.topojson";
395    let boundary = map
396        .streets
397        .boundary_polygon
398        .to_geo_wgs84(&map.streets.gps_bounds);
399    for (geo_polygon, census_zone) in popgetter::clip_zones(input_path, boundary)? {
400        match Polygon::from_geo_wgs84(geo_polygon, &map.streets.gps_bounds) {
401            Ok(polygon) => {
402                map.census_zones.push((polygon, census_zone));
403            }
404            Err(err) => {
405                warn!("Skipping census zone {}: {}", census_zone.id, err);
406            }
407        }
408    }
409    Ok(())
410}