diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index 36832d866..966f67982 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -300,11 +300,11 @@ impl Dataset { /// /// # Safety /// - /// `data_ptr` must point to valid band data that outlives this dataset. + /// `data_ptr` must point to valid mutable band data that outlives this dataset. pub unsafe fn add_band_with_data( &self, data_type: RustGdalDataType, - data_ptr: *const u8, + data_ptr: *mut u8, pixel_offset: Option, line_offset: Option, ) -> Result<()> { diff --git a/c/sedona-gdal/src/gdal.rs b/c/sedona-gdal/src/gdal.rs new file mode 100644 index 000000000..e1afbd1f5 --- /dev/null +++ b/c/sedona-gdal/src/gdal.rs @@ -0,0 +1,237 @@ +// 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. + +//! High-level convenience wrapper around [`GdalApi`]. +//! +//! [`Gdal`] bundles a `&'static GdalApi` reference and exposes ergonomic +//! methods that delegate to the lower-level constructors and free functions +//! scattered across the crate, eliminating the need to pass `api` explicitly +//! at every call site. + +use crate::config; +use crate::dataset::Dataset; +use crate::driver::{Driver, DriverManager}; +use crate::errors::Result; +use crate::gdal_api::GdalApi; +use crate::gdal_dyn_bindgen::{GDALOpenFlags, OGRFieldType}; +use crate::mem::create_mem_dataset; +use crate::raster::polygonize::{polygonize, PolygonizeOptions}; +use crate::raster::rasterband::RasterBand; +use crate::raster::rasterize::{rasterize, RasterizeOptions}; +use crate::raster::rasterize_affine::rasterize_affine; +use crate::raster::types::DatasetOptions; +use crate::raster::types::GdalDataType; +use crate::spatial_ref::SpatialRef; +use crate::vector::feature::FieldDefn; +use crate::vector::geometry::Geometry; +use crate::vector::layer::Layer; +use crate::vrt::VrtDataset; +use crate::vsi; + +/// High-level convenience wrapper around [`GdalApi`]. +/// +/// Stores a `&'static GdalApi` reference and provides ergonomic methods that +/// delegate to the various constructors and free functions in the crate. +pub struct Gdal { + api: &'static GdalApi, +} + +impl Gdal { + /// Create a `Gdal` wrapper for the given API reference. + pub(crate) fn new(api: &'static GdalApi) -> Self { + Self { api } + } + + // -- Info ---------------------------------------------------------------- + + /// Return the name of the loaded GDAL library. + pub fn name(&self) -> &str { + self.api.name() + } + + /// Fetch GDAL version information for a standard request key. + pub fn version_info(&self, request: &str) -> String { + self.api.version_info(request) + } + + // -- Config -------------------------------------------------------------- + + /// Set a thread-local GDAL configuration option. + pub fn set_thread_local_config_option(&self, key: &str, value: &str) -> Result<()> { + config::set_thread_local_config_option(self.api, key, value) + } + + // -- Driver -------------------------------------------------------------- + + /// Fetch a GDAL driver by its short name. + pub fn get_driver_by_name(&self, name: &str) -> Result { + DriverManager::get_driver_by_name(self.api, name) + } + + // -- Dataset ------------------------------------------------------------- + + /// Open a dataset with extended options. + pub fn open_ex( + &self, + path: &str, + open_flags: GDALOpenFlags, + allowed_drivers: Option<&[&str]>, + open_options: Option<&[&str]>, + sibling_files: Option<&[&str]>, + ) -> Result { + Dataset::open_ex( + self.api, + path, + open_flags, + allowed_drivers, + open_options, + sibling_files, + ) + } + + /// Open a dataset using a [`DatasetOptions`] struct. + pub fn open_ex_with_options(&self, path: &str, options: DatasetOptions<'_>) -> Result { + Dataset::open_ex_with_options(self.api, path, options) + } + + // -- Spatial Reference --------------------------------------------------- + + /// Create a spatial reference from a WKT string. + pub fn spatial_ref_from_wkt(&self, wkt: &str) -> Result { + SpatialRef::from_wkt(self.api, wkt) + } + + // -- VRT ----------------------------------------------------------------- + + /// Create an empty VRT dataset with the given raster size. + pub fn create_vrt(&self, x_size: usize, y_size: usize) -> Result { + VrtDataset::create(self.api, x_size, y_size) + } + + // -- Geometry ------------------------------------------------------------ + + /// Create a geometry from WKB bytes. + pub fn geometry_from_wkb(&self, wkb: &[u8]) -> Result { + Geometry::from_wkb(self.api, wkb) + } + + /// Create a geometry from a WKT string. + pub fn geometry_from_wkt(&self, wkt: &str) -> Result { + Geometry::from_wkt(self.api, wkt) + } + + // -- Vector -------------------------------------------------------------- + + /// Create an OGR field definition. + pub fn create_field_defn(&self, name: &str, field_type: OGRFieldType) -> Result { + FieldDefn::new(self.api, name, field_type) + } + + // -- VSI (Virtual File System) ------------------------------------------- + + /// Create a VSI in-memory file from the given bytes. + pub fn create_mem_file(&self, file_name: &str, data: &[u8]) -> Result<()> { + vsi::create_mem_file(self.api, file_name, data) + } + + /// Delete a VSI in-memory file. + pub fn unlink_mem_file(&self, file_name: &str) -> Result<()> { + vsi::unlink_mem_file(self.api, file_name) + } + + /// Copy the bytes of a VSI in-memory file, taking ownership of the GDAL buffer. + pub fn get_vsi_mem_file_bytes_owned(&self, file_name: &str) -> Result> { + vsi::get_vsi_mem_file_bytes_owned(self.api, file_name) + } + + // -- Raster operations --------------------------------------------------- + + /// Create a bare in-memory MEM dataset with GDAL-owned bands. + /// For a higher-level builder with external bands and metadata, use `MemDatasetBuilder`. + pub fn create_mem_dataset( + &self, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Result { + create_mem_dataset( + self.api, + width, + height, + n_owned_bands, + owned_bands_data_type, + ) + } + + /// Rasterize geometries using the dataset geotransform as the transformer. + pub fn rasterize_affine( + &self, + dataset: &Dataset, + bands: &[usize], + geometries: &[Geometry], + burn_values: &[f64], + all_touched: bool, + ) -> Result<()> { + rasterize_affine( + self.api, + dataset, + bands, + geometries, + burn_values, + all_touched, + ) + } + + /// Rasterize geometries into the selected dataset bands. + pub fn rasterize( + &self, + dataset: &Dataset, + band_list: &[i32], + geometries: &[&Geometry], + burn_values: &[f64], + options: Option, + ) -> Result<()> { + rasterize( + self.api, + dataset, + band_list, + geometries, + burn_values, + options, + ) + } + + /// Polygonize a raster band into a vector layer. + pub fn polygonize( + &self, + src_band: &RasterBand<'_>, + mask_band: Option<&RasterBand<'_>>, + out_layer: &Layer<'_>, + pixel_value_field: i32, + options: &PolygonizeOptions, + ) -> Result<()> { + polygonize( + self.api, + src_band, + mask_band, + out_layer, + pixel_value_field, + options, + ) + } +} diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs index c4baa6b19..cef854d4c 100644 --- a/c/sedona-gdal/src/gdal_api.rs +++ b/c/sedona-gdal/src/gdal_api.rs @@ -33,6 +33,7 @@ use crate::gdal_dyn_bindgen::SedonaGdalApi; /// initialization of [`GdalApi`] via [`GdalApi::try_from_shared_library`] or /// [`GdalApi::try_from_current_process`], and you cannot obtain a `&GdalApi` /// without successful initialization. +#[macro_export] macro_rules! call_gdal_api { ($api:expr, $func:ident $(, $arg:expr)*) => { if let Some(func) = $api.inner.$func { diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs index 9365ef89a..5b9581163 100644 --- a/c/sedona-gdal/src/global.rs +++ b/c/sedona-gdal/src/global.rs @@ -16,6 +16,7 @@ // under the License. use crate::errors::GdalInitLibraryError; +use crate::gdal::Gdal; use crate::gdal_api::GdalApi; use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; @@ -203,6 +204,16 @@ where Ok(func(api)) } +/// Execute a closure with the process-global high-level [`Gdal`] handle. +/// The global API is initialized lazily on first use and then reused. +pub fn with_global_gdal(func: F) -> Result +where + F: FnOnce(&Gdal) -> R, +{ + let api = get_global_gdal_api()?; + Ok(func(&Gdal::new(api))) +} + /// Verify that the GDAL library meets the minimum version requirement. /// /// We use `GDALVersionInfo("VERSION_NUM")` instead of `GDALCheckVersion` because diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs index f61384515..6f8f70275 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -23,6 +23,7 @@ pub mod gdal_dyn_bindgen; pub mod errors; // --- Core API --- +pub mod gdal; pub mod gdal_api; pub mod global; @@ -32,6 +33,7 @@ pub mod cpl; pub mod dataset; pub mod driver; pub mod geo_transform; +pub mod mem; pub mod raster; pub mod spatial_ref; pub mod vector; diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs new file mode 100644 index 000000000..e86acbe23 --- /dev/null +++ b/c/sedona-gdal/src/mem.rs @@ -0,0 +1,441 @@ +// 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. + +//! High-level builder for creating in-memory (MEM) GDAL datasets. +//! +//! [`MemDatasetBuilder`] provides a fluent, type-safe API for constructing GDAL MEM +//! datasets with zero-copy band attachment, optional geo-transform, projection, and +//! per-band nodata values. + +use crate::dataset::Dataset; +use crate::errors::Result; +use crate::gdal::Gdal; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::CE_Failure; +use crate::raster::types::GdalDataType; + +/// Nodata value for a raster band. +/// +/// GDAL has three separate APIs for setting nodata depending on the band data type: +/// - [`f64`] for most types (UInt8 through Float64, excluding Int64/UInt64) +/// - [`i64`] for Int64 bands +/// - [`u64`] for UInt64 bands +/// +/// This enum encapsulates the three nodata value representations exposed by GDAL. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Nodata { + F64(f64), + I64(i64), + U64(u64), +} + +/// A band specification for [`MemDatasetBuilder`]. +struct MemBand { + data_type: GdalDataType, + data_ptr: *mut u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, +} + +/// A builder for constructing in-memory (MEM) GDAL datasets. +/// +/// This creates datasets using `MEMDataset::Create` (bypassing GDAL's open-dataset-list +/// mutex for better concurrency) and attaches bands via `GDALAddBand` with `DATAPOINTER` +/// options for zero-copy operation. +/// +/// # Safety +/// +/// All `add_band*` methods are `unsafe` because the caller must ensure that the +/// provided data pointers remain valid for the lifetime of the built [`Dataset`], +/// satisfy the alignment requirements of the band data type, and refer to writable +/// memory if GDAL may write through the attached `DATAPOINTER` band. +pub struct MemDatasetBuilder { + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: Option, + bands: Vec, + geo_transform: Option<[f64; 6]>, + projection: Option, +} + +impl MemDatasetBuilder { + /// Create a builder for a MEM dataset with the given dimensions. + pub fn new(width: usize, height: usize) -> Self { + Self { + width, + height, + n_owned_bands: 0, + owned_bands_data_type: None, + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a builder for a MEM dataset with GDAL-owned bands. + pub fn new_with_owned_bands( + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Self { + Self { + width, + height, + n_owned_bands, + owned_bands_data_type: Some(owned_bands_data_type), + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a MEM dataset with GDAL-owned bands. + /// This is a safe shortcut for `new_with_owned_bands(...).build(gdal)`. + pub fn create( + gdal: &Gdal, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Result { + // SAFETY: `new_with_owned_bands` creates a builder with zero external bands, + // so no data pointers need to outlive the dataset. + unsafe { + Self::new_with_owned_bands(width, height, n_owned_bands, owned_bands_data_type) + .build(gdal) + } + } + + /// Add a zero-copy band from a raw data pointer. + /// Use default contiguous row-major pixel and line offsets. + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of at least + /// `height * width * data_type.byte_size()` bytes, is properly aligned for + /// `data_type`, and outlives the built [`Dataset`]. + pub unsafe fn add_band(self, data_type: GdalDataType, data_ptr: *mut u8) -> Self { + self.add_band_with_options(data_type, data_ptr, None, None, None) + } + + /// Add a zero-copy band with custom offsets and optional nodata. + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of sufficient size + /// for the given dimensions and offsets, is properly aligned for `data_type`, and + /// outlives the built [`Dataset`]. + pub unsafe fn add_band_with_options( + mut self, + data_type: GdalDataType, + data_ptr: *mut u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, + ) -> Self { + self.bands.push(MemBand { + data_type, + data_ptr, + pixel_offset, + line_offset, + nodata, + }); + self + } + + /// Set the dataset geotransform coefficients. + pub fn geo_transform(mut self, gt: [f64; 6]) -> Self { + self.geo_transform = Some(gt); + self + } + + /// Set the dataset projection definition string. + pub fn projection(mut self, wkt: impl Into) -> Self { + self.projection = Some(wkt.into()); + self + } + + /// Build the MEM dataset and attach the configured bands and metadata. + /// + /// # Safety + /// + /// This method is unsafe because the built dataset references memory provided via + /// the `add_band*` methods. The caller must ensure all data pointers remain valid + /// for the lifetime of the returned [`Dataset`] and satisfy the alignment + /// requirements of their band data types. + pub unsafe fn build(self, gdal: &Gdal) -> Result { + let dataset = gdal.create_mem_dataset( + self.width, + self.height, + self.n_owned_bands, + self.owned_bands_data_type.unwrap_or(GdalDataType::UInt8), + )?; + + // Attach bands (zero-copy via DATAPOINTER). + for band_spec in &self.bands { + dataset.add_band_with_data( + band_spec.data_type, + band_spec.data_ptr, + band_spec.pixel_offset, + band_spec.line_offset, + )?; + } + + // Set geo-transform. + if let Some(gt) = &self.geo_transform { + dataset.set_geo_transform(gt)?; + } + + // Set projection/CRS. + if let Some(proj) = &self.projection { + dataset.set_projection(proj)?; + } + + // Set per-band nodata values. + for (i, band_spec) in self.bands.iter().enumerate() { + if let Some(nodata) = &band_spec.nodata { + let raster_band = dataset.rasterband(i + 1 + self.n_owned_bands)?; + match nodata { + Nodata::F64(v) => raster_band.set_no_data_value(Some(*v))?, + Nodata::I64(v) => raster_band.set_no_data_value_i64(Some(*v))?, + Nodata::U64(v) => raster_band.set_no_data_value_u64(Some(*v))?, + } + } + } + + Ok(dataset) + } +} + +/// Create a bare in-memory MEM dataset with GDAL-owned bands. +/// For a higher-level builder with external bands and metadata, use `MemDatasetBuilder`. +pub(crate) fn create_mem_dataset( + api: &'static GdalApi, + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, +) -> Result { + let empty_filename = c""; + let c_data_type = owned_bands_data_type.to_c(); + let handle = unsafe { + call_gdal_api!( + api, + MEMDatasetCreate, + empty_filename.as_ptr(), + width.try_into()?, + height.try_into()?, + n_owned_bands.try_into()?, + c_data_type, + std::ptr::null_mut() + ) + }; + + if handle.is_null() { + return Err(api.last_cpl_err(CE_Failure as u32)); + } + Ok(Dataset::new(api, handle)) +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::global::with_global_gdal; + use crate::mem::{MemDatasetBuilder, Nodata}; + use crate::raster::types::GdalDataType; + + #[test] + fn test_mem_builder_single_band() { + with_global_gdal(|gdal| { + let mut data = vec![42u8; 64 * 64]; + let dataset = unsafe { + MemDatasetBuilder::new(64, 64) + .add_band(GdalDataType::UInt8, data.as_mut_ptr()) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_size(), (64, 64)); + assert_eq!(dataset.raster_count(), 1); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_multi_band() { + with_global_gdal(|gdal| { + let mut band1 = vec![1u16; 32 * 32]; + let mut band2 = vec![2u16; 32 * 32]; + let mut band3 = vec![3u16; 32 * 32]; + let dataset = unsafe { + MemDatasetBuilder::new(32, 32) + .add_band(GdalDataType::UInt16, band1.as_mut_ptr() as *mut u8) + .add_band(GdalDataType::UInt16, band2.as_mut_ptr() as *mut u8) + .add_band(GdalDataType::UInt16, band3.as_mut_ptr() as *mut u8) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 3); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_geo_transform() { + with_global_gdal(|gdal| { + let mut data = vec![0f32; 10 * 10]; + let gt = [100.0, 0.5, 0.0, 200.0, 0.0, -0.5]; + let dataset = unsafe { + MemDatasetBuilder::new(10, 10) + .add_band(GdalDataType::Float32, data.as_mut_ptr() as *mut u8) + .geo_transform(gt) + .build(gdal) + .unwrap() + }; + let got = dataset.geo_transform().unwrap(); + assert_eq!(gt, got); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_projection() { + with_global_gdal(|gdal| { + let mut data = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, data.as_mut_ptr()) + .projection(r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#) + .build(gdal) + .unwrap() + }; + let proj = dataset.projection(); + assert!(proj.contains("WGS 84"), "Expected WGS 84 in: {proj}"); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_with_nodata() { + with_global_gdal(|gdal| { + let mut data = [0f64; 4 * 4]; + let dataset = unsafe { + MemDatasetBuilder::new(4, 4) + .add_band_with_options( + GdalDataType::Float64, + data.as_mut_ptr() as *mut u8, + None, + None, + Some(Nodata::F64(-9999.0)), + ) + .build(gdal) + .unwrap() + }; + let band = dataset.rasterband(1).unwrap(); + let nodata = band.no_data_value(); + assert_eq!(nodata, Some(-9999.0)); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_zero_bands() { + with_global_gdal(|gdal| { + let dataset = unsafe { MemDatasetBuilder::new(16, 16).build(gdal).unwrap() }; + assert_eq!(dataset.raster_count(), 0); + assert_eq!(dataset.raster_size(), (16, 16)); + }) + .unwrap(); + } + + #[test] + fn test_mem_builder_mixed_band_types() { + with_global_gdal(|gdal| { + let mut band_u8 = [0u8; 8 * 8]; + let mut band_f64 = vec![0f64; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, band_u8.as_mut_ptr()) + .add_band(GdalDataType::Float64, band_f64.as_mut_ptr() as *mut u8) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + }) + .unwrap(); + } + + #[test] + pub fn test_mem_builder_with_owned_bands() { + with_global_gdal(|gdal| { + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(16, 16, 2, GdalDataType::UInt16) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::UInt16 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt16 + ); + + let dataset = MemDatasetBuilder::create(gdal, 10, 8, 1, GdalDataType::Float32).unwrap(); + assert_eq!(dataset.raster_count(), 1); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + }) + .unwrap(); + } + + #[test] + pub fn test_mem_builder_mixed_owned_and_external_bands() { + with_global_gdal(|gdal| { + let mut external_band = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(8, 8, 1, GdalDataType::Float32) + .add_band_with_options( + GdalDataType::UInt8, + external_band.as_mut_ptr(), + None, + None, + Some(Nodata::U64(255)), + ) + .build(gdal) + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt8 + ); + let nodata = dataset.rasterband(2).unwrap().no_data_value(); + assert_eq!(nodata, Some(255.0)); + }) + .unwrap(); + } +}