Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wrappers for GDALBuildVRT, GDALApplyGeoTransform, GDALInvGeoTransform #239

Merged
merged 20 commits into from Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Add `raster_programs::build_vrt`
amartin96 marked this conversation as resolved.
Show resolved Hide resolved
- Add `apply_geo_transform`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines need to be removed

- Add `inv_geo_transform`

- <https://github.com/georust/gdal/pull/239>

## 0.11

- Remove the `datetime` feature
Expand Down
38 changes: 38 additions & 0 deletions src/lib.rs
Expand Up @@ -30,6 +30,7 @@ pub mod errors;
mod gdal_major_object;
mod metadata;
pub mod raster;
pub mod raster_programs;
pub mod spatial_ref;
mod utils;
pub mod vector;
Expand All @@ -42,6 +43,43 @@ pub use dataset::{
pub use driver::Driver;
pub use metadata::Metadata;

/// Apply GeoTransform to x/y coordinate.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GeoTransform relevant methods might be better placed inside the dataset or the rasters module. It is anyway only relevant to rasters IIUC.

/// Wraps [GDALApplyGeoTransform].
///
/// [GDALApplyGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv421GDALApplyGeoTransformPdddPdPd
pub fn apply_geo_transform(geo_transform: &GeoTransform, pixel: f64, line: f64) -> (f64, f64) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more ergonomic to throw these inside a trait that is impl by GeoTransform so we get the nicer:

let (wx, wy) = geo_t.apply(pix, line);
let inv_t = geo_t.invert()?;

Optional suggestion.

let mut geo_x: f64 = 0.;
let mut geo_y: f64 = 0.;
unsafe {
gdal_sys::GDALApplyGeoTransform(
geo_transform.as_ptr() as *mut f64,
pixel,
line,
&mut geo_x,
&mut geo_y,
);
}
(geo_x, geo_y)
}

/// Invert Geotransform.
/// Wraps [GDALInvGeoTransform].
///
/// [GDALInvGeoTransform]: https://gdal.org/api/raster_c_api.html#_CPPv419GDALInvGeoTransformPdPd
pub fn invert_geo_transform(geo_transform: &GeoTransform) -> errors::Result<GeoTransform> {
let mut gt_out: GeoTransform = Default::default();
unsafe {
if gdal_sys::GDALInvGeoTransform(geo_transform.as_ptr() as *mut f64, gt_out.as_mut_ptr())
== 0
{
return Err(errors::GdalError::BadArgument(
"Geo transform is uninvertible".to_string(),
));
}
}
Ok(gt_out)
}

#[cfg(test)]
fn assert_almost_eq(a: f64, b: f64) {
let f: f64 = a / b;
Expand Down
107 changes: 107 additions & 0 deletions src/raster_programs.rs
@@ -0,0 +1,107 @@
use crate::{errors::*, utils::_last_null_pointer_err, Dataset};
use gdal_sys::GDALBuildVRTOptions;
use libc::{c_char, c_int};
use std::{
borrow::Borrow,
ffi::CString,
path::Path,
ptr::{null, null_mut},
};

/// Wraps a [GDALBuildVRTOptions] object.
///
/// [GDALBuildVRTOptions]: https://gdal.org/api/gdal_utils.html#_CPPv419GDALBuildVRTOptions
struct BuildVRTOptions {
c_options: *mut GDALBuildVRTOptions,
}

impl BuildVRTOptions {
/// See [GDALBuildVRTOptionsNew].
///
/// [GDALBuildVRTOptionsNew]: https://gdal.org/api/gdal_utils.html#_CPPv422GDALBuildVRTOptionsNewPPcP28GDALBuildVRTOptionsForBinary
fn new(args: Vec<String>) -> Result<Self> {
Copy link
Contributor

@rmanoka rmanoka Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does args have to be a Vec<String>? Wouldn't any IntoIterator<Item = ...> work? This would avoid allocations if it's a fixed sized array.

Going one more step, does the inner type have to be String or anything that impls TryInto<CString> (like Vec<U8> or Vec<NonZeroU8>, etc.)?

We can leave it as-is if this makes the API too complicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted to implement this as follows:

pub fn new<S: TryInto<CString>, I: IntoIterator<Item = S>>(args: I) -> Result<Self> {
    let cstr_args = args.into_iter().map(|x| x.try_into()).collect::<std::result::Result<Vec<CString>, _>>()?;

However, I do not know how to convert TryInto<CString>::Error to GdalError::FfiNulError.

Can you show me how its done? Is the rest of it as you intended?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good pt.; we can't give a good error for arbitrary conversion failures, and the user could anyway map as req. before passing it. How about:

fn new<S: Into<Vec<u8>>, I: IntoIterator<Item = S>>(args: I) -> Result<Self> {
    let c_args = args
        .into_iter()
        .map(|x| CString::new(x))
        .collect::<std::result::Result<Vec<CString>, _>>()?;
    ....
}

This is inline with the rest of the lib, where we expect &str to pass CString to the FFI. The above seems to accept both &str, and String, so that should be good.

// Convert args to CStrings to add terminating null bytes
let mut cstr_args = Vec::<CString>::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested, but:

let cstr_args = args.into_iter()
    .map(|arg| CString::new(arg))
    .collect::<Result<Vec<_>>>()?;

This looks nicer and (as a minor point) pre-allocates the destination Vec.

for arg in args {
cstr_args.push(CString::new(arg)?);
}
// Get pointers to the strings
// These strings don't actually get modified, the C API is just not const-correct
// Null-terminate the list
let mut c_args = cstr_args
.iter()
.map(|x| x.as_ptr() as *mut c_char)
.chain(std::iter::once(null_mut()))
.collect::<Vec<_>>();
unsafe {
Ok(Self {
c_options: gdal_sys::GDALBuildVRTOptionsNew(c_args.as_mut_ptr(), null_mut()),
})
}
}
}

impl Drop for BuildVRTOptions {
fn drop(&mut self) {
unsafe {
gdal_sys::GDALBuildVRTOptionsFree(self.c_options);
}
}
}

/// Build a VRT from a list of datasets.
/// Wraps [GDALBuildVRT].
/// See the [program docs] for more details.
///
/// [GDALBuildVRT]: https://gdal.org/api/gdal_utils.html#gdal__utils_8h_1a057aaea8b0ed0476809a781ffa377ea4
/// [program docs]: https://gdal.org/programs/gdalbuildvrt.html
pub fn build_vrt<D: Borrow<Dataset>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose the BuildVRTOptions struct, and let this method take an Option<BuildVrtOptions> instead of a Vec<String> ? Seems more similar to the C api, and also a bit more natural.

dest: Option<&Path>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for missing this. I guess dest: Option<P> ... where P: AsRef<Path> would be nicer, so you can call it with strings.

datasets: &[D],
args: Vec<String>,
) -> Result<Dataset> {
lnicola marked this conversation as resolved.
Show resolved Hide resolved
_build_vrt(
dest,
&datasets
.iter()
.map(|x| x.borrow())
.collect::<Vec<&Dataset>>(),
args,
)
}

fn _build_vrt(dest: Option<&Path>, datasets: &[&Dataset], args: Vec<String>) -> Result<Dataset> {
// Convert dest to CString
let dest = match dest {
Copy link
Member

@lnicola lnicola Jan 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not great, but let's use utils::_path_to_c_string here.

(untested)

let dest = dest.map(crate::_path_to_c_string).transpose()?;
let c_dest = dest.map(CString::as_ptr).unwrap_or(ptr::null());

Some(x) => Some(CString::new(x.to_string_lossy().to_string())?),
None => None,
};
let c_dest = match dest {
Some(x) => x.as_ptr(),
None => null(),
};

lnicola marked this conversation as resolved.
Show resolved Hide resolved
let result = unsafe {
let options = BuildVRTOptions::new(args)?;
// Get raw handles to the datasets
let mut datasets_raw: Vec<gdal_sys::GDALDatasetH> =
datasets.iter().map(|x| x.c_dataset()).collect();

let dataset_out = gdal_sys::GDALBuildVRT(
c_dest,
datasets_raw.len() as c_int,
datasets_raw.as_mut_ptr(),
null(),
options.c_options,
null_mut(),
);

if dataset_out.is_null() {
return Err(_last_null_pointer_err("GDALBuildVRT"));
}

Dataset::from_c_dataset(dataset_out)
};

Ok(result)
}