diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs new file mode 100644 index 000000000..e7cc6384e --- /dev/null +++ b/c/sedona-gdal/src/dataset.rs @@ -0,0 +1,471 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::ffi::{CStr, CString}; +use std::ptr; + +use crate::cpl::CslStringList; +use crate::driver::Driver; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::raster::rasterband::RasterBand; +use crate::raster::types::{DatasetOptions, GdalDataType as RustGdalDataType}; +use crate::spatial_ref::SpatialRef; +use crate::vector::layer::Layer; + +/// A GDAL dataset. +pub struct Dataset { + api: &'static GdalApi, + c_dataset: GDALDatasetH, + owned: bool, +} + +unsafe impl Send for Dataset {} + +impl Drop for Dataset { + fn drop(&mut self) { + if self.owned && !self.c_dataset.is_null() { + unsafe { call_gdal_api!(self.api, GDALClose, self.c_dataset) }; + } + } +} + +impl Dataset { + /// Open a dataset with extended options. + pub fn open_ex( + api: &'static GdalApi, + path: &str, + open_flags: GDALOpenFlags, + allowed_drivers: Option<&[&str]>, + open_options: Option<&[&str]>, + sibling_files: Option<&[&str]>, + ) -> Result { + let c_path = CString::new(path)?; + + // Build CslStringLists from Option<&[&str]>. + // None → null pointer (use GDAL default). + // Some(&[]) → pointer to [null] (explicitly empty list). + let drivers_csl = allowed_drivers + .map(|v| CslStringList::try_from_iter(v.iter().copied())) + .transpose()?; + let options_csl = open_options + .map(|v| CslStringList::try_from_iter(v.iter().copied())) + .transpose()?; + let siblings_csl = sibling_files + .map(|v| CslStringList::try_from_iter(v.iter().copied())) + .transpose()?; + + let c_dataset = unsafe { + call_gdal_api!( + api, + GDALOpenEx, + c_path.as_ptr(), + open_flags, + drivers_csl + .as_ref() + .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const _), + options_csl + .as_ref() + .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const _), + siblings_csl + .as_ref() + .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const _) + ) + }; + + if c_dataset.is_null() { + return Err(api.last_cpl_err(CE_Failure as u32)); + } + + Ok(Self { + api, + c_dataset, + owned: true, + }) + } + + /// Create a new owned Dataset from a C handle. + pub(crate) fn new_owned(api: &'static GdalApi, c_dataset: GDALDatasetH) -> Self { + Self { + api, + c_dataset, + owned: true, + } + } + + /// Wrap an existing C dataset handle (non-owning). + /// + /// # Safety + /// + /// The caller must ensure the handle is valid and outlives this `Dataset`. + pub unsafe fn from_c_dataset(api: &'static GdalApi, c_dataset: GDALDatasetH) -> Self { + Self { + api, + c_dataset, + owned: false, + } + } + + /// Return the raw C dataset handle. + pub fn c_dataset(&self) -> GDALDatasetH { + self.c_dataset + } + + /// Return raster size as (x_size, y_size). + pub fn raster_size(&self) -> (usize, usize) { + let x = unsafe { call_gdal_api!(self.api, GDALGetRasterXSize, self.c_dataset) }; + let y = unsafe { call_gdal_api!(self.api, GDALGetRasterYSize, self.c_dataset) }; + (x as usize, y as usize) + } + + /// Return the number of raster bands. + pub fn raster_count(&self) -> usize { + unsafe { call_gdal_api!(self.api, GDALGetRasterCount, self.c_dataset) as usize } + } + + /// Get a raster band (1-indexed). + pub fn rasterband(&self, band_index: usize) -> Result> { + let band_index_i32 = i32::try_from(band_index)?; + let c_band = + unsafe { call_gdal_api!(self.api, GDALGetRasterBand, self.c_dataset, band_index_i32) }; + if c_band.is_null() { + return Err(GdalError::NullPointer { + method_name: "GDALGetRasterBand", + msg: format!("band index {band_index}"), + }); + } + Ok(RasterBand::new(self.api, c_band, self)) + } + + /// Get the geo-transform. + pub fn geo_transform(&self) -> Result<[f64; 6]> { + let mut gt = [0.0f64; 6]; + let rv = unsafe { + call_gdal_api!( + self.api, + GDALGetGeoTransform, + self.c_dataset, + gt.as_mut_ptr() + ) + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(gt) + } + + /// Set the geo-transform. + pub fn set_geo_transform(&self, gt: &[f64; 6]) -> Result<()> { + let rv = unsafe { + call_gdal_api!( + self.api, + GDALSetGeoTransform, + self.c_dataset, + gt.as_ptr() as *mut f64 + ) + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Get the projection string. + pub fn projection(&self) -> String { + unsafe { + let ptr = call_gdal_api!(self.api, GDALGetProjectionRef, self.c_dataset); + if ptr.is_null() { + String::new() + } else { + CStr::from_ptr(ptr).to_string_lossy().into_owned() + } + } + } + + /// Set the projection string. + pub fn set_projection(&self, projection: &str) -> Result<()> { + let c_projection = CString::new(projection)?; + let rv = unsafe { + call_gdal_api!( + self.api, + GDALSetProjection, + self.c_dataset, + c_projection.as_ptr() + ) + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Get the spatial reference. + pub fn spatial_ref(&self) -> Result { + let c_srs = unsafe { call_gdal_api!(self.api, GDALGetSpatialRef, self.c_dataset) }; + if c_srs.is_null() { + return Err(GdalError::NullPointer { + method_name: "GDALGetSpatialRef", + msg: "returned null".to_string(), + }); + } + // GDALGetSpatialRef returns a borrowed reference — clone it via OSRClone. + unsafe { SpatialRef::from_c_srs_clone(self.api, c_srs) } + } + + /// Set the spatial reference. + pub fn set_spatial_ref(&self, srs: &SpatialRef) -> Result<()> { + let rv = + unsafe { call_gdal_api!(self.api, GDALSetSpatialRef, self.c_dataset, srs.c_srs()) }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Create a copy of this dataset to a new file using the given driver. + pub fn create_copy( + &self, + driver: &Driver, + filename: &str, + options: &[&str], + ) -> Result { + let c_filename = CString::new(filename)?; + let csl = CslStringList::try_from_iter(options.iter().copied())?; + + let c_ds = unsafe { + call_gdal_api!( + self.api, + GDALCreateCopy, + driver.c_driver(), + c_filename.as_ptr(), + self.c_dataset, + 0, // bStrict + csl.as_ptr(), + ptr::null_mut(), + ptr::null_mut() + ) + }; + if c_ds.is_null() { + return Err(self.api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset { + api: self.api, + c_dataset: c_ds, + owned: true, + }) + } + + /// Create a new vector layer. + pub fn create_layer(&self, options: LayerOptions<'_>) -> Result> { + let c_name = CString::new(options.name)?; + let c_srs = options.srs.map_or(ptr::null_mut(), |s| s.c_srs()); + + let csl = CslStringList::try_from_iter(options.options.unwrap_or(&[]).iter().copied())?; + + let c_layer = unsafe { + call_gdal_api!( + self.api, + GDALDatasetCreateLayer, + self.c_dataset, + c_name.as_ptr(), + c_srs, + options.ty, + csl.as_ptr() + ) + }; + if c_layer.is_null() { + return Err(GdalError::NullPointer { + method_name: "GDALDatasetCreateLayer", + msg: format!("failed to create layer '{}'", options.name), + }); + } + Ok(Layer::new(self.api, c_layer, self)) + } + + /// Get the GDAL API reference. + pub fn api(&self) -> &'static GdalApi { + self.api + } + + /// Open a dataset using a `DatasetOptions` struct (georust-compatible convenience). + pub fn open_ex_with_options( + api: &'static GdalApi, + path: &str, + options: DatasetOptions<'_>, + ) -> Result { + Self::open_ex( + api, + path, + options.open_flags, + options.allowed_drivers, + options.open_options, + options.sibling_files, + ) + } + + /// Add a raster band backed by an existing memory buffer (zero-copy). + /// + /// This wraps `GDALAddBand` with the `DATAPOINTER`, `PIXELOFFSET`, and `LINEOFFSET` + /// options, allowing you to attach existing memory to a MEM dataset without copying. + /// + /// # Arguments + /// * `data_type` - The GDAL data type of the band. + /// * `data_ptr` - Pointer to the band pixel data. + /// * `pixel_offset` - Byte offset between consecutive pixels. `None` defaults to the + /// byte size of `data_type`. + /// * `line_offset` - Byte offset between consecutive lines. `None` defaults to + /// `pixel_offset * width`. + /// + /// # Safety + /// + /// The caller must ensure that `data_ptr` points to a valid buffer of at least + /// `height * line_offset` bytes (or `height * width * data_type.byte_size()` when + /// using defaults), and that the buffer outlives this dataset. + pub unsafe fn add_band_with_data( + &self, + data_type: RustGdalDataType, + data_ptr: *const u8, + pixel_offset: Option, + line_offset: Option, + ) -> Result<()> { + let data_pointer = format!("DATAPOINTER={data_ptr:p}"); + + let mut options = CslStringList::with_capacity(3); + options.add_string(&data_pointer)?; + + if let Some(pixel) = pixel_offset { + options.set_name_value("PIXELOFFSET", &pixel.to_string())?; + } + + if let Some(line) = line_offset { + options.set_name_value("LINEOFFSET", &line.to_string())?; + } + + let err = call_gdal_api!( + self.api, + GDALAddBand, + self.c_dataset, + data_type.to_c(), + options.as_ptr() + ); + if err != CE_None { + return Err(self.api.last_cpl_err(err as u32)); + } + Ok(()) + } + + /// Mark this dataset as owning its handle (for `Drop`). + pub fn set_owned(&mut self, owned: bool) { + self.owned = owned; + } +} + +/// Options for creating a vector layer. +pub struct LayerOptions<'a> { + pub name: &'a str, + pub srs: Option<&'a SpatialRef>, + pub ty: OGRwkbGeometryType, + /// Additional driver-specific options, in the form `"name=value"`. + pub options: Option<&'a [&'a str]>, +} + +impl Default for LayerOptions<'_> { + fn default() -> Self { + Self { + name: "", + srs: None, + ty: OGRwkbGeometryType::wkbUnknown, + options: None, + } + } +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::driver::DriverManager; + use crate::global::with_global_gdal_api; + + #[test] + fn test_geo_transform_roundtrip() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create("", 256, 256, 1).unwrap(); + + let gt = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0]; + ds.set_geo_transform(>).unwrap(); + let got = ds.geo_transform().unwrap(); + assert_eq!(gt, got); + }) + .unwrap(); + } + + #[test] + fn test_geo_transform_unset() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create("", 256, 256, 1).unwrap(); + + // MEM driver without an explicit set_geo_transform returns an error + assert!(ds.geo_transform().is_err()); + }) + .unwrap(); + } + + #[test] + fn test_set_projection_roundtrip() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create("", 256, 256, 1).unwrap(); + + let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#; + ds.set_projection(wkt).unwrap(); + let got = ds.projection(); + // The returned WKT may be reformatted by GDAL, so just check it contains WGS 84 + assert!(got.contains("WGS 84"), "Expected WGS 84 in: {got}"); + }) + .unwrap(); + } + + #[test] + fn test_dataset_raster_count() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + + let ds1 = driver.create("", 64, 64, 1).unwrap(); + assert_eq!(ds1.raster_count(), 1); + + let ds3 = driver.create("", 64, 64, 3).unwrap(); + assert_eq!(ds3.raster_count(), 3); + }) + .unwrap(); + } + + #[test] + fn test_dataset_raster_size() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create("", 123, 456, 1).unwrap(); + assert_eq!(ds.raster_size(), (123, 456)); + }) + .unwrap(); + } +} diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs new file mode 100644 index 000000000..f0e6c5963 --- /dev/null +++ b/c/sedona-gdal/src/driver.rs @@ -0,0 +1,246 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::ffi::CString; +use std::ptr; + +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::raster::types::GdalDataType as RustGdalDataType; +use crate::raster::types::GdalType; + +/// A GDAL driver. +pub struct Driver { + api: &'static GdalApi, + c_driver: GDALDriverH, +} + +impl Driver { + /// Wrap an existing C driver handle. + /// + /// # Safety + /// + /// The caller must ensure the handle is valid. + pub unsafe fn from_c_driver(api: &'static GdalApi, c_driver: GDALDriverH) -> Self { + Self { api, c_driver } + } + + /// Return the raw C driver handle. + pub fn c_driver(&self) -> GDALDriverH { + self.c_driver + } + + /// Create a new raster dataset (with u8 band type). + pub fn create( + &self, + filename: &str, + size_x: usize, + size_y: usize, + bands: usize, + ) -> Result { + self.create_with_band_type::(filename, size_x, size_y, bands) + } + + /// Create a new raster dataset with a specific band type. + pub fn create_with_band_type( + &self, + filename: &str, + size_x: usize, + size_y: usize, + bands: usize, + ) -> Result { + let c_filename = CString::new(filename)?; + let x: i32 = size_x.try_into()?; + let y: i32 = size_y.try_into()?; + let b: i32 = bands.try_into()?; + let c_ds = unsafe { + call_gdal_api!( + self.api, + GDALCreate, + self.c_driver, + c_filename.as_ptr(), + x, + y, + b, + T::gdal_ordinal(), + ptr::null_mut() + ) + }; + if c_ds.is_null() { + return Err(self.api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset::new_owned(self.api, c_ds)) + } + + /// Create a new raster dataset with a runtime data type. + /// + /// Unlike [`create_with_band_type`](Self::create_with_band_type), this accepts a + /// [`GdalDataType`](RustGdalDataType) enum value instead of a compile-time generic, + /// which is useful when the data type is only known at runtime. + pub fn create_with_data_type( + &self, + filename: &str, + size_x: usize, + size_y: usize, + bands: usize, + data_type: RustGdalDataType, + ) -> Result { + let c_filename = CString::new(filename)?; + let x: i32 = size_x.try_into()?; + let y: i32 = size_y.try_into()?; + let b: i32 = bands.try_into()?; + let c_ds = unsafe { + call_gdal_api!( + self.api, + GDALCreate, + self.c_driver, + c_filename.as_ptr(), + x, + y, + b, + data_type.to_c(), + ptr::null_mut() + ) + }; + if c_ds.is_null() { + return Err(self.api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset::new_owned(self.api, c_ds)) + } + + /// Create a new dataset (vector-only, no raster bands). + pub fn create_vector_only(&self, filename: &str) -> Result { + let c_filename = CString::new(filename)?; + let c_ds = unsafe { + call_gdal_api!( + self.api, + GDALCreate, + self.c_driver, + c_filename.as_ptr(), + 1, + 1, + 0, + GDALDataType::GDT_Unknown, + ptr::null_mut() + ) + }; + if c_ds.is_null() { + return Err(self.api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset::new_owned(self.api, c_ds)) + } +} + +/// Driver manager for looking up drivers by name. +pub struct DriverManager; + +impl DriverManager { + pub fn get_driver_by_name(api: &'static GdalApi, name: &str) -> Result { + let c_name = CString::new(name)?; + let c_driver = unsafe { call_gdal_api!(api, GDALGetDriverByName, c_name.as_ptr()) }; + if c_driver.is_null() { + // `GDALGetDriverByName` just returns `null` and sets no error message + return Err(GdalError::NullPointer { + method_name: "GDALGetDriverByName", + msg: format!("driver '{name}' not found"), + }); + } + Ok(Driver { api, c_driver }) + } +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::driver::DriverManager; + use crate::errors::GdalError; + use crate::global::with_global_gdal_api; + use crate::raster::types::GdalDataType; + + #[test] + fn test_get_driver_by_name() { + with_global_gdal_api(|api| { + let gtiff = DriverManager::get_driver_by_name(api, "GTiff").unwrap(); + assert!(!gtiff.c_driver().is_null()); + let mem = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + assert!(!mem.c_driver().is_null()); + }) + .unwrap(); + } + + #[test] + fn test_get_driver_by_name_invalid() { + with_global_gdal_api(|api| { + let err = DriverManager::get_driver_by_name(api, "NO_SUCH_DRIVER"); + assert!(matches!(err, Err(GdalError::NullPointer { .. }))); + }) + .unwrap(); + } + + #[test] + fn test_driver_create() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create("", 32, 16, 2).unwrap(); + assert_eq!(ds.raster_size(), (32, 16)); + assert_eq!(ds.raster_count(), 2); + }) + .unwrap(); + } + + #[test] + fn test_driver_create_with_band_type() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create_with_band_type::("", 10, 20, 1).unwrap(); + assert_eq!(ds.raster_count(), 1); + let ds = driver.create_with_band_type::("", 10, 20, 2).unwrap(); + assert_eq!(ds.raster_count(), 2); + let ds = driver.create_with_band_type::("", 10, 20, 3).unwrap(); + assert_eq!(ds.raster_count(), 3); + }) + .unwrap(); + } + + #[test] + fn test_driver_create_with_data_type() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver + .create_with_data_type("", 8, 8, 1, GdalDataType::UInt16) + .unwrap(); + assert_eq!(ds.raster_count(), 1); + }) + .unwrap(); + } + + #[test] + fn test_driver_create_vector_only() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let ds = driver.create_vector_only("").unwrap(); + assert_eq!(ds.raster_count(), 0); + assert_eq!(ds.raster_size(), (1, 1)); + }) + .unwrap(); + } +} diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs index 6f2ad1a0f..3ff5d03db 100644 --- a/c/sedona-gdal/src/errors.rs +++ b/c/sedona-gdal/src/errors.rs @@ -20,6 +20,7 @@ //! Original code is licensed under MIT. use std::ffi::NulError; +use std::num::TryFromIntError; use thiserror::Error; @@ -45,8 +46,26 @@ pub enum GdalError { #[error("Bad argument: {0}")] BadArgument(String), + #[error("GDAL method '{method_name}' returned a NULL pointer. Error msg: '{msg}'")] + NullPointer { + method_name: &'static str, + msg: String, + }, + + #[error("OGR method '{method_name}' returned error: '{err:?}'")] + OgrError { err: i32, method_name: &'static str }, + + #[error("Unable to unlink mem file: {file_name}")] + UnlinkMemFile { file_name: String }, + #[error("FFI NUL error: {0}")] FfiNulError(#[from] NulError), + + #[error(transparent)] + IntConversionError(#[from] TryFromIntError), + + #[error("Buffer length {0} does not match raster size {1:?}")] + BufferSizeMismatch(usize, (usize, usize)), } pub type Result = std::result::Result; diff --git a/c/sedona-gdal/src/geo_transform.rs b/c/sedona-gdal/src/geo_transform.rs new file mode 100644 index 000000000..5504e8501 --- /dev/null +++ b/c/sedona-gdal/src/geo_transform.rs @@ -0,0 +1,152 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. +//! +//! GeoTransform type and extension trait. +//! +//! The [`apply`](GeoTransformEx::apply) and [`invert`](GeoTransformEx::invert) +//! methods are pure-Rust reimplementations of GDAL's `GDALApplyGeoTransform` +//! and `GDALInvGeoTransform` (from `alg/gdaltransformer.cpp`). No FFI call or +//! thread-local state is needed. + +use crate::errors; +use crate::errors::GdalError; + +/// An affine geo-transform: six coefficients mapping pixel/line to projection coordinates. +/// +/// - `[0]`: x-coordinate of the upper-left corner of the upper-left pixel. +/// - `[1]`: W-E pixel resolution (pixel width). +/// - `[2]`: row rotation (typically zero). +/// - `[3]`: y-coordinate of the upper-left corner of the upper-left pixel. +/// - `[4]`: column rotation (typically zero). +/// - `[5]`: N-S pixel resolution (pixel height, negative for North-up). +pub type GeoTransform = [f64; 6]; + +/// Extension methods on [`GeoTransform`]. +pub trait GeoTransformEx { + /// Apply the geo-transform to a pixel/line coordinate, returning (geo_x, geo_y). + fn apply(&self, x: f64, y: f64) -> (f64, f64); + + /// Invert this geo-transform, returning the inverse coefficients for + /// computing (geo_x, geo_y) -> (x, y) transformations. + fn invert(&self) -> errors::Result; +} + +impl GeoTransformEx for GeoTransform { + /// Pure-Rust equivalent of GDAL's `GDALApplyGeoTransform`. + fn apply(&self, x: f64, y: f64) -> (f64, f64) { + let geo_x = self[0] + x * self[1] + y * self[2]; + let geo_y = self[3] + x * self[4] + y * self[5]; + (geo_x, geo_y) + } + + /// Pure-Rust equivalent of GDAL's `GDALInvGeoTransform`. + fn invert(&self) -> errors::Result { + let gt = self; + + // Fast path: no rotation/skew — avoid determinant and precision issues. + if gt[2] == 0.0 && gt[4] == 0.0 && gt[1] != 0.0 && gt[5] != 0.0 { + return Ok([ + -gt[0] / gt[1], + 1.0 / gt[1], + 0.0, + -gt[3] / gt[5], + 0.0, + 1.0 / gt[5], + ]); + } + + // General case: 2x2 matrix inverse via adjugate / determinant. + let det = gt[1] * gt[5] - gt[2] * gt[4]; + let magnitude = gt[1] + .abs() + .max(gt[2].abs()) + .max(gt[4].abs().max(gt[5].abs())); + + if det.abs() <= 1e-10 * magnitude * magnitude { + return Err(GdalError::BadArgument( + "Geo transform is uninvertible".to_string(), + )); + } + + let inv_det = 1.0 / det; + + Ok([ + (gt[2] * gt[3] - gt[0] * gt[5]) * inv_det, + gt[5] * inv_det, + -gt[2] * inv_det, + (-gt[1] * gt[3] + gt[0] * gt[4]) * inv_det, + -gt[4] * inv_det, + gt[1] * inv_det, + ]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_apply_no_rotation() { + // Origin at (100, 200), 10m pixels, north-up + let gt: GeoTransform = [100.0, 10.0, 0.0, 200.0, 0.0, -10.0]; + let (x, y) = gt.apply(5.0, 3.0); + assert!((x - 150.0).abs() < 1e-12); + assert!((y - 170.0).abs() < 1e-12); + } + + #[test] + fn test_apply_with_rotation() { + let gt: GeoTransform = [100.0, 10.0, 2.0, 200.0, 3.0, -10.0]; + let (x, y) = gt.apply(5.0, 3.0); + // 100 + 5*10 + 3*2 = 156 + assert!((x - 156.0).abs() < 1e-12); + // 200 + 5*3 + 3*(-10) = 185 + assert!((y - 185.0).abs() < 1e-12); + } + + #[test] + fn test_invert_no_rotation() { + let gt: GeoTransform = [100.0, 10.0, 0.0, 200.0, 0.0, -10.0]; + let inv = gt.invert().unwrap(); + // Round-trip: apply then apply inverse should recover pixel/line. + let (geo_x, geo_y) = gt.apply(7.0, 4.0); + let (px, ln) = inv.apply(geo_x, geo_y); + assert!((px - 7.0).abs() < 1e-10); + assert!((ln - 4.0).abs() < 1e-10); + } + + #[test] + fn test_invert_with_rotation() { + let gt: GeoTransform = [100.0, 10.0, 2.0, 200.0, 3.0, -10.0]; + let inv = gt.invert().unwrap(); + let (geo_x, geo_y) = gt.apply(7.0, 4.0); + let (px, ln) = inv.apply(geo_x, geo_y); + assert!((px - 7.0).abs() < 1e-10); + assert!((ln - 4.0).abs() < 1e-10); + } + + #[test] + fn test_invert_singular() { + // Determinant is zero: both rows are proportional. + let gt: GeoTransform = [0.0, 1.0, 2.0, 0.0, 2.0, 4.0]; + assert!(gt.invert().is_err()); + } +} diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs index b64a2275c..c331f5dfa 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -29,4 +29,10 @@ pub mod global; // --- High-level wrappers --- pub mod config; pub mod cpl; +pub mod dataset; +pub mod driver; +pub mod geo_transform; pub mod raster; +pub mod spatial_ref; +pub mod vector; +pub mod vsi; diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs index 1ddc9b2ed..389d9d73b 100644 --- a/c/sedona-gdal/src/raster.rs +++ b/c/sedona-gdal/src/raster.rs @@ -15,4 +15,5 @@ // specific language governing permissions and limitations // under the License. +pub mod rasterband; pub mod types; diff --git a/c/sedona-gdal/src/raster/rasterband.rs b/c/sedona-gdal/src/raster/rasterband.rs new file mode 100644 index 000000000..12f17616e --- /dev/null +++ b/c/sedona-gdal/src/raster/rasterband.rs @@ -0,0 +1,368 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::marker::PhantomData; + +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::raster::types::{Buffer, GdalType, ResampleAlg}; +use crate::{gdal_dyn_bindgen::*, raster::types::GdalDataType}; + +/// A raster band of a dataset. +pub struct RasterBand<'a> { + api: &'static GdalApi, + c_rasterband: GDALRasterBandH, + _dataset: PhantomData<&'a Dataset>, +} + +impl<'a> RasterBand<'a> { + pub(crate) fn new( + api: &'static GdalApi, + c_rasterband: GDALRasterBandH, + _dataset: &'a Dataset, + ) -> Self { + Self { + api, + c_rasterband, + _dataset: PhantomData, + } + } + + /// Return the raw C raster band handle. + pub fn c_rasterband(&self) -> GDALRasterBandH { + self.c_rasterband + } + + /// Read a region of the band as a typed buffer. + /// + /// If `e_resample_alg` is `None`, nearest-neighbour resampling is used. + pub fn read_as( + &self, + window: (isize, isize), + window_size: (usize, usize), + size: (usize, usize), + e_resample_alg: Option, + ) -> Result> { + let len = size.0 * size.1; + // Safety: all GdalType implementations are numeric primitives (u8, i8, u16, ..., f64), + // for which zeroed memory is a valid bit pattern. + let mut data: Vec = vec![unsafe { std::mem::zeroed() }; len]; + + let resample_alg = e_resample_alg.unwrap_or(ResampleAlg::NearestNeighbour); + let mut extra_arg = GDALRasterIOExtraArg { + eResampleAlg: resample_alg.to_gdal(), + ..GDALRasterIOExtraArg::default() + }; + + let rv = unsafe { + call_gdal_api!( + self.api, + GDALRasterIOEx, + self.c_rasterband, + GF_Read, + i32::try_from(window.0)?, + i32::try_from(window.1)?, + i32::try_from(window_size.0)?, + i32::try_from(window_size.1)?, + data.as_mut_ptr() as *mut std::ffi::c_void, + i32::try_from(size.0)?, + i32::try_from(size.1)?, + T::gdal_ordinal(), + 0, // nPixelSpace (auto) + 0, // nLineSpace (auto) + &mut extra_arg + ) + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + + Ok(Buffer::new(size, data)) + } + + /// Write a buffer to this raster band. + pub fn write( + &self, + window: (isize, isize), + window_size: (usize, usize), + buffer: &mut Buffer, + ) -> Result<()> { + let expected_len = buffer.shape.0 * buffer.shape.1; + if buffer.data.len() != expected_len { + return Err(GdalError::BufferSizeMismatch( + buffer.data.len(), + buffer.shape, + )); + } + let rv = unsafe { + call_gdal_api!( + self.api, + GDALRasterIO, + self.c_rasterband, + GF_Write, + i32::try_from(window.0)?, + i32::try_from(window.1)?, + i32::try_from(window_size.0)?, + i32::try_from(window_size.1)?, + buffer.data.as_mut_ptr() as *mut std::ffi::c_void, + i32::try_from(buffer.shape.0)?, + i32::try_from(buffer.shape.1)?, + T::gdal_ordinal(), + 0, // nPixelSpace (auto) + 0 // nLineSpace (auto) + ) + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Get the data type of this band. + pub fn band_type(&self) -> GdalDataType { + GdalDataType::from_c(self.c_band_type()).unwrap_or(GdalDataType::Unknown) + } + + /// Get the GDAL data type of this band. + pub fn c_band_type(&self) -> GDALDataType { + unsafe { call_gdal_api!(self.api, GDALGetRasterDataType, self.c_rasterband) } + } + + /// Get band size as (x_size, y_size). + pub fn size(&self) -> (usize, usize) { + let x = unsafe { call_gdal_api!(self.api, GDALGetRasterBandXSize, self.c_rasterband) }; + let y = unsafe { call_gdal_api!(self.api, GDALGetRasterBandYSize, self.c_rasterband) }; + (x as usize, y as usize) + } + + /// Get the block size as (x_size, y_size). + pub fn block_size(&self) -> (usize, usize) { + let mut x: i32 = 0; + let mut y: i32 = 0; + unsafe { + call_gdal_api!( + self.api, + GDALGetBlockSize, + self.c_rasterband, + &mut x, + &mut y + ) + }; + (x as usize, y as usize) + } + + /// Get the no-data value. Returns `Some(value)` if set, `None` otherwise. + pub fn no_data_value(&self) -> Option { + let mut success: i32 = 0; + let value = unsafe { + call_gdal_api!( + self.api, + GDALGetRasterNoDataValue, + self.c_rasterband, + &mut success + ) + }; + if success != 0 { + Some(value) + } else { + None + } + } + + /// Set or clear the no-data value. + pub fn set_no_data_value(&self, value: Option) -> Result<()> { + let rv = if let Some(val) = value { + unsafe { call_gdal_api!(self.api, GDALSetRasterNoDataValue, self.c_rasterband, val) } + } else { + unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, self.c_rasterband) } + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Set or clear the no-data value as u64. + pub fn set_no_data_value_u64(&self, value: Option) -> Result<()> { + let rv = if let Some(val) = value { + unsafe { + call_gdal_api!( + self.api, + GDALSetRasterNoDataValueAsUInt64, + self.c_rasterband, + val + ) + } + } else { + unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, self.c_rasterband) } + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Set or clear the no-data value as i64. + pub fn set_no_data_value_i64(&self, value: Option) -> Result<()> { + let rv = if let Some(val) = value { + unsafe { + call_gdal_api!( + self.api, + GDALSetRasterNoDataValueAsInt64, + self.c_rasterband, + val + ) + } + } else { + unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, self.c_rasterband) } + }; + if rv != CE_None { + return Err(self.api.last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Get the GDAL API reference. + pub fn api(&self) -> &'static GdalApi { + self.api + } +} + +/// Compute the actual block size (clamped to raster extent) for a given block index. +pub fn actual_block_size( + band: &RasterBand<'_>, + block_index: (usize, usize), +) -> Result<(usize, usize)> { + let (block_x, block_y) = band.block_size(); + let (raster_x, raster_y) = band.size(); + let x_off = block_index.0 * block_x; + let y_off = block_index.1 * block_y; + if x_off >= raster_x || y_off >= raster_y { + return Err(GdalError::BadArgument(format!( + "block index ({}, {}) is out of bounds for raster size ({}, {})", + block_index.0, block_index.1, raster_x, raster_y + ))); + } + let actual_x = if x_off + block_x > raster_x { + raster_x - x_off + } else { + block_x + }; + let actual_y = if y_off + block_y > raster_y { + raster_y - y_off + } else { + block_y + }; + Ok((actual_x, actual_y)) +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::dataset::Dataset; + use crate::driver::DriverManager; + use crate::gdal_dyn_bindgen::*; + use crate::global::with_global_gdal_api; + use crate::raster::types::ResampleAlg; + + fn fixture(name: &str) -> String { + sedona_testing::data::test_raster(name).unwrap() + } + + #[test] + fn test_read_raster() { + with_global_gdal_api(|api| { + let path = fixture("tinymarble.tif"); + let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, None, None).unwrap(); + let rb = dataset.rasterband(1).unwrap(); + let rv = rb.read_as::((20, 30), (2, 3), (2, 3), None).unwrap(); + assert_eq!(rv.shape, (2, 3)); + assert_eq!(rv.data(), [7, 7, 7, 10, 8, 12]); + }) + .unwrap(); + } + + #[test] + fn test_read_raster_with_default_resample() { + with_global_gdal_api(|api| { + let path = fixture("tinymarble.tif"); + let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, None, None).unwrap(); + let rb = dataset.rasterband(1).unwrap(); + let rv = rb.read_as::((20, 30), (4, 4), (2, 2), None).unwrap(); + assert_eq!(rv.shape, (2, 2)); + // Default is NearestNeighbour; exact values are GDAL-version-dependent + // when downsampling from 4x4 to 2x2. Just verify shape and non-emptiness. + assert_eq!(rv.data().len(), 4); + }) + .unwrap(); + } + + #[test] + fn test_read_raster_with_average_resample() { + with_global_gdal_api(|api| { + let path = fixture("tinymarble.tif"); + let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, None, None).unwrap(); + let rb = dataset.rasterband(1).unwrap(); + let rv = rb + .read_as::((20, 30), (4, 4), (2, 2), Some(ResampleAlg::Average)) + .unwrap(); + assert_eq!(rv.shape, (2, 2)); + // Average resampling; exact values are GDAL-version-dependent. + // Verify shape and that results differ from the non-resampled full read. + assert_eq!(rv.data().len(), 4); + }) + .unwrap(); + } + + #[test] + fn test_get_no_data_value() { + with_global_gdal_api(|api| { + // tinymarble.tif has no nodata + let path = fixture("tinymarble.tif"); + let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, None, None).unwrap(); + let rb = dataset.rasterband(1).unwrap(); + assert!(rb.no_data_value().is_none()); + + // labels.tif has nodata=255 + let path = fixture("labels.tif"); + let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, None, None).unwrap(); + let rb = dataset.rasterband(1).unwrap(); + assert_eq!(rb.no_data_value(), Some(255.0)); + }) + .unwrap(); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_set_no_data_value() { + with_global_gdal_api(|api| { + let driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let dataset = driver.create("", 20, 10, 1).unwrap(); + let rasterband = dataset.rasterband(1).unwrap(); + assert_eq!(rasterband.no_data_value(), None); + assert!(rasterband.set_no_data_value(Some(1.23)).is_ok()); + assert_eq!(rasterband.no_data_value(), Some(1.23)); + assert!(rasterband.set_no_data_value(None).is_ok()); + assert_eq!(rasterband.no_data_value(), None); + }) + .unwrap(); + } +} diff --git a/c/sedona-gdal/src/spatial_ref.rs b/c/sedona-gdal/src/spatial_ref.rs new file mode 100644 index 000000000..77559a0de --- /dev/null +++ b/c/sedona-gdal/src/spatial_ref.rs @@ -0,0 +1,156 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::ffi::{CStr, CString}; +use std::ptr; + +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::OGRERR_NONE; +use crate::gdal_dyn_bindgen::*; + +/// An OGR spatial reference system. +pub struct SpatialRef { + api: &'static GdalApi, + c_srs: OGRSpatialReferenceH, +} + +unsafe impl Send for SpatialRef {} + +impl Drop for SpatialRef { + fn drop(&mut self) { + if !self.c_srs.is_null() { + unsafe { call_gdal_api!(self.api, OSRRelease, self.c_srs) }; + } + } +} + +impl SpatialRef { + /// Create a new SpatialRef from a WKT string. + pub fn from_wkt(api: &'static GdalApi, wkt: &str) -> Result { + let c_wkt = CString::new(wkt)?; + let c_srs = unsafe { call_gdal_api!(api, OSRNewSpatialReference, c_wkt.as_ptr()) }; + if c_srs.is_null() { + return Err(GdalError::NullPointer { + method_name: "OSRNewSpatialReference", + msg: "failed to create spatial reference from WKT".to_string(), + }); + } + Ok(Self { api, c_srs }) + } + + /// Create a SpatialRef by cloning a borrowed C handle via `OSRClone`. + /// + /// # Safety + /// + /// The caller must ensure `c_srs` is a valid `OGRSpatialReferenceH`. + pub unsafe fn from_c_srs_clone( + api: &'static GdalApi, + c_srs: OGRSpatialReferenceH, + ) -> Result { + let cloned = call_gdal_api!(api, OSRClone, c_srs); + if cloned.is_null() { + return Err(GdalError::NullPointer { + method_name: "OSRClone", + msg: "failed to clone spatial reference".to_string(), + }); + } + Ok(Self { api, c_srs: cloned }) + } + + /// Return the raw C handle. + pub fn c_srs(&self) -> OGRSpatialReferenceH { + self.c_srs + } + + /// Export to PROJJSON string. + pub fn to_projjson(&self) -> Result { + unsafe { + let mut ptr: *mut std::os::raw::c_char = ptr::null_mut(); + let rv = call_gdal_api!( + self.api, + OSRExportToPROJJSON, + self.c_srs, + &mut ptr, + ptr::null() + ); + if rv != OGRERR_NONE || ptr.is_null() { + return Err(GdalError::NullPointer { + method_name: "OSRExportToPROJJSON", + msg: "returned null".to_string(), + }); + } + let result = CStr::from_ptr(ptr).to_string_lossy().into_owned(); + call_gdal_api!(self.api, VSIFree, ptr as *mut std::ffi::c_void); + Ok(result) + } + } +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::errors::GdalError; + use crate::global::with_global_gdal_api; + use crate::spatial_ref::SpatialRef; + + const WGS84_WKT: &str = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#; + + #[test] + fn test_from_wkt() { + with_global_gdal_api(|api| { + let srs = SpatialRef::from_wkt(api, WGS84_WKT).unwrap(); + assert!(!srs.c_srs().is_null()); + }) + .unwrap(); + } + + #[test] + fn test_from_wkt_invalid() { + with_global_gdal_api(|api| { + let err = SpatialRef::from_wkt(api, "WGS\u{0}84"); + assert!(matches!(err, Err(GdalError::FfiNulError(_)))); + }) + .unwrap(); + } + + #[test] + fn test_to_projjson() { + with_global_gdal_api(|api| { + let srs = SpatialRef::from_wkt(api, WGS84_WKT).unwrap(); + let projjson = srs.to_projjson().unwrap(); + assert!( + projjson.contains("WGS 84"), + "unexpected projjson: {projjson}" + ); + }) + .unwrap(); + } + + #[test] + fn test_from_c_srs_clone() { + with_global_gdal_api(|api| { + let srs = SpatialRef::from_wkt(api, WGS84_WKT).unwrap(); + let cloned = unsafe { SpatialRef::from_c_srs_clone(api, srs.c_srs()) }.unwrap(); + assert_eq!(srs.to_projjson().unwrap(), cloned.to_projjson().unwrap()); + }) + .unwrap(); + } +} diff --git a/c/sedona-gdal/src/vector.rs b/c/sedona-gdal/src/vector.rs new file mode 100644 index 000000000..52ff4e1bf --- /dev/null +++ b/c/sedona-gdal/src/vector.rs @@ -0,0 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub mod feature; +pub mod geometry; +pub mod layer; diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs new file mode 100644 index 000000000..b29813799 --- /dev/null +++ b/c/sedona-gdal/src/vector/feature.rs @@ -0,0 +1,217 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::ffi::CString; +use std::marker::PhantomData; + +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::vector::geometry::Envelope; + +/// An OGR feature. +pub struct Feature<'a> { + api: &'static GdalApi, + c_feature: OGRFeatureH, + _lifetime: PhantomData<&'a ()>, +} + +impl Drop for Feature<'_> { + fn drop(&mut self) { + if !self.c_feature.is_null() { + unsafe { call_gdal_api!(self.api, OGR_F_Destroy, self.c_feature) }; + } + } +} + +impl<'a> Feature<'a> { + pub(crate) fn new(api: &'static GdalApi, c_feature: OGRFeatureH) -> Self { + Self { + api, + c_feature, + _lifetime: PhantomData, + } + } + + /// Get the geometry reference (borrowed, not owned — do not destroy). + /// + /// Returns None if the feature has no geometry. + pub fn geometry(&self) -> Option> { + let c_geom = unsafe { call_gdal_api!(self.api, OGR_F_GetGeometryRef, self.c_feature) }; + if c_geom.is_null() { + None + } else { + Some(BorrowedGeometry { + api: self.api, + c_geom, + _lifetime: PhantomData, + }) + } + } + + /// Get a field's index by name. Returns an error if the field is not found. + pub fn field_index(&self, name: &str) -> Result { + let c_name = CString::new(name)?; + let idx = unsafe { + call_gdal_api!( + self.api, + OGR_F_GetFieldIndex, + self.c_feature, + c_name.as_ptr() + ) + }; + if idx < 0 { + return Err(GdalError::BadArgument(format!("field '{name}' not found"))); + } + Ok(idx) + } + + /// Get a field value as f64. + pub fn field_as_double(&self, field_index: i32) -> f64 { + unsafe { + call_gdal_api!( + self.api, + OGR_F_GetFieldAsDouble, + self.c_feature, + field_index + ) + } + } + + /// Get a field value as i32. + /// + /// Returns `Some(value)` if the field is set and not null, `None` otherwise. + pub fn field_as_integer(&self, field_index: i32) -> Option { + let is_set = unsafe { + call_gdal_api!( + self.api, + OGR_F_IsFieldSetAndNotNull, + self.c_feature, + field_index + ) + }; + if is_set != 0 { + Some(unsafe { + call_gdal_api!( + self.api, + OGR_F_GetFieldAsInteger, + self.c_feature, + field_index + ) + }) + } else { + None + } + } +} + +/// A geometry borrowed from a feature (not owned — will NOT be destroyed). +pub struct BorrowedGeometry<'a> { + api: &'static GdalApi, + c_geom: OGRGeometryH, + _lifetime: PhantomData<&'a ()>, +} + +impl<'a> BorrowedGeometry<'a> { + /// Return the raw C geometry handle. + pub fn c_geometry(&self) -> OGRGeometryH { + self.c_geom + } + + /// Export to ISO WKB. + pub fn wkb(&self) -> Result> { + let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize, self.c_geom) }; + if size < 0 { + return Err(GdalError::BadArgument(format!( + "OGR_G_WkbSize returned negative size: {size}" + ))); + } + let mut buf = vec![0u8; size as usize]; + let rv = unsafe { + call_gdal_api!( + self.api, + OGR_G_ExportToIsoWkb, + self.c_geom, + wkbNDR, + buf.as_mut_ptr() + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToIsoWkb", + }); + } + Ok(buf) + } + + /// Get the bounding envelope. + pub fn envelope(&self) -> Envelope { + let mut env = OGREnvelope { + MinX: 0.0, + MaxX: 0.0, + MinY: 0.0, + MaxY: 0.0, + }; + unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut env) }; + Envelope { + MinX: env.MinX, + MaxX: env.MaxX, + MinY: env.MinY, + MaxY: env.MaxY, + } + } +} + +/// An OGR field definition. +pub struct FieldDefn { + api: &'static GdalApi, + c_field_defn: OGRFieldDefnH, +} + +impl Drop for FieldDefn { + fn drop(&mut self) { + if !self.c_field_defn.is_null() { + unsafe { call_gdal_api!(self.api, OGR_Fld_Destroy, self.c_field_defn) }; + } + } +} + +impl FieldDefn { + /// Create a new field definition. + pub fn new(api: &'static GdalApi, name: &str, field_type: OGRFieldType) -> Result { + let c_name = CString::new(name)?; + let c_field_defn = + unsafe { call_gdal_api!(api, OGR_Fld_Create, c_name.as_ptr(), field_type) }; + if c_field_defn.is_null() { + return Err(GdalError::NullPointer { + method_name: "OGR_Fld_Create", + msg: format!("failed to create field definition '{name}'"), + }); + } + Ok(Self { api, c_field_defn }) + } + + /// Return the raw C handle. + pub fn c_field_defn(&self) -> OGRFieldDefnH { + self.c_field_defn + } +} diff --git a/c/sedona-gdal/src/vector/geometry.rs b/c/sedona-gdal/src/vector/geometry.rs new file mode 100644 index 000000000..aefc153a6 --- /dev/null +++ b/c/sedona-gdal/src/vector/geometry.rs @@ -0,0 +1,162 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::ffi::CString; +use std::ptr; + +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; + +/// An OGR geometry. +pub struct Geometry { + api: &'static GdalApi, + c_geom: OGRGeometryH, +} + +unsafe impl Send for Geometry {} + +impl Drop for Geometry { + fn drop(&mut self) { + if !self.c_geom.is_null() { + unsafe { call_gdal_api!(self.api, OGR_G_DestroyGeometry, self.c_geom) }; + } + } +} + +impl Geometry { + /// Create a geometry from WKB bytes. + pub fn from_wkb(api: &'static GdalApi, wkb: &[u8]) -> Result { + let wkb_len: i32 = wkb.len().try_into()?; + let mut c_geom: OGRGeometryH = ptr::null_mut(); + let rv = unsafe { + call_gdal_api!( + api, + OGR_G_CreateFromWkb, + wkb.as_ptr() as *const std::ffi::c_void, + ptr::null_mut(), // hSRS + &mut c_geom, + wkb_len + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkb", + }); + } + if c_geom.is_null() { + return Err(GdalError::NullPointer { + method_name: "OGR_G_CreateFromWkb", + msg: "returned null geometry".to_string(), + }); + } + Ok(Self { api, c_geom }) + } + + /// Create a geometry from WKT string. + pub fn from_wkt(api: &'static GdalApi, wkt: &str) -> Result { + let c_wkt = CString::new(wkt)?; + let mut wkt_ptr = c_wkt.as_ptr() as *mut std::os::raw::c_char; + let mut c_geom: OGRGeometryH = ptr::null_mut(); + let rv = unsafe { + call_gdal_api!( + api, + OGR_G_CreateFromWkt, + &mut wkt_ptr, + ptr::null_mut(), // hSRS + &mut c_geom + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkt", + }); + } + if c_geom.is_null() { + return Err(GdalError::NullPointer { + method_name: "OGR_G_CreateFromWkt", + msg: "returned null geometry".to_string(), + }); + } + Ok(Self { api, c_geom }) + } + + /// Return the raw C geometry handle. + pub fn c_geometry(&self) -> OGRGeometryH { + self.c_geom + } + + /// Get the bounding envelope. + pub fn envelope(&self) -> Envelope { + let mut env = OGREnvelope { + MinX: 0.0, + MaxX: 0.0, + MinY: 0.0, + MaxY: 0.0, + }; + unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut env) }; + Envelope { + MinX: env.MinX, + MaxX: env.MaxX, + MinY: env.MinY, + MaxY: env.MaxY, + } + } + + /// Export to ISO WKB. + pub fn wkb(&self) -> Result> { + let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize, self.c_geom) }; + if size < 0 { + return Err(GdalError::BadArgument(format!( + "OGR_G_WkbSize returned negative size: {size}" + ))); + } + let mut buf = vec![0u8; size as usize]; + let rv = unsafe { + call_gdal_api!( + self.api, + OGR_G_ExportToIsoWkb, + self.c_geom, + wkbNDR, // little-endian + buf.as_mut_ptr() + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToIsoWkb", + }); + } + Ok(buf) + } +} + +/// Bounding envelope. +#[derive(Debug, Clone, Copy)] +#[allow(non_snake_case)] +pub struct Envelope { + pub MinX: f64, + pub MaxX: f64, + pub MinY: f64, + pub MaxY: f64, +} diff --git a/c/sedona-gdal/src/vector/layer.rs b/c/sedona-gdal/src/vector/layer.rs new file mode 100644 index 000000000..0e23a4246 --- /dev/null +++ b/c/sedona-gdal/src/vector/layer.rs @@ -0,0 +1,118 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. + +use std::marker::PhantomData; + +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::vector::feature::{Feature, FieldDefn}; + +/// An OGR layer (borrowed from a Dataset). +pub struct Layer<'a> { + api: &'static GdalApi, + c_layer: OGRLayerH, + _dataset: PhantomData<&'a Dataset>, +} + +impl<'a> Layer<'a> { + pub(crate) fn new(api: &'static GdalApi, c_layer: OGRLayerH, _dataset: &'a Dataset) -> Self { + Self { + api, + c_layer, + _dataset: PhantomData, + } + } + + /// Return the raw C layer handle. + pub fn c_layer(&self) -> OGRLayerH { + self.c_layer + } + + /// Reset reading to the first feature. + pub fn reset_reading(&self) { + unsafe { call_gdal_api!(self.api, OGR_L_ResetReading, self.c_layer) }; + } + + /// Get the next feature (returns None when exhausted). + pub fn next_feature(&self) -> Option> { + let c_feature = unsafe { call_gdal_api!(self.api, OGR_L_GetNextFeature, self.c_layer) }; + if c_feature.is_null() { + None + } else { + Some(Feature::new(self.api, c_feature)) + } + } + + /// Create a field on this layer. + pub fn create_field(&self, field_defn: &FieldDefn) -> Result<()> { + let rv = unsafe { + call_gdal_api!( + self.api, + OGR_L_CreateField, + self.c_layer, + field_defn.c_field_defn(), + 1 // bApproxOK + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_L_CreateField", + }); + } + Ok(()) + } + + /// Get the number of features in this layer. + /// + /// If `force` is true, the count will be computed even if it is expensive. + pub fn feature_count(&self, force: bool) -> i64 { + unsafe { + call_gdal_api!( + self.api, + OGR_L_GetFeatureCount, + self.c_layer, + if force { 1 } else { 0 } + ) + } + } + + /// Iterate over all features. + pub fn features(&self) -> FeatureIterator<'_> { + self.reset_reading(); + FeatureIterator { layer: self } + } +} + +/// Iterator over features in a layer. +pub struct FeatureIterator<'a> { + layer: &'a Layer<'a>, +} + +impl<'a> Iterator for FeatureIterator<'a> { + type Item = Feature<'a>; + + fn next(&mut self) -> Option { + self.layer.next_feature() + } +} diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs new file mode 100644 index 000000000..7c4e93f2f --- /dev/null +++ b/c/sedona-gdal/src/vsi.rs @@ -0,0 +1,187 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Ported (and contains copied code) from georust/gdal: +//! . +//! Original code is licensed under MIT. +//! +//! GDAL Virtual File System (VSI) wrappers. + +use std::ffi::CString; + +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; + +/// Creates a new VSI in-memory file from a given buffer. +/// +/// The data is copied into GDAL-allocated memory (via `VSIMalloc`) so that +/// GDAL can safely free it with `VSIFree` when ownership is taken. +pub fn create_mem_file(api: &'static GdalApi, file_name: &str, data: Vec) -> Result<()> { + let c_file_name = CString::new(file_name)?; + let len = data.len(); + + // Allocate via GDAL's allocator so GDAL can safely free it. + let gdal_buf = unsafe { call_gdal_api!(api, VSIMalloc, len) } as *mut u8; + if gdal_buf.is_null() { + return Err(GdalError::NullPointer { + method_name: "VSIMalloc", + msg: format!("failed to allocate {len} bytes"), + }); + } + + // Copy data into GDAL-allocated buffer + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), gdal_buf, len); + } + // Rust Vec is dropped here, freeing the Rust-allocated memory. + + let handle = unsafe { + call_gdal_api!( + api, + VSIFileFromMemBuffer, + c_file_name.as_ptr(), + gdal_buf, + len as i64, + 1 // bTakeOwnership = true — GDAL will VSIFree gdal_buf + ) + }; + + if handle.is_null() { + // GDAL did not take ownership, so we must free. + unsafe { call_gdal_api!(api, VSIFree, gdal_buf as *mut std::ffi::c_void) }; + return Err(GdalError::NullPointer { + method_name: "VSIFileFromMemBuffer", + msg: String::new(), + }); + } + + unsafe { + call_gdal_api!(api, VSIFCloseL, handle); + } + + Ok(()) +} + +/// Unlink (delete) a VSI in-memory file. +pub fn unlink_mem_file(api: &'static GdalApi, file_name: &str) -> Result<()> { + let c_file_name = CString::new(file_name)?; + + let rv = unsafe { call_gdal_api!(api, VSIUnlink, c_file_name.as_ptr()) }; + + if rv != 0 { + return Err(GdalError::UnlinkMemFile { + file_name: file_name.to_string(), + }); + } + + Ok(()) +} + +/// Copies the bytes of the VSI in-memory file, taking ownership and freeing the GDAL memory. +pub fn get_vsi_mem_file_bytes_owned(api: &'static GdalApi, file_name: &str) -> Result> { + let c_file_name = CString::new(file_name)?; + + let owned_bytes = unsafe { + let mut length: i64 = 0; + let bytes = call_gdal_api!( + api, + VSIGetMemFileBuffer, + c_file_name.as_ptr(), + &mut length, + 1 // bUnlinkAndSeize = true + ); + + if bytes.is_null() { + return Err(GdalError::NullPointer { + method_name: "VSIGetMemFileBuffer", + msg: String::new(), + }); + } + + if length < 0 { + call_gdal_api!(api, VSIFree, bytes.cast::()); + return Err(GdalError::BadArgument(format!( + "VSIGetMemFileBuffer returned negative length: {length}" + ))); + } + + let slice = std::slice::from_raw_parts(bytes, length as usize); + let vec = slice.to_vec(); + + call_gdal_api!(api, VSIFree, bytes.cast::()); + + vec + }; + + Ok(owned_bytes) +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use super::*; + use crate::global::with_global_gdal_api; + + #[test] + fn create_and_retrieve_mem_file() { + let file_name = "/vsimem/525ebf24-a030-4677-bb4e-a921741cabe0"; + + with_global_gdal_api(|api| { + create_mem_file(api, file_name, vec![1_u8, 2, 3, 4]).unwrap(); + + let bytes = get_vsi_mem_file_bytes_owned(api, file_name).unwrap(); + + assert_eq!(bytes, vec![1_u8, 2, 3, 4]); + + // mem file must not be there anymore + assert!(matches!( + unlink_mem_file(api, file_name).unwrap_err(), + GdalError::UnlinkMemFile { + file_name: err_file_name + } + if err_file_name == file_name + )); + }) + .unwrap(); + } + + #[test] + fn create_and_unlink_mem_file() { + let file_name = "/vsimem/bbf5f1d6-c1e9-4469-a33b-02cd9173132d"; + + with_global_gdal_api(|api| { + create_mem_file(api, file_name, vec![1_u8, 2, 3, 4]).unwrap(); + + unlink_mem_file(api, file_name).unwrap(); + }) + .unwrap(); + } + + #[test] + fn no_mem_file() { + with_global_gdal_api(|api| { + assert!(matches!( + get_vsi_mem_file_bytes_owned(api, "foobar").unwrap_err(), + GdalError::NullPointer { + method_name: "VSIGetMemFileBuffer", + msg, + } + if msg.is_empty() + )); + }) + .unwrap(); + } +}