Restore the map_editor to some of its former glory:

- start showing partial instructions again
- remove old debug popup
- start a new road editor
This commit is contained in:
Dustin Carlino 2021-02-19 16:24:13 -08:00
parent 5139a88a66
commit cce43daecc
4 changed files with 256 additions and 119 deletions

View File

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

179
map_editor/src/edit.rs Normal file
View File

@ -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<dyn State<App>> {
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::<isize>().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::<isize>().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<App> for EditRoad {
fn on_click(
&mut self,
ctx: &mut EventCtx,
app: &mut App,
x: &str,
panel: &Panel,
) -> Transition<App> {
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::<String, &str>("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::<String, &str>("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<App> {
ctx.canvas_movement();
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::PreviousState
}
}

View File

@ -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<Drawable>,
last_id: Option<ID>,
}
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<App> for MainState {
impl State<App> for MainState {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
ctx.canvas_movement();
if ctx.redo_mouseover() {
@ -117,8 +143,8 @@ impl widgetry::State<App> 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<App> 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<App> 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<App> 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<App> 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<App> 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<App> for MainState {
};
self.panel.draw(g);
if let Some(ref popup) = self.popup {
g.redraw_at(ScreenPt::new(0.0, 0.0), popup);
}
}
}

View File

@ -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));
}