use slider to browse hints

This commit is contained in:
Dustin Carlino 2019-06-14 16:03:24 -07:00
parent efb6954a97
commit ecaa8bea53
2 changed files with 252 additions and 144 deletions

View File

@ -1,7 +1,10 @@
use abstutil::Timer; use abstutil::Timer;
use ezgui::{hotkey, Color, EventCtx, EventLoopMode, GfxCtx, Key, ModalMenu, Text, GUI}; use ezgui::{
use geom::{Distance, Polygon}; hotkey, Color, EventCtx, EventLoopMode, GfxCtx, Key, ModalMenu, Text, WarpingItemSlider, GUI,
};
use geom::{Circle, Distance, PolyLine, Polygon, Pt2D};
use map_model::raw_data::{Hint, Hints, InitialMap, Map, StableIntersectionID, StableRoadID}; use map_model::raw_data::{Hint, Hints, InitialMap, Map, StableIntersectionID, StableRoadID};
use map_model::LANE_THICKNESS;
use std::collections::HashSet; use std::collections::HashSet;
use std::{env, process}; use std::{env, process};
use viewer::World; use viewer::World;
@ -10,14 +13,41 @@ use viewer::World;
const MIN_ROAD_LENGTH: Distance = Distance::const_meters(13.0); const MIN_ROAD_LENGTH: Distance = Distance::const_meters(13.0);
struct UI { struct UI {
menu: ModalMenu,
world: World<ID>, world: World<ID>,
data: InitialMap, data: InitialMap,
raw: Map, raw: Map,
hints: Hints, hints: Hints,
// TODO Or, if these are common things, the World could also hold this state. state: State,
selected: Option<ID>, }
osd: Text,
enum State {
Main {
menu: ModalMenu,
// TODO Or, if these are common things, the World could also hold this state.
selected: Option<ID>,
osd: Text,
},
BrowsingHints(WarpingItemSlider<Hint>),
}
impl State {
fn main(ctx: &mut EventCtx) -> State {
State::Main {
menu: ModalMenu::new(
"Fix Map Geometry",
vec![
(hotkey(Key::Escape), "quit"),
(hotkey(Key::S), "save"),
(hotkey(Key::R), "reset hints"),
(hotkey(Key::U), "undo last hint"),
(hotkey(Key::B), "browse hints"),
],
ctx,
),
selected: None,
osd: Text::new(),
}
}
} }
impl UI { impl UI {
@ -39,22 +69,11 @@ impl UI {
let world = initial_map_to_world(&data, ctx); let world = initial_map_to_world(&data, ctx);
UI { UI {
menu: ModalMenu::new(
"Fix Map Geometry",
vec![
(hotkey(Key::Escape), "quit"),
(hotkey(Key::S), "save"),
(hotkey(Key::R), "reset hints"),
(hotkey(Key::U), "undo last hint"),
],
ctx,
),
world, world,
raw,
data, data,
raw,
hints, hints,
selected: None, state: State::main(ctx),
osd: Text::new(),
} }
}) })
} }
@ -62,134 +81,183 @@ impl UI {
impl GUI for UI { impl GUI for UI {
fn event(&mut self, ctx: &mut EventCtx) -> EventLoopMode { fn event(&mut self, ctx: &mut EventCtx) -> EventLoopMode {
{ match self.state {
let len = self.hints.hints.len(); State::Main {
let mut txt = Text::prompt("Fix Map Geometry"); ref mut menu,
txt.push(format!("[cyan:{}] hints", len)); ref mut selected,
for i in (1..=5).rev() { ref mut osd,
if len >= i { } => {
txt.add_line(match self.hints.hints[len - i] { {
Hint::MergeRoad(_) => "MergeRoad(...)".to_string(), let len = self.hints.hints.len();
Hint::DeleteRoad(_) => "DeleteRoad(...)".to_string(), let mut txt = Text::prompt("Fix Map Geometry");
Hint::MergeDegenerateIntersection(_) => { txt.push(format!("[cyan:{}] hints", len));
"MergeDegenerateIntersection(...)".to_string() for i in (1..=5).rev() {
} if len >= i {
}); txt.add_line(describe(&self.hints.hints[len - i]));
} else {
txt.add_line("...".to_string());
}
}
if let Some(ID::Road(r)) = self.selected {
txt.push(format!(
"[red:{}] is {} long",
r,
self.data.roads[&r].trimmed_center_pts.length()
));
for (k, v) in &self.raw.roads[&r].osm_tags {
txt.push(format!("[cyan:{}] = [red:{}]", k, v));
}
}
if let Some(ID::Intersection(i)) = self.selected {
txt.push(format!("[red:{}] OSM tag diffs:", i));
let roads = &self.data.intersections[&i].roads;
if roads.len() == 2 {
let mut iter = roads.iter();
let r1_tags = &self.raw.roads[iter.next().unwrap()].osm_tags;
let r2_tags = &self.raw.roads[iter.next().unwrap()].osm_tags;
for (k, v1) in r1_tags {
if let Some(v2) = r2_tags.get(k) {
if v1 != v2 {
txt.push(format!("[cyan:{}] = [red:{}] / [red:{}]", k, v1, v2));
}
} else { } else {
txt.push(format!("[cyan:{}] = [red:{}] / MISSING", k, v1)); txt.add_line("...".to_string());
} }
} }
for (k, v2) in r2_tags { if let Some(ID::Road(r)) = selected {
if !r1_tags.contains_key(k) { txt.push(format!(
txt.push(format!("[cyan:{}] = MISSING / [red:{}] ", k, v2)); "[red:{}] is {} long",
r,
self.data.roads[&r].trimmed_center_pts.length()
));
for (k, v) in &self.raw.roads[&r].osm_tags {
txt.push(format!("[cyan:{}] = [red:{}]", k, v));
} }
} }
if let Some(ID::Intersection(i)) = selected {
txt.push(format!("[red:{}] OSM tag diffs:", i));
let roads = &self.data.intersections[&i].roads;
if roads.len() == 2 {
let mut iter = roads.iter();
let r1_tags = &self.raw.roads[iter.next().unwrap()].osm_tags;
let r2_tags = &self.raw.roads[iter.next().unwrap()].osm_tags;
for (k, v1) in r1_tags {
if let Some(v2) = r2_tags.get(k) {
if v1 != v2 {
txt.push(format!(
"[cyan:{}] = [red:{}] / [red:{}]",
k, v1, v2
));
}
} else {
txt.push(format!("[cyan:{}] = [red:{}] / MISSING", k, v1));
}
}
for (k, v2) in r2_tags {
if !r1_tags.contains_key(k) {
txt.push(format!("[cyan:{}] = MISSING / [red:{}] ", k, v2));
}
}
}
}
menu.handle_event(ctx, Some(txt));
}
ctx.canvas.handle_event(ctx.input);
if ctx.redo_mouseover() {
*selected = self.world.mouseover_something(ctx, &HashSet::new());
}
if menu.action("quit") {
process::exit(0);
}
if !self.hints.hints.is_empty() {
if menu.action("save") {
abstutil::write_json("../data/hints.json", &self.hints).unwrap();
println!("Saved hints.json");
}
if menu.action("browse hints") {
self.state = State::BrowsingHints(WarpingItemSlider::new(
// TODO bleh
self.hints
.hints
.iter()
.filter_map(|h| {
let gps_pt = match h {
Hint::MergeRoad(r) | Hint::DeleteRoad(r) => {
self.raw.roads[&self.raw.find_r(*r)?].points[0]
}
Hint::MergeDegenerateIntersection(i) => {
self.raw.intersections[&self.raw.find_i(*i)?].point
}
};
let pt = Pt2D::from_gps(gps_pt, &self.raw.gps_bounds)?;
Some((pt, h.clone()))
})
.collect(),
"Hints Browser",
"hint",
ctx,
));
return EventLoopMode::InputOnly;
}
let recalc = if menu.action("undo last hint") {
self.hints.hints.pop();
true
} else if menu.action("reset hints") {
self.hints.hints.clear();
true
} else {
false
};
if recalc {
*selected = None;
ctx.loading_screen("recalculate map from hints", |ctx, mut timer| {
let gps_bounds = &self.raw.gps_bounds;
self.data = InitialMap::new(
self.data.name.clone(),
&self.raw,
gps_bounds,
&gps_bounds.to_bounds(),
&mut timer,
);
self.data.apply_hints(&self.hints, &self.raw, &mut timer);
self.world = initial_map_to_world(&self.data, ctx);
});
return EventLoopMode::InputOnly;
}
} }
if let Some(ID::Road(r)) = selected {
if ctx.input.key_pressed(Key::M, "merge") {
self.hints
.hints
.push(Hint::MergeRoad(self.raw.roads[&r].orig_id()));
self.data.merge_road(*r, &mut Timer::new("merge road"));
self.world = initial_map_to_world(&self.data, ctx);
*selected = None;
} else if ctx.input.key_pressed(Key::D, "delete") {
self.hints
.hints
.push(Hint::DeleteRoad(self.raw.roads[r].orig_id()));
self.data.delete_road(*r, &mut Timer::new("delete road"));
self.world = initial_map_to_world(&self.data, ctx);
*selected = None;
}
}
if let Some(ID::Intersection(i)) = selected {
if self.data.intersections[i].roads.len() == 2
&& ctx.input.key_pressed(Key::M, "merge")
{
self.hints.hints.push(Hint::MergeDegenerateIntersection(
self.raw.intersections[i].orig_id(),
));
self.data.merge_degenerate_intersection(
*i,
&mut Timer::new("merge intersection"),
);
self.world = initial_map_to_world(&self.data, ctx);
*selected = None;
}
}
*osd = Text::new();
ctx.input.populate_osd(osd);
EventLoopMode::InputOnly
} }
self.menu.handle_event(ctx, Some(txt)); State::BrowsingHints(ref mut slider) => {
} ctx.canvas.handle_event(ctx.input);
ctx.canvas.handle_event(ctx.input); let mut txt = Text::prompt("Hints Browser");
{
if ctx.redo_mouseover() { let (idx, hint) = slider.get();
self.selected = self.world.mouseover_something(ctx, &HashSet::new()); txt.add_line(format!("Hint {}/{}", idx + 1, slider.len()));
} txt.add_line(describe(hint));
}
if self.menu.action("quit") { if let Some((evmode, _)) = slider.event(ctx, Some(txt)) {
process::exit(0); evmode
} } else {
if !self.hints.hints.is_empty() { self.state = State::main(ctx);
if self.menu.action("save") { EventLoopMode::InputOnly
abstutil::write_json("../data/hints.json", &self.hints).unwrap(); }
println!("Saved hints.json");
}
let recalc = if self.menu.action("undo last hint") {
self.hints.hints.pop();
true
} else if self.menu.action("reset hints") {
self.hints.hints.clear();
true
} else {
false
};
if recalc {
ctx.loading_screen("recalculate map from hints", |ctx, mut timer| {
let gps_bounds = &self.raw.gps_bounds;
self.data = InitialMap::new(
self.data.name.clone(),
&self.raw,
gps_bounds,
&gps_bounds.to_bounds(),
&mut timer,
);
self.data.apply_hints(&self.hints, &self.raw, &mut timer);
self.world = initial_map_to_world(&self.data, ctx);
self.selected = None;
});
} }
} }
if let Some(ID::Road(r)) = self.selected {
if ctx.input.key_pressed(Key::M, "merge") {
self.hints
.hints
.push(Hint::MergeRoad(self.raw.roads[&r].orig_id()));
self.data.merge_road(r, &mut Timer::new("merge road"));
self.world = initial_map_to_world(&self.data, ctx);
self.selected = None;
} else if ctx.input.key_pressed(Key::D, "delete") {
self.hints
.hints
.push(Hint::DeleteRoad(self.raw.roads[&r].orig_id()));
self.data.delete_road(r, &mut Timer::new("delete road"));
self.world = initial_map_to_world(&self.data, ctx);
self.selected = None;
}
}
if let Some(ID::Intersection(i)) = self.selected {
if self.data.intersections[&i].roads.len() == 2
&& ctx.input.key_pressed(Key::M, "merge")
{
self.hints.hints.push(Hint::MergeDegenerateIntersection(
self.raw.intersections[&i].orig_id(),
));
self.data
.merge_degenerate_intersection(i, &mut Timer::new("merge intersection"));
self.world = initial_map_to_world(&self.data, ctx);
self.selected = None;
}
}
self.osd = Text::new();
ctx.input.populate_osd(&mut self.osd);
EventLoopMode::InputOnly
} }
fn draw(&self, g: &mut GfxCtx) { fn draw(&self, g: &mut GfxCtx) {
@ -197,12 +265,44 @@ impl GUI for UI {
self.world.draw(g, &HashSet::new()); self.world.draw(g, &HashSet::new());
if let Some(id) = self.selected { match self.state {
self.world.draw_selected(g, id); State::Main {
} ref selected,
ref menu,
ref osd,
} => {
if let Some(id) = selected {
self.world.draw_selected(g, *id);
}
self.menu.draw(g); menu.draw(g);
g.draw_blocking_text(&self.osd, ezgui::BOTTOM_LEFT); g.draw_blocking_text(osd, ezgui::BOTTOM_LEFT);
}
State::BrowsingHints(ref slider) => {
let poly =
match slider.get().1 {
Hint::MergeRoad(r) | Hint::DeleteRoad(r) => {
PolyLine::new(self.raw.gps_bounds.must_convert(
&self.raw.roads[&self.raw.find_r(*r).unwrap()].points,
))
// Just make up a width
.make_polygons(4.0 * LANE_THICKNESS)
}
Hint::MergeDegenerateIntersection(i) => Circle::new(
Pt2D::from_gps(
self.raw.intersections[&self.raw.find_i(*i).unwrap()].point,
&self.raw.gps_bounds,
)
.unwrap(),
Distance::meters(10.0),
)
.to_polygon(),
};
g.draw_polygon(Color::PURPLE.alpha(0.7), &poly);
slider.draw(g);
}
}
} }
} }
@ -270,3 +370,11 @@ fn initial_map_to_world(data: &InitialMap, ctx: &mut EventCtx) -> World<ID> {
w w
} }
fn describe(hint: &Hint) -> String {
match hint {
Hint::MergeRoad(_) => "MergeRoad(...)".to_string(),
Hint::DeleteRoad(_) => "DeleteRoad(...)".to_string(),
Hint::MergeDegenerateIntersection(_) => "MergeDegenerateIntersection(...)".to_string(),
}
}

View File

@ -280,7 +280,7 @@ impl Hints {
} }
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Hint { pub enum Hint {
MergeRoad(raw_data::OriginalRoad), MergeRoad(raw_data::OriginalRoad),
DeleteRoad(raw_data::OriginalRoad), DeleteRoad(raw_data::OriginalRoad),