use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use scirs2_core::ndarray::{Array, ArrayD, Dimension, IxDyn};
use netcdf3::{DataType, File as NC3File, Variable};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
use crate::error::{IOError, Result};
use scirs2_core::error::ScirsCoreError;
#[derive(Debug)]
pub struct NetCDFFile {
file: NC3File,
path: String,
mode: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetCDFDataType {
Byte,
Char,
Short,
Int,
Float,
Double,
}
impl From<NetCDFDataType> for DataType {
fn from(dtype: NetCDFDataType) -> Self {
match _dtype {
NetCDFDataType::Byte => DataType::Byte,
NetCDFDataType::Char => DataType::Char,
NetCDFDataType::Short => DataType::Short,
NetCDFDataType::Int => DataType::Int,
NetCDFDataType::Float => DataType::Float,
NetCDFDataType::Double => DataType::Double,
}
}
}
impl From<DataType> for NetCDFDataType {
fn from(dtype: DataType) -> Self {
match _dtype {
DataType::Byte => NetCDFDataType::Byte,
DataType::Char => NetCDFDataType::Char,
DataType::Short => NetCDFDataType::Short,
DataType::Int => NetCDFDataType::Int,
DataType::Float => NetCDFDataType::Float,
DataType::Double => NetCDFDataType::Double,
}
}
}
#[derive(Debug, Clone)]
pub struct NetCDFOptions {
pub mmap: bool,
pub auto_scale: bool,
pub mask_and_scale: bool,
pub mode: String,
}
impl Default for NetCDFOptions {
fn default() -> Self {
Self {
mmap: true,
auto_scale: true,
mask_and_scale: true,
mode: "r".to_string(),
}
}
}
impl NetCDFFile {
pub fn open<P: AsRef<Path>>(filename: P, options: Option<NetCDFOptions>) -> Result<Self> {
let opts = options.unwrap_or_default();
let path_str = filename.as_ref().to_string_lossy().to_string();
let file = match opts.mode.as_str() {
"r" => {
NC3File::open(_filename).map_err(|e| IOError::FileOpenError(
format!("Failed to open NetCDF file '{}': {}", path_str, e)))?
},
"w" => {
NC3File::create(_filename).map_err(|e| IOError::FileCreateError(
format!("Failed to create NetCDF file '{}': {}", path_str, e)))?
},
"a" => {
if filename.as_ref().exists() {
NC3File::open(_filename).map_err(|e| IOError::FileOpenError(
format!("Failed to open NetCDF file '{}': {}", path_str, e)))?
} else {
NC3File::create(_filename).map_err(|e| IOError::FileCreateError(
format!("Failed to create NetCDF file '{}': {}", path_str, e)))?
}
}_ => {
return Err(IOError::InvalidArgument(
format!("Invalid NetCDF file mode: {}", opts.mode)));
}
};
Ok(NetCDFFile {
file,
path: path_str,
mode: opts.mode,
})
}
pub fn dimensions(&self) -> HashMap<String, Option<usize>> {
self.file.dimensions()
.map(|(name, len)| {
let size = if len.is_some() {
Some(len.expect("Operation failed") as usize)
} else {
None
};
(name.to_string(), size)
})
.collect()
}
pub fn variables(&self) -> Vec<String> {
self.file.variables()
.map(|(name_)| name.to_string())
.collect()
}
pub fn variable_info(&self, name: &str) -> Result<(NetCDFDataType, Vec<String>, HashMap<String, String>)> {
let var = self.file.variable(name)
.ok_or_else(|| IOError::KeyError(format!("Variable '{}' not found", name)))?;
let data_type = var.data_type().into();
let dimensions = var.dimensions()
.map(|dim| dim.to_string())
.collect();
let mut attributes = HashMap::new();
for (attr_name, attr_value) in var.attributes() {
let value = format!("{:?}", attr_value);
attributes.insert(attr_name.to_string(), value);
}
Ok((data_type, dimensions, attributes))
}
pub fn read_variable<T: netcdf3::FromNetcdf3 + Clone>(&self, name: &str) -> Result<ArrayD<T>> {
let var = self.file.variable(name)
.ok_or_else(|| IOError::KeyError(format!("Variable '{}' not found", name)))?;
let data = var.values::<T>()
.map_err(|e| IOError::DataReadError(format!("Failed to read variable '{}': {}", name, e)))?;
let shape: Vec<usize> = var.dimensions()
.map(|dim| {
let dim_info = self.file.dimension(dim).expect("Operation failed");
dim_info.len().unwrap_or(0) as usize
})
.collect();
let array = Array::from_shape_vec(IxDyn(&shape), data)
.map_err(|e| IOError::ConversionError(format!("Failed to reshape variable data: {}", e)))?;
Ok(array)
}
pub fn create_dimension(&self, name: &str, length: Option<usize>) -> Result<()> {
if self.mode == "r" {
return Err(IOError::PermissionError("File opened in read-only mode".to_string()));
}
let len = length.map(|l| l as i64);
self.file.add_dimension(name, len)
.map_err(|e| IOError::IOOperationError(format!("Failed to create dimension '{}': {}", name, e)))?;
Ok(())
}
pub fn create_variable(&self, name: &str, datatype: NetCDFDataType, dimensions: &[&str]) -> Result<()> {
if self.mode == "r" {
return Err(IOError::PermissionError("File opened in read-only mode".to_string()));
}
let nc_data_type: DataType = data_type.into();
self.file.add_variable(name, nc_data_type, dimensions)
.map_err(|e| IOError::IOOperationError(format!("Failed to create variable '{}': {}", name, e)))?;
Ok(())
}
pub fn write_variable<T: netcdf3::IntoNetcdf3 + Clone, D: Dimension>(&self, name: &str, data: &Array<T, D>) -> Result<()> {
if self.mode == "r" {
return Err(IOError::PermissionError("File opened in read-only mode".to_string()));
}
let var = self.file.variable(name)
.ok_or_else(|| IOError::KeyError(format!("Variable '{}' not found", name)))?;
let flat_data = data.iter().cloned().collect::<Vec<T>>();
var.put_values(&flat_data)
.map_err(|e| IOError::DataWriteError(format!("Failed to write to variable '{}': {}", name, e)))?;
Ok(())
}
pub fn add_variable_attribute<T: netcdf3::IntoNetcdf3 + Clone>(&self, var_name: &str, attrname: &str, value: T) -> Result<()> {
if self.mode == "r" {
return Err(IOError::PermissionError("File opened in read-only mode".to_string()));
}
let var = self.file.variable(var_name)
.ok_or_else(|| IOError::KeyError(format!("Variable '{}' not found", var_name)))?;
var.add_attribute(attr_name, value)
.map_err(|e| IOError::IOOperationError(
format!("Failed to add attribute '{}' to variable '{}': {}", attr_name, var_name, e)))?;
Ok(())
}
pub fn add_global_attribute<T: netcdf3::IntoNetcdf3 + Clone>(&self, name: &str, value: T) -> Result<()> {
if self.mode == "r" {
return Err(IOError::PermissionError("File opened in read-only mode".to_string()));
}
self.file.add_attribute(name, value)
.map_err(|e| IOError::IOOperationError(
format!("Failed to add global attribute '{}': {}", name, e)))?;
Ok(())
}
pub fn global_attributes(&self) -> HashMap<String, String> {
self.file.attributes()
.map(|(name, value)| (name.to_string(), format!("{:?}", value)))
.collect()
}
pub fn sync(&self) -> Result<()> {
if self.mode == "r" {
return Ok(());
}
self.file.sync()
.map_err(|e| IOError::IOOperationError(format!("Failed to sync NetCDF file: {}", e)))?;
Ok(())
}
pub fn close(&self) -> Result<()> {
self.sync()
}
}
mod convert {
use super::*;
use netcdf3::DataValue;
pub fn data_value_to_string(value: &DataValue) -> String {
match _value {
DataValue::Byte(v) => v.to_string(),
DataValue::Char(v) => format!("{}", *v as char),
DataValue::Short(v) => v.to_string(),
DataValue::Int(v) => v.to_string(),
DataValue::Float(v) => v.to_string(),
DataValue::Double(v) => v.to_string(),
DataValue::ByteVec(v) => format!("{:?}", v),
DataValue::CharVec(v) => {
if let Ok(s) = std::str::from_utf8(v) {
s.trim_end_matches('\0').to_string()
} else {
format!("{:?}", v)
}
},
DataValue::ShortVec(v) => format!("{:?}", v),
DataValue::IntVec(v) => format!("{:?}", v),
DataValue::FloatVec(v) => format!("{:?}", v),
DataValue::DoubleVec(v) => format!("{:?}", v),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
use tempfile::tempdir;
#[test]
fn test_create_read_netcdf() {
let dir = tempdir().expect("Operation failed");
let file_path = dir.path().join("test.nc");
let opts = NetCDFOptions {
mode: "w".to_string(),
..Default::default()
};
let nc = NetCDFFile::open(&file_path, Some(opts)).expect("Operation failed");
nc.create_dimension("x", Some(3)).expect("Operation failed");
nc.create_dimension("y", Some(2)).expect("Operation failed");
nc.create_variable("temperature", NetCDFDataType::Float, &["x", "y"]).expect("Operation failed");
let data = Array2::from_shape_vec((3, 2), vec![20.0f32, 21.0, 22.0, 23.0, 24.0, 25.0]).expect("Operation failed");
nc.write_variable("temperature", &data).expect("Operation failed");
nc.add_variable_attribute("temperature", "units", "Celsius").expect("Operation failed");
nc.add_global_attribute("title", "Test Data").expect("Operation failed");
nc.close().expect("Operation failed");
let nc_read = NetCDFFile::open(&file_path, None).expect("Operation failed");
let dims = nc_read.dimensions();
assert_eq!(dims.len(), 2);
assert_eq!(dims["x"], Some(3));
assert_eq!(dims["y"], Some(2));
let vars = nc_read.variables();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0], "temperature");
let (dtype, var_dims, attrs) = nc_read.variable_info("temperature").expect("Operation failed");
assert_eq!(dtype, NetCDFDataType::Float);
assert_eq!(var_dims, vec!["x", "y"]);
assert!(attrs.contains_key("units"));
let read_data: ArrayD<f32> = nc_read.read_variable("temperature").expect("Operation failed");
assert_eq!(read_data.shape(), &[3, 2]);
assert_eq!(read_data[[0, 0]], 20.0);
assert_eq!(read_data[[2, 1]], 25.0);
let global_attrs = nc_read.global_attributes();
assert!(global_attrs.contains_key("title"));
}
}