1use std::collections::{BTreeMap, HashSet};
2use std::marker::PhantomData;
3
4use serde::{Deserialize, Serialize};
5
6use abstutil::Timer;
7use synthpop::TripEndpoint;
8use widgetry::tools::ChooseSomething;
9use widgetry::{
10 Choice, Color, EventCtx, GfxCtx, Key, Line, Panel, SimpleState, State, Text, TextBox, TextExt,
11 Transition, Widget,
12};
13
14use crate::tools::grey_out_map;
15use crate::AppLike;
16
17pub struct TripManagement<A: AppLike + 'static, S: TripManagementState<A>> {
21 pub current: NamedTrip,
22 all: SavedTrips,
24
25 app_type: PhantomData<A>,
26 state_type: PhantomData<S>,
27}
28
29pub trait TripManagementState<A: AppLike + 'static>: State<A> {
30 fn mut_files(&mut self) -> &mut TripManagement<A, Self>
31 where
32 Self: Sized;
33 fn app_session_current_trip_name(app: &mut A) -> &mut Option<String>
34 where
35 Self: Sized;
36 fn sync_from_file_management(&mut self, ctx: &mut EventCtx, app: &mut A);
37}
38
39#[derive(Clone, PartialEq, Serialize, Deserialize)]
40pub struct NamedTrip {
41 name: String,
42 pub waypoints: Vec<TripEndpoint>,
43}
44
45#[derive(Serialize, Deserialize)]
46struct SavedTrips {
47 trips: BTreeMap<String, NamedTrip>,
48}
49
50impl SavedTrips {
51 fn load(app: &dyn AppLike) -> SavedTrips {
52 let map_name = app.map().get_name();
55 if map_name.city.city == "oneshot" && map_name.map.starts_with("imported_") {
56 return SavedTrips {
57 trips: BTreeMap::new(),
58 };
59 }
60
61 abstio::maybe_read_json::<SavedTrips>(
62 abstio::path_trips(app.map().get_name()),
63 &mut Timer::throwaway(),
64 )
65 .unwrap_or_else(|_| SavedTrips {
66 trips: BTreeMap::new(),
67 })
68 }
69
70 fn save(&self, app: &dyn AppLike) {
72 abstio::write_json(abstio::path_trips(app.map().get_name()), self);
73 }
74
75 fn prev(&self, current: &str) -> Option<&NamedTrip> {
76 if self.trips.contains_key(current) {
78 self.trips
79 .range(..current.to_string())
80 .next_back()
81 .map(|pair| pair.1)
82 } else {
83 self.trips.values().last()
84 }
85 }
86
87 fn next(&self, current: &str) -> Option<&NamedTrip> {
88 if self.trips.contains_key(current) {
89 let mut iter = self.trips.range(current.to_string()..);
90 iter.next();
91 iter.next().map(|pair| pair.1)
92 } else {
93 None
94 }
95 }
96
97 fn len(&self) -> usize {
98 self.trips.len()
99 }
100
101 fn new_name(&self) -> String {
102 let mut i = self.trips.len() + 1;
103 loop {
104 let name = format!("Trip {}", i);
105 if self.trips.contains_key(&name) {
106 i += 1;
107 } else {
108 return name;
109 }
110 }
111 }
112}
113
114impl<A: AppLike + 'static, S: TripManagementState<A>> TripManagement<A, S> {
115 pub fn new(app: &A) -> TripManagement<A, S> {
116 let all = SavedTrips::load(app);
117 let current = all
118 .trips
119 .iter()
120 .next()
121 .map(|(_k, v)| v.clone())
122 .unwrap_or(NamedTrip {
123 name: all.new_name(),
124 waypoints: Vec::new(),
125 });
126 TripManagement {
127 all,
128 current,
129 app_type: PhantomData,
130 state_type: PhantomData,
131 }
132 }
133
134 pub fn get_panel_widget(&self, ctx: &mut EventCtx) -> Widget {
135 let current_name = &self.current.name;
136 Widget::row(vec![
137 Widget::row(vec![
138 ctx.style()
139 .btn_prev()
140 .hotkey(Key::LeftArrow)
141 .disabled(self.all.prev(current_name).is_none())
142 .build_widget(ctx, "previous trip"),
143 ctx.style()
144 .btn_plain
145 .btn()
146 .label_underlined_text(current_name)
147 .build_widget(ctx, "rename trip"),
148 ctx.style()
149 .btn_next()
150 .hotkey(Key::RightArrow)
151 .disabled(self.all.next(current_name).is_none())
152 .build_widget(ctx, "next trip"),
153 ]),
154 Widget::row(vec![
155 ctx.style()
156 .btn_plain
157 .icon("system/assets/speed/plus.svg")
158 .disabled(self.current.waypoints.is_empty())
159 .build_widget(ctx, "Start new trip"),
160 ctx.style()
161 .btn_plain
162 .icon("system/assets/tools/folder.svg")
163 .disabled(self.all.len() < 2)
164 .build_widget(ctx, "Load another trip"),
165 ctx.style()
166 .btn_plain
167 .icon("system/assets/tools/trash.svg")
168 .disabled(self.current.waypoints.is_empty())
169 .build_widget(ctx, "Delete"),
170 ctx.style()
172 .btn_plain
173 .icon("system/assets/tools/help.svg")
174 .tooltip("Click to add a waypoint, drag to move one")
175 .build_widget(ctx, "waypoint instructions"),
176 ])
177 .align_right(),
178 ])
179 }
180
181 pub fn autosave(&mut self, app: &mut A) {
183 match self.all.trips.get(&self.current.name) {
184 None if self.current.waypoints.is_empty() => return,
185 Some(existing) if existing == &self.current => return,
186 _ => {}
187 }
188
189 self.all
190 .trips
191 .insert(self.current.name.clone(), self.current.clone());
192 self.all.save(app);
193 self.save_current_trip_to_session(app);
194 }
195
196 pub fn set_current(&mut self, name: &str) {
197 if self.all.trips.contains_key(name) {
198 self.current = self.all.trips[name].clone();
199 }
200 }
201
202 pub fn add_new_trip(&mut self, app: &mut A, from: TripEndpoint, to: TripEndpoint) {
203 self.current = NamedTrip {
204 name: self.all.new_name(),
205 waypoints: vec![from, to],
206 };
207 self.all
208 .trips
209 .insert(self.current.name.clone(), self.current.clone());
210 self.all.save(app);
211 self.save_current_trip_to_session(app);
212 }
213
214 pub fn on_click(
215 &mut self,
216 ctx: &mut EventCtx,
217 app: &mut A,
218 action: &str,
219 ) -> Option<Transition<A>> {
220 match action {
221 "Delete" => {
222 if self.all.trips.remove(&self.current.name).is_some() {
223 self.all.save(app);
224 }
225 self.current = self
226 .all
227 .trips
228 .iter()
229 .next()
230 .map(|(_k, v)| v.clone())
231 .unwrap_or_else(|| NamedTrip {
232 name: self.all.new_name(),
233 waypoints: Vec::new(),
234 });
235 self.save_current_trip_to_session(app);
236 Some(Transition::Keep)
237 }
238 "Start new trip" => {
239 self.current = NamedTrip {
240 name: self.all.new_name(),
241 waypoints: Vec::new(),
242 };
243 *S::app_session_current_trip_name(app) = None;
244 Some(Transition::Keep)
245 }
246 "Load another trip" => Some(Transition::Push(ChooseSomething::new_state(
247 ctx,
248 "Load another trip",
249 self.all.trips.keys().map(|x| Choice::string(x)).collect(),
250 Box::new(move |choice, _, _| {
251 Transition::Multi(vec![
252 Transition::Pop,
253 Transition::ModifyState(Box::new(move |state, ctx, app| {
254 let state = state.downcast_mut::<S>().unwrap();
255 let files = state.mut_files();
256 files.current = files.all.trips[&choice].clone();
257 files.save_current_trip_to_session(app);
258 state.sync_from_file_management(ctx, app);
259 })),
260 ])
261 }),
262 ))),
263 "previous trip" => {
264 self.current = self.all.prev(&self.current.name).unwrap().clone();
265 self.save_current_trip_to_session(app);
266 Some(Transition::Keep)
267 }
268 "next trip" => {
269 self.current = self.all.next(&self.current.name).unwrap().clone();
270 self.save_current_trip_to_session(app);
271 Some(Transition::Keep)
272 }
273 "rename trip" => Some(Transition::Push(RenameTrip::<A, S>::new_state(
274 ctx,
275 &self.current,
276 &self.all,
277 ))),
278 "waypoint instructions" => Some(Transition::Keep),
279 _ => None,
280 }
281 }
282
283 fn save_current_trip_to_session(&self, app: &mut A) {
284 let name = S::app_session_current_trip_name(app);
285 if name.as_ref() != Some(&self.current.name) {
286 *name = Some(self.current.name.clone());
287 }
288 }
289}
290
291struct RenameTrip<A: AppLike + 'static, S: TripManagementState<A>> {
292 current_name: String,
293 all_names: HashSet<String>,
294
295 app_type: PhantomData<A>,
296 state_type: PhantomData<dyn TripManagementState<S>>,
297}
298
299impl<A: AppLike + 'static, S: TripManagementState<A>> RenameTrip<A, S> {
300 fn new_state(ctx: &mut EventCtx, current: &NamedTrip, all: &SavedTrips) -> Box<dyn State<A>> {
301 let panel = Panel::new_builder(Widget::col(vec![
302 Widget::row(vec![
303 Line("Name this trip").small_heading().into_widget(ctx),
304 ctx.style().btn_close_widget(ctx),
305 ]),
306 Widget::row(vec![
307 "Name:".text_widget(ctx).centered_vert(),
308 TextBox::default_widget(ctx, "name", current.name.clone()),
309 ]),
310 Widget::placeholder(ctx, "warning"),
311 ctx.style()
312 .btn_solid_primary
313 .text("Rename")
314 .hotkey(Key::Enter)
315 .build_def(ctx),
316 ]))
317 .build(ctx);
318 let state: RenameTrip<A, S> = RenameTrip {
319 current_name: current.name.clone(),
320 all_names: all.trips.keys().cloned().collect(),
321
322 app_type: PhantomData,
323 state_type: PhantomData,
324 };
325 <dyn SimpleState<_>>::new_state(panel, Box::new(state))
326 }
327}
328
329impl<A: AppLike + 'static, S: TripManagementState<A>> SimpleState<A> for RenameTrip<A, S> {
330 fn on_click(
331 &mut self,
332 _: &mut EventCtx,
333 _: &mut A,
334 x: &str,
335 panel: &mut Panel,
336 ) -> Transition<A> {
337 match x {
338 "close" => Transition::Pop,
339 "Rename" => {
340 let old_name = self.current_name.clone();
341 let new_name = panel.text_box("name");
342 Transition::Multi(vec![
343 Transition::Pop,
344 Transition::ModifyState(Box::new(move |state, ctx, app| {
345 let state = state.downcast_mut::<S>().unwrap();
346 let files = state.mut_files();
347 files.all.trips.remove(&old_name);
348 files.current.name = new_name.clone();
349 *S::app_session_current_trip_name(app) = Some(new_name.clone());
350 files.all.trips.insert(new_name, files.current.clone());
351 files.all.save(app);
352 state.sync_from_file_management(ctx, app);
353 })),
354 ])
355 }
356 _ => unreachable!(),
357 }
358 }
359
360 fn panel_changed(
361 &mut self,
362 ctx: &mut EventCtx,
363 _: &mut A,
364 panel: &mut Panel,
365 ) -> Option<Transition<A>> {
366 let new_name = panel.text_box("name");
367 let can_save = if new_name != self.current_name && self.all_names.contains(&new_name) {
368 panel.replace(
369 ctx,
370 "warning",
371 Line("A trip with this name already exists")
372 .fg(Color::hex("#FF5E5E"))
373 .into_widget(ctx),
374 );
375 false
376 } else {
377 panel.replace(ctx, "warning", Text::new().into_widget(ctx));
378 true
379 };
380 panel.replace(
381 ctx,
382 "Rename",
383 ctx.style()
384 .btn_solid_primary
385 .text("Rename")
386 .hotkey(Key::Enter)
387 .disabled(!can_save)
388 .build_def(ctx),
389 );
390 None
391 }
392
393 fn draw(&self, g: &mut GfxCtx, app: &A) {
394 grey_out_map(g, app);
395 }
396}