importer/
pick_geofabrik.rs

1use std::convert::TryInto;
2
3use anyhow::{bail, Result};
4use geo::{Area, Contains};
5use geojson::GeoJson;
6
7use abstutil::Timer;
8
9/// Given the path to a GeoJSON boundary polygon, return the URL of the smallest Geofabrik osm.pbf
10/// file that completely covers the boundary, and the path to where the local copy should go.
11pub async fn pick_geofabrik(input: String) -> Result<(String, String)> {
12    let boundary = load_boundary(input)?;
13
14    let geofabrik_idx = load_remote_geojson(
15        abstio::path_shared_input("geofabrik-index.json"),
16        "https://download.geofabrik.de/index-v1.json",
17    )
18    .await?;
19    let matches = find_matching_regions(geofabrik_idx, boundary);
20    info!("{} regions contain boundary", matches.len(),);
21    // Find the smallest matching region. Just round to the nearest square meter for comparison.
22    let (_, url) = matches
23        .into_iter()
24        .min_by_key(|(mp, _)| mp.unsigned_area() as usize)
25        .unwrap();
26
27    // Contains some directory structure, like north-america/us/wyoming-latest.osm.pbf or
28    // asia/yemen-latest.osm.pbf
29    let basename = url
30        .strip_prefix("https://download.geofabrik.de/")
31        .expect("Geofabrik URLs changed");
32    let local = abstio::path_shared_input(format!("geofabrik/{basename}"));
33
34    Ok((url, local))
35}
36
37fn load_boundary(path: String) -> Result<geo::Polygon> {
38    let gj: GeoJson = abstio::maybe_read_json(path, &mut Timer::throwaway())?;
39    let mut features = match gj {
40        GeoJson::Feature(feature) => vec![feature],
41        GeoJson::FeatureCollection(feature_collection) => feature_collection.features,
42        _ => bail!("Unexpected geojson: {:?}", gj),
43    };
44    if features.len() != 1 {
45        bail!("Expected exactly 1 feature");
46    }
47    let poly: geo::Polygon = features
48        .pop()
49        .unwrap()
50        .geometry
51        .take()
52        .unwrap()
53        .value
54        .try_into()
55        .unwrap();
56    Ok(poly)
57}
58
59async fn load_remote_geojson(path: String, url: &str) -> Result<GeoJson> {
60    if !abstio::file_exists(&path) {
61        info!("Downloading {}", url);
62        abstio::download_to_file(url, None, &path).await?;
63    }
64    abstio::maybe_read_json(path, &mut Timer::throwaway())
65}
66
67fn find_matching_regions(
68    geojson: GeoJson,
69    boundary: geo::Polygon,
70) -> Vec<(geo::MultiPolygon, String)> {
71    let mut matches = Vec::new();
72
73    // We're assuming some things about the geofabrik_idx index format -- it's a feature
74    // collection, every feature has a multipolygon geometry, the properties have a particular
75    // form.
76    if let GeoJson::FeatureCollection(fc) = geojson {
77        info!("Searching {} regions", fc.features.len());
78        for mut feature in fc.features {
79            let mp: geo::MultiPolygon = feature.geometry.take().unwrap().value.try_into().unwrap();
80            if mp.contains(&boundary) {
81                matches.push((
82                    mp,
83                    feature
84                        .property("urls")
85                        .unwrap()
86                        .get("pbf")
87                        .unwrap()
88                        .as_str()
89                        .unwrap()
90                        .to_string(),
91                ));
92            }
93        }
94    }
95
96    matches
97}