game/
app.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3
4use anyhow::Result;
5use maplit::btreemap;
6use structopt::StructOpt;
7
8use crate::ID;
9use abstio::MapName;
10use abstutil::{Tags, Timer};
11use geom::{Bounds, Circle, Distance, Duration, FindClosest, Polygon, Pt2D, Tessellation, Time};
12use map_gui::colors::ColorScheme;
13use map_gui::options::Options;
14use map_gui::render::{DrawMap, DrawOptions};
15use map_gui::tools::CameraState;
16use map_model::AreaType;
17use map_model::{BufferType, IntersectionID, LaneType, Map, Traversable};
18use sim::{AgentID, Analytics, Sim, SimCallback, SimFlags, VehicleType};
19use synthpop::Scenario;
20use widgetry::mapspace::ToggleZoomed;
21use widgetry::{Cached, Canvas, EventCtx, GfxCtx, Prerender, SharedAppState, State};
22
23use crate::challenges::HighScore;
24use crate::common::Warping;
25use crate::edit::apply_map_edits;
26use crate::layer::Layer;
27use crate::render::{unzoomed_agent_radius, AgentCache, GameRenderable};
28use crate::sandbox::dashboards::DashTab;
29use crate::sandbox::{GameplayMode, TutorialState};
30
31// Convenient typedef
32pub type Transition = widgetry::Transition<App>;
33
34/// The top-level data that lasts through the entire game, no matter what state the game is in.
35pub struct App {
36    /// State (like the simulation and drawing stuff) associated with the "primary" map.
37    pub primary: PerMap,
38    /// Represents state for a different version of the `primary` map. `swap_map` can be used to
39    /// switch the primary and secondary state. This is currently used to compare an edited and
40    /// unedited map for Ungap the Map and as a debug mode to compare different files representing
41    /// the same area.
42    pub secondary: Option<PerMap>,
43    pub store_unedited_map_in_secondary: bool,
44
45    pub cs: ColorScheme,
46    pub opts: Options,
47
48    pub per_obj: PerObjectActions,
49
50    /// Static data that lasts the entire session. Use sparingly.
51    pub session: SessionState,
52}
53
54impl App {
55    // TODO Should the prebaked methods be on primary along with the data?
56    pub fn has_prebaked(&self) -> Option<(&MapName, &String)> {
57        self.primary.prebaked.as_ref().map(|(m, s, _)| (m, s))
58    }
59    pub fn prebaked(&self) -> &Analytics {
60        &self.primary.prebaked.as_ref().unwrap().2
61    }
62    pub fn set_prebaked(&mut self, prebaked: Option<(MapName, String, Analytics)>) {
63        self.primary.prebaked = prebaked;
64
65        if false {
66            if let Some((_, _, ref a)) = self.primary.prebaked {
67                use abstutil::{prettyprint_usize, serialized_size_bytes};
68                println!(
69                    "- road_thruput: {} bytes",
70                    prettyprint_usize(serialized_size_bytes(&a.road_thruput))
71                );
72                println!(
73                    "- intersection_thruput: {} bytes",
74                    prettyprint_usize(serialized_size_bytes(&a.intersection_thruput))
75                );
76                println!(
77                    "- traffic_signal_thruput: {} bytes",
78                    prettyprint_usize(serialized_size_bytes(&a.traffic_signal_thruput))
79                );
80                println!(
81                    "- demand : {} bytes",
82                    prettyprint_usize(serialized_size_bytes(&a.demand))
83                );
84                println!(
85                    "- bus_arrivals : {} bytes",
86                    prettyprint_usize(serialized_size_bytes(&a.bus_arrivals))
87                );
88                println!(
89                    "- passengers_boarding: {} bytes",
90                    prettyprint_usize(serialized_size_bytes(&a.passengers_boarding))
91                );
92                println!(
93                    "- passengers_alighting: {} bytes",
94                    prettyprint_usize(serialized_size_bytes(&a.passengers_alighting))
95                );
96                println!(
97                    "- started_trips: {} bytes",
98                    prettyprint_usize(serialized_size_bytes(&a.started_trips))
99                );
100                println!(
101                    "- finished_trips: {} bytes",
102                    prettyprint_usize(serialized_size_bytes(&a.finished_trips))
103                );
104                println!(
105                    "- trip_log: {} bytes",
106                    prettyprint_usize(serialized_size_bytes(&a.trip_log))
107                );
108                println!(
109                    "- intersection_delays: {} bytes",
110                    prettyprint_usize(serialized_size_bytes(&a.intersection_delays))
111                );
112                println!(
113                    "- parking_lane_changes: {} bytes",
114                    prettyprint_usize(serialized_size_bytes(&a.parking_lane_changes))
115                );
116                println!(
117                    "- parking_lot_changes: {} bytes",
118                    prettyprint_usize(serialized_size_bytes(&a.parking_lot_changes))
119                );
120            }
121        }
122    }
123
124    pub fn draw(&self, g: &mut GfxCtx, opts: DrawOptions, show_objs: &dyn ShowObject) {
125        let map = &self.primary.map;
126        let draw_map = &self.primary.draw_map;
127
128        let mut sample_intersection: Option<String> = None;
129
130        g.clear(self.cs.void_background);
131        g.redraw(&draw_map.boundary_polygon);
132
133        if g.canvas.is_unzoomed() {
134            let layers = show_objs.layers();
135            if layers.show_areas {
136                g.redraw(&draw_map.draw_all_areas);
137            }
138            if layers.show_parking_lots {
139                g.redraw(&draw_map.draw_all_unzoomed_parking_lots);
140            }
141            if layers.show_intersections || layers.show_lanes {
142                g.redraw(
143                    &self
144                        .primary
145                        .draw_map
146                        .draw_all_unzoomed_roads_and_intersections,
147                );
148            }
149            if layers.show_buildings {
150                g.redraw(&draw_map.draw_all_buildings);
151                g.redraw(&draw_map.draw_all_building_outlines);
152            }
153
154            // Still show some shape selection when zoomed out.
155            // TODO Refactor! Ideally use get_obj
156            if let Some(ID::Area(id)) = self.primary.current_selection {
157                g.draw_polygon(self.cs.selected, draw_map.get_a(id).get_outline(map));
158            } else if let Some(ID::Road(id)) = self.primary.current_selection {
159                g.draw_polygon(self.cs.selected, draw_map.get_r(id).get_outline(map));
160            } else if let Some(ID::Intersection(id)) = self.primary.current_selection {
161                // Actually, don't use get_outline here! Full polygon is easier to see.
162                g.draw_polygon(self.cs.selected, map.get_i(id).polygon.clone());
163            } else if let Some(ID::Building(id)) = self.primary.current_selection {
164                g.draw_polygon(self.cs.selected, map.get_b(id).polygon.clone());
165            }
166
167            let mut cache = self.primary.agents.borrow_mut();
168            cache.draw_unzoomed_agents(
169                g,
170                &self.primary.map,
171                &self.primary.sim,
172                &self.cs,
173                &self.opts,
174            );
175
176            if let Some(a) = self
177                .primary
178                .current_selection
179                .as_ref()
180                .and_then(|id| id.agent_id())
181            {
182                if let Some(pt) = self.primary.sim.canonical_pt_for_agent(a, map) {
183                    // Usually we show selection with an outline, but no thickness/color is really
184                    // visible for these tiny crowded dots.
185                    g.draw_polygon(
186                        self.cs.selected,
187                        Circle::new(pt, unzoomed_agent_radius(a.to_vehicle_type())).to_polygon(),
188                    );
189                }
190            }
191        } else {
192            let mut cache = self.primary.agents.borrow_mut();
193            let objects = self.get_renderables_back_to_front(
194                g.get_screen_bounds(),
195                g.prerender,
196                &mut cache,
197                show_objs,
198            );
199
200            let mut drawn_all_buildings = false;
201            let mut drawn_all_areas = false;
202
203            for obj in objects {
204                obj.draw(g, self, &opts);
205
206                match obj.get_id() {
207                    ID::Building(_) => {
208                        if !drawn_all_buildings {
209                            g.redraw(&draw_map.draw_all_buildings);
210                            g.redraw(&draw_map.draw_all_building_outlines);
211                            drawn_all_buildings = true;
212                        }
213                    }
214                    ID::Area(_) => {
215                        if !drawn_all_areas {
216                            g.redraw(&draw_map.draw_all_areas);
217                            drawn_all_areas = true;
218                        }
219                    }
220                    _ => {}
221                }
222
223                if self.primary.current_selection == Some(obj.get_id()) {
224                    g.draw_polygon(self.cs.selected, obj.get_outline(map));
225                }
226
227                if g.is_screencap() && sample_intersection.is_none() {
228                    if let ID::Intersection(id) = obj.get_id() {
229                        sample_intersection = Some(format!("_i{}", id.0));
230                    }
231                }
232            }
233        }
234
235        if let Some(i) = sample_intersection {
236            g.set_screencap_naming_hint(i);
237        }
238    }
239
240    /// Assumes some defaults.
241    pub fn recalculate_current_selection(&mut self, ctx: &EventCtx) {
242        self.primary.current_selection =
243            self.calculate_current_selection(ctx, &ShowEverything::new(), false, false, false);
244    }
245
246    pub fn mouseover_unzoomed_roads_and_intersections(&self, ctx: &EventCtx) -> Option<ID> {
247        self.calculate_current_selection(ctx, &ShowEverything::new(), false, true, false)
248    }
249    pub fn mouseover_unzoomed_intersections(&self, ctx: &EventCtx) -> Option<ID> {
250        self.calculate_current_selection(ctx, &ShowEverything::new(), false, true, false)
251            .filter(|id| matches!(id, ID::Intersection(_)))
252    }
253    pub fn mouseover_unzoomed_buildings(&self, ctx: &EventCtx) -> Option<ID> {
254        self.calculate_current_selection(ctx, &ShowEverything::new(), false, false, true)
255    }
256    pub fn mouseover_unzoomed_everything(&self, ctx: &EventCtx) -> Option<ID> {
257        self.calculate_current_selection(ctx, &ShowEverything::new(), false, true, true)
258    }
259    pub fn mouseover_debug_mode(&self, ctx: &EventCtx, show_objs: &dyn ShowObject) -> Option<ID> {
260        self.calculate_current_selection(ctx, show_objs, true, false, false)
261    }
262
263    fn calculate_current_selection(
264        &self,
265        ctx: &EventCtx,
266        show_objs: &dyn ShowObject,
267        debug_mode: bool,
268        unzoomed_roads_and_intersections: bool,
269        unzoomed_buildings: bool,
270    ) -> Option<ID> {
271        let unzoomed = ctx.canvas.is_unzoomed();
272
273        // Unzoomed mode. Ignore when debugging areas.
274        if unzoomed && !(debug_mode || unzoomed_roads_and_intersections || unzoomed_buildings) {
275            return None;
276        }
277
278        let pt = ctx.canvas.get_cursor_in_map_space()?;
279
280        let mut cache = self.primary.agents.borrow_mut();
281        let mut objects = self.get_renderables_back_to_front(
282            Circle::new(pt, Distance::meters(3.0)).get_bounds(),
283            ctx.prerender,
284            &mut cache,
285            show_objs,
286        );
287        objects.reverse();
288
289        let mut small_agents: Vec<(ID, Pt2D)> = Vec::new();
290
291        // In most cases, this loop is greedy -- as soon as we find the first object containing the
292        // cursor, return it. But if we happen to match a pedestrian or bike while zoomed in, there
293        // might be several overlapping hitboxes. Collect them all in small_agents, then pick the
294        // closest to the cursor.
295        for obj in objects {
296            let id = obj.get_id();
297            match id {
298                ID::Area(_) => {
299                    if !debug_mode {
300                        continue;
301                    }
302                }
303                ID::Road(_) => {
304                    if !unzoomed_roads_and_intersections || !unzoomed {
305                        continue;
306                    }
307                }
308                ID::Intersection(_) => {
309                    if unzoomed && !unzoomed_roads_and_intersections {
310                        continue;
311                    }
312                }
313                ID::Building(_) => {
314                    if unzoomed && !unzoomed_buildings {
315                        continue;
316                    }
317                }
318                _ => {
319                    if unzoomed {
320                        continue;
321                    }
322                }
323            }
324            if obj.contains_pt(pt, &self.primary.map) {
325                if unzoomed {
326                    // If we're selecting agents unzoomed, they're all tiny circles, and we really
327                    // don't care about picking the one closest to the cursor.
328                    return Some(id);
329                }
330
331                match id {
332                    ID::Pedestrian(_) => {}
333                    ID::Car(c) => {
334                        if c.vehicle_type != VehicleType::Bike {
335                            return Some(id);
336                        }
337                    }
338                    _ => {
339                        // Once we match at least one small agent, keep scanning through all
340                        // possible hits. Ignore lanes and intersections and other thngs; they have
341                        // a lower z-order.
342                        if small_agents.is_empty() {
343                            return Some(id);
344                        } else {
345                            continue;
346                        }
347                    }
348                }
349
350                small_agents.push((id, obj.get_outline(&self.primary.map).center()));
351            }
352        }
353
354        let (id, _) = small_agents
355            .into_iter()
356            .min_by_key(|(_, center)| center.fast_dist(pt))?;
357        Some(id)
358    }
359
360    // TODO This could probably belong to DrawMap again, but it's annoying to plumb things that
361    // State does, like show_icons_for() and show().
362    fn get_renderables_back_to_front<'a>(
363        &'a self,
364        bounds: Bounds,
365        prerender: &Prerender,
366        agents: &'a mut AgentCache,
367        show_objs: &dyn ShowObject,
368    ) -> Vec<&'a (dyn GameRenderable + 'a)> {
369        let map = &self.primary.map;
370        let draw_map = &self.primary.draw_map;
371
372        let mut areas: Vec<&dyn GameRenderable> = Vec::new();
373        let mut parking_lots: Vec<&dyn GameRenderable> = Vec::new();
374        let mut lanes: Vec<&dyn GameRenderable> = Vec::new();
375        let mut roads: Vec<&dyn GameRenderable> = Vec::new();
376        let mut intersections: Vec<&dyn GameRenderable> = Vec::new();
377        let mut buildings: Vec<&dyn GameRenderable> = Vec::new();
378        let mut transit_stops: Vec<&dyn GameRenderable> = Vec::new();
379        let mut agents_on: Vec<Traversable> = Vec::new();
380
381        for id in draw_map
382            .get_matching_objects(bounds)
383            .into_iter()
384            .map(|id| id.into())
385        {
386            if !show_objs.show(&id) {
387                continue;
388            }
389            match id {
390                ID::Area(id) => areas.push(draw_map.get_a(id)),
391                ID::Road(id) => {
392                    let road = draw_map.get_r(id);
393                    for lane in &road.lanes {
394                        agents_on.push(Traversable::Lane(lane.id));
395                        lanes.push(lane);
396                    }
397                    for ts in &map.get_r(id).transit_stops {
398                        if show_objs.show(&ID::TransitStop(*ts)) {
399                            transit_stops.push(draw_map.get_ts(*ts));
400                        }
401                    }
402                    roads.push(road);
403                }
404                ID::Intersection(id) => {
405                    intersections.push(draw_map.get_i(id));
406                    for t in &map.get_i(id).turns {
407                        agents_on.push(Traversable::Turn(t.id));
408                    }
409                }
410                ID::Building(id) => buildings.push(draw_map.get_b(id)),
411                ID::ParkingLot(id) => {
412                    parking_lots.push(draw_map.get_pl(id));
413                    // Slight hack
414                    agents_on.push(Traversable::Lane(map.get_pl(id).driving_pos.lane()));
415                }
416
417                ID::Lane(_)
418                | ID::TransitStop(_)
419                | ID::Car(_)
420                | ID::Pedestrian(_)
421                | ID::PedCrowd(_) => {
422                    panic!("{:?} shouldn't be in the quadtree", id)
423                }
424            }
425        }
426
427        // From background to foreground Z-order
428        let mut borrows: Vec<&dyn GameRenderable> = Vec::new();
429        borrows.extend(areas);
430        borrows.extend(parking_lots);
431        borrows.extend(lanes);
432        borrows.extend(roads);
433        borrows.extend(intersections);
434        borrows.extend(buildings);
435        borrows.extend(transit_stops);
436
437        // Expand all of the Traversables into agents, populating the cache if needed.
438        for on in &agents_on {
439            agents.populate_if_needed(*on, map, &self.primary.sim, &self.cs, prerender);
440        }
441
442        for on in agents_on {
443            for obj in agents.get(on) {
444                borrows.push(obj);
445            }
446        }
447
448        borrows.retain(|x| x.get_zorder() <= self.primary.draw_map.show_zorder);
449
450        // This is a stable sort.
451        borrows.sort_by_key(|x| x.get_zorder());
452
453        borrows
454    }
455
456    /// Ensure the map edits are blank, reset the simulation, and blank out prebaked results.
457    pub fn clear_everything(&mut self, ctx: &mut EventCtx) {
458        ctx.loading_screen("reset map and sim", |ctx, timer| {
459            apply_map_edits(ctx, self, self.primary.map.new_edits());
460            self.primary.map.recalculate_pathfinding_after_edits(timer);
461
462            self.primary.clear_sim();
463            self.set_prebaked(None);
464        });
465    }
466
467    /// This swaps the primary and secondary PerMaps. Depending on what state the rest of the app
468    /// is in, things like IDs might totally change!
469    pub fn swap_map(&mut self) {
470        let secondary = self.secondary.take().expect("no secondary map set");
471        let primary = std::mem::replace(&mut self.primary, secondary);
472        self.secondary = Some(primary);
473    }
474}
475
476impl App {
477    /// If an intersection was clicked, return its ID.
478    pub fn click_on_intersection<S: Into<String>>(
479        &mut self,
480        ctx: &mut EventCtx,
481        label: S,
482    ) -> Option<IntersectionID> {
483        if let Some(ID::Intersection(i)) = self.primary.current_selection {
484            if self.per_obj.left_click(ctx, label) {
485                return Some(i);
486            }
487        }
488        None
489    }
490}
491
492// I haven't measured build or runtime impact of inlining vs not, but I assume for these simple
493// accessors it makes sense.
494impl map_gui::AppLike for App {
495    #[inline]
496    fn map(&self) -> &Map {
497        &self.primary.map
498    }
499    #[inline]
500    fn cs(&self) -> &ColorScheme {
501        &self.cs
502    }
503    #[inline]
504    fn mut_cs(&mut self) -> &mut ColorScheme {
505        &mut self.cs
506    }
507    #[inline]
508    fn draw_map(&self) -> &DrawMap {
509        &self.primary.draw_map
510    }
511    #[inline]
512    fn mut_draw_map(&mut self) -> &mut DrawMap {
513        &mut self.primary.draw_map
514    }
515    #[inline]
516    fn opts(&self) -> &Options {
517        &self.opts
518    }
519    #[inline]
520    fn mut_opts(&mut self) -> &mut Options {
521        &mut self.opts
522    }
523
524    fn map_switched(&mut self, ctx: &mut EventCtx, map: Map, timer: &mut Timer) {
525        let sim = Sim::new(&map, self.primary.current_flags.sim_flags.opts.clone());
526
527        CameraState::save(ctx.canvas, self.primary.map.get_name());
528        self.primary = PerMap::map_loaded(
529            map,
530            sim,
531            self.primary.current_flags.clone(),
532            &self.opts,
533            &self.cs,
534            ctx,
535            timer,
536        );
537        self.primary.init_camera_for_loaded_map(ctx);
538        self.secondary = None;
539
540        // This may override user settings unexpectedly, but way more often, I think matching units
541        // based on the country is intuitive.
542        self.opts.units.metric = self.primary.map.get_name().city.uses_metric();
543    }
544
545    fn draw_with_opts(&self, g: &mut GfxCtx, opts: DrawOptions) {
546        self.draw(g, opts, &ShowEverything::new());
547    }
548    fn make_warper(
549        &mut self,
550        ctx: &EventCtx,
551        pt: Pt2D,
552        target_cam_zoom: Option<f64>,
553        id: Option<map_gui::ID>,
554    ) -> Box<dyn State<App>> {
555        Warping::new_state(
556            ctx,
557            pt,
558            target_cam_zoom,
559            id.map(|x| x.into()),
560            &mut self.primary,
561        )
562    }
563
564    fn sim_time(&self) -> Time {
565        self.primary.sim.time()
566    }
567
568    fn current_stage_and_remaining_time(&self, id: IntersectionID) -> (usize, Duration) {
569        self.primary.sim.current_stage_and_remaining_time(id)
570    }
571}
572
573pub struct ShowLayers {
574    pub show_buildings: bool,
575    pub show_parking_lots: bool,
576    pub show_intersections: bool,
577    pub show_lanes: bool,
578    pub show_areas: bool,
579    pub show_labels: bool,
580}
581
582impl ShowLayers {
583    pub fn new() -> ShowLayers {
584        ShowLayers {
585            show_buildings: true,
586            show_parking_lots: true,
587            show_intersections: true,
588            show_lanes: true,
589            show_areas: true,
590            show_labels: false,
591        }
592    }
593}
594
595pub trait ShowObject {
596    fn show(&self, obj: &ID) -> bool;
597    fn layers(&self) -> &ShowLayers;
598}
599
600pub struct ShowEverything {
601    layers: ShowLayers,
602}
603
604impl ShowEverything {
605    pub fn new() -> ShowEverything {
606        ShowEverything {
607            layers: ShowLayers::new(),
608        }
609    }
610}
611
612impl ShowObject for ShowEverything {
613    fn show(&self, _: &ID) -> bool {
614        true
615    }
616
617    fn layers(&self) -> &ShowLayers {
618        &self.layers
619    }
620}
621
622#[derive(Clone, StructOpt)]
623pub struct Flags {
624    #[structopt(flatten)]
625    pub sim_flags: SimFlags,
626    /// If true, all map edits immediately apply to the live simulation. Otherwise, most edits
627    /// require resetting to midnight.
628    #[structopt(long)]
629    pub live_map_edits: bool,
630    /// Display an extra area with this name on the map. This gets applied to every map loaded, if
631    /// the area is within map bounds.
632    #[structopt(long)]
633    pub study_area: Option<String>,
634}
635
636/// All of the state that's bound to a specific map.
637pub struct PerMap {
638    pub map: Map,
639    pub draw_map: DrawMap,
640    pub sim: Sim,
641    pub agents: RefCell<AgentCache>,
642
643    pub current_selection: Option<ID>,
644    pub current_flags: Flags,
645    pub last_warped_from: Option<(Pt2D, f64)>,
646    pub sim_cb: Option<Box<dyn SimCallback>>,
647    /// If we ever left edit mode and resumed without restarting from midnight, this is true.
648    pub dirty_from_edits: bool,
649    /// Any ScenarioModifiers in effect?
650    pub has_modified_trips: bool,
651
652    /// If the map has been edited and `app.store_unedited_map_in_secondary` is false, store the
653    /// unedited map here.
654    pub unedited_map: Option<Map>,
655
656    pub layer: Option<Box<dyn Layer>>,
657    /// Only filled out in edit mode. Stored here once to avoid lots of clones. Used for preview.
658    pub suspended_sim: Option<Sim>,
659    /// Only exists in some gameplay modes. Must be carefully reset otherwise. Has the map and
660    /// scenario name too.
661    // TODO Embed that in Analytics directly instead.
662    prebaked: Option<(MapName, String, Analytics)>,
663    /// The most recent Scenario loaded from a file. Don't depend on it always matching the current
664    /// gameplay mode; always verify the name matches what's needed.
665    ///
666    /// Storing this may cost some memory, but otherwise resetting to midnight would require
667    /// loading it again from a file. This is particularly painful on the web!
668    pub scenario: Option<Scenario>,
669
670    /// Is this the original "secondary" state, loaded via --diff?
671    pub is_secondary: bool,
672}
673
674impl PerMap {
675    pub fn map_loaded(
676        mut map: Map,
677        sim: Sim,
678        flags: Flags,
679        opts: &Options,
680        cs: &ColorScheme,
681        ctx: &mut EventCtx,
682        timer: &mut Timer,
683    ) -> PerMap {
684        if let Some(ref name) = flags.study_area {
685            if let Err(err) = add_study_area(&mut map, name) {
686                error!("Didn't apply study area {}: {}", name, err);
687            }
688        }
689
690        timer.start("draw_map");
691        let draw_map = DrawMap::new(ctx, &map, opts, cs, timer);
692        timer.stop("draw_map");
693
694        PerMap {
695            map,
696            draw_map,
697            sim,
698            agents: RefCell::new(AgentCache::new()),
699            current_selection: None,
700            current_flags: flags,
701            last_warped_from: None,
702            sim_cb: None,
703            dirty_from_edits: false,
704            has_modified_trips: false,
705            unedited_map: None,
706            layer: None,
707            suspended_sim: None,
708            prebaked: None,
709            scenario: None,
710            is_secondary: false,
711        }
712    }
713
714    pub fn init_camera_for_loaded_map(&mut self, ctx: &mut EventCtx) {
715        if !CameraState::load(ctx, self.map.get_name()) {
716            // If we didn't restore a previous camera position, start zoomed out, centered on the
717            // map's center.
718            ctx.canvas.cam_zoom = ctx.canvas.min_zoom();
719            ctx.canvas
720                .center_on_map_pt(self.map.get_boundary_polygon().center());
721        }
722    }
723
724    /// Returns whatever was there
725    pub fn clear_sim(&mut self) -> Sim {
726        self.dirty_from_edits = false;
727        std::mem::replace(
728            &mut self.sim,
729            Sim::new(&self.map, self.current_flags.sim_flags.opts.clone()),
730        )
731    }
732
733    pub fn canonical_point(&self, id: ID) -> Option<Pt2D> {
734        match id {
735            ID::Road(id) => self.map.maybe_get_r(id).map(|r| r.center_pts.first_pt()),
736            ID::Lane(id) => self.map.maybe_get_l(id).map(|l| l.first_pt()),
737            ID::Intersection(id) => self.map.maybe_get_i(id).map(|i| i.polygon.center()),
738            ID::Building(id) => self.map.maybe_get_b(id).map(|b| b.polygon.center()),
739            ID::ParkingLot(id) => self.map.maybe_get_pl(id).map(|pl| pl.polygon.center()),
740            ID::Car(id) => self.sim.canonical_pt_for_agent(AgentID::Car(id), &self.map),
741            ID::Pedestrian(id) => self
742                .sim
743                .canonical_pt_for_agent(AgentID::Pedestrian(id), &self.map),
744            ID::PedCrowd(ref members) => self
745                .sim
746                .canonical_pt_for_agent(AgentID::Pedestrian(members[0]), &self.map),
747            ID::TransitStop(id) => self
748                .map
749                .maybe_get_ts(id)
750                .map(|bs| bs.sidewalk_pos.pt(&self.map)),
751            ID::Area(id) => self.map.maybe_get_a(id).map(|a| a.polygon.center()),
752        }
753    }
754
755    // TODO This would be just get_obj and return GameRenderable, but I can't get the type system
756    // to understand that Renderable can act like GameRenderable
757    pub fn get_obj_outline(
758        &self,
759        ctx: &EventCtx,
760        id: ID,
761        cs: &ColorScheme,
762        map: &Map,
763        agents: &mut AgentCache,
764    ) -> Option<Tessellation> {
765        let on = match id {
766            ID::Car(id) => {
767                // Cars might be parked in a garage!
768                self.sim.get_draw_car(id, &self.map)?.on
769            }
770            ID::Pedestrian(id) => self.sim.get_draw_ped(id, &self.map).unwrap().on,
771            ID::PedCrowd(ref members) => {
772                // If the first member has vanished, just give up
773                self.sim.get_draw_ped(members[0], &self.map)?.on
774            }
775            _ => {
776                // Static map elements
777                let obj = self.draw_map.get_obj(id.to_map_gui());
778                return Some(obj.get_outline(map));
779            }
780        };
781
782        agents.populate_if_needed(on, &self.map, &self.sim, cs, ctx.prerender);
783
784        // Why might this fail? Pedestrians merge into crowds, and crowds dissipate into
785        // individuals
786        let obj = agents.get(on).into_iter().find(|r| r.get_id() == id)?;
787        Some(obj.get_outline(map))
788    }
789}
790
791// TODO Serialize these, but in a very careful, future-compatible way
792pub struct SessionState {
793    pub tutorial: Option<TutorialState>,
794    pub high_scores: BTreeMap<GameplayMode, Vec<HighScore>>,
795    pub info_panel_tab: BTreeMap<&'static str, &'static str>,
796    pub last_gmns_timing_csv: Option<(String, Vec<u8>)>,
797    pub dash_tab: DashTab,
798    pub buffer_lane_type: LaneType,
799
800    // Specific to the ungap tool
801    pub elevation_contours: Cached<MapName, (FindClosest<Distance>, ToggleZoomed)>,
802    pub routing_preferences: crate::ungap::RoutingPreferences,
803    pub ungap_current_trip_name: Option<String>,
804    // Map and edit change key
805    pub mode_shift: Cached<(MapName, usize), crate::ungap::ModeShiftData>,
806}
807
808impl SessionState {
809    pub fn empty() -> SessionState {
810        SessionState {
811            tutorial: None,
812            high_scores: BTreeMap::new(),
813            info_panel_tab: btreemap! {
814                "lane" => "info",
815                "intersection" => "info",
816                "bldg" => "info",
817                "person" => "trips",
818                "bus" => "status",
819            },
820            last_gmns_timing_csv: None,
821            dash_tab: DashTab::TripTable,
822            buffer_lane_type: LaneType::Buffer(BufferType::Stripes),
823
824            elevation_contours: Cached::new(),
825            routing_preferences: crate::ungap::RoutingPreferences::default(),
826            ungap_current_trip_name: None,
827            mode_shift: Cached::new(),
828        }
829    }
830}
831
832// TODO Reconsider this; maybe it does belong in widgetry.
833pub struct PerObjectActions {
834    pub click_action: Option<String>,
835}
836
837impl PerObjectActions {
838    pub fn new() -> PerObjectActions {
839        PerObjectActions { click_action: None }
840    }
841
842    pub fn reset(&mut self) {
843        self.click_action = None;
844    }
845
846    pub fn left_click<S: Into<String>>(&mut self, ctx: &mut EventCtx, label: S) -> bool {
847        let label = label.into();
848        if let Some(ref old) = self.click_action {
849            panic!("left_click for \"{old}\" already called; can't also do \"{label}\"");
850        }
851        self.click_action = Some(label);
852        ctx.normal_left_click()
853    }
854}
855
856pub struct FindDelayedIntersections {
857    pub halt_limit: Duration,
858    pub report_limit: Duration,
859
860    pub currently_delayed: Vec<(IntersectionID, Time)>,
861}
862
863impl SimCallback for FindDelayedIntersections {
864    fn run(&mut self, sim: &Sim, _: &Map) -> bool {
865        self.currently_delayed = sim.delayed_intersections(self.report_limit);
866        if let Some((_, t)) = self.currently_delayed.get(0) {
867            sim.time() - *t >= self.halt_limit
868        } else {
869            false
870        }
871    }
872}
873
874impl SharedAppState for App {
875    fn before_event(&mut self) {
876        self.per_obj.reset();
877    }
878
879    fn draw_default(&self, g: &mut GfxCtx) {
880        self.draw(g, DrawOptions::new(), &ShowEverything::new());
881    }
882
883    fn dump_before_abort(&self, canvas: &Canvas) {
884        println!();
885        println!(
886            "********************************************************************************"
887        );
888        CameraState::save(canvas, self.primary.map.get_name());
889        println!(
890            "Crash! Please report to https://github.com/a-b-street/abstreet/issues/ and include \
891             all output.txt; at least everything starting from the stack trace above!"
892        );
893
894        println!();
895        self.primary.sim.dump_before_abort();
896
897        println!();
898        println!("Camera:");
899        println!(
900            r#"{{ "cam_x": {}, "cam_y": {}, "cam_zoom": {} }}"#,
901            canvas.cam_x, canvas.cam_y, canvas.cam_zoom
902        );
903
904        println!();
905        if self.primary.map.get_edits().commands.is_empty() {
906            println!("No edits");
907        } else {
908            abstio::write_json(
909                "edits_during_crash.json".to_string(),
910                &self.primary.map.get_edits().to_permanent(&self.primary.map),
911            );
912            println!("Please include edits_during_crash.json in your bug report.");
913        }
914
915        // Repeat, because it can be hard to see the top of the report if it's long
916        println!();
917        println!(
918            "Crash! Please report to https://github.com/a-b-street/abstreet/issues/ and include \
919             all output.txt; at least everything above here until the start of the report!"
920        );
921        println!(
922            "********************************************************************************"
923        );
924    }
925
926    fn before_quit(&self, canvas: &Canvas) {
927        CameraState::save(canvas, self.primary.map.get_name());
928    }
929
930    fn free_memory(&mut self) {
931        self.primary.draw_map.free_memory();
932    }
933}
934
935/// Load an extra GeoJSON file, and add the area to the map dynamically.
936fn add_study_area(map: &mut Map, name: &str) -> Result<()> {
937    let require_in_bounds = true;
938    for (polygon, tags) in Polygon::from_geojson_bytes(
939        &abstio::slurp_file(abstio::path(format!("system/study_areas/{}.geojson", name)))?,
940        map.get_gps_bounds(),
941        require_in_bounds,
942    )? {
943        map.hack_add_area(AreaType::StudyArea, polygon, Tags::new(tags));
944    }
945    Ok(())
946}