convert_osm/
parking.rs

1use abstutil::{Tags, Timer};
2use geom::{Distance, FindClosest, PolyLine};
3use kml::ExtraShapes;
4use osm2streets::{osm, RoadID};
5use raw_map::RawMap;
6
7use crate::{OnstreetParking, Options, PrivateOffstreetParking, PublicOffstreetParking};
8
9// Just used for matching hints to different sides of a road.
10const DIRECTED_ROAD_THICKNESS: Distance = Distance::const_meters(2.5);
11
12pub fn apply_parking(map: &mut RawMap, opts: &Options, timer: &mut Timer) {
13    match opts.onstreet_parking {
14        OnstreetParking::JustOSM => {}
15        OnstreetParking::Blockface(ref path) => {
16            use_parking_hints(map, path.clone(), timer);
17        }
18    }
19    match opts.public_offstreet_parking {
20        PublicOffstreetParking::None => {}
21        PublicOffstreetParking::Gis(ref path) => {
22            use_offstreet_parking(map, path.clone(), timer);
23        }
24    }
25    apply_private_offstreet_parking(map, &opts.private_offstreet_parking);
26}
27
28fn unknown_parking(tags: &Tags) -> bool {
29    !tags.contains_key("parking:lane:left")
30        && !tags.contains_key("parking:lane:right")
31        && !tags.contains_key("parking:lane:both")
32        && !tags.is_any(osm::HIGHWAY, vec!["motorway", "motorway_link", "service"])
33        && !tags.is("junction", "roundabout")
34}
35
36fn use_parking_hints(map: &mut RawMap, path: String, timer: &mut Timer) {
37    timer.start("apply parking hints");
38    let shapes: ExtraShapes = abstio::read_binary(path, timer);
39
40    // Match shapes with the nearest road + direction (true for forwards)
41    let mut closest: FindClosest<(RoadID, bool)> = FindClosest::new();
42    for (id, r) in &map.streets.roads {
43        if r.is_service() || !r.is_driveable() {
44            continue;
45        }
46        closest.add(
47            (*id, true),
48            r.reference_line
49                .must_shift_right(DIRECTED_ROAD_THICKNESS)
50                .points(),
51        );
52        closest.add(
53            (*id, false),
54            r.reference_line
55                .must_shift_left(DIRECTED_ROAD_THICKNESS)
56                .points(),
57        );
58    }
59
60    for s in shapes.shapes.into_iter() {
61        let pts = map.streets.gps_bounds.convert(&s.points);
62        if pts.len() <= 1 {
63            continue;
64        }
65        // The blockface line endpoints will be close to other roads, so match based on the
66        // middle of the blockface.
67        // TODO Long blockfaces sometimes cover two roads. Should maybe find ALL matches within
68        // the threshold distance?
69        let middle = if let Ok(pl) = PolyLine::new(pts) {
70            pl.middle()
71        } else {
72            // Weird blockface with duplicate points. Shrug.
73            continue;
74        };
75        if let Some(((r, fwds), _)) = closest.closest_pt(middle, DIRECTED_ROAD_THICKNESS * 5.0) {
76            let mut tags = map.road_to_osm_tags(r).cloned().unwrap_or_else(Tags::empty);
77
78            // Skip if the road already has this mapped.
79            if !unknown_parking(&tags) {
80                continue;
81            }
82
83            let category = s.attributes.get("PARKING_CATEGORY");
84            let has_parking = category != Some(&"None".to_string())
85                && category != Some(&"No Parking Allowed".to_string());
86
87            let definitely_no_parking =
88                tags.is_any(osm::HIGHWAY, vec!["motorway", "motorway_link", "trunk"]);
89            if has_parking && definitely_no_parking {
90                warn!(
91                    "Blockface says there's parking along motorway {}, ignoring",
92                    r
93                );
94                continue;
95            }
96
97            // Let's assume there isn't parking on the inner part of a dual carriageway
98            if !fwds && tags.is("dual_carriageway", "yes") {
99                continue;
100            }
101            // And definitely no parking in the middle of an intersection
102            if tags.is("junction", "intersection") {
103                continue;
104            }
105
106            if let Some(both) = tags.remove("parking:lane:both") {
107                tags.insert("parking:lane:left", both.clone());
108                tags.insert("parking:lane:right", both);
109            }
110
111            tags.insert(
112                if fwds {
113                    "parking:lane:right"
114                } else {
115                    "parking:lane:left"
116                },
117                if has_parking {
118                    "parallel"
119                } else {
120                    "no_parking"
121                },
122            );
123
124            // Maybe fold back into "both"
125            if tags.contains_key("parking:lane:left")
126                && tags.get("parking:lane:left") == tags.get("parking:lane:right")
127            {
128                let value = tags.remove("parking:lane:left").unwrap();
129                tags.remove("parking:lane:right").unwrap();
130                tags.insert("parking:lane:both", value);
131            }
132            // Note the change to the tag isn't saved, so regenerating lanes from tags later would
133            // lose this
134
135            // TODO It'd be better to do this directly to the LaneSpecs, not using OSM tags
136            let lane_specs_ltr = osm2streets::get_lane_specs_ltr(&tags, &map.streets.config);
137            let road = map.streets.roads.get_mut(&r).unwrap();
138            road.lane_specs_ltr = lane_specs_ltr;
139            road.update_center_line(map.streets.config.driving_side);
140            let i1 = road.src_i;
141            let i2 = road.dst_i;
142            map.streets.update_i(i1);
143            map.streets.update_i(i2);
144        }
145    }
146    timer.stop("apply parking hints");
147}
148
149fn use_offstreet_parking(map: &mut RawMap, path: String, timer: &mut Timer) {
150    timer.start("match offstreet parking points");
151    let shapes: ExtraShapes = abstio::read_binary(path, timer);
152
153    let mut closest: FindClosest<osm::OsmID> = FindClosest::new();
154    for (id, b) in &map.buildings {
155        closest.add_polygon(*id, &b.polygon);
156    }
157
158    // TODO Another function just to use ?. Try blocks would rock.
159    let mut handle_shape: Box<dyn FnMut(kml::ExtraShape) -> Option<()>> = Box::new(|s| {
160        assert_eq!(s.points.len(), 1);
161        let pt = s.points[0].to_pt(&map.streets.gps_bounds);
162        let (id, _) = closest.closest_pt(pt, Distance::meters(50.0))?;
163        // TODO Handle parking lots.
164        if !map.buildings[&id].polygon.contains_pt(pt) {
165            return None;
166        }
167        let name = s.attributes.get("DEA_FACILITY_NAME")?.to_string();
168        let num_stalls = s.attributes.get("DEA_STALLS")?.parse::<usize>().ok()?;
169        // Well that's silly. Why's it listed?
170        if num_stalls == 0 {
171            return None;
172        }
173
174        let bldg = map.buildings.get_mut(&id).unwrap();
175        if bldg.num_parking_spots > 0 {
176            // TODO Can't use timer inside this closure
177            let old_name = bldg.public_garage_name.take().unwrap();
178            println!(
179                "Two offstreet parking hints apply to {}: {} @ {}, and {} @ {}",
180                id, bldg.num_parking_spots, old_name, num_stalls, name
181            );
182            bldg.public_garage_name = Some(format!("{} and {}", old_name, name));
183            bldg.num_parking_spots += num_stalls;
184        } else {
185            bldg.public_garage_name = Some(name);
186            bldg.num_parking_spots = num_stalls;
187        }
188        None
189    });
190
191    for s in shapes.shapes.into_iter() {
192        handle_shape(s);
193    }
194    timer.stop("match offstreet parking points");
195}
196
197fn apply_private_offstreet_parking(map: &mut RawMap, policy: &PrivateOffstreetParking) {
198    match policy {
199        PrivateOffstreetParking::FixedPerBldg(n) => {
200            for b in map.buildings.values_mut() {
201                if b.public_garage_name.is_none() {
202                    assert_eq!(b.num_parking_spots, 0);
203
204                    // Is it a parking garage?
205                    if b.osm_tags.is("building", "parking") || b.osm_tags.is("amenity", "parking") {
206                        let levels = b
207                            .osm_tags
208                            .get("parking:levels")
209                            .or_else(|| b.osm_tags.get("building:levels"))
210                            .and_then(|x| x.parse::<usize>().ok())
211                            .unwrap_or(1);
212                        // For multi-story garages, assume every floor has the same capacity. Guess
213                        // 1 spot per 30m^2.
214                        b.num_parking_spots = ((b.polygon.area() / 30.0) as usize) * levels;
215                        // Not useful to list this
216                        b.amenities.retain(|a| a.amenity_type != "parking");
217                    } else {
218                        b.num_parking_spots = *n;
219                    }
220                }
221            }
222        }
223    }
224}