1use std::path::Path;
7
8use crate::attribute::{Attribute, AttributeValue};
9use crate::dimension::Dimension;
10use crate::error::{NetCdfError, Result};
11use crate::metadata::{NetCdfMetadata, NetCdfVersion};
12use crate::variable::{DataType, Variable};
13
14#[cfg(feature = "netcdf3")]
16enum PendingData {
17 F32(Vec<f32>),
18 F64(Vec<f64>),
19 I32(Vec<i32>),
20 I16(Vec<i16>),
21 I8(Vec<i8>),
22}
23
24pub struct NetCdfWriter {
28 metadata: NetCdfMetadata,
29 #[cfg(feature = "netcdf3")]
30 dataset_nc3: Option<netcdf3::DataSet>,
31 #[cfg(feature = "netcdf3")]
32 pending_data: std::collections::HashMap<String, PendingData>,
33 #[cfg(feature = "netcdf4")]
34 file_nc4: Option<netcdf::FileMut>,
35 path: std::path::PathBuf,
36 is_define_mode: bool,
37}
38
39impl std::fmt::Debug for NetCdfWriter {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("NetCdfWriter")
42 .field("path", &self.path)
43 .field("is_define_mode", &self.is_define_mode)
44 .finish_non_exhaustive()
45 }
46}
47
48impl NetCdfWriter {
49 #[allow(unused_variables)]
60 pub fn create(path: impl AsRef<Path>, version: NetCdfVersion) -> Result<Self> {
61 let path = path.as_ref();
62
63 if version.is_netcdf4() {
64 #[cfg(feature = "netcdf4")]
65 {
66 Self::create_netcdf4(path)
67 }
68 #[cfg(not(feature = "netcdf4"))]
69 {
70 Err(NetCdfError::NetCdf4NotAvailable)
71 }
72 } else {
73 #[cfg(feature = "netcdf3")]
74 {
75 Self::create_netcdf3(path)
76 }
77 #[cfg(not(feature = "netcdf3"))]
78 {
79 Err(NetCdfError::FeatureNotEnabled {
80 feature: "netcdf3".to_string(),
81 message: "Enable 'netcdf3' feature to write NetCDF-3 files".to_string(),
82 })
83 }
84 }
85 }
86
87 #[cfg(feature = "netcdf3")]
93 pub fn create_netcdf3(path: impl AsRef<Path>) -> Result<Self> {
94 let path = path.as_ref();
95 let dataset = netcdf3::DataSet::new();
96 let metadata = NetCdfMetadata::new_classic();
97
98 Ok(Self {
99 metadata,
100 dataset_nc3: Some(dataset),
101 pending_data: std::collections::HashMap::new(),
102 #[cfg(feature = "netcdf4")]
103 file_nc4: None,
104 path: path.to_path_buf(),
105 is_define_mode: true,
106 })
107 }
108
109 #[cfg(feature = "netcdf4")]
115 pub fn create_netcdf4(_path: impl AsRef<Path>) -> Result<Self> {
116 Err(NetCdfError::NetCdf4NotAvailable)
118 }
119
120 #[must_use]
122 pub const fn metadata(&self) -> &NetCdfMetadata {
123 &self.metadata
124 }
125
126 pub fn add_dimension(&mut self, dimension: Dimension) -> Result<()> {
132 if !self.is_define_mode {
133 return Err(NetCdfError::Other(
134 "Cannot add dimension outside of define mode".to_string(),
135 ));
136 }
137
138 self.metadata.dimensions_mut().add(dimension.clone())?;
140
141 #[cfg(feature = "netcdf3")]
143 if let Some(ref mut dataset) = self.dataset_nc3 {
144 if dimension.is_unlimited() {
145 dataset.set_unlimited_dim(dimension.name(), dimension.len())?;
146 } else {
147 dataset.add_fixed_dim(dimension.name(), dimension.len())?;
148 }
149 }
150
151 Ok(())
152 }
153
154 pub fn add_variable(&mut self, variable: Variable) -> Result<()> {
161 if !self.is_define_mode {
162 return Err(NetCdfError::Other(
163 "Cannot add variable outside of define mode".to_string(),
164 ));
165 }
166
167 for dim_name in variable.dimension_names() {
169 if !self.metadata.dimensions().contains(dim_name) {
170 return Err(NetCdfError::DimensionNotFound {
171 name: dim_name.clone(),
172 });
173 }
174 }
175
176 self.metadata.variables_mut().add(variable.clone())?;
178
179 #[cfg(feature = "netcdf3")]
181 if let Some(ref mut dataset) = self.dataset_nc3 {
182 let nc3_type = Self::convert_datatype_to_nc3(variable.data_type())?;
183 let dims: Vec<&str> = variable
184 .dimension_names()
185 .iter()
186 .map(|s| s.as_str())
187 .collect();
188 dataset.add_var(variable.name(), &dims, nc3_type)?;
189 }
190
191 Ok(())
192 }
193
194 pub fn add_global_attribute(&mut self, attribute: Attribute) -> Result<()> {
200 if !self.is_define_mode {
201 return Err(NetCdfError::Other(
202 "Cannot add global attribute outside of define mode".to_string(),
203 ));
204 }
205
206 self.metadata
208 .global_attributes_mut()
209 .add(attribute.clone())?;
210
211 #[cfg(feature = "netcdf3")]
213 if let Some(ref mut dataset) = self.dataset_nc3 {
214 Self::write_global_attribute_nc3(dataset, &attribute)?;
215 }
216
217 Ok(())
218 }
219
220 pub fn add_variable_attribute(&mut self, var_name: &str, attribute: Attribute) -> Result<()> {
226 if !self.is_define_mode {
227 return Err(NetCdfError::Other(
228 "Cannot add variable attribute outside of define mode".to_string(),
229 ));
230 }
231
232 let var = self
234 .metadata
235 .variables_mut()
236 .get_mut(var_name)
237 .ok_or_else(|| NetCdfError::VariableNotFound {
238 name: var_name.to_string(),
239 })?;
240 var.attributes_mut().add(attribute.clone())?;
241
242 #[cfg(feature = "netcdf3")]
244 if let Some(ref mut dataset) = self.dataset_nc3 {
245 Self::write_variable_attribute_nc3(dataset, var_name, &attribute)?;
246 }
247
248 Ok(())
249 }
250
251 pub fn end_define_mode(&mut self) -> Result<()> {
260 if !self.is_define_mode {
261 return Err(NetCdfError::Other("Already in data mode".to_string()));
262 }
263
264 self.metadata.validate()?;
266
267 self.is_define_mode = false;
268 Ok(())
269 }
270
271 pub fn write_f32(&mut self, var_name: &str, data: &[f32]) -> Result<()> {
278 if self.is_define_mode {
279 return Err(NetCdfError::Other(
280 "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
281 ));
282 }
283
284 let var = self.metadata.variables().get(var_name).ok_or_else(|| {
286 NetCdfError::VariableNotFound {
287 name: var_name.to_string(),
288 }
289 })?;
290
291 let expected_size = var.size(self.metadata.dimensions())?;
293 if data.len() != expected_size {
294 return Err(NetCdfError::InvalidShape {
295 message: format!(
296 "Data size {} does not match variable size {}",
297 data.len(),
298 expected_size
299 ),
300 });
301 }
302
303 #[cfg(feature = "netcdf3")]
305 {
306 self.pending_data
307 .insert(var_name.to_string(), PendingData::F32(data.to_vec()));
308 }
309
310 Ok(())
311 }
312
313 pub fn write_f64(&mut self, var_name: &str, data: &[f64]) -> Result<()> {
320 if self.is_define_mode {
321 return Err(NetCdfError::Other(
322 "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
323 ));
324 }
325
326 let var = self.metadata.variables().get(var_name).ok_or_else(|| {
327 NetCdfError::VariableNotFound {
328 name: var_name.to_string(),
329 }
330 })?;
331
332 let expected_size = var.size(self.metadata.dimensions())?;
333 if data.len() != expected_size {
334 return Err(NetCdfError::InvalidShape {
335 message: format!(
336 "Data size {} does not match variable size {}",
337 data.len(),
338 expected_size
339 ),
340 });
341 }
342
343 #[cfg(feature = "netcdf3")]
344 {
345 self.pending_data
346 .insert(var_name.to_string(), PendingData::F64(data.to_vec()));
347 }
348
349 Ok(())
350 }
351
352 pub fn write_i32(&mut self, var_name: &str, data: &[i32]) -> Result<()> {
359 if self.is_define_mode {
360 return Err(NetCdfError::Other(
361 "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
362 ));
363 }
364
365 let var = self.metadata.variables().get(var_name).ok_or_else(|| {
366 NetCdfError::VariableNotFound {
367 name: var_name.to_string(),
368 }
369 })?;
370
371 let expected_size = var.size(self.metadata.dimensions())?;
372 if data.len() != expected_size {
373 return Err(NetCdfError::InvalidShape {
374 message: format!(
375 "Data size {} does not match variable size {}",
376 data.len(),
377 expected_size
378 ),
379 });
380 }
381
382 #[cfg(feature = "netcdf3")]
383 {
384 self.pending_data
385 .insert(var_name.to_string(), PendingData::I32(data.to_vec()));
386 }
387
388 Ok(())
389 }
390
391 #[cfg(feature = "netcdf3")]
399 pub fn close(self) -> Result<()> {
400 if let Some(dataset) = self.dataset_nc3 {
401 if self.path.exists() {
403 std::fs::remove_file(&self.path).map_err(|e| {
404 NetCdfError::Io(format!("Failed to remove existing file: {}", e))
405 })?;
406 }
407 let mut writer = netcdf3::FileWriter::create_new(&self.path)?;
408 writer.set_def(&dataset, netcdf3::Version::Classic, 0)?;
409
410 for (var_name, data) in &self.pending_data {
412 match data {
413 PendingData::F32(values) => {
414 writer.write_var_f32(var_name, values)?;
415 }
416 PendingData::F64(values) => {
417 writer.write_var_f64(var_name, values)?;
418 }
419 PendingData::I32(values) => {
420 writer.write_var_i32(var_name, values)?;
421 }
422 PendingData::I16(values) => {
423 writer.write_var_i16(var_name, values)?;
424 }
425 PendingData::I8(values) => {
426 writer.write_var_i8(var_name, values)?;
427 }
428 }
429 }
430
431 writer.close()?;
432 }
433 Ok(())
434 }
435
436 #[cfg(not(feature = "netcdf3"))]
438 pub fn close(self) -> Result<()> {
439 Ok(())
440 }
441
442 #[cfg(feature = "netcdf3")]
444 fn convert_datatype_to_nc3(dtype: DataType) -> Result<netcdf3::DataType> {
445 use netcdf3::DataType as Nc3Type;
446
447 match dtype {
448 DataType::I8 => Ok(Nc3Type::I8),
449 DataType::I16 => Ok(Nc3Type::I16),
450 DataType::I32 => Ok(Nc3Type::I32),
451 DataType::F32 => Ok(Nc3Type::F32),
452 DataType::F64 => Ok(Nc3Type::F64),
453 DataType::Char => Ok(Nc3Type::U8), _ => Err(NetCdfError::DataTypeMismatch {
455 expected: "NetCDF-3 compatible type".to_string(),
456 found: dtype.name().to_string(),
457 }),
458 }
459 }
460
461 #[cfg(feature = "netcdf3")]
463 fn write_global_attribute_nc3(dataset: &mut netcdf3::DataSet, attr: &Attribute) -> Result<()> {
464 match attr.value() {
465 AttributeValue::Text(s) => {
466 dataset.add_global_attr_string(attr.name(), s)?;
467 }
468 AttributeValue::I8(v) => {
469 dataset.add_global_attr_i8(attr.name(), v.clone())?;
470 }
471 AttributeValue::U8(v) => {
472 dataset.add_global_attr_u8(attr.name(), v.clone())?;
473 }
474 AttributeValue::I16(v) => {
475 dataset.add_global_attr_i16(attr.name(), v.clone())?;
476 }
477 AttributeValue::I32(v) => {
478 dataset.add_global_attr_i32(attr.name(), v.clone())?;
479 }
480 AttributeValue::F32(v) => {
481 dataset.add_global_attr_f32(attr.name(), v.clone())?;
482 }
483 AttributeValue::F64(v) => {
484 dataset.add_global_attr_f64(attr.name(), v.clone())?;
485 }
486 _ => {
487 return Err(NetCdfError::AttributeError(
488 "Attribute type not supported in NetCDF-3".to_string(),
489 ));
490 }
491 }
492 Ok(())
493 }
494
495 #[cfg(feature = "netcdf3")]
497 fn write_variable_attribute_nc3(
498 dataset: &mut netcdf3::DataSet,
499 var_name: &str,
500 attr: &Attribute,
501 ) -> Result<()> {
502 match attr.value() {
503 AttributeValue::Text(s) => {
504 dataset.add_var_attr_string(var_name, attr.name(), s)?;
505 }
506 AttributeValue::I8(v) => {
507 dataset.add_var_attr_i8(var_name, attr.name(), v.clone())?;
508 }
509 AttributeValue::U8(v) => {
510 dataset.add_var_attr_u8(var_name, attr.name(), v.clone())?;
511 }
512 AttributeValue::I16(v) => {
513 dataset.add_var_attr_i16(var_name, attr.name(), v.clone())?;
514 }
515 AttributeValue::I32(v) => {
516 dataset.add_var_attr_i32(var_name, attr.name(), v.clone())?;
517 }
518 AttributeValue::F32(v) => {
519 dataset.add_var_attr_f32(var_name, attr.name(), v.clone())?;
520 }
521 AttributeValue::F64(v) => {
522 dataset.add_var_attr_f64(var_name, attr.name(), v.clone())?;
523 }
524 _ => {
525 return Err(NetCdfError::AttributeError(
526 "Attribute type not supported in NetCDF-3".to_string(),
527 ));
528 }
529 }
530 Ok(())
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn test_data_type_conversion() {
540 #[cfg(feature = "netcdf3")]
541 {
542 use netcdf3::DataType as Nc3Type;
543 assert_eq!(
544 NetCdfWriter::convert_datatype_to_nc3(DataType::F32).expect("F32 conversion"),
545 Nc3Type::F32
546 );
547 assert_eq!(
548 NetCdfWriter::convert_datatype_to_nc3(DataType::F64).expect("F64 conversion"),
549 Nc3Type::F64
550 );
551 assert_eq!(
552 NetCdfWriter::convert_datatype_to_nc3(DataType::I32).expect("I32 conversion"),
553 Nc3Type::I32
554 );
555
556 assert!(NetCdfWriter::convert_datatype_to_nc3(DataType::U16).is_err());
558 }
559 }
560}