diff --git a/CHANGES.md b/CHANGES.md index be92a79d..a78c91bf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,10 @@ - https://github.com/georust/gdal/pull/267 +- **Breaking**: Rename `Driver::get` to `Driver::get_by_name`, add `Driver::get(usize)` and `Driver::count` + + - + - Add `programs::raster::build_vrt` - Add `GeoTransformEx` extension trait with `apply` and `invert` @@ -35,6 +39,14 @@ - +- Add support for MDArray API + + - + +- Add `gdal::srs::CoordTransform::transform_bounds` as wrapper for `OCTTransformBounds` for GDAL 3.4 + + - + ## 0.12 - Bump Rust edition to 2021 @@ -92,7 +104,7 @@ - ```rust -let driver = Driver::get("GTiff").unwrap(); +let driver = Driver::get_by_name("GTiff").unwrap(); let options = &[ RasterCreationOption { key: "COMPRESS", diff --git a/examples/metadata.rs b/examples/metadata.rs index 73759be5..80b79962 100644 --- a/examples/metadata.rs +++ b/examples/metadata.rs @@ -2,7 +2,7 @@ fn main() { use gdal::{Dataset, Metadata}; use std::path::Path; - let driver = gdal::Driver::get("mem").unwrap(); + let driver = gdal::Driver::get_by_name("mem").unwrap(); println!("driver description: {:?}", driver.description()); let path = Path::new("./fixtures/tinymarble.png"); diff --git a/examples/read_write_ogr.rs b/examples/read_write_ogr.rs index 7dc86b38..39bfa510 100644 --- a/examples/read_write_ogr.rs +++ b/examples/read_write_ogr.rs @@ -17,7 +17,7 @@ fn run() -> Result<()> { // Create a new dataset: let path = std::env::temp_dir().join("abcde.shp"); let _ = fs::remove_file(&path); - let drv = Driver::get("ESRI Shapefile")?; + let drv = Driver::get_by_name("ESRI Shapefile")?; let mut ds = drv.create_vector_only(path.to_str().unwrap())?; let lyr = ds.create_layer(Default::default())?; diff --git a/examples/read_write_ogr_datetime.rs b/examples/read_write_ogr_datetime.rs index 62656594..6470d1c7 100644 --- a/examples/read_write_ogr_datetime.rs +++ b/examples/read_write_ogr_datetime.rs @@ -15,7 +15,7 @@ fn run() -> gdal::errors::Result<()> { // Create a new dataset: let path = std::env::temp_dir().join("later.geojson"); let _ = std::fs::remove_file(&path); - let drv = Driver::get("GeoJSON")?; + let drv = Driver::get_by_name("GeoJSON")?; let mut ds = drv.create_vector_only(path.to_str().unwrap())?; let lyr = ds.create_layer(Default::default())?; diff --git a/examples/write_ogr.rs b/examples/write_ogr.rs index 60c15f73..b71cdc83 100644 --- a/examples/write_ogr.rs +++ b/examples/write_ogr.rs @@ -7,7 +7,7 @@ use std::fs; fn example_1() -> Result<()> { let path = std::env::temp_dir().join("output1.geojson"); let _ = fs::remove_file(&path); - let drv = Driver::get("GeoJSON")?; + let drv = Driver::get_by_name("GeoJSON")?; let mut ds = drv.create_vector_only(path.to_str().unwrap())?; let lyr = ds.create_layer(Default::default())?; @@ -52,7 +52,7 @@ fn example_1() -> Result<()> { fn example_2() -> Result<()> { let path = std::env::temp_dir().join("output2.geojson"); let _ = fs::remove_file(&path); - let driver = Driver::get("GeoJSON")?; + let driver = Driver::get_by_name("GeoJSON")?; let mut ds = driver.create_vector_only(path.to_str().unwrap())?; let mut layer = ds.create_layer(Default::default())?; diff --git a/fixtures/alldatatypes.nc b/fixtures/alldatatypes.nc new file mode 100644 index 00000000..ab6048f4 Binary files /dev/null and b/fixtures/alldatatypes.nc differ diff --git a/fixtures/byte_no_cf.nc b/fixtures/byte_no_cf.nc new file mode 100644 index 00000000..b9bf53e9 Binary files /dev/null and b/fixtures/byte_no_cf.nc differ diff --git a/fixtures/cf_nasa_4326.nc b/fixtures/cf_nasa_4326.nc new file mode 100644 index 00000000..f4c2bae7 Binary files /dev/null and b/fixtures/cf_nasa_4326.nc differ diff --git a/src/dataset.rs b/src/dataset.rs index fe7003e7..dbfe7e34 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -17,12 +17,16 @@ use crate::{ gdal_major_object::MajorObject, raster::RasterBand, spatial_ref::SpatialRef, vector::Layer, Driver, Metadata, }; + use gdal_sys::OGRGeometryH; use gdal_sys::{ self, CPLErr, GDALAccess, GDALDatasetH, GDALMajorObjectH, OGRErr, OGRLayerH, OGRwkbGeometryType, }; use libc::{c_double, c_int, c_uint}; +#[cfg(all(major_ge_3, minor_ge_1))] +use crate::raster::Group; + use bitflags::bitflags; /// A 2-D affine transform mapping pixel coordiates to world @@ -415,6 +419,23 @@ impl Dataset { } } + /// Opens the root group of a multi-dim GDAL raster + /// + /// # Note + /// You must have opened the dataset with the `GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER` + /// flag in order for it to work. + /// + #[cfg(all(major_ge_3, minor_ge_1))] + pub fn root_group(&self) -> Result { + unsafe { + let c_group = gdal_sys::GDALDatasetGetRootGroup(self.c_dataset()); + if c_group.is_null() { + return Err(_last_null_pointer_err("GDALDatasetGetRootGroup")); + } + Ok(Group::from_c_group(self, c_group)) + } + } + /// Builds overviews for the current `Dataset`. See [`GDALBuildOverviews`]. /// /// # Arguments @@ -531,7 +552,7 @@ impl Dataset { /// /// ``` /// # use gdal::Driver; - /// # let driver = Driver::get("GPKG").unwrap(); + /// # let driver = Driver::get_by_name("GPKG").unwrap(); /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); /// let blank_layer = dataset.create_layer(Default::default()).unwrap(); /// ``` @@ -541,7 +562,7 @@ impl Dataset { /// ``` /// # use gdal::{Driver, LayerOptions}; /// # use gdal::spatial_ref::SpatialRef; - /// # let driver = Driver::get("GPKG").unwrap(); + /// # let driver = Driver::get_by_name("GPKG").unwrap(); /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); /// let roads = dataset.create_layer(LayerOptions { /// name: "roads", @@ -699,7 +720,7 @@ impl Dataset { /// } /// # /// # fn main() -> gdal::errors::Result<()> { - /// # let driver = gdal::Driver::get("SQLite")?; + /// # let driver = gdal::Driver::get_by_name("SQLite")?; /// # let mut dataset = driver.create_vector_only(":memory:")?; /// # create_point_grid(&mut dataset)?; /// # assert_eq!(dataset.layer(0)?.features().count(), 10000); diff --git a/src/driver.rs b/src/driver.rs index d37de998..f8949a1f 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -30,7 +30,8 @@ pub struct Driver { } impl Driver { - pub fn get(name: &str) -> Result { + /// Returns the driver with the given short name. + pub fn get_by_name(name: &str) -> Result { _register_drivers(); let c_name = CString::new(name)?; let c_driver = unsafe { gdal_sys::GDALGetDriverByName(c_name.as_ptr()) }; @@ -40,6 +41,24 @@ impl Driver { Ok(Driver { c_driver }) } + /// Returns the driver with the given index, which must be less than the value returned by + /// `Driver::count()`. + pub fn get(index: usize) -> Result { + _register_drivers(); + let c_driver = unsafe { gdal_sys::GDALGetDriver(index.try_into().unwrap()) }; + if c_driver.is_null() { + return Err(_last_null_pointer_err("GDALGetDriver")); + } + Ok(Driver { c_driver }) + } + + /// Returns the number of registered drivers. + pub fn count() -> usize { + _register_drivers(); + let count = unsafe { gdal_sys::GDALGetDriverCount() }; + count.try_into().unwrap() + } + /// Creates a new Driver object by wrapping a C pointer /// /// # Safety diff --git a/src/raster/mdarray.rs b/src/raster/mdarray.rs new file mode 100644 index 00000000..d9a809e1 --- /dev/null +++ b/src/raster/mdarray.rs @@ -0,0 +1,904 @@ +use super::GdalType; +use crate::errors::*; +use crate::spatial_ref::SpatialRef; +use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string, _string_array}; +use crate::{cpl::CslStringList, Dataset}; +use gdal_sys::{ + CPLErr, CSLDestroy, GDALAttributeGetDataType, GDALAttributeGetDimensionsSize, GDALAttributeH, + GDALAttributeReadAsDouble, GDALAttributeReadAsDoubleArray, GDALAttributeReadAsInt, + GDALAttributeReadAsIntArray, GDALAttributeReadAsString, GDALAttributeReadAsStringArray, + GDALAttributeRelease, GDALDataType, GDALDimensionGetIndexingVariable, GDALDimensionGetName, + GDALDimensionGetSize, GDALDimensionHS, GDALDimensionRelease, GDALExtendedDataTypeClass, + GDALExtendedDataTypeGetClass, GDALExtendedDataTypeGetNumericDataType, GDALExtendedDataTypeH, + GDALExtendedDataTypeRelease, GDALGroupGetAttribute, GDALGroupGetGroupNames, + GDALGroupGetMDArrayNames, GDALGroupGetName, GDALGroupH, GDALGroupOpenGroup, + GDALGroupOpenMDArray, GDALGroupRelease, GDALMDArrayGetAttribute, GDALMDArrayGetDataType, + GDALMDArrayGetDimensionCount, GDALMDArrayGetDimensions, GDALMDArrayGetNoDataValueAsDouble, + GDALMDArrayGetSpatialRef, GDALMDArrayGetTotalElementsCount, GDALMDArrayGetUnit, GDALMDArrayH, + GDALMDArrayRelease, OSRDestroySpatialReference, VSIFree, +}; +use libc::c_void; +use std::ffi::CString; +use std::os::raw::c_char; + +#[cfg(feature = "ndarray")] +use ndarray::{ArrayD, IxDyn}; +use std::fmt::Debug; + +/// Represent an MDArray in a Group +/// +/// This object carries the lifetime of the Group that +/// contains it. This is necessary to prevent the Group +/// from being dropped before the mdarray. +#[derive(Debug)] +pub struct MDArray<'a> { + c_mdarray: GDALMDArrayH, + _parent: GroupOrDimension<'a>, +} + +#[derive(Debug)] +enum GroupOrDimension<'a> { + Group { _group: &'a Group<'a> }, + Dimension { _dimension: &'a Dimension<'a> }, +} + +impl Drop for MDArray<'_> { + fn drop(&mut self) { + unsafe { + GDALMDArrayRelease(self.c_mdarray); + } + } +} + +impl<'a> MDArray<'a> { + /// Create a MDArray from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub unsafe fn from_c_mdarray_and_group(_group: &'a Group, c_mdarray: GDALMDArrayH) -> Self { + Self { + c_mdarray, + _parent: GroupOrDimension::Group { _group }, + } + } + + /// Create a MDArray from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub unsafe fn from_c_mdarray_and_dimension( + _dimension: &'a Dimension, + c_mdarray: GDALMDArrayH, + ) -> Self { + Self { + c_mdarray, + _parent: GroupOrDimension::Dimension { _dimension }, + } + } + + pub fn num_dimensions(&self) -> usize { + unsafe { GDALMDArrayGetDimensionCount(self.c_mdarray) } + } + + pub fn num_elements(&self) -> u64 { + unsafe { GDALMDArrayGetTotalElementsCount(self.c_mdarray) } + } + + pub fn dimensions(&self) -> Result> { + unsafe { + let mut num_dimensions: usize = 0; + let c_dimensions = + GDALMDArrayGetDimensions(self.c_mdarray, std::ptr::addr_of_mut!(num_dimensions)); + + let dimensions_ref = std::slice::from_raw_parts_mut(c_dimensions, num_dimensions); + + let mut dimensions: Vec = Vec::with_capacity(num_dimensions); + + for c_dimension in dimensions_ref { + let dimension = Dimension::from_c_dimension(self, *c_dimension); + dimensions.push(dimension); + } + + // only free the array, not the dimensions themselves + VSIFree(c_dimensions as *mut c_void); + + Ok(dimensions) + } + } + + pub fn datatype(&self) -> ExtendedDataType { + unsafe { + let c_data_type = GDALMDArrayGetDataType(self.c_mdarray); + + ExtendedDataType::from_c_extended_data_type(c_data_type) + } + } + + /// Wrapper for `GDALMDArrayRead` + /// + /// # Params + /// * buffer - Mutable buffer to read into + /// * array_start_index - Values representing the starting index to read in each dimension (in [0, aoDims[i].GetSize()-1] range). + /// Array of GetDimensionCount() values. Must not be empty, unless for a zero-dimensional array. + /// * count - Values representing the number of values to extract in each dimension. Array of GetDimensionCount() values. + /// Must not be empty, unless for a zero-dimensional array. + /// + pub fn read_into_slice( + &self, + buffer: &mut [T], + array_start_index: Vec, + count: Vec, + ) -> Result<()> { + // If set to nullptr, [1, 1, … 1] will be used as a default to indicate consecutive elements. + let array_step: *const i64 = std::ptr::null(); + // If set to nullptr, will be set so that pDstBuffer is written in a compact way, + // with elements of the last / fastest varying dimension being consecutive. + let buffer_stride: *const i64 = std::ptr::null(); + let p_dst_buffer_alloc_start: *mut libc::c_void = std::ptr::null_mut(); + let n_dst_buffer_alloc_size = 0; + + let rv = unsafe { + let data_type = GDALMDArrayGetDataType(self.c_mdarray); + + let rv = gdal_sys::GDALMDArrayRead( + self.c_mdarray, + array_start_index.as_ptr(), + count.as_ptr(), + array_step, + buffer_stride, + data_type, + buffer.as_mut_ptr() as *mut c_void, + p_dst_buffer_alloc_start, + n_dst_buffer_alloc_size, + ); + + GDALExtendedDataTypeRelease(data_type); + + rv + }; + + // `rv` is boolean + if rv != 1 { + // OSGeo Python wrapper treats it as `CE_Failure` + return Err(_last_cpl_err(CPLErr::CE_Failure)); + } + + Ok(()) + } + + /// Read a 'Buffer' from this band. T implements 'GdalType' + /// + /// # Arguments + /// * array_start_index - Values representing the starting index to read in each dimension (in [0, aoDims[i].GetSize()-1] range). + /// Array of GetDimensionCount() values. Must not be empty, unless for a zero-dimensional array. + /// * count - Values representing the number of values to extract in each dimension. Array of GetDimensionCount() values. + /// Must not be empty, unless for a zero-dimensional array. + /// + pub fn read_as( + &self, + array_start_index: Vec, + count: Vec, + ) -> Result> { + let pixels: usize = count.iter().product(); + let mut data: Vec = Vec::with_capacity(pixels); + + // Safety: the read_into_slice line below writes + // exactly pixel elements into the slice, before we + // read from this slice. This paradigm is suggested + // in the rust std docs + // (https://doc.rust-lang.org/std/vec/struct.Vec.html#examples-18) + unsafe { + self.read_into_slice(&mut data, array_start_index, count)?; + data.set_len(pixels); + }; + + Ok(data) + } + + #[cfg(feature = "ndarray")] + /// Read a 'Array2' from this band. T implements 'GdalType'. + /// + /// # Arguments + /// * window - the window position from top left + /// * window_size - the window size (GDAL will interpolate data if window_size != array_size) + /// * array_size - the desired size of the 'Array' + /// * e_resample_alg - the resample algorithm used for the interpolation + /// # Docs + /// The Matrix shape is (rows, cols) and raster shape is (cols in x-axis, rows in y-axis). + pub fn read_as_array( + &self, + array_start_index: Vec, + count: Vec, + array_size: Vec, + ) -> Result> { + let data = self.read_as::(array_start_index, count)?; + // Matrix shape is (rows, cols) and raster shape is (cols in x-axis, rows in y-axis) + + let dim: IxDyn = IxDyn(&array_size); + Ok(ArrayD::from_shape_vec(dim, data)?) + } + + /// Read `MDArray` as one-dimensional string array + pub fn read_as_string_array(&self) -> Result> { + let num_values = self.num_elements() as usize; + let mut string_pointers: Vec<*const c_char> = vec![std::ptr::null(); num_values]; + + let count: Vec = self + .dimensions()? + .into_iter() + .map(|dim| dim.size()) + .collect(); + let array_start_index: Vec = vec![0; count.len()]; + + // If set to nullptr, [1, 1, … 1] will be used as a default to indicate consecutive elements. + let array_step: *const i64 = std::ptr::null(); + // If set to nullptr, will be set so that pDstBuffer is written in a compact way, + // with elements of the last / fastest varying dimension being consecutive. + let buffer_stride: *const i64 = std::ptr::null(); + + let p_dst_buffer_alloc_start: *mut libc::c_void = std::ptr::null_mut(); + let n_dst_buffer_alloc_size = 0; + + unsafe { + let data_type = GDALMDArrayGetDataType(self.c_mdarray); + + let rv = gdal_sys::GDALMDArrayRead( + self.c_mdarray, + array_start_index.as_ptr(), + count.as_ptr(), + array_step, + buffer_stride, + data_type, + string_pointers.as_mut_ptr().cast::(), + p_dst_buffer_alloc_start, + n_dst_buffer_alloc_size, + ); + + GDALExtendedDataTypeRelease(data_type); + + // `rv` is boolean + if rv != 1 { + // OSGeo Python wrapper treats it as `CE_Failure` + return Err(_last_cpl_err(CPLErr::CE_Failure)); + } + + let strings = string_pointers + .into_iter() + .map(|string_ptr| { + let string = _string(string_ptr); + + VSIFree(string_ptr as *mut c_void); + + string + }) + .collect(); + + Ok(strings) + } + } + + pub fn spatial_reference(&self) -> Result { + unsafe { + let c_gdal_spatial_ref = GDALMDArrayGetSpatialRef(self.c_mdarray); + + let gdal_spatial_ref = SpatialRef::from_c_obj(c_gdal_spatial_ref); + + OSRDestroySpatialReference(c_gdal_spatial_ref); + + gdal_spatial_ref + } + } + + pub fn no_data_value_as_double(&self) -> Option { + let mut has_nodata = 0; + + let no_data_value = unsafe { + GDALMDArrayGetNoDataValueAsDouble(self.c_mdarray, std::ptr::addr_of_mut!(has_nodata)) + }; + + if has_nodata == 0 { + None + } else { + Some(no_data_value) + } + } + + pub fn unit(&self) -> String { + unsafe { + // should not be freed + let c_unit = GDALMDArrayGetUnit(self.c_mdarray); + + _string(c_unit) + } + } + + pub fn attribute(&self, name: &str) -> Result { + let name = CString::new(name)?; + + unsafe { + let c_attribute = GDALMDArrayGetAttribute(self.c_mdarray, name.as_ptr()); + + if c_attribute.is_null() { + return Err(_last_null_pointer_err("GDALGroupGetAttribute")); + } + + Ok(Attribute::from_c_attribute(c_attribute)) + } + } +} + +/// Represent a mdarray in a dataset +/// +/// This object carries the lifetime of the dataset that +/// contains it. This is necessary to prevent the dataset +/// from being dropped before the group. +#[derive(Debug)] +pub struct Group<'a> { + c_group: GDALGroupH, + _dataset: &'a Dataset, +} + +impl Drop for Group<'_> { + fn drop(&mut self) { + unsafe { + GDALGroupRelease(self.c_group); + } + } +} + +impl<'a> Group<'a> { + /// Create a Group from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub unsafe fn from_c_group(_dataset: &'a Dataset, c_group: GDALGroupH) -> Self { + Group { c_group, _dataset } + } + + pub fn name(&self) -> String { + _string(unsafe { GDALGroupGetName(self.c_group) }) + } + + pub fn group_names(&self, options: CslStringList) -> Vec { + unsafe { + let c_group_names = GDALGroupGetGroupNames(self.c_group, options.as_ptr()); + + let strings = _string_array(c_group_names); + + CSLDestroy(c_group_names); + + strings + } + } + + pub fn array_names(&self, options: CslStringList) -> Vec { + unsafe { + let c_array_names = GDALGroupGetMDArrayNames(self.c_group, options.as_ptr()); + + let strings = _string_array(c_array_names); + + CSLDestroy(c_array_names); + + strings + } + } + + pub fn open_md_array(&self, name: &str, options: CslStringList) -> Result { + let name = CString::new(name)?; + + unsafe { + let c_mdarray = GDALGroupOpenMDArray(self.c_group, name.as_ptr(), options.as_ptr()); + + if c_mdarray.is_null() { + return Err(_last_null_pointer_err("GDALGroupOpenMDArray")); + } + + Ok(MDArray::from_c_mdarray_and_group(self, c_mdarray)) + } + } + + pub fn open_group(&self, name: &str, options: CslStringList) -> Result { + let name = CString::new(name)?; + + unsafe { + let c_group = GDALGroupOpenGroup(self.c_group, name.as_ptr(), options.as_ptr()); + + if c_group.is_null() { + return Err(_last_null_pointer_err("GDALGroupOpenGroup")); + } + + Ok(Group::from_c_group(self._dataset, c_group)) + } + } + + pub fn attribute(&self, name: &str) -> Result { + let name = CString::new(name)?; + + unsafe { + let c_attribute = GDALGroupGetAttribute(self.c_group, name.as_ptr()); + + if c_attribute.is_null() { + return Err(_last_null_pointer_err("GDALGroupGetAttribute")); + } + + Ok(Attribute::from_c_attribute(c_attribute)) + } + } +} + +/// A `GDALDimension` with name and size +#[derive(Debug)] +pub struct Dimension<'a> { + c_dimension: *mut GDALDimensionHS, + _md_array: &'a MDArray<'a>, +} + +impl Drop for Dimension<'_> { + fn drop(&mut self) { + unsafe { + GDALDimensionRelease(self.c_dimension); + } + } +} + +impl<'a> Dimension<'a> { + /// Create a MDArray from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub fn from_c_dimension(_md_array: &'a MDArray<'a>, c_dimension: *mut GDALDimensionHS) -> Self { + Self { + c_dimension, + _md_array, + } + } + pub fn size(&self) -> usize { + unsafe { GDALDimensionGetSize(self.c_dimension) as usize } + } + + pub fn name(&self) -> String { + _string(unsafe { GDALDimensionGetName(self.c_dimension) }) + } + + pub fn indexing_variable(&self) -> MDArray { + unsafe { + let c_md_array = GDALDimensionGetIndexingVariable(self.c_dimension); + + MDArray::from_c_mdarray_and_dimension(self, c_md_array) + } + } +} + +/// Wrapper for `GDALExtendedDataType` +#[derive(Debug)] +pub struct ExtendedDataType { + c_data_type: GDALExtendedDataTypeH, +} + +impl Drop for ExtendedDataType { + fn drop(&mut self) { + unsafe { + GDALExtendedDataTypeRelease(self.c_data_type); + } + } +} + +impl ExtendedDataType { + /// Create an `ExtendedDataTypeNumeric` from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub fn from_c_extended_data_type(c_data_type: GDALExtendedDataTypeH) -> Self { + Self { c_data_type } + } + + /// The result is only valid if the data type is numeric + pub fn class(&self) -> GDALExtendedDataTypeClass::Type { + unsafe { GDALExtendedDataTypeGetClass(self.c_data_type) } + } + + /// The result is only valid if the data type is numeric + pub fn numeric_datatype(&self) -> GDALDataType::Type { + unsafe { GDALExtendedDataTypeGetNumericDataType(self.c_data_type) } + } +} + +// Wrapper for `GDALExtendedDataType` +#[derive(Debug)] +pub struct Attribute { + c_attribute: GDALAttributeH, +} + +impl Drop for Attribute { + fn drop(&mut self) { + unsafe { + GDALAttributeRelease(self.c_attribute); + } + } +} + +impl Attribute { + /// Create an `ExtendedDataTypeNumeric` from a wrapped C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + pub fn from_c_attribute(c_attribute: GDALAttributeH) -> Self { + Self { c_attribute } + } + + /// Return the size of the dimensions of the attribute. + /// This will be an empty array for a scalar (single value) attribute. + pub fn dimension_sizes(&self) -> Vec { + unsafe { + let mut num_dimensions = 0; + + let c_dimension_sizes = GDALAttributeGetDimensionsSize( + self.c_attribute, + std::ptr::addr_of_mut!(num_dimensions), + ); + + let dimension_sizes = std::slice::from_raw_parts(c_dimension_sizes, num_dimensions) + .iter() + .map(|&size| size as usize) + .collect(); + + VSIFree(c_dimension_sizes as *mut c_void); + + dimension_sizes + } + } + + pub fn datatype(&self) -> ExtendedDataType { + unsafe { + let c_data_type = GDALAttributeGetDataType(self.c_attribute); + ExtendedDataType::from_c_extended_data_type(c_data_type) + } + } + + pub fn read_as_string(&self) -> String { + unsafe { + // SAFETY: should no be freed + let c_string = GDALAttributeReadAsString(self.c_attribute); + + _string(c_string) + } + } + + pub fn read_as_string_array(&self) -> Vec { + unsafe { + let c_string_array = GDALAttributeReadAsStringArray(self.c_attribute); + + let string_array = _string_array(c_string_array); + + CSLDestroy(c_string_array); + + string_array + } + } + + pub fn read_as_i64(&self) -> i32 { + unsafe { GDALAttributeReadAsInt(self.c_attribute) } + } + + pub fn read_as_i64_array(&self) -> Vec { + unsafe { + let mut array_len = 0; + let c_int_array = + GDALAttributeReadAsIntArray(self.c_attribute, std::ptr::addr_of_mut!(array_len)); + + let int_array = std::slice::from_raw_parts(c_int_array, array_len).to_vec(); + + VSIFree(c_int_array as *mut c_void); + + int_array + } + } + + pub fn read_as_f64(&self) -> f64 { + unsafe { GDALAttributeReadAsDouble(self.c_attribute) } + } + + pub fn read_as_f64_array(&self) -> Vec { + unsafe { + let mut array_len = 0; + let c_int_array = + GDALAttributeReadAsDoubleArray(self.c_attribute, std::ptr::addr_of_mut!(array_len)); + + let float_array = std::slice::from_raw_parts(c_int_array, array_len).to_vec(); + + VSIFree(c_int_array as *mut c_void); + + float_array + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{Dataset, DatasetOptions, GdalOpenFlags}; + + #[test] + fn test_root_group_name() { + let options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let root_group_name = root_group.name(); + assert_eq!(root_group_name, "/"); + } + + #[test] + fn test_array_names() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let options = CslStringList::new(); //Driver specific options determining how groups should be retrieved. Pass nullptr for default behavior. + let array_names = root_group.array_names(options); + assert_eq!(array_names, vec!["Band1".to_string()]) + } + + #[test] + fn test_n_dimension() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let array_name = "Band1".to_string(); + let options = CslStringList::new(); //Driver specific options determining how the array should be opened. Pass nullptr for default behavior. + let md_array = root_group.open_md_array(&array_name, options).unwrap(); + let n_dimension = md_array.num_dimensions(); + assert_eq!(2, n_dimension); + } + + #[test] + fn test_n_elements() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let array_name = "Band1".to_string(); + let options = CslStringList::new(); //Driver specific options determining how the array should be opened. Pass nullptr for default behavior. + let md_array = root_group.open_md_array(&array_name, options).unwrap(); + let n_elements = md_array.num_elements(); + assert_eq!(400, n_elements); + } + + #[test] + fn test_dimension_name() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let array_name = "Band1".to_string(); + let options = CslStringList::new(); //Driver specific options determining how the array should be opened. Pass nullptr for default behavior. + let md_array = root_group.open_md_array(&array_name, options).unwrap(); + let dimensions = md_array.dimensions().unwrap(); + let mut dimension_names = Vec::new(); + for dimension in dimensions { + dimension_names.push(dimension.name()); + } + assert_eq!(dimension_names, vec!["y".to_string(), "x".to_string()]) + } + #[test] + fn test_dimension_size() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + let root_group = dataset.root_group().unwrap(); + let array_name = "Band1".to_string(); + let options = CslStringList::new(); //Driver specific options determining how the array should be opened. Pass nullptr for default behavior. + let md_array = root_group.open_md_array(&array_name, options).unwrap(); + let dimensions = md_array.dimensions().unwrap(); + let mut dimensions_size = Vec::new(); + for dimension in dimensions { + dimensions_size.push(dimension.size()); + } + assert_eq!(dimensions_size, vec![20, 20]) + } + + #[test] + fn test_read_data() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + let md_array = root_group + .open_md_array("Band1", CslStringList::new()) + .unwrap(); + + let values = md_array.read_as::(vec![0, 0], vec![20, 20]).unwrap(); + + assert_eq!(&values[..4], &[181, 181, 156, 148]); + } + + #[test] + fn test_read_string_array() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/alldatatypes.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + + let string_array = root_group + .open_md_array("string_var", CslStringList::new()) + .unwrap(); + + assert_eq!(string_array.read_as_string_array().unwrap(), ["abcd", "ef"]); + } + + #[test] + fn test_datatype() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + let md_array = root_group + .open_md_array("Band1", CslStringList::new()) + .unwrap(); + + let datatype = md_array.datatype(); + + assert_eq!(datatype.class(), GDALExtendedDataTypeClass::GEDTC_NUMERIC); + assert_eq!(datatype.numeric_datatype(), GDALDataType::GDT_Byte); + } + + #[test] + fn test_spatial_ref() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + let md_array = root_group + .open_md_array("Band1", CslStringList::new()) + .unwrap(); + + let spatial_ref = md_array.spatial_reference().unwrap(); + + assert_eq!(spatial_ref.name().unwrap(), "NAD27 / UTM zone 11N"); + + assert_eq!(spatial_ref.authority().unwrap(), "EPSG:26711"); + } + + #[test] + fn test_no_data_value() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/byte_no_cf.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + let md_array = root_group + .open_md_array("Band1", CslStringList::new()) + .unwrap(); + + assert_eq!(md_array.no_data_value_as_double(), Some(0.)); + } + + #[test] + fn test_attributes() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/cf_nasa_4326.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + + assert_eq!( + root_group.attribute("title").unwrap().read_as_string(), + "Simple CF file" + ); + + let group_science = root_group + .open_group("science", CslStringList::new()) + .unwrap(); + let group_grids = group_science + .open_group("grids", CslStringList::new()) + .unwrap(); + let group_data = group_grids + .open_group("data", CslStringList::new()) + .unwrap(); + + let md_array = group_data + .open_md_array("temp", CslStringList::new()) + .unwrap(); + + assert_eq!( + md_array + .attribute("standard_name") + .unwrap() + .read_as_string(), + "air_temperature" + ); + + assert_eq!( + md_array.attribute("_FillValue").unwrap().read_as_f64(), + -9999. + ); + } + + #[test] + fn test_unit() { + let dataset_options = DatasetOptions { + open_flags: GdalOpenFlags::GDAL_OF_MULTIDIM_RASTER, + allowed_drivers: None, + open_options: None, + sibling_files: None, + }; + let dataset = Dataset::open_ex("fixtures/cf_nasa_4326.nc", dataset_options).unwrap(); + + let root_group = dataset.root_group().unwrap(); + + assert_eq!( + root_group.attribute("title").unwrap().read_as_string(), + "Simple CF file" + ); + + let group_science = root_group + .open_group("science", CslStringList::new()) + .unwrap(); + let group_grids = group_science + .open_group("grids", CslStringList::new()) + .unwrap(); + let group_data = group_grids + .open_group("data", CslStringList::new()) + .unwrap(); + + let md_array = group_data + .open_md_array("temp", CslStringList::new()) + .unwrap(); + + assert_eq!(md_array.unit(), "K"); + } +} diff --git a/src/raster/mod.rs b/src/raster/mod.rs index 2a524508..fb71dd04 100644 --- a/src/raster/mod.rs +++ b/src/raster/mod.rs @@ -1,10 +1,14 @@ //! GDAL Raster Data +#[cfg(all(major_ge_3, minor_ge_1))] +mod mdarray; mod rasterband; mod rasterize; mod types; mod warp; +#[cfg(all(major_ge_3, minor_ge_1))] +pub use mdarray::{Group, MDArray}; pub use rasterband::{ Buffer, ByteBuffer, CmykEntry, ColorEntry, ColorInterpretation, ColorTable, GrayEntry, HlsEntry, PaletteInterpretation, RasterBand, ResampleAlg, RgbaEntry, diff --git a/src/raster/tests.rs b/src/raster/tests.rs index 65da3653..b40fadb9 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -121,7 +121,7 @@ fn test_read_raster_with_average_resample() { #[test] fn test_write_raster() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 20, 10, 1).unwrap(); // create a 2x1 raster @@ -153,7 +153,7 @@ fn test_rename_remove_raster() { let mem_file_path_a = Path::new("/vsimem/030bd1d1-8955-4604-8e37-177dade13863"); let mem_file_path_b = Path::new("/vsimem/c7bfce32-2474-48fa-a907-2af95f83c824"); - let driver = Driver::get("GTiff").unwrap(); + let driver = Driver::get_by_name("GTiff").unwrap(); dataset.create_copy(&driver, &mem_file_path_a, &[]).unwrap(); @@ -179,7 +179,7 @@ fn test_get_dataset_driver() { #[test] fn test_get_description() { - let driver = Driver::get("mem").unwrap(); + let driver = Driver::get_by_name("mem").unwrap(); assert_eq!(driver.description().unwrap(), "MEM".to_string()); } @@ -230,7 +230,7 @@ fn test_get_metadata_item() { #[test] fn test_set_metadata_item() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let mut dataset = driver.create("", 1, 1, 1).unwrap(); let key = "Test_Key"; @@ -245,7 +245,7 @@ fn test_set_metadata_item() { #[test] fn test_set_description() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 1, 1, 1).unwrap(); let mut band = dataset.rasterband(1).unwrap(); @@ -258,7 +258,7 @@ fn test_set_description() { #[test] fn test_create() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 10, 20, 3).unwrap(); assert_eq!(dataset.raster_size(), (10, 20)); assert_eq!(dataset.raster_count(), 3); @@ -267,7 +267,7 @@ fn test_create() { #[test] fn test_create_with_band_type() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver .create_with_band_type::("", 10, 20, 3) .unwrap(); @@ -280,7 +280,7 @@ fn test_create_with_band_type() { #[test] fn test_create_with_band_type_with_options() { - let driver = Driver::get("GTiff").unwrap(); + let driver = Driver::get_by_name("GTiff").unwrap(); let options = [ RasterCreationOption { key: "TILED", @@ -327,7 +327,7 @@ fn test_create_with_band_type_with_options() { #[test] fn test_create_copy() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = Dataset::open(fixture!("tinymarble.png")).unwrap(); let copy = dataset.create_copy(&driver, "", &[]).unwrap(); assert_eq!(copy.raster_size(), (100, 50)); @@ -347,7 +347,7 @@ fn test_create_copy_with_options() { let copy = dataset .create_copy( - &Driver::get("GTiff").unwrap(), + &Driver::get_by_name("GTiff").unwrap(), mem_file_path, &[ RasterCreationOption { @@ -376,7 +376,7 @@ fn test_create_copy_with_options() { #[test] #[allow(clippy::float_cmp)] fn test_geo_transform() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let mut dataset = driver.create("", 20, 10, 1).unwrap(); let transform = [0., 1., 0., 0., 0., 1.]; assert!(dataset.set_geo_transform(&transform).is_ok()); @@ -385,10 +385,10 @@ fn test_geo_transform() { #[test] fn test_get_driver_by_name() { - let missing_driver = Driver::get("wtf"); + let missing_driver = Driver::get_by_name("wtf"); assert!(missing_driver.is_err()); - let ok_driver = Driver::get("GTiff"); + let ok_driver = Driver::get_by_name("GTiff"); assert!(ok_driver.is_ok()); let driver = ok_driver.unwrap(); assert_eq!(driver.short_name(), "GTiff"); @@ -486,7 +486,7 @@ fn test_read_block_data() { #[test] fn test_get_band_type() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 20, 10, 1).unwrap(); let rb = dataset.rasterband(1).unwrap(); assert_eq!(rb.band_type(), GDALDataType::GDT_Byte); @@ -494,7 +494,7 @@ fn test_get_band_type() { #[test] fn test_get_rasterband() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 20, 10, 1).unwrap(); let rasterband = dataset.rasterband(1); assert!(rasterband.is_ok()); @@ -518,7 +518,7 @@ fn test_get_no_data_value() { #[test] #[allow(clippy::float_cmp)] fn test_set_no_data_value() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 20, 10, 1).unwrap(); let mut rasterband = dataset.rasterband(1).unwrap(); assert_eq!(rasterband.no_data_value(), None); @@ -648,7 +648,7 @@ fn test_get_rasterband_color_interp() { #[test] fn test_set_rasterband_color_interp() { - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let dataset = driver.create("", 1, 1, 1).unwrap(); let mut rasterband = dataset.rasterband(1).unwrap(); rasterband @@ -678,7 +678,7 @@ fn test_rasterize() { let rows = 5; let cols = 5; - let driver = Driver::get("MEM").unwrap(); + let driver = Driver::get_by_name("MEM").unwrap(); let mut dataset = driver.create("", rows, cols, 1).unwrap(); let bands = [1]; diff --git a/src/spatial_ref/srs.rs b/src/spatial_ref/srs.rs index b4368dd3..c21f92c9 100644 --- a/src/spatial_ref/srs.rs +++ b/src/spatial_ref/srs.rs @@ -33,6 +33,58 @@ impl CoordTransform { }) } + /// Transform bounding box, densifying the edges to account for nonlinear + /// transformations. + /// + /// # Arguments + /// * bounds - array of [axis0_min, axis1_min, axis0_max, axis1_min], + /// interpreted in the axis order of the source SpatialRef, + /// typically [xmin, ymin, xmax, ymax] + /// * densify_pts - number of points per edge (recommended: 21) + /// + /// # Returns + /// Some([f64; 4]) with bounds in axis order of target SpatialRef + /// None if there is an error. + #[cfg(all(major_ge_3, minor_ge_4))] + pub fn transform_bounds(&self, bounds: &[f64; 4], densify_pts: i32) -> Result<([f64; 4])> { + let mut out_xmin: f64 = 0.; + let mut out_ymin: f64 = 0.; + let mut out_xmax: f64 = 0.; + let mut out_ymax: f64 = 0.; + + let ret_val = unsafe { + gdal_sys::OCTTransformBounds( + self.inner, + bounds[0], + bounds[1], + bounds[2], + bounds[3], + &mut out_xmin, + &mut out_ymin, + &mut out_xmax, + &mut out_ymax, + densify_pts as c_int, + ) == 1 + }; + + if !ret_val { + let msg = match _last_cpl_err(CPLErr::CE_Failure) { + GdalError::CplError { msg, .. } => match msg.is_empty() { + false => Some(msg), + _ => None, + }, + err => return Err(err), + }; + return Err(GdalError::InvalidCoordinateRange { + from: self.from.clone(), + to: self.to.clone(), + msg, + }); + } + + Ok([out_xmin, out_ymin, out_xmax, out_ymax]) + } + /// Transform coordinates in place. /// /// # Arguments diff --git a/src/spatial_ref/tests.rs b/src/spatial_ref/tests.rs index 03e72373..495e0cdc 100644 --- a/src/spatial_ref/tests.rs +++ b/src/spatial_ref/tests.rs @@ -64,6 +64,68 @@ fn comparison() { assert!(spatial_ref5 == spatial_ref4); } +#[cfg(all(major_ge_3, minor_ge_4))] +#[test] +fn transform_bounds() { + let bounds: [f64; 4] = [-180., -80., 180., 80.]; + // bounds for y,x ordered SpatialRefs + let yx_bounds: [f64; 4] = [-80.0, -180.0, 80.0, 180.]; + + let spatial_ref1 = SpatialRef::from_definition("OGC:CRS84").unwrap(); + + // transforming between the same SpatialRef should return existing bounds + let mut transform = CoordTransform::new(&spatial_ref1, &spatial_ref1).unwrap(); + let mut out_bounds = transform.transform_bounds(&bounds, 21).unwrap(); + assert_almost_eq(out_bounds[0], bounds[0]); + assert_almost_eq(out_bounds[1], bounds[1]); + assert_almost_eq(out_bounds[2], bounds[2]); + assert_almost_eq(out_bounds[3], bounds[3]); + + // EPSG:4326 is in y,x order by default; returned bounds are [ymin, xmin, ymax, xmax] + let mut spatial_ref2 = SpatialRef::from_epsg(4326).unwrap(); + transform = CoordTransform::new(&spatial_ref1, &spatial_ref2).unwrap(); + out_bounds = transform.transform_bounds(&bounds, 21).unwrap(); + assert_almost_eq(out_bounds[0], yx_bounds[0]); + assert_almost_eq(out_bounds[1], yx_bounds[1]); + assert_almost_eq(out_bounds[2], yx_bounds[2]); + assert_almost_eq(out_bounds[3], yx_bounds[3]); + + // if source SpatialRef is in y,x order and and target SpatialRef is in x,y order + // input bounds are interpreted as [ymin, xmin, ymax, xmax] and returns + // [xmin, ymin, xmax, ymax] + transform = CoordTransform::new(&spatial_ref2, &spatial_ref1).unwrap(); + out_bounds = transform.transform_bounds(&yx_bounds, 21).unwrap(); + assert_almost_eq(out_bounds[0], bounds[0]); + assert_almost_eq(out_bounds[1], bounds[1]); + assert_almost_eq(out_bounds[2], bounds[2]); + assert_almost_eq(out_bounds[3], bounds[3]); + + // force EPSG:4326 into x,y order to match source SpatialRef + spatial_ref2 + .set_axis_mapping_strategy(gdal_sys::OSRAxisMappingStrategy::OAMS_TRADITIONAL_GIS_ORDER); + transform = CoordTransform::new(&spatial_ref1, &spatial_ref2).unwrap(); + out_bounds = transform.transform_bounds(&bounds, 21).unwrap(); + assert_almost_eq(out_bounds[0], bounds[0]); + assert_almost_eq(out_bounds[1], bounds[1]); + assert_almost_eq(out_bounds[2], bounds[2]); + assert_almost_eq(out_bounds[3], bounds[3]); + + spatial_ref2 = SpatialRef::from_epsg(3857).unwrap(); + transform = CoordTransform::new(&spatial_ref1, &spatial_ref2).unwrap(); + out_bounds = transform.transform_bounds(&bounds, 21).unwrap(); + + let expected_bounds: [f64; 4] = [ + -20037508.342789244, + -15538711.096309224, + 20037508.342789244, + 15538711.09630923, + ]; + assert_almost_eq(out_bounds[0], expected_bounds[0]); + assert_almost_eq(out_bounds[1], expected_bounds[1]); + assert_almost_eq(out_bounds[2], expected_bounds[2]); + assert_almost_eq(out_bounds[3], expected_bounds[3]); +} + #[test] fn transform_coordinates() { let spatial_ref1 = SpatialRef::from_wkt("GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",7030]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",6326]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",8901]],UNIT[\"DMSH\",0.0174532925199433,AUTHORITY[\"EPSG\",9108]],AXIS[\"Lat\",NORTH],AXIS[\"Long\",EAST],AUTHORITY[\"EPSG\",4326]]").unwrap(); diff --git a/src/vector/vector_tests/mod.rs b/src/vector/vector_tests/mod.rs index e9c54b6b..6e2f3781 100644 --- a/src/vector/vector_tests/mod.rs +++ b/src/vector/vector_tests/mod.rs @@ -710,7 +710,7 @@ mod tests { use std::fs; { - let driver = Driver::get("GeoJSON").unwrap(); + let driver = Driver::get_by_name("GeoJSON").unwrap(); let mut ds = driver .create_vector_only(&fixture!("output.geojson")) .unwrap();