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 pub list: Vec<Proposal>,
24 pub current: usize,
25}
26
27#[derive(Clone)]
31pub struct Proposal {
32 pub partitioning: Partitioning,
33 pub edits: MapEdits,
34}
35
36impl Proposal {
37 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 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 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 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 pub fn before_edit(&mut self, edits: MapEdits) {
130 if self.current == 0 {
131 self.list.insert(1, self.list[0].clone());
133 self.current = 1;
134 }
135 self.list[self.current].edits = edits;
137 }
138}
139
140#[derive(Clone)]
146pub enum PreserveState {
147 PickArea,
148 Route,
149 Crossings,
150 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 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 *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}