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 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 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 self.update_input_panel(ctx, app, main_route.details_widget);
98
99 self.alt_routes.clear();
100 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 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 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 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 _ => 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 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 self.recalculate_routes(ctx, app);
222 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 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 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 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 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}