game/ungap/trip/
mod.rs

1use map_gui::tools::{InputWaypoints, TripManagement, TripManagementState, WaypointID};
2use map_model::{PathConstraints, RoutingParams};
3use widgetry::mapspace::{ObjectID, World, WorldOutcome};
4use widgetry::{
5    ControlState, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, Text, Toggle, Widget,
6};
7
8use self::results::RouteDetails;
9use crate::app::{App, Transition};
10use crate::ungap::{Layers, Tab, TakeLayers};
11
12mod results;
13
14pub struct TripPlanner {
15    layers: Layers,
16
17    input_panel: Panel,
18    waypoints: InputWaypoints,
19    main_route: RouteDetails,
20    files: TripManagement<App, TripPlanner>,
21    // TODO We really only need to store preferences and stats, but...
22    alt_routes: Vec<RouteDetails>,
23    world: World<ID>,
24}
25
26impl TakeLayers for TripPlanner {
27    fn take_layers(self) -> Layers {
28        self.layers
29    }
30}
31
32impl TripManagementState<App> for TripPlanner {
33    fn mut_files(&mut self) -> &mut TripManagement<App, Self> {
34        &mut self.files
35    }
36
37    fn app_session_current_trip_name(app: &mut App) -> &mut Option<String> {
38        &mut app.session.ungap_current_trip_name
39    }
40
41    fn sync_from_file_management(&mut self, ctx: &mut EventCtx, app: &mut App) {
42        self.waypoints
43            .overwrite(app, self.files.current.waypoints.clone());
44        self.recalculate_routes(ctx, app);
45    }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
49enum ID {
50    MainRoute,
51    AltRoute(usize),
52    Waypoint(WaypointID),
53}
54impl ObjectID for ID {}
55
56impl TripPlanner {
57    pub fn new_state(ctx: &mut EventCtx, app: &mut App, layers: Layers) -> Box<dyn State<App>> {
58        ctx.loading_screen("apply edits", |_, timer| {
59            app.primary.map.recalculate_pathfinding_after_edits(timer);
60        });
61
62        let mut rp = TripPlanner {
63            layers,
64
65            input_panel: Panel::empty(ctx),
66            waypoints: InputWaypoints::new(app, vec![PathConstraints::Bike]),
67            main_route: RouteDetails::main_route(ctx, app, Vec::new()).details,
68            files: TripManagement::new(app),
69            alt_routes: Vec::new(),
70            world: World::new(),
71        };
72
73        if let Some(current_name) = &app.session.ungap_current_trip_name {
74            rp.files.set_current(current_name);
75        }
76        rp.sync_from_file_management(ctx, app);
77        Box::new(rp)
78    }
79
80    // Use the current session settings to determine "main" and alts
81    fn recalculate_routes(&mut self, ctx: &mut EventCtx, app: &mut App) {
82        let mut world = World::new();
83
84        let main_route = RouteDetails::main_route(ctx, app, self.waypoints.get_waypoints());
85        self.main_route = main_route.details;
86        if !main_route.hitboxes.is_empty() {
87            world
88                .add(ID::MainRoute)
89                .hitboxes(main_route.hitboxes)
90                .zorder(1)
91                .draw(main_route.draw)
92                .build(ctx);
93        }
94
95        self.files.autosave(app);
96        // This doesn't depend on the alt routes, so just do it here
97        self.update_input_panel(ctx, app, main_route.details_widget);
98
99        self.alt_routes.clear();
100        // Just show one alternate trip by default, unless the user enables one checkbox but not
101        // the other. We could show more variations, but it makes the view too messy.
102        for preferences in [
103            RoutingPreferences {
104                avoid_hills: false,
105                avoid_stressful_roads: false,
106            },
107            RoutingPreferences {
108                avoid_hills: true,
109                avoid_stressful_roads: true,
110            },
111        ] {
112            if app.session.routing_preferences == preferences
113                || self.waypoints.get_waypoints().len() < 2
114            {
115                continue;
116            }
117            let mut alt = RouteDetails::alt_route(
118                ctx,
119                app,
120                self.waypoints.get_waypoints(),
121                &self.main_route,
122                preferences,
123            );
124            // Dedupe equivalent routes based on their stats, which is usually detailed enough
125            if alt.details.stats != self.main_route.stats
126                && self.alt_routes.iter().all(|x| alt.details.stats != x.stats)
127                && !alt.hitboxes.is_empty()
128            {
129                self.alt_routes.push(alt.details);
130                world
131                    .add(ID::AltRoute(self.alt_routes.len() - 1))
132                    .hitboxes(alt.hitboxes)
133                    .zorder(0)
134                    .draw(alt.draw)
135                    .hover_alpha(0.8)
136                    .tooltip(alt.tooltip_for_alt.take().unwrap())
137                    .clickable()
138                    .build(ctx);
139            }
140        }
141
142        self.waypoints
143            .rebuild_world(ctx, &mut world, ID::Waypoint, 2);
144
145        world.initialize_hover(ctx);
146        world.rebuilt_during_drag(ctx, &self.world);
147        self.world = world;
148    }
149
150    fn update_input_panel(&mut self, ctx: &mut EventCtx, app: &App, main_route: Widget) {
151        let mut sections = vec![Widget::col(vec![
152            self.files.get_panel_widget(ctx),
153            Widget::horiz_separator(ctx, 1.0),
154            self.waypoints.get_panel_widget(ctx),
155        ])
156        .section(ctx)];
157        if self.waypoints.len() >= 2 {
158            sections.push(
159                Widget::row(vec![
160                    Toggle::checkbox(
161                        ctx,
162                        "Avoid steep hills",
163                        None,
164                        app.session.routing_preferences.avoid_hills,
165                    ),
166                    Toggle::checkbox(
167                        ctx,
168                        "Avoid stressful roads",
169                        None,
170                        app.session.routing_preferences.avoid_stressful_roads,
171                    ),
172                ])
173                .section(ctx),
174            );
175            sections.push(main_route.section(ctx));
176        }
177
178        let col = Widget::col(sections);
179        let mut new_panel = Tab::Trip.make_left_panel(ctx, app, col);
180
181        // TODO After scrolling down and dragging a slider, sometimes releasing the slider
182        // registers as clicking "X" on the waypoints! Maybe just replace() in that case?
183        new_panel.restore_scroll(ctx, &self.input_panel);
184        self.input_panel = new_panel;
185    }
186}
187
188impl State<App> for TripPlanner {
189    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
190        let world_outcome_for_waypoints = match self.world.event(ctx) {
191            WorldOutcome::ClickedObject(ID::AltRoute(idx)) => {
192                // Switch routes
193                app.session.routing_preferences = self.alt_routes[idx].preferences;
194                self.recalculate_routes(ctx, app);
195                return Transition::Keep;
196            }
197            x => x
198                .maybe_map_id(|id| match id {
199                    ID::Waypoint(id) => Some(id),
200                    // Ignore HoverChanged events
201                    _ => None,
202                })
203                .unwrap_or(WorldOutcome::Nothing),
204        };
205
206        let panel_outcome = self.input_panel.event(ctx);
207        if let Outcome::Clicked(ref x) = panel_outcome {
208            if let Some(t) = Tab::Trip.handle_action::<TripPlanner>(ctx, app, x) {
209                return t;
210            }
211            if let Some(t) = self.files.on_click(ctx, app, x) {
212                // Bit hacky...
213                if matches!(t, Transition::Keep) {
214                    self.sync_from_file_management(ctx, app);
215                }
216                return t;
217            }
218            if x == "show original map" || x == "show edited map" {
219                app.swap_map();
220                // We're assuming building and intersection IDs haven't changed
221                self.recalculate_routes(ctx, app);
222                // Immediately recalculate the edited layer
223                self.layers.event(ctx, app);
224                return Transition::Keep;
225            }
226        }
227        if let Outcome::Changed(ref x) = panel_outcome {
228            if x == "Avoid steep hills" || x == "Avoid stressful roads" {
229                app.session.routing_preferences = RoutingPreferences {
230                    avoid_hills: self.input_panel.is_checked("Avoid steep hills"),
231                    avoid_stressful_roads: self.input_panel.is_checked("Avoid stressful roads"),
232                };
233                self.recalculate_routes(ctx, app);
234                return Transition::Keep;
235            }
236        }
237        // Send all other outcomes here
238        // TODO This routing of outcomes and the brittle ordering totally breaks encapsulation :(
239        if let Some(t) = self
240            .main_route
241            .event(ctx, app, &panel_outcome, &mut self.input_panel)
242        {
243            return t;
244        }
245
246        if self
247            .waypoints
248            .event(app, panel_outcome, world_outcome_for_waypoints)
249        {
250            // Sync from waypoints to file management
251            // TODO Maaaybe this directly live in the InputWaypoints system?
252            self.files.current.waypoints = self.waypoints.get_waypoints();
253            self.recalculate_routes(ctx, app);
254        }
255
256        if let Some(t) = self.layers.event(ctx, app) {
257            return t;
258        }
259
260        Transition::Keep
261    }
262
263    fn draw(&self, g: &mut GfxCtx, app: &App) {
264        self.layers.draw(g, app);
265        self.input_panel.draw(g);
266        self.world.draw(g);
267        self.main_route.draw(g, &self.input_panel);
268    }
269
270    fn on_destroy(&mut self, _: &mut EventCtx, app: &mut App) {
271        // When we switch tabs, always use the edited map
272        if app.primary.is_secondary {
273            app.swap_map();
274        }
275    }
276}
277
278#[derive(Clone, Copy, PartialEq)]
279pub struct RoutingPreferences {
280    avoid_hills: bool,
281    avoid_stressful_roads: bool,
282}
283
284impl RoutingPreferences {
285    // TODO Consider changing this now, and also for the mode shift calculation
286    pub fn default() -> Self {
287        Self {
288            avoid_hills: false,
289            avoid_stressful_roads: false,
290        }
291    }
292
293    fn name(self) -> &'static str {
294        match (self.avoid_hills, self.avoid_stressful_roads) {
295            (false, false) => "fastest",
296            (true, false) => "flat",
297            (false, true) => "low-stress",
298            (true, true) => "flat & low-stress",
299        }
300    }
301
302    fn routing_params(self) -> RoutingParams {
303        RoutingParams {
304            avoid_steep_incline_penalty: if self.avoid_hills { 2.0 } else { 1.0 },
305            avoid_high_stress: if self.avoid_stressful_roads { 2.0 } else { 1.0 },
306            ..Default::default()
307        }
308    }
309}
310
311fn before_after_button(ctx: &mut EventCtx, app: &App) -> Widget {
312    let edits = app.primary.map.get_edits();
313    if app.secondary.is_none() {
314        return Widget::nothing();
315    }
316    let (txt, label) = if edits.commands.is_empty() {
317        (
318            Text::from_all(vec![
319                Line("After / ").secondary(),
320                Line("Before"),
321                Line(" proposal"),
322            ]),
323            "show edited map",
324        )
325    } else {
326        (
327            Text::from_all(vec![
328                Line("After"),
329                Line(" / Before").secondary(),
330                Line(" proposal"),
331            ]),
332            "show original map",
333        )
334    };
335
336    ctx.style()
337        .btn_outline
338        .btn()
339        .label_styled_text(txt, ControlState::Default)
340        .hotkey(Key::Slash)
341        .build_widget(ctx, label)
342}