importer/
utils.rs

1use std::path::Path;
2use std::process::Command;
3
4use abstio::{CityName, MapName};
5use abstutil::{must_run_cmd, Timer};
6use map_model::RawToMapOptions;
7use raw_map::RawMap;
8
9use crate::configuration::ImporterConfiguration;
10
11/// If the output file doesn't already exist, downloads the URL into that location. Automatically
12/// uncompresses .zip and .gz files. Assumes a proper path is passed in (including data/).
13pub async fn download(config: &ImporterConfiguration, output: String, url: &str) {
14    if Path::new(&output).exists() {
15        println!("- {} already exists", output);
16        return;
17    }
18    // Create the directory
19    fs_err::create_dir_all(Path::new(&output).parent().unwrap())
20        .expect("Creating parent dir failed");
21
22    let tmp_file = format!("{output}_TMP");
23    let tmp = &tmp_file;
24    println!("- Missing {}, so downloading {}", output, url);
25    abstio::download_to_file(url, None, tmp).await.unwrap();
26
27    if url.contains(".zip") {
28        let unzip_to = if output.ends_with('/') {
29            output
30        } else {
31            // TODO In this case, there's no guarantee there's only one unzipped file and the name
32            // matches!
33            Path::new(&output).parent().unwrap().display().to_string()
34        };
35        println!("- Unzipping into {}", unzip_to);
36        must_run_cmd(Command::new(&config.unzip).arg(tmp).arg("-d").arg(unzip_to));
37        fs_err::remove_file(tmp).unwrap();
38    } else if url.contains(".gz") {
39        println!("- Gunzipping");
40        fs_err::rename(tmp, format!("{}.gz", output)).unwrap();
41
42        let mut gunzip_cmd = Command::new(&config.gunzip);
43        for arg in config.gunzip_args.split_ascii_whitespace() {
44            gunzip_cmd.arg(arg);
45        }
46        must_run_cmd(gunzip_cmd.arg(format!("{}.gz", output)));
47    } else {
48        fs_err::rename(tmp, output).unwrap();
49    }
50}
51
52/// If the output file doesn't already exist, downloads the URL into that location. Clips .kml
53/// files and converts to a .bin.
54pub async fn download_kml(
55    output: String,
56    url: &str,
57    bounds: &geom::GPSBounds,
58    require_all_pts_in_bounds: bool,
59    timer: &mut Timer<'_>,
60) {
61    assert!(url.ends_with(".kml"));
62    if Path::new(&output).exists() {
63        println!("- {} already exists", output);
64        return;
65    }
66    // Create the directory
67    fs_err::create_dir_all(Path::new(&output).parent().unwrap())
68        .expect("Creating parent dir failed");
69
70    let tmp_file = format!("{output}_TMP");
71    let tmp = &tmp_file;
72    if Path::new(&output.replace(".bin", ".kml")).exists() {
73        fs_err::copy(output.replace(".bin", ".kml"), tmp).unwrap();
74    } else {
75        println!("- Missing {}, so downloading {}", output, url);
76        abstio::download_to_file(url, None, tmp).await.unwrap();
77    }
78
79    println!("- Extracting KML data");
80
81    let shapes = kml::load(tmp.to_string(), bounds, require_all_pts_in_bounds, timer).unwrap();
82    abstio::write_binary(output.clone(), &shapes);
83    // Keep the intermediate file; otherwise we inadvertently grab new upstream data when
84    // changing some binary formats
85    fs_err::rename(tmp, output.replace(".bin", ".kml")).unwrap();
86}
87
88/// Uses osmium to clip the input .osm.xml or osm.pbf against a polygon and produce some output pbf
89/// file. Skips if the output exists.
90pub fn osmium(
91    input: String,
92    clipping_polygon: String,
93    output: String,
94    config: &ImporterConfiguration,
95) {
96    if Path::new(&output).exists() {
97        println!("- {} already exists", output);
98        return;
99    }
100    // Create the output directory if needed
101    fs_err::create_dir_all(Path::new(&output).parent().unwrap())
102        .expect("Creating parent dir failed");
103
104    println!("- Clipping {} to {}", input, clipping_polygon);
105
106    // --strategy complete_ways is default
107    must_run_cmd(
108        Command::new(&config.osmium)
109            .arg("extract")
110            .arg("-p")
111            .arg(clipping_polygon)
112            .arg(input)
113            .arg("-o")
114            .arg(output)
115            .arg("-f")
116            // Smaller files without author, timestamp, version
117            .arg("pbf,add_metadata=false"),
118    );
119}
120
121/// Creates a RawMap from OSM and other input data.
122pub async fn osm_to_raw(
123    name: MapName,
124    timer: &mut abstutil::Timer<'_>,
125    config: &ImporterConfiguration,
126) -> RawMap {
127    if name.city == CityName::seattle() {
128        crate::seattle::input(config, timer).await;
129    }
130    let opts = crate::map_config::config_for_map(&name);
131    if let Some(ref url) = opts.gtfs_url {
132        download(config, name.city.input_path("gtfs/"), url).await;
133    }
134
135    let boundary_polygon = format!(
136        "importer/config/{}/{}/{}.geojson",
137        name.city.country, name.city.city, name.map
138    );
139    let (osm_url, local_osm_file) = crate::pick_geofabrik(boundary_polygon.clone())
140        .await
141        .unwrap();
142    download(config, local_osm_file.clone(), &osm_url).await;
143
144    osmium(
145        local_osm_file,
146        boundary_polygon.clone(),
147        name.city.input_path(format!("osm/{}.osm.pbf", name.map)),
148        config,
149    );
150
151    let map = convert_osm::convert(
152        name.city.input_path(format!("osm/{}.osm.pbf", name.map)),
153        name.clone(),
154        Some(boundary_polygon),
155        opts,
156        timer,
157    );
158    map.save();
159    map
160}
161
162/// Converts a RawMap to a Map.
163pub fn raw_to_map(name: &MapName, opts: RawToMapOptions, timer: &mut Timer) -> map_model::Map {
164    timer.start(format!("Raw->Map for {}", name.describe()));
165    let raw: RawMap = abstio::read_binary(abstio::path_raw_map(name), timer);
166    let map = map_model::Map::create_from_raw(raw, opts, timer);
167    timer.start("save map");
168    map.save();
169    timer.stop("save map");
170    timer.stop(format!("Raw->Map for {}", name.describe()));
171
172    // TODO Just sticking this here for now
173    if name.map == "huge_seattle" || name == &MapName::new("gb", "leeds", "huge") {
174        timer.start("generating city manifest");
175        abstio::write_binary(
176            abstio::path(format!(
177                "system/{}/{}/city.bin",
178                map.get_city_name().country,
179                map.get_city_name().city
180            )),
181            &map_model::City::from_huge_map(&map),
182        );
183        timer.stop("generating city manifest");
184    }
185
186    map
187}