ltn/save/
mod.rs

1mod perma;
2mod proposals_ui;
3mod save_dialog;
4mod share;
5
6use std::collections::BTreeSet;
7
8use anyhow::Result;
9
10use abstutil::{Counter, Timer};
11use map_model::{BuildingID, Map, MapEdits};
12use widgetry::tools::PopupMsg;
13use widgetry::{EventCtx, State};
14
15use crate::logic::{BlockID, Partitioning};
16use crate::{pages, App, Transition};
17
18pub use share::PROPOSAL_HOST_URL;
19
20pub struct Proposals {
21    // The 0th thing is always treated as the immutable basemap
22    // Note for the current proposal, we have to be very careful to sync MapEdits with this
23    pub list: Vec<Proposal>,
24    pub current: usize,
25}
26
27/// Captures all of the edits somebody makes to a map in the LTN tool.
28/// TODO This should just be MapEdits, but we need to deal with Partitioning still
29/// Note "existing LTNs" is a special reserved name
30#[derive(Clone)]
31pub struct Proposal {
32    pub partitioning: Partitioning,
33    pub edits: MapEdits,
34}
35
36impl Proposal {
37    /// Try to load a proposal. If it fails, returns a popup message state.
38    pub fn load_from_path(
39        ctx: &mut EventCtx,
40        app: &mut App,
41        path: String,
42    ) -> Option<Box<dyn State<App>>> {
43        Self::load_from_bytes(ctx, app, &path, abstio::slurp_file(path.clone()))
44    }
45
46    pub fn load_from_bytes(
47        ctx: &mut EventCtx,
48        app: &mut App,
49        name: &str,
50        bytes: Result<Vec<u8>>,
51    ) -> Option<Box<dyn State<App>>> {
52        match bytes.and_then(|bytes| Self::inner_load(ctx, app, bytes)) {
53            Ok(()) => None,
54            Err(err) => Some(PopupMsg::new_state(
55                ctx,
56                "Error",
57                vec![
58                    format!("Couldn't load proposal {}", name),
59                    err.to_string(),
60                    "The format of saved proposals recently changed.".to_string(),
61                    "Contact dabreegster@gmail.com if you need help restoring a file.".to_string(),
62                ],
63            )),
64        }
65    }
66
67    fn inner_load(ctx: &mut EventCtx, app: &mut App, bytes: Vec<u8>) -> Result<()> {
68        let decoder = flate2::read::GzDecoder::new(&bytes[..]);
69        let value = serde_json::from_reader(decoder)?;
70        let proposal = perma::from_permanent(&app.per_map.map, value)?;
71
72        // TODO We could try to detect if the file's partitioning (road IDs and such) still matches
73        // this version of the map or not
74
75        app.per_map.proposals.list.push(proposal);
76        app.per_map.proposals.current = app.per_map.proposals.list.len() - 1;
77
78        app.per_map.map.must_apply_edits(
79            app.per_map.proposals.get_current().edits.clone(),
80            &mut Timer::throwaway(),
81        );
82        crate::redraw_all_icons(ctx, app);
83
84        Ok(())
85    }
86
87    fn to_gzipped_bytes(&self, app: &App) -> Result<Vec<u8>> {
88        let json_value = perma::to_permanent(&app.per_map.map, self)?;
89        let mut output_buffer = Vec::new();
90        let mut encoder =
91            flate2::write::GzEncoder::new(&mut output_buffer, flate2::Compression::best());
92        serde_json::to_writer(&mut encoder, &json_value)?;
93        encoder.finish()?;
94        Ok(output_buffer)
95    }
96
97    fn checksum(&self, app: &App) -> Result<String> {
98        let bytes = self.to_gzipped_bytes(app)?;
99        let mut context = md5::Context::new();
100        context.consume(&bytes);
101        Ok(format!("{:x}", context.compute()))
102    }
103}
104
105impl Proposals {
106    // This calculates partitioning, which is expensive
107    pub fn new(map: &Map, timer: &mut Timer) -> Self {
108        Self {
109            list: vec![Proposal {
110                partitioning: Partitioning::seed_using_heuristics(map, timer),
111                edits: map.get_edits().clone(),
112            }],
113            current: 0,
114        }
115    }
116
117    pub fn get_current(&self) -> &Proposal {
118        &self.list[self.current]
119    }
120
121    // Special case for locking into a consultation mode
122    pub fn force_current_to_basemap(&mut self) {
123        let current = self.list.remove(self.current);
124        self.list = vec![current];
125        self.current = 0;
126    }
127
128    /// Call before making any changes
129    pub fn before_edit(&mut self, edits: MapEdits) {
130        if self.current == 0 {
131            // TODO Regenerate a better edits_name?
132            self.list.insert(1, self.list[0].clone());
133            self.current = 1;
134        }
135        // TODO Maybe we could mark this as unsaved, depending how we decide to do autosave
136        self.list[self.current].edits = edits;
137    }
138}
139
140// After switching proposals, we have to recreate state
141//
142// To preserve per-neighborhood states, we have to transform neighbourhood IDs, which may change if
143// the partitioning is different. If the boundary is a bit different, match up by all the blocks in
144// the current neighbourhood.
145#[derive(Clone)]
146pub enum PreserveState {
147    PickArea,
148    Route,
149    Crossings,
150    // TODO app.session.edit_mode now has state for Shortcuts...
151    DesignLTN(BTreeSet<BlockID>),
152    PerResidentImpact(BTreeSet<BlockID>, Option<BuildingID>),
153    CycleNetwork,
154    Census,
155}
156
157impl PreserveState {
158    fn switch_to_state(&self, ctx: &mut EventCtx, app: &mut App) -> Transition {
159        match self {
160            PreserveState::PickArea => Transition::Replace(pages::PickArea::new_state(ctx, app)),
161            PreserveState::Route => Transition::Replace(pages::RoutePlanner::new_state(ctx, app)),
162            PreserveState::Crossings => Transition::Replace(pages::Crossings::new_state(ctx, app)),
163            PreserveState::DesignLTN(blocks) => {
164                // Count which new neighbourhoods have the blocks from the original. Pick the one
165                // with the most matches
166                let mut count = Counter::new();
167                for block in blocks {
168                    count.inc(app.partitioning().block_to_neighbourhood(*block));
169                }
170
171                if let pages::EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
172                    // TODO We should try to preserve the focused road at least, or the specific
173                    // shortcut maybe.
174                    *maybe_focus = None;
175                }
176                if let pages::EditMode::FreehandFilters(_) = app.session.edit_mode {
177                    app.session.edit_mode = pages::EditMode::Filters;
178                }
179
180                Transition::Replace(pages::DesignLTN::new_state(ctx, app, count.max_key()))
181            }
182            PreserveState::PerResidentImpact(blocks, current_target) => {
183                let mut count = Counter::new();
184                for block in blocks {
185                    count.inc(app.partitioning().block_to_neighbourhood(*block));
186                }
187                Transition::Replace(pages::PerResidentImpact::new_state(
188                    ctx,
189                    app,
190                    count.max_key(),
191                    *current_target,
192                ))
193            }
194            PreserveState::CycleNetwork => {
195                Transition::Replace(pages::CycleNetwork::new_state(ctx, app))
196            }
197            PreserveState::Census => Transition::Replace(pages::Census::new_state(ctx, app)),
198        }
199    }
200}
201
202#[cfg(not(target_arch = "wasm32"))]
203fn start_dir() -> Option<String> {
204    home::home_dir().map(|x| x.display().to_string())
205}
206
207#[cfg(target_arch = "wasm32")]
208fn start_dir() -> Option<String> {
209    None
210}