abstreet/editor/src/ui.rs

602 lines
22 KiB
Rust
Raw Normal View History

// Copyright 2018 Google LLC, licensed under http://www.apache.org/licenses/LICENSE-2.0
// TODO this should just be a way to handle interactions between plugins
2018-06-22 21:01:44 +03:00
use abstutil;
2018-06-21 20:03:51 +03:00
use colors::{ColorScheme, Colors};
use control::ControlMap;
2018-06-22 21:01:44 +03:00
use control::{ModifiedStopSign, ModifiedTrafficSignal};
2018-06-23 00:42:11 +03:00
use ezgui::GfxCtx;
use ezgui::ToggleableLayer;
2018-06-23 00:42:11 +03:00
use ezgui::canvas::Canvas;
use ezgui::input::UserInput;
use graphics::types::Color;
2018-06-23 19:01:53 +03:00
use gui;
2018-06-25 19:11:25 +03:00
use map_model;
2018-06-22 21:01:44 +03:00
use map_model::IntersectionID;
use piston::input::{Key, MouseCursorEvent};
use piston::window::Size;
use plugins::classification::OsmClassifier;
2018-06-21 04:30:38 +03:00
use plugins::color_picker::ColorPicker;
use plugins::floodfill::Floodfiller;
2018-07-06 01:37:35 +03:00
use plugins::geom_validation::Validator;
use plugins::search::SearchState;
2018-06-29 21:53:55 +03:00
use plugins::selection::{Hider, SelectionState, ID};
use plugins::sim_controls::SimController;
use plugins::steep::SteepnessVisualizer;
use plugins::stop_sign_editor::StopSignEditor;
use plugins::traffic_signal_editor::TrafficSignalEditor;
use plugins::turn_colors::TurnColors;
use plugins::warp::WarpState;
use render;
use sim::CarID;
2018-06-22 21:01:44 +03:00
use std::collections::HashMap;
use std::process;
// TODO ideally these would be tuned kind of dynamically based on rendering speed
const MIN_ZOOM_FOR_ROADS: f64 = 0.15;
const MIN_ZOOM_FOR_PARCELS: f64 = 1.0;
const MIN_ZOOM_FOR_MOUSEOVER: f64 = 1.0;
const MIN_ZOOM_FOR_ROAD_MARKERS: f64 = 5.0;
pub struct UI {
map: map_model::Map,
draw_map: render::DrawMap,
control_map: ControlMap,
show_roads: ToggleableLayer,
show_buildings: ToggleableLayer,
show_intersections: ToggleableLayer,
show_parcels: ToggleableLayer,
show_icons: ToggleableLayer,
debug_mode: ToggleableLayer,
// This is a particularly special plugin, since it's always kind of active and other things
// read/write it.
current_selection_state: SelectionState,
2018-06-29 21:53:55 +03:00
hider: Hider,
current_search_state: SearchState,
warp: WarpState,
floodfiller: Floodfiller,
steepness_viz: SteepnessVisualizer,
osm_classifier: OsmClassifier,
turn_colors: TurnColors,
2018-07-06 23:45:05 +03:00
traffic_signal_editor: TrafficSignalEditor,
2018-07-06 23:49:03 +03:00
stop_sign_editor: StopSignEditor,
sim_ctrl: SimController,
2018-06-21 04:30:38 +03:00
color_picker: ColorPicker,
2018-07-06 23:56:00 +03:00
geom_validator: Validator,
canvas: Canvas,
2018-06-21 20:03:51 +03:00
// TODO maybe never pass this to other places? Always resolve colors here?
cs: ColorScheme,
}
impl UI {
2018-07-06 20:02:01 +03:00
pub fn new(abst_path: &str, window_size: Size, rng_seed: Option<u8>) -> UI {
println!("Opening {}", abst_path);
2018-06-26 02:26:43 +03:00
let map = map_model::Map::new(abst_path).expect("Couldn't load map");
2018-06-25 20:31:41 +03:00
let (draw_map, center_pt) = render::DrawMap::new(&map);
let control_map = ControlMap::new(&map);
let steepness_viz = SteepnessVisualizer::new(&map);
let turn_colors = TurnColors::new(&control_map);
let sim_ctrl = SimController::new(&map, rng_seed);
let mut ui = UI {
map,
draw_map,
control_map,
steepness_viz,
turn_colors,
sim_ctrl,
show_roads: ToggleableLayer::new("roads", Key::D3, "3", Some(MIN_ZOOM_FOR_ROADS)),
show_buildings: ToggleableLayer::new("buildings", Key::D1, "1", Some(0.0)),
show_intersections: ToggleableLayer::new(
"intersections",
Key::D2,
"2",
Some(MIN_ZOOM_FOR_ROADS),
),
show_parcels: ToggleableLayer::new("parcels", Key::D4, "4", Some(MIN_ZOOM_FOR_PARCELS)),
show_icons: ToggleableLayer::new(
"turn icons",
Key::D7,
"7",
Some(MIN_ZOOM_FOR_ROAD_MARKERS),
),
debug_mode: ToggleableLayer::new("debug mode", Key::G, "G", None),
current_selection_state: SelectionState::Empty,
2018-06-29 21:53:55 +03:00
hider: Hider::new(),
current_search_state: SearchState::Empty,
warp: WarpState::Empty,
floodfiller: Floodfiller::new(),
osm_classifier: OsmClassifier::new(),
2018-07-06 23:45:05 +03:00
traffic_signal_editor: TrafficSignalEditor::new(),
2018-07-06 23:49:03 +03:00
stop_sign_editor: StopSignEditor::new(),
2018-06-21 04:30:38 +03:00
color_picker: ColorPicker::new(),
2018-07-06 23:56:00 +03:00
geom_validator: Validator::new(),
2018-07-06 20:02:01 +03:00
canvas: Canvas::new(window_size),
cs: ColorScheme::load("color_scheme").unwrap(),
};
2018-06-22 21:01:44 +03:00
match abstutil::read_json::<EditorState>("editor_state") {
Ok(state) => {
println!("Loaded previous editor_state");
ui.canvas.cam_x = state.cam_x;
ui.canvas.cam_y = state.cam_y;
ui.canvas.cam_zoom = state.cam_zoom;
ui.control_map
.load_savestate(&state.traffic_signals, &state.stop_signs);
}
Err(_) => {
println!("Couldn't load editor_state, just centering initial view");
2018-07-06 20:02:01 +03:00
ui.canvas.center_on_map_pt(center_pt.x(), center_pt.y());
}
}
let new_zoom = ui.canvas.cam_zoom;
2018-06-22 20:50:38 +03:00
ui.zoom_for_toggleable_layers(-1.0, new_zoom);
ui
}
2018-06-23 19:01:53 +03:00
// TODO or make a custom event for zoom change
fn zoom_for_toggleable_layers(&mut self, old_zoom: f64, new_zoom: f64) {
self.show_roads.handle_zoom(old_zoom, new_zoom);
self.show_buildings.handle_zoom(old_zoom, new_zoom);
self.show_intersections.handle_zoom(old_zoom, new_zoom);
self.show_parcels.handle_zoom(old_zoom, new_zoom);
self.show_icons.handle_zoom(old_zoom, new_zoom);
self.debug_mode.handle_zoom(old_zoom, new_zoom);
}
2018-07-06 20:02:01 +03:00
fn mouseover_something(&self) -> Option<ID> {
2018-06-23 19:01:53 +03:00
let (x, y) = self.canvas.get_cursor_in_map_space();
2018-07-06 20:02:01 +03:00
let screen_bbox = self.canvas.get_screen_bbox();
2018-06-23 19:01:53 +03:00
let roads_onscreen = if self.show_roads.is_enabled() {
2018-06-29 21:53:55 +03:00
self.draw_map.get_roads_onscreen(screen_bbox, &self.hider)
2018-06-23 19:01:53 +03:00
} else {
Vec::new()
};
for r in &roads_onscreen {
for c in &self.sim_ctrl.sim.get_draw_cars_on_road(r.id, &self.map) {
2018-06-23 19:01:53 +03:00
if c.contains_pt(x, y) {
return Some(ID::Car(c.id));
}
}
}
if self.show_icons.is_enabled() {
for t in &self.draw_map.get_turn_icons_onscreen(screen_bbox) {
if t.contains_pt(x, y) {
return Some(ID::Turn(t.id));
}
}
}
if self.show_intersections.is_enabled() {
2018-06-29 21:53:55 +03:00
for i in &self.draw_map
.get_intersections_onscreen(screen_bbox, &self.hider)
{
2018-06-23 19:01:53 +03:00
for t in &self.map.get_i(i.id).turns {
for c in &self.sim_ctrl.sim.get_draw_cars_on_turn(*t, &self.map) {
2018-06-23 19:01:53 +03:00
if c.contains_pt(x, y) {
return Some(ID::Car(c.id));
}
}
}
if i.contains_pt(x, y) {
return Some(ID::Intersection(i.id));
}
}
}
if self.show_roads.is_enabled() {
for r in &roads_onscreen {
for c in &self.sim_ctrl.sim.get_draw_cars_on_road(r.id, &self.map) {
2018-06-23 19:01:53 +03:00
if c.contains_pt(x, y) {
return Some(ID::Car(c.id));
}
}
if r.road_contains_pt(x, y) {
return Some(ID::Road(r.id));
}
}
}
if self.show_buildings.is_enabled() {
2018-06-29 21:53:55 +03:00
for b in &self.draw_map
.get_buildings_onscreen(screen_bbox, &self.hider)
{
2018-06-23 19:01:53 +03:00
if b.contains_pt(x, y) {
return Some(ID::Building(b.id));
}
}
}
None
}
fn color_road(&self, id: map_model::RoadID) -> Color {
let r = self.map.get_r(id);
let mut default = match r.lane_type {
2018-06-23 19:01:53 +03:00
map_model::LaneType::Driving => self.cs.get(Colors::Road),
map_model::LaneType::Parking => self.cs.get(Colors::Parking),
map_model::LaneType::Sidewalk => self.cs.get(Colors::Sidewalk),
};
if r.probably_broken {
default = self.cs.get(Colors::Broken);
}
2018-06-23 19:01:53 +03:00
// TODO This evaluates all the color methods, which may be expensive. But the option
// chaining is harder to read. :(
vec![
self.current_selection_state.color_r(r, &self.cs),
self.current_search_state.color_r(r, &self.cs),
self.floodfiller.color_r(r, &self.cs),
2018-06-23 19:01:53 +03:00
self.steepness_viz.color_r(&self.map, r),
self.osm_classifier.color_r(r, &self.cs),
].iter()
.filter_map(|c| *c)
.next()
.unwrap_or(default)
}
fn color_intersection(&self, id: map_model::IntersectionID) -> Color {
let i = self.map.get_i(id);
2018-07-04 23:27:04 +03:00
let changed = if let Some(s) = self.control_map.traffic_signals.get(&i.id) {
s.changed()
2018-06-23 19:01:53 +03:00
} else if let Some(s) = self.control_map.stop_signs.get(&i.id) {
2018-07-04 23:27:04 +03:00
s.changed()
} else {
false
};
let default_color = if changed {
self.cs.get(Colors::ChangedIntersection)
2018-06-23 19:01:53 +03:00
} else {
2018-07-04 23:27:04 +03:00
self.cs.get(Colors::UnchangedIntersection)
2018-06-23 19:01:53 +03:00
};
self.current_selection_state
.color_i(i, &self.cs)
.unwrap_or(default_color)
}
fn color_turn_icon(&self, id: map_model::TurnID) -> Color {
let t = self.map.get_t(id);
// TODO traffic signal selection logic maybe moves here
self.current_selection_state
.color_t(t, &self.cs)
.unwrap_or_else(|| {
self.stop_sign_editor
2018-07-06 23:49:03 +03:00
.color_t(t, &self.control_map, &self.cs)
2018-06-23 19:01:53 +03:00
.unwrap_or_else(|| {
self.traffic_signal_editor
2018-07-06 23:45:05 +03:00
.color_t(t, &self.map, &self.control_map, &self.cs)
2018-06-23 19:01:53 +03:00
.unwrap_or_else(|| {
self.turn_colors
.color_t(t)
.unwrap_or(self.cs.get(Colors::TurnIconInactive))
})
})
})
}
fn color_building(&self, id: map_model::BuildingID) -> Color {
let b = self.map.get_b(id);
vec![
self.current_selection_state.color_b(b, &self.cs),
self.current_search_state.color_b(b, &self.cs),
self.osm_classifier.color_b(b, &self.cs),
].iter()
.filter_map(|c| *c)
.next()
.unwrap_or(self.cs.get(Colors::Building))
}
// Returns (boundary, fill) color
fn color_parcel(&self, id: map_model::ParcelID) -> (Color, Color) {
let _p = self.map.get_p(id);
(
self.cs.get(Colors::ParcelBoundary),
self.cs.get(Colors::ParcelInterior),
)
}
fn color_car(&self, id: CarID) -> Color {
if let Some(c) = self.current_selection_state.color_c(id, &self.cs) {
return c;
}
if self.sim_ctrl.sim.is_moving(id) {
self.cs.get(Colors::MovingCar)
} else {
self.cs.get(Colors::StuckCar)
}
}
}
impl gui::GUI for UI {
2018-07-07 00:15:19 +03:00
fn event(&mut self, input: &mut UserInput) -> gui::EventLoopMode {
// First update the camera and handle zoom
let old_zoom = self.canvas.cam_zoom;
self.canvas.handle_event(input.use_event_directly());
let new_zoom = self.canvas.cam_zoom;
self.zoom_for_toggleable_layers(old_zoom, new_zoom);
// Always handle mouseover
if old_zoom >= MIN_ZOOM_FOR_MOUSEOVER && new_zoom < MIN_ZOOM_FOR_MOUSEOVER {
self.current_selection_state = SelectionState::Empty;
}
if !self.canvas.is_dragging() && input.use_event_directly().mouse_cursor_args().is_some()
&& new_zoom >= MIN_ZOOM_FOR_MOUSEOVER
{
let item = self.mouseover_something();
self.current_selection_state = self.current_selection_state.handle_mouseover(item);
}
2018-07-07 00:48:38 +03:00
// Run each plugin, short-circuiting if the plugin claimed it was active.
macro_rules! stop_if_done {
($plugin:expr) => {
if $plugin {
return gui::EventLoopMode::InputOnly;
}
2018-07-07 00:48:38 +03:00
};
}
2018-07-07 00:48:38 +03:00
stop_if_done!(self.traffic_signal_editor.event(
input,
&self.map,
&mut self.control_map,
&self.current_selection_state,
));
stop_if_done!(self.stop_sign_editor.event(
input,
&self.map,
&mut self.control_map,
&self.current_selection_state,
));
stop_if_done!(
self.color_picker
.handle_event(input, &mut self.canvas, &mut self.cs)
);
stop_if_done!(self.current_search_state.event(input));
stop_if_done!(self.warp.event(
input,
&self.map,
&mut self.canvas,
&mut self.current_selection_state,
));
if self.show_roads.handle_event(input) {
if let SelectionState::SelectedRoad(_, _) = self.current_selection_state {
self.current_selection_state = SelectionState::Empty;
}
if let SelectionState::TooltipRoad(_) = self.current_selection_state {
self.current_selection_state = SelectionState::Empty;
}
return gui::EventLoopMode::InputOnly;
}
if self.show_buildings.handle_event(input) {
if let SelectionState::SelectedBuilding(_) = self.current_selection_state {
self.current_selection_state = SelectionState::Empty;
}
2018-07-07 00:48:38 +03:00
return gui::EventLoopMode::InputOnly;
}
if self.show_intersections.handle_event(input) {
if let SelectionState::SelectedIntersection(_) = self.current_selection_state {
self.current_selection_state = SelectionState::Empty;
}
return gui::EventLoopMode::InputOnly;
}
stop_if_done!(self.show_parcels.handle_event(input));
stop_if_done!(self.show_icons.handle_event(input));
stop_if_done!(self.debug_mode.handle_event(input));
stop_if_done!(self.steepness_viz.handle_event(input));
stop_if_done!(self.osm_classifier.handle_event(input));
stop_if_done!(self.hider.event(input, &mut self.current_selection_state));
stop_if_done!(self.floodfiller.event(&self.map, input));
stop_if_done!(
self.geom_validator
.event(input, &mut self.canvas, &self.map)
);
if input.unimportant_key_pressed(Key::I, "Validate map geometry") {
self.geom_validator = Validator::start(&self.draw_map);
return gui::EventLoopMode::InputOnly;
}
if self.sim_ctrl.sim.total_cars() == 0 {
if input.unimportant_key_pressed(Key::S, "Seed the map with 50% parked cars") {
self.sim_ctrl.sim.seed_parked_cars(0.5);
return gui::EventLoopMode::InputOnly;
}
} else {
if input.unimportant_key_pressed(Key::S, "Make 1000 parked cars start driving") {
self.sim_ctrl.sim.start_many_parked_cars(&self.map, 1000);
return gui::EventLoopMode::InputOnly;
}
}
match self.current_selection_state {
SelectionState::SelectedCar(id) => {
// TODO not sure if we should debug like this (pushing the bit down to all the
// layers representing an entity) or by using some scary global mutable singleton
if input.unimportant_key_pressed(Key::D, "press D to debug") {
self.sim_ctrl.sim.toggle_debug(id);
2018-07-07 00:15:19 +03:00
return gui::EventLoopMode::InputOnly;
}
}
SelectionState::SelectedRoad(id, _) => {
if input.key_pressed(Key::F, "Press F to start floodfilling from this road") {
self.floodfiller = Floodfiller::start(id);
2018-07-07 00:15:19 +03:00
return gui::EventLoopMode::InputOnly;
}
2018-06-19 22:24:22 +03:00
if self.map.get_r(id).lane_type == map_model::LaneType::Driving {
if input.key_pressed(Key::A, "Press A to start a parked car on this road") {
self.sim_ctrl.sim.start_parked_car(&self.map, id);
2018-07-07 00:15:19 +03:00
return gui::EventLoopMode::InputOnly;
}
}
}
SelectionState::SelectedIntersection(id) => {
if self.control_map.traffic_signals.contains_key(&id) {
2018-06-26 01:49:38 +03:00
if input.key_pressed(
Key::E,
&format!("Press E to edit traffic signal for {:?}", id),
) {
2018-07-06 23:45:05 +03:00
self.traffic_signal_editor = TrafficSignalEditor::start(id);
2018-07-07 00:15:19 +03:00
return gui::EventLoopMode::InputOnly;
}
}
if self.control_map.stop_signs.contains_key(&id) {
2018-06-26 01:49:38 +03:00
if input.key_pressed(Key::E, &format!("Press E to edit stop sign for {:?}", id))
{
2018-07-06 23:49:03 +03:00
self.stop_sign_editor = StopSignEditor::start(id);
2018-07-07 00:15:19 +03:00
return gui::EventLoopMode::InputOnly;
}
}
}
_ => {}
}
// Do this one lastish, since it conflicts with lots of other stuff
2018-07-07 00:48:38 +03:00
stop_if_done!(self.current_selection_state.event(input, &self.map));
2018-07-06 01:37:35 +03:00
if input.unimportant_key_pressed(Key::Escape, "Press escape to quit") {
2018-06-22 21:01:44 +03:00
let state = EditorState {
cam_x: self.canvas.cam_x,
cam_y: self.canvas.cam_y,
cam_zoom: self.canvas.cam_zoom,
traffic_signals: self.control_map.get_traffic_signals_savestate(),
stop_signs: self.control_map.get_stop_signs_savestate(),
};
// TODO maybe make state line up with the map, so loading from a new map doesn't break
2018-06-22 21:01:44 +03:00
abstutil::write_json("editor_state", &state).expect("Saving editor_state failed");
abstutil::write_json("color_scheme", &self.cs).expect("Saving color_scheme failed");
println!("Saved editor_state and color_scheme");
process::exit(0);
}
// Sim controller plugin is kind of always active? If nothing else ran, let it use keys.
if self.sim_ctrl.event(input, &self.map, &self.control_map) {
2018-07-07 00:15:19 +03:00
gui::EventLoopMode::Animation
} else {
2018-07-07 00:15:19 +03:00
gui::EventLoopMode::InputOnly
}
}
2018-07-06 20:02:01 +03:00
// TODO Weird to mut self just to set window_size on the canvas
fn draw(&mut self, g: &mut GfxCtx, input: UserInput, window_size: Size) {
g.clear(self.cs.get(Colors::Background));
self.canvas.start_drawing(g, window_size);
2018-07-06 20:02:01 +03:00
let screen_bbox = self.canvas.get_screen_bbox();
if self.show_parcels.is_enabled() {
for p in &self.draw_map.get_parcels_onscreen(screen_bbox) {
p.draw(g, self.color_parcel(p.id));
}
}
let roads_onscreen = if self.show_roads.is_enabled() {
2018-06-29 21:53:55 +03:00
self.draw_map.get_roads_onscreen(screen_bbox, &self.hider)
} else {
Vec::new()
};
for r in &roads_onscreen {
r.draw(g, self.color_road(r.id));
if self.canvas.cam_zoom >= MIN_ZOOM_FOR_ROAD_MARKERS {
2018-06-21 20:03:51 +03:00
r.draw_detail(g, &self.cs);
}
if self.debug_mode.is_enabled() {
r.draw_debug(g, &self.cs, self.map.get_r(r.id));
}
}
if self.show_intersections.is_enabled() {
2018-06-29 21:53:55 +03:00
for i in &self.draw_map
.get_intersections_onscreen(screen_bbox, &self.hider)
{
2018-06-29 19:30:31 +03:00
i.draw(g, self.color_intersection(i.id), &self.cs);
}
}
if self.show_icons.is_enabled() {
for t in &self.draw_map.get_turn_icons_onscreen(screen_bbox) {
2018-06-21 20:03:51 +03:00
t.draw_icon(g, self.color_turn_icon(t.id), &self.cs);
for c in &self.sim_ctrl.sim.get_draw_cars_on_turn(t.id, &self.map) {
c.draw(g, self.color_car(c.id));
}
}
}
for r in &roads_onscreen {
for c in &self.sim_ctrl.sim.get_draw_cars_on_road(r.id, &self.map) {
c.draw(g, self.color_car(c.id));
}
}
if self.show_buildings.is_enabled() {
2018-06-29 21:53:55 +03:00
for b in &self.draw_map
.get_buildings_onscreen(screen_bbox, &self.hider)
{
2018-06-26 20:44:14 +03:00
b.draw(
g,
self.color_building(b.id),
self.cs.get(Colors::BuildingPath),
);
}
}
self.current_selection_state.draw(
&self.map,
&self.canvas,
&self.draw_map,
&self.control_map,
&self.sim_ctrl.sim,
2018-06-21 20:03:51 +03:00
&self.cs,
g,
);
self.color_picker.draw(&self.canvas, g);
2018-06-21 04:30:38 +03:00
let mut osd_lines = self.sim_ctrl.get_osd_lines();
let action_lines = input.get_possible_actions();
if !action_lines.is_empty() {
osd_lines.push(String::from(""));
osd_lines.extend(action_lines);
}
let search_lines = self.current_search_state.get_osd_lines();
if !search_lines.is_empty() {
osd_lines.push(String::from(""));
osd_lines.extend(search_lines);
}
let warp_lines = self.warp.get_osd_lines();
if !warp_lines.is_empty() {
osd_lines.push(String::from(""));
osd_lines.extend(warp_lines);
}
self.canvas.draw_osd_notification(g, &osd_lines);
}
}
2018-06-22 21:01:44 +03:00
#[derive(Serialize, Deserialize, Debug)]
pub struct EditorState {
pub cam_x: f64,
pub cam_y: f64,
pub cam_zoom: f64,
pub traffic_signals: HashMap<IntersectionID, ModifiedTrafficSignal>,
pub stop_signs: HashMap<IntersectionID, ModifiedStopSign>,
}