use crate::{
gallery::TrailSort,
library::validate_trail_name,
map::{TrailColoring, Viewport},
persistence,
};
use anyhow::{Context as _, Result};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use trailgen_core::{RouteShape, SupportPoint};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ManualDraft {
pub name: String,
pub shape: RouteShape,
pub support_points: Vec<SupportPoint>,
pub viewport: Viewport,
}
impl ManualDraft {
pub fn normalize(mut self) -> Option<Self> {
if !matches!(self.shape, RouteShape::Open | RouteShape::Loop)
|| self.support_points.is_empty()
|| !self
.support_points
.iter()
.all(|support| SupportPoint::forge(support.coord()).is_some())
|| !self.viewport.zoom.is_finite()
|| !self.viewport.center.into_iter().all(f64::is_finite)
{
return None;
}
self.name = validate_trail_name(&self.name)
.unwrap_or("manual trail")
.to_owned();
self.viewport.normalize();
Some(self)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct Slate {
pub project: PathBuf,
pub viewport: Option<Viewport>,
pub manual_draft: Option<ManualDraft>,
pub shutters: BTreeMap<String, bool>,
pub inspector_scroll: f32,
pub sort: TrailSort,
pub trail_coloring: TrailColoring,
}
impl Default for Slate {
fn default() -> Self {
Self {
project: PathBuf::new(),
viewport: None,
manual_draft: None,
shutters: BTreeMap::new(),
inspector_scroll: 0.0,
sort: TrailSort::default(),
trail_coloring: TrailColoring::default(),
}
}
}
impl Slate {
pub fn load(path: &Path, project: &Path) -> Self {
let mut slate = std::fs::read_to_string(path)
.ok()
.and_then(|text| toml::from_str::<Self>(&text).ok())
.filter(|slate| slate.project == project)
.unwrap_or_default();
project.clone_into(&mut slate.project);
slate.viewport = slate.viewport.filter(|viewport| {
viewport.zoom.is_finite() && viewport.center.into_iter().all(f64::is_finite)
});
if let Some(viewport) = &mut slate.viewport {
viewport.normalize();
}
slate.manual_draft = slate.manual_draft.and_then(ManualDraft::normalize);
if !slate.inspector_scroll.is_finite() {
slate.inspector_scroll = 0.0;
}
slate.inspector_scroll = slate.inspector_scroll.max(0.0);
slate.shutters.retain(|section, _| {
matches!(
section.as_str(),
"search" | "library" | "calibration" | "areas" | "overlays"
)
});
slate
}
pub fn save(&self, path: &Path) -> Result<()> {
let body = toml::to_string_pretty(self).context("serialize workbench slate")?;
persistence::replace(path, body.as_bytes())
.with_context(|| format!("replace workbench slate {}", path.display()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use trailgen_core::Coord;
fn support(lon: f64, lat: f64) -> SupportPoint {
SupportPoint::forge(Coord::new(lon, lat)).expect("fixture support must be valid")
}
#[test]
fn slate_round_trips_and_repels_other_projects() -> Result<()> {
let temp = tempfile::tempdir()?;
let path = temp.path().join("slate.toml");
let alpha = temp.path().join("alpha");
let beta = temp.path().join("beta");
let mut slate = Slate::load(&path, &alpha);
slate.viewport = Some(Viewport {
center: [0.29, 0.37],
zoom: 15.5,
});
slate.shutters.insert("areas".to_owned(), true);
slate.trail_coloring = TrailColoring::Terrain;
slate.manual_draft = Some(ManualDraft {
name: "unfinished crossing".to_owned(),
shape: RouteShape::Open,
support_points: vec![support(-74.02, 40.71), support(-73.98, 40.72)],
viewport: Viewport {
center: [0.294, 0.376],
zoom: 16.0,
},
});
slate.save(&path)?;
assert_eq!(Slate::load(&path, &alpha), slate);
let foreign = Slate::load(&path, &beta);
assert_eq!(foreign.project, beta);
assert!(foreign.viewport.is_none());
assert!(foreign.manual_draft.is_none());
assert!(foreign.shutters.is_empty());
Ok(())
}
#[test]
fn malformed_manual_drafts_do_not_possess_the_workbench() -> Result<()> {
let temp = tempfile::tempdir()?;
let path = temp.path().join("slate.toml");
let project = temp.path().join("alpha");
let mut slate = Slate::load(&path, &project);
slate.manual_draft = Some(ManualDraft {
name: "irrelevant".to_owned(),
shape: RouteShape::OutAndBack,
support_points: vec![support(-74.02, 40.71)],
viewport: Viewport::WORLD,
});
slate.save(&path)?;
assert!(Slate::load(&path, &project).manual_draft.is_none());
Ok(())
}
#[test]
fn obsolete_organizer_state_evaporates_on_load() -> Result<()> {
let temp = tempfile::tempdir()?;
let path = temp.path().join("slate.toml");
let project = temp.path().join("alpha");
std::fs::write(
&path,
format!(
"project = {:?}\ngallery = \"library\"\nactive_family = 7\n[shutters]\nfamilies = true\nsearch = false\n",
project.to_string_lossy()
),
)?;
let slate = Slate::load(&path, &project);
assert_eq!(slate.shutters.len(), 1);
assert_eq!(slate.shutters.get("search"), Some(&false));
slate.save(&path)?;
assert!(!std::fs::read_to_string(path)?.contains("family"));
Ok(())
}
}