diff --git a/src/raster/buffer.rs b/src/raster/buffer.rs index db8947df..9542bffd 100644 --- a/src/raster/buffer.rs +++ b/src/raster/buffer.rs @@ -6,6 +6,7 @@ use std::vec::IntoIter; #[cfg(feature = "ndarray")] use ndarray::Array2; +#[cfg_attr(not(feature = "ndarray"), allow(rustdoc::broken_intra_doc_links))] /// `Buffer` manages cell values in in raster I/O operations. /// /// It conceptually represents a 2-D array backed by a `Vec` with row-major organization diff --git a/src/raster/warp/mod.rs b/src/raster/warp/mod.rs index 413f3a04..cce886ce 100644 --- a/src/raster/warp/mod.rs +++ b/src/raster/warp/mod.rs @@ -43,7 +43,7 @@ pub fn create_and_reproject>( let dst_wkt = CString::new(dst_srs.to_wkt()?)?; // Format the source projection, if specified. let src_wkt = options - .src_projection() + .src_spatial_ref() .map(|s| s.to_wkt()) .transpose()? .map(CString::new) @@ -82,7 +82,8 @@ pub fn create_and_reproject>( } // See https://lists.osgeo.org/pipermail/gdal-dev/2023-November/057887.html for - // why this is required. To get around it We should rewrite this function to use the lower + // why this is required. To get around it We should rewrite this function to use the + // lower-level `GDALWarp` API. if options.dst_nodata().is_some() { let ds = Dataset::open(dst_file)?; for b in 1..=ds.raster_count() { @@ -98,10 +99,10 @@ pub fn create_and_reproject>( /// Reproject one dataset into another dataset. /// -/// Assumes destination dataset is properly sized and setup with a [`SpatialRef`][crate::SpatialRef], -/// [`GeoTransform`][crate::GeoTransform], [`Rasterband`][crate::Rasterband], etc. +/// Assumes destination dataset is properly sized and setup with a [`SpatialRef`], +/// [`GeoTransform`][crate::GeoTransform], [`RasterBand`][crate::raster::RasterBand], etc. /// -/// See [`reproject_options`] for a more flexible alternative. +/// See [`create_and_reproject`] for a more flexible alternative. pub fn reproject_into( src: &Dataset, dst: &mut Dataset, @@ -109,7 +110,7 @@ pub fn reproject_into( ) -> Result<()> { // Format the source projection, if specified. let src_wkt = options - .src_projection() + .src_spatial_ref() .map(|s| s.to_wkt()) .transpose()? .map(CString::new) @@ -118,7 +119,7 @@ pub fn reproject_into( // Format the destination projection, if specified. let dst_wkt = options - .src_projection() + .src_spatial_ref() .map(|s| s.to_wkt()) .transpose()? .map(CString::new) @@ -150,7 +151,8 @@ pub fn reproject_into( } // See https://lists.osgeo.org/pipermail/gdal-dev/2023-November/057887.html for - // why this is required. To get around it We should rewrite this function to use the lower + // why this is required. To get around it We should rewrite this function to use the + // lower-level `GDALWarp` API. if options.dst_nodata().is_some() { for b in 1..=dst.raster_count() { let mut rb = dst.rasterband(b)?; @@ -185,6 +187,7 @@ mod tests { opts.with_output_format("GTiff") .with_dst_nodata(255.0) .warp_options_mut() + .with_initial_value(InitValue::NoData) .with_resampling_alg(WarpResampleAlg::NearestNeighbour); create_and_reproject(&ds, &dest, &dst_srs, &opts)?; @@ -228,6 +231,7 @@ mod tests { let mut opts = ReprojectIntoOptions::default(); opts.with_dst_nodata(255.0) .warp_options_mut() + .with_initial_value(InitValue::NoData) .with_resampling_alg(WarpResampleAlg::NearestNeighbour); reproject_into(&source_ds, &mut dest_ds, &opts)?; diff --git a/src/raster/warp/reproject_options.rs b/src/raster/warp/reproject_options.rs index cf6794c0..885a5c4a 100644 --- a/src/raster/warp/reproject_options.rs +++ b/src/raster/warp/reproject_options.rs @@ -3,7 +3,7 @@ use crate::raster::warp::GdalWarpOptions; use crate::spatial_ref::SpatialRef; /// Injects methods associated with specifying warp no-data values. -macro_rules! common_nodata_methods { +macro_rules! nodata_accessors { () => { /// Specify the source no-data value. /// @@ -32,8 +32,14 @@ macro_rules! common_nodata_methods { } /// Injects methods around [`GdalWarpOptions`]. -macro_rules! common_warp_methods { +macro_rules! warp_options_accessors { () => { + /// Set the general Warp options. + pub fn with_warp_options(&mut self, warp_options: GdalWarpOptions) -> &mut Self { + self.warp_options = warp_options; + self + } + /// Fetch an immutable reference to the general Warp options. pub fn warp_options(&self) -> &GdalWarpOptions { &self.warp_options @@ -79,7 +85,26 @@ macro_rules! common_warp_methods { }; } -/// Settings for [`create_and_reproject`]. +macro_rules! src_sr_accessors { + () => { + /// Set the source spatial reference system. + /// + /// If not specified here, the source [`SpatialRef`] is read from the source dataset. + /// + /// If specified here, any [`SpatialRef`] in the source dataset is overridden. + pub fn with_src_spatial_ref(&mut self, srs: SpatialRef) -> &mut Self { + self.src_srs = Some(srs); + self + } + + /// Fetch the source spatial reference system, if set. + pub fn src_spatial_ref(&self) -> Option<&SpatialRef> { + self.src_srs.as_ref() + } + }; +} + +/// Settings for [`create_and_reproject`][super::create_and_reproject]. #[derive(Debug, Clone, Default)] pub struct ReprojectOptions { warp_options: GdalWarpOptions, @@ -113,20 +138,6 @@ impl ReprojectOptions { self.max_error } - /// Specify the source projection. - /// - /// If unset, the source projection is read from the source dataset. - /// If set, the source projection os overridden. - pub fn with_src_projection(&mut self, srs: &SpatialRef) -> &mut Self { - self.src_srs = Some(srs.clone()); - self - } - - /// Fetch the specified source projection, if any. - pub fn src_projection(&self) -> Option<&SpatialRef> { - self.src_srs.as_ref() - } - /// Explicitly specify output raster format. /// /// This is equivalent to the `-of ` CLI flag accepted by many GDAL tools. @@ -157,11 +168,12 @@ impl ReprojectOptions { self.output_format.clone() } - common_nodata_methods!(); - common_warp_methods!(); + src_sr_accessors!(); + nodata_accessors!(); + warp_options_accessors!(); } -/// Settings for [`reproject_into`]. +/// Settings for [`reproject_into`][super::reproject_into]. #[derive(Debug, Clone, Default)] pub struct ReprojectIntoOptions { warp_options: GdalWarpOptions, @@ -195,34 +207,22 @@ impl ReprojectIntoOptions { self.max_error } - /// Specify the source projection. + /// Set the destination spatial reference system. /// - /// If unset, the source projection is read from the source dataset. - /// If set, the source projection os overridden. - pub fn with_src_projection(&mut self, srs: &SpatialRef) -> &mut Self { - self.src_srs = Some(srs.clone()); - self - } - - /// Fetch the specified source projection, if any. - pub fn src_projection(&self) -> Option<&SpatialRef> { - self.src_srs.as_ref() - } - - /// Specify the destination projection. + /// If not specified here, the source [`SpatialRef`] is read from the destination dataset. /// - /// If unset, the destination projection is read from the destination dataset. - /// If set, the destination projection os overridden. - pub fn with_dst_projection(&mut self, srs: &SpatialRef) -> &mut Self { - self.dst_srs = Some(srs.clone()); + /// If specified here, any [`SpatialRef`] in the destination dataset is overridden. + pub fn with_dst_spatial_ref(&mut self, srs: SpatialRef) -> &mut Self { + self.dst_srs = Some(srs); self } - /// Fetch the specified destination projection, if any. - pub fn dst_projection(&self) -> Option<&SpatialRef> { + /// Fetch the destination spatial reference system, if set. + pub fn dst_spatial_ref(&self) -> Option<&SpatialRef> { self.dst_srs.as_ref() } - common_nodata_methods!(); - common_warp_methods!(); + src_sr_accessors!(); + nodata_accessors!(); + warp_options_accessors!(); } diff --git a/src/raster/warp/resample.rs b/src/raster/warp/resample.rs index 6ac260b6..df46df91 100644 --- a/src/raster/warp/resample.rs +++ b/src/raster/warp/resample.rs @@ -43,6 +43,7 @@ impl WarpResampleAlg { pub fn to_gdal(self) -> GDALResampleAlg::Type { self as GDALResampleAlg::Type } + pub fn from_gdal(alg: GDALResampleAlg::Type) -> Result { Ok(match alg { GDALResampleAlg::GRA_NearestNeighbour => WarpResampleAlg::NearestNeighbour, diff --git a/src/raster/warp/warp_options.rs b/src/raster/warp/warp_options.rs index d00638e4..b70a6b90 100644 --- a/src/raster/warp/warp_options.rs +++ b/src/raster/warp/warp_options.rs @@ -1,5 +1,4 @@ use std::fmt::{Debug, Display, Formatter}; -use std::mem::transmute; use std::ptr::NonNull; use crate::cpl::CslStringList; @@ -70,6 +69,34 @@ impl GdalWarpOptions { } } + /// This option is a convenience method for setting `INIT_DEST` setting via [`extra_options_mut`]. + /// + /// It forces the destination image to be initialized to the indicated value (for all bands), + /// or indicates that it should be initialized to the band's no-data value. + /// + /// If this value isn't set the destination image will be read and overlaid. + /// + /// See: [`GDALWarpOptions::papszWarpOptions](https://gdal.org/api/gdalwarp_cpp.html#_CPPv4N15GDALWarpOptions16papszWarpOptionsE) + pub fn with_initial_value(&mut self, init: InitValue) -> &mut Self { + self.extra_options_mut() + .set_name_value("INIT_DEST", &init.to_string()) + .expect("Failed to set INIT_DEST"); // We can unwrap because we know the strings are valid. + self + } + + /// Fetch the initial value setting, if any. + /// + /// See [`with_initial_value`][Self::with_initial_value] for details. + pub fn initial_value(&self) -> Option { + let init = self.extra_options().fetch_name_value("INIT_DEST"); + + match init.as_deref() { + Some("NO_DATA") => Some(InitValue::NoData), + Some(s) => s.parse::().ok().map(InitValue::Value), + None => None, + } + } + /// If the working data type is unknown, this method will determine a valid working /// data type to support the data in the src and dest data sets and any noData values. pub fn with_auto_working_datatype(&mut self) -> &mut Self { @@ -110,12 +137,13 @@ impl GdalWarpOptions { /// Sets the source Dataset no-data value. Internal use only. /// - /// Specifying a no-data value for GDAL Warp requires it be specified for every band. - /// This method facilitates delaying the specification of a homogeneous no-data value + /// This method exists to facilitate delaying the specification of a homogeneous no-data value /// until the number of bands is known (at the point of warp call), via `with_band_count`, - /// which initializes the band mapping. - /// Returns an `Err(GdalError::UnexpectedLogicError(...))` - /// if `with_band_count` has yet to be called with a specific value. + /// which initializes the band mapping, as required by GDALWarp. + /// + /// Therefore, the caller of this method is responsible for ensuring that a non-zero number of bands has been + /// first set via [`with_band_count`][Self::with_band_count]. If not, this method will return + /// `Err(GdalError::UnexpectedLogicError(...))` pub(super) fn apply_src_nodata(&mut self, no_data_value: f64) -> Result<&mut Self> { if self.band_count() == 0 { return Err(GdalError::UnexpectedLogicError( @@ -132,7 +160,7 @@ impl GdalWarpOptions { /// Sets the destination Dataset no-data value. Internal use only. /// - /// See [`apply_src_nodata`] for additional details. + /// See [`Self::apply_src_nodata`] for additional details. pub(super) fn apply_dst_nodata(&mut self, no_data_value: f64) -> Result<&mut Self> { if self.band_count() == 0 { return Err(GdalError::UnexpectedLogicError( @@ -144,11 +172,6 @@ impl GdalWarpOptions { // https://github.com/OSGeo/gdal/blob/a9635785a2db8f575328326f2b1833e743ec8828/alg/gdalwarper.cpp#L1295 unsafe { GDALWarpInitDstNoDataReal(self.as_ptr_mut(), no_data_value) }; - // This ensures the destination cells are initialized with no-data - // See: https://gdal.org/api/gdalwarp_cpp.html#_CPPv4N15GDALWarpOptions16papszWarpOptionsE - self.extra_options_mut() - .set_name_value("INIT_DEST", "NO_DATA")?; - Ok(self) } @@ -158,7 +181,7 @@ impl GdalWarpOptions { // Proof that GDALWarpOptions owns the CslStringList, and we just need to wrap it: // https://github.com/OSGeo/gdal/blob/a9635785a2db8f575328326f2b1833e743ec8828/alg/gdalwarper.cpp#L1290 // `CslStringList` is `transparent` with a single field, so this should be ok. - unsafe { transmute(opts_array) } + unsafe { std::mem::transmute(opts_array) } } /// Get a mutable reference to extra options attached to the Warp options. @@ -216,31 +239,53 @@ impl Default for GdalWarpOptions { } } -impl Display for GdalWarpOptions { +impl Debug for GdalWarpOptions { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let xml = self.to_xml().map_err(|_| std::fmt::Error)?; - Display::fmt(&xml, f) + Debug::fmt(&xml, f) } } -impl Debug for GdalWarpOptions { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let xml = self.to_xml().map_err(|_| std::fmt::Error)?; - Debug::fmt(&xml, f) +/// Specifies the initial value cells in the destination dataset during a warp operation. +/// +/// See [`GdalWarpOptions::with_initial_value`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum InitValue { + NoData, + Value(f64), +} + +impl From for InitValue { + fn from(v: f64) -> Self { + InitValue::Value(v) + } +} + +impl Display for InitValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + InitValue::NoData => "NO_DATA".to_string(), + InitValue::Value(v) => v.to_string(), + }; + write!(f, "{}", str) } } #[cfg(test)] mod tests { use crate::errors::Result; - use crate::raster::warp::resample::WarpResampleAlg; - use crate::raster::warp::GdalWarpOptions; + use crate::raster::warp::*; use crate::raster::GdalDataType; + fn ops_str(ops: &GdalWarpOptions) -> String { + let xml = ops.to_xml().unwrap(); + xml.to_string() + } + #[test] fn defaults() { let opts = GdalWarpOptions::default(); - assert!(opts.to_string().contains("NearestNeighbour")); + assert!(ops_str(&opts).contains("NearestNeighbour")); } #[test] @@ -269,7 +314,7 @@ mod tests { "#; - assert_eq!(opts.to_string(), expected); + assert_eq!(ops_str(&opts), expected); Ok(()) } @@ -281,19 +326,31 @@ mod tests { opts.with_band_count(3); assert_eq!(opts.band_count(), 3); - assert!(!opts.to_string().contains("")); + assert!(!ops_str(&opts).contains("")); opts.apply_src_nodata(255.0)?; - assert!(opts - .to_string() - .contains("255")); + assert!(ops_str(&opts).contains("255")); opts.apply_dst_nodata(0.0)?; - assert!(opts - .to_string() - .contains("0")); + assert!(ops_str(&opts).contains("0")); + + Ok(()) + } + + #[test] + fn init_value() -> Result<()> { + let rendered = |v: &str| format!(""); + + let mut opts = GdalWarpOptions::default(); + assert_eq!(opts.initial_value(), None); + + opts.with_initial_value(InitValue::NoData); + assert!(ops_str(&opts).contains(&rendered("NO_DATA"))); + + opts.with_initial_value(InitValue::Value(255.0)); + assert!(ops_str(&opts).contains(&rendered("255"))); Ok(()) } diff --git a/src/xml.rs b/src/xml.rs index bb4a0531..ade2af64 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -47,8 +47,8 @@ impl FromStr for GdalXmlNode { type Err = crate::errors::GdalError; fn from_str(s: &str) -> Result { - let s2 = CString::new(s)?; - let c_xml = unsafe { CPLParseXMLString(s2.as_ptr()) }; + let s = CString::new(s)?; + let c_xml = unsafe { CPLParseXMLString(s.as_ptr()) }; if c_xml.is_null() { Err(_last_cpl_err(CPLErr::CE_Failure)) } else {