mod perma;
mod proposals_ui;
mod save_dialog;
mod share;
use std::collections::BTreeSet;
use anyhow::Result;
use abstutil::{Counter, Timer};
use map_model::{BuildingID, Map, MapEdits};
use widgetry::tools::PopupMsg;
use widgetry::{EventCtx, State};
use crate::logic::{BlockID, Partitioning};
use crate::{pages, App, Transition};
pub use share::PROPOSAL_HOST_URL;
pub struct Proposals {
pub list: Vec<Proposal>,
pub current: usize,
}
#[derive(Clone)]
pub struct Proposal {
pub partitioning: Partitioning,
pub edits: MapEdits,
}
impl Proposal {
pub fn load_from_path(
ctx: &mut EventCtx,
app: &mut App,
path: String,
) -> Option<Box<dyn State<App>>> {
Self::load_from_bytes(ctx, app, &path, abstio::slurp_file(path.clone()))
}
pub fn load_from_bytes(
ctx: &mut EventCtx,
app: &mut App,
name: &str,
bytes: Result<Vec<u8>>,
) -> Option<Box<dyn State<App>>> {
match bytes.and_then(|bytes| Self::inner_load(ctx, app, bytes)) {
Ok(()) => None,
Err(err) => Some(PopupMsg::new_state(
ctx,
"Error",
vec![
format!("Couldn't load proposal {}", name),
err.to_string(),
"The format of saved proposals recently changed.".to_string(),
"Contact dabreegster@gmail.com if you need help restoring a file.".to_string(),
],
)),
}
}
fn inner_load(ctx: &mut EventCtx, app: &mut App, bytes: Vec<u8>) -> Result<()> {
let decoder = flate2::read::GzDecoder::new(&bytes[..]);
let value = serde_json::from_reader(decoder)?;
let proposal = perma::from_permanent(&app.per_map.map, value)?;
app.per_map.proposals.list.push(proposal);
app.per_map.proposals.current = app.per_map.proposals.list.len() - 1;
app.per_map.map.must_apply_edits(
app.per_map.proposals.get_current().edits.clone(),
&mut Timer::throwaway(),
);
crate::redraw_all_icons(ctx, app);
Ok(())
}
fn to_gzipped_bytes(&self, app: &App) -> Result<Vec<u8>> {
let json_value = perma::to_permanent(&app.per_map.map, self)?;
let mut output_buffer = Vec::new();
let mut encoder =
flate2::write::GzEncoder::new(&mut output_buffer, flate2::Compression::best());
serde_json::to_writer(&mut encoder, &json_value)?;
encoder.finish()?;
Ok(output_buffer)
}
fn checksum(&self, app: &App) -> Result<String> {
let bytes = self.to_gzipped_bytes(app)?;
let mut context = md5::Context::new();
context.consume(&bytes);
Ok(format!("{:x}", context.compute()))
}
}
impl Proposals {
pub fn new(map: &Map, timer: &mut Timer) -> Self {
Self {
list: vec![Proposal {
partitioning: Partitioning::seed_using_heuristics(map, timer),
edits: map.get_edits().clone(),
}],
current: 0,
}
}
pub fn get_current(&self) -> &Proposal {
&self.list[self.current]
}
pub fn force_current_to_basemap(&mut self) {
let current = self.list.remove(self.current);
self.list = vec![current];
self.current = 0;
}
pub fn before_edit(&mut self, edits: MapEdits) {
if self.current == 0 {
self.list.insert(1, self.list[0].clone());
self.current = 1;
}
self.list[self.current].edits = edits;
}
}
#[derive(Clone)]
pub enum PreserveState {
PickArea,
Route,
Crossings,
DesignLTN(BTreeSet<BlockID>),
PerResidentImpact(BTreeSet<BlockID>, Option<BuildingID>),
CycleNetwork,
Census,
}
impl PreserveState {
fn switch_to_state(&self, ctx: &mut EventCtx, app: &mut App) -> Transition {
match self {
PreserveState::PickArea => Transition::Replace(pages::PickArea::new_state(ctx, app)),
PreserveState::Route => Transition::Replace(pages::RoutePlanner::new_state(ctx, app)),
PreserveState::Crossings => Transition::Replace(pages::Crossings::new_state(ctx, app)),
PreserveState::DesignLTN(blocks) => {
let mut count = Counter::new();
for block in blocks {
count.inc(app.partitioning().block_to_neighbourhood(*block));
}
if let pages::EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
*maybe_focus = None;
}
if let pages::EditMode::FreehandFilters(_) = app.session.edit_mode {
app.session.edit_mode = pages::EditMode::Filters;
}
Transition::Replace(pages::DesignLTN::new_state(ctx, app, count.max_key()))
}
PreserveState::PerResidentImpact(blocks, current_target) => {
let mut count = Counter::new();
for block in blocks {
count.inc(app.partitioning().block_to_neighbourhood(*block));
}
Transition::Replace(pages::PerResidentImpact::new_state(
ctx,
app,
count.max_key(),
*current_target,
))
}
PreserveState::CycleNetwork => {
Transition::Replace(pages::CycleNetwork::new_state(ctx, app))
}
PreserveState::Census => Transition::Replace(pages::Census::new_state(ctx, app)),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn start_dir() -> Option<String> {
home::home_dir().map(|x| x.display().to_string())
}
#[cfg(target_arch = "wasm32")]
fn start_dir() -> Option<String> {
None
}