pub(crate) mod lexer;
mod parser;
use crate::{EastNorthElevation, Error, Survey, UtmLocation};
use std::{
marker::PhantomData,
path::{Path, PathBuf},
};
use uuid::Uuid;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Datum {
Adindan,
Arc1950,
Arc1960,
Australian1966,
Australian1984,
CampAreaAstro,
Cape,
European1950,
European1979,
Geodetic1949,
HongKong1963,
HuTzuShan,
Indian,
NorthAmerican1927,
NorthAmerican1983,
Oman,
OrdinanceSurvey1936,
Pulkovo1942,
SouthAmerican1956,
SouthAmerican1969,
Tokyo,
WGS1972,
WGS1984,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DeclinationMode {
Ignore,
#[default]
Entered,
Auto,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[allow(clippy::struct_excessive_bools)]
pub struct ProjectParameters {
pub global_override: bool,
pub declination_mode: DeclinationMode,
pub utm_convergence_applied: bool,
pub override_lrud_association: bool,
pub lrud_to_station: bool,
pub shot_flags_applied: bool,
pub total_exclusion_applied: bool,
pub plotting_exclusion_applied: bool,
pub length_exclusion_applied: bool,
pub close_exclusion_applied: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FileConvergence {
pub enabled: bool,
pub angle: f64,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FileState {
pub datum: Datum,
pub base_location: UtmLocation,
pub utm_zone: Option<u8>,
pub file_convergence: Option<FileConvergence>,
pub project_parameters: Option<ProjectParameters>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Station {
pub(crate) name: String,
pub(crate) location: Option<EastNorthElevation>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Unloaded;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Loaded;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DatFile<S> {
pub file_path: PathBuf,
pub project_stations: Vec<Station>,
pub file_state: FileState,
pub(crate) surveys: Vec<Survey>,
pub(crate) state: PhantomData<S>,
}
impl DatFile<Unloaded> {
pub fn load(self, project_path: &Path) -> Result<DatFile<Loaded>, Error> {
let full_path = project_path.join(&self.file_path);
if !full_path.exists() {
return Err(Error::SurveyFileNotFound(full_path));
}
let file_contents = std::fs::read_to_string(&full_path).map_err(Error::CouldntReadFile)?;
let surveys = Survey::parse_dat_file(&file_contents)?;
Ok(DatFile {
file_path: self.file_path,
project_stations: self.project_stations,
file_state: self.file_state,
surveys,
state: PhantomData,
})
}
}
impl DatFile<Loaded> {
#[must_use]
pub fn surveys(&self) -> &[Survey] {
&self.surveys
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Project<S> {
pub id: Option<Uuid>,
pub file_path: PathBuf,
pub survey_files: Vec<DatFile<S>>,
pub(crate) state: PhantomData<S>,
}
impl PartialEq for Project<Unloaded> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.survey_files == other.survey_files
}
}
impl PartialEq for Project<Loaded> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.survey_files == other.survey_files
}
}
impl Project<Unloaded> {
pub fn read(file_path: impl AsRef<Path>) -> Result<Self, Error> {
let path = file_path.as_ref().to_path_buf();
if !path.exists() {
return Err(Error::ProjectFileNotFound(path));
}
let file_contents = std::fs::read_to_string(&path).map_err(Error::CouldntReadFile)?;
let tokens = lexer::tokenize(&file_contents)
.map_err(|e| Error::CouldntParseProject(e.to_string()))?;
let project = parser::parse_project(path, &tokens, &file_contents)
.map_err(|e| Error::CouldntParseProject(e.to_string()))?;
Ok(project)
}
#[allow(clippy::missing_panics_doc)]
pub fn load_survey_files(self) -> Result<Project<Loaded>, Error> {
let mut survey_files = Vec::new();
let project_dir = self.file_path.parent().unwrap();
for survey_file in self.survey_files {
let survey_file = survey_file.load(project_dir)?;
survey_files.push(survey_file);
}
Ok(Project {
id: self.id,
file_path: self.file_path,
survey_files,
state: PhantomData::<Loaded>,
})
}
}
impl Project<Loaded> {
#[must_use]
pub fn new(project_id: Option<Uuid>, file_path: impl AsRef<Path>) -> Self {
let file_path = file_path.as_ref().to_path_buf();
Self {
id: project_id,
file_path,
survey_files: Vec::new(),
state: PhantomData::<Loaded>,
}
}
pub fn add_survey_file(&mut self, dat_file: DatFile<Loaded>) {
self.survey_files.push(dat_file);
}
}
#[cfg(test)]
mod tests {
use crate::Error;
use super::*;
use std::path::PathBuf;
#[test]
fn programatic_creation() {
let new_project = Project::new(None, "Ginnie.mak");
assert!(new_project.survey_files.is_empty());
}
#[test]
fn bad_path() {
let path = PathBuf::from("does_not_exist.mak");
let result = Project::read(&path);
assert!(result.is_err_and(|err| matches!(err, Error::ProjectFileNotFound(_path))));
}
#[test]
fn parse_and_load_compass_sample() {
let mut sample_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
sample_path.push("test_data/Fulfords.mak");
let read_project = Project::read(&sample_path).unwrap();
assert_eq!(read_project.survey_files.len(), 2);
for dat_file in &read_project.survey_files {
assert_eq!(dat_file.file_state.datum, Datum::NorthAmerican1983);
}
let _loaded_project = read_project.load_survey_files().unwrap();
}
}