game/common/
mod.rs

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
24// TODO This is now just used in two modes...
25pub struct CommonState {
26    // TODO Better to express these as mutex
27    info_panel: Option<InfoPanel>,
28    // Just for drawing the OSD
29    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        // Layers can be launched from many places, many of which don't have a way of getting at
51        // CommonState, which is only in sandbox and debug mode. It suffices to detect here if a
52        // layer is open and close the info panel, luckily.
53        if app.primary.layer.is_some() {
54            self.info_panel = None;
55        }
56
57        if let Some(id) = app.primary.current_selection.clone() {
58            // TODO Also have a hotkey binding for this?
59            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                // Allow hotkeys to work without opening the panel.
81                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                // Only selectable in dev mode anyway
214                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        // TODO Rendering the OSD is actually a bit hacky.
252
253        // First the constant background
254        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    // Meant to be used for launching from other states
285    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    /// Allow toggling of dev mode and warping to an object by ID.
301    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
316// TODO Kinda misnomer
317pub 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
351// Shorter is better
352pub 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
393/// If you want a simulation to start after midnight, pass the result of this to
394/// `SandboxMode::async_new`. It's less visually overwhelming and more performant to continue with a
395/// loading screen than launching the jump-to-time UI. After a deadline of 0.5s, we will switch
396/// over to the jump-to-time UI so the user can cancel.
397pub 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    // If the other simulation is at a later time, catch up to it
420    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}