cli/
main.rs

1//! A collection of tools, mostly related to importing maps and scenarios. These are bundled as a
2//! single executable to reduce the cost of static linking in the release's file size.
3
4#[macro_use]
5extern crate log;
6
7mod augment_scenario;
8mod clip_osm;
9mod generate_houses;
10mod import_grid2demand;
11mod import_scenario;
12mod one_step_import;
13
14use std::io::Write;
15
16use abstio::CityName;
17use anyhow::Result;
18use fs_err::File;
19use importer::Job;
20use structopt::StructOpt;
21
22use abstutil::Timer;
23
24#[derive(StructOpt)]
25#[structopt(name = "abcli", about = "The A/B Street multi-tool")]
26enum Command {
27    /// Print a binary map or scenario file as JSON
28    DumpJSON {
29        #[structopt()]
30        path: String,
31    },
32    /// Generates a random scenario using the proletariat robot travel demand model
33    RandomScenario {
34        /// A seed for generating random numbers
35        #[structopt(long)]
36        rng_seed: u64,
37        /// The path to a map to generate a scenario for
38        #[structopt(long)]
39        map: String,
40        /// The name of the scenario to generate
41        #[structopt(long)]
42        scenario_name: String,
43    },
44    /// Modifies the schedule of every person in an existing scenario.
45    AugmentScenario {
46        /// The path to a scenario to augment. This will be modified in-place.
47        ///
48        /// This tool isn't very smart about detecting if a scenario already has these extra trips
49        /// added in; be careful about running this on the correct input.
50        #[structopt(long)]
51        input_scenario: String,
52        /// For people with only a single trip, add a return trip back home sometime 4-12 hours
53        /// later
54        #[structopt(long)]
55        add_return_trips: bool,
56        /// Before a person's final trip home, insert a round-trip to a nearby cafe or restaurant
57        #[structopt(long)]
58        add_lunch_trips: bool,
59        /// A JSON list of modifiers to transform the scenario. These can be generated with the GUI.
60        #[structopt(long, parse(try_from_str = parse_modifiers), default_value = "[]")]
61        scenario_modifiers: ModifierList,
62        /// Delete cancelled trips, and delete people with no remaining trips.
63        #[structopt(long)]
64        delete_cancelled_trips: bool,
65        /// A seed for generating random numbers
66        #[structopt(long, default_value = "42")]
67        rng_seed: u64,
68    },
69    /// Clips an OSM file to a boundary. This is a simple Rust port of `osmium extract large_map.osm
70    /// -p clipping.poly -o smaller_map.osm`.
71    ClipOSM {
72        /// The path to the input .osm.pbf file
73        #[structopt(long)]
74        pbf_path: String,
75        /// The path to a GeoJSON file with one boundary polygon
76        #[structopt(long)]
77        clip_path: String,
78        /// The path to write the XML results
79        #[structopt(long)]
80        out_path: String,
81    },
82    /// Import a scenario from https://github.com/asu-trans-ai-lab/grid2demand.
83    ImportGrid2Demand {
84        /// The path to a grid2demand CSV file
85        #[structopt(long)]
86        input: String,
87        /// The path to a map matching the grid2demand data
88        #[structopt(long)]
89        map: String,
90    },
91    /// Import a JSON scenario in the
92    /// https://a-b-street.github.io/docs/tech/dev/formats/scenarios.html format
93    ImportScenario {
94        /// The path to a JSON scenario file
95        #[structopt(long)]
96        input: String,
97        /// The path to a map matching the scenario data
98        #[structopt(long)]
99        map: String,
100        /// Problems occur when a position is within the map boundary, but not close enough to
101        /// buildings. Skip people with problematic positions if true, abort otherwise.
102        #[structopt(long)]
103        skip_problems: bool,
104    },
105    /// Transform a JSON map that's been manually edited into the binary format suitable for
106    /// simulation.
107    ImportJSONMap {
108        /// The path to a JSON map file to import
109        #[structopt(long)]
110        input: String,
111        /// The path to write
112        #[structopt(long)]
113        output: String,
114    },
115    /// Removes nonessential parts of a Map, for the bike network tool.
116    MinifyMap {
117        /// The path to a map to shrink. The map is modified in-place.
118        #[structopt()]
119        map: String,
120    },
121    /// Procedurally generates houses along empty residential roads of a map
122    GenerateHouses {
123        /// The path to a map to generate houses for
124        #[structopt(long)]
125        map: String,
126        /// If the tool doesn't generate at least this many houses, then fail. This can be used to
127        /// autodetect if a map probably already has most houses tagged in OSM.
128        #[structopt(long)]
129        num_required: usize,
130        /// A seed for generating random numbers
131        #[structopt(long, default_value = "42")]
132        rng_seed: u64,
133        /// The GeoJSON file to write
134        #[structopt(long)]
135        output: String,
136    },
137    /// Prints the osm.pbf file from download.geofabrik.de that covers a given boundary.
138    ///
139    /// This is a useful tool when importing a new map, if you don't already know which geofabrik
140    /// file you should use as your OSM input.
141    PickGeofabrik {
142        /// The path to a GeoJSON file with one boundary polygon
143        #[structopt()]
144        input: String,
145    },
146    /// Imports a one-shot A/B Street map from a GeoJSON boundary in a single command.
147    OneStepImport {
148        /// The path to a GeoJSON file with a boundary
149        #[structopt(long)]
150        geojson_path: String,
151        /// What to name the new imported map. The country will always be "zz" (a fake country
152        /// code), with the city as "oneshot." This name shouldn't contain spaces or be empty.
153        #[structopt(long)]
154        map_name: String,
155        /// Use Geofabrik to grab OSM input if true, or Overpass if false. Overpass is faster.
156        #[structopt(long)]
157        use_geofabrik: bool,
158        /// Use osmium to clip osm.pbf files. Faster, but requires an external system dependency.
159        /// Falls back to something built-in and slower.
160        #[structopt(long)]
161        use_osmium: bool,
162        /// If true, roads without explicitly tagged sidewalks may be assigned sidewalks or shoulders.
163        /// If false, no inference will occur and separate sidewalks and crossings will be included.
164        #[structopt(long)]
165        inferred_sidewalks: bool,
166        /// Downgrade crosswalks not matching a `highway=crossing` OSM node into unmarked crossings.
167        #[structopt(long)]
168        filter_crosswalks: bool,
169        /// Generate a simple travel demand model based on 2011 UK commuting data. This will only
170        /// work if the boundary is in the UK.
171        #[structopt(long)]
172        create_uk_travel_demand_model: bool,
173        #[structopt(flatten)]
174        opts: map_model::RawToMapOptions,
175    },
176    /// Imports a one-shot A/B Street map from an .osm file in a single command.
177    OneshotImport {
178        #[structopt()]
179        osm_input: String,
180        /// The path to a GeoJSON file with one boundary polygon. If omitted, a boundary will be
181        /// derived from the .osm file, but borders will likely be broken or missing.
182        #[structopt(long)]
183        clip_path: Option<String>,
184        /// If true, roads without explicitly tagged sidewalks may be assigned sidewalks or shoulders.
185        /// If false, no inference will occur and separate sidewalks and crossings will be included.
186        #[structopt(long)]
187        inferred_sidewalks: bool,
188        /// Downgrade crosswalks not matching a `highway=crossing` OSM node into unmarked crossings.
189        /// Downgrade crosswalks not matching a `highway=crossing` OSM node into unmarked crossings.
190        #[structopt(long)]
191        filter_crosswalks: bool,
192        /// Generate a simple travel demand model based on 2011 UK commuting data. This will only
193        /// work if the boundary is in the UK.
194        #[structopt(long)]
195        create_uk_travel_demand_model: bool,
196        #[structopt(flatten)]
197        opts: map_model::RawToMapOptions,
198    },
199    /// Regenerate all maps and scenarios from scratch.
200    RegenerateEverything {
201        /// If this command is being run in the cloud, parallelize the jobs by specifying which
202        /// shard this invocation should run.
203        #[structopt(long, default_value = "0")]
204        shard_num: usize,
205        /// If this command is being run in the cloud, parallelize the jobs by specifying how many
206        /// total shards there are.
207        #[structopt(long, default_value = "1")]
208        num_shards: usize,
209    },
210    /// Generate a shell script to regenerate all cities that uses an external task runner.
211    RegenerateEverythingExternally,
212    /// Import RawMaps, maps, scenarios, and city overviews for a single city.
213    Import {
214        #[structopt(flatten)]
215        job: Job,
216    },
217    /// Simulate a full day of a scenario, and write the "prebaked results," so the UI can later be
218    /// used for A/B testing.
219    #[structopt(name = "prebake-scenario")]
220    PrebakeScenario {
221        /// The path to a scenario file
222        #[structopt()]
223        scenario_path: String,
224    },
225}
226
227// See https://github.com/TeXitoi/structopt/issues/94
228type ModifierList = Vec<synthpop::ScenarioModifier>;
229
230fn parse_modifiers(x: &str) -> Result<ModifierList> {
231    abstutil::from_json(&x.to_string().into_bytes())
232}
233
234#[tokio::main]
235async fn main() -> Result<()> {
236    let cmd = Command::from_args();
237
238    // All but a few commands want logging
239    if !matches!(
240        cmd,
241        Command::DumpJSON { .. } | Command::PickGeofabrik { .. },
242    ) {
243        abstutil::logger::setup();
244    }
245
246    // Short implementations can stay in this file, but please split larger subcommands to their
247    // own module.
248    match Command::from_args() {
249        Command::DumpJSON { path } => dump_json(path),
250        Command::RandomScenario {
251            rng_seed,
252            map,
253            scenario_name,
254        } => random_scenario(rng_seed, map, scenario_name),
255        Command::AugmentScenario {
256            input_scenario,
257            add_return_trips,
258            add_lunch_trips,
259            scenario_modifiers,
260            delete_cancelled_trips,
261            rng_seed,
262        } => augment_scenario::run(
263            input_scenario,
264            add_return_trips,
265            add_lunch_trips,
266            scenario_modifiers,
267            delete_cancelled_trips,
268            rng_seed,
269        ),
270        Command::ClipOSM {
271            pbf_path,
272            clip_path,
273            out_path,
274        } => clip_osm::run(pbf_path, clip_path, out_path)?,
275        Command::ImportGrid2Demand { input, map } => import_grid2demand::run(input, map)?,
276        Command::ImportScenario {
277            input,
278            map,
279            skip_problems,
280        } => import_scenario::run(input, map, skip_problems),
281        Command::ImportJSONMap { input, output } => import_json_map(input, output),
282        Command::MinifyMap { map } => minify_map(map),
283        Command::GenerateHouses {
284            map,
285            num_required,
286            rng_seed,
287            output,
288        } => generate_houses::run(map, num_required, rng_seed, output),
289        Command::PickGeofabrik { input } => {
290            println!("{}", importer::pick_geofabrik(input).await?.0)
291        }
292        Command::OneStepImport {
293            geojson_path,
294            map_name,
295            use_geofabrik,
296            use_osmium,
297            inferred_sidewalks,
298            filter_crosswalks,
299            create_uk_travel_demand_model,
300            opts,
301        } => {
302            let mut options = convert_osm::Options::default();
303            options.map_config.inferred_sidewalks = inferred_sidewalks;
304            options.filter_crosswalks = filter_crosswalks;
305            one_step_import::run(
306                geojson_path,
307                map_name,
308                use_geofabrik,
309                use_osmium,
310                options,
311                create_uk_travel_demand_model,
312                opts,
313            )
314            .await?
315        }
316        Command::OneshotImport {
317            osm_input,
318            clip_path,
319            inferred_sidewalks,
320            filter_crosswalks,
321            create_uk_travel_demand_model,
322            opts,
323        } => {
324            let mut options = convert_osm::Options::default();
325            options.map_config.inferred_sidewalks = inferred_sidewalks;
326            options.filter_crosswalks = filter_crosswalks;
327            importer::oneshot(
328                osm_input,
329                clip_path,
330                options,
331                create_uk_travel_demand_model,
332                opts,
333            )
334            .await
335        }
336        Command::RegenerateEverything {
337            shard_num,
338            num_shards,
339        } => importer::regenerate_everything(shard_num, num_shards).await,
340        Command::RegenerateEverythingExternally => regenerate_everything_externally()?,
341        Command::Import { job } => job.run(&mut Timer::new("import one city")).await,
342        Command::PrebakeScenario { scenario_path } => prebake_scenario(scenario_path),
343    }
344    Ok(())
345}
346
347fn dump_json(path: String) {
348    // Just try to deserialize as different formats
349    if path.contains("/maps/") {
350        if let Ok(map) =
351            abstio::maybe_read_binary::<map_model::Map>(path.clone(), &mut Timer::throwaway())
352        {
353            println!("{}", abstutil::to_json(&map));
354            return;
355        }
356    }
357    if path.contains("/raw_maps/") {
358        if let Ok(map) =
359            abstio::maybe_read_binary::<raw_map::RawMap>(path.clone(), &mut Timer::throwaway())
360        {
361            println!("{}", abstutil::to_json(&map));
362            return;
363        }
364    }
365    if path.contains("/scenarios/") {
366        if let Ok(scenario) =
367            abstio::maybe_read_binary::<synthpop::Scenario>(path.clone(), &mut Timer::throwaway())
368        {
369            println!("{}", abstutil::to_json(&scenario));
370            return;
371        }
372    }
373    panic!(
374        "Don't know how to dump JSON for {}. Only maps, raw maps, and scenarios are supported.",
375        path
376    );
377}
378
379fn random_scenario(rng_seed: u64, map: String, scenario_name: String) {
380    use rand::SeedableRng;
381    use rand_xorshift::XorShiftRng;
382
383    let mut rng = XorShiftRng::seed_from_u64(rng_seed);
384    let map = map_model::Map::load_synchronously(map, &mut Timer::throwaway());
385    let mut scenario =
386        sim::ScenarioGenerator::proletariat_robot(&map, &mut rng, &mut Timer::throwaway());
387    scenario.scenario_name = scenario_name;
388    scenario.save();
389    println!(
390        "Wrote {}",
391        abstio::path_scenario(&scenario.map_name, &scenario.scenario_name)
392    );
393}
394
395fn import_json_map(input: String, output: String) {
396    // TODO This can't handle the output of dump_map! What?!
397    let mut map: map_model::Map = abstio::read_json(input, &mut Timer::throwaway());
398    map.map_loaded_directly(&mut Timer::throwaway());
399    abstio::write_binary(output, &map);
400}
401
402fn minify_map(path: String) {
403    let mut timer = Timer::new("minify map");
404    let mut map = map_model::Map::load_synchronously(path, &mut timer);
405    map.minify(&mut timer);
406    // This also changes the name, so this won't overwrite anything
407    map.save();
408}
409
410fn regenerate_everything_externally() -> Result<()> {
411    let path = "regenerate.sh";
412    let mut f = File::create(path)?;
413    writeln!(f, "#!/bin/sh")?;
414    writeln!(f, "pueue parallel 16")?;
415    for city in CityName::list_all_cities_from_importer_config() {
416        if city == CityName::new("gb", "london") {
417            // Special case because there are so many maps
418            for map in city.list_all_maps_in_city_from_importer_config() {
419                writeln!(
420                    f,
421                    "pueue add -- ./import.sh --raw --map --scenario {} --city=gb/london",
422                    map.map
423                )?;
424            }
425            continue;
426        }
427
428        let job = Job::full_for_city(city);
429        writeln!(f, "pueue add -- ./import.sh {}", job.flags().join(" "))?;
430    }
431    println!("");
432    println!(
433        "You can run {}. You'll need https://github.com/Nukesor/pueue set up first",
434        path
435    );
436    println!("Handy reminders: pueue status / pause / reset");
437    println!("pueue status | grep Success | wc -l");
438    println!("For the long-tail: pueue status | grep Running");
439    Ok(())
440}
441
442fn prebake_scenario(path: String) {
443    let mut timer = Timer::new("prebake scenario");
444    let scenario: synthpop::Scenario = abstio::must_read_object(path, &mut timer);
445    let map = map_model::Map::load_synchronously(scenario.map_name.path(), &mut timer);
446    sim::prebake::prebake(&map, scenario, &mut timer);
447}