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 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 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}