c3dio 0.8.0

A library for reading and writing C3D motion capture files.
Documentation
//! STO file formats are used by OpenSim to store table-based data such as joint angles, muscle activations, and muscle forces.
//! They can be used to store ground reaction forces/moments and emg data as well.
use crate::{C3d, C3dWriteError};
use std::io::Write;
use std::path::PathBuf;
use grid::Grid;

/// The STO struct contains the data for writing an STO file.
#[derive(Debug, Clone)]
pub struct Sto {
    pub file_description: Option<String>,
    pub version: u8,
    pub in_degrees: bool,
    pub first_frame: usize,
    pub data_rate: f32,
    pub column_names: Vec<String>,
    pub data: Grid<f64>,
}

impl Sto {
    pub fn from_c3d(c3d: &C3d) -> Self {
        Sto {
            file_description: None,
            version: 1,
            in_degrees: false,
            first_frame: c3d.points.first_frame as usize,
            column_names: c3d.analog.labels.clone(),
            data_rate: c3d.analog.rate,
            data: c3d.analog.analog.clone(),
        }
    }

    pub fn write(&self, file_name: PathBuf) -> Result<(), C3dWriteError> {
        if file_name.is_dir() {
            return Err(C3dWriteError::InvalidFilePath(file_name));
        }
        if !file_name
            .extension()
            .unwrap()
            .to_str()
            .unwrap()
            .to_lowercase()
            .eq("sto")
        {
            return Err(C3dWriteError::InvalidFileExtension(
                file_name.extension().unwrap().to_str().unwrap().to_string(),
            ));
        }
        let mut file = std::fs::File::create(file_name.clone()).map_err(|e| {
            C3dWriteError::WriteError(
                file_name.clone(),
                std::io::Error::new(std::io::ErrorKind::Other, e),
            )
        })?;
        let mut header = String::new();
        let description = match &self.file_description {
            None => file_name.to_str().unwrap().to_string(),
            Some(description) => description.clone(),
        };
        let degrees = if self.in_degrees { "yes" } else { "no" };
        header.push_str(&format!(
            "{}\n\
             version: {}\n\
             nRows: {}\n\
             nColumns: {}\n\
             inDegrees: {}\n\
             endheader\n",
            description,
            self.version,
            self.data.size().0,
            self.data.size().1,
            degrees,
        ));
        header.push_str("time\t");
        for i in 0..self.data.size().1 {
            if i >= self.column_names.len() {
                header.push_str(&format!("Column_{}\t", i));
            } else {
                header.push_str(&format!("{}\t", self.column_names[i]));
            }
        }
        header.push_str("\n");
        file.write_all(header.as_bytes()).map_err(|e| {
            C3dWriteError::WriteError(
                file_name.clone(),
                std::io::Error::new(std::io::ErrorKind::Other, e),
            )
        })?;
        for row in 0..self.data.size().0 {
            let mut row_string = String::new();
            row_string.push_str(&format!(
                "{}\t",
                (row + self.first_frame) as f32 / self.data_rate
            ));
            for column in 0..self.data.size().1 {
                row_string.push_str(&format!("{}\t", self.data[(row, column)]));
            }
            row_string.push_str("\n");
            file.write_all(row_string.as_bytes()).map_err(|e| {
                C3dWriteError::WriteError(
                    file_name.clone(),
                    std::io::Error::new(std::io::ErrorKind::Other, e),
                )
            })?;
        }
        Ok(())
    }
}