map_gui/tools/
trip_files.rs

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
17/// Save sequences of waypoints as named trips. Basic file management -- save, load, browse. This
18/// is useful to define "test cases," then edit the bike network and "run the tests" to compare
19/// results.
20pub struct TripManagement<A: AppLike + 'static, S: TripManagementState<A>> {
21    pub current: NamedTrip,
22    // We assume the file won't change out from beneath us
23    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        // Special case: if this is a one-shot imported map without an explicit name, ignore any
53        // saved file. It's likely for a previously imported and different map!
54        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    // TODO This is now shared between Ungap the Map and the LTN tool. Is that weird?
71    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        // Pretend unsaved trips are at the end of the list
77        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                // This info more applies to InputWaypoints, but the button fits better here
171                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    /// saves iff current trip is changed.
182    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}