From 8c61cdc3472d83b957931547ae015ea1f018e5be Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:26:16 +0800 Subject: [PATCH 01/11] feat(sedona-gdal): add foundational wrapper utilities --- c/sedona-gdal/src/config.rs | 42 ++ c/sedona-gdal/src/cpl.rs | 661 ++++++++++++++++++++++++++++++ c/sedona-gdal/src/errors.rs | 10 + c/sedona-gdal/src/gdal_api.rs | 2 + c/sedona-gdal/src/lib.rs | 5 + c/sedona-gdal/src/raster/mod.rs | 22 + c/sedona-gdal/src/raster/types.rs | 246 +++++++++++ 7 files changed, 988 insertions(+) create mode 100644 c/sedona-gdal/src/config.rs create mode 100644 c/sedona-gdal/src/cpl.rs create mode 100644 c/sedona-gdal/src/raster/mod.rs create mode 100644 c/sedona-gdal/src/raster/types.rs diff --git a/c/sedona-gdal/src/config.rs b/c/sedona-gdal/src/config.rs new file mode 100644 index 000000000..2735d6b56 --- /dev/null +++ b/c/sedona-gdal/src/config.rs @@ -0,0 +1,42 @@ +// 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 configuration option wrappers. + +use std::ffi::CString; + +use crate::errors::Result; +use crate::gdal_api::{call_gdal_api, GdalApi}; + +/// Set a GDAL library configuration option with **thread-local** scope. +pub fn set_thread_local_config_option(api: &'static GdalApi, key: &str, value: &str) -> Result<()> { + let c_key = CString::new(key)?; + let c_val = CString::new(value)?; + unsafe { + call_gdal_api!( + api, + CPLSetThreadLocalConfigOption, + c_key.as_ptr(), + c_val.as_ptr() + ); + } + Ok(()) +} diff --git a/c/sedona-gdal/src/cpl.rs b/c/sedona-gdal/src/cpl.rs new file mode 100644 index 000000000..6cae7673b --- /dev/null +++ b/c/sedona-gdal/src/cpl.rs @@ -0,0 +1,661 @@ +// 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 Common Portability Library Functions. +//! +//! Provides [`CslStringList`], a pure-Rust implementation of GDAL's null-terminated +//! string list (`char **papszStrList`), compatible with the georust/gdal API surface. + +use std::ffi::{c_char, CString}; +use std::fmt::{Debug, Display, Formatter}; +use std::ptr; + +use crate::errors::{GdalError, Result}; + +/// A null-terminated array of null-terminated C strings (`char **papszStrList`). +/// +/// This data structure is used throughout GDAL to pass `KEY=VALUE`-formatted options +/// to various functions. +/// +/// This is a pure Rust implementation that mirrors the API of georust/gdal's +/// `CslStringList`. Memory is managed entirely in Rust — no GDAL `CSL*` functions +/// are called for list management. This should be fine as long as GDAL does not +/// take ownership of the string lists and free them using `CSLDestroy`. +/// +/// # Example +/// +/// There are a number of ways to populate a [`CslStringList`]: +/// +/// ```rust,ignore +/// use sedona_gdal::cpl::{CslStringList, CslStringListEntry}; +/// +/// let mut sl1 = CslStringList::new(); +/// sl1.set_name_value("NUM_THREADS", "ALL_CPUS").unwrap(); +/// sl1.set_name_value("COMPRESS", "LZW").unwrap(); +/// sl1.add_string("MAGIC_FLAG").unwrap(); +/// +/// let sl2: CslStringList = "NUM_THREADS=ALL_CPUS COMPRESS=LZW MAGIC_FLAG".parse().unwrap(); +/// let sl3 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); +/// +/// assert_eq!(sl1.to_string(), sl2.to_string()); +/// assert_eq!(sl2.to_string(), sl3.to_string()); +/// ``` +pub struct CslStringList { + /// Owned strings. + strings: Vec, + /// Null-terminated pointer array into `strings`, rebuilt on every mutation. + /// Invariant: `ptrs.len() == strings.len() + 1` and `ptrs.last() == Some(&null_mut())`. + ptrs: Vec<*mut c_char>, +} + +// Safety: CslStringList is Send + Sync because: +// - `strings` (Vec) is Send + Sync. +// - `ptrs` contains pointers derived from `strings` (stable heap-allocated CString data). +// They are only used for read-only FFI calls. +unsafe impl Send for CslStringList {} +unsafe impl Sync for CslStringList {} + +impl CslStringList { + /// Creates an empty GDAL string list. + pub fn new() -> Self { + Self::with_capacity(0) + } + + /// Create an empty GDAL string list with given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + strings: Vec::with_capacity(capacity), + ptrs: vec![ptr::null_mut(); capacity + 1], + } + } + + /// Rebuilds the null-terminated pointer array from `self.strings`. + /// + /// Must be called after every mutation to `self.strings`. + /// This is O(n) but n is always small (option lists are typically < 20 entries). + /// + /// Safety argument: `CString` stores its data on the heap. Moving a `CString` + /// (as happens during `Vec` reallocation) does not invalidate the heap pointer + /// returned by `CString::as_ptr()`. Therefore pointers stored in `self.ptrs` + /// remain valid as long as the corresponding `CString` in `self.strings` is alive. + fn rebuild_ptrs(&mut self) { + self.ptrs.clear(); + for s in &self.strings { + self.ptrs.push(s.as_ptr() as *mut c_char); + } + self.ptrs.push(ptr::null_mut()); + } + + /// Check that the given `name` is a valid [`CslStringList`] key. + /// + /// Per [GDAL documentation](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc), + /// a key cannot have non-alphanumeric characters in it (underscores are allowed). + /// + /// Returns `Err(GdalError::BadArgument)` on invalid name, `Ok(())` otherwise. + fn check_valid_name(name: &str) -> Result<()> { + if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + Err(GdalError::BadArgument(format!( + "Invalid characters in name: '{name}'" + ))) + } else { + Ok(()) + } + } + + /// Check that the given `value` is a valid [`CslStringList`] value. + /// + /// Per [GDAL documentation](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc), + /// a value cannot have newline characters in it. + /// + /// Returns `Err(GdalError::BadArgument)` on invalid value, `Ok(())` otherwise. + fn check_valid_value(value: &str) -> Result<()> { + if value.contains(['\n', '\r']) { + Err(GdalError::BadArgument(format!( + "Invalid characters in value: '{value}'" + ))) + } else { + Ok(()) + } + } + + /// Assigns `value` to the key `name` without checking for pre-existing assignments. + /// + /// Returns `Ok(())` on success, or `Err(GdalError::BadArgument)` + /// if `name` has non-alphanumeric characters or `value` has newline characters. + /// + /// See: [`CSLAddNameValue`](https://gdal.org/api/cpl.html#_CPPv415CSLAddNameValuePPcPKcPKc) + /// for details. + pub fn add_name_value(&mut self, name: &str, value: &str) -> Result<()> { + Self::check_valid_name(name)?; + Self::check_valid_value(value)?; + let entry = CString::new(format!("{name}={value}"))?; + self.strings.push(entry); + self.rebuild_ptrs(); + Ok(()) + } + + /// Assigns `value` to the key `name`, overwriting any existing assignment to `name`. + /// + /// Name lookup is case-insensitive, matching GDAL's `CSLSetNameValue` behavior. + /// + /// Returns `Ok(())` on success, or `Err(GdalError::BadArgument)` + /// if `name` has non-alphanumeric characters or `value` has newline characters. + /// + /// See: [`CSLSetNameValue`](https://gdal.org/api/cpl.html#_CPPv415CSLSetNameValuePPcPKcPKc) + /// for details. + pub fn set_name_value(&mut self, name: &str, value: &str) -> Result<()> { + Self::check_valid_name(name)?; + Self::check_valid_value(value)?; + let existing = self.strings.iter().position(|s| { + s.to_str().is_ok_and(|v| { + v.split_once('=') + .is_some_and(|(k, _)| k.eq_ignore_ascii_case(name)) + }) + }); + let new_entry = CString::new(format!("{name}={value}"))?; + if let Some(idx) = existing { + self.strings[idx] = new_entry; + } else { + self.strings.push(new_entry); + } + self.rebuild_ptrs(); + Ok(()) + } + + /// Adds a copy of the string slice `value` to the list. + /// + /// Returns `Ok(())` on success, `Err(GdalError::FfiNulError)` if `value` cannot be + /// converted to a C string (e.g. `value` contains a `0` byte). + /// + /// See: [`CSLAddString`](https://gdal.org/api/cpl.html#_CPPv412CSLAddStringPPcPKc) + pub fn add_string(&mut self, value: &str) -> Result<()> { + let v = CString::new(value)?; + self.strings.push(v); + self.rebuild_ptrs(); + Ok(()) + } + + /// Adds the contents of a [`CslStringListEntry`] to `self`. + /// + /// Returns `Err(GdalError::BadArgument)` if entry doesn't meet entry restrictions as + /// described by [`CslStringListEntry`]. + pub fn add_entry(&mut self, entry: &CslStringListEntry) -> Result<()> { + match entry { + CslStringListEntry::Flag(f) => self.add_string(f), + CslStringListEntry::Pair { name, value } => self.add_name_value(name, value), + } + } + + /// Looks up the value corresponding to `name` (case-insensitive). + /// + /// See [`CSLFetchNameValue`](https://gdal.org/doxygen/cpl__string_8h.html#a4f23675f8b6f015ed23d9928048361a1) + /// for details. + pub fn fetch_name_value(&self, name: &str) -> Option { + for s in &self.strings { + if let Ok(v) = s.to_str() { + if let Some((k, val)) = v.split_once('=') { + if k.eq_ignore_ascii_case(name) { + return Some(val.to_string()); + } + } + } + } + None + } + + /// Perform a case **insensitive** search for the given string. + /// + /// Returns `Some(usize)` of value index position, or `None` if not found. + /// + /// See: [`CSLFindString`](https://gdal.org/api/cpl.html#_CPPv413CSLFindString12CSLConstListPKc) + /// for details. + pub fn find_string(&self, value: &str) -> Option { + self.strings + .iter() + .position(|s| s.to_str().is_ok_and(|v| v.eq_ignore_ascii_case(value))) + } + + /// Perform a case sensitive search for the given string. + /// + /// Returns `Some(usize)` of value index position, or `None` if not found. + pub fn find_string_case_sensitive(&self, value: &str) -> Option { + self.strings.iter().position(|s| s.to_str() == Ok(value)) + } + + /// Perform a case sensitive partial string search indicated by `fragment`. + /// + /// Returns `Some(usize)` of value index position, or `None` if not found. + /// + /// See: [`CSLPartialFindString`](https://gdal.org/api/cpl.html#_CPPv420CSLPartialFindString12CSLConstListPKc) + /// for details. + pub fn partial_find_string(&self, fragment: &str) -> Option { + self.strings + .iter() + .position(|s| s.to_str().is_ok_and(|v| v.contains(fragment))) + } + + /// Fetch the [`CslStringListEntry`] for the entry at the given index. + /// + /// Returns `None` if index is out of bounds, `Some(entry)` otherwise. + pub fn get_field(&self, index: usize) -> Option { + self.strings + .get(index) + .and_then(|s| s.to_str().ok()) + .map(CslStringListEntry::from) + } + + /// Determine the number of entries in the list. + /// + /// See: [`CSLCount`](https://gdal.org/api/cpl.html#_CPPv48CSLCount12CSLConstList) for details. + pub fn len(&self) -> usize { + self.strings.len() + } + + /// Determine if the list has any values. + pub fn is_empty(&self) -> bool { + self.strings.is_empty() + } + + /// Get an iterator over the entries of the list. + pub fn iter(&self) -> CslStringListIterator<'_> { + CslStringListIterator { list: self, idx: 0 } + } + + /// Get the raw null-terminated `char**` pointer for passing to GDAL functions. + /// + /// The returned pointer is valid as long as `self` is alive and not mutated. + /// An empty list returns a pointer to `[null]`, which is a valid empty CSL. + pub fn as_ptr(&self) -> *mut *mut c_char { + self.ptrs.as_ptr() as *mut *mut c_char + } + + /// Construct a `CslStringList` from a fallible iterator of string slices. + /// + /// Unlike `FromIterator<&str>`, this returns `Err` if any string contains NUL bytes + /// instead of panicking. + pub fn try_from_iter<'a>(iter: impl IntoIterator) -> Result { + let mut list = Self::new(); + for s in iter { + list.add_string(s)?; + } + Ok(list) + } +} + +impl Default for CslStringList { + fn default() -> Self { + Self::new() + } +} + +impl Clone for CslStringList { + fn clone(&self) -> Self { + let strings = self.strings.clone(); + let mut result = Self { + strings, + ptrs: Vec::new(), + }; + result.rebuild_ptrs(); + result + } +} + +impl Debug for CslStringList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut b = f.debug_tuple("CslStringList"); + for e in self.iter() { + b.field(&e.to_string()); + } + b.finish() + } +} + +impl Display for CslStringList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for e in self.iter() { + f.write_fmt(format_args!("{e}\n"))?; + } + Ok(()) + } +} + +impl<'a> IntoIterator for &'a CslStringList { + type Item = CslStringListEntry; + type IntoIter = CslStringListIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl FromIterator for CslStringList { + fn from_iter>(iter: T) -> Self { + let mut result = Self::default(); + for e in iter { + result.add_entry(&e).unwrap_or_default(); + } + result + } +} + +impl<'a> FromIterator<&'a str> for CslStringList { + fn from_iter>(iter: T) -> Self { + iter.into_iter() + .map(Into::::into) + .collect() + } +} + +impl FromIterator for CslStringList { + fn from_iter>(iter: T) -> Self { + iter.into_iter() + .map(Into::::into) + .collect() + } +} + +impl Extend for CslStringList { + fn extend>(&mut self, iter: T) { + for e in iter { + self.add_entry(&e).unwrap_or_default(); + } + } +} + +/// Represents an entry in a [`CslStringList`]. +/// +/// An entry is either a single token ([`Flag`](Self::Flag)), or a `name=value` +/// assignment ([`Pair`](Self::Pair)). +/// +/// Note: When constructed directly, assumes string values do not contain newline characters +/// nor the null `\0` character. If these conditions are violated, the provided values will +/// be ignored. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CslStringListEntry { + /// A single token entry. + Flag(String), + /// A `name=value` pair entry. + Pair { name: String, value: String }, +} + +impl CslStringListEntry { + /// Create a new [`Self::Flag`] entry. + pub fn new_flag(flag: &str) -> Self { + CslStringListEntry::Flag(flag.to_owned()) + } + + /// Create a new [`Self::Pair`] entry. + pub fn new_pair(name: &str, value: &str) -> Self { + CslStringListEntry::Pair { + name: name.to_owned(), + value: value.to_owned(), + } + } +} + +impl From<&str> for CslStringListEntry { + fn from(value: &str) -> Self { + value.to_owned().into() + } +} + +impl From<(&str, &str)> for CslStringListEntry { + fn from((key, value): (&str, &str)) -> Self { + Self::new_pair(key, value) + } +} + +impl From for CslStringListEntry { + fn from(value: String) -> Self { + match value.split_once('=') { + Some((name, value)) => Self::new_pair(name, value), + None => Self::new_flag(&value), + } + } +} + +impl From<(String, String)> for CslStringListEntry { + fn from((name, value): (String, String)) -> Self { + Self::Pair { name, value } + } +} + +impl Display for CslStringListEntry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CslStringListEntry::Flag(s) => f.write_str(s), + CslStringListEntry::Pair { name, value } => f.write_fmt(format_args!("{name}={value}")), + } + } +} + +/// State for iterator over [`CslStringList`] entries. +pub struct CslStringListIterator<'a> { + list: &'a CslStringList, + idx: usize, +} + +impl Iterator for CslStringListIterator<'_> { + type Item = CslStringListEntry; + + fn next(&mut self) -> Option { + let entry = self.list.strings.get(self.idx)?; + self.idx += 1; + Some(entry.to_string_lossy().into_owned().into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::Result; + + fn fixture() -> Result { + let mut l = CslStringList::new(); + l.set_name_value("ONE", "1")?; + l.set_name_value("TWO", "2")?; + l.set_name_value("THREE", "3")?; + l.add_string("SOME_FLAG")?; + Ok(l) + } + + #[test] + fn construct() -> Result<()> { + let mut sl1 = CslStringList::new(); + sl1.set_name_value("NUM_THREADS", "ALL_CPUS").unwrap(); + sl1.set_name_value("COMPRESS", "LZW").unwrap(); + sl1.add_string("MAGIC_FLAG").unwrap(); + + let sl2 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); + let sl3 = CslStringList::from_iter([ + CslStringListEntry::from(("NUM_THREADS", "ALL_CPUS")), + CslStringListEntry::from(("COMPRESS", "LZW")), + CslStringListEntry::from("MAGIC_FLAG"), + ]); + + assert_eq!(sl1.to_string(), sl2.to_string()); + assert_eq!(sl2.to_string(), sl3.to_string()); + + Ok(()) + } + + #[test] + fn basic_list() -> Result<()> { + let l = fixture()?; + assert!(matches!(l.fetch_name_value("ONE"), Some(s) if s == *"1")); + assert!(matches!(l.fetch_name_value("THREE"), Some(s) if s == *"3")); + assert!(l.fetch_name_value("FOO").is_none()); + + Ok(()) + } + + #[test] + fn has_length() -> Result<()> { + let l = fixture()?; + assert_eq!(l.len(), 4); + + Ok(()) + } + + #[test] + fn can_be_empty() -> Result<()> { + let l = CslStringList::new(); + assert!(l.is_empty()); + + let l = fixture()?; + assert!(!l.is_empty()); + + Ok(()) + } + + #[test] + fn has_iterator() -> Result<()> { + let f = fixture()?; + let mut it = f.iter(); + assert_eq!(it.next(), Some(("ONE", "1").into())); + assert_eq!(it.next(), Some(("TWO", "2").into())); + assert_eq!(it.next(), Some(("THREE", "3").into())); + assert_eq!(it.next(), Some("SOME_FLAG".into())); + assert_eq!(it.next(), None); + assert_eq!(it.next(), None); + Ok(()) + } + + #[test] + fn invalid_name_value() -> Result<()> { + let mut l = fixture()?; + assert!(l.set_name_value("l==t", "2").is_err()); + assert!(l.set_name_value("foo", "2\n4\r5").is_err()); + + Ok(()) + } + + #[test] + fn add_vs_set() -> Result<()> { + let mut f = CslStringList::new(); + f.add_name_value("ONE", "1")?; + f.add_name_value("ONE", "2")?; + let s = f.to_string(); + assert!(s.contains("ONE") && s.contains('1') && s.contains('2')); + + let mut f = CslStringList::new(); + f.set_name_value("ONE", "1")?; + f.set_name_value("ONE", "2")?; + let s = f.to_string(); + assert!(s.contains("ONE") && !s.contains('1') && s.contains('2')); + + Ok(()) + } + + #[test] + fn try_from_impl() -> Result<()> { + let l = CslStringList::from_iter(["ONE=1", "TWO=2"]); + assert!(matches!(l.fetch_name_value("ONE"), Some(s) if s == *"1")); + assert!(matches!(l.fetch_name_value("TWO"), Some(s) if s == *"2")); + + Ok(()) + } + + #[test] + fn debug_fmt() -> Result<()> { + let l = fixture()?; + let s = format!("{l:?}"); + assert!(s.contains("ONE=1")); + assert!(s.contains("TWO=2")); + assert!(s.contains("THREE=3")); + assert!(s.contains("SOME_FLAG")); + + Ok(()) + } + + #[test] + fn can_add_strings() -> Result<()> { + let mut l = CslStringList::new(); + assert!(l.is_empty()); + l.add_string("-abc")?; + l.add_string("-d_ef")?; + l.add_string("A")?; + l.add_string("B")?; + assert_eq!(l.len(), 4); + + Ok(()) + } + + #[test] + fn find_string() -> Result<()> { + let f = fixture()?; + assert_eq!(f.find_string("NON_FLAG"), None); + assert_eq!(f.find_string("SOME_FLAG"), Some(3)); + assert_eq!(f.find_string("ONE=1"), Some(0)); + assert_eq!(f.find_string("one=1"), Some(0)); + assert_eq!(f.find_string("TWO="), None); + Ok(()) + } + + #[test] + fn find_string_case_sensitive() -> Result<()> { + let f = fixture()?; + assert_eq!(f.find_string_case_sensitive("ONE=1"), Some(0)); + assert_eq!(f.find_string_case_sensitive("one=1"), None); + assert_eq!(f.find_string_case_sensitive("SOME_FLAG"), Some(3)); + Ok(()) + } + + #[test] + fn partial_find_string() -> Result<()> { + let f = fixture()?; + assert_eq!(f.partial_find_string("ONE=1"), Some(0)); + assert_eq!(f.partial_find_string("ONE="), Some(0)); + assert_eq!(f.partial_find_string("=1"), Some(0)); + assert_eq!(f.partial_find_string("1"), Some(0)); + assert_eq!(f.partial_find_string("THREE="), Some(2)); + assert_eq!(f.partial_find_string("THREE"), Some(2)); + assert_eq!(f.partial_find_string("three"), None); + Ok(()) + } + + #[test] + fn as_ptr_is_null_terminated() { + let mut l = CslStringList::new(); + l.add_string("A").unwrap(); + l.add_string("B").unwrap(); + let ptr = l.as_ptr(); + unsafe { + // First entry + assert!(!(*ptr).is_null()); + // Second entry + assert!(!(*ptr.add(1)).is_null()); + // Null terminator + assert!((*ptr.add(2)).is_null()); + } + } + + #[test] + fn clone_is_independent() -> Result<()> { + let f = fixture()?; + let mut g = f.clone(); + g.set_name_value("ONE", "999")?; + // Original is unchanged. + assert_eq!(f.fetch_name_value("ONE"), Some("1".into())); + assert_eq!(g.fetch_name_value("ONE"), Some("999".into())); + Ok(()) + } +} diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs index 166e04236..6f2ad1a0f 100644 --- a/c/sedona-gdal/src/errors.rs +++ b/c/sedona-gdal/src/errors.rs @@ -19,6 +19,8 @@ //! . //! Original code is licensed under MIT. +use std::ffi::NulError; + use thiserror::Error; /// Error type for the sedona-gdal crate initialization and library loading. @@ -39,4 +41,12 @@ pub enum GdalError { number: i32, msg: String, }, + + #[error("Bad argument: {0}")] + BadArgument(String), + + #[error("FFI NUL error: {0}")] + FfiNulError(#[from] NulError), } + +pub type Result = std::result::Result; diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs index d0c07f6f6..4b8381a62 100644 --- a/c/sedona-gdal/src/gdal_api.rs +++ b/c/sedona-gdal/src/gdal_api.rs @@ -43,6 +43,8 @@ macro_rules! call_gdal_api { }; } +pub(crate) use call_gdal_api; + #[derive(Debug)] pub struct GdalApi { pub(crate) inner: SedonaGdalApi, diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs index e05a3bd02..b64a2275c 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -25,3 +25,8 @@ pub mod errors; // --- Core API --- pub mod gdal_api; pub mod global; + +// --- High-level wrappers --- +pub mod config; +pub mod cpl; +pub mod raster; diff --git a/c/sedona-gdal/src/raster/mod.rs b/c/sedona-gdal/src/raster/mod.rs new file mode 100644 index 000000000..5669ec66e --- /dev/null +++ b/c/sedona-gdal/src/raster/mod.rs @@ -0,0 +1,22 @@ +// 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 types; + +pub use types::{ + Buffer, DatasetOptions, GdalDataType, GdalType, RasterCreationOptions, ResampleAlg, +}; diff --git a/c/sedona-gdal/src/raster/types.rs b/c/sedona-gdal/src/raster/types.rs new file mode 100644 index 000000000..c2cf7f1d8 --- /dev/null +++ b/c/sedona-gdal/src/raster/types.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 crate::gdal_dyn_bindgen::{self, GDALDataType, GDALRIOResampleAlg}; + +/// A Rust-friendly enum mirroring the georust/gdal `GdalDataType` names. +/// +/// This maps 1-to-1 with [`GDALDataType`] but uses Rust-idiomatic names like `UInt8` +/// instead of `GDT_Byte`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GdalDataType { + Unknown, + UInt8, + Int8, + UInt16, + Int16, + UInt32, + Int32, + UInt64, + Int64, + Float32, + Float64, +} + +impl GdalDataType { + /// Convert from the C-level `GDALDataType` enum. + /// + /// Returns `None` for complex types and `GDT_TypeCount`. + pub fn from_c(c_type: GDALDataType) -> Option { + match c_type { + GDALDataType::GDT_Unknown => Some(Self::Unknown), + GDALDataType::GDT_Byte => Some(Self::UInt8), + GDALDataType::GDT_Int8 => Some(Self::Int8), + GDALDataType::GDT_UInt16 => Some(Self::UInt16), + GDALDataType::GDT_Int16 => Some(Self::Int16), + GDALDataType::GDT_UInt32 => Some(Self::UInt32), + GDALDataType::GDT_Int32 => Some(Self::Int32), + GDALDataType::GDT_UInt64 => Some(Self::UInt64), + GDALDataType::GDT_Int64 => Some(Self::Int64), + GDALDataType::GDT_Float32 => Some(Self::Float32), + GDALDataType::GDT_Float64 => Some(Self::Float64), + _ => None, // Complex types, Float16, TypeCount + } + } + + /// Convert to the C-level `GDALDataType` enum. + pub fn to_c(self) -> GDALDataType { + match self { + Self::Unknown => GDALDataType::GDT_Unknown, + Self::UInt8 => GDALDataType::GDT_Byte, + Self::Int8 => GDALDataType::GDT_Int8, + Self::UInt16 => GDALDataType::GDT_UInt16, + Self::Int16 => GDALDataType::GDT_Int16, + Self::UInt32 => GDALDataType::GDT_UInt32, + Self::Int32 => GDALDataType::GDT_Int32, + Self::UInt64 => GDALDataType::GDT_UInt64, + Self::Int64 => GDALDataType::GDT_Int64, + Self::Float32 => GDALDataType::GDT_Float32, + Self::Float64 => GDALDataType::GDT_Float64, + } + } + + /// Return the ordinal value compatible with the C API (same as `self.to_c() as i32`). + pub fn ordinal(self) -> i32 { + self.to_c() as i32 + } + + /// Return the byte size of this data type (0 for Unknown). + pub fn byte_size(self) -> usize { + match self { + Self::Unknown => 0, + Self::UInt8 | Self::Int8 => 1, + Self::UInt16 | Self::Int16 => 2, + Self::UInt32 | Self::Int32 | Self::Float32 => 4, + Self::UInt64 | Self::Int64 | Self::Float64 => 8, + } + } +} + +/// Trait mapping Rust primitive types to GDAL data types. +pub trait GdalType { + fn gdal_ordinal() -> GDALDataType; +} + +impl GdalType for u8 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Byte + } +} + +impl GdalType for i8 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Int8 + } +} + +impl GdalType for u16 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_UInt16 + } +} + +impl GdalType for i16 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Int16 + } +} + +impl GdalType for u32 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_UInt32 + } +} + +impl GdalType for i32 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Int32 + } +} + +impl GdalType for u64 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_UInt64 + } +} + +impl GdalType for i64 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Int64 + } +} + +impl GdalType for f32 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Float32 + } +} + +impl GdalType for f64 { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::GDT_Float64 + } +} + +/// A 2D raster buffer. +#[derive(Debug, Clone)] +pub struct Buffer { + /// Shape as (cols, rows) — matches georust/gdal convention. + pub shape: (usize, usize), + pub data: Vec, +} + +impl Buffer { + pub fn new(shape: (usize, usize), data: Vec) -> Self { + Self { shape, data } + } + + /// Return the buffer data as a slice (georust compatibility). + pub fn data(&self) -> &[T] { + &self.data + } +} + +/// Options for opening a dataset. +pub struct DatasetOptions<'a> { + pub open_flags: crate::gdal_dyn_bindgen::GDALOpenFlags, + pub allowed_drivers: Option<&'a [&'a str]>, + pub open_options: Option<&'a [&'a str]>, + pub sibling_files: Option<&'a [&'a str]>, +} + +impl<'a> Default for DatasetOptions<'a> { + fn default() -> Self { + Self { + open_flags: crate::gdal_dyn_bindgen::GDAL_OF_READONLY + | crate::gdal_dyn_bindgen::GDAL_OF_VERBOSE_ERROR, + allowed_drivers: None, + open_options: None, + sibling_files: None, + } + } +} + +/// Raster creation options (list of "KEY=VALUE" strings). +pub type RasterCreationOptions<'a> = &'a [&'a str]; + +/// GDAL resample algorithm. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResampleAlg { + NearestNeighbour, + Bilinear, + Cubic, + CubicSpline, + Lanczos, + Average, + Mode, + Gauss, +} + +impl ResampleAlg { + /// Convert to the numeric `GDALRIOResampleAlg` value used by `GDALRasterIOExtraArg`. + pub fn to_gdal(self) -> GDALRIOResampleAlg { + match self { + ResampleAlg::NearestNeighbour => gdal_dyn_bindgen::GRIORA_NearestNeighbour, + ResampleAlg::Bilinear => gdal_dyn_bindgen::GRIORA_Bilinear, + ResampleAlg::Cubic => gdal_dyn_bindgen::GRIORA_Cubic, + ResampleAlg::CubicSpline => gdal_dyn_bindgen::GRIORA_CubicSpline, + ResampleAlg::Lanczos => gdal_dyn_bindgen::GRIORA_Lanczos, + ResampleAlg::Average => gdal_dyn_bindgen::GRIORA_Average, + ResampleAlg::Mode => gdal_dyn_bindgen::GRIORA_Mode, + ResampleAlg::Gauss => gdal_dyn_bindgen::GRIORA_Gauss, + } + } + + /// Return the string name for use in overview building and VRT resampling options. + pub fn to_gdal_str(self) -> &'static str { + match self { + ResampleAlg::NearestNeighbour => "NearestNeighbour", + ResampleAlg::Bilinear => "Bilinear", + ResampleAlg::Cubic => "Cubic", + ResampleAlg::CubicSpline => "CubicSpline", + ResampleAlg::Lanczos => "Lanczos", + ResampleAlg::Average => "Average", + ResampleAlg::Mode => "Mode", + ResampleAlg::Gauss => "Gauss", + } + } +} From b13e68f498db83c631ffcbb90f37aef788b1414e Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:40:54 +0800 Subject: [PATCH 02/11] refactor(sedona-gdal): use flat raster module file --- c/sedona-gdal/src/{raster/mod.rs => raster.rs} | 0 c/sedona-gdal/src/raster/types.rs | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename c/sedona-gdal/src/{raster/mod.rs => raster.rs} (100%) diff --git a/c/sedona-gdal/src/raster/mod.rs b/c/sedona-gdal/src/raster.rs similarity index 100% rename from c/sedona-gdal/src/raster/mod.rs rename to c/sedona-gdal/src/raster.rs diff --git a/c/sedona-gdal/src/raster/types.rs b/c/sedona-gdal/src/raster/types.rs index c2cf7f1d8..8121a7707 100644 --- a/c/sedona-gdal/src/raster/types.rs +++ b/c/sedona-gdal/src/raster/types.rs @@ -19,7 +19,9 @@ //! . //! Original code is licensed under MIT. -use crate::gdal_dyn_bindgen::{self, GDALDataType, GDALRIOResampleAlg}; +use crate::gdal_dyn_bindgen::{ + self, GDALDataType, GDALOpenFlags, GDALRIOResampleAlg, GDAL_OF_READONLY, GDAL_OF_VERBOSE_ERROR, +}; /// A Rust-friendly enum mirroring the georust/gdal `GdalDataType` names. /// @@ -181,7 +183,7 @@ impl Buffer { /// Options for opening a dataset. pub struct DatasetOptions<'a> { - pub open_flags: crate::gdal_dyn_bindgen::GDALOpenFlags, + pub open_flags: GDALOpenFlags, pub allowed_drivers: Option<&'a [&'a str]>, pub open_options: Option<&'a [&'a str]>, pub sibling_files: Option<&'a [&'a str]>, @@ -190,8 +192,7 @@ pub struct DatasetOptions<'a> { impl<'a> Default for DatasetOptions<'a> { fn default() -> Self { Self { - open_flags: crate::gdal_dyn_bindgen::GDAL_OF_READONLY - | crate::gdal_dyn_bindgen::GDAL_OF_VERBOSE_ERROR, + open_flags: GDAL_OF_READONLY | GDAL_OF_VERBOSE_ERROR, allowed_drivers: None, open_options: None, sibling_files: None, From 5f2e16e26af30d507d52f9a5a0708779de80e54f Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 02:05:36 +0800 Subject: [PATCH 03/11] refactor(sedona-gdal): derive primitive GdalType impls with macro --- c/sedona-gdal/src/raster/types.rs | 81 +++++++++---------------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/c/sedona-gdal/src/raster/types.rs b/c/sedona-gdal/src/raster/types.rs index 8121a7707..49ba23b88 100644 --- a/c/sedona-gdal/src/raster/types.rs +++ b/c/sedona-gdal/src/raster/types.rs @@ -102,64 +102,29 @@ pub trait GdalType { fn gdal_ordinal() -> GDALDataType; } -impl GdalType for u8 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Byte - } -} - -impl GdalType for i8 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Int8 - } -} - -impl GdalType for u16 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_UInt16 - } -} - -impl GdalType for i16 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Int16 - } -} - -impl GdalType for u32 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_UInt32 - } -} - -impl GdalType for i32 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Int32 - } -} - -impl GdalType for u64 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_UInt64 - } -} - -impl GdalType for i64 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Int64 - } -} - -impl GdalType for f32 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Float32 - } -} - -impl GdalType for f64 { - fn gdal_ordinal() -> GDALDataType { - GDALDataType::GDT_Float64 - } +macro_rules! impl_gdal_type { + ($($ty:ty => $variant:ident),+ $(,)?) => { + $( + impl GdalType for $ty { + fn gdal_ordinal() -> GDALDataType { + GDALDataType::$variant + } + } + )+ + }; +} + +impl_gdal_type! { + u8 => GDT_Byte, + i8 => GDT_Int8, + u16 => GDT_UInt16, + i16 => GDT_Int16, + u32 => GDT_UInt32, + i32 => GDT_Int32, + u64 => GDT_UInt64, + i64 => GDT_Int64, + f32 => GDT_Float32, + f64 => GDT_Float64, } /// A 2D raster buffer. From a93447e4fa171b79407d5706be90794869d8da57 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 02:30:47 +0800 Subject: [PATCH 04/11] Fix review comments --- c/sedona-gdal/src/cpl.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/c/sedona-gdal/src/cpl.rs b/c/sedona-gdal/src/cpl.rs index 6cae7673b..b43319aea 100644 --- a/c/sedona-gdal/src/cpl.rs +++ b/c/sedona-gdal/src/cpl.rs @@ -44,7 +44,7 @@ use crate::errors::{GdalError, Result}; /// /// There are a number of ways to populate a [`CslStringList`]: /// -/// ```rust,ignore +/// ```rust /// use sedona_gdal::cpl::{CslStringList, CslStringListEntry}; /// /// let mut sl1 = CslStringList::new(); @@ -52,11 +52,9 @@ use crate::errors::{GdalError, Result}; /// sl1.set_name_value("COMPRESS", "LZW").unwrap(); /// sl1.add_string("MAGIC_FLAG").unwrap(); /// -/// let sl2: CslStringList = "NUM_THREADS=ALL_CPUS COMPRESS=LZW MAGIC_FLAG".parse().unwrap(); -/// let sl3 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); +/// let sl2 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); /// /// assert_eq!(sl1.to_string(), sl2.to_string()); -/// assert_eq!(sl2.to_string(), sl3.to_string()); /// ``` pub struct CslStringList { /// Owned strings. @@ -81,9 +79,11 @@ impl CslStringList { /// Create an empty GDAL string list with given capacity. pub fn with_capacity(capacity: usize) -> Self { + let mut ptrs = Vec::with_capacity(capacity + 1); + ptrs.push(ptr::null_mut()); Self { strings: Vec::with_capacity(capacity), - ptrs: vec![ptr::null_mut(); capacity + 1], + ptrs, } } From 17208e517145ef01747675a4878bc50e56fec177 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 02:49:41 +0800 Subject: [PATCH 05/11] refactor(sedona-gdal): make csl construction transactional --- c/sedona-gdal/src/cpl.rs | 125 ++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/c/sedona-gdal/src/cpl.rs b/c/sedona-gdal/src/cpl.rs index b43319aea..b09e9f773 100644 --- a/c/sedona-gdal/src/cpl.rs +++ b/c/sedona-gdal/src/cpl.rs @@ -52,7 +52,7 @@ use crate::errors::{GdalError, Result}; /// sl1.set_name_value("COMPRESS", "LZW").unwrap(); /// sl1.add_string("MAGIC_FLAG").unwrap(); /// -/// let sl2 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); +/// let sl2 = CslStringList::try_from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]).unwrap(); /// /// assert_eq!(sl1.to_string(), sl2.to_string()); /// ``` @@ -287,15 +287,37 @@ impl CslStringList { self.ptrs.as_ptr() as *mut *mut c_char } - /// Construct a `CslStringList` from a fallible iterator of string slices. - /// - /// Unlike `FromIterator<&str>`, this returns `Err` if any string contains NUL bytes - /// instead of panicking. - pub fn try_from_iter<'a>(iter: impl IntoIterator) -> Result { - let mut list = Self::new(); - for s in iter { - list.add_string(s)?; + /// Truncate the list to at most `len` entries. + pub fn truncate(&mut self, len: usize) { + self.strings.truncate(len); + self.rebuild_ptrs(); + } + + /// Extend the list from an iterator, rolling back to the original size on error. + pub fn try_extend(&mut self, iter: I) -> Result<()> + where + I: IntoIterator, + T: Into, + { + let original_len = self.len(); + for item in iter { + let entry = item.into(); + if let Err(err) = self.add_entry(&entry) { + self.truncate(original_len); + return Err(err); + } } + Ok(()) + } + + /// Construct a `CslStringList` from a fallible iterator of entries. + pub fn try_from_iter(iter: I) -> Result + where + I: IntoIterator, + T: Into, + { + let mut list = Self::new(); + list.try_extend(iter)?; Ok(list) } } @@ -346,40 +368,6 @@ impl<'a> IntoIterator for &'a CslStringList { } } -impl FromIterator for CslStringList { - fn from_iter>(iter: T) -> Self { - let mut result = Self::default(); - for e in iter { - result.add_entry(&e).unwrap_or_default(); - } - result - } -} - -impl<'a> FromIterator<&'a str> for CslStringList { - fn from_iter>(iter: T) -> Self { - iter.into_iter() - .map(Into::::into) - .collect() - } -} - -impl FromIterator for CslStringList { - fn from_iter>(iter: T) -> Self { - iter.into_iter() - .map(Into::::into) - .collect() - } -} - -impl Extend for CslStringList { - fn extend>(&mut self, iter: T) { - for e in iter { - self.add_entry(&e).unwrap_or_default(); - } - } -} - /// Represents an entry in a [`CslStringList`]. /// /// An entry is either a single token ([`Flag`](Self::Flag)), or a `name=value` @@ -484,12 +472,13 @@ mod tests { sl1.set_name_value("COMPRESS", "LZW").unwrap(); sl1.add_string("MAGIC_FLAG").unwrap(); - let sl2 = CslStringList::from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"]); - let sl3 = CslStringList::from_iter([ + let sl2 = + CslStringList::try_from_iter(["NUM_THREADS=ALL_CPUS", "COMPRESS=LZW", "MAGIC_FLAG"])?; + let sl3 = CslStringList::try_from_iter([ CslStringListEntry::from(("NUM_THREADS", "ALL_CPUS")), CslStringListEntry::from(("COMPRESS", "LZW")), CslStringListEntry::from("MAGIC_FLAG"), - ]); + ])?; assert_eq!(sl1.to_string(), sl2.to_string()); assert_eq!(sl2.to_string(), sl3.to_string()); @@ -566,14 +555,54 @@ mod tests { } #[test] - fn try_from_impl() -> Result<()> { - let l = CslStringList::from_iter(["ONE=1", "TWO=2"]); + fn try_from_iter_constructs_list() -> Result<()> { + let l = CslStringList::try_from_iter(["ONE=1", "TWO=2"])?; assert!(matches!(l.fetch_name_value("ONE"), Some(s) if s == *"1")); assert!(matches!(l.fetch_name_value("TWO"), Some(s) if s == *"2")); Ok(()) } + #[test] + fn try_from_iter_rejects_invalid_entry() { + let result = CslStringList::try_from_iter([CslStringListEntry::from(("bad-name", "1"))]); + assert!(matches!(result, Err(GdalError::BadArgument(_)))); + } + + #[test] + fn try_extend_is_transactional() -> Result<()> { + let mut list = CslStringList::try_from_iter([("ONE", "1"), ("TWO", "2")])?; + let before = list.clone(); + + let result = list.try_extend([ + CslStringListEntry::from(("THREE", "3")), + CslStringListEntry::from(("bad-name", "4")), + ]); + + assert!(matches!(result, Err(GdalError::BadArgument(_)))); + assert_eq!(list.to_string(), before.to_string()); + + Ok(()) + } + + #[test] + fn truncate_preserves_ptr_invariant() -> Result<()> { + let mut list = CslStringList::try_from_iter(["A", "B", "C"])?; + list.truncate(1); + + assert_eq!(list.len(), 1); + assert_eq!(list.get_field(0), Some(CslStringListEntry::from("A"))); + assert_eq!(list.get_field(1), None); + + let ptr = list.as_ptr(); + unsafe { + assert!(!(*ptr).is_null()); + assert!((*ptr.add(1)).is_null()); + } + + Ok(()) + } + #[test] fn debug_fmt() -> Result<()> { let l = fixture()?; From 5a608c6f6d87e4c08ae74b6a92069cdbd5fb04a2 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Wed, 11 Mar 2026 13:14:10 +0800 Subject: [PATCH 06/11] refactor(sedona-gdal): remove raster type re-export aliases --- c/sedona-gdal/src/raster.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs index 5669ec66e..1ddc9b2ed 100644 --- a/c/sedona-gdal/src/raster.rs +++ b/c/sedona-gdal/src/raster.rs @@ -16,7 +16,3 @@ // under the License. pub mod types; - -pub use types::{ - Buffer, DatasetOptions, GdalDataType, GdalType, RasterCreationOptions, ResampleAlg, -}; From 5d3c87d575b19e5c6d0e74c6965718bec0add29f Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:27:09 +0800 Subject: [PATCH 07/11] feat(sedona-gdal): add geometry and spatial ref primitives --- c/sedona-gdal/src/errors.rs | 16 +++ c/sedona-gdal/src/geo_transform.rs | 152 ++++++++++++++++++++++ c/sedona-gdal/src/lib.rs | 4 + c/sedona-gdal/src/spatial_ref.rs | 155 ++++++++++++++++++++++ c/sedona-gdal/src/vector/geometry.rs | 162 +++++++++++++++++++++++ c/sedona-gdal/src/vector/mod.rs | 20 +++ c/sedona-gdal/src/vsi.rs | 187 +++++++++++++++++++++++++++ 7 files changed, 696 insertions(+) create mode 100644 c/sedona-gdal/src/geo_transform.rs create mode 100644 c/sedona-gdal/src/spatial_ref.rs create mode 100644 c/sedona-gdal/src/vector/geometry.rs create mode 100644 c/sedona-gdal/src/vector/mod.rs create mode 100644 c/sedona-gdal/src/vsi.rs diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs index 6f2ad1a0f..f344667ec 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,23 @@ 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), } 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..0646a241a 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -29,4 +29,8 @@ pub mod global; // --- High-level wrappers --- pub mod config; pub mod cpl; +pub mod geo_transform; pub mod raster; +pub mod spatial_ref; +pub mod vector; +pub mod vsi; diff --git a/c/sedona-gdal/src/spatial_ref.rs b/c/sedona-gdal/src/spatial_ref.rs new file mode 100644 index 000000000..61aa37d10 --- /dev/null +++ b/c/sedona-gdal/src/spatial_ref.rs @@ -0,0 +1,155 @@ +// 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::*; + +/// 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 != crate::gdal_dyn_bindgen::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/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/mod.rs b/c/sedona-gdal/src/vector/mod.rs new file mode 100644 index 000000000..14c13d8a5 --- /dev/null +++ b/c/sedona-gdal/src/vector/mod.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 geometry; + +pub use geometry::{Envelope, Geometry}; 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(); + } +} From acb840d07a4e70eb4920c2854ff38109a6fe0f0b Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:41:46 +0800 Subject: [PATCH 08/11] refactor(sedona-gdal): use flat vector module file --- c/sedona-gdal/src/spatial_ref.rs | 3 ++- c/sedona-gdal/src/{vector/mod.rs => vector.rs} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename c/sedona-gdal/src/{vector/mod.rs => vector.rs} (100%) diff --git a/c/sedona-gdal/src/spatial_ref.rs b/c/sedona-gdal/src/spatial_ref.rs index 61aa37d10..77559a0de 100644 --- a/c/sedona-gdal/src/spatial_ref.rs +++ b/c/sedona-gdal/src/spatial_ref.rs @@ -24,6 +24,7 @@ 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. @@ -91,7 +92,7 @@ impl SpatialRef { &mut ptr, ptr::null() ); - if rv != crate::gdal_dyn_bindgen::OGRERR_NONE || ptr.is_null() { + if rv != OGRERR_NONE || ptr.is_null() { return Err(GdalError::NullPointer { method_name: "OSRExportToPROJJSON", msg: "returned null".to_string(), diff --git a/c/sedona-gdal/src/vector/mod.rs b/c/sedona-gdal/src/vector.rs similarity index 100% rename from c/sedona-gdal/src/vector/mod.rs rename to c/sedona-gdal/src/vector.rs From 0b7d4b5b087d4d8f2024f8eaf46b213bfde5db4d Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:28:06 +0800 Subject: [PATCH 09/11] feat(sedona-gdal): add dataset and vector/raster wrappers --- c/sedona-gdal/src/dataset.rs | 470 +++++++++++++++++++++++++ c/sedona-gdal/src/driver.rs | 246 +++++++++++++ c/sedona-gdal/src/errors.rs | 3 + c/sedona-gdal/src/lib.rs | 2 + c/sedona-gdal/src/raster.rs | 3 + c/sedona-gdal/src/raster/rasterband.rs | 368 +++++++++++++++++++ c/sedona-gdal/src/vector.rs | 4 + c/sedona-gdal/src/vector/feature.rs | 218 ++++++++++++ c/sedona-gdal/src/vector/layer.rs | 118 +++++++ 9 files changed, 1432 insertions(+) create mode 100644 c/sedona-gdal/src/dataset.rs create mode 100644 c/sedona-gdal/src/driver.rs create mode 100644 c/sedona-gdal/src/raster/rasterband.rs create mode 100644 c/sedona-gdal/src/vector/feature.rs create mode 100644 c/sedona-gdal/src/vector/layer.rs diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs new file mode 100644 index 000000000..b286535b0 --- /dev/null +++ b/c/sedona-gdal/src/dataset.rs @@ -0,0 +1,470 @@ +// 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::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::raster::types::{DatasetOptions, GdalDataType as RustGdalDataType}; +use crate::raster::RasterBand; +use crate::spatial_ref::SpatialRef; +use crate::vector::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: &crate::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..b3d82379f --- /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::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 f344667ec..3ff5d03db 100644 --- a/c/sedona-gdal/src/errors.rs +++ b/c/sedona-gdal/src/errors.rs @@ -63,6 +63,9 @@ pub enum GdalError { #[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/lib.rs b/c/sedona-gdal/src/lib.rs index 0646a241a..c331f5dfa 100644 --- a/c/sedona-gdal/src/lib.rs +++ b/c/sedona-gdal/src/lib.rs @@ -29,6 +29,8 @@ 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; diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs index 1ddc9b2ed..a0ce55b5f 100644 --- a/c/sedona-gdal/src/raster.rs +++ b/c/sedona-gdal/src/raster.rs @@ -15,4 +15,7 @@ // specific language governing permissions and limitations // under the License. +pub mod rasterband; pub mod types; + +pub use rasterband::{actual_block_size, RasterBand}; 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/vector.rs b/c/sedona-gdal/src/vector.rs index 14c13d8a5..e93cac98f 100644 --- a/c/sedona-gdal/src/vector.rs +++ b/c/sedona-gdal/src/vector.rs @@ -15,6 +15,10 @@ // specific language governing permissions and limitations // under the License. +pub mod feature; pub mod geometry; +pub mod layer; +pub use feature::{Feature, FieldDefn}; pub use geometry::{Envelope, Geometry}; +pub use layer::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..de3013fca --- /dev/null +++ b/c/sedona-gdal/src/vector/feature.rs @@ -0,0 +1,218 @@ +// 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::Result; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; + +/// 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(crate::errors::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(crate::errors::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(crate::errors::GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToIsoWkb", + }); + } + Ok(buf) + } + + /// Get the bounding envelope. + pub fn envelope(&self) -> crate::vector::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) }; + crate::vector::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(crate::errors::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/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() + } +} From c201cab8904a62507efb5af702724c09a0b6c552 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 10 Mar 2026 01:43:28 +0800 Subject: [PATCH 10/11] refactor(sedona-gdal): prefer imported core types --- c/sedona-gdal/src/dataset.rs | 3 ++- c/sedona-gdal/src/vector/feature.rs | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index b286535b0..111d8f8ef 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -23,6 +23,7 @@ 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::*; @@ -243,7 +244,7 @@ impl Dataset { /// Create a copy of this dataset to a new file using the given driver. pub fn create_copy( &self, - driver: &crate::driver::Driver, + driver: &Driver, filename: &str, options: &[&str], ) -> Result { diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs index de3013fca..a5bee5588 100644 --- a/c/sedona-gdal/src/vector/feature.rs +++ b/c/sedona-gdal/src/vector/feature.rs @@ -22,9 +22,10 @@ use std::ffi::CString; use std::marker::PhantomData; -use crate::errors::Result; +use crate::errors::{GdalError, Result}; use crate::gdal_api::{call_gdal_api, GdalApi}; use crate::gdal_dyn_bindgen::*; +use crate::vector::Envelope; /// An OGR feature. pub struct Feature<'a> { @@ -78,9 +79,7 @@ impl<'a> Feature<'a> { ) }; if idx < 0 { - return Err(crate::errors::GdalError::BadArgument(format!( - "field '{name}' not found" - ))); + return Err(GdalError::BadArgument(format!("field '{name}' not found"))); } Ok(idx) } @@ -141,7 +140,7 @@ impl<'a> BorrowedGeometry<'a> { pub fn wkb(&self) -> Result> { let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize, self.c_geom) }; if size < 0 { - return Err(crate::errors::GdalError::BadArgument(format!( + return Err(GdalError::BadArgument(format!( "OGR_G_WkbSize returned negative size: {size}" ))); } @@ -156,7 +155,7 @@ impl<'a> BorrowedGeometry<'a> { ) }; if rv != OGRERR_NONE { - return Err(crate::errors::GdalError::OgrError { + return Err(GdalError::OgrError { err: rv, method_name: "OGR_G_ExportToIsoWkb", }); @@ -165,7 +164,7 @@ impl<'a> BorrowedGeometry<'a> { } /// Get the bounding envelope. - pub fn envelope(&self) -> crate::vector::Envelope { + pub fn envelope(&self) -> Envelope { let mut env = OGREnvelope { MinX: 0.0, MaxX: 0.0, @@ -173,7 +172,7 @@ impl<'a> BorrowedGeometry<'a> { MaxY: 0.0, }; unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut env) }; - crate::vector::Envelope { + Envelope { MinX: env.MinX, MaxX: env.MaxX, MinY: env.MinY, @@ -203,7 +202,7 @@ impl FieldDefn { 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(crate::errors::GdalError::NullPointer { + return Err(GdalError::NullPointer { method_name: "OGR_Fld_Create", msg: format!("failed to create field definition '{name}'"), }); From 1c53d551f3b265db606c5064039cc7df2fc38af6 Mon Sep 17 00:00:00 2001 From: Kontinuation Date: Wed, 11 Mar 2026 15:44:16 +0800 Subject: [PATCH 11/11] fix(sedona-gdal): import raster types via module path --- c/sedona-gdal/src/driver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs index b3d82379f..f0e6c5963 100644 --- a/c/sedona-gdal/src/driver.rs +++ b/c/sedona-gdal/src/driver.rs @@ -27,7 +27,7 @@ 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::GdalType; +use crate::raster::types::GdalType; /// A GDAL driver. pub struct Driver {