map_editor/
app.rs

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            // TODO Should we also say click and drag to move it? Or for clickable roads, click to
122            // edit?
123            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                // TODO Update URL when canvas moves
146                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                            // Shouldn't need to call initialize_hover, unless the user somehow
185                            // warped their cursor to precisely the location of a point, in the
186                            // middle of the road!
187                        }
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                        // There may be something underneath the road, so recalculate immediately
218                        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                    // TODO redo mouseover?
309                } 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                        // TODO redo mouseover?
314                    }
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        // It's useful to see the origin.
363        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}