use crate::core::persist::Persistable;
use crate::core::{PointSet, PolygonSet, Surface, Well};
use crate::foundation::{GeoError, Result, Unit};
use crate::io::container::{self, Section};
use crate::io::serial::DATA_VERSION;
use crate::manager::GeoData;
use serde_json::{json, Value};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Debug, Clone)]
pub struct ModelSection {
pub version: u32,
pub tags: Vec<String>,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ProjectInfo {
pub owner: Option<String>,
pub tags: Vec<String>,
pub created: Option<u64>,
pub modified: Option<u64>,
pub unit: Option<String>,
pub elements: Vec<(String, String)>,
}
impl GeoData {
pub fn owner(&self) -> Option<&str> {
self.owner.as_deref()
}
pub fn set_owner(&mut self, owner: impl Into<String>) {
self.owner = Some(owner.into());
}
pub fn tags(&self) -> &[String] {
&self.tags
}
pub fn set_tags(&mut self, tags: Vec<String>) {
self.tags = tags;
}
pub fn set_element_tags(&mut self, name: impl Into<String>, tags: Vec<String>) {
self.element_tags.insert(name.into(), tags);
}
pub fn put_model_section(
&mut self,
name: impl Into<String>,
tags: Vec<String>,
version: u32,
bytes: Vec<u8>,
) {
self.model_sections.insert(
name.into(),
ModelSection {
version,
tags,
bytes,
},
);
}
pub fn model_section_names(&self) -> Vec<String> {
self.model_sections.keys().cloned().collect()
}
pub fn model_section(&self, name: &str) -> Option<(u32, Vec<u8>)> {
self.model_sections
.get(name)
.map(|m| (m.version, m.bytes.clone()))
}
fn named_section(&self, name: &str, mut sec: Section) -> Section {
sec.name = name.to_string();
sec.tags = self.element_tags.get(name).cloned().unwrap_or_default();
sec
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let mut sections: Vec<Section> = Vec::new();
for (name, s) in &self.surfaces {
sections.push(self.named_section(name, s.to_section()?));
}
for (id, w) in &self.wells {
sections.push(self.named_section(id, w.to_section()?));
}
for (name, p) in &self.points {
sections.push(self.named_section(name, p.to_section()?));
}
for (name, p) in &self.polygons {
sections.push(self.named_section(name, p.to_section()?));
}
for (name, m) in &self.model_sections {
sections.push(Section {
kind: "model".to_string(),
name: name.clone(),
tags: m.tags.clone(),
version: m.version,
payload: m.bytes.clone(),
});
}
let now = now_secs();
let app = json!({
"petekio_version": env!("CARGO_PKG_VERSION"),
"unit": self.unit,
"owner": self.owner,
"created": self.created.unwrap_or(now),
"modified": now,
"tags": self.tags,
"strat_hints": self.strat_hints,
"strat_order": self.strat_order,
});
Ok(container::write(
path.as_ref(),
&app,
DATA_VERSION,
§ions,
)?)
}
pub fn open(path: impl AsRef<Path>) -> Result<GeoData> {
let mut r = container::open(path.as_ref())?;
migrate_gate(r.data_version())?;
let app = r.app().clone();
let unit: Unit = app
.get("unit")
.cloned()
.and_then(|v| serde_json::from_value(v).ok())
.ok_or_else(|| GeoError::Parse(".pproj manifest missing/invalid unit".into()))?;
let mut geo = GeoData::new(unit);
geo.owner = app.get("owner").and_then(|v| v.as_str()).map(String::from);
geo.tags = from_json(&app, "tags");
geo.created = app.get("created").and_then(Value::as_u64);
geo.strat_hints = from_json(&app, "strat_hints");
geo.strat_order = from_json(&app, "strat_order");
let index: Vec<(String, String)> = r
.entries()
.iter()
.map(|e| (e.kind.clone(), e.name.clone()))
.collect();
for (kind, name) in index {
match kind.as_str() {
k if k == Surface::KIND => {
let s = Surface::from_payload(&r.read(&name)?.payload)?;
geo.surfaces.insert(name, s);
}
k if k == Well::KIND => {
let w = Well::from_payload(&r.read(&name)?.payload)?;
geo.wells.insert(name, w);
}
k if k == PointSet::KIND => {
let p = PointSet::from_payload(&r.read(&name)?.payload)?;
geo.points.insert(name, p);
}
k if k == PolygonSet::KIND => {
let p = PolygonSet::from_payload(&r.read(&name)?.payload)?;
geo.polygons.insert(name, p);
}
"model" => {
let s = r.read(&name)?;
geo.model_sections.insert(
name,
ModelSection {
version: s.version,
tags: s.tags,
bytes: s.payload,
},
);
}
_ => {} }
}
Ok(geo)
}
pub fn split(src: impl AsRef<Path>, dst: impl AsRef<Path>, names: &[&str]) -> Result<()> {
Ok(container::filter_to(src.as_ref(), dst.as_ref(), |e| {
names.contains(&e.name.as_str())
})?)
}
pub fn export(src: impl AsRef<Path>, dst: impl AsRef<Path>, tags: &[&str]) -> Result<()> {
Ok(container::filter_to(src.as_ref(), dst.as_ref(), |e| {
e.tags.iter().any(|t| tags.contains(&t.as_str()))
})?)
}
pub fn merge(a: impl AsRef<Path>, b: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
Ok(container::merge_to(a.as_ref(), b.as_ref(), dst.as_ref())?)
}
pub fn inspect(path: impl AsRef<Path>) -> Result<ProjectInfo> {
let r = container::open(path.as_ref())?;
let app = r.app();
Ok(ProjectInfo {
owner: app.get("owner").and_then(|v| v.as_str()).map(String::from),
tags: from_json(app, "tags"),
created: app.get("created").and_then(Value::as_u64),
modified: app.get("modified").and_then(Value::as_u64),
unit: app.get("unit").and_then(|v| v.as_str()).map(String::from),
elements: r
.entries()
.iter()
.map(|e| (e.kind.clone(), e.name.clone()))
.collect(),
})
}
}
fn migrate_gate(file_version: u32) -> Result<()> {
if file_version > DATA_VERSION {
return Err(GeoError::Parse(format!(
".pproj element schema v{file_version} is newer than this petekIO (reads ≤ v{DATA_VERSION}) — upgrade petekIO"
)));
}
Ok(())
}
fn from_json<T: serde::de::DeserializeOwned + Default>(app: &Value, key: &str) -> T {
app.get(key)
.cloned()
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::foundation::Unit;
use serde_json::json;
fn tmp(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("pio_ver_{tag}_{}.pproj", std::process::id()))
}
#[test]
fn rejects_newer_element_schema() {
let p = tmp("newer");
container::write(&p, &json!({"unit": "Metres"}), DATA_VERSION + 1, &[]).unwrap();
let err = GeoData::open(&p)
.err()
.expect("newer schema must be rejected");
assert!(format!("{err}").contains("newer than this petekIO"));
std::fs::remove_file(&p).ok();
}
#[test]
fn current_version_opens_and_restores_unit() {
let p = tmp("cur");
container::write(&p, &json!({"unit": "Feet"}), DATA_VERSION, &[]).unwrap();
assert_eq!(GeoData::open(&p).unwrap().unit, Unit::Feet);
std::fs::remove_file(&p).ok();
}
}