1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use std::collections::{BTreeMap, HashMap, HashSet};

use abstutil::MultiMap;
use abstutil::Timer;
use geom::{Distance, FindClosest, Line, PolyLine};
use kml::{ExtraShape, ExtraShapes};
use map_model::osm::WayID;
use map_model::raw::{OriginalRoad, RawMap};
use map_model::{osm, Direction};

/// Attempt to snap separately mapped cycleways to main roads. Emit extra KML files to debug later.
pub fn snap_cycleways(map: &RawMap, timer: &mut Timer) {
    if map.city_name == "oneshot" {
        return;
    }

    // TODO Hack! Fix upstream problems.
    if map.name == "xian" {
        return;
    }

    let mut cycleways = HashMap::new();
    for shape in abstutil::read_binary::<ExtraShapes>(
        abstutil::path(format!("input/{}/footways.bin", map.city_name)),
        timer,
    )
    .shapes
    {
        // Just cycleways for now. This same general strategy should later work for sidewalks,
        // tramways, and blockface parking too.
        if shape.attributes.get("highway") == Some(&"cycleway".to_string()) {
            cycleways.insert(
                WayID(shape.attributes[osm::OSM_WAY_ID].parse().unwrap()),
                shape,
            );
        }
    }

    let mut road_edges: HashMap<(OriginalRoad, Direction), PolyLine> = HashMap::new();
    for (id, r) in &map.roads {
        if r.is_light_rail() || r.is_footway() || r.is_service() {
            continue;
        }
        let (pl, total_width) = r.get_geometry(*id, map.config.driving_side);
        road_edges.insert(
            (*id, Direction::Fwd),
            pl.must_shift_right(total_width / 2.0),
        );
        road_edges.insert(
            (*id, Direction::Back),
            pl.must_shift_left(total_width / 2.0),
        );
    }

    let matches = v1(map, &cycleways, &road_edges);
    // TODO A v2 idea: just look for cycleways strictly overlapping a thick road polygon
    dump_output(map, &cycleways, &road_edges, matches);
}

fn dump_output(
    map: &RawMap,
    cycleways: &HashMap<WayID, ExtraShape>,
    road_edges: &HashMap<(OriginalRoad, Direction), PolyLine>,
    matches: MultiMap<(OriginalRoad, Direction), WayID>,
) {
    let mut separate_cycleways = ExtraShapes { shapes: Vec::new() };
    let mut snapped_cycleways = ExtraShapes { shapes: Vec::new() };

    let mut used_cycleways = HashSet::new();
    for ((r, dir), ids) in matches.consume() {
        let mut attributes = BTreeMap::new();
        used_cycleways.extend(ids.clone());
        attributes.insert(
            "cycleways".to_string(),
            ids.into_iter()
                .map(|x| x.to_string())
                .collect::<Vec<_>>()
                .join(", "),
        );
        snapped_cycleways.shapes.push(ExtraShape {
            points: map.gps_bounds.convert_back(road_edges[&(r, dir)].points()),
            attributes,
        });
    }

    for (id, shape) in cycleways {
        if !used_cycleways.contains(id) {
            separate_cycleways.shapes.push(shape.clone());
        }
    }

    abstutil::write_binary(
        abstutil::path(format!(
            "input/{}/{}_separate_cycleways.bin",
            map.city_name, map.name
        )),
        &separate_cycleways,
    );
    abstutil::write_binary(
        abstutil::path(format!(
            "input/{}/{}_snapped_cycleways.bin",
            map.city_name, map.name
        )),
        &snapped_cycleways,
    );
}

// Walk along every cycleway, form a perpendicular line, and mark all road edges that it hits.
//
// TODO Inverse idea: Walk every road, project perpendicular from each of the 4 corners and see what
// cycleways hit.
//
// TODO Should we run this before splitting ways? Possibly less work to do.
fn v1(
    map: &RawMap,
    cycleways: &HashMap<WayID, ExtraShape>,
    road_edges: &HashMap<(OriginalRoad, Direction), PolyLine>,
) -> MultiMap<(OriginalRoad, Direction), WayID> {
    let mut matches: MultiMap<(OriginalRoad, Direction), WayID> = MultiMap::new();

    let mut closest: FindClosest<(OriginalRoad, Direction)> =
        FindClosest::new(&map.gps_bounds.to_bounds());
    for (id, pl) in road_edges {
        closest.add(*id, pl.points());
    }

    // TODO If this is too large, we might miss some intermediate pieces of the road.
    let step_size = Distance::meters(5.0);
    // This gives the length of the perpendicular test line
    let cycleway_half_width = Distance::meters(3.0);
    // How many degrees difference to consider parallel ways
    let parallel_threshold = 30.0;
    for (cycleway_id, cycleway) in cycleways {
        let pl = PolyLine::must_new(map.gps_bounds.convert(&cycleway.points));

        let mut dist = Distance::ZERO;
        loop {
            let (pt, cycleway_angle) = pl.must_dist_along(dist);
            let perp_line = Line::must_new(
                pt.project_away(cycleway_half_width, cycleway_angle.rotate_degs(90.0)),
                pt.project_away(cycleway_half_width, cycleway_angle.rotate_degs(-90.0)),
            );
            for (id, _, _) in closest.all_close_pts(perp_line.pt1(), cycleway_half_width) {
                if let Some((_, road_angle)) =
                    road_edges[&id].intersection(&perp_line.to_polyline())
                {
                    if road_angle.approx_eq(cycleway_angle, parallel_threshold) {
                        matches.insert(id, *cycleway_id);
                        // TODO Just stop at the first hit?
                        break;
                    }
                }
            }

            if dist == pl.length() {
                break;
            }
            dist += step_size;
            dist = dist.min(pl.length());
        }
    }

    matches
}