1use std::collections::BTreeSet;
2
3use crate::ID;
4use geom::{Duration, Polygon, Time};
5use sim::{AgentType, TripPhaseType};
6use widgetry::{
7 lctrl, Color, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Panel, ScreenDims,
8 ScreenPt, ScreenRectangle, Text, TextSpan, VerticalAlignment, Widget,
9};
10
11pub use self::route_sketcher::RouteSketcher;
12pub use self::select::RoadSelector;
13pub use self::warp::{inner_warp_to_id, warp_to_id, Warping};
14use crate::app::App;
15use crate::app::Transition;
16use crate::info::{ContextualActions, InfoPanel, Tab};
17use crate::sandbox::TimeWarpScreen;
18
19mod route_sketcher;
20mod select;
21pub mod share;
22mod warp;
23
24pub struct CommonState {
26 info_panel: Option<InfoPanel>,
28 cached_actions: Vec<Key>,
30}
31
32impl CommonState {
33 pub fn new() -> CommonState {
34 CommonState {
35 info_panel: None,
36 cached_actions: Vec::new(),
37 }
38 }
39
40 pub fn event(
41 &mut self,
42 ctx: &mut EventCtx,
43 app: &mut App,
44 ctx_actions: &mut dyn ContextualActions,
45 ) -> Option<Transition> {
46 if let Some(t) = CommonState::debug_actions(ctx, app) {
47 return Some(t);
48 }
49
50 if app.primary.layer.is_some() {
54 self.info_panel = None;
55 }
56
57 if let Some(id) = app.primary.current_selection.clone() {
58 if app.per_obj.left_click(ctx, "show info") {
60 app.primary.layer = None;
61 self.info_panel =
62 Some(InfoPanel::new(ctx, app, Tab::from_id(app, id), ctx_actions));
63 return None;
64 }
65 }
66
67 if let Some(ref mut info) = self.info_panel {
68 let (closed, maybe_t) = info.event(ctx, app, ctx_actions);
69 if closed {
70 self.info_panel = None;
71 }
72 if let Some(t) = maybe_t {
73 return Some(t);
74 }
75 }
76
77 if self.info_panel.is_none() {
78 self.cached_actions.clear();
79 if let Some(id) = app.primary.current_selection.clone() {
80 for (k, action) in ctx_actions.actions(app, id.clone()) {
82 if ctx.input.pressed(k) {
83 return Some(ctx_actions.execute(ctx, app, id, action, &mut false));
84 }
85 self.cached_actions.push(k);
86 }
87 }
88 }
89
90 None
91 }
92
93 pub fn draw(&self, g: &mut GfxCtx, app: &App) {
94 let keys = if let Some(ref info) = self.info_panel {
95 info.draw(g, app);
96 info.active_keys()
97 } else {
98 &self.cached_actions
99 };
100 let mut osd = if let Some(ref id) = app.primary.current_selection {
101 CommonState::osd_for(app, id.clone())
102 } else if app.opts.dev {
103 Text::from_all(vec![
104 Line("Nothing selected. Hint: "),
105 Line("Ctrl+J").fg(g.style().text_hotkey_color),
106 Line(" to warp"),
107 ])
108 } else {
109 Text::new()
110 };
111 if !keys.is_empty() {
112 osd.append(Line(" Hotkeys: "));
113 for (idx, key) in keys.iter().enumerate() {
114 if idx != 0 {
115 osd.append(Line(", "));
116 }
117 osd.append(Line(key.describe()).fg(g.style().text_hotkey_color));
118 }
119 }
120
121 CommonState::draw_custom_osd(g, app, osd);
122 }
123
124 fn osd_for(app: &App, id: ID) -> Text {
125 let map = &app.primary.map;
126 let mut osd = Text::new();
127 match id {
128 ID::Lane(l) => {
129 if app.opts.dev {
130 osd.append(Line(l.to_string()).bold_body());
131 osd.append(Line(" is "));
132 }
133 let r = map.get_parent(l);
134 osd.append_all(vec![
135 Line(format!("{} of ", map.get_l(l).lane_type.describe())),
136 Line(r.get_name(app.opts.language.as_ref())).underlined(),
137 ]);
138 if app.opts.dev {
139 osd.append(Line(" ("));
140 osd.append(Line(r.id.to_string()).bold_body());
141 osd.append(Line(")"));
142 }
143 }
144 ID::Building(b) => {
145 if app.opts.dev {
146 osd.append(Line(b.to_string()).bold_body());
147 osd.append(Line(" is "));
148 }
149 let bldg = map.get_b(b);
150 osd.append(Line(&bldg.address).underlined())
151 }
152 ID::ParkingLot(pl) => {
153 osd.append(Line(pl.to_string()).bold_body());
154 }
155 ID::Intersection(i) => {
156 if map.get_i(i).is_border() {
157 osd.append(Line("Border "));
158 }
159
160 if app.opts.dev {
161 osd.append(Line(i.to_string()).bold_body());
162 } else {
163 osd.append(Line("Intersection"));
164 }
165 osd.append(Line(" of "));
166
167 let mut road_names = BTreeSet::new();
168 for r in &map.get_i(i).roads {
169 road_names.insert(map.get_r(*r).get_name(app.opts.language.as_ref()));
170 }
171 list_names(&mut osd, |l| l.underlined(), road_names);
172 }
173 ID::Car(c) => {
174 if app.opts.dev {
175 osd.append(Line(c.to_string()).bold_body());
176 } else {
177 osd.append(Line(format!("a {}", c.vehicle_type)));
178 }
179 if let Some(r) = app.primary.sim.bus_route_id(c) {
180 osd.append_all(vec![
181 Line(" serving "),
182 Line(&map.get_tr(r).long_name).underlined(),
183 ]);
184 }
185 }
186 ID::Pedestrian(p) => {
187 if app.opts.dev {
188 osd.append(Line(p.to_string()).bold_body());
189 } else {
190 osd.append(Line("a pedestrian"));
191 }
192 }
193 ID::PedCrowd(list) => {
194 osd.append(Line(format!("a crowd of {} pedestrians", list.len())));
195 }
196 ID::TransitStop(bs) => {
197 if app.opts.dev {
198 osd.append(Line(bs.to_string()).bold_body());
199 } else {
200 osd.append(Line("transit stop "));
201 osd.append(Line(&map.get_ts(bs).name).underlined());
202 }
203 osd.append(Line(" served by "));
204
205 let routes: BTreeSet<String> = map
206 .get_routes_serving_stop(bs)
207 .into_iter()
208 .map(|r| r.short_name.clone())
209 .collect();
210 list_names(&mut osd, |l| l.underlined(), routes);
211 }
212 ID::Area(a) => {
213 osd.append(Line(a.to_string()).bold_body());
215 }
216 ID::Road(r) => {
217 if app.opts.dev {
218 osd.append(Line(r.to_string()).bold_body());
219 osd.append(Line(" is "));
220 }
221 osd.append(Line(map.get_r(r).get_name(app.opts.language.as_ref())).underlined());
222 }
223 }
224 osd
225 }
226
227 pub fn draw_osd(g: &mut GfxCtx, app: &App) {
228 let osd = if let Some(ref id) = app.primary.current_selection {
229 CommonState::osd_for(app, id.clone())
230 } else if app.opts.dev {
231 Text::from_all(vec![
232 Line("Nothing selected. Hint: "),
233 Line("Ctrl+J").fg(g.style().text_hotkey_color),
234 Line(" to warp"),
235 ])
236 } else {
237 Text::new()
238 };
239 CommonState::draw_custom_osd(g, app, osd);
240 }
241
242 pub fn draw_custom_osd(g: &mut GfxCtx, app: &App, mut osd: Text) {
243 if let Some(ref action) = app.per_obj.click_action {
244 osd.append_all(vec![
245 Line("; "),
246 Line("click").fg(g.style().text_hotkey_color),
247 Line(format!(" to {}", action)),
248 ]);
249 }
250
251 let mut batch = GeomBatch::from(vec![(
255 if app.primary.is_secondary {
256 Color::BLUE
257 } else {
258 app.cs.panel_bg
259 },
260 Polygon::rectangle(g.canvas.window_width, 1.5 * g.default_line_height()),
261 )]);
262 batch.append(
263 osd.render(g)
264 .translate(10.0, 0.25 * g.default_line_height()),
265 );
266
267 if app.opts.dev && !g.is_screencap() {
268 let dev_batch = Text::from("DEV").bg(Color::RED).render(g);
269 let dims = dev_batch.get_dims();
270 batch.append(dev_batch.translate(
271 g.canvas.window_width - dims.width - 10.0,
272 0.25 * g.default_line_height(),
273 ));
274 }
275 let draw = g.upload(batch);
276 let top_left = ScreenPt::new(0.0, g.canvas.window_height - 1.5 * g.default_line_height());
277 g.redraw_at(top_left, &draw);
278 g.canvas.mark_covered_area(ScreenRectangle::top_left(
279 top_left,
280 ScreenDims::new(g.canvas.window_width, 1.5 * g.default_line_height()),
281 ));
282 }
283
284 pub fn launch_info_panel(
286 &mut self,
287 ctx: &mut EventCtx,
288 app: &mut App,
289 tab: Tab,
290 ctx_actions: &mut dyn ContextualActions,
291 ) {
292 app.primary.layer = None;
293 self.info_panel = Some(InfoPanel::new(ctx, app, tab, ctx_actions));
294 }
295
296 pub fn info_panel_open(&self, app: &App) -> Option<ID> {
297 self.info_panel.as_ref().and_then(|i| i.active_id(app))
298 }
299
300 pub fn debug_actions(ctx: &mut EventCtx, app: &mut App) -> Option<Transition> {
302 if ctx.input.pressed(lctrl(Key::S)) {
303 app.opts.dev = !app.opts.dev;
304 }
305 if ctx.input.pressed(lctrl(Key::J)) {
306 return Some(Transition::Push(warp::DebugWarp::new_state(ctx)));
307 }
308 if app.secondary.is_some() && ctx.input.pressed(lctrl(Key::Tab)) {
309 app.swap_map();
310 sync_abtest(ctx, app);
311 }
312 None
313 }
314}
315
316pub fn tool_panel(ctx: &mut EventCtx) -> Panel {
318 Panel::new_builder(Widget::row(vec![
319 ctx.style()
320 .btn_plain
321 .icon("system/assets/tools/home.svg")
322 .hotkey(Key::Escape)
323 .build_widget(ctx, "back"),
324 ctx.style()
325 .btn_plain
326 .icon("system/assets/tools/settings.svg")
327 .build_widget(ctx, "settings"),
328 ]))
329 .aligned(HorizontalAlignment::Left, VerticalAlignment::BottomAboveOSD)
330 .build(ctx)
331}
332
333pub fn list_names<F: Fn(TextSpan) -> TextSpan>(txt: &mut Text, styler: F, names: BTreeSet<String>) {
334 let len = names.len();
335 for (idx, n) in names.into_iter().enumerate() {
336 if idx != 0 {
337 if idx == len - 1 {
338 if len == 2 {
339 txt.append(Line(" and "));
340 } else {
341 txt.append(Line(", and "));
342 }
343 } else {
344 txt.append(Line(", "));
345 }
346 }
347 txt.append(styler(Line(n)));
348 }
349}
350
351pub fn cmp_duration_shorter(app: &App, after: Duration, before: Duration) -> Vec<TextSpan> {
353 if after.epsilon_eq(before) {
354 vec![Line("same")]
355 } else if after < before {
356 vec![
357 Line((before - after).to_string(&app.opts.units)).fg(Color::GREEN),
358 Line(" faster"),
359 ]
360 } else if after > before {
361 vec![
362 Line((after - before).to_string(&app.opts.units)).fg(Color::RED),
363 Line(" slower"),
364 ]
365 } else {
366 unreachable!()
367 }
368}
369
370pub fn color_for_agent_type(app: &App, a: AgentType) -> Color {
371 match a {
372 AgentType::Pedestrian => app.cs.unzoomed_pedestrian,
373 AgentType::Bike => app.cs.unzoomed_bike,
374 AgentType::Bus | AgentType::Train => app.cs.unzoomed_bus,
375 AgentType::TransitRider => app.cs.bus_trip,
376 AgentType::Car => app.cs.unzoomed_car,
377 }
378}
379
380pub fn color_for_trip_phase(app: &App, tpt: TripPhaseType) -> Color {
381 match tpt {
382 TripPhaseType::Driving => app.cs.unzoomed_car,
383 TripPhaseType::Walking => app.cs.unzoomed_pedestrian,
384 TripPhaseType::Biking => app.cs.bike_trip,
385 TripPhaseType::Parking => app.cs.parking_trip,
386 TripPhaseType::WaitingForBus(_, _) => app.cs.bus_layer,
387 TripPhaseType::RidingBus(_, _, _) => app.cs.bus_trip,
388 TripPhaseType::Cancelled | TripPhaseType::Finished => unreachable!(),
389 TripPhaseType::DelayedStart => Color::YELLOW,
390 }
391}
392
393pub fn jump_to_time_upon_startup(
398 dt: Duration,
399) -> Box<dyn FnOnce(&mut EventCtx, &mut App) -> Vec<Transition>> {
400 Box::new(move |ctx, app| {
401 ctx.loading_screen(format!("jump forward {}", dt), |ctx, _| {
402 let deadline = Duration::seconds(0.5);
403 app.primary
404 .sim
405 .time_limited_step(&app.primary.map, dt, deadline, &mut None);
406 let target = Time::START_OF_DAY + dt;
407 if app.primary.sim.time() != target {
408 vec![Transition::Push(TimeWarpScreen::new_state(
409 ctx, app, target, None,
410 ))]
411 } else {
412 vec![Transition::Keep]
413 }
414 })
415 })
416}
417
418fn sync_abtest(ctx: &mut EventCtx, app: &mut App) {
419 let other_time = app.secondary.as_ref().unwrap().sim.time();
421 let our_time = app.primary.sim.time();
422 if our_time >= other_time {
423 return;
424 }
425 ctx.loading_screen("catch up", |_, timer| {
426 app.primary
427 .sim
428 .timed_step(&app.primary.map, other_time - our_time, &mut None, timer);
429 });
430}