mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-27 08:24:15 +03:00
use slider to browse hints
This commit is contained in:
parent
efb6954a97
commit
ecaa8bea53
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user