#[cfg(not(target_arch = "wasm32"))]
mod ff10;
#[cfg(not(target_arch = "wasm32"))]
mod geotiff;
#[cfg(not(target_arch = "wasm32"))]
mod netcdf;
#[cfg(not(target_arch = "wasm32"))]
mod parquet;
#[cfg(not(target_arch = "wasm32"))]
mod shapefile;
#[cfg(not(target_arch = "wasm32"))]
mod zarr;
#[cfg(all(feature = "object-store", not(target_arch = "wasm32")))]
mod zarr_object_store;
#[cfg(all(target_arch = "wasm32", feature = "opfs"))]
mod zarr_opfs;
#[cfg(not(target_arch = "wasm32"))]
mod zarr_store;
mod zarr_write;
#[cfg(not(target_arch = "wasm32"))]
pub use ff10::Ff10Reader;
#[cfg(not(target_arch = "wasm32"))]
pub use geotiff::GeoTiffReader;
#[cfg(not(target_arch = "wasm32"))]
pub use netcdf::NetcdfReader;
#[cfg(not(target_arch = "wasm32"))]
pub use parquet::ParquetReader;
#[cfg(not(target_arch = "wasm32"))]
pub use shapefile::ShapefileReader;
#[cfg(not(target_arch = "wasm32"))]
pub use zarr::ZarrReader;
#[cfg(not(target_arch = "wasm32"))]
pub use zarr_write::write_zarr_v3;
pub use zarr_write::{
write_all_to_store, BloscProfile, CodecProfile, OutputSchema, WriteCoord, WriteVar,
ZstdProfile, BLOSC_CHECKPOINT, BLOSC_DIAGNOSTIC, ZSTD_WASM,
};
#[cfg(all(feature = "object-store", not(target_arch = "wasm32")))]
pub(crate) use zarr_object_store::{resolve_backend, runtime};
#[cfg(all(feature = "object-store", not(target_arch = "wasm32")))]
pub use zarr_object_store::{
array_shape_object_store, read_store_options, read_zarr_object_store,
read_zarr_object_store_with_options, store_options_from_env, write_zarr_object_store,
write_zarr_object_store_with_options,
};
#[cfg(all(target_arch = "wasm32", feature = "opfs"))]
pub use zarr_opfs::{read_zarr_opfs_array, write_zarr_opfs, OpfsStore};
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use crate::cache::Cache;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DType {
Float64,
Int64,
Int32,
Str,
Bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArrayData {
F64(Vec<f64>),
I64(Vec<i64>),
I32(Vec<i32>),
Str(Vec<String>),
Bool(Vec<bool>),
}
impl ArrayData {
pub fn len(&self) -> usize {
match self {
ArrayData::F64(v) => v.len(),
ArrayData::I64(v) => v.len(),
ArrayData::I32(v) => v.len(),
ArrayData::Str(v) => v.len(),
ArrayData::Bool(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn dtype(&self) -> DType {
match self {
ArrayData::F64(_) => DType::Float64,
ArrayData::I64(_) => DType::Int64,
ArrayData::I32(_) => DType::Int32,
ArrayData::Str(_) => DType::Str,
ArrayData::Bool(_) => DType::Bool,
}
}
fn block(&self, start: usize, len: usize) -> ArrayData {
match self {
ArrayData::F64(v) => ArrayData::F64(v[start..start + len].to_vec()),
ArrayData::I64(v) => ArrayData::I64(v[start..start + len].to_vec()),
ArrayData::I32(v) => ArrayData::I32(v[start..start + len].to_vec()),
ArrayData::Str(v) => ArrayData::Str(v[start..start + len].to_vec()),
ArrayData::Bool(v) => ArrayData::Bool(v[start..start + len].to_vec()),
}
}
}
#[derive(Debug, Clone)]
pub struct NativeField {
pub dtype: DType,
pub dims: Vec<String>,
pub shape: Vec<usize>,
pub data: ArrayData,
pub fill_value: Option<f64>,
}
impl NativeField {
pub fn select_leading(&self, i: usize) -> Result<NativeField> {
let Some((&lead, rest)) = self.shape.split_first() else {
return Err(crate::Error::Format {
format: "native".to_string(),
detail: "cannot select a record from a scalar field".to_string(),
});
};
if i >= lead {
return Err(crate::Error::Format {
format: "native".to_string(),
detail: format!("record index {i} out of range (leading dim = {lead})"),
});
}
let block: usize = rest.iter().product();
Ok(NativeField {
dtype: self.dtype,
dims: self.dims[1..].to_vec(),
shape: rest.to_vec(),
data: self.data.block(i * block, block),
fill_value: self.fill_value,
})
}
}
#[derive(Debug, Clone)]
pub struct Coord {
pub field: NativeField,
pub units: Option<String>,
pub calendar: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct NativeDataset {
pub variables: HashMap<String, NativeField>,
pub coords: HashMap<String, Coord>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum Selection {
#[default]
All,
Orthogonal(Vec<AxisSelect>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Records {
pub dim: String,
pub indices: Vec<usize>,
}
impl Records {
pub fn new(dim: impl Into<String>, indices: Vec<usize>) -> Self {
Self {
dim: dim.into(),
indices,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AxisSelect {
All,
Indices(Vec<usize>),
Range {
start: usize,
stop: usize,
step: usize,
},
}
impl AxisSelect {
pub fn resolve(&self, dim_len: usize) -> Result<Vec<usize>> {
self.resolve_in(dim_len, "zarr")
}
pub fn resolve_in(&self, dim_len: usize, format: &str) -> Result<Vec<usize>> {
match self {
AxisSelect::All => Ok((0..dim_len).collect()),
AxisSelect::Indices(v) => {
for &g in v {
if g >= dim_len {
return Err(crate::Error::Format {
format: format.to_string(),
detail: format!("index {g} out of range for dimension length {dim_len}"),
});
}
}
Ok(v.clone())
}
AxisSelect::Range { start, stop, step } => {
if *step < 1 {
return Err(crate::Error::Format {
format: format.to_string(),
detail: format!("slice step must be >= 1, got {step}"),
});
}
let out: Vec<usize> = (*start..*stop).step_by(*step).collect();
if let Some(&g) = out.last() {
if g >= dim_len {
return Err(crate::Error::Format {
format: format.to_string(),
detail: format!(
"slice [{start}, {stop}) by {step} reaches index {g}, out of \
range for dimension length {dim_len}"
),
});
}
}
Ok(out)
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub trait Reader: Send + Sync {
fn formats(&self) -> &'static [&'static str];
fn extensions(&self) -> &'static [&'static str];
fn read_native(
&self,
blob_path: &Path,
variables: &[String],
select: &Selection,
) -> Result<NativeDataset>;
fn read_native_records(
&self,
blob_path: &Path,
variables: &[String],
select: &Selection,
records: Option<&Records>,
) -> Result<NativeDataset> {
match records {
None => self.read_native(blob_path, variables, select),
Some(r) => Err(crate::Error::Format {
format: self.formats().first().copied().unwrap_or("native").to_string(),
detail: format!(
"reader does not honour a `records` pushdown, but one was asked \
for along dimension '{}'; read the whole record axis and slice \
on the caller's side instead",
r.dim
),
}),
}
}
fn supports_records(&self) -> bool {
false
}
fn dim_length(&self, _blob_path: &Path, _dim: &str) -> Result<Option<usize>> {
Ok(None)
}
fn store_backed(&self) -> bool {
false
}
fn supports_selection(&self) -> bool {
false
}
fn read_store(
&self,
_cache: Arc<Cache>,
_base_url: &str,
_variables: &[String],
_select: &Selection,
) -> Result<NativeDataset> {
Err(crate::Error::Format {
format: "native".to_string(),
detail: "reader is not store-backed".to_string(),
})
}
fn array_shape(
&self,
_cache: Arc<Cache>,
_base_url: &str,
_var: &str,
) -> Result<Option<Vec<usize>>> {
Ok(None)
}
fn configured(
&self,
options: &serde_json::Map<String, serde_json::Value>,
) -> Result<Option<Arc<dyn Reader>>> {
if options.is_empty() {
return Ok(None);
}
let mut keys: Vec<&str> = options.keys().map(String::as_str).collect();
keys.sort_unstable();
Err(crate::Error::Format {
format: self.formats().first().copied().unwrap_or("native").to_string(),
detail: format!("reader takes no reader_options, but the loader declares {keys:?}"),
})
}
fn supports_direct_read(&self) -> bool {
false
}
fn read_store_direct(
&self,
_base_url: &str,
_variables: &[String],
_select: &Selection,
_options: &[(String, String)],
) -> Result<NativeDataset> {
Err(crate::Error::Format {
format: "native".to_string(),
detail: "reader does not support direct (uncached) store reads".to_string(),
})
}
fn array_shape_direct(
&self,
_base_url: &str,
_var: &str,
_options: &[(String, String)],
) -> Result<Option<Vec<usize>>> {
Ok(None)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Default, Clone)]
pub struct FormatRegistry {
by_name: HashMap<String, Arc<dyn Reader>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl FormatRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_builtins() -> Self {
let mut r = Self::new();
r.register(Arc::new(NetcdfReader::new()));
r.register(Arc::new(GeoTiffReader::new()));
r.register(Arc::new(Ff10Reader::new()));
r.register(Arc::new(ShapefileReader::new()));
r.register(Arc::new(ParquetReader::new()));
r.register(Arc::new(ZarrReader::new()));
r
}
pub fn register(&mut self, reader: Arc<dyn Reader>) -> &mut Self {
for name in reader.formats() {
self.by_name.insert((*name).to_string(), reader.clone());
}
self
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Reader>> {
self.by_name.get(name).cloned()
}
pub fn registered(&self) -> Vec<String> {
self.by_name.keys().cloned().collect()
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn builtins_cover_netcdf() {
let r = FormatRegistry::with_builtins();
assert!(r.get("netcdf").is_some());
assert!(r.get("zarr").is_some()); assert!(r.get("zarr").unwrap().store_backed());
assert!(!r.get("netcdf").unwrap().store_backed());
}
#[test]
fn a_second_reader_plugs_in_without_touching_the_provider() {
struct DummyReader;
impl Reader for DummyReader {
fn formats(&self) -> &'static [&'static str] {
&["dummy"]
}
fn extensions(&self) -> &'static [&'static str] {
&["dmy"]
}
fn read_native(&self, _: &Path, _: &[String], _: &Selection) -> Result<NativeDataset> {
Ok(NativeDataset::default())
}
}
let mut r = FormatRegistry::with_builtins();
r.register(Arc::new(DummyReader));
assert!(r.get("dummy").is_some());
assert!(r.get("netcdf").is_some()); }
#[test]
fn select_leading_drops_the_record_axis() {
let f = NativeField {
dtype: DType::Float64,
dims: vec!["time".into(), "lat".into(), "lon".into()],
shape: vec![2, 2, 2],
data: ArrayData::F64(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]),
fill_value: None,
};
let r0 = f.select_leading(0).unwrap();
assert_eq!(r0.dims, vec!["lat".to_string(), "lon".to_string()]);
assert_eq!(r0.shape, vec![2, 2]);
assert_eq!(r0.data, ArrayData::F64(vec![0.0, 1.0, 2.0, 3.0]));
let r1 = f.select_leading(1).unwrap();
assert_eq!(r1.data, ArrayData::F64(vec![4.0, 5.0, 6.0, 7.0]));
assert!(f.select_leading(2).is_err()); }
}