diff --git a/map_editor/Cargo.toml b/map_editor/Cargo.toml index fe52c92408..d75fdb6523 100644 --- a/map_editor/Cargo.toml +++ b/map_editor/Cargo.toml @@ -14,6 +14,6 @@ abstio = { path = "../abstio" } abstutil = { path = "../abstutil" } geom = { path = "../geom" } log = "0.4.14" -map_gui = { path = "../map_gui" } +map_gui = { path = "../map_gui", features=["native"] } map_model = { path = "../map_model" } widgetry = { path = "../widgetry" } diff --git a/map_editor/src/edit.rs b/map_editor/src/edit.rs new file mode 100644 index 0000000000..342d72a41f --- /dev/null +++ b/map_editor/src/edit.rs @@ -0,0 +1,179 @@ +use map_model::raw::OriginalRoad; +use widgetry::{ + Choice, DrawBaselayer, EventCtx, HorizontalAlignment, Key, Line, Panel, SimpleState, Spinner, + State, StyledButtons, Text, TextExt, Transition, VerticalAlignment, Widget, +}; + +use crate::App; + +pub struct EditRoad { + r: OriginalRoad, +} + +impl EditRoad { + pub(crate) fn new(ctx: &mut EventCtx, app: &App, r: OriginalRoad) -> Box> { + let road = &app.model.map.roads[&r]; + + let mut txt = Text::new(); + for (k, v) in road.osm_tags.inner() { + txt.add(Line(format!("{} = {}", k, v)).secondary()); + } + let info = txt.draw(ctx); + + let controls = Widget::col(vec![ + Widget::row(vec![ + "lanes:forward".draw_text(ctx).margin_right(20), + Spinner::new( + ctx, + (1, 5), + road.osm_tags + .get("lanes:forward") + .and_then(|x| x.parse::().ok()) + .unwrap_or(1), + ) + .named("lanes:forward"), + ]), + Widget::row(vec![ + "lanes:backward".draw_text(ctx).margin_right(20), + Spinner::new( + ctx, + (0, 5), + road.osm_tags + .get("lanes:backward") + .and_then(|x| x.parse::().ok()) + .unwrap_or(1), + ) + .named("lanes:backward"), + ]), + Widget::row(vec![ + "sidewalk".draw_text(ctx).margin_right(20), + Widget::dropdown( + ctx, + "sidewalk", + if road.osm_tags.is("sidewalk", "both") { + "both" + } else if road.osm_tags.is("sidewalk", "none") { + "none" + } else if road.osm_tags.is("sidewalk", "left") { + "left" + } else if road.osm_tags.is("sidewalk", "right") { + "right" + } else { + "both" + } + .to_string(), + Choice::strings(vec!["both", "none", "left", "right"]), + ), + ]), + Widget::row(vec![ + "parking".draw_text(ctx).margin_right(20), + Widget::dropdown( + ctx, + "parking", + // TODO Not all possibilities represented here; very simplified. + if road.osm_tags.is("parking:lane:both", "parallel") { + "both" + } else if road + .osm_tags + .is_any("parking:lane:both", vec!["no_parking", "no_stopping"]) + { + "none" + } else if road.osm_tags.is("parking:lane:left", "parallel") { + "left" + } else if road.osm_tags.is("parking:lane:right", "parallel") { + "right" + } else { + "none" + } + .to_string(), + Choice::strings(vec!["both", "none", "left", "right"]), + ), + ]), + ]); + + let col = vec![ + Widget::row(vec![ + Line("Editing road").small_heading().draw(ctx), + ctx.style().btn_close_widget(ctx), + ]), + Widget::row(vec![info, controls]), + ctx.style() + .btn_solid_dark_text("Apply") + .hotkey(Key::Enter) + .build_def(ctx), + ]; + let panel = Panel::new(Widget::col(col)) + .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) + .build(ctx); + SimpleState::new(panel, Box::new(EditRoad { r })) + } +} + +impl SimpleState for EditRoad { + fn on_click( + &mut self, + ctx: &mut EventCtx, + app: &mut App, + x: &str, + panel: &Panel, + ) -> Transition { + match x { + "close" => Transition::Pop, + "Apply" => { + app.model.road_deleted(self.r); + + let road = app.model.map.roads.get_mut(&self.r).unwrap(); + + road.osm_tags.remove("lanes"); + road.osm_tags.remove("oneway"); + let fwd = panel.spinner("lanes:forward") as usize; + let back = panel.spinner("lanes:backward") as usize; + if back == 0 { + road.osm_tags.insert("oneway", "yes"); + road.osm_tags.insert("lanes", fwd.to_string()); + } else { + road.osm_tags.insert("lanes", (fwd + back).to_string()); + road.osm_tags.insert("lanes:forward", fwd.to_string()); + road.osm_tags.insert("lanes:backward", back.to_string()); + } + + road.osm_tags + .insert("sidewalk", panel.dropdown_value::("sidewalk")); + + road.osm_tags.remove("parking:lane:both"); + road.osm_tags.remove("parking:lane:left"); + road.osm_tags.remove("parking:lane:right"); + match panel.dropdown_value::("parking").as_ref() { + "both" => { + road.osm_tags.insert("parking:lane:both", "parallel"); + } + "none" => { + road.osm_tags.insert("parking:lane:both", "none"); + } + "left" => { + road.osm_tags.insert("parking:lane:left", "parallel"); + road.osm_tags.insert("parking:lane:right", "none"); + } + "right" => { + road.osm_tags.insert("parking:lane:left", "none"); + road.osm_tags.insert("parking:lane:right", "parallel"); + } + _ => unreachable!(), + } + + app.model.road_added(self.r, ctx); + Transition::Pop + } + _ => unreachable!(), + } + } + + fn other_event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition { + ctx.canvas_movement(); + Transition::Keep + } + + fn draw_baselayer(&self) -> DrawBaselayer { + DrawBaselayer::PreviousState + } +} diff --git a/map_editor/src/main.rs b/map_editor/src/main.rs index 0d39188a70..4276a28152 100644 --- a/map_editor/src/main.rs +++ b/map_editor/src/main.rs @@ -1,3 +1,7 @@ +//! The map_editor renders and lets you edit RawMaps, which are a format in between OSM and the +//! full Map. It's useful for debugging maps imported from OSM, and for drawing synthetic maps for +//! testing. + #[macro_use] extern crate log; @@ -10,9 +14,10 @@ use map_model::osm; use map_model::raw::OriginalRoad; use widgetry::{ Canvas, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome, - Panel, ScreenPt, SharedAppState, StyledButtons, Text, Transition, VerticalAlignment, Widget, + Panel, SharedAppState, State, StyledButtons, Text, Transition, VerticalAlignment, Widget, }; +mod edit; mod model; mod world; @@ -35,14 +40,13 @@ impl SharedAppState for App { } struct MainState { - state: State, + mode: Mode, panel: Panel, - popup: Option, last_id: Option, } -enum State { +enum Mode { Viewing, MovingIntersection(osm::NodeID), MovingBuilding(osm::OsmID), @@ -69,30 +73,52 @@ impl MainState { } let bounds = model.map.gps_bounds.to_bounds(); ctx.canvas.map_dims = (bounds.width(), bounds.height()); + + // TODO Make these dynamic! + let mut instructions = Text::new(); + instructions.add_appended(vec![ + Line("Press "), + Key::I.txt(ctx), + Line(" to create a new intersection"), + ]); + instructions.add(Line("Hover on an intersection, then...")); + instructions.add_appended(vec![ + Line("- Press "), + Key::R.txt(ctx), + Line(" to start/end a new road"), + ]); + instructions.add_appended(vec![ + Line("- Hold "), + Key::LeftControl.txt(ctx), + Line(" to move it"), + ]); + instructions.add_appended(vec![ + Line("Press "), + Key::Backspace.txt(ctx), + Line(" to delete something"), + ]); + ( App { model }, MainState { - state: State::Viewing, + mode: Mode::Viewing, panel: Panel::new(Widget::col(vec![ - Line("Map Editor").small_heading().draw(ctx), - Text::new().draw(ctx).named("current info"), + Widget::row(vec![ + Line("Map Editor").small_heading().draw(ctx), + ctx.style().btn_close_widget(ctx), + ]), + instructions.draw(ctx), Widget::col(vec![ ctx.style() - .btn_outline_light_text("quit") - .hotkey(Key::Escape) - .build_def(ctx), - ctx.style() - .btn_outline_light_text("export to OSM") + .btn_solid_dark_text("export to OSM") .build_def(ctx), ctx.style() .btn_outline_light_text("preview all intersections") - .hotkey(Key::G) .build_def(ctx), ]), ])) .aligned(HorizontalAlignment::Right, VerticalAlignment::Top) .build(ctx), - popup: None, last_id: None, }, @@ -100,7 +126,7 @@ impl MainState { } } -impl widgetry::State for MainState { +impl State for MainState { fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { ctx.canvas_movement(); if ctx.redo_mouseover() { @@ -117,8 +143,8 @@ impl widgetry::State for MainState { } } - match self.state { - State::Viewing => { + match self.mode { + Mode::Viewing => { { let before = match self.last_id { Some(ID::Road(r)) | Some(ID::RoadPoint(r, _)) => Some(r), @@ -142,20 +168,20 @@ impl widgetry::State for MainState { match app.model.world.get_selection() { Some(ID::Intersection(i)) => { if ctx.input.pressed(Key::LeftControl) { - self.state = State::MovingIntersection(i); + self.mode = Mode::MovingIntersection(i); } else if ctx.input.pressed(Key::R) { - self.state = State::CreatingRoad(i); + self.mode = Mode::CreatingRoad(i); } else if ctx.input.pressed(Key::Backspace) { app.model.delete_i(i); app.model.world.handle_mouseover(ctx); } else if !app.model.intersection_geom && ctx.input.pressed(Key::P) { let draw = preview_intersection(i, &app.model, ctx); - self.state = State::PreviewIntersection(draw); + self.mode = Mode::PreviewIntersection(draw); } } Some(ID::Building(b)) => { if ctx.input.pressed(Key::LeftControl) { - self.state = State::MovingBuilding(b); + self.mode = Mode::MovingBuilding(b); } else if ctx.input.pressed(Key::Backspace) { app.model.delete_b(b); app.model.world.handle_mouseover(ctx); @@ -174,11 +200,13 @@ impl widgetry::State for MainState { } else if ctx.input.pressed(Key::M) { app.model.merge_r(r, ctx); app.model.world.handle_mouseover(ctx); + } else if ctx.normal_left_click() { + return Transition::Push(edit::EditRoad::new(ctx, app, r)); } } Some(ID::RoadPoint(r, idx)) => { if ctx.input.pressed(Key::LeftControl) { - self.state = State::MovingRoadPoint(r, idx); + self.mode = Mode::MovingRoadPoint(r, idx); } else if ctx.input.pressed(Key::Backspace) { app.model.delete_r_pt(r, idx, ctx); app.model.world.handle_mouseover(ctx); @@ -187,17 +215,16 @@ impl widgetry::State for MainState { None => { match self.panel.event(ctx) { Outcome::Clicked(x) => match x.as_ref() { - "quit" => { + "close" => { return Transition::Pop; } "export to OSM" => { - // TODO Only do this for synthetic maps app.model.export_to_osm(); } "preview all intersections" => { if !app.model.intersection_geom { let draw = preview_all_intersections(&app.model, ctx); - self.state = State::PreviewIntersection(draw); + self.mode = Mode::PreviewIntersection(draw); } } _ => unreachable!(), @@ -222,59 +249,50 @@ impl widgetry::State for MainState { } } } - State::MovingIntersection(id) => { + Mode::MovingIntersection(id) => { if let Some(pt) = cursor { app.model.move_i(id, pt, ctx); if ctx.input.key_released(Key::LeftControl) { - self.state = State::Viewing; + self.mode = Mode::Viewing; } } } - State::MovingBuilding(id) => { + Mode::MovingBuilding(id) => { if let Some(pt) = cursor { app.model.move_b(id, pt, ctx); if ctx.input.key_released(Key::LeftControl) { - self.state = State::Viewing; + self.mode = Mode::Viewing; } } } - State::MovingRoadPoint(r, idx) => { + Mode::MovingRoadPoint(r, idx) => { if let Some(pt) = cursor { app.model.move_r_pt(r, idx, pt, ctx); if ctx.input.key_released(Key::LeftControl) { - self.state = State::Viewing; + self.mode = Mode::Viewing; } } } - State::CreatingRoad(i1) => { + Mode::CreatingRoad(i1) => { if ctx.input.pressed(Key::Escape) { - self.state = State::Viewing; + self.mode = Mode::Viewing; app.model.world.handle_mouseover(ctx); } else if let Some(ID::Intersection(i2)) = app.model.world.get_selection() { if i1 != i2 && ctx.input.pressed(Key::R) { app.model.create_r(i1, i2, ctx); - self.state = State::Viewing; + self.mode = Mode::Viewing; app.model.world.handle_mouseover(ctx); } } } - State::PreviewIntersection(_) => { + Mode::PreviewIntersection(_) => { if ctx.input.pressed(Key::P) { - self.state = State::Viewing; + self.mode = Mode::Viewing; app.model.world.handle_mouseover(ctx); } } } - self.popup = None; - if ctx.is_key_down(Key::LeftAlt) { - if let Some(id) = app.model.world.get_selection() { - let txt = app.model.describe_obj(id); - // TODO We used to display actions and hotkeys here - self.popup = Some(ctx.upload(txt.render_autocropped(ctx))); - } - } - self.last_id = app.model.world.get_selection(); Transition::Keep @@ -291,27 +309,27 @@ impl widgetry::State for MainState { Color::rgb(242, 239, 233), app.model.map.boundary_polygon.clone(), ); - match self.state { - State::PreviewIntersection(_) => app.model.world.draw(g, |id| match id { + match self.mode { + Mode::PreviewIntersection(_) => app.model.world.draw(g, |id| match id { ID::Intersection(_) => false, _ => true, }), _ => app.model.world.draw(g, |_| true), } - match self.state { - State::CreatingRoad(i1) => { + match self.mode { + Mode::CreatingRoad(i1) => { if let Some(cursor) = g.get_cursor_in_map_space() { if let Some(l) = Line::new(app.model.map.intersections[&i1].point, cursor) { g.draw_polygon(Color::GREEN, l.make_polygons(Distance::meters(5.0))); } } } - State::Viewing - | State::MovingIntersection(_) - | State::MovingBuilding(_) - | State::MovingRoadPoint(_, _) => {} - State::PreviewIntersection(ref draw) => { + Mode::Viewing + | Mode::MovingIntersection(_) + | Mode::MovingBuilding(_) + | Mode::MovingRoadPoint(_, _) => {} + Mode::PreviewIntersection(ref draw) => { g.redraw(draw); if g.is_key_down(Key::RightAlt) { @@ -324,9 +342,6 @@ impl widgetry::State for MainState { }; self.panel.draw(g); - if let Some(ref popup) = self.popup { - g.redraw_at(ScreenPt::new(0.0, 0.0), popup); - } } } diff --git a/map_editor/src/model.rs b/map_editor/src/model.rs index d9601fdf7c..13f750920c 100644 --- a/map_editor/src/model.rs +++ b/map_editor/src/model.rs @@ -3,12 +3,10 @@ use std::io::Write; use abstio::{CityName, MapName}; use abstutil::{Tags, Timer}; -use geom::{ - Bounds, Circle, Distance, FindClosest, GPSBounds, HashablePt2D, LonLat, PolyLine, Polygon, Pt2D, -}; +use geom::{Bounds, Circle, Distance, FindClosest, GPSBounds, HashablePt2D, LonLat, Polygon, Pt2D}; use map_model::raw::{OriginalRoad, RawBuilding, RawIntersection, RawMap, RawRoad}; use map_model::{osm, IntersectionType}; -use widgetry::{Color, EventCtx, Line, Text}; +use widgetry::{Color, EventCtx}; use crate::world::{Object, ObjectID, World}; @@ -135,61 +133,6 @@ impl Model { } bounds } - - pub fn describe_obj(&self, id: ID) -> Text { - let mut txt = Text::new().with_bg(); - match id { - ID::Building(b) => { - txt.add_highlighted(Line(b.to_string()), Color::BLUE); - for (k, v) in self.map.buildings[&b].osm_tags.inner() { - txt.add_appended(vec![ - Line(k).fg(Color::RED), - Line(" = "), - Line(v).fg(Color::CYAN), - ]); - } - } - ID::Intersection(i) => { - txt.add_highlighted(Line(i.to_string()), Color::BLUE); - for r in self.map.roads_per_intersection(i) { - txt.add(Line(format!("- {}", r))); - } - } - ID::Road(r) => { - txt.add_highlighted(Line(r.to_string()), Color::BLUE); - let road = &self.map.roads[&r]; - - if let Some(name) = road.osm_tags.get(osm::NAME) { - txt.add(Line(name)); - } else if let Some(name) = road.osm_tags.get("ref") { - txt.add(Line(name)); - } else { - txt.add(Line("some road")); - } - - for (k, v) in road.osm_tags.inner() { - txt.add_appended(vec![ - Line(k).fg(Color::RED), - Line(" = "), - Line(v).fg(Color::CYAN), - ]); - } - - // (MAX_CAR_LENGTH + sim::FOLLOWING_DISTANCE) from sim, but without the dependency - txt.add(Line(format!( - "Can fit ~{} cars", - (PolyLine::must_new(road.center_points.clone()).length() - / (Distance::meters(6.5 + 1.0))) - .floor() as usize - ))); - } - ID::RoadPoint(r, idx) => { - txt.add_highlighted(Line(format!("Point {}", idx)), Color::BLUE); - txt.add(Line(format!("of {}", r))); - } - } - txt - } } // Intersections @@ -250,11 +193,11 @@ impl Model { // Roads impl Model { - fn road_added(&mut self, id: OriginalRoad, ctx: &EventCtx) { + pub fn road_added(&mut self, id: OriginalRoad, ctx: &EventCtx) { self.world.add(ctx, self.road_object(id)); } - fn road_deleted(&mut self, id: OriginalRoad) { + pub fn road_deleted(&mut self, id: OriginalRoad) { self.world.delete(ID::Road(id)); }