diff --git a/.gitignore b/.gitignore index a2f257b75..446b55dd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /gdal-sys/target /.idea /3rd +/fixtures/tinymarble.tif.aux.xml # gtags GPATH diff --git a/CHANGES.md b/CHANGES.md index 6056f9dc7..463be0452 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,12 @@ ## Unreleased + - Refactored `dataset.rs` to focus on core `Dataset` operations, moving ancillary types and operations to other files. + - **Breaking**: Moved `LayerIterator`, `LayerOptions` and `Transaction` to `crate::vector`. + - Accessors `MajorObject::gdal_object_ptr` and `Dataset::c_dataset()` are no longer marked as `unsafe` (it's not idiomatic Rust to do so). + + - + - Fixed build script error with development GDAL versions diff --git a/src/dataset.rs b/src/dataset.rs index 1a1139864..a3fa9c88a 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -1,166 +1,15 @@ -use ptr::null_mut; -use std::convert::TryInto; -use std::mem::MaybeUninit; -use std::{ - ffi::NulError, - ffi::{CStr, CString}, - ops::{Deref, DerefMut}, - path::Path, - ptr, -}; +use std::{ffi::CString, ffi::NulError, path::Path, ptr}; + +use gdal_sys::{self, CPLErr, GDALDatasetH, GDALMajorObjectH}; use crate::cpl::CslStringList; use crate::errors::*; +use crate::options::DatasetOptions; use crate::raster::RasterCreationOption; use crate::utils::{_last_cpl_err, _last_null_pointer_err, _path_to_c_string, _string}; -use crate::vector::{sql, Geometry, OwnedLayer}; use crate::{ - gdal_major_object::MajorObject, raster::RasterBand, spatial_ref::SpatialRef, vector::Layer, - Driver, Metadata, -}; - -use gdal_sys::{ - self, CPLErr, GDALAccess, GDALDatasetH, GDALMajorObjectH, OGRErr, OGRGeometryH, OGRLayerH, - OGRwkbGeometryType, + gdal_major_object::MajorObject, spatial_ref::SpatialRef, Driver, GeoTransform, Metadata, }; -use libc::{c_double, c_int, c_uint}; - -#[cfg(all(major_ge_3, minor_ge_1))] -use crate::raster::Group; - -use bitflags::bitflags; - -/// A six-element array storing the coefficients of an [affine transform] -/// used in mapping coordinates between pixel/line `(P, L)` (raster) space, -/// and `(Xp,Yp)` (projection/[`SpatialRef`]) space. -/// -/// # Interpretation -/// -/// A `GeoTransform`'s components have the following meanings: -/// -/// * `GeoTransform[0]`: x-coordinate of the upper-left corner of the upper-left pixel. -/// * `GeoTransform[1]`: W-E pixel resolution (pixel width). -/// * `GeoTransform[2]`: row rotation (typically zero). -/// * `GeoTransform[3]`: y-coordinate of the upper-left corner of the upper-left pixel. -/// * `GeoTransform[4]`: column rotation (typically zero). -/// * `GeoTransform[5]`: N-S pixel resolution (pixel height), negative value for a North-up image. -/// -/// -/// ## Note -/// -/// Care with coefficient ordering is required when constructing an [affine transform matrix] from -/// a `GeoTransform`. If a 3x3 transform matrix is defined as: -/// -/// ```text -/// | a b c | -/// | d e f | -/// | 0 0 1 | -/// ``` -/// -/// The corresponding `GeoTransform` ordering is: -/// -/// ```text -/// [c, a, b, f, d, e] -/// ``` -/// -/// # Usage -/// * [`apply`](GeoTransformEx::apply): perform a `(P,L) -> (Xp,Yp)` transformation -/// * [`invert`](GeoTransformEx::invert): construct the inverse transformation coefficients -/// for computing `(Xp,Yp) -> (P,L)` transformations -/// -/// # Example -/// -/// ```rust, no_run -/// # fn main() -> gdal::errors::Result<()> { -/// use gdal::{Dataset, GeoTransformEx}; -/// let ds = Dataset::open("fixtures/m_3607824_se_17_1_20160620_sub.tif")?; -/// let transform = ds.geo_transform()?; -/// let (p, l) = (0.0, 0.0); -/// let (x,y) = transform.apply(p, l); -/// println!("(x,y): ({x},{y})"); -/// let inverse = transform.invert()?; -/// let (p, l) = inverse.apply(x, y); -/// println!("(p,l): ({p},{l})"); -/// # Ok(()) -/// # } -/// ``` -/// Output: -/// -/// ```text -/// (x,y): (768269,4057292) -/// (p,l): (0,0) -/// ``` -/// # See Also -/// -/// * [GDAL GeoTransform Tutorial] -/// * [GDALGetGeoTransform] -/// * [Raster Data Model Affine Transform] -/// -/// [GDAL GeoTransform Tutorial]: https://gdal.org/tutorials/geotransforms_tut.html -/// [GDALGetGeoTransform]: https://gdal.org/api/gdaldataset_cpp.html#classGDALDataset_1a5101119705f5fa2bc1344ab26f66fd1d -/// [Raster Data Model Affine Transform]: https://gdal.org/user/raster_data_model.html#affine-geotransform -/// [affine transform]: https://en.wikipedia.org/wiki/Affine_transformation -/// [affine transform matrix]: https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations -pub type GeoTransform = [c_double; 6]; - -/// Extension methods on [`GeoTransform`] -pub trait GeoTransformEx { - /// Apply GeoTransform to x/y coordinate. - /// - /// Wraps [GDALApplyGeoTransform]. - /// - /// # Example - /// - /// See [`GeoTransform`](GeoTransform#example) - /// - /// [GDALApplyGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv421GDALApplyGeoTransformPdddPdPd - fn apply(&self, pixel: f64, line: f64) -> (f64, f64); - - /// Invert a [`GeoTransform`]. - /// - /// Wraps [GDALInvGeoTransform]. - /// - /// # Example - /// - /// See [`GeoTransform`](GeoTransform#example) - /// - /// [GDALInvGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv419GDALInvGeoTransformPdPd - fn invert(&self) -> Result; -} - -impl GeoTransformEx for GeoTransform { - fn apply(&self, pixel: f64, line: f64) -> (f64, f64) { - let mut geo_x = MaybeUninit::::uninit(); - let mut geo_y = MaybeUninit::::uninit(); - unsafe { - gdal_sys::GDALApplyGeoTransform( - self.as_ptr() as *mut f64, - pixel, - line, - geo_x.as_mut_ptr(), - geo_y.as_mut_ptr(), - ); - (geo_x.assume_init(), geo_y.assume_init()) - } - } - - fn invert(&self) -> Result { - let mut gt_out = MaybeUninit::::uninit(); - let rv = unsafe { - gdal_sys::GDALInvGeoTransform( - self.as_ptr() as *mut f64, - (*gt_out.as_mut_ptr()).as_mut_ptr(), - ) - }; - if rv == 0 { - return Err(GdalError::BadArgument( - "Geo transform is uninvertible".to_string(), - )); - } - let result = unsafe { gt_out.assume_init() }; - Ok(result) - } -} /// Wrapper around a [`GDALDataset`][GDALDataset] object. /// @@ -177,112 +26,6 @@ pub struct Dataset { closed: bool, } -// These are skipped by bindgen and manually updated. -#[cfg(major_ge_2)] -bitflags! { - /// GDal extended open flags used by [`Dataset::open_ex`]. - /// - /// Used in the `nOpenFlags` argument to [`GDALOpenEx`]. - /// - /// Note that the `GDAL_OF_SHARED` option is removed - /// from the set of allowed option because it subverts - /// the [`Send`] implementation that allow passing the - /// dataset the another thread. See - /// https://github.com/georust/gdal/issues/154. - /// - /// [`GDALOpenEx`]: https://gdal.org/doxygen/gdal_8h.html#a9cb8585d0b3c16726b08e25bcc94274a - #[derive(Debug)] - #[allow(clippy::assign_op_pattern)] - pub struct GdalOpenFlags: c_uint { - /// Open in read-only mode (default). - const GDAL_OF_READONLY = 0x00; - /// Open in update mode. - const GDAL_OF_UPDATE = 0x01; - /// Allow raster and vector drivers to be used. - const GDAL_OF_ALL = 0x00; - /// Allow raster drivers to be used. - const GDAL_OF_RASTER = 0x02; - /// Allow vector drivers to be used. - const GDAL_OF_VECTOR = 0x04; - /// Allow gnm drivers to be used. - #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] - const GDAL_OF_GNM = 0x08; - /// Allow multidimensional raster drivers to be used. - #[cfg(all(major_ge_3,minor_ge_1))] - const GDAL_OF_MULTIDIM_RASTER = 0x10; - /// Emit error message in case of failed open. - const GDAL_OF_VERBOSE_ERROR = 0x40; - /// Open as internal dataset. Such dataset isn't - /// registered in the global list of opened dataset. - /// Cannot be used with GDAL_OF_SHARED. - const GDAL_OF_INTERNAL = 0x80; - - /// Default strategy for cached blocks. - #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] - const GDAL_OF_DEFAULT_BLOCK_ACCESS = 0; - - /// Array based strategy for cached blocks. - #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] - const GDAL_OF_ARRAY_BLOCK_ACCESS = 0x100; - - /// Hashset based strategy for cached blocks. - #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] - const GDAL_OF_HASHSET_BLOCK_ACCESS = 0x200; - } -} - -impl Default for GdalOpenFlags { - fn default() -> GdalOpenFlags { - GdalOpenFlags::GDAL_OF_READONLY - } -} - -impl From for GdalOpenFlags { - fn from(val: GDALAccess::Type) -> GdalOpenFlags { - if val == GDALAccess::GA_Update { - GdalOpenFlags::GDAL_OF_UPDATE - } else { - GdalOpenFlags::GDAL_OF_READONLY - } - } -} - -// Open parameters -#[derive(Debug, Default)] -pub struct DatasetOptions<'a> { - pub open_flags: GdalOpenFlags, - pub allowed_drivers: Option<&'a [&'a str]>, - pub open_options: Option<&'a [&'a str]>, - pub sibling_files: Option<&'a [&'a str]>, -} - -/// Parameters for [`Dataset::create_layer`]. -#[derive(Clone, Debug)] -pub struct LayerOptions<'a> { - /// The name of the newly created layer. May be an empty string. - pub name: &'a str, - /// The SRS of the newly created layer, or `None` for no SRS. - pub srs: Option<&'a SpatialRef>, - /// The type of geometry for the new layer. - pub ty: OGRwkbGeometryType::Type, - /// Additional driver-specific options to pass to GDAL, in the form `name=value`. - pub options: Option<&'a [&'a str]>, -} - -const EMPTY_LAYER_NAME: &str = ""; - -impl<'a> Default for LayerOptions<'a> { - /// Returns creation options for a new layer with no name, no SRS and unknown geometry type. - fn default() -> Self { - LayerOptions { - name: EMPTY_LAYER_NAME, - srs: None, - ty: OGRwkbGeometryType::wkbUnknown, - options: None, - } - } -} - // GDAL Docs state: The returned dataset should only be accessed by one thread at a time. // See: https://gdal.org/api/raster_c_api.html#_CPPv48GDALOpenPKc10GDALAccess // Additionally, VRT Datasets are not safe before GDAL 2.3. @@ -290,15 +33,28 @@ impl<'a> Default for LayerOptions<'a> { #[cfg(any(all(major_is_2, minor_ge_3), major_ge_3))] unsafe impl Send for Dataset {} +/// Core dataset methods impl Dataset { /// Returns the wrapped C pointer /// /// # Safety /// This method returns a raw C pointer - pub unsafe fn c_dataset(&self) -> GDALDatasetH { + pub fn c_dataset(&self) -> GDALDatasetH { self.c_dataset } + /// Creates a new Dataset by wrapping a C pointer + /// + /// # Safety + /// This method operates on a raw C pointer + /// The dataset must not have been closed (using [`GDALClose`]) before. + pub unsafe fn from_c_dataset(c_dataset: GDALDatasetH) -> Dataset { + Dataset { + c_dataset, + closed: false, + } + } + /// Open a dataset at the given `path` with default /// options. pub fn open>(path: P) -> Result { @@ -452,18 +208,6 @@ impl Dataset { Ok(()) } - /// Creates a new Dataset by wrapping a C pointer - /// - /// # Safety - /// This method operates on a raw C pointer - /// The dataset must not have been closed (using [`GDALClose`]) before. - pub unsafe fn from_c_dataset(c_dataset: GDALDatasetH) -> Dataset { - Dataset { - c_dataset, - closed: false, - } - } - /// Fetch the projection definition string for this dataset. pub fn projection(&self) -> String { let rv = unsafe { gdal_sys::GDALGetProjectionRef(self.c_dataset) }; @@ -499,37 +243,36 @@ impl Dataset { filename: P, options: &[RasterCreationOption], ) -> Result { - Self::_create_copy(self, driver, filename.as_ref(), options) - } - - fn _create_copy( - &self, - driver: &Driver, - filename: &Path, - options: &[RasterCreationOption], - ) -> Result { - let c_filename = _path_to_c_string(filename)?; - - let mut c_options = CslStringList::new(); - for option in options { - c_options.set_name_value(option.key, option.value)?; - } + fn _create_copy( + ds: &Dataset, + driver: &Driver, + filename: &Path, + options: &[RasterCreationOption], + ) -> Result { + let c_filename = _path_to_c_string(filename)?; + + let mut c_options = CslStringList::new(); + for option in options { + c_options.set_name_value(option.key, option.value)?; + } - let c_dataset = unsafe { - gdal_sys::GDALCreateCopy( - driver.c_driver(), - c_filename.as_ptr(), - self.c_dataset, - 0, - c_options.as_ptr(), - None, - ptr::null_mut(), - ) - }; - if c_dataset.is_null() { - return Err(_last_null_pointer_err("GDALCreateCopy")); + let c_dataset = unsafe { + gdal_sys::GDALCreateCopy( + driver.c_driver(), + c_filename.as_ptr(), + ds.c_dataset, + 0, + c_options.as_ptr(), + None, + ptr::null_mut(), + ) + }; + if c_dataset.is_null() { + return Err(_last_null_pointer_err("GDALCreateCopy")); + } + Ok(unsafe { Dataset::from_c_dataset(c_dataset) }) } - Ok(unsafe { Dataset::from_c_dataset(c_dataset) }) + _create_copy(self, driver, filename.as_ref(), options) } /// Fetch the driver to which this dataset relates. @@ -540,217 +283,6 @@ impl Dataset { } } - /// Fetch a band object for a dataset. - /// - /// Applies to raster datasets, and fetches the - /// rasterband at the given _1-based_ index. - pub fn rasterband(&self, band_index: isize) -> Result { - unsafe { - let c_band = gdal_sys::GDALGetRasterBand(self.c_dataset, band_index as c_int); - if c_band.is_null() { - return Err(_last_null_pointer_err("GDALGetRasterBand")); - } - Ok(RasterBand::from_c_rasterband(self, c_band)) - } - } - - /// 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 - /// * `resampling` - resampling method, as accepted by GDAL, e.g. `"CUBIC"` - /// * `overviews` - list of overview decimation factors, e.g. `&[2, 4, 8, 16, 32]` - /// * `bands` - list of bands to build the overviews for, or empty for all bands - /// - /// [`GDALBuildOverviews`]: https://gdal.org/api/raster_c_api.html#_CPPv418GDALBuildOverviews12GDALDatasetHPKciPKiiPKi16GDALProgressFuncPv - pub fn build_overviews( - &mut self, - resampling: &str, - overviews: &[i32], - bands: &[i32], - ) -> Result<()> { - let c_resampling = CString::new(resampling)?; - let rv = unsafe { - gdal_sys::GDALBuildOverviews( - self.c_dataset, - c_resampling.as_ptr(), - overviews.len() as i32, - overviews.as_ptr() as *mut i32, - bands.len() as i32, - bands.as_ptr() as *mut i32, - None, - null_mut(), - ) - }; - if rv != CPLErr::CE_None { - return Err(_last_cpl_err(rv)); - } - Ok(()) - } - - fn child_layer(&self, c_layer: OGRLayerH) -> Layer { - unsafe { Layer::from_c_layer(self, c_layer) } - } - - fn into_child_layer(self, c_layer: OGRLayerH) -> OwnedLayer { - unsafe { OwnedLayer::from_c_layer(self, c_layer) } - } - - /// Get the number of layers in this dataset. - pub fn layer_count(&self) -> isize { - (unsafe { gdal_sys::OGR_DS_GetLayerCount(self.c_dataset) }) as isize - } - - /// Fetch a layer by index. - /// - /// Applies to vector datasets, and fetches by the given - /// _0-based_ index. - pub fn layer(&self, idx: isize) -> Result { - let c_layer = unsafe { gdal_sys::OGR_DS_GetLayer(self.c_dataset, idx as c_int) }; - if c_layer.is_null() { - return Err(_last_null_pointer_err("OGR_DS_GetLayer")); - } - Ok(self.child_layer(c_layer)) - } - - /// Fetch a layer by index. - /// - /// Applies to vector datasets, and fetches by the given - /// _0-based_ index. - pub fn into_layer(self, idx: isize) -> Result { - let c_layer = unsafe { gdal_sys::OGR_DS_GetLayer(self.c_dataset, idx as c_int) }; - if c_layer.is_null() { - return Err(_last_null_pointer_err("OGR_DS_GetLayer")); - } - Ok(self.into_child_layer(c_layer)) - } - - /// Fetch a layer by name. - pub fn layer_by_name(&self, name: &str) -> Result { - let c_name = CString::new(name)?; - let c_layer = unsafe { gdal_sys::OGR_DS_GetLayerByName(self.c_dataset(), c_name.as_ptr()) }; - if c_layer.is_null() { - return Err(_last_null_pointer_err("OGR_DS_GetLayerByName")); - } - Ok(self.child_layer(c_layer)) - } - - /// Fetch a layer by name. - pub fn into_layer_by_name(self, name: &str) -> Result { - let c_name = CString::new(name)?; - let c_layer = unsafe { gdal_sys::OGR_DS_GetLayerByName(self.c_dataset(), c_name.as_ptr()) }; - if c_layer.is_null() { - return Err(_last_null_pointer_err("OGR_DS_GetLayerByName")); - } - Ok(self.into_child_layer(c_layer)) - } - - /// Returns an iterator over the layers of the dataset. - pub fn layers(&self) -> LayerIterator { - LayerIterator::with_dataset(self) - } - - /// Fetch the number of raster bands on this dataset. - pub fn raster_count(&self) -> isize { - (unsafe { gdal_sys::GDALGetRasterCount(self.c_dataset) }) as isize - } - - /// Returns the raster dimensions: (width, height). - pub fn raster_size(&self) -> (usize, usize) { - let size_x = unsafe { gdal_sys::GDALGetRasterXSize(self.c_dataset) } as usize; - let size_y = unsafe { gdal_sys::GDALGetRasterYSize(self.c_dataset) } as usize; - (size_x, size_y) - } - - /// Creates a new layer. The [`LayerOptions`] struct implements `Default`, so you only need to - /// specify those options that deviate from the default. - /// - /// # Examples - /// - /// Create a new layer with an empty name, no spatial reference, and unknown geometry type: - /// - /// ``` - /// # use gdal::DriverManager; - /// # let driver = DriverManager::get_driver_by_name("GPKG").unwrap(); - /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); - /// let blank_layer = dataset.create_layer(Default::default()).unwrap(); - /// ``` - /// - /// Create a new named line string layer using WGS84: - /// - /// ``` - /// # use gdal::{DriverManager, LayerOptions}; - /// # use gdal::spatial_ref::SpatialRef; - /// # let driver = DriverManager::get_driver_by_name("GPKG").unwrap(); - /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); - /// let roads = dataset.create_layer(LayerOptions { - /// name: "roads", - /// srs: Some(&SpatialRef::from_epsg(4326).unwrap()), - /// ty: gdal_sys::OGRwkbGeometryType::wkbLineString, - /// ..Default::default() - /// }).unwrap(); - /// ``` - pub fn create_layer(&mut self, options: LayerOptions<'_>) -> Result { - let c_name = CString::new(options.name)?; - let c_srs = match options.srs { - Some(srs) => srs.to_c_hsrs(), - None => null_mut(), - }; - - // Handle string options: we need to keep the CStrings and the pointers around. - let c_options = options.options.map(|d| { - d.iter() - .map(|&s| CString::new(s)) - .collect::, NulError>>() - }); - let c_options_vec = match c_options { - Some(Err(e)) => return Err(e.into()), - Some(Ok(c_options_vec)) => c_options_vec, - None => Vec::from([]), - }; - let mut c_options_ptrs = c_options_vec.iter().map(|s| s.as_ptr()).collect::>(); - c_options_ptrs.push(ptr::null()); - - let c_options_ptr = if options.options.is_some() { - c_options_ptrs.as_ptr() - } else { - ptr::null() - }; - - let c_layer = unsafe { - // The C function takes `char **papszOptions` without mention of `const`, and this is - // propagated to the gdal_sys wrapper. The lack of `const` seems like a mistake in the - // GDAL API, so we just do a cast here. - gdal_sys::OGR_DS_CreateLayer( - self.c_dataset, - c_name.as_ptr(), - c_srs, - options.ty, - c_options_ptr as *mut *mut libc::c_char, - ) - }; - if c_layer.is_null() { - return Err(_last_null_pointer_err("OGR_DS_CreateLayer")); - }; - Ok(self.child_layer(c_layer)) - } - /// Set the [`Dataset`]'s affine transformation; also called a _geo-transformation_. /// /// This is like a linear transformation preserves points, straight lines and planes. @@ -795,232 +327,10 @@ impl Dataset { } Ok(transformation) } - - /// For datasources which support transactions, this creates a transaction. - /// - /// Because the transaction implements `DerefMut`, it can be used in place of the original - /// `Dataset` to make modifications. All changes done after the start of the transaction are - /// applied to the datasource when [`commit`](Transaction::commit) is called. They may be - /// canceled by calling [`rollback`](Transaction::rollback) instead, or by dropping the - /// `Transaction` without calling `commit`. - /// - /// Depending on the driver, using a transaction can give a huge performance improvement when - /// creating a lot of geometry at once. This is because the driver doesn't need to commit every - /// feature to disk individually. - /// - /// If starting the transaction fails, this function will return [`OGRErr::OGRERR_FAILURE`]. - /// For datasources that do not support transactions, this function will always return - /// [`OGRErr::OGRERR_UNSUPPORTED_OPERATION`]. - /// - /// Limitations: - /// - /// * Datasources which do not support efficient transactions natively may use less efficient - /// emulation of transactions instead; as of GDAL 3.1, this only applies to the closed-source - /// FileGDB driver, which (unlike OpenFileGDB) is not available in a GDAL build by default. - /// - /// * At the time of writing, transactions only apply on vector layers. - /// - /// * Nested transactions are not supported. - /// - /// * If an error occurs after a successful `start_transaction`, the whole transaction may or - /// may not be implicitly canceled, depending on the driver. For example, the PG driver will - /// cancel it, but the SQLite and GPKG drivers will not. - /// - /// Example: - /// - /// ``` - /// # use gdal::{Dataset, LayerOptions}; - /// # use gdal::vector::LayerAccess; - /// # - /// fn create_point_grid(dataset: &mut Dataset) -> gdal::errors::Result<()> { - /// use gdal::vector::Geometry; - /// - /// // Start the transaction. - /// let mut txn = dataset.start_transaction()?; - /// - /// let mut layer = txn.create_layer(LayerOptions { - /// name: "grid", - /// ty: gdal_sys::OGRwkbGeometryType::wkbPoint, - /// ..Default::default() - /// })?; - /// for y in 0..100 { - /// for x in 0..100 { - /// let wkt = format!("POINT ({} {})", x, y); - /// layer.create_feature(Geometry::from_wkt(&wkt)?)?; - /// } - /// } - /// - /// // We got through without errors. Commit the transaction and return. - /// txn.commit()?; - /// Ok(()) - /// } - /// # - /// # fn main() -> gdal::errors::Result<()> { - /// # let driver = gdal::DriverManager::get_driver_by_name("SQLite")?; - /// # let mut dataset = driver.create_vector_only(":memory:")?; - /// # create_point_grid(&mut dataset)?; - /// # assert_eq!(dataset.layer(0)?.features().count(), 10000); - /// # Ok(()) - /// # } - /// ``` - pub fn start_transaction(&mut self) -> Result> { - let force = 1; - let rv = unsafe { gdal_sys::GDALDatasetStartTransaction(self.c_dataset, force) }; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "GDALDatasetStartTransaction", - }); - } - Ok(Transaction::new(self)) - } - - /// Execute a SQL query against the Dataset. It is equivalent to calling - /// [`GDALDatasetExecuteSQL`](https://gdal.org/api/raster_c_api.html#_CPPv421GDALDatasetExecuteSQL12GDALDatasetHPKc12OGRGeometryHPKc). - /// Returns a [`sql::ResultSet`], which can be treated just as any other [`Layer`]. - /// - /// Queries such as `ALTER TABLE`, `CREATE INDEX`, etc. have no [`sql::ResultSet`], and return - /// `None`, which is distinct from an empty [`sql::ResultSet`]. - /// - /// # Arguments - /// * `query`: The SQL query - /// * `spatial_filter`: Limit results of the query to features that intersect the given - /// [`Geometry`] - /// * `dialect`: The dialect of SQL to use. See - /// - /// - /// # Example - /// - /// ``` - /// # use gdal::Dataset; - /// # use std::path::Path; - /// use gdal::vector::sql; - /// use gdal::vector::LayerAccess; - /// - /// let ds = Dataset::open(Path::new("fixtures/roads.geojson")).unwrap(); - /// let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; - /// let mut result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap().unwrap(); - /// - /// assert_eq!(10, result_set.feature_count()); - /// - /// for feature in result_set.features() { - /// let highway = feature - /// .field("highway") - /// .unwrap() - /// .unwrap() - /// .into_string() - /// .unwrap(); - /// - /// assert_eq!("pedestrian", highway); - /// } - /// ``` - pub fn execute_sql>( - &self, - query: S, - spatial_filter: Option<&Geometry>, - dialect: sql::Dialect, - ) -> Result> { - let query = CString::new(query.as_ref())?; - - let dialect_c_str = match dialect { - sql::Dialect::DEFAULT => None, - sql::Dialect::OGR => Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::OGRSQL) }), - sql::Dialect::SQLITE => { - Some(unsafe { CStr::from_bytes_with_nul_unchecked(sql::SQLITE) }) - } - }; - - self._execute_sql(query, spatial_filter, dialect_c_str) - } - - fn _execute_sql( - &self, - query: CString, - spatial_filter: Option<&Geometry>, - dialect_c_str: Option<&CStr>, - ) -> Result> { - let mut filter_geom: OGRGeometryH = std::ptr::null_mut(); - - let dialect_ptr = match dialect_c_str { - None => std::ptr::null(), - Some(d) => d.as_ptr(), - }; - - if let Some(spatial_filter) = spatial_filter { - filter_geom = unsafe { spatial_filter.c_geometry() }; - } - - let c_dataset = unsafe { self.c_dataset() }; - - unsafe { gdal_sys::CPLErrorReset() }; - - let c_layer = unsafe { - gdal_sys::GDALDatasetExecuteSQL(c_dataset, query.as_ptr(), filter_geom, dialect_ptr) - }; - - let cpl_err = unsafe { gdal_sys::CPLGetLastErrorType() }; - - if cpl_err != CPLErr::CE_None { - return Err(_last_cpl_err(cpl_err)); - } - - if c_layer.is_null() { - return Ok(None); - } - - let layer = unsafe { Layer::from_c_layer(self, c_layer) }; - - Ok(Some(sql::ResultSet { - layer, - dataset: c_dataset, - })) - } -} - -pub struct LayerIterator<'a> { - dataset: &'a Dataset, - idx: isize, - count: isize, -} - -impl<'a> Iterator for LayerIterator<'a> { - type Item = Layer<'a>; - - #[inline] - fn next(&mut self) -> Option> { - let idx = self.idx; - if idx < self.count { - self.idx += 1; - let c_layer = - unsafe { gdal_sys::OGR_DS_GetLayer(self.dataset.c_dataset, idx as c_int) }; - if !c_layer.is_null() { - let layer = unsafe { Layer::from_c_layer(self.dataset, c_layer) }; - return Some(layer); - } - } - None - } - - fn size_hint(&self) -> (usize, Option) { - match Some(self.count).and_then(|s| s.try_into().ok()) { - Some(size) => (size, Some(size)), - None => (0, None), - } - } -} - -impl<'a> LayerIterator<'a> { - pub fn with_dataset(dataset: &'a Dataset) -> LayerIterator<'a> { - LayerIterator { - dataset, - idx: 0, - count: dataset.layer_count(), - } - } } impl MajorObject for Dataset { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_dataset } } @@ -1037,139 +347,14 @@ impl Drop for Dataset { } } -/// Represents an in-flight transaction on a dataset. -/// -/// It can either be committed by calling [`commit`](Transaction::commit) or rolled back by calling -/// [`rollback`](Transaction::rollback). -/// -/// If the transaction is not explicitly committed when it is dropped, it is implicitly rolled -/// back. -/// -/// The transaction holds a mutable borrow on the `Dataset` that it was created from, so during the -/// lifetime of the transaction you will need to access the dataset by dereferencing the -/// `Transaction` through its [`Deref`] or [`DerefMut`] implementations. -#[derive(Debug)] -pub struct Transaction<'a> { - dataset: &'a mut Dataset, - rollback_on_drop: bool, -} - -impl<'a> Transaction<'a> { - fn new(dataset: &'a mut Dataset) -> Self { - Transaction { - dataset, - rollback_on_drop: true, - } - } - - /// Returns a reference to the dataset from which this `Transaction` was created. - #[deprecated = "Transaction now implements Deref, so you can call Dataset methods on it directly. Use .deref() if you need a reference to the underlying Dataset."] - pub fn dataset(&self) -> &Dataset { - self.dataset - } - - /// Returns a mutable reference to the dataset from which this `Transaction` was created. - #[deprecated = "Transaction now implements DerefMut, so you can call Dataset methods on it directly. Use .deref_mut() if you need a mutable reference to the underlying Dataset."] - pub fn dataset_mut(&mut self) -> &mut Dataset { - self.dataset - } - - /// Commits this transaction. - /// - /// If the commit fails, will return [`OGRErr::OGRERR_FAILURE`]. - /// - /// Depending on drivers, this may or may not abort layer sequential readings that are active. - pub fn commit(mut self) -> Result<()> { - let rv = unsafe { gdal_sys::GDALDatasetCommitTransaction(self.dataset.c_dataset) }; - self.rollback_on_drop = false; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "GDALDatasetCommitTransaction", - }); - } - Ok(()) - } - - /// Rolls back the dataset to its state before the start of this transaction. - /// - /// If the rollback fails, will return [`OGRErr::OGRERR_FAILURE`]. - pub fn rollback(mut self) -> Result<()> { - let rv = unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.dataset.c_dataset) }; - self.rollback_on_drop = false; - if rv != OGRErr::OGRERR_NONE { - return Err(GdalError::OgrError { - err: rv, - method_name: "GDALDatasetRollbackTransaction", - }); - } - Ok(()) - } -} - -impl<'a> Deref for Transaction<'a> { - type Target = Dataset; - - fn deref(&self) -> &Self::Target { - self.dataset - } -} - -impl<'a> DerefMut for Transaction<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.dataset - } -} - -impl<'a> Drop for Transaction<'a> { - fn drop(&mut self) { - if self.rollback_on_drop { - // We silently swallow any errors, because we have no way to report them from a drop - // function apart from panicking. - unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.dataset.c_dataset) }; - } - } -} - #[cfg(test)] mod tests { - use super::*; - use crate::test_utils::fixture; - use crate::vector::{Geometry, LayerAccess}; - use tempfile::TempPath; - - /// Copies the given file to a temporary file and opens it for writing. When the returned - /// `TempPath` is dropped, the file is deleted. - fn open_gpkg_for_update(path: &Path) -> (TempPath, Dataset) { - use std::fs; - use std::io::Write; + use gdal_sys::GDALAccess; - let input_data = fs::read(path).unwrap(); - let (mut file, temp_path) = tempfile::Builder::new() - .suffix(".gpkg") - .tempfile() - .unwrap() - .into_parts(); - file.write_all(&input_data).unwrap(); - // Close the temporary file so that Dataset can open it safely even if the filesystem uses - // exclusive locking (Windows?). - drop(file); - - let ds = Dataset::open_ex( - &temp_path, - DatasetOptions { - open_flags: GDALAccess::GA_Update.into(), - allowed_drivers: Some(&["GPKG"]), - ..DatasetOptions::default() - }, - ) - .unwrap(); - (temp_path, ds) - } + use crate::test_utils::fixture; + use crate::GdalOpenFlags; - fn polygon() -> Geometry { - Geometry::from_wkt("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))").unwrap() - } + use super::*; #[test] fn test_open_vector() { @@ -1261,83 +446,9 @@ mod tests { .unwrap_err(); } - #[test] - fn test_layer_count() { - let ds = Dataset::open(fixture("roads.geojson")).unwrap(); - assert_eq!(ds.layer_count(), 1); - } - #[test] fn test_raster_count_on_vector() { let ds = Dataset::open(fixture("roads.geojson")).unwrap(); assert_eq!(ds.raster_count(), 0); } - - #[test] - fn test_create_layer_options() { - use gdal_sys::OGRwkbGeometryType::wkbPoint; - let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); - let mut options = LayerOptions { - name: "new", - ty: wkbPoint, - ..Default::default() - }; - ds.create_layer(options.clone()).unwrap(); - assert!(ds.create_layer(options.clone()).is_err()); - options.options = Some(&["OVERWRITE=YES"]); - assert!(ds.create_layer(options).is_ok()); - } - - #[test] - fn test_start_transaction() { - let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); - let txn = ds.start_transaction(); - assert!(txn.is_ok()); - } - - #[test] - fn test_transaction_commit() { - let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); - let orig_feature_count = ds.layer(0).unwrap().feature_count(); - - let txn = ds.start_transaction().unwrap(); - let mut layer = txn.layer(0).unwrap(); - layer.create_feature(polygon()).unwrap(); - assert!(txn.commit().is_ok()); - - assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1); - } - - #[test] - fn test_transaction_rollback() { - let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); - let orig_feature_count = ds.layer(0).unwrap().feature_count(); - - let txn = ds.start_transaction().unwrap(); - let mut layer = txn.layer(0).unwrap(); - layer.create_feature(polygon()).unwrap(); - assert!(txn.rollback().is_ok()); - - assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count); - } - - #[test] - fn test_transaction_implicit_rollback() { - let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); - let orig_feature_count = ds.layer(0).unwrap().feature_count(); - - { - let txn = ds.start_transaction().unwrap(); - let mut layer = txn.layer(0).unwrap(); - layer.create_feature(polygon()).unwrap(); - } // txn is dropped here. - - assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count); - } - - #[test] - fn test_start_transaction_unsupported() { - let mut ds = Dataset::open(fixture("roads.geojson")).unwrap(); - assert!(ds.start_transaction().is_err()); - } } diff --git a/src/driver.rs b/src/driver.rs index 06b0953d9..c17f46b9c 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -313,7 +313,7 @@ impl Driver { } impl MajorObject for Driver { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_driver } } diff --git a/src/gdal_major_object.rs b/src/gdal_major_object.rs index d78b40052..a1d558cd9 100644 --- a/src/gdal_major_object.rs +++ b/src/gdal_major_object.rs @@ -2,5 +2,5 @@ use gdal_sys::GDALMajorObjectH; /// Common trait for GDAL data types backed by [`GDALMajorObjectH`]. pub trait MajorObject { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH; + fn gdal_object_ptr(&self) -> GDALMajorObjectH; } diff --git a/src/geo_transform.rs b/src/geo_transform.rs new file mode 100644 index 000000000..d4b17f083 --- /dev/null +++ b/src/geo_transform.rs @@ -0,0 +1,136 @@ +use crate::errors; +use crate::errors::GdalError; +use libc::c_double; +use std::mem::MaybeUninit; + +/// A six-element array storing the coefficients of an [affine transform] +/// used in mapping coordinates between pixel/line `(P, L)` (raster) space, +/// and `(Xp,Yp)` (projection/[`SpatialRef`]) space. +/// +/// # Interpretation +/// +/// A `GeoTransform`'s components have the following meanings: +/// +/// * `GeoTransform[0]`: x-coordinate of the upper-left corner of the upper-left pixel. +/// * `GeoTransform[1]`: W-E pixel resolution (pixel width). +/// * `GeoTransform[2]`: row rotation (typically zero). +/// * `GeoTransform[3]`: y-coordinate of the upper-left corner of the upper-left pixel. +/// * `GeoTransform[4]`: column rotation (typically zero). +/// * `GeoTransform[5]`: N-S pixel resolution (pixel height), negative value for a North-up image. +/// +/// +/// ## Note +/// +/// Care with coefficient ordering is required when constructing an [affine transform matrix] from +/// a `GeoTransform`. If a 3x3 transform matrix is defined as: +/// +/// ```text +/// | a b c | +/// | d e f | +/// | 0 0 1 | +/// ``` +/// +/// The corresponding `GeoTransform` ordering is: +/// +/// ```text +/// [c, a, b, f, d, e] +/// ``` +/// +/// # Usage +/// * [`apply`](GeoTransformEx::apply): perform a `(P,L) -> (Xp,Yp)` transformation +/// * [`invert`](GeoTransformEx::invert): construct the inverse transformation coefficients +/// for computing `(Xp,Yp) -> (P,L)` transformations +/// +/// # Example +/// +/// ```rust, no_run +/// # fn main() -> gdal::errors::Result<()> { +/// use gdal::{Dataset, GeoTransformEx}; +/// let ds = Dataset::open("fixtures/m_3607824_se_17_1_20160620_sub.tif")?; +/// let transform = ds.geo_transform()?; +/// let (p, l) = (0.0, 0.0); +/// let (x,y) = transform.apply(p, l); +/// println!("(x,y): ({x},{y})"); +/// let inverse = transform.invert()?; +/// let (p, l) = inverse.apply(x, y); +/// println!("(p,l): ({p},{l})"); +/// # Ok(()) +/// # } +/// ``` +/// Output: +/// +/// ```text +/// (x,y): (768269,4057292) +/// (p,l): (0,0) +/// ``` +/// # See Also +/// +/// * [GDAL GeoTransform Tutorial] +/// * [GDALGetGeoTransform] +/// * [Raster Data Model Affine Transform] +/// +/// [GDAL GeoTransform Tutorial]: https://gdal.org/tutorials/geotransforms_tut.html +/// [GDALGetGeoTransform]: https://gdal.org/api/gdaldataset_cpp.html#classGDALDataset_1a5101119705f5fa2bc1344ab26f66fd1d +/// [Raster Data Model Affine Transform]: https://gdal.org/user/raster_data_model.html#affine-geotransform +/// [affine transform]: https://en.wikipedia.org/wiki/Affine_transformation +/// [affine transform matrix]: https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations +pub type GeoTransform = [c_double; 6]; + +/// Extension methods on [`GeoTransform`] +pub trait GeoTransformEx { + /// Apply GeoTransform to x/y coordinate. + /// + /// Wraps [GDALApplyGeoTransform]. + /// + /// # Example + /// + /// See [`GeoTransform`](GeoTransform#example) + /// + /// [GDALApplyGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv421GDALApplyGeoTransformPdddPdPd + fn apply(&self, pixel: f64, line: f64) -> (f64, f64); + + /// Invert a [`GeoTransform`]. + /// + /// Wraps [GDALInvGeoTransform]. + /// + /// # Example + /// + /// See [`GeoTransform`](GeoTransform#example) + /// + /// [GDALInvGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv419GDALInvGeoTransformPdPd + fn invert(&self) -> errors::Result; +} + +impl GeoTransformEx for GeoTransform { + fn apply(&self, pixel: f64, line: f64) -> (f64, f64) { + let mut geo_x = MaybeUninit::::uninit(); + let mut geo_y = MaybeUninit::::uninit(); + unsafe { + gdal_sys::GDALApplyGeoTransform( + self.as_ptr() as *mut f64, + pixel, + line, + geo_x.as_mut_ptr(), + geo_y.as_mut_ptr(), + ); + (geo_x.assume_init(), geo_y.assume_init()) + } + } + + fn invert(&self) -> errors::Result { + let mut gt_out = MaybeUninit::::uninit(); + let rv = unsafe { + gdal_sys::GDALInvGeoTransform( + self.as_ptr() as *mut f64, + (*gt_out.as_mut_ptr()).as_mut_ptr(), + ) + }; + if rv == 0 { + return Err(GdalError::BadArgument( + "Geo transform is uninvertible".to_string(), + )); + } + let result = unsafe { gt_out.assume_init() }; + Ok(result) + } +} diff --git a/src/lib.rs b/src/lib.rs index 75de79a80..e4c67bf05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,9 @@ mod driver; pub mod errors; mod gcp; mod gdal_major_object; +mod geo_transform; mod metadata; +mod options; pub mod programs; pub mod raster; pub mod spatial_ref; @@ -119,10 +121,10 @@ pub mod vector; pub mod version; pub mod vsi; -pub use dataset::{ - Dataset, DatasetOptions, GdalOpenFlags, GeoTransform, GeoTransformEx, LayerIterator, - LayerOptions, Transaction, -}; +pub use dataset::Dataset; +pub use geo_transform::{GeoTransform, GeoTransformEx}; +pub use options::{DatasetOptions, GdalOpenFlags}; + pub use driver::{Driver, DriverManager}; pub use gcp::{Gcp, GcpRef}; #[cfg(any(major_ge_4, all(major_is_3, minor_ge_6)))] diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 000000000..7f00c8bd5 --- /dev/null +++ b/src/options.rs @@ -0,0 +1,82 @@ +use bitflags::bitflags; +use gdal_sys::GDALAccess; +use libc::c_uint; + +/// Open options for [Dataset] +#[derive(Debug, Default)] +pub struct DatasetOptions<'a> { + pub open_flags: GdalOpenFlags, + pub allowed_drivers: Option<&'a [&'a str]>, + pub open_options: Option<&'a [&'a str]>, + pub sibling_files: Option<&'a [&'a str]>, +} + +// These are skipped by bindgen and manually updated. +#[cfg(major_ge_2)] +bitflags! { + /// GDal extended open flags used by [`Dataset::open_ex`]. + /// + /// Used in the `nOpenFlags` argument to [`GDALOpenEx`]. + /// + /// Note that the `GDAL_OF_SHARED` option is removed + /// from the set of allowed option because it subverts + /// the [`Send`] implementation that allow passing the + /// dataset the another thread. See + /// https://github.com/georust/gdal/issues/154. + /// + /// [`GDALOpenEx`]: https://gdal.org/doxygen/gdal_8h.html#a9cb8585d0b3c16726b08e25bcc94274a + #[derive(Debug)] + #[allow(clippy::assign_op_pattern)] + pub struct GdalOpenFlags: c_uint { + /// Open in read-only mode (default). + const GDAL_OF_READONLY = 0x00; + /// Open in update mode. + const GDAL_OF_UPDATE = 0x01; + /// Allow raster and vector drivers to be used. + const GDAL_OF_ALL = 0x00; + /// Allow raster drivers to be used. + const GDAL_OF_RASTER = 0x02; + /// Allow vector drivers to be used. + const GDAL_OF_VECTOR = 0x04; + /// Allow gnm drivers to be used. + #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] + const GDAL_OF_GNM = 0x08; + /// Allow multidimensional raster drivers to be used. + #[cfg(all(major_ge_3,minor_ge_1))] + const GDAL_OF_MULTIDIM_RASTER = 0x10; + /// Emit error message in case of failed open. + const GDAL_OF_VERBOSE_ERROR = 0x40; + /// Open as internal dataset. Such dataset isn't + /// registered in the global list of opened dataset. + /// Cannot be used with GDAL_OF_SHARED. + const GDAL_OF_INTERNAL = 0x80; + + /// Default strategy for cached blocks. + #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] + const GDAL_OF_DEFAULT_BLOCK_ACCESS = 0; + + /// Array based strategy for cached blocks. + #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] + const GDAL_OF_ARRAY_BLOCK_ACCESS = 0x100; + + /// Hashset based strategy for cached blocks. + #[cfg(any( all(major_ge_2,minor_ge_1), major_ge_3 ))] + const GDAL_OF_HASHSET_BLOCK_ACCESS = 0x200; + } +} + +impl Default for GdalOpenFlags { + fn default() -> GdalOpenFlags { + GdalOpenFlags::GDAL_OF_READONLY + } +} + +impl From for GdalOpenFlags { + fn from(val: GDALAccess::Type) -> GdalOpenFlags { + if val == GDALAccess::GA_Update { + GdalOpenFlags::GDAL_OF_UPDATE + } else { + GdalOpenFlags::GDAL_OF_READONLY + } + } +} diff --git a/src/programs/raster/mdimtranslate.rs b/src/programs/raster/mdimtranslate.rs index 75cf1f8cf..d90bd6363 100644 --- a/src/programs/raster/mdimtranslate.rs +++ b/src/programs/raster/mdimtranslate.rs @@ -183,15 +183,12 @@ fn _multi_dim_translate( ) -> Result { let (psz_dest_option, h_dst_ds) = match &destination { MultiDimTranslateDestination::Path(c_path) => (Some(c_path), null_mut()), - MultiDimTranslateDestination::Dataset { dataset, .. } => { - (None, unsafe { dataset.c_dataset() }) - } + MultiDimTranslateDestination::Dataset { dataset, .. } => (None, dataset.c_dataset()), }; let psz_dest = psz_dest_option.map(|x| x.as_ptr()).unwrap_or_else(null); - let mut pah_src_ds: Vec = - input.iter().map(|x| unsafe { x.c_dataset() }).collect(); + let mut pah_src_ds: Vec = input.iter().map(|x| x.c_dataset()).collect(); let ps_options = options .as_ref() @@ -230,7 +227,8 @@ mod tests { use super::*; - use crate::{DatasetOptions, DriverManager, GdalOpenFlags}; + use crate::options::DatasetOptions; + use crate::{DriverManager, GdalOpenFlags}; #[test] #[cfg_attr(not(all(major_ge_3, minor_ge_4)), ignore)] diff --git a/src/raster/mdarray.rs b/src/raster/mdarray.rs index 46c3c80b3..932951bb9 100644 --- a/src/raster/mdarray.rs +++ b/src/raster/mdarray.rs @@ -159,7 +159,7 @@ impl<'a> MDArray<'a> { // 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 p_dst_buffer_alloc_start: *mut c_void = std::ptr::null_mut(); let n_dst_buffer_alloc_size = 0; let rv = unsafe { @@ -282,7 +282,7 @@ impl<'a> MDArray<'a> { // 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 p_dst_buffer_alloc_start: *mut c_void = std::ptr::null_mut(); let n_dst_buffer_alloc_size = 0; unsafe { @@ -295,7 +295,7 @@ impl<'a> MDArray<'a> { array_step, buffer_stride, data_type, - string_pointers.as_mut_ptr().cast::(), + string_pointers.as_mut_ptr().cast::(), p_dst_buffer_alloc_start, n_dst_buffer_alloc_size, ); @@ -796,12 +796,33 @@ impl Attribute { } } +/// [Dataset] methods supporting multi-dimensional array operations. +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)) + } + } +} + #[cfg(test)] mod tests { use super::*; - use crate::{test_utils::TempFixture, Dataset, DatasetOptions, GdalOpenFlags}; + use crate::options::DatasetOptions; + use crate::{test_utils::TempFixture, Dataset, GdalOpenFlags}; #[test] #[cfg_attr(not(all(major_ge_3, minor_ge_4)), ignore)] diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index 075d3ebe6..1cc89163f 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -19,6 +19,68 @@ use ndarray::Array2; use crate::errors::*; +/// [Dataset] methods for raster datasets. +impl Dataset { + /// Fetch a band object for a dataset. + /// + /// Applies to raster datasets, and fetches the + /// rasterband at the given _1-based_ index. + pub fn rasterband(&self, band_index: isize) -> Result { + unsafe { + let c_band = gdal_sys::GDALGetRasterBand(self.c_dataset(), band_index as c_int); + if c_band.is_null() { + return Err(_last_null_pointer_err("GDALGetRasterBand")); + } + Ok(RasterBand::from_c_rasterband(self, c_band)) + } + } + + /// Builds overviews for the current `Dataset`. See [`GDALBuildOverviews`]. + /// + /// # Arguments + /// * `resampling` - resampling method, as accepted by GDAL, e.g. `"CUBIC"` + /// * `overviews` - list of overview decimation factors, e.g. `&[2, 4, 8, 16, 32]` + /// * `bands` - list of bands to build the overviews for, or empty for all bands + /// + /// [`GDALBuildOverviews`]: https://gdal.org/api/raster_c_api.html#_CPPv418GDALBuildOverviews12GDALDatasetHPKciPKiiPKi16GDALProgressFuncPv + pub fn build_overviews( + &mut self, + resampling: &str, + overviews: &[i32], + bands: &[i32], + ) -> Result<()> { + let c_resampling = CString::new(resampling)?; + let rv = unsafe { + gdal_sys::GDALBuildOverviews( + self.c_dataset(), + c_resampling.as_ptr(), + overviews.len() as i32, + overviews.as_ptr() as *mut i32, + bands.len() as i32, + bands.as_ptr() as *mut i32, + None, + std::ptr::null_mut(), + ) + }; + if rv != CPLErr::CE_None { + return Err(_last_cpl_err(rv)); + } + Ok(()) + } + + /// Fetch the number of raster bands on this dataset. + pub fn raster_count(&self) -> isize { + (unsafe { gdal_sys::GDALGetRasterCount(self.c_dataset()) }) as isize + } + + /// Returns the raster dimensions: (width, height). + pub fn raster_size(&self) -> (usize, usize) { + let size_x = unsafe { gdal_sys::GDALGetRasterXSize(self.c_dataset()) } as usize; + let size_y = unsafe { gdal_sys::GDALGetRasterYSize(self.c_dataset()) } as usize; + (size_x, size_y) + } +} + /// Resampling algorithms used throughout various GDAL raster I/O operations. /// /// # Example @@ -746,7 +808,7 @@ pub struct StatisticsAll { } impl<'a> MajorObject for RasterBand<'a> { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_rasterband } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 5665e0d46..b28dbeb67 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,6 +1,9 @@ +use crate::{Dataset, DatasetOptions}; +use gdal_sys::GDALAccess; use std::ffi::c_void; use std::marker::PhantomData; use std::path::{Path, PathBuf}; +use tempfile::TempPath; /// A struct that contains a temporary directory and a path to a file in that directory. pub struct TempFixture { @@ -75,3 +78,32 @@ impl Drop for SuppressGDALErrorLog { unsafe { gdal_sys::CPLPopErrorHandler() }; } } + +/// Copies the given file to a temporary file and opens it for writing. When the returned +/// `TempPath` is dropped, the file is deleted. +pub fn open_gpkg_for_update(path: &Path) -> (TempPath, Dataset) { + use std::fs; + use std::io::Write; + + let input_data = fs::read(path).unwrap(); + let (mut file, temp_path) = tempfile::Builder::new() + .suffix(".gpkg") + .tempfile() + .unwrap() + .into_parts(); + file.write_all(&input_data).unwrap(); + // Close the temporary file so that Dataset can open it safely even if the filesystem uses + // exclusive locking (Windows?). + drop(file); + + let ds = Dataset::open_ex( + &temp_path, + DatasetOptions { + open_flags: GDALAccess::GA_Update.into(), + allowed_drivers: Some(&["GPKG"]), + ..DatasetOptions::default() + }, + ) + .unwrap(); + (temp_path, ds) +} diff --git a/src/vector/feature.rs b/src/vector/feature.rs index e6fe8c40d..069f9e922 100644 --- a/src/vector/feature.rs +++ b/src/vector/feature.rs @@ -1,7 +1,7 @@ use crate::utils::{_last_null_pointer_err, _string, _string_array}; use crate::vector::geometry::Geometry; -use crate::vector::{Defn, LayerAccess}; -use gdal_sys::{self, OGRErr, OGRFeatureH, OGRFieldType}; +use crate::vector::{Defn, LayerAccess, OwnedLayer}; +use gdal_sys::{self, OGRErr, OGRFeatureH, OGRFieldType, OGRLayerH}; use libc::{c_char, c_double, c_int, c_longlong}; use std::convert::TryInto; use std::ffi::{CString, NulError}; @@ -727,6 +727,103 @@ impl<'a> Drop for Feature<'a> { } } +pub struct FeatureIterator<'a> { + defn: &'a Defn, + c_layer: OGRLayerH, + size_hint: Option, +} + +impl<'a> Iterator for FeatureIterator<'a> { + type Item = Feature<'a>; + + #[inline] + fn next(&mut self) -> Option> { + let c_feature = unsafe { gdal_sys::OGR_L_GetNextFeature(self.c_layer) }; + if c_feature.is_null() { + None + } else { + Some(unsafe { Feature::from_c_feature(self.defn, c_feature) }) + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.size_hint { + Some(size) => (size, Some(size)), + None => (0, None), + } + } +} + +impl<'a> FeatureIterator<'a> { + pub(crate) fn _with_layer(layer: &'a L) -> Self { + let defn = layer.defn(); + let size_hint = layer.try_feature_count().and_then(|s| s.try_into().ok()); + Self { + c_layer: unsafe { layer.c_layer() }, + size_hint, + defn, + } + } +} + +pub struct OwnedFeatureIterator { + pub(crate) layer: OwnedLayer, + size_hint: Option, +} + +impl<'a> Iterator for &'a mut OwnedFeatureIterator +where + Self: 'a, +{ + type Item = Feature<'a>; + + #[inline] + fn next(&mut self) -> Option> { + let c_feature = unsafe { gdal_sys::OGR_L_GetNextFeature(self.layer.c_layer()) }; + + if c_feature.is_null() { + return None; + } + + Some(unsafe { + // We have to convince the compiler that our `Defn` adheres to our iterator lifetime `<'a>` + let defn: &'a Defn = std::mem::transmute::<&'_ _, &'a _>(self.layer.defn()); + + Feature::from_c_feature(defn, c_feature) + }) + } + + fn size_hint(&self) -> (usize, Option) { + match self.size_hint { + Some(size) => (size, Some(size)), + None => (0, None), + } + } +} + +impl OwnedFeatureIterator { + pub(crate) fn _with_layer(layer: OwnedLayer) -> Self { + let size_hint = layer.try_feature_count().and_then(|s| s.try_into().ok()); + Self { layer, size_hint } + } + + pub fn into_layer(self) -> OwnedLayer { + self.layer + } +} + +impl AsMut for OwnedFeatureIterator { + fn as_mut(&mut self) -> &mut Self { + self + } +} + +impl From for OwnedLayer { + fn from(feature_iterator: OwnedFeatureIterator) -> Self { + feature_iterator.into_layer() + } +} + #[derive(Clone, Debug, PartialEq)] pub enum FieldValue { IntegerValue(i32), diff --git a/src/vector/layer.rs b/src/vector/layer.rs index 11a6288ac..0ecf16e48 100644 --- a/src/vector/layer.rs +++ b/src/vector/layer.rs @@ -2,15 +2,17 @@ use crate::metadata::Metadata; use crate::spatial_ref::SpatialRef; use crate::utils::{_last_null_pointer_err, _string}; use crate::vector::defn::Defn; -use crate::vector::{Envelope, Feature, FieldValue, Geometry}; +use crate::vector::{Envelope, Feature, FieldValue, Geometry, LayerOptions}; use crate::{dataset::Dataset, gdal_major_object::MajorObject}; use gdal_sys::{self, GDALMajorObjectH, OGRErr, OGRFieldDefnH, OGRFieldType, OGRLayerH}; use libc::c_int; +use std::ffi::NulError; use std::mem::MaybeUninit; use std::ptr::null_mut; use std::{convert::TryInto, ffi::CString, marker::PhantomData}; use crate::errors::*; +use crate::vector::feature::{FeatureIterator, OwnedFeatureIterator}; /// Layer capabilities #[allow(clippy::upper_case_acronyms)] @@ -104,7 +106,7 @@ pub struct Layer<'a> { } impl<'a> MajorObject for Layer<'a> { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_layer } } @@ -159,7 +161,7 @@ pub struct OwnedLayer { } impl MajorObject for OwnedLayer { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_layer } } @@ -503,103 +505,47 @@ pub trait LayerAccess: Sized { } } -pub struct FeatureIterator<'a> { - defn: &'a Defn, - c_layer: OGRLayerH, - size_hint: Option, +pub struct LayerIterator<'a> { + dataset: &'a Dataset, + idx: isize, + count: isize, } -impl<'a> Iterator for FeatureIterator<'a> { - type Item = Feature<'a>; +impl<'a> Iterator for LayerIterator<'a> { + type Item = Layer<'a>; #[inline] - fn next(&mut self) -> Option> { - let c_feature = unsafe { gdal_sys::OGR_L_GetNextFeature(self.c_layer) }; - if c_feature.is_null() { - None - } else { - Some(unsafe { Feature::from_c_feature(self.defn, c_feature) }) + fn next(&mut self) -> Option> { + let idx = self.idx; + if idx < self.count { + self.idx += 1; + let c_layer = + unsafe { gdal_sys::OGR_DS_GetLayer(self.dataset.c_dataset(), idx as c_int) }; + if !c_layer.is_null() { + let layer = unsafe { Layer::from_c_layer(self.dataset, c_layer) }; + return Some(layer); + } } + None } fn size_hint(&self) -> (usize, Option) { - match self.size_hint { + match Some(self.count).and_then(|s| s.try_into().ok()) { Some(size) => (size, Some(size)), None => (0, None), } } } -impl<'a> FeatureIterator<'a> { - pub(crate) fn _with_layer(layer: &'a L) -> Self { - let defn = layer.defn(); - let size_hint = layer.try_feature_count().and_then(|s| s.try_into().ok()); - Self { - c_layer: unsafe { layer.c_layer() }, - size_hint, - defn, +impl<'a> LayerIterator<'a> { + pub fn with_dataset(dataset: &'a Dataset) -> LayerIterator<'a> { + LayerIterator { + dataset, + idx: 0, + count: dataset.layer_count(), } } } - -pub struct OwnedFeatureIterator { - pub(crate) layer: OwnedLayer, - size_hint: Option, -} - -impl<'a> Iterator for &'a mut OwnedFeatureIterator -where - Self: 'a, -{ - type Item = Feature<'a>; - - #[inline] - fn next(&mut self) -> Option> { - let c_feature = unsafe { gdal_sys::OGR_L_GetNextFeature(self.layer.c_layer()) }; - - if c_feature.is_null() { - return None; - } - - Some(unsafe { - // We have to convince the compiler that our `Defn` adheres to our iterator lifetime `<'a>` - let defn: &'a Defn = std::mem::transmute::<&'_ _, &'a _>(self.layer.defn()); - - Feature::from_c_feature(defn, c_feature) - }) - } - - fn size_hint(&self) -> (usize, Option) { - match self.size_hint { - Some(size) => (size, Some(size)), - None => (0, None), - } - } -} - -impl OwnedFeatureIterator { - pub(crate) fn _with_layer(layer: OwnedLayer) -> Self { - let size_hint = layer.try_feature_count().and_then(|s| s.try_into().ok()); - Self { layer, size_hint } - } - - pub fn into_layer(self) -> OwnedLayer { - self.layer - } -} - -impl AsMut for OwnedFeatureIterator { - fn as_mut(&mut self) -> &mut Self { - self - } -} - -impl From for OwnedLayer { - fn from(feature_iterator: OwnedFeatureIterator) -> Self { - feature_iterator.into_layer() - } -} - pub struct FieldDefn { c_obj: OGRFieldDefnH, } @@ -611,7 +557,7 @@ impl Drop for FieldDefn { } impl MajorObject for FieldDefn { - unsafe fn gdal_object_ptr(&self) -> GDALMajorObjectH { + fn gdal_object_ptr(&self) -> GDALMajorObjectH { self.c_obj } } @@ -643,11 +589,152 @@ impl FieldDefn { } } +/// [Layer] related methods for [Dataset]. +impl Dataset { + fn child_layer(&self, c_layer: OGRLayerH) -> Layer { + unsafe { Layer::from_c_layer(self, c_layer) } + } + + fn into_child_layer(self, c_layer: OGRLayerH) -> OwnedLayer { + unsafe { OwnedLayer::from_c_layer(self, c_layer) } + } + + /// Get the number of layers in this dataset. + pub fn layer_count(&self) -> isize { + (unsafe { gdal_sys::OGR_DS_GetLayerCount(self.c_dataset()) }) as isize + } + + /// Fetch a layer by index. + /// + /// Applies to vector datasets, and fetches by the given + /// _0-based_ index. + pub fn layer(&self, idx: isize) -> Result { + let c_layer = unsafe { gdal_sys::OGR_DS_GetLayer(self.c_dataset(), idx as c_int) }; + if c_layer.is_null() { + return Err(_last_null_pointer_err("OGR_DS_GetLayer")); + } + Ok(self.child_layer(c_layer)) + } + + /// Fetch a layer by index. + /// + /// Applies to vector datasets, and fetches by the given + /// _0-based_ index. + pub fn into_layer(self, idx: isize) -> Result { + let c_layer = unsafe { gdal_sys::OGR_DS_GetLayer(self.c_dataset(), idx as c_int) }; + if c_layer.is_null() { + return Err(_last_null_pointer_err("OGR_DS_GetLayer")); + } + Ok(self.into_child_layer(c_layer)) + } + + /// Fetch a layer by name. + pub fn layer_by_name(&self, name: &str) -> Result { + let c_name = CString::new(name)?; + let c_layer = unsafe { gdal_sys::OGR_DS_GetLayerByName(self.c_dataset(), c_name.as_ptr()) }; + if c_layer.is_null() { + return Err(_last_null_pointer_err("OGR_DS_GetLayerByName")); + } + Ok(self.child_layer(c_layer)) + } + + /// Fetch a layer by name. + pub fn into_layer_by_name(self, name: &str) -> Result { + let c_name = CString::new(name)?; + let c_layer = unsafe { gdal_sys::OGR_DS_GetLayerByName(self.c_dataset(), c_name.as_ptr()) }; + if c_layer.is_null() { + return Err(_last_null_pointer_err("OGR_DS_GetLayerByName")); + } + Ok(self.into_child_layer(c_layer)) + } + + /// Returns an iterator over the layers of the dataset. + pub fn layers(&self) -> LayerIterator { + LayerIterator::with_dataset(self) + } + + /// Creates a new layer. The [`LayerOptions`] struct implements `Default`, so you only need to + /// specify those options that deviate from the default. + /// + /// # Examples + /// + /// Create a new layer with an empty name, no spatial reference, and unknown geometry type: + /// + /// ``` + /// # use gdal::DriverManager; + /// # let driver = DriverManager::get_driver_by_name("GPKG").unwrap(); + /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); + /// let blank_layer = dataset.create_layer(Default::default()).unwrap(); + /// ``` + /// + /// Create a new named line string layer using WGS84: + /// + /// ``` + /// # use gdal::{DriverManager }; + /// # use gdal::spatial_ref::SpatialRef; + /// # use gdal::vector::LayerOptions; + /// # let driver = DriverManager::get_driver_by_name("GPKG").unwrap(); + /// # let mut dataset = driver.create_vector_only("/vsimem/example.gpkg").unwrap(); + /// let roads = dataset.create_layer(LayerOptions { + /// name: "roads", + /// srs: Some(&SpatialRef::from_epsg(4326).unwrap()), + /// ty: gdal_sys::OGRwkbGeometryType::wkbLineString, + /// ..Default::default() + /// }).unwrap(); + /// ``` + pub fn create_layer(&mut self, options: LayerOptions<'_>) -> Result { + let c_name = CString::new(options.name)?; + let c_srs = match options.srs { + Some(srs) => srs.to_c_hsrs(), + None => null_mut(), + }; + + // Handle string options: we need to keep the CStrings and the pointers around. + let c_options = options.options.map(|d| { + d.iter() + .map(|&s| CString::new(s)) + .collect::, NulError>>() + }); + let c_options_vec = match c_options { + Some(Err(e)) => return Err(e.into()), + Some(Ok(c_options_vec)) => c_options_vec, + None => Vec::from([]), + }; + let mut c_options_ptrs = c_options_vec.iter().map(|s| s.as_ptr()).collect::>(); + c_options_ptrs.push(std::ptr::null()); + + let c_options_ptr = if options.options.is_some() { + c_options_ptrs.as_ptr() + } else { + std::ptr::null() + }; + + let c_layer = unsafe { + // The C function takes `char **papszOptions` without mention of `const`, and this is + // propagated to the gdal_sys wrapper. The lack of `const` seems like a mistake in the + // GDAL API, so we just do a cast here. + gdal_sys::OGR_DS_CreateLayer( + self.c_dataset(), + c_name.as_ptr(), + c_srs, + options.ty, + c_options_ptr as *mut *mut libc::c_char, + ) + }; + if c_layer.is_null() { + return Err(_last_null_pointer_err("OGR_DS_CreateLayer")); + }; + Ok(self.child_layer(c_layer)) + } +} + #[cfg(test)] mod tests { use super::{LayerCaps::*, *}; - use crate::test_utils::{fixture, SuppressGDALErrorLog, TempFixture}; - use crate::{assert_almost_eq, Dataset, DatasetOptions, DriverManager, GdalOpenFlags}; + use crate::options::DatasetOptions; + use crate::test_utils::{fixture, open_gpkg_for_update, SuppressGDALErrorLog, TempFixture}; + use crate::vector::feature::FeatureIterator; + use crate::{assert_almost_eq, Dataset, DriverManager, GdalOpenFlags}; use gdal_sys::OGRwkbGeometryType; fn ds_with_layer(ds_name: &str, layer_name: &str, f: F) @@ -691,6 +778,21 @@ mod tests { with_layer(name, |layer| f(layer.feature(fid).unwrap())); } + #[test] + fn test_create_layer_options() { + use gdal_sys::OGRwkbGeometryType::wkbPoint; + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let mut options = LayerOptions { + name: "new", + ty: wkbPoint, + ..Default::default() + }; + ds.create_layer(options.clone()).unwrap(); + assert!(ds.create_layer(options.clone()).is_err()); + options.options = Some(&["OVERWRITE=YES"]); + assert!(ds.create_layer(options).is_ok()); + } + #[test] fn test_layer_count() { let ds = Dataset::open(fixture("roads.geojson")).unwrap(); diff --git a/src/vector/mod.rs b/src/vector/mod.rs index a0077457c..8431e97e7 100644 --- a/src/vector/mod.rs +++ b/src/vector/mod.rs @@ -69,15 +69,20 @@ mod feature; mod geometry; mod layer; mod ops; +mod options; pub mod sql; +mod transaction; pub use defn::{Defn, Field, FieldIterator}; -pub use feature::{field_type_to_name, Feature, FieldValue, FieldValueIterator}; +pub use feature::{ + field_type_to_name, Feature, FeatureIterator, FieldValue, FieldValueIterator, + OwnedFeatureIterator, +}; pub use gdal_sys::{OGRFieldType, OGRwkbGeometryType}; pub use geometry::{geometry_type_to_name, Geometry}; -pub use layer::{ - FeatureIterator, FieldDefn, Layer, LayerAccess, LayerCaps, OwnedFeatureIterator, OwnedLayer, -}; +pub use layer::{FieldDefn, Layer, LayerAccess, LayerCaps, LayerIterator, OwnedLayer}; +pub use options::LayerOptions; +pub use transaction::Transaction; /// Axis aligned 2D bounding box. pub type Envelope = gdal_sys::OGREnvelope; diff --git a/src/vector/options.rs b/src/vector/options.rs new file mode 100644 index 000000000..5e1842cc6 --- /dev/null +++ b/src/vector/options.rs @@ -0,0 +1,29 @@ +use crate::spatial_ref::SpatialRef; +use gdal_sys::OGRwkbGeometryType; + +/// Parameters for [`Dataset::create_layer`]. +#[derive(Clone, Debug)] +pub struct LayerOptions<'a> { + /// The name of the newly created layer. May be an empty string. + pub name: &'a str, + /// The SRS of the newly created layer, or `None` for no SRS. + pub srs: Option<&'a SpatialRef>, + /// The type of geometry for the new layer. + pub ty: OGRwkbGeometryType::Type, + /// Additional driver-specific options to pass to GDAL, in the form `name=value`. + pub options: Option<&'a [&'a str]>, +} + +const EMPTY_LAYER_NAME: &str = ""; + +impl<'a> Default for LayerOptions<'a> { + /// Returns creation options for a new layer with no name, no SRS and unknown geometry type. + fn default() -> Self { + LayerOptions { + name: EMPTY_LAYER_NAME, + srs: None, + ty: OGRwkbGeometryType::wkbUnknown, + options: None, + } + } +} diff --git a/src/vector/sql.rs b/src/vector/sql.rs index 3906fa71c..00f6ca9a7 100644 --- a/src/vector/sql.rs +++ b/src/vector/sql.rs @@ -1,11 +1,15 @@ +use std::ffi::{CStr, CString}; use std::ops::{Deref, DerefMut}; -use gdal_sys::GDALDatasetH; +use crate::Dataset; +use gdal_sys::{CPLErr, GDALDatasetH, OGRGeometryH}; -use crate::vector::{Layer, LayerAccess}; +use crate::errors::*; +use crate::utils::_last_cpl_err; +use crate::vector::{sql, Geometry, Layer, LayerAccess}; /// The result of a SQL query executed by -/// [`Dataset::execute_sql()`](crate::Dataset::execute_sql()). It is just a thin wrapper around a +/// [`Dataset::execute_sql()`](Dataset::execute_sql()). It is just a thin wrapper around a /// [`Layer`], and you can treat it as such. #[derive(Debug)] pub struct ResultSet<'a> { @@ -52,6 +56,108 @@ pub enum Dialect { pub(crate) const OGRSQL: &[u8] = b"OGRSQL\0"; pub(crate) const SQLITE: &[u8] = b"SQLITE\0"; +/// [Dataset] methods relating to SQL over vector datasets. +impl Dataset { + /// Execute a SQL query against the Dataset. It is equivalent to calling + /// [`GDALDatasetExecuteSQL`](https://gdal.org/api/raster_c_api.html#_CPPv421GDALDatasetExecuteSQL12GDALDatasetHPKc12OGRGeometryHPKc). + /// Returns a [`ResultSet`], which can be treated just as any other [`Layer`]. + /// + /// Queries such as `ALTER TABLE`, `CREATE INDEX`, etc. have no [`ResultSet`], and return + /// `None`, which is distinct from an empty [`ResultSet`]. + /// + /// # Arguments + /// * `query`: The SQL query + /// * `spatial_filter`: Limit results of the query to features that intersect the given + /// [`Geometry`] + /// * `dialect`: The dialect of SQL to use. See + /// + /// + /// # Example + /// + /// ``` + /// # use gdal::Dataset; + /// # use std::path::Path; + /// use gdal::vector::sql; + /// use gdal::vector::LayerAccess; + /// + /// let ds = Dataset::open(Path::new("fixtures/roads.geojson")).unwrap(); + /// let query = "SELECT kind, is_bridge, highway FROM roads WHERE highway = 'pedestrian'"; + /// let mut result_set = ds.execute_sql(query, None, sql::Dialect::DEFAULT).unwrap().unwrap(); + /// + /// assert_eq!(10, result_set.feature_count()); + /// + /// for feature in result_set.features() { + /// let highway = feature + /// .field("highway") + /// .unwrap() + /// .unwrap() + /// .into_string() + /// .unwrap(); + /// + /// assert_eq!("pedestrian", highway); + /// } + /// ``` + pub fn execute_sql>( + &self, + query: S, + spatial_filter: Option<&Geometry>, + dialect: Dialect, + ) -> Result> { + let query = CString::new(query.as_ref())?; + + let dialect_c_str = match dialect { + Dialect::DEFAULT => None, + Dialect::OGR => Some(unsafe { CStr::from_bytes_with_nul_unchecked(OGRSQL) }), + Dialect::SQLITE => Some(unsafe { CStr::from_bytes_with_nul_unchecked(SQLITE) }), + }; + + self._execute_sql(query, spatial_filter, dialect_c_str) + } + + fn _execute_sql( + &self, + query: CString, + spatial_filter: Option<&Geometry>, + dialect_c_str: Option<&CStr>, + ) -> Result> { + let mut filter_geom: OGRGeometryH = std::ptr::null_mut(); + + let dialect_ptr = match dialect_c_str { + None => std::ptr::null(), + Some(d) => d.as_ptr(), + }; + + if let Some(spatial_filter) = spatial_filter { + filter_geom = unsafe { spatial_filter.c_geometry() }; + } + + let c_dataset = self.c_dataset(); + + unsafe { gdal_sys::CPLErrorReset() }; + + let c_layer = unsafe { + gdal_sys::GDALDatasetExecuteSQL(c_dataset, query.as_ptr(), filter_geom, dialect_ptr) + }; + + let cpl_err = unsafe { gdal_sys::CPLGetLastErrorType() }; + + if cpl_err != CPLErr::CE_None { + return Err(_last_cpl_err(cpl_err)); + } + + if c_layer.is_null() { + return Ok(None); + } + + let layer = unsafe { Layer::from_c_layer(self, c_layer) }; + + Ok(Some(sql::ResultSet { + layer, + dataset: c_dataset, + })) + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/src/vector/transaction.rs b/src/vector/transaction.rs new file mode 100644 index 000000000..720dbcdfc --- /dev/null +++ b/src/vector/transaction.rs @@ -0,0 +1,244 @@ +use crate::errors::{GdalError, Result}; +use crate::Dataset; +use gdal_sys::OGRErr; +use std::ops::{Deref, DerefMut}; + +/// Represents an in-flight transaction on a dataset. +/// +/// It can either be committed by calling [`commit`](Transaction::commit) or rolled back by calling +/// [`rollback`](Transaction::rollback). +/// +/// If the transaction is not explicitly committed when it is dropped, it is implicitly rolled +/// back. +/// +/// The transaction holds a mutable borrow on the `Dataset` that it was created from, so during the +/// lifetime of the transaction you will need to access the dataset by dereferencing the +/// `Transaction` through its [`Deref`] or [`DerefMut`] implementations. +#[derive(Debug)] +pub struct Transaction<'a> { + dataset: &'a mut Dataset, + rollback_on_drop: bool, +} + +impl<'a> Transaction<'a> { + fn new(dataset: &'a mut Dataset) -> Self { + Transaction { + dataset, + rollback_on_drop: true, + } + } + + /// Returns a reference to the dataset from which this `Transaction` was created. + #[deprecated = "Transaction now implements Deref, so you can call Dataset methods on it directly. Use .deref() if you need a reference to the underlying Dataset."] + pub fn dataset(&self) -> &Dataset { + self.dataset + } + + /// Returns a mutable reference to the dataset from which this `Transaction` was created. + #[deprecated = "Transaction now implements DerefMut, so you can call Dataset methods on it directly. Use .deref_mut() if you need a mutable reference to the underlying Dataset."] + pub fn dataset_mut(&mut self) -> &mut Dataset { + self.dataset + } + + /// Commits this transaction. + /// + /// If the commit fails, will return [`OGRErr::OGRERR_FAILURE`]. + /// + /// Depending on drivers, this may or may not abort layer sequential readings that are active. + pub fn commit(mut self) -> Result<()> { + let rv = unsafe { gdal_sys::GDALDatasetCommitTransaction(self.dataset.c_dataset()) }; + self.rollback_on_drop = false; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "GDALDatasetCommitTransaction", + }); + } + Ok(()) + } + + /// Rolls back the dataset to its state before the start of this transaction. + /// + /// If the rollback fails, will return [`OGRErr::OGRERR_FAILURE`]. + pub fn rollback(mut self) -> Result<()> { + let rv = unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.dataset.c_dataset()) }; + self.rollback_on_drop = false; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "GDALDatasetRollbackTransaction", + }); + } + Ok(()) + } +} + +impl<'a> Deref for Transaction<'a> { + type Target = Dataset; + + fn deref(&self) -> &Self::Target { + self.dataset + } +} + +impl<'a> DerefMut for Transaction<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dataset + } +} + +impl<'a> Drop for Transaction<'a> { + fn drop(&mut self) { + if self.rollback_on_drop { + // We silently swallow any errors, because we have no way to report them from a drop + // function apart from panicking. + unsafe { gdal_sys::GDALDatasetRollbackTransaction(self.dataset.c_dataset()) }; + } + } +} + +impl Dataset { + /// For datasources which support transactions, this creates a transaction. + /// + /// Because the transaction implements `DerefMut`, it can be used in place of the original + /// `Dataset` to make modifications. All changes done after the start of the transaction are + /// applied to the datasource when [`commit`](Transaction::commit) is called. They may be + /// canceled by calling [`rollback`](Transaction::rollback) instead, or by dropping the + /// `Transaction` without calling `commit`. + /// + /// Depending on the driver, using a transaction can give a huge performance improvement when + /// creating a lot of geometry at once. This is because the driver doesn't need to commit every + /// feature to disk individually. + /// + /// If starting the transaction fails, this function will return [`OGRErr::OGRERR_FAILURE`]. + /// For datasources that do not support transactions, this function will always return + /// [`OGRErr::OGRERR_UNSUPPORTED_OPERATION`]. + /// + /// Limitations: + /// + /// * Datasources which do not support efficient transactions natively may use less efficient + /// emulation of transactions instead; as of GDAL 3.1, this only applies to the closed-source + /// FileGDB driver, which (unlike OpenFileGDB) is not available in a GDAL build by default. + /// + /// * At the time of writing, transactions only apply on vector layers. + /// + /// * Nested transactions are not supported. + /// + /// * If an error occurs after a successful `start_transaction`, the whole transaction may or + /// may not be implicitly canceled, depending on the driver. For example, the PG driver will + /// cancel it, but the SQLite and GPKG drivers will not. + /// + /// Example: + /// + /// ``` + /// # use gdal::{Dataset }; + /// # use gdal::vector::LayerAccess; + /// use gdal::vector::LayerOptions; + /// # + /// fn create_point_grid(dataset: &mut Dataset) -> gdal::errors::Result<()> { + /// use gdal::vector::Geometry; + /// + /// // Start the transaction. + /// let mut txn = dataset.start_transaction()?; + /// + /// let mut layer = txn.create_layer(LayerOptions { + /// name: "grid", + /// ty: gdal_sys::OGRwkbGeometryType::wkbPoint, + /// ..Default::default() + /// })?; + /// for y in 0..100 { + /// for x in 0..100 { + /// let wkt = format!("POINT ({} {})", x, y); + /// layer.create_feature(Geometry::from_wkt(&wkt)?)?; + /// } + /// } + /// + /// // We got through without errors. Commit the transaction and return. + /// txn.commit()?; + /// Ok(()) + /// } + /// # + /// # fn main() -> gdal::errors::Result<()> { + /// # let driver = gdal::DriverManager::get_driver_by_name("SQLite")?; + /// # let mut dataset = driver.create_vector_only(":memory:")?; + /// # create_point_grid(&mut dataset)?; + /// # assert_eq!(dataset.layer(0)?.features().count(), 10000); + /// # Ok(()) + /// # } + /// ``` + pub fn start_transaction(&mut self) -> Result> { + let force = 1; + let rv = unsafe { gdal_sys::GDALDatasetStartTransaction(self.c_dataset(), force) }; + if rv != OGRErr::OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "GDALDatasetStartTransaction", + }); + } + Ok(Transaction::new(self)) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::{fixture, open_gpkg_for_update}; + use crate::vector::{Geometry, LayerAccess}; + use crate::Dataset; + + fn polygon() -> Geometry { + Geometry::from_wkt("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))").unwrap() + } + + #[test] + fn test_start_transaction() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let txn = ds.start_transaction(); + assert!(txn.is_ok()); + } + + #[test] + fn test_transaction_commit() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + let txn = ds.start_transaction().unwrap(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + assert!(txn.commit().is_ok()); + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count + 1); + } + + #[test] + fn test_transaction_rollback() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + let txn = ds.start_transaction().unwrap(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + assert!(txn.rollback().is_ok()); + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count); + } + + #[test] + fn test_transaction_implicit_rollback() { + let (_temp_path, mut ds) = open_gpkg_for_update(&fixture("poly.gpkg")); + let orig_feature_count = ds.layer(0).unwrap().feature_count(); + + { + let txn = ds.start_transaction().unwrap(); + let mut layer = txn.layer(0).unwrap(); + layer.create_feature(polygon()).unwrap(); + } // txn is dropped here. + + assert_eq!(ds.layer(0).unwrap().feature_count(), orig_feature_count); + } + + #[test] + fn test_start_transaction_unsupported() { + let mut ds = Dataset::open(fixture("roads.geojson")).unwrap(); + assert!(ds.start_transaction().is_err()); + } +}