map_gui/tools/
importer.rs

1use std::io::Write;
2
3use anyhow::Result;
4
5use abstio::MapName;
6use widgetry::tools::{open_browser, PopupMsg};
7use widgetry::{
8    EventCtx, GfxCtx, Line, Outcome, Panel, State, TextBox, TextExt, Toggle, Transition, Widget,
9};
10
11use crate::load::MapLoader;
12use crate::tools::find_exe;
13use crate::AppLike;
14
15pub struct ImportCity<A: AppLike> {
16    panel: Panel,
17    // Wrapped in an Option just to make calling from event() work.
18    on_load: Option<Box<dyn FnOnce(&mut EventCtx, &mut A) -> Transition<A>>>,
19}
20
21impl<A: AppLike + 'static> ImportCity<A> {
22    pub fn new_state(
23        ctx: &mut EventCtx,
24        on_load: Box<dyn FnOnce(&mut EventCtx, &mut A) -> Transition<A>>,
25    ) -> Box<dyn State<A>> {
26        let panel = Panel::new_builder(Widget::col(vec![
27            Widget::row(vec![
28                Line("Import a new city").small_heading().into_widget(ctx),
29                ctx.style().btn_close_widget(ctx),
30            ]),
31            Widget::col(vec![
32                Widget::row(vec![
33                    "Step 1)".text_widget(ctx).centered_vert(),
34                    ctx.style()
35                        .btn_plain
36                        .btn()
37                        .label_underlined_text("Go to geojson.io")
38                        .build_def(ctx),
39                ]),
40                Widget::row(vec![
41                    "Step 2)".text_widget(ctx).margin_right(16),
42                    "Draw a polygon boundary where you want to import"
43                        .text_widget(ctx)
44                        .margin_below(16),
45                ])
46                .margin_below(16),
47                Widget::row(vec![
48                    "Step 3)".text_widget(ctx).margin_right(16),
49                    "Copy the JSON text on the right into your clipboard".text_widget(ctx),
50                ])
51                .margin_below(16),
52                Widget::row(vec![
53                    "Name the map:".text_widget(ctx).centered_vert(),
54                    TextBox::widget(ctx, "new_map_name", generate_new_map_name(), true, 20),
55                ]),
56                ctx.style()
57                    .btn_solid_primary
58                    .text("Import the area from your clipboard")
59                    .build_def(ctx)
60                    .margin_below(32),
61                ctx.style()
62                    .btn_plain
63                    .btn()
64                    .label_underlined_text("Alternate instructions")
65                    .build_def(ctx),
66                Widget::col(vec![
67                    Line("Advanced settings").secondary().into_widget(ctx),
68                    Widget::row(vec![
69                        "Import data from:".text_widget(ctx).centered_vert(),
70                        Toggle::choice(
71                            ctx,
72                            "source",
73                            "GeoFabrik",
74                            "Overpass (faster)",
75                            None,
76                            false,
77                        ),
78                    ]),
79                    Toggle::switch(ctx, "Infer sidewalks on roads", None, true),
80                    Toggle::switch(ctx, "Filter crosswalks", None, false),
81                    Toggle::switch(ctx, "Generate travel demand model (UK only)", None, false),
82                ])
83                .section(ctx),
84            ])
85            .section(ctx),
86        ]))
87        .build(ctx);
88        Box::new(ImportCity {
89            panel,
90            on_load: Some(on_load),
91        })
92    }
93}
94
95impl<A: AppLike + 'static> State<A> for ImportCity<A> {
96    fn event(&mut self, ctx: &mut EventCtx, _: &mut A) -> Transition<A> {
97        match self.panel.event(ctx) {
98            Outcome::Clicked(x) => match x.as_ref() {
99                "close" => Transition::Pop,
100                "Alternate instructions" => {
101                    open_browser("https://a-b-street.github.io/docs/user/new_city.html");
102                    Transition::Keep
103                }
104                "Go to geojson.io" => {
105                    open_browser("http://geojson.io");
106                    Transition::Keep
107                }
108                "Import the area from your clipboard" => {
109                    let name = sanitize_name(self.panel.text_box("new_map_name"));
110
111                    let mut args = vec![
112                        find_exe("cli"),
113                        "one-step-import".to_string(),
114                        "--geojson-path=boundary.geojson".to_string(),
115                        format!("--map-name={}", name),
116                    ];
117                    if self.panel.is_checked("source") {
118                        args.push("--use-geofabrik".to_string());
119                    }
120                    if self.panel.is_checked("Infer sidewalks on roads") {
121                        args.push("--inferred-sidewalks".to_string());
122                    }
123                    if self.panel.is_checked("Filter crosswalks") {
124                        args.push("--filter-crosswalks".to_string());
125                    }
126                    if self
127                        .panel
128                        .is_checked("Generate travel demand model (UK only)")
129                    {
130                        args.push("--create-uk-travel-demand-model".to_string());
131                    }
132                    match grab_geojson_from_clipboard() {
133                        Ok(()) => Transition::Push(crate::tools::RunCommand::new_state(
134                            ctx,
135                            true,
136                            args,
137                            Box::new(|_, _, success, _| {
138                                if success {
139                                    abstio::delete_file("boundary.geojson");
140
141                                    Transition::ConsumeState(Box::new(move |state, ctx, app| {
142                                        let mut state =
143                                            state.downcast::<ImportCity<A>>().ok().unwrap();
144                                        let on_load = state.on_load.take().unwrap();
145                                        let map_name = MapName::new("zz", "oneshot", &name);
146                                        vec![MapLoader::new_state(ctx, app, map_name, on_load)]
147                                    }))
148                                } else {
149                                    // The popup already explained the failure
150                                    Transition::Keep
151                                }
152                            }),
153                        )),
154                        Err(err) => Transition::Push(PopupMsg::new_state(
155                            ctx,
156                            "Error",
157                            vec![
158                                "Couldn't get GeoJSON from your clipboard".to_string(),
159                                err.to_string(),
160                            ],
161                        )),
162                    }
163                }
164                _ => unreachable!(),
165            },
166            _ => Transition::Keep,
167        }
168    }
169
170    fn draw(&self, g: &mut GfxCtx, _: &A) {
171        self.panel.draw(g);
172    }
173}
174
175fn grab_geojson_from_clipboard() -> Result<()> {
176    let contents = widgetry::tools::get_clipboard()?;
177    if contents.parse::<geojson::GeoJson>().is_err() {
178        bail!(
179            "Your clipboard doesn't seem to have GeoJSON. Got: {}",
180            contents
181        );
182    }
183    let mut f = fs_err::File::create("boundary.geojson")?;
184    write!(f, "{}", contents)?;
185    Ok(())
186}
187
188fn sanitize_name(x: String) -> String {
189    x.replace(" ", "_")
190}
191
192fn generate_new_map_name() -> String {
193    let mut i = 0;
194    loop {
195        let name = format!("imported_{}", i);
196        if !abstio::file_exists(MapName::new("zz", "oneshot", &name).path()) {
197            return name;
198        }
199        i += 1;
200    }
201}