1use geom::{Distance, Line, Polygon, Pt2D};
2use osm2streets::{IntersectionID, Transformation};
3use widgetry::mapspace::WorldOutcome;
4use widgetry::tools::{open_browser, URLManager};
5use widgetry::{
6 lctrl, Canvas, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
7 SharedAppState, State, Text, Toggle, Transition, VerticalAlignment, Widget,
8};
9
10use crate::camera::CameraState;
11use crate::model::{Model, ID};
12
13pub struct App {
14 pub model: Model,
15}
16
17impl SharedAppState for App {
18 fn draw_default(&self, g: &mut GfxCtx) {
19 g.clear(Color::BLACK);
20 }
21
22 fn dump_before_abort(&self, canvas: &Canvas) {
23 if !self.model.map.name.map.is_empty() {
24 CameraState::save(canvas, &self.model.map.name);
25 }
26 }
27
28 fn before_quit(&self, canvas: &Canvas) {
29 if !self.model.map.name.map.is_empty() {
30 CameraState::save(canvas, &self.model.map.name);
31 }
32 }
33}
34
35pub struct MainState {
36 mode: Mode,
37 panel: Panel,
38}
39
40enum Mode {
41 Neutral,
42 CreatingRoad(IntersectionID),
43 SetBoundaryPt1,
44 SetBoundaryPt2(Pt2D),
45}
46
47impl MainState {
48 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
49 if !app.model.map.name.map.is_empty() {
50 URLManager::update_url_free_param(
51 abstio::path_raw_map(&app.model.map.name)
52 .strip_prefix(&abstio::path(""))
53 .unwrap()
54 .to_string(),
55 );
56 }
57 let bounds = app.model.map.streets.gps_bounds.to_bounds();
58 ctx.canvas.map_dims = (bounds.width(), bounds.height());
59
60 let mut state = MainState {
61 mode: Mode::Neutral,
62 panel: Panel::new_builder(Widget::col(vec![
63 Line("RawMap Editor").small_heading().into_widget(ctx),
64 Widget::col(vec![
65 Widget::col(vec![
66 Widget::row(vec![
67 ctx.style()
68 .btn_popup_icon_text(
69 "system/assets/tools/map.svg",
70 &app.model.map.name.as_filename(),
71 )
72 .hotkey(lctrl(Key::L))
73 .build_widget(ctx, "open another RawMap"),
74 ctx.style()
75 .btn_solid_destructive
76 .text("reload")
77 .build_def(ctx),
78 ]),
79 if cfg!(target_arch = "wasm32") {
80 Widget::nothing()
81 } else {
82 Widget::row(vec![
83 ctx.style()
84 .btn_solid_primary
85 .text("export to OSM")
86 .build_def(ctx),
87 ctx.style()
88 .btn_solid_destructive
89 .text("overwrite RawMap")
90 .build_def(ctx),
91 ])
92 },
93 ])
94 .section(ctx),
95 Widget::col(vec![
96 Toggle::choice(ctx, "create", "intersection", "building", None, true),
97 Toggle::switch(ctx, "show intersection geometry", Key::G, false),
98 ctx.style()
99 .btn_outline
100 .text("adjust boundary")
101 .build_def(ctx),
102 ctx.style()
103 .btn_outline
104 .text("simplify RawMap")
105 .build_def(ctx),
106 ])
107 .section(ctx),
108 ]),
109 Widget::placeholder(ctx, "instructions"),
110 ]))
111 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
112 .build(ctx),
113 };
114 state.update_instructions(ctx, app);
115 Box::new(state)
116 }
117
118 fn update_instructions(&mut self, ctx: &mut EventCtx, app: &App) {
119 let mut txt = Text::new();
120 if let Some(keybindings) = app.model.world.get_hovered_keybindings() {
121 for (key, action) in keybindings {
124 txt.add_appended(vec![
125 Line("- Press "),
126 key.txt(ctx),
127 Line(format!(" to {}", action)),
128 ]);
129 }
130 } else {
131 txt.add_appended(vec![
132 Line("Click").fg(ctx.style().text_hotkey_color),
133 Line(" to create a new intersection or building"),
134 ]);
135 }
136 let instructions = txt.into_widget(ctx);
137 self.panel.replace(ctx, "instructions", instructions);
138 }
139}
140
141impl State<App> for MainState {
142 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
143 match self.mode {
144 Mode::Neutral => {
145 match app.model.world.event(ctx) {
147 WorldOutcome::ClickedFreeSpace(pt) => {
148 if self.panel.is_checked("create") {
149 app.model.create_i(ctx, pt);
150 } else {
151 app.model.create_b(ctx, pt);
152 }
153 app.model.world.initialize_hover(ctx);
154 self.update_instructions(ctx, app);
155 }
156 WorldOutcome::Dragging {
157 obj: ID::Intersection(i),
158 cursor,
159 ..
160 } => {
161 app.model.move_i(ctx, i, cursor);
162 }
163 WorldOutcome::Dragging {
164 obj: ID::Building(b),
165 dx,
166 dy,
167 ..
168 } => {
169 app.model.move_b(ctx, b, dx, dy);
170 }
171 WorldOutcome::Dragging {
172 obj: ID::RoadPoint(r, idx),
173 cursor,
174 ..
175 } => {
176 app.model.move_r_pt(ctx, r, idx, cursor);
177 }
178 WorldOutcome::HoverChanged(before, after) => {
179 if let Some(ID::Road(r)) | Some(ID::RoadPoint(r, _)) = before {
180 app.model.stop_showing_pts(r);
181 }
182 if let Some(ID::Road(r)) | Some(ID::RoadPoint(r, _)) = after {
183 app.model.show_r_points(ctx, r);
184 }
188
189 self.update_instructions(ctx, app);
190 }
191 WorldOutcome::Keypress("start a road here", ID::Intersection(i)) => {
192 self.mode = Mode::CreatingRoad(i);
193 }
194 WorldOutcome::Keypress("delete", ID::Intersection(i)) => {
195 app.model.delete_i(i);
196 app.model.world.initialize_hover(ctx);
197 self.update_instructions(ctx, app);
198 }
199 WorldOutcome::Keypress(
200 "toggle stop sign / traffic signal",
201 ID::Intersection(i),
202 ) => {
203 app.model.toggle_i(ctx, i);
204 }
205 WorldOutcome::Keypress("debug in OSM", ID::Intersection(i)) => {
206 if let Some(id) = app.model.map.streets.intersections[&i].osm_ids.get(0) {
207 open_browser(id.to_string());
208 }
209 }
210 WorldOutcome::Keypress("delete", ID::Building(b)) => {
211 app.model.delete_b(b);
212 app.model.world.initialize_hover(ctx);
213 self.update_instructions(ctx, app);
214 }
215 WorldOutcome::Keypress("delete", ID::Road(r)) => {
216 app.model.delete_r(ctx, r);
217 app.model.world.initialize_hover(ctx);
219 self.update_instructions(ctx, app);
220 }
221 WorldOutcome::Keypress("insert a new point here", ID::Road(r)) => {
222 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
223 app.model.insert_r_pt(ctx, r, pt);
224 app.model.world.initialize_hover(ctx);
225 self.update_instructions(ctx, app);
226 }
227 }
228 WorldOutcome::Keypress("remove interior points", ID::Road(r)) => {
229 app.model.clear_r_pts(ctx, r);
230 app.model.world.initialize_hover(ctx);
231 self.update_instructions(ctx, app);
232 }
233 WorldOutcome::Keypress("delete", ID::RoadPoint(r, idx)) => {
234 app.model.delete_r_pt(ctx, r, idx);
235 app.model.world.initialize_hover(ctx);
236 self.update_instructions(ctx, app);
237 }
238 WorldOutcome::Keypress("merge", ID::Road(r)) => {
239 app.model.merge_r(ctx, r);
240 app.model.world.initialize_hover(ctx);
241 self.update_instructions(ctx, app);
242 }
243 WorldOutcome::Keypress("mark/unmark as a junction", ID::Road(r)) => {
244 app.model.toggle_junction(ctx, r);
245 }
246 WorldOutcome::Keypress("debug in OSM", ID::Road(r)) => {
247 if let Some(id) = app.model.map.streets.roads[&r].osm_ids.get(0) {
248 open_browser(id.to_string());
249 }
250 }
251 WorldOutcome::ClickedObject(ID::Road(r)) => {
252 return Transition::Push(crate::edit::EditRoad::new_state(ctx, app, r));
253 }
254 _ => {}
255 }
256
257 match self.panel.event(ctx) {
258 Outcome::Clicked(x) => match x.as_ref() {
259 "adjust boundary" => {
260 self.mode = Mode::SetBoundaryPt1;
261 }
262 "simplify RawMap" => {
263 ctx.loading_screen("simplify", |ctx, timer| {
264 app.model
265 .map
266 .streets
267 .apply_transformations(Transformation::abstreet(), timer);
268 app.model.recreate_world(ctx, timer);
269 });
270 }
271 "export to OSM" => {
272 app.model.export_to_osm();
273 }
274 "overwrite RawMap" => {
275 app.model.map.save();
276 }
277 "reload" => {
278 CameraState::save(ctx.canvas, &app.model.map.name);
279 return Transition::Push(crate::load::load_map(
280 ctx,
281 abstio::path_raw_map(&app.model.map.name),
282 app.model.include_bldgs,
283 None,
284 ));
285 }
286 "open another RawMap" => {
287 CameraState::save(ctx.canvas, &app.model.map.name);
288 return Transition::Push(crate::load::PickMap::new_state(ctx));
289 }
290 _ => unreachable!(),
291 },
292 Outcome::Changed(_) => {
293 app.model.show_intersection_geometry(
294 ctx,
295 self.panel.is_checked("show intersection geometry"),
296 );
297 }
298 _ => {}
299 }
300 }
301 Mode::CreatingRoad(i1) => {
302 if ctx.canvas_movement() {
303 URLManager::update_url_cam(ctx, &app.model.map.streets.gps_bounds);
304 }
305
306 if ctx.input.pressed(Key::Escape) {
307 self.mode = Mode::Neutral;
308 } else if let Some(ID::Intersection(i2)) = app.model.world.calculate_hovering(ctx) {
310 if i1 != i2 && ctx.input.pressed(Key::R) {
311 app.model.create_r(ctx, i1, i2);
312 self.mode = Mode::Neutral;
313 }
315 }
316 }
317 Mode::SetBoundaryPt1 => {
318 if ctx.canvas_movement() {
319 URLManager::update_url_cam(ctx, &app.model.map.streets.gps_bounds);
320 }
321
322 let mut txt = Text::new();
323 txt.add_appended(vec![
324 Line("Click").fg(ctx.style().text_hotkey_color),
325 Line(" the top-left corner of this map"),
326 ]);
327 let instructions = txt.into_widget(ctx);
328 self.panel.replace(ctx, "instructions", instructions);
329
330 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
331 if ctx.normal_left_click() {
332 self.mode = Mode::SetBoundaryPt2(pt);
333 }
334 }
335 }
336 Mode::SetBoundaryPt2(pt1) => {
337 if ctx.canvas_movement() {
338 URLManager::update_url_cam(ctx, &app.model.map.streets.gps_bounds);
339 }
340
341 let mut txt = Text::new();
342 txt.add_appended(vec![
343 Line("Click").fg(ctx.style().text_hotkey_color),
344 Line(" the bottom-right corner of this map"),
345 ]);
346 let instructions = txt.into_widget(ctx);
347 self.panel.replace(ctx, "instructions", instructions);
348
349 if let Some(pt2) = ctx.canvas.get_cursor_in_map_space() {
350 if ctx.normal_left_click() {
351 app.model.set_boundary(ctx, pt1, pt2);
352 self.mode = Mode::Neutral;
353 }
354 }
355 }
356 }
357
358 Transition::Keep
359 }
360
361 fn draw(&self, g: &mut GfxCtx, app: &App) {
362 g.draw_polygon(Color::WHITE, Polygon::rectangle(100.0, 10.0));
364 g.draw_polygon(Color::WHITE, Polygon::rectangle(10.0, 100.0));
365
366 g.draw_polygon(
367 Color::rgb(242, 239, 233),
368 app.model.map.streets.boundary_polygon.clone(),
369 );
370 app.model.world.draw(g);
371
372 match self.mode {
373 Mode::Neutral | Mode::SetBoundaryPt1 => {}
374 Mode::CreatingRoad(i1) => {
375 if let Some(cursor) = g.get_cursor_in_map_space() {
376 if let Ok(l) = Line::new(
377 app.model.map.streets.intersections[&i1].polygon.center(),
378 cursor,
379 ) {
380 g.draw_polygon(Color::GREEN, l.make_polygons(Distance::meters(5.0)));
381 }
382 }
383 }
384 Mode::SetBoundaryPt2(pt1) => {
385 if let Some(pt2) = g.canvas.get_cursor_in_map_space() {
386 if let Some(rect) = Polygon::rectangle_two_corners(pt1, pt2) {
387 g.draw_polygon(Color::YELLOW.alpha(0.5), rect);
388 }
389 }
390 }
391 };
392
393 self.panel.draw(g);
394 }
395}