grouping UI state that's bound to a certain map+edits

This commit is contained in:
Dustin Carlino 2018-10-08 12:55:42 -07:00
parent af2d49debb
commit 2aa47bbd51
4 changed files with 271 additions and 183 deletions

View File

@ -278,6 +278,16 @@ Alright, I think this is the sequence of things to do:
2) make it possible to completely reload UI and everything from scratch, from a plugin. rationale: it'd be nice to switch maps from inside the editor anyway. not necessary, but useful.
3) make road edits propogate correctly, and somehow have a strategy for ensuring nothing is forgotten. impl today is VERY incomplete.
Thinking about this again now that we need two copies of everything to be alive at a time and switch between them...
- very tied together: map, control map, draw map, sim
- current selection is UI state that has to refresh when changing maps
- which plugins have state tied to the map?
- have a method on UI to switch map+edits? no, dont want to reload all this stuff every time...
- bundle that state together, including the plugins!
- make the existing 'load new edits' thing use this new mechanism
- then go back to managing a second sim...
## Rendering a map differently
For "Project Halloween", I want to draw the map model in a very different
@ -298,3 +308,48 @@ So, try adding the quadtree for roads and buildings (diff quadtrees or one
unified? hmm) and see what looks common. Remember we could use quadtrees in map
model construction for building/sidewalk pruning, but there's the awkwardness
of quadtrees _kind of_ being a UI concept.
## Side-by-side
What should this feature do? Is it easier to watch two maps side-by-side moving
in lockstep, with the same camera? Or would a ghostly trace overlay on one map
be easier to understand? The use cases:
- Glancing at a vague overview of how the two runs are doing. Watching graphs
side-by-side might even be more useful here. But for a zoomed out view,
side-by-side with reasonably clear pockets of color (weather model style,
almost) seems nice.
- Detailed inspection of a fixed area. Side-by-side view with full detail seems
nice.
- Detailed inspection of a specific agent, following it. Side-by-side would
have to trace it in both canvases.
- Looking for differences... what are these? For a single agent, wanting to
know are they farther along their journey at some point in time? That could
be visualized nicely with a red or green thick route in front or behind them.
Maybe they're ahead of the baseline by some amount, or behind it. This could
use relative score or relative distance to goal or something. Would it trace
ahead by pure distance or by anticipated distance in a given time?
The side-by-side canvas seems incredibly difficult -- have to revamp everything
to dispatch mouse events, maybe synchronize cameras, other plugins
arbitrarily... No way.
Let's start with two concrete things:
- Start running an A/B test sets up a second optional simulation in the UI.
Some keys can toggle between showing one of the two, for now. Stepping will
step BOTH of them. Have to adjust the OSD and other sim control things.
Possibly even sim control should own the second sim?
- Q: how to prevent scenario instatiation or adding new agents while an
A/B test is in progress? need plugin groups / modes!
- road editing during an A/B test is _definitely_ not k
- argh!! wait, we need a different map, since we need different edits!
- that means also a different control map
- should the Sim own map and control_map to make it clear they're tied? I think so!
- practically all of the UI touches the map...
- wait wait draw map also needs to be duplicated then.
- we're back to the problem of loading map edits
- Make a visual trace abstraction to show something in front of or behind an
agent. It follows bends in the road, crosses intersections, etc. Could be
used for lookahead debugging right now, and this relative ghost comparison
thing next.

View File

@ -8,7 +8,7 @@ use objects::{Ctx, ID};
use piston::input::Key;
use plugins::Colorizer;
use render::DrawMap;
use sim::Sim;
use sim::Tick;
#[derive(Clone, Debug)]
pub enum TurnCyclerState {
@ -68,7 +68,7 @@ impl TurnCyclerState {
map: &Map,
draw_map: &DrawMap,
control_map: &ControlMap,
sim: &Sim,
time: Tick,
cs: &ColorScheme,
g: &mut GfxCtx,
) {
@ -92,7 +92,7 @@ impl TurnCyclerState {
}
TurnCyclerState::Intersection(id) => {
if let Some(signal) = control_map.traffic_signals.get(&id) {
let (cycle, _) = signal.current_cycle_and_remaining_time(sim.time.as_time());
let (cycle, _) = signal.current_cycle_and_remaining_time(time.as_time());
for t in &cycle.turns {
draw_map.get_t(*t).draw_full(g, cs.get(Colors::Turn));
}

View File

@ -35,16 +35,13 @@ pub struct DrawMap {
pub bus_stops: HashMap<BusStopID, DrawBusStop>,
pub areas: Vec<DrawArea>,
pub center_pt: Pt2D,
quadtree: QuadTree<ID>,
}
impl DrawMap {
// Also returns the center of the map in map-space
pub fn new(
map: &Map,
control_map: &ControlMap,
raw_extra_shapes: Vec<ExtraShape>,
) -> (DrawMap, Pt2D) {
pub fn new(map: &Map, control_map: &ControlMap, raw_extra_shapes: Vec<ExtraShape>) -> DrawMap {
let mut lanes: Vec<DrawLane> = Vec::new();
for l in map.all_lanes() {
lanes.push(DrawLane::new(l, map, control_map));
@ -120,7 +117,6 @@ impl DrawMap {
quadtree.insert_with_box(obj.get_id(), get_bbox(obj.get_bounds()));
}
(
DrawMap {
lanes,
intersections,
@ -131,10 +127,10 @@ impl DrawMap {
bus_stops,
areas,
center_pt: Pt2D::new(max_screen_pt.x() / 2.0, max_screen_pt.y() / 2.0),
quadtree,
},
Pt2D::new(max_screen_pt.x() / 2.0, max_screen_pt.y() / 2.0),
)
}
}
fn compute_turn_to_lane_offset(result: &mut HashMap<TurnID, usize>, l: &Lane, map: &Map) {

View File

@ -10,8 +10,7 @@ use ezgui::{
};
use flame;
use kml;
use map_model;
use map_model::IntersectionID;
use map_model::{IntersectionID, Map};
use objects::{Ctx, DEBUG_LAYERS, ID, ROOT_MENU};
use piston::input::Key;
use plugins::a_b_tests::ABTestManager;
@ -82,55 +81,16 @@ impl UIWrapper {
// Do this first, so anything logged by sim::load isn't lost.
let logs = DisplayLogs::new();
flame::start("setup");
let (map, control_map, sim) = sim::load(flags.clone(), Some(sim::Tick::from_seconds(30)));
let extra_shapes = if let Some(path) = kml.clone() {
kml::load(&path, &map.get_gps_bounds()).expect("Couldn't load extra KML shapes")
} else {
Vec::new()
};
flame::start("draw_map");
let (draw_map, center_pt) = DrawMap::new(&map, &control_map, extra_shapes);
flame::end("draw_map");
flame::end("setup");
flame::dump_stdout();
let steepness_viz = SteepnessVisualizer::new(&map);
let road_editor = RoadEditor::new(map.get_road_edits().clone());
let mut ui = UI {
// TODO organize this by section
map,
draw_map,
control_map,
sim,
steepness_viz,
road_editor,
sim_ctrl: SimController::new(),
primary: PerMapUI::new(flags, kml.clone()),
secondary: None,
layers: ToggleableLayers::new(),
current_selection: None,
hider: Hider::new(),
debug_objects: DebugObjectsState::new(),
search_state: SearchState::Empty,
warp: WarpState::Empty,
follow: FollowState::Empty,
show_route: ShowRouteState::Empty,
floodfiller: Floodfiller::new(),
osm_classifier: OsmClassifier::new(),
traffic_signal_editor: TrafficSignalEditor::new(),
stop_sign_editor: StopSignEditor::new(),
sim_ctrl: SimController::new(),
color_picker: ColorPicker::new(),
geom_validator: Validator::new(),
turn_cycler: TurnCyclerState::new(),
draw_neighborhoods: DrawNeighborhoodState::new(),
scenarios: ScenarioManager::new(),
edits_manager: EditsManager::new(flags),
ab_test_manager: ABTestManager::new(),
logs,
@ -141,7 +101,7 @@ impl UIWrapper {
};
match abstutil::read_json::<EditorState>("editor_state") {
Ok(ref state) if *ui.map.get_name() == state.map_name => {
Ok(ref state) if ui.primary.map.get_name().to_string() == state.map_name => {
info!("Loaded previous editor_state");
ui.canvas.cam_x = state.cam_x;
ui.canvas.cam_y = state.cam_y;
@ -149,7 +109,7 @@ impl UIWrapper {
}
_ => {
warn!("Couldn't load editor_state or it's for a different map, so just centering initial view");
ui.canvas.center_on_map_pt(center_pt);
ui.canvas.center_on_map_pt(ui.primary.draw_map.center_pt);
}
}
@ -173,117 +133,132 @@ impl UIWrapper {
changed
};
if layer_changed {
ctx.ui.current_selection = ctx.ui.mouseover_something();
ctx.ui.primary.current_selection = ctx.ui.mouseover_something();
true
} else {
false
}
}),
Box::new(|ctx| {
ctx.ui.traffic_signal_editor.event(
ctx.ui.primary.traffic_signal_editor.event(
ctx.input,
&ctx.ui.map,
&mut ctx.ui.control_map,
ctx.ui.current_selection,
&ctx.ui.primary.map,
&mut ctx.ui.primary.control_map,
ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui.stop_sign_editor.event(
ctx.ui.primary.stop_sign_editor.event(
ctx.input,
&ctx.ui.map,
&mut ctx.ui.control_map,
ctx.ui.current_selection,
&ctx.ui.primary.map,
&mut ctx.ui.primary.control_map,
ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui.road_editor.event(
ctx.ui.primary.road_editor.event(
ctx.input,
ctx.ui.current_selection,
&mut ctx.ui.map,
&mut ctx.ui.draw_map,
&ctx.ui.control_map,
&mut ctx.ui.sim,
ctx.ui.primary.current_selection,
&mut ctx.ui.primary.map,
&mut ctx.ui.primary.draw_map,
&ctx.ui.primary.control_map,
&mut ctx.ui.primary.sim,
)
}),
Box::new(|ctx| ctx.ui.search_state.event(ctx.input)),
Box::new(|ctx| {
ctx.ui.warp.event(
ctx.input,
&ctx.ui.map,
&ctx.ui.sim,
&ctx.ui.primary.map,
&ctx.ui.primary.sim,
&mut ctx.ui.canvas,
&mut ctx.ui.current_selection,
&mut ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui.follow.event(
ctx.ui.primary.follow.event(
ctx.input,
&ctx.ui.map,
&ctx.ui.sim,
&ctx.ui.primary.map,
&ctx.ui.primary.sim,
&mut ctx.ui.canvas,
ctx.ui.current_selection,
ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui
.show_route
.event(ctx.input, &ctx.ui.sim, ctx.ui.current_selection)
ctx.ui.primary.show_route.event(
ctx.input,
&ctx.ui.primary.sim,
ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui
.color_picker
.event(ctx.input, &mut ctx.ui.canvas, &mut ctx.ui.cs)
}),
Box::new(|ctx| ctx.ui.steepness_viz.event(ctx.input)),
Box::new(|ctx| ctx.ui.primary.steepness_viz.event(ctx.input)),
Box::new(|ctx| ctx.ui.osm_classifier.event(ctx.input)),
Box::new(|ctx| ctx.ui.hider.event(ctx.input, &mut ctx.ui.current_selection)),
Box::new(|ctx| {
ctx.ui.debug_objects.event(
ctx.ui.current_selection,
ctx.ui
.primary
.hider
.event(ctx.input, &mut ctx.ui.primary.current_selection)
}),
Box::new(|ctx| {
ctx.ui.primary.debug_objects.event(
ctx.ui.primary.current_selection,
ctx.input,
&ctx.ui.map,
&mut ctx.ui.sim,
&ctx.ui.control_map,
&ctx.ui.primary.map,
&mut ctx.ui.primary.sim,
&ctx.ui.primary.control_map,
)
}),
Box::new(|ctx| {
ctx.ui
.floodfiller
.event(&ctx.ui.map, ctx.input, ctx.ui.current_selection)
ctx.ui.primary.floodfiller.event(
&ctx.ui.primary.map,
ctx.input,
ctx.ui.primary.current_selection,
)
}),
Box::new(|ctx| {
ctx.ui.geom_validator.event(
ctx.ui.primary.geom_validator.event(
ctx.input,
&mut ctx.ui.canvas,
&ctx.ui.map,
&ctx.ui.draw_map,
&ctx.ui.primary.map,
&ctx.ui.primary.draw_map,
)
}),
Box::new(|ctx| {
ctx.ui
.primary
.turn_cycler
.event(ctx.input, ctx.ui.current_selection)
.event(ctx.input, ctx.ui.primary.current_selection)
}),
Box::new(|ctx| {
ctx.ui
.draw_neighborhoods
.event(ctx.input, &ctx.ui.canvas, &ctx.ui.map, ctx.osd)
}),
Box::new(|ctx| {
ctx.ui
.scenarios
.event(ctx.input, &ctx.ui.map, &mut ctx.ui.sim)
}),
Box::new(|ctx| {
ctx.ui.edits_manager.event(
ctx.ui.primary.draw_neighborhoods.event(
ctx.input,
&ctx.ui.map,
&ctx.ui.control_map,
&ctx.ui.road_editor,
&ctx.ui.canvas,
&ctx.ui.primary.map,
ctx.osd,
)
}),
Box::new(|ctx| {
ctx.ui.primary.scenarios.event(
ctx.input,
&ctx.ui.primary.map,
&mut ctx.ui.primary.sim,
)
}),
Box::new(|ctx| {
ctx.ui.primary.edits_manager.event(
ctx.input,
&ctx.ui.primary.map,
&ctx.ui.primary.control_map,
&ctx.ui.primary.road_editor,
ctx.new_flags,
)
}),
Box::new(|ctx| ctx.ui.ab_test_manager.event(ctx.input, &ctx.ui.map)),
Box::new(|ctx| ctx.ui.ab_test_manager.event(ctx.input, &ctx.ui.primary.map)),
Box::new(|ctx| ctx.ui.logs.event(ctx.input)),
],
@ -292,35 +267,92 @@ impl UIWrapper {
}
}
struct UI {
map: map_model::Map,
// All of the state that's bound to a specific map+edit has to live here.
// TODO How can we arrange the code so that we statically know that we don't pass anything from UI
// to something in PerMapUI?
struct PerMapUI {
map: Map,
draw_map: DrawMap,
control_map: ControlMap,
sim: Sim,
layers: ToggleableLayers,
current_selection: Option<ID>,
// Anything that holds onto any kind of ID has to live here!
hider: Hider,
debug_objects: DebugObjectsState,
search_state: SearchState,
warp: WarpState,
follow: FollowState,
show_route: ShowRouteState,
floodfiller: Floodfiller,
steepness_viz: SteepnessVisualizer,
osm_classifier: OsmClassifier,
traffic_signal_editor: TrafficSignalEditor,
stop_sign_editor: StopSignEditor,
road_editor: RoadEditor,
sim_ctrl: SimController,
color_picker: ColorPicker,
geom_validator: Validator,
turn_cycler: TurnCyclerState,
draw_neighborhoods: DrawNeighborhoodState,
scenarios: ScenarioManager,
edits_manager: EditsManager,
}
impl PerMapUI {
fn new(flags: SimFlags, kml: Option<String>) -> PerMapUI {
flame::start("setup");
let (map, control_map, sim) = sim::load(flags.clone(), Some(sim::Tick::from_seconds(30)));
let extra_shapes = if let Some(path) = kml.clone() {
kml::load(&path, &map.get_gps_bounds()).expect("Couldn't load extra KML shapes")
} else {
Vec::new()
};
flame::start("draw_map");
let draw_map = DrawMap::new(&map, &control_map, extra_shapes);
flame::end("draw_map");
flame::end("setup");
flame::dump_stdout();
let steepness_viz = SteepnessVisualizer::new(&map);
let road_editor = RoadEditor::new(map.get_road_edits().clone());
PerMapUI {
map,
draw_map,
control_map,
sim,
current_selection: None,
hider: Hider::new(),
debug_objects: DebugObjectsState::new(),
follow: FollowState::Empty,
show_route: ShowRouteState::Empty,
floodfiller: Floodfiller::new(),
steepness_viz,
traffic_signal_editor: TrafficSignalEditor::new(),
stop_sign_editor: StopSignEditor::new(),
road_editor,
geom_validator: Validator::new(),
turn_cycler: TurnCyclerState::new(),
draw_neighborhoods: DrawNeighborhoodState::new(),
scenarios: ScenarioManager::new(),
edits_manager: EditsManager::new(flags),
}
}
}
struct UI {
primary: PerMapUI,
// When running an A/B test, this is populated too.
secondary: Option<PerMapUI>,
layers: ToggleableLayers,
search_state: SearchState,
warp: WarpState,
osm_classifier: OsmClassifier,
// TODO This one has per-sim state right now, but soon will understand how to handle two sims.
sim_ctrl: SimController,
color_picker: ColorPicker,
ab_test_manager: ABTestManager,
logs: DisplayLogs,
@ -347,11 +379,11 @@ impl UI {
fn mouseover_something(&self) -> Option<ID> {
let pt = self.canvas.get_cursor_in_map_space();
let (statics, dynamics) = self.draw_map.get_objects_onscreen(
let (statics, dynamics) = self.primary.draw_map.get_objects_onscreen(
self.canvas.get_screen_bbox(),
&self.hider,
&self.map,
&self.sim,
&self.primary.hider,
&self.primary.map,
&self.primary.sim,
&self.layers,
self,
);
@ -387,13 +419,13 @@ impl UI {
// Always handle mouseover
if old_zoom >= MIN_ZOOM_FOR_MOUSEOVER && new_zoom < MIN_ZOOM_FOR_MOUSEOVER {
self.current_selection = None;
self.primary.current_selection = None;
}
if !self.canvas.is_dragging()
&& input.get_moved_mouse().is_some()
&& new_zoom >= MIN_ZOOM_FOR_MOUSEOVER
{
self.current_selection = self.mouseover_something();
self.primary.current_selection = self.mouseover_something();
}
// TODO Normally we'd return InputOnly here if there was an active plugin, but actually, we
@ -426,7 +458,7 @@ impl UI {
if input.unimportant_key_pressed(Key::Escape, ROOT_MENU, "quit") {
let state = EditorState {
map_name: self.map.get_name().clone(),
map_name: self.primary.map.get_name().clone(),
cam_x: self.canvas.cam_x,
cam_y: self.canvas.cam_y,
cam_zoom: self.canvas.cam_zoom,
@ -441,10 +473,10 @@ impl UI {
// Sim controller plugin is kind of always active? If nothing else ran, let it use keys.
let result = self.sim_ctrl.event(
&mut input,
&self.map,
&self.control_map,
&mut self.sim,
self.current_selection,
&self.primary.map,
&self.primary.control_map,
&mut self.primary.sim,
self.primary.current_selection,
osd,
);
input.populate_osd(osd);
@ -454,11 +486,11 @@ impl UI {
fn draw(&self, g: &mut GfxCtx, osd: Text) {
g.clear(self.cs.get(Colors::Background));
let (statics, dynamics) = self.draw_map.get_objects_onscreen(
let (statics, dynamics) = self.primary.draw_map.get_objects_onscreen(
self.canvas.get_screen_bbox(),
&self.hider,
&self.map,
&self.sim,
&self.primary.hider,
&self.primary.map,
&self.primary.sim,
&self.layers,
self,
);
@ -473,10 +505,10 @@ impl UI {
opts,
Ctx {
cs: &self.cs,
map: &self.map,
control_map: &self.control_map,
map: &self.primary.map,
control_map: &self.primary.control_map,
canvas: &self.canvas,
sim: &self.sim,
sim: &self.primary.sim,
},
);
}
@ -491,29 +523,34 @@ impl UI {
opts,
Ctx {
cs: &self.cs,
map: &self.map,
control_map: &self.control_map,
map: &self.primary.map,
control_map: &self.primary.control_map,
canvas: &self.canvas,
sim: &self.sim,
sim: &self.primary.sim,
},
);
}
// TODO Only if active?
self.turn_cycler.draw(
&self.map,
&self.draw_map,
&self.control_map,
&self.sim,
self.primary.turn_cycler.draw(
&self.primary.map,
&self.primary.draw_map,
&self.primary.control_map,
self.primary.sim.time,
&self.cs,
g,
);
self.debug_objects
.draw(&self.map, &self.canvas, &self.draw_map, &self.sim, g);
self.primary.debug_objects.draw(
&self.primary.map,
&self.canvas,
&self.primary.draw_map,
&self.primary.sim,
g,
);
self.color_picker.draw(&self.canvas, g);
self.draw_neighborhoods.draw(g, &self.canvas);
self.scenarios.draw(g, &self.canvas);
self.edits_manager.draw(g, &self.canvas);
self.primary.draw_neighborhoods.draw(g, &self.canvas);
self.primary.scenarios.draw(g, &self.canvas);
self.primary.edits_manager.draw(g, &self.canvas);
self.ab_test_manager.draw(g, &self.canvas);
self.logs.draw(g, &self.canvas);
self.search_state.draw(g, &self.canvas);
@ -524,7 +561,7 @@ impl UI {
}
fn color_obj(&self, id: ID) -> Option<Color> {
if Some(id) == self.current_selection {
if Some(id) == self.primary.current_selection {
return Some(self.cs.get(Colors::Selected));
}
@ -533,10 +570,10 @@ impl UI {
id,
Ctx {
cs: &self.cs,
map: &self.map,
control_map: &self.control_map,
map: &self.primary.map,
control_map: &self.primary.control_map,
canvas: &self.canvas,
sim: &self.sim,
sim: &self.primary.sim,
},
) {
return Some(c);
@ -553,24 +590,24 @@ impl UI {
match idx {
// The first plugin is all the ToggleableLayers, which doesn't implement Colorizer.
0 => None,
1 => Some(Box::new(&self.traffic_signal_editor)),
2 => Some(Box::new(&self.stop_sign_editor)),
3 => Some(Box::new(&self.road_editor)),
1 => Some(Box::new(&self.primary.traffic_signal_editor)),
2 => Some(Box::new(&self.primary.stop_sign_editor)),
3 => Some(Box::new(&self.primary.road_editor)),
4 => Some(Box::new(&self.search_state)),
5 => Some(Box::new(&self.warp)),
6 => Some(Box::new(&self.follow)),
7 => Some(Box::new(&self.show_route)),
6 => Some(Box::new(&self.primary.follow)),
7 => Some(Box::new(&self.primary.show_route)),
8 => Some(Box::new(&self.color_picker)),
9 => Some(Box::new(&self.steepness_viz)),
9 => Some(Box::new(&self.primary.steepness_viz)),
10 => Some(Box::new(&self.osm_classifier)),
11 => Some(Box::new(&self.hider)),
12 => Some(Box::new(&self.debug_objects)),
13 => Some(Box::new(&self.floodfiller)),
14 => Some(Box::new(&self.geom_validator)),
15 => Some(Box::new(&self.turn_cycler)),
16 => Some(Box::new(&self.draw_neighborhoods)),
17 => Some(Box::new(&self.scenarios)),
18 => Some(Box::new(&self.edits_manager)),
11 => Some(Box::new(&self.primary.hider)),
12 => Some(Box::new(&self.primary.debug_objects)),
13 => Some(Box::new(&self.primary.floodfiller)),
14 => Some(Box::new(&self.primary.geom_validator)),
15 => Some(Box::new(&self.primary.turn_cycler)),
16 => Some(Box::new(&self.primary.draw_neighborhoods)),
17 => Some(Box::new(&self.primary.scenarios)),
18 => Some(Box::new(&self.primary.edits_manager)),
19 => Some(Box::new(&self.ab_test_manager)),
20 => Some(Box::new(&self.logs)),
_ => panic!("Active plugin {} is too high", idx),
@ -648,8 +685,8 @@ pub trait ShowTurnIcons {
impl ShowTurnIcons for UI {
fn show_icons_for(&self, id: IntersectionID) -> bool {
self.layers.show_all_turn_icons.is_enabled()
|| self.stop_sign_editor.show_turn_icons(id)
|| self.traffic_signal_editor.show_turn_icons(id)
|| self.primary.stop_sign_editor.show_turn_icons(id)
|| self.primary.traffic_signal_editor.show_turn_icons(id)
}
}