map_gui/
simple_app.rs

1use structopt::StructOpt;
2
3use abstio::MapName;
4use abstutil::Timer;
5use geom::{Circle, Distance, Duration, Pt2D, Time};
6use map_model::{IntersectionID, Map};
7use widgetry::tools::URLManager;
8use widgetry::{Canvas, EventCtx, GfxCtx, Settings, SharedAppState, State, Transition, Warper};
9
10use crate::colors::{ColorScheme, ColorSchemeChoice};
11use crate::load::MapLoader;
12use crate::options::Options;
13use crate::render::DrawMap;
14use crate::render::{DrawOptions, Renderable};
15use crate::tools::CameraState;
16use crate::{AppLike, ID};
17
18/// Simple app state that just renders a static map, without any dynamic agents on the map.
19pub struct SimpleApp<T> {
20    pub map: Map,
21    pub draw_map: DrawMap,
22    pub cs: ColorScheme,
23    pub opts: Options,
24    pub current_selection: Option<ID>,
25    /// Custom per-app state can be stored here
26    pub session: T,
27    /// If desired, this can be advanced to render traffic signals changing.
28    pub time: Time,
29}
30
31// A SimpleApp can directly use this (`let args = SimpleAppArgs::from_iter(abstutil::cli_args())`)
32// or embed in their own struct and define other flags.
33#[derive(StructOpt)]
34pub struct SimpleAppArgs {
35    /// Path to a map to initially load. If not provided, load the last map used or a fixed
36    /// default.
37    #[structopt()]
38    pub map_path: Option<String>,
39    /// Initially position the camera here. The format is an OSM-style `zoom/lat/lon` string
40    /// (https://wiki.openstreetmap.org/wiki/Browsing#Other_URL_tricks).
41    #[structopt(long)]
42    pub cam: Option<String>,
43    /// Dev mode exposes experimental tools useful for debugging, but that'd likely confuse most
44    /// players.
45    #[structopt(long)]
46    pub dev: bool,
47    /// The color scheme for map elements, agents, and the UI.
48    #[structopt(long, parse(try_from_str = ColorSchemeChoice::parse))]
49    pub color_scheme: Option<ColorSchemeChoice>,
50    /// When making a screen recording, enable this option to hide some UI elements
51    #[structopt(long)]
52    pub minimal_controls: bool,
53    /// Override the monitor's auto-detected scale factor
54    #[structopt(long)]
55    pub scale_factor: Option<f64>,
56}
57
58impl SimpleAppArgs {
59    /// Options are passed in by each app, usually seeded with defaults or from a config file.  For
60    /// the few options that we allow to be specified by command-line, overwrite the values.
61    pub fn override_options(&self, opts: &mut Options) {
62        opts.dev = self.dev;
63        opts.minimal_controls = self.minimal_controls;
64        if let Some(cs) = self.color_scheme {
65            opts.color_scheme = cs;
66            opts.toggle_day_night_colors = false;
67        }
68    }
69
70    pub fn update_widgetry_settings(&self, mut settings: Settings) -> Settings {
71        settings = settings
72            .read_svg(Box::new(abstio::slurp_bytes))
73            .window_icon(abstio::path("system/assets/pregame/icon.png"));
74        if let Some(s) = self.scale_factor {
75            settings = settings.scale_factor(s);
76        }
77        settings
78    }
79
80    pub fn map_name(&self) -> MapName {
81        self.map_path
82            .as_ref()
83            .map(|path| {
84                MapName::from_path(path).unwrap_or_else(|| panic!("bad map path: {}", path))
85            })
86            .or_else(|| {
87                abstio::maybe_read_json::<crate::tools::DefaultMap>(
88                    abstio::path_player("maps.json"),
89                    &mut Timer::throwaway(),
90                )
91                .ok()
92                .map(|x| x.last_map)
93            })
94            .unwrap_or_else(|| MapName::seattle("montlake"))
95    }
96}
97
98impl<T: 'static> SimpleApp<T> {
99    pub fn new<
100        F: 'static + Fn(&mut EventCtx, &mut SimpleApp<T>) -> Vec<Box<dyn State<SimpleApp<T>>>>,
101    >(
102        ctx: &mut EventCtx,
103        opts: Options,
104        map_name: Option<MapName>,
105        cam: Option<String>,
106        session: T,
107        init_states: F,
108    ) -> (SimpleApp<T>, Vec<Box<dyn State<SimpleApp<T>>>>) {
109        abstutil::logger::setup();
110        ctx.canvas.settings = opts.canvas_settings.clone();
111
112        let cs = ColorScheme::new(ctx, opts.color_scheme);
113        // Start with a minimal map
114        let map = Map::almost_blank();
115        let draw_map = DrawMap::new(ctx, &map, &opts, &cs, &mut Timer::throwaway());
116        let mut app = SimpleApp {
117            map,
118            draw_map,
119            cs,
120            opts,
121            current_selection: None,
122            session,
123            time: Time::START_OF_DAY,
124        };
125
126        let states = if let Some(map_name) = map_name {
127            vec![MapLoader::new_state(
128                ctx,
129                &app,
130                map_name,
131                Box::new(move |ctx, app| {
132                    URLManager::change_camera(ctx, cam.as_ref(), app.map().get_gps_bounds());
133                    Transition::Clear(init_states(ctx, app))
134                }),
135            )]
136        } else {
137            init_states(ctx, &mut app)
138        };
139        (app, states)
140    }
141
142    pub fn draw_unzoomed(&self, g: &mut GfxCtx) {
143        g.clear(self.cs.void_background);
144        g.redraw(&self.draw_map.boundary_polygon);
145        g.redraw(&self.draw_map.draw_all_areas);
146        g.redraw(&self.draw_map.draw_all_unzoomed_parking_lots);
147        g.redraw(&self.draw_map.draw_all_unzoomed_roads_and_intersections);
148        g.redraw(&self.draw_map.draw_all_buildings);
149        g.redraw(&self.draw_map.draw_all_building_outlines);
150        // Not the building paths
151
152        // Still show some shape selection when zoomed out.
153        // TODO Refactor! Ideally use get_obj
154        if let Some(ID::Area(id)) = self.current_selection {
155            g.draw_polygon(
156                self.cs.selected,
157                self.draw_map.get_a(id).get_outline(&self.map),
158            );
159        } else if let Some(ID::Road(id)) = self.current_selection {
160            g.draw_polygon(
161                self.cs.selected,
162                self.draw_map.get_r(id).get_outline(&self.map),
163            );
164        } else if let Some(ID::Intersection(id)) = self.current_selection {
165            // Actually, don't use get_outline here! Full polygon is easier to see.
166            g.draw_polygon(self.cs.selected, self.map.get_i(id).polygon.clone());
167        } else if let Some(ID::Building(id)) = self.current_selection {
168            g.draw_polygon(self.cs.selected, self.map.get_b(id).polygon.clone());
169        }
170    }
171
172    pub fn draw_zoomed(&self, g: &mut GfxCtx, opts: DrawOptions) {
173        g.clear(self.cs.void_background);
174        g.redraw(&self.draw_map.boundary_polygon);
175
176        let objects = self
177            .draw_map
178            .get_renderables_back_to_front(g.get_screen_bounds(), &self.map);
179
180        let mut drawn_all_buildings = false;
181        let mut drawn_all_areas = false;
182
183        for obj in objects {
184            obj.draw(g, self, &opts);
185
186            match obj.get_id() {
187                ID::Building(_) => {
188                    if !drawn_all_buildings {
189                        g.redraw(&self.draw_map.draw_all_buildings);
190                        g.redraw(&self.draw_map.draw_all_building_outlines);
191                        drawn_all_buildings = true;
192                    }
193                }
194                ID::Area(_) => {
195                    if !drawn_all_areas {
196                        g.redraw(&self.draw_map.draw_all_areas);
197                        drawn_all_areas = true;
198                    }
199                }
200                _ => {}
201            }
202
203            if self.current_selection == Some(obj.get_id()) {
204                g.draw_polygon(self.cs.selected, obj.get_outline(&self.map));
205            }
206        }
207    }
208
209    /// Assumes some defaults.
210    pub fn recalculate_current_selection(&mut self, ctx: &EventCtx) {
211        self.current_selection = self.calculate_current_selection(ctx, false, false);
212    }
213
214    // TODO Returns anything; I think it should just return roads
215    pub fn mouseover_unzoomed_roads_and_intersections(&self, ctx: &EventCtx) -> Option<ID> {
216        self.calculate_current_selection(ctx, true, false)
217    }
218    /// Only select buildings, and work whether zoomed in or not.
219    pub fn mouseover_unzoomed_buildings(&self, ctx: &EventCtx) -> Option<ID> {
220        self.calculate_current_selection(ctx, false, true)
221            .filter(|id| matches!(id, ID::Building(_)))
222    }
223
224    fn calculate_current_selection(
225        &self,
226        ctx: &EventCtx,
227        unzoomed_roads_and_intersections: bool,
228        unzoomed_buildings: bool,
229    ) -> Option<ID> {
230        // Unzoomed mode. Ignore when debugging areas.
231        if ctx.canvas.is_unzoomed() && !(unzoomed_roads_and_intersections || unzoomed_buildings) {
232            return None;
233        }
234
235        let pt = ctx.canvas.get_cursor_in_map_space()?;
236
237        let mut objects = self.draw_map.get_renderables_back_to_front(
238            Circle::new(pt, Distance::meters(3.0)).get_bounds(),
239            &self.map,
240        );
241        objects.reverse();
242
243        for obj in objects {
244            match obj.get_id() {
245                ID::Road(_) => {
246                    if !unzoomed_roads_and_intersections || ctx.canvas.is_zoomed() {
247                        continue;
248                    }
249                }
250                ID::Intersection(_) => {
251                    if ctx.canvas.is_unzoomed() && !unzoomed_roads_and_intersections {
252                        continue;
253                    }
254                }
255                ID::Building(_) => {
256                    if ctx.canvas.is_unzoomed() && !unzoomed_buildings {
257                        continue;
258                    }
259                }
260                _ => {
261                    if ctx.canvas.is_unzoomed() {
262                        continue;
263                    }
264                }
265            }
266            if obj.contains_pt(pt, &self.map) {
267                return Some(obj.get_id());
268            }
269        }
270        None
271    }
272}
273
274impl<T: 'static> AppLike for SimpleApp<T> {
275    #[inline]
276    fn map(&self) -> &Map {
277        &self.map
278    }
279    #[inline]
280    fn cs(&self) -> &ColorScheme {
281        &self.cs
282    }
283    #[inline]
284    fn mut_cs(&mut self) -> &mut ColorScheme {
285        &mut self.cs
286    }
287    #[inline]
288    fn draw_map(&self) -> &DrawMap {
289        &self.draw_map
290    }
291    #[inline]
292    fn mut_draw_map(&mut self) -> &mut DrawMap {
293        &mut self.draw_map
294    }
295    #[inline]
296    fn opts(&self) -> &Options {
297        &self.opts
298    }
299    #[inline]
300    fn mut_opts(&mut self) -> &mut Options {
301        &mut self.opts
302    }
303
304    fn map_switched(&mut self, ctx: &mut EventCtx, map: Map, timer: &mut Timer) {
305        CameraState::save(ctx.canvas, self.map.get_name());
306        self.map = map;
307        self.draw_map = DrawMap::new(ctx, &self.map, &self.opts, &self.cs, timer);
308        if !CameraState::load(ctx, self.map.get_name()) {
309            // If we didn't restore a previous camera position, start zoomed out, centered on the
310            // map's center.
311            ctx.canvas.cam_zoom = ctx.canvas.min_zoom();
312            ctx.canvas
313                .center_on_map_pt(self.map.get_boundary_polygon().center());
314        }
315
316        self.opts.units.metric = self.map.get_name().city.uses_metric();
317    }
318
319    fn draw_with_opts(&self, g: &mut GfxCtx, opts: DrawOptions) {
320        if g.canvas.is_unzoomed() {
321            self.draw_unzoomed(g);
322        } else {
323            self.draw_zoomed(g, opts);
324        }
325    }
326
327    fn make_warper(
328        &mut self,
329        ctx: &EventCtx,
330        pt: Pt2D,
331        target_cam_zoom: Option<f64>,
332        _: Option<ID>,
333    ) -> Box<dyn State<SimpleApp<T>>> {
334        Box::new(SimpleWarper {
335            warper: Warper::new(ctx, pt, target_cam_zoom),
336        })
337    }
338
339    fn sim_time(&self) -> Time {
340        self.time
341    }
342
343    fn current_stage_and_remaining_time(&self, id: IntersectionID) -> (usize, Duration) {
344        let signal = self.map.get_traffic_signal(id);
345        let mut time_left = (self.time - Time::START_OF_DAY) % signal.simple_cycle_duration();
346        for (idx, stage) in signal.stages.iter().enumerate() {
347            if time_left < stage.stage_type.simple_duration() {
348                return (idx, time_left);
349            }
350            time_left -= stage.stage_type.simple_duration();
351        }
352        unreachable!()
353    }
354}
355
356impl<T: 'static> SharedAppState for SimpleApp<T> {
357    fn draw_default(&self, g: &mut GfxCtx) {
358        self.draw_with_opts(g, DrawOptions::new());
359    }
360
361    fn dump_before_abort(&self, canvas: &Canvas) {
362        CameraState::save(canvas, self.map.get_name());
363    }
364
365    fn before_quit(&self, canvas: &Canvas) {
366        CameraState::save(canvas, self.map.get_name());
367    }
368
369    fn free_memory(&mut self) {
370        self.draw_map.free_memory();
371    }
372}
373
374struct SimpleWarper {
375    warper: Warper,
376}
377
378impl<T> State<SimpleApp<T>> for SimpleWarper {
379    fn event(&mut self, ctx: &mut EventCtx, _: &mut SimpleApp<T>) -> Transition<SimpleApp<T>> {
380        if self.warper.event(ctx) {
381            Transition::Keep
382        } else {
383            Transition::Pop
384        }
385    }
386
387    fn draw(&self, _: &mut GfxCtx, _: &SimpleApp<T>) {}
388}