From feebfcb9e5e58e3b9eb41b49d073c7bfb42c97e2 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Mon, 2 Mar 2026 12:05:44 +0800 Subject: [PATCH 1/8] feat(gdal): add sedona-gdal crate with runtime-loaded bindings --- Cargo.lock | 21 + Cargo.toml | 3 + c/sedona-gdal/Cargo.toml | 45 ++ c/sedona-gdal/src/config.rs | 38 ++ c/sedona-gdal/src/cpl.rs | 657 +++++++++++++++++++ c/sedona-gdal/src/dataset.rs | 456 +++++++++++++ c/sedona-gdal/src/driver.rs | 230 +++++++ c/sedona-gdal/src/dyn_load.rs | 252 +++++++ c/sedona-gdal/src/errors.rs | 73 +++ c/sedona-gdal/src/gdal_api.rs | 100 +++ c/sedona-gdal/src/gdal_dyn_bindgen.rs | 532 +++++++++++++++ c/sedona-gdal/src/geo_transform.rs | 148 +++++ c/sedona-gdal/src/lib.rs | 56 ++ c/sedona-gdal/src/mem.rs | 422 ++++++++++++ c/sedona-gdal/src/raster/mod.rs | 30 + c/sedona-gdal/src/raster/polygonize.rs | 270 ++++++++ c/sedona-gdal/src/raster/rasterband.rs | 355 ++++++++++ c/sedona-gdal/src/raster/rasterize.rs | 255 +++++++ c/sedona-gdal/src/raster/rasterize_affine.rs | 351 ++++++++++ c/sedona-gdal/src/raster/types.rs | 242 +++++++ c/sedona-gdal/src/register.rs | 152 +++++ c/sedona-gdal/src/spatial_ref.rs | 143 ++++ c/sedona-gdal/src/vector/feature.rs | 212 ++++++ c/sedona-gdal/src/vector/geometry.rs | 158 +++++ c/sedona-gdal/src/vector/layer.rs | 114 ++++ c/sedona-gdal/src/vector/mod.rs | 24 + c/sedona-gdal/src/vrt.rs | 324 +++++++++ c/sedona-gdal/src/vsi.rs | 177 +++++ 28 files changed, 5840 insertions(+) create mode 100644 c/sedona-gdal/Cargo.toml 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/dataset.rs create mode 100644 c/sedona-gdal/src/driver.rs create mode 100644 c/sedona-gdal/src/dyn_load.rs create mode 100644 c/sedona-gdal/src/errors.rs create mode 100644 c/sedona-gdal/src/gdal_api.rs create mode 100644 c/sedona-gdal/src/gdal_dyn_bindgen.rs create mode 100644 c/sedona-gdal/src/geo_transform.rs create mode 100644 c/sedona-gdal/src/lib.rs create mode 100644 c/sedona-gdal/src/mem.rs create mode 100644 c/sedona-gdal/src/raster/mod.rs create mode 100644 c/sedona-gdal/src/raster/polygonize.rs create mode 100644 c/sedona-gdal/src/raster/rasterband.rs create mode 100644 c/sedona-gdal/src/raster/rasterize.rs create mode 100644 c/sedona-gdal/src/raster/rasterize_affine.rs create mode 100644 c/sedona-gdal/src/raster/types.rs create mode 100644 c/sedona-gdal/src/register.rs create mode 100644 c/sedona-gdal/src/spatial_ref.rs create mode 100644 c/sedona-gdal/src/vector/feature.rs create mode 100644 c/sedona-gdal/src/vector/geometry.rs create mode 100644 c/sedona-gdal/src/vector/layer.rs create mode 100644 c/sedona-gdal/src/vector/mod.rs create mode 100644 c/sedona-gdal/src/vrt.rs create mode 100644 c/sedona-gdal/src/vsi.rs diff --git a/Cargo.lock b/Cargo.lock index ac91274b2..a7b5bd24b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2736,6 +2736,17 @@ dependencies = [ "slab", ] +[[package]] +name = "gdal-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cceef1cc08a1f031c5717cb645bb361a3114470cc142cc96bc5e62b79695632e" +dependencies = [ + "bindgen", + "pkg-config", + "semver", +] + [[package]] name = "generational-arena" version = "0.2.9" @@ -5252,6 +5263,16 @@ dependencies = [ "wkt 0.14.0", ] +[[package]] +name = "sedona-gdal" +version = "0.3.0" +dependencies = [ + "gdal-sys", + "libloading 0.9.0", + "sedona-testing", + "thiserror 2.0.17", +] + [[package]] name = "sedona-geo" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index abe66917b..118b84fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "c/sedona-geoarrow-c", "c/sedona-geos", "c/sedona-libgpuspatial", + "c/sedona-gdal", "c/sedona-proj", "c/sedona-s2geography", "c/sedona-tg", @@ -103,6 +104,7 @@ geo-types = "0.7.17" geojson = "0.24.2" geos = { version = "11.0.1", features = ["geo", "v3_12_0"] } glam = "0.32.0" +libloading = "0.9" libmimalloc-sys = { version = "0.1", default-features = false } log = "^0.4" libloading = "0.9" @@ -149,6 +151,7 @@ sedona-testing = { version = "0.3.0", path = "rust/sedona-testing" } # C wrapper crates sedona-geoarrow-c = { version = "0.3.0", path = "c/sedona-geoarrow-c" } sedona-geos = { version = "0.3.0", path = "c/sedona-geos" } +sedona-gdal = { version = "0.3.0", path = "c/sedona-gdal", default-features = false } sedona-proj = { version = "0.3.0", path = "c/sedona-proj", default-features = false } sedona-s2geography = { version = "0.3.0", path = "c/sedona-s2geography" } sedona-tg = { version = "0.3.0", path = "c/sedona-tg" } diff --git a/c/sedona-gdal/Cargo.toml b/c/sedona-gdal/Cargo.toml new file mode 100644 index 000000000..1984d3e22 --- /dev/null +++ b/c/sedona-gdal/Cargo.toml @@ -0,0 +1,45 @@ +# 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. + +[package] +name = "sedona-gdal" +version.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +description.workspace = true +readme.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["staticlib", "cdylib", "lib"] + +[dependencies] +gdal-sys = { version = "0.12.0", features = ["bindgen"], optional = true } +libloading = { workspace = true } +thiserror = { workspace = true } + +[features] +default = ["gdal-sys"] +gdal-sys = ["dep:gdal-sys"] + +[dev-dependencies] +sedona-testing = { workspace = true } diff --git a/c/sedona-gdal/src/config.rs b/c/sedona-gdal/src/config.rs new file mode 100644 index 000000000..2f9b1910b --- /dev/null +++ b/c/sedona-gdal/src/config.rs @@ -0,0 +1,38 @@ +// 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. + +//! 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..15257285c --- /dev/null +++ b/c/sedona-gdal/src/cpl.rs @@ -0,0 +1,657 @@ +// 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. + +//! 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/dataset.rs b/c/sedona-gdal/src/dataset.rs new file mode 100644 index 000000000..49084ec55 --- /dev/null +++ b/c/sedona-gdal/src/dataset.rs @@ -0,0 +1,456 @@ +// 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. + +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::register::get_global_gdal_api; + + #[test] + fn test_geo_transform_roundtrip() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_geo_transform_unset() { + let api = get_global_gdal_api().unwrap(); + 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()); + } + + #[test] + fn test_set_projection_roundtrip() { + let api = get_global_gdal_api().unwrap(); + 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}"); + } + + #[test] + fn test_dataset_raster_count() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_dataset_raster_size() { + let api = get_global_gdal_api().unwrap(); + 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)); + } +} diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs new file mode 100644 index 000000000..9bddb58dd --- /dev/null +++ b/c/sedona-gdal/src/driver.rs @@ -0,0 +1,230 @@ +// 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. + +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(), + 0, + 0, + 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::raster::types::GdalDataType; + use crate::register::get_global_gdal_api; + + #[test] + fn test_get_driver_by_name() { + let api = get_global_gdal_api().unwrap(); + 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()); + } + + #[test] + fn test_get_driver_by_name_invalid() { + let api = get_global_gdal_api().unwrap(); + let err = DriverManager::get_driver_by_name(api, "NO_SUCH_DRIVER"); + assert!(matches!(err, Err(GdalError::NullPointer { .. }))); + } + + #[test] + fn test_driver_create() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_driver_create_with_band_type() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_driver_create_with_data_type() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_driver_create_vector_only() { + let api = get_global_gdal_api().unwrap(); + 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(), (0, 0)); + } +} diff --git a/c/sedona-gdal/src/dyn_load.rs b/c/sedona-gdal/src/dyn_load.rs new file mode 100644 index 000000000..97d3bf015 --- /dev/null +++ b/c/sedona-gdal/src/dyn_load.rs @@ -0,0 +1,252 @@ +// 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. + +use std::path::Path; + +use libloading::Library; + +use crate::errors::GdalInitLibraryError; +use crate::gdal_dyn_bindgen::SedonaGdalApi; + +/// Load a single symbol from the library and write it into the given field. +/// +/// We load as a raw `*const ()` pointer and transmute to the target function pointer +/// type. This is the standard pattern for dynamic symbol loading where the loaded +/// symbol's signature is known but cannot be expressed as a generic parameter to +/// `Library::get` (because each field has a different signature). +/// +/// On failure returns a `GdalInitLibraryError` with the symbol name and the +/// underlying OS error message. +macro_rules! load_fn { + ($lib:expr, $api:expr, $name:ident) => { + // The target types here are too verbose to annotate for each call site + #[allow(clippy::missing_transmute_annotations)] + { + $api.$name = Some(unsafe { + let sym = $lib + .get::<*const ()>(concat!(stringify!($name), "\0").as_bytes()) + .map_err(|e| { + GdalInitLibraryError::LibraryError(format!( + "Failed to load symbol {}: {}", + stringify!($name), + e + )) + })?; + std::mem::transmute(sym.into_raw().into_raw()) + }); + } + }; +} + +/// Try to load a symbol under one of several names (e.g. for C++ mangled symbols). +/// Writes the first successful match into `$api.$field`. Returns an error only if +/// *none* of the names resolve. +macro_rules! load_fn_any { + ($lib:expr, $api:expr, $field:ident, [$($name:expr),+ $(,)?]) => {{ + let mut found = false; + $( + if !found { + if let Ok(sym) = unsafe { $lib.get::<*const ()>($name) } { + // The target types here are too verbose to annotate for each call site + #[allow(clippy::missing_transmute_annotations)] + { + $api.$field = Some(unsafe { std::mem::transmute(sym.into_raw().into_raw()) }); + } + found = true; + } + } + )+ + if !found { + return Err(GdalInitLibraryError::LibraryError(format!( + "Failed to resolve {} under any known mangled name", + stringify!($field), + ))); + } + }}; +} + +/// Populate all 64 function-pointer fields of [`SedonaGdalApi`] from the given +/// [`Library`] handle. +fn load_all_symbols(lib: &Library, api: &mut SedonaGdalApi) -> Result<(), GdalInitLibraryError> { + // --- Dataset --- + load_fn!(lib, api, GDALOpenEx); + load_fn!(lib, api, GDALClose); + load_fn!(lib, api, GDALGetRasterXSize); + load_fn!(lib, api, GDALGetRasterYSize); + load_fn!(lib, api, GDALGetRasterCount); + load_fn!(lib, api, GDALGetRasterBand); + load_fn!(lib, api, GDALGetGeoTransform); + load_fn!(lib, api, GDALSetGeoTransform); + load_fn!(lib, api, GDALGetProjectionRef); + load_fn!(lib, api, GDALSetProjection); + load_fn!(lib, api, GDALGetSpatialRef); + load_fn!(lib, api, GDALSetSpatialRef); + load_fn!(lib, api, GDALCreateCopy); + load_fn!(lib, api, GDALDatasetCreateLayer); + + // --- Driver --- + load_fn!(lib, api, GDALAllRegister); + load_fn!(lib, api, GDALGetDriverByName); + load_fn!(lib, api, GDALCreate); + + // --- Band --- + load_fn!(lib, api, GDALAddBand); + load_fn!(lib, api, GDALRasterIO); + load_fn!(lib, api, GDALRasterIOEx); + load_fn!(lib, api, GDALGetRasterDataType); + load_fn!(lib, api, GDALGetRasterBandXSize); + load_fn!(lib, api, GDALGetRasterBandYSize); + load_fn!(lib, api, GDALGetBlockSize); + load_fn!(lib, api, GDALGetRasterNoDataValue); + load_fn!(lib, api, GDALSetRasterNoDataValue); + load_fn!(lib, api, GDALDeleteRasterNoDataValue); + load_fn!(lib, api, GDALSetRasterNoDataValueAsUInt64); + load_fn!(lib, api, GDALSetRasterNoDataValueAsInt64); + + // --- SpatialRef --- + load_fn!(lib, api, OSRNewSpatialReference); + load_fn!(lib, api, OSRDestroySpatialReference); + load_fn!(lib, api, OSRExportToPROJJSON); + load_fn!(lib, api, OSRClone); + load_fn!(lib, api, OSRRelease); + + // --- Geometry --- + load_fn!(lib, api, OGR_G_CreateFromWkb); + load_fn!(lib, api, OGR_G_CreateFromWkt); + load_fn!(lib, api, OGR_G_ExportToIsoWkb); + load_fn!(lib, api, OGR_G_WkbSize); + load_fn!(lib, api, OGR_G_GetEnvelope); + load_fn!(lib, api, OGR_G_DestroyGeometry); + + // --- Vector / Layer --- + load_fn!(lib, api, OGR_L_ResetReading); + load_fn!(lib, api, OGR_L_GetNextFeature); + load_fn!(lib, api, OGR_L_CreateField); + load_fn!(lib, api, OGR_L_GetFeatureCount); + load_fn!(lib, api, OGR_F_GetGeometryRef); + load_fn!(lib, api, OGR_F_GetFieldIndex); + load_fn!(lib, api, OGR_F_GetFieldAsDouble); + load_fn!(lib, api, OGR_F_GetFieldAsInteger); + load_fn!(lib, api, OGR_F_IsFieldSetAndNotNull); + load_fn!(lib, api, OGR_F_Destroy); + load_fn!(lib, api, OGR_Fld_Create); + load_fn!(lib, api, OGR_Fld_Destroy); + + // --- VSI --- + load_fn!(lib, api, VSIFileFromMemBuffer); + load_fn!(lib, api, VSIFCloseL); + load_fn!(lib, api, VSIUnlink); + load_fn!(lib, api, VSIGetMemFileBuffer); + load_fn!(lib, api, VSIFree); + load_fn!(lib, api, VSIMalloc); + + // --- VRT --- + load_fn!(lib, api, VRTCreate); + load_fn!(lib, api, VRTAddSimpleSource); + + // --- Rasterize / Polygonize --- + load_fn!(lib, api, GDALRasterizeGeometries); + load_fn!(lib, api, GDALFPolygonize); + load_fn!(lib, api, GDALPolygonize); + + // --- Config --- + load_fn!(lib, api, CPLSetThreadLocalConfigOption); + + // --- Error --- + load_fn!(lib, api, CPLGetLastErrorNo); + load_fn!(lib, api, CPLGetLastErrorMsg); + load_fn!(lib, api, CPLErrorReset); + + // --- Data Type --- + load_fn!(lib, api, GDALGetDataTypeSizeBytes); + + // --- C++ API: MEMDataset::Create (resolved via mangled symbol names) --- + // The symbol is mangled differently on Linux, macOS, and MSVC, and the + // `char**` vs `const char**` parameter also affects the mangling. + load_fn_any!( + lib, + api, + MEMDatasetCreate, + [ + // Linux (char**) + b"_ZN10MEMDataset6CreateEPKciii12GDALDataTypePPc\0", + // macOS (char**) + b"__ZN10MEMDataset6CreateEPKciii12GDALDataTypePPc\0", + // Linux (const char**) + b"_ZN10MEMDataset6CreateEPKciii12GDALDataTypePPKc\0", + // macOS (const char**) + b"__ZN10MEMDataset6CreateEPKciii12GDALDataTypePPKc\0", + // MSVC (char**) + b"?Create@MEMDataset@@SAPEAV1@PEBDHHH4GDALDataType@@PEAPEAD@Z\0", + // MSVC (const char**) + b"?Create@MEMDataset@@SAPEAV1@PEBDHHH4GDALDataType@@PEAPEBAD@Z\0", + ] + ); + + Ok(()) +} + +/// Load a GDAL shared library from `path` and populate a [`SedonaGdalApi`] struct. +/// +/// Returns the `(Library, SedonaGdalApi)` pair. The caller is responsible for +/// keeping the `Library` alive for the lifetime of the function pointers. +pub(crate) fn load_gdal_from_path( + path: &Path, +) -> Result<(Library, SedonaGdalApi), GdalInitLibraryError> { + let lib = unsafe { Library::new(path.as_os_str()) }.map_err(|e| { + GdalInitLibraryError::LibraryError(format!( + "Failed to load GDAL library from {}: {}", + path.display(), + e + )) + })?; + + let mut api = SedonaGdalApi::default(); + load_all_symbols(&lib, &mut api)?; + Ok((lib, api)) +} + +/// Load GDAL symbols from the current process image (equivalent to `dlopen(NULL)`). +/// +/// Returns the `(Library, SedonaGdalApi)` pair. The caller is responsible for +/// keeping the `Library` alive for the lifetime of the function pointers. +pub(crate) fn load_gdal_from_current_process( +) -> Result<(Library, SedonaGdalApi), GdalInitLibraryError> { + let lib = current_process_library()?; + let mut api = SedonaGdalApi::default(); + load_all_symbols(&lib, &mut api)?; + Ok((lib, api)) +} + +/// Open a handle to the current process image. +#[cfg(unix)] +fn current_process_library() -> Result { + Ok(libloading::os::unix::Library::this().into()) +} + +#[cfg(windows)] +fn current_process_library() -> Result { + // Safety: loading symbols from the current process is safe. + Ok(unsafe { libloading::os::windows::Library::this() } + .map_err(|e| { + GdalInitLibraryError::LibraryError(format!( + "Failed to open current process handle: {}", + e + )) + })? + .into()) +} diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs new file mode 100644 index 000000000..3e990d80e --- /dev/null +++ b/c/sedona-gdal/src/errors.rs @@ -0,0 +1,73 @@ +// 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. + +use std::ffi::NulError; +use std::num::TryFromIntError; + +use thiserror::Error; + +/// Error type for the sedona-gdal crate initialization and library loading. +#[derive(Error, Debug)] +pub enum GdalInitLibraryError { + #[error("{0}")] + Invalid(String), + #[error("{0}")] + LibraryError(String), +} + +/// Error type compatible with the georust/gdal error variants used in this codebase. +#[derive(Clone, Debug, Error)] +pub enum GdalError { + #[error("CPL error class: '{class:?}', error number: '{number}', error msg: '{msg}'")] + CplError { + class: u32, + number: i32, + msg: String, + }, + + #[error("GDAL method '{method_name}' returned a NULL pointer. Error msg: '{msg}'")] + NullPointer { + method_name: &'static str, + msg: String, + }, + + #[error("Bad argument: {0}")] + BadArgument(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("FfiIntoStringError")] + FfiIntoStringError(#[from] std::ffi::IntoStringError), + + #[error("StrUtf8Error")] + StrUtf8Error(#[from] std::str::Utf8Error), + + #[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/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs new file mode 100644 index 000000000..1d110097e --- /dev/null +++ b/c/sedona-gdal/src/gdal_api.rs @@ -0,0 +1,100 @@ +// 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. + +use std::ffi::CStr; +use std::path::PathBuf; + +use libloading::Library; + +use crate::dyn_load; +use crate::errors::{GdalError, GdalInitLibraryError}; +use crate::gdal_dyn_bindgen::SedonaGdalApi; + +/// Invoke a function pointer from the `SedonaGdalApi` struct. +/// +/// # Panics +/// +/// Panics if the function pointer is `None`. This is unreachable in correct usage +/// because all function pointers are guaranteed to be `Some` after successful +/// `sedona_gdal_dyn_api_init` (the C loader fails if any symbol is missing), +/// and you cannot obtain a `&GdalApi` without successful initialization +/// (it's behind `OnceLock`). +macro_rules! call_gdal_api { + ($api:expr, $func:ident $(, $arg:expr)*) => { + if let Some(func) = $api.inner.$func { + func($($arg),*) + } else { + panic!("{} function not available", stringify!($func)) + } + }; +} + +// Re-export for use in other modules in this crate +pub(crate) use call_gdal_api; + +#[derive(Debug)] +pub struct GdalApi { + pub(crate) inner: SedonaGdalApi, + /// The dynamically loaded GDAL library. Kept alive for the lifetime of the + /// function pointers in `inner`. This is never dropped because the `GdalApi` + /// lives in a `static OnceLock` (see `register.rs`). + _lib: Library, + name: String, +} + +impl GdalApi { + pub fn try_from_shared_library(shared_library: PathBuf) -> Result { + let (lib, inner) = dyn_load::load_gdal_from_path(&shared_library)?; + Ok(Self { + inner, + _lib: lib, + name: shared_library.to_string_lossy().into_owned(), + }) + } + + pub fn try_from_current_process() -> Result { + let (lib, inner) = dyn_load::load_gdal_from_current_process()?; + Ok(Self { + inner, + _lib: lib, + name: "current_process".to_string(), + }) + } + + pub fn name(&self) -> &str { + &self.name + } + + /// Check the last CPL error and return a `GdalError` if there was one. + pub fn last_cpl_err(&self, default_err_class: u32) -> GdalError { + let err_no = unsafe { call_gdal_api!(self, CPLGetLastErrorNo) }; + let err_msg = unsafe { + let msg_ptr = call_gdal_api!(self, CPLGetLastErrorMsg); + if msg_ptr.is_null() { + String::new() + } else { + CStr::from_ptr(msg_ptr).to_string_lossy().into_owned() + } + }; + unsafe { call_gdal_api!(self, CPLErrorReset) }; + GdalError::CplError { + class: default_err_class, + number: err_no, + msg: err_msg, + } + } +} diff --git a/c/sedona-gdal/src/gdal_dyn_bindgen.rs b/c/sedona-gdal/src/gdal_dyn_bindgen.rs new file mode 100644 index 000000000..1489d40d4 --- /dev/null +++ b/c/sedona-gdal/src/gdal_dyn_bindgen.rs @@ -0,0 +1,532 @@ +// 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. +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(dead_code)] +#![allow(clippy::type_complexity)] + +use std::os::raw::{c_char, c_double, c_int, c_uchar, c_uint, c_void}; + +// --- Scalar type aliases --- + +pub type GSpacing = i64; +pub type CPLErr = c_int; +pub type OGRErr = c_int; +pub type GDALRWFlag = c_int; +pub type OGRwkbByteOrder = c_int; +pub type GDALOpenFlags = c_uint; +pub type GDALRIOResampleAlg = c_int; + +// --- Opaque handle types --- + +pub type GDALDatasetH = *mut c_void; +pub type GDALDriverH = *mut c_void; +pub type GDALRasterBandH = *mut c_void; +pub type OGRSpatialReferenceH = *mut c_void; +pub type OGRGeometryH = *mut c_void; +pub type OGRLayerH = *mut c_void; +pub type OGRFeatureH = *mut c_void; +pub type OGRFieldDefnH = *mut c_void; +pub type VSILFILE = *mut c_void; + +// --- Enum types --- + +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum GDALDataType { + GDT_Unknown = 0, + GDT_Byte = 1, + GDT_Int8 = 14, + GDT_UInt16 = 2, + GDT_Int16 = 3, + GDT_UInt32 = 4, + GDT_Int32 = 5, + GDT_UInt64 = 12, + GDT_Int64 = 13, + GDT_Float16 = 15, + GDT_Float32 = 6, + GDT_Float64 = 7, + GDT_CInt16 = 8, + GDT_CInt32 = 9, + GDT_CFloat16 = 16, + GDT_CFloat32 = 10, + GDT_CFloat64 = 11, + GDT_TypeCount = 17, +} + +impl GDALDataType { + #[allow(clippy::result_unit_err)] + pub fn try_from_ordinal(value: i32) -> Result { + match value { + 0 => Ok(GDALDataType::GDT_Unknown), + 1 => Ok(GDALDataType::GDT_Byte), + 2 => Ok(GDALDataType::GDT_UInt16), + 3 => Ok(GDALDataType::GDT_Int16), + 4 => Ok(GDALDataType::GDT_UInt32), + 5 => Ok(GDALDataType::GDT_Int32), + 6 => Ok(GDALDataType::GDT_Float32), + 7 => Ok(GDALDataType::GDT_Float64), + 8 => Ok(GDALDataType::GDT_CInt16), + 9 => Ok(GDALDataType::GDT_CInt32), + 10 => Ok(GDALDataType::GDT_CFloat32), + 11 => Ok(GDALDataType::GDT_CFloat64), + 12 => Ok(GDALDataType::GDT_UInt64), + 13 => Ok(GDALDataType::GDT_Int64), + 14 => Ok(GDALDataType::GDT_Int8), + 15 => Ok(GDALDataType::GDT_Float16), + 16 => Ok(GDALDataType::GDT_CFloat16), + 17 => Ok(GDALDataType::GDT_TypeCount), + _ => Err(()), + } + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum OGRwkbGeometryType { + wkbUnknown = 0, + wkbPoint = 1, + wkbLineString = 2, + wkbPolygon = 3, + wkbMultiPoint = 4, + wkbMultiLineString = 5, + wkbMultiPolygon = 6, + wkbGeometryCollection = 7, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum OGRFieldType { + OFTInteger = 0, + OFTIntegerList = 1, + OFTReal = 2, + OFTRealList = 3, + OFTString = 4, + OFTStringList = 5, + OFTWideString = 6, + OFTWideStringList = 7, + OFTBinary = 8, + OFTDate = 9, + OFTTime = 10, + OFTDateTime = 11, + OFTInteger64 = 12, + OFTInteger64List = 13, +} + +// --- Function pointer type aliases --- + +/// Type alias for the GDAL transformer callback (`GDALTransformerFunc`). +/// +/// Signature: `(pTransformerArg, bDstToSrc, nPointCount, x, y, z, panSuccess) -> c_int` +pub type GDALTransformerFunc = unsafe extern "C" fn( + pTransformerArg: *mut c_void, + bDstToSrc: c_int, + nPointCount: c_int, + x: *mut c_double, + y: *mut c_double, + z: *mut c_double, + panSuccess: *mut c_int, +) -> c_int; + +// --- Structs --- + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct OGREnvelope { + pub MinX: c_double, + pub MaxX: c_double, + pub MinY: c_double, + pub MaxY: c_double, +} + +/// GDAL progress callback type. +pub type GDALProgressFunc = Option< + unsafe extern "C" fn( + dfComplete: c_double, + pszMessage: *const c_char, + pProgressArg: *mut c_void, + ) -> c_int, +>; + +/// Extra arguments for `GDALRasterIOEx`. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct GDALRasterIOExtraArg { + pub nVersion: c_int, + pub eResampleAlg: GDALRIOResampleAlg, + pub pfnProgress: GDALProgressFunc, + pub pProgressData: *mut c_void, + pub bFloatingPointWindowValidity: c_int, + pub dfXOff: c_double, + pub dfYOff: c_double, + pub dfXSize: c_double, + pub dfYSize: c_double, +} + +impl Default for GDALRasterIOExtraArg { + fn default() -> Self { + Self { + nVersion: 1, + eResampleAlg: GRIORA_NearestNeighbour, + pfnProgress: None, + pProgressData: std::ptr::null_mut(), + bFloatingPointWindowValidity: 0, + dfXOff: 0.0, + dfYOff: 0.0, + dfXSize: 0.0, + dfYSize: 0.0, + } + } +} + +// --- GDALRIOResampleAlg constants --- + +pub const GRIORA_NearestNeighbour: GDALRIOResampleAlg = 0; +pub const GRIORA_Bilinear: GDALRIOResampleAlg = 1; +pub const GRIORA_Cubic: GDALRIOResampleAlg = 2; +pub const GRIORA_CubicSpline: GDALRIOResampleAlg = 3; +pub const GRIORA_Lanczos: GDALRIOResampleAlg = 4; +pub const GRIORA_Average: GDALRIOResampleAlg = 5; +pub const GRIORA_Mode: GDALRIOResampleAlg = 6; +pub const GRIORA_Gauss: GDALRIOResampleAlg = 7; + +// --- GDAL open flags constants --- + +pub const GDAL_OF_READONLY: GDALOpenFlags = 0x00; +pub const GDAL_OF_UPDATE: GDALOpenFlags = 0x01; +pub const GDAL_OF_RASTER: GDALOpenFlags = 0x02; +pub const GDAL_OF_VECTOR: GDALOpenFlags = 0x04; +pub const GDAL_OF_VERBOSE_ERROR: GDALOpenFlags = 0x40; + +// --- GDALRWFlag constants --- + +pub const GF_Read: GDALRWFlag = 0; +pub const GF_Write: GDALRWFlag = 1; + +// --- CPLErr constants --- + +pub const CE_None: CPLErr = 0; +pub const CE_Debug: CPLErr = 1; +pub const CE_Warning: CPLErr = 2; +pub const CE_Failure: CPLErr = 3; +pub const CE_Fatal: CPLErr = 4; + +// --- OGRErr constants --- + +pub const OGRERR_NONE: OGRErr = 0; + +// --- OGRwkbByteOrder constants --- + +pub const wkbXDR: OGRwkbByteOrder = 0; // Big endian +pub const wkbNDR: OGRwkbByteOrder = 1; // Little endian + +// --- The main API struct mirroring C SedonaGdalApi --- + +#[repr(C)] +#[derive(Debug, Copy, Clone, Default)] +pub struct SedonaGdalApi { + // --- Dataset --- + pub GDALOpenEx: Option< + unsafe extern "C" fn( + pszFilename: *const c_char, + nOpenFlags: c_uint, + papszAllowedDrivers: *const *const c_char, + papszOpenOptions: *const *const c_char, + papszSiblingFiles: *const *const c_char, + ) -> GDALDatasetH, + >, + pub GDALClose: Option, + pub GDALGetRasterXSize: Option c_int>, + pub GDALGetRasterYSize: Option c_int>, + pub GDALGetRasterCount: Option c_int>, + pub GDALGetRasterBand: + Option GDALRasterBandH>, + pub GDALGetGeoTransform: + Option CPLErr>, + pub GDALSetGeoTransform: + Option CPLErr>, + pub GDALGetProjectionRef: Option *const c_char>, + pub GDALSetProjection: + Option CPLErr>, + pub GDALGetSpatialRef: Option OGRSpatialReferenceH>, + pub GDALSetSpatialRef: + Option CPLErr>, + pub GDALCreateCopy: Option< + unsafe extern "C" fn( + hDriver: GDALDriverH, + pszFilename: *const c_char, + hSrcDS: GDALDatasetH, + bStrict: c_int, + papszOptions: *mut *mut c_char, + pfnProgress: *mut c_void, + pProgressData: *mut c_void, + ) -> GDALDatasetH, + >, + pub GDALDatasetCreateLayer: Option< + unsafe extern "C" fn( + hDS: GDALDatasetH, + pszName: *const c_char, + hSpatialRef: OGRSpatialReferenceH, + eGType: OGRwkbGeometryType, + papszOptions: *mut *mut c_char, + ) -> OGRLayerH, + >, + + // --- Driver --- + pub GDALAllRegister: Option, + pub GDALGetDriverByName: Option GDALDriverH>, + pub GDALCreate: Option< + unsafe extern "C" fn( + hDriver: GDALDriverH, + pszFilename: *const c_char, + nXSize: c_int, + nYSize: c_int, + nBands: c_int, + eType: GDALDataType, + papszOptions: *mut *mut c_char, + ) -> GDALDatasetH, + >, + + // --- Band --- + pub GDALAddBand: Option< + unsafe extern "C" fn( + hDS: GDALDatasetH, + eType: GDALDataType, + papszOptions: *mut *mut c_char, + ) -> CPLErr, + >, + pub GDALRasterIO: Option< + unsafe extern "C" fn( + hRBand: GDALRasterBandH, + eRWFlag: GDALRWFlag, + nDSXOff: c_int, + nDSYOff: c_int, + nDSXSize: c_int, + nDSYSize: c_int, + pBuffer: *mut c_void, + nBXSize: c_int, + nBYSize: c_int, + eBDataType: GDALDataType, + nPixelSpace: GSpacing, + nLineSpace: GSpacing, + ) -> CPLErr, + >, + pub GDALRasterIOEx: Option< + unsafe extern "C" fn( + hRBand: GDALRasterBandH, + eRWFlag: GDALRWFlag, + nDSXOff: c_int, + nDSYOff: c_int, + nDSXSize: c_int, + nDSYSize: c_int, + pBuffer: *mut c_void, + nBXSize: c_int, + nBYSize: c_int, + eBDataType: GDALDataType, + nPixelSpace: GSpacing, + nLineSpace: GSpacing, + psExtraArg: *mut GDALRasterIOExtraArg, + ) -> CPLErr, + >, + pub GDALGetRasterDataType: Option GDALDataType>, + pub GDALGetRasterBandXSize: Option c_int>, + pub GDALGetRasterBandYSize: Option c_int>, + pub GDALGetBlockSize: Option< + unsafe extern "C" fn(hBand: GDALRasterBandH, pnXSize: *mut c_int, pnYSize: *mut c_int), + >, + pub GDALGetRasterNoDataValue: + Option c_double>, + pub GDALSetRasterNoDataValue: + Option CPLErr>, + pub GDALDeleteRasterNoDataValue: Option CPLErr>, + pub GDALSetRasterNoDataValueAsUInt64: + Option CPLErr>, + pub GDALSetRasterNoDataValueAsInt64: + Option CPLErr>, + + // --- SpatialRef --- + pub OSRNewSpatialReference: + Option OGRSpatialReferenceH>, + pub OSRDestroySpatialReference: Option, + pub OSRExportToPROJJSON: Option< + unsafe extern "C" fn( + hSRS: OGRSpatialReferenceH, + ppszResult: *mut *mut c_char, + papszOptions: *const *const c_char, + ) -> OGRErr, + >, + pub OSRClone: Option OGRSpatialReferenceH>, + pub OSRRelease: Option, + + // --- Geometry --- + pub OGR_G_CreateFromWkb: Option< + unsafe extern "C" fn( + pabyData: *const c_void, + hSRS: OGRSpatialReferenceH, + phGeometry: *mut OGRGeometryH, + nBytes: c_int, + ) -> OGRErr, + >, + pub OGR_G_CreateFromWkt: Option< + unsafe extern "C" fn( + ppszData: *mut *mut c_char, + hSRS: OGRSpatialReferenceH, + phGeometry: *mut OGRGeometryH, + ) -> OGRErr, + >, + pub OGR_G_ExportToIsoWkb: Option< + unsafe extern "C" fn( + hGeom: OGRGeometryH, + eOrder: OGRwkbByteOrder, + pabyData: *mut c_uchar, + ) -> OGRErr, + >, + pub OGR_G_WkbSize: Option c_int>, + pub OGR_G_GetEnvelope: + Option, + pub OGR_G_DestroyGeometry: Option, + + // --- Vector / Layer --- + pub OGR_L_ResetReading: Option, + pub OGR_L_GetNextFeature: Option OGRFeatureH>, + pub OGR_L_CreateField: Option< + unsafe extern "C" fn( + hLayer: OGRLayerH, + hFieldDefn: OGRFieldDefnH, + bApproxOK: c_int, + ) -> OGRErr, + >, + pub OGR_L_GetFeatureCount: + Option i64>, + pub OGR_F_GetGeometryRef: Option OGRGeometryH>, + pub OGR_F_GetFieldIndex: + Option c_int>, + pub OGR_F_GetFieldAsDouble: + Option c_double>, + pub OGR_F_GetFieldAsInteger: + Option c_int>, + pub OGR_F_IsFieldSetAndNotNull: + Option c_int>, + pub OGR_F_Destroy: Option, + pub OGR_Fld_Create: + Option OGRFieldDefnH>, + pub OGR_Fld_Destroy: Option, + + // --- VSI --- + pub VSIFileFromMemBuffer: Option< + unsafe extern "C" fn( + pszFilename: *const c_char, + pabyData: *mut c_uchar, + nDataLength: i64, + bTakeOwnership: c_int, + ) -> VSILFILE, + >, + pub VSIFCloseL: Option c_int>, + pub VSIUnlink: Option c_int>, + pub VSIGetMemFileBuffer: Option< + unsafe extern "C" fn( + pszFilename: *const c_char, + pnDataLength: *mut i64, + bUnlinkAndSeize: c_int, + ) -> *mut c_uchar, + >, + pub VSIFree: Option, + pub VSIMalloc: Option *mut c_void>, + + // --- VRT --- + pub VRTCreate: Option GDALDatasetH>, + pub VRTAddSimpleSource: Option< + unsafe extern "C" fn( + hVRTBand: GDALRasterBandH, + hSrcBand: GDALRasterBandH, + nSrcXOff: c_int, + nSrcYOff: c_int, + nSrcXSize: c_int, + nSrcYSize: c_int, + nDstXOff: c_int, + nDstYOff: c_int, + nDstXSize: c_int, + nDstYSize: c_int, + pszResampling: *const c_char, + dfNoDataValue: c_double, + ) -> CPLErr, + >, + + // --- Rasterize / Polygonize --- + pub GDALRasterizeGeometries: Option< + unsafe extern "C" fn( + hDS: GDALDatasetH, + nBandCount: c_int, + panBandList: *const c_int, + nGeomCount: c_int, + pahGeometries: *const OGRGeometryH, + pfnTransformer: *mut c_void, + pTransformArg: *mut c_void, + padfGeomBurnValues: *const c_double, + papszOptions: *mut *mut c_char, + pfnProgress: *mut c_void, + pProgressData: *mut c_void, + ) -> CPLErr, + >, + pub GDALFPolygonize: Option< + unsafe extern "C" fn( + hSrcBand: GDALRasterBandH, + hMaskBand: GDALRasterBandH, + hOutLayer: OGRLayerH, + iPixValField: c_int, + papszOptions: *mut *mut c_char, + pfnProgress: *mut c_void, + pProgressData: *mut c_void, + ) -> CPLErr, + >, + pub GDALPolygonize: Option< + unsafe extern "C" fn( + hSrcBand: GDALRasterBandH, + hMaskBand: GDALRasterBandH, + hOutLayer: OGRLayerH, + iPixValField: c_int, + papszOptions: *mut *mut c_char, + pfnProgress: *mut c_void, + pProgressData: *mut c_void, + ) -> CPLErr, + >, + + // --- Config --- + pub CPLSetThreadLocalConfigOption: + Option, + + // --- Error --- + pub CPLGetLastErrorNo: Option c_int>, + pub CPLGetLastErrorMsg: Option *const c_char>, + pub CPLErrorReset: Option, + + // --- Data Type --- + pub GDALGetDataTypeSizeBytes: Option c_int>, + + // --- C++ API --- + pub MEMDatasetCreate: Option< + unsafe extern "C" fn( + pszFilename: *const c_char, + nXSize: c_int, + nYSize: c_int, + nBandsIn: c_int, + eType: GDALDataType, + papszOptions: *mut *mut c_char, + ) -> GDALDatasetH, + >, +} diff --git a/c/sedona-gdal/src/geo_transform.rs b/c/sedona-gdal/src/geo_transform.rs new file mode 100644 index 000000000..78fb0d19d --- /dev/null +++ b/c/sedona-gdal/src/geo_transform.rs @@ -0,0 +1,148 @@ +// 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. + +//! 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 new file mode 100644 index 000000000..5948f9e1c --- /dev/null +++ b/c/sedona-gdal/src/lib.rs @@ -0,0 +1,56 @@ +// 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. + +// --- FFI layer --- +pub(crate) mod dyn_load; +pub mod gdal_dyn_bindgen; + +// --- Error types --- +pub mod errors; // GdalError + SedonaGdalError + +// --- Core API --- +pub mod gdal_api; +pub mod register; + +// --- High-level wrappers --- +pub mod config; +pub mod cpl; +pub mod dataset; +pub mod driver; +pub mod geo_transform; +pub mod mem; +pub mod raster; +pub mod spatial_ref; +pub mod vector; +pub mod vrt; +pub mod vsi; + +// --- Re-exports for convenient use --- +pub use config::set_thread_local_config_option; +pub use cpl::{CslStringList, CslStringListEntry}; +pub use dataset::{Dataset, LayerOptions}; +pub use driver::{Driver, DriverManager}; +pub use errors::GdalError; +pub use gdal_api::GdalApi; +pub use geo_transform::{GeoTransform, GeoTransformEx}; +pub use mem::{MemDatasetBuilder, Nodata}; +pub use raster::GdalDataType; +pub use register::{ + configure_global_gdal_api, get_global_gdal_api, is_gdal_api_configured, with_global_gdal_api, +}; +pub use spatial_ref::SpatialRef; +pub use vrt::{VrtDataset, VrtRasterBand, NODATA_UNSET}; diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs new file mode 100644 index 000000000..254e02dc5 --- /dev/null +++ b/c/sedona-gdal/src/mem.rs @@ -0,0 +1,422 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! High-level builder for creating in-memory (MEM) GDAL datasets. +//! +//! [`MemDatasetBuilder`] provides a fluent, type-safe API for constructing GDAL MEM +//! datasets with zero-copy band attachment, optional geo-transform, projection, and +//! per-band nodata values. +//! +//! # Example +//! +//! ```rust,ignore +//! use sedona_gdal::mem::{MemDatasetBuilder, Nodata}; +//! use sedona_gdal::GdalDataType; +//! +//! let data: Vec = vec![0u8; 256 * 256]; +//! let dataset = unsafe { +//! MemDatasetBuilder::new(256, 256) +//! .add_band(GdalDataType::UInt8, data.as_ptr()) +//! .geo_transform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0]) +//! .projection("EPSG:4326") +//! .build() +//! .unwrap() +//! }; +//! assert_eq!(dataset.raster_count(), 1); +//! ``` + +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::call_gdal_api; +use crate::gdal_dyn_bindgen::CE_Failure; +use crate::raster::types::GdalDataType; + +/// Nodata value for a raster band. +/// +/// GDAL has three separate APIs for setting nodata depending on the band data type: +/// - [`f64`] for most types (UInt8 through Float64, excluding Int64/UInt64) +/// - [`i64`] for Int64 bands +/// - [`u64`] for UInt64 bands +/// +/// This enum encapsulates all three variants so callers don't need to match on +/// the band type when setting nodata. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Nodata { + F64(f64), + I64(i64), + U64(u64), +} + +/// A band specification for [`MemDatasetBuilder`]. +struct MemBand { + data_type: GdalDataType, + data_ptr: *const u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, +} + +/// A builder for constructing in-memory (MEM) GDAL datasets. +/// +/// This creates datasets using `MEMDataset::Create` (bypassing GDAL's open-dataset-list +/// mutex for better concurrency) and attaches bands via `GDALAddBand` with `DATAPOINTER` +/// options for zero-copy operation. +/// +/// # Safety +/// +/// All `add_band*` methods are `unsafe` because the caller must ensure that the +/// provided data pointers remain valid for the lifetime of the built [`Dataset`]. +pub struct MemDatasetBuilder { + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: Option, + bands: Vec, + geo_transform: Option<[f64; 6]>, + projection: Option, +} + +impl MemDatasetBuilder { + /// Create a new builder for a MEM dataset with the given dimensions. + pub fn new(width: usize, height: usize) -> Self { + Self { + width, + height, + n_owned_bands: 0, + owned_bands_data_type: None, + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a new builder for a MEM dataset with the given dimensions and number of owned bands. + pub fn new_with_owned_bands( + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Self { + Self { + width, + height, + n_owned_bands, + owned_bands_data_type: Some(owned_bands_data_type), + bands: Vec::new(), + geo_transform: None, + projection: None, + } + } + + /// Create a MEM dataset with owned bands + pub fn create( + width: usize, + height: usize, + n_owned_bands: usize, + owned_bands_data_type: GdalDataType, + ) -> Result { + unsafe { + Self::new_with_owned_bands(width, height, n_owned_bands, owned_bands_data_type).build() + } + } + + /// Add a zero-copy band from a raw data pointer. + /// + /// Uses default pixel and line offsets (contiguous, row-major layout). + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of at least + /// `height * width * data_type.byte_size()` bytes, and that the buffer + /// outlives the built [`Dataset`]. + pub unsafe fn add_band(self, data_type: GdalDataType, data_ptr: *const u8) -> Self { + self.add_band_with_options(data_type, data_ptr, None, None, None) + } + + /// Add a zero-copy band with custom offsets and optional nodata. + /// + /// # 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`. + /// * `nodata` - Optional nodata value for the band. + /// + /// # Safety + /// + /// The caller must ensure `data_ptr` points to a valid buffer of sufficient size + /// for the given dimensions and offsets, and that the buffer outlives the built + /// [`Dataset`]. + pub unsafe fn add_band_with_options( + mut self, + data_type: GdalDataType, + data_ptr: *const u8, + pixel_offset: Option, + line_offset: Option, + nodata: Option, + ) -> Self { + self.bands.push(MemBand { + data_type, + data_ptr, + pixel_offset, + line_offset, + nodata, + }); + self + } + + /// Set the geo-transform for the dataset. + /// + /// The array is `[origin_x, pixel_width, rotation_x, origin_y, rotation_y, pixel_height]`. + pub fn geo_transform(mut self, gt: [f64; 6]) -> Self { + self.geo_transform = Some(gt); + self + } + + /// Set the projection (CRS) for the dataset as a WKT or PROJ string. + pub fn projection(mut self, wkt: impl Into) -> Self { + self.projection = Some(wkt.into()); + self + } + + /// Build the GDAL MEM dataset. + /// + /// This creates an empty MEM dataset using `MEMDataset::Create` (bypassing GDAL's + /// open-dataset-list), then attaches bands, sets the geo-transform, projection, + /// and per-band nodata values. + /// + /// # Safety + /// + /// This method is unsafe because the built dataset references memory provided via + /// the `add_band*` methods. The caller must ensure all data pointers remain valid + /// for the lifetime of the returned [`Dataset`]. + pub unsafe fn build(self) -> Result { + let api = crate::register::get_global_gdal_api() + .map_err(|e| GdalError::BadArgument(e.to_string()))?; + + // Create an initial MEM dataset via MEMDataset::Create directly, + // bypassing GDAL's open-dataset-list mutex. + let empty_filename = c""; + let owned_bands_data_type = self + .owned_bands_data_type + .unwrap_or(GdalDataType::UInt8) + .to_c(); + let handle = unsafe { + call_gdal_api!( + api, + MEMDatasetCreate, + empty_filename.as_ptr(), + self.width as i32, + self.height as i32, + self.n_owned_bands as i32, + owned_bands_data_type, + std::ptr::null_mut() + ) + }; + + if handle.is_null() { + return Err(api.last_cpl_err(CE_Failure as u32)); + } + let dataset = Dataset::new_owned(api, handle); + + // Attach bands (zero-copy via DATAPOINTER). + for band_spec in &self.bands { + dataset.add_band_with_data( + band_spec.data_type, + band_spec.data_ptr, + band_spec.pixel_offset, + band_spec.line_offset, + )?; + } + + // Set geo-transform. + if let Some(gt) = &self.geo_transform { + dataset.set_geo_transform(gt)?; + } + + // Set projection/CRS. + if let Some(proj) = &self.projection { + dataset.set_projection(proj)?; + } + + // Set per-band nodata values. + for (i, band_spec) in self.bands.iter().enumerate() { + if let Some(nodata) = &band_spec.nodata { + let raster_band = dataset.rasterband(i + 1)?; + match nodata { + Nodata::F64(v) => raster_band.set_no_data_value(Some(*v))?, + Nodata::I64(v) => raster_band.set_no_data_value_i64(Some(*v))?, + Nodata::U64(v) => raster_band.set_no_data_value_u64(Some(*v))?, + } + } + } + + Ok(dataset) + } +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::mem::{MemDatasetBuilder, Nodata}; + use crate::raster::types::GdalDataType; + + #[test] + fn test_mem_builder_single_band() { + let data = vec![42u8; 64 * 64]; + let dataset = unsafe { + MemDatasetBuilder::new(64, 64) + .add_band(GdalDataType::UInt8, data.as_ptr()) + .build() + .unwrap() + }; + assert_eq!(dataset.raster_size(), (64, 64)); + assert_eq!(dataset.raster_count(), 1); + } + + #[test] + fn test_mem_builder_multi_band() { + let band1 = vec![1u16; 32 * 32]; + let band2 = vec![2u16; 32 * 32]; + let band3 = vec![3u16; 32 * 32]; + let dataset = unsafe { + MemDatasetBuilder::new(32, 32) + .add_band(GdalDataType::UInt16, band1.as_ptr() as *const u8) + .add_band(GdalDataType::UInt16, band2.as_ptr() as *const u8) + .add_band(GdalDataType::UInt16, band3.as_ptr() as *const u8) + .build() + .unwrap() + }; + assert_eq!(dataset.raster_count(), 3); + } + + #[test] + fn test_mem_builder_with_geo_transform() { + let data = vec![0f32; 10 * 10]; + let gt = [100.0, 0.5, 0.0, 200.0, 0.0, -0.5]; + let dataset = unsafe { + MemDatasetBuilder::new(10, 10) + .add_band(GdalDataType::Float32, data.as_ptr() as *const u8) + .geo_transform(gt) + .build() + .unwrap() + }; + let got = dataset.geo_transform().unwrap(); + assert_eq!(gt, got); + } + + #[test] + fn test_mem_builder_with_projection() { + let data = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, data.as_ptr()) + .projection(r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#) + .build() + .unwrap() + }; + let proj = dataset.projection(); + assert!(proj.contains("WGS 84"), "Expected WGS 84 in: {proj}"); + } + + #[test] + fn test_mem_builder_with_nodata() { + let data = [0f64; 4 * 4]; + let dataset = unsafe { + MemDatasetBuilder::new(4, 4) + .add_band_with_options( + GdalDataType::Float64, + data.as_ptr() as *const u8, + None, + None, + Some(Nodata::F64(-9999.0)), + ) + .build() + .unwrap() + }; + let band = dataset.rasterband(1).unwrap(); + let nodata = band.no_data_value(); + assert_eq!(nodata, Some(-9999.0)); + } + + #[test] + fn test_mem_builder_zero_bands() { + let dataset = unsafe { MemDatasetBuilder::new(16, 16).build().unwrap() }; + assert_eq!(dataset.raster_count(), 0); + assert_eq!(dataset.raster_size(), (16, 16)); + } + + #[test] + fn test_mem_builder_mixed_band_types() { + let band_u8 = [0u8; 8 * 8]; + let band_f64 = vec![0f64; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new(8, 8) + .add_band(GdalDataType::UInt8, band_u8.as_ptr()) + .add_band(GdalDataType::Float64, band_f64.as_ptr() as *const u8) + .build() + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + } + + #[test] + pub fn test_mem_builder_with_owned_bands() { + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(16, 16, 2, GdalDataType::UInt16) + .build() + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::UInt16 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt16 + ); + + let dataset = MemDatasetBuilder::create(10, 8, 1, GdalDataType::Float32).unwrap(); + assert_eq!(dataset.raster_count(), 1); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + } + + #[test] + pub fn test_mem_builder_mixed_owned_and_external_bands() { + let external_band = [0u8; 8 * 8]; + let dataset = unsafe { + MemDatasetBuilder::new_with_owned_bands(8, 8, 1, GdalDataType::Float32) + .add_band(GdalDataType::UInt8, external_band.as_ptr()) + .build() + .unwrap() + }; + assert_eq!(dataset.raster_count(), 2); + assert_eq!( + dataset.rasterband(1).unwrap().band_type(), + GdalDataType::Float32 + ); + assert_eq!( + dataset.rasterband(2).unwrap().band_type(), + GdalDataType::UInt8 + ); + } +} diff --git a/c/sedona-gdal/src/raster/mod.rs b/c/sedona-gdal/src/raster/mod.rs new file mode 100644 index 000000000..23a841479 --- /dev/null +++ b/c/sedona-gdal/src/raster/mod.rs @@ -0,0 +1,30 @@ +// 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 polygonize; +pub mod rasterband; +pub mod rasterize; +pub mod rasterize_affine; +pub mod types; + +pub use polygonize::{polygonize, PolygonizeOptions}; +pub use rasterband::{actual_block_size, RasterBand}; +pub use rasterize::{rasterize, BurnSource, MergeAlgorithm, OptimizeMode, RasterizeOptions}; +pub use rasterize_affine::rasterize_affine; +pub use types::{ + Buffer, DatasetOptions, GdalDataType, GdalType, RasterCreationOptions, ResampleAlg, +}; diff --git a/c/sedona-gdal/src/raster/polygonize.rs b/c/sedona-gdal/src/raster/polygonize.rs new file mode 100644 index 000000000..eba843af8 --- /dev/null +++ b/c/sedona-gdal/src/raster/polygonize.rs @@ -0,0 +1,270 @@ +// 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. + +use std::ptr; + +use crate::cpl::CslStringList; +use crate::errors::Result; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::raster::RasterBand; +use crate::vector::Layer; + +#[derive(Clone, Debug, Default)] +pub struct PolygonizeOptions { + /// Use 8 connectedness (diagonal pixels are considered connected). + /// + /// If `false` (default), 4 connectedness is used. + pub eight_connected: bool, + + /// Name of a dataset from which to read the geotransform. + /// + /// This is useful if the source band has no related dataset, which is typical for mask bands. + /// + /// Corresponds to GDAL's `DATASET_FOR_GEOREF=dataset_name` option. + pub dataset_for_georef: Option, + + /// Interval in number of features at which transactions must be flushed. + /// + /// - `0` means that no transactions are opened. + /// - a negative value means a single transaction. + /// + /// Corresponds to GDAL's `COMMIT_INTERVAL=num` option. + pub commit_interval: Option, +} + +impl PolygonizeOptions { + /// Build a [`CslStringList`] from this options struct. + pub fn to_options_list(&self) -> Result { + let mut options = CslStringList::new(); + + if self.eight_connected { + options.set_name_value("8CONNECTED", "8")?; + } + + if let Some(ref ds) = self.dataset_for_georef { + options.set_name_value("DATASET_FOR_GEOREF", ds)?; + } + + if let Some(interval) = self.commit_interval { + options.set_name_value("COMMIT_INTERVAL", &interval.to_string())?; + } + + Ok(options) + } +} + +/// Polygonize a raster band into a vector layer. +/// +/// Uses `GDALPolygonize` (integer pixel values). +pub fn polygonize( + api: &'static GdalApi, + src_band: &RasterBand<'_>, + mask_band: Option<&RasterBand<'_>>, + out_layer: &Layer<'_>, + pixel_value_field: i32, + options: &PolygonizeOptions, +) -> Result<()> { + let mask = mask_band.map_or(ptr::null_mut(), |b| b.c_rasterband()); + let csl = options.to_options_list()?; + + let rv = unsafe { + call_gdal_api!( + api, + GDALPolygonize, + src_band.c_rasterband(), + mask, + out_layer.c_layer(), + pixel_value_field, + csl.as_ptr(), + ptr::null_mut(), // pfnProgress + ptr::null_mut() // pProgressData + ) + }; + if rv != CE_None { + return Err(api.last_cpl_err(rv as u32)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_polygonizeoptions_as_ptr() { + let c_options = PolygonizeOptions::default().to_options_list().unwrap(); + assert_eq!(c_options.fetch_name_value("8CONNECTED"), None); + assert_eq!(c_options.fetch_name_value("DATASET_FOR_GEOREF"), None); + assert_eq!(c_options.fetch_name_value("COMMIT_INTERVAL"), None); + + let c_options = PolygonizeOptions { + eight_connected: true, + dataset_for_georef: Some("/vsimem/georef.tif".to_string()), + commit_interval: Some(12345), + } + .to_options_list() + .unwrap(); + assert_eq!(c_options.fetch_name_value("8CONNECTED"), Some("8".into())); + assert_eq!( + c_options.fetch_name_value("DATASET_FOR_GEOREF"), + Some("/vsimem/georef.tif".into()) + ); + assert_eq!( + c_options.fetch_name_value("COMMIT_INTERVAL"), + Some("12345".into()) + ); + } + + #[cfg(feature = "gdal-sys")] + #[test] + fn test_polygonize_connectivity_affects_regions() { + use crate::dataset::LayerOptions; + use crate::driver::DriverManager; + use crate::raster::Buffer; + use crate::register::get_global_gdal_api; + use crate::vector::FieldDefn; + + let api = get_global_gdal_api().unwrap(); + let mem_driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let raster_ds = mem_driver.create("", 3, 3, 1).unwrap(); + let band = raster_ds.rasterband(1).unwrap(); + + // 3x3 raster with diagonal 1s: + // 1 0 0 + // 0 1 0 + // 0 0 1 + let mut data = Buffer::new((3, 3), vec![1u8, 0, 0, 0, 1, 0, 0, 0, 1]); + band.write((0, 0), (3, 3), &mut data).unwrap(); + + let gpkg_path = "/vsimem/test_polygonize_connectivity.gpkg"; + let gpkg_driver = DriverManager::get_driver_by_name(api, "GPKG").unwrap(); + let vector_ds = gpkg_driver.create_vector_only(gpkg_path).unwrap(); + + // 4-connected output + let layer_4 = vector_ds + .create_layer(LayerOptions { + name: "four", + srs: None, + ty: OGRwkbGeometryType::wkbPolygon, + options: None, + }) + .unwrap(); + let field_defn = FieldDefn::new(api, "val", OGRFieldType::OFTInteger).unwrap(); + layer_4.create_field(&field_defn).unwrap(); + + polygonize(api, &band, None, &layer_4, 0, &PolygonizeOptions::default()).unwrap(); + + let ones_4 = layer_4 + .features() + .filter_map(|f| f.field_as_integer(0)) + .filter(|v| *v == 1) + .count(); + assert_eq!(ones_4, 3); + + // 8-connected output + let layer_8 = vector_ds + .create_layer(LayerOptions { + name: "eight", + srs: None, + ty: OGRwkbGeometryType::wkbPolygon, + options: None, + }) + .unwrap(); + let field_defn = FieldDefn::new(api, "val", OGRFieldType::OFTInteger).unwrap(); + layer_8.create_field(&field_defn).unwrap(); + + polygonize( + api, + &band, + None, + &layer_8, + 0, + &PolygonizeOptions { + eight_connected: true, + dataset_for_georef: None, + commit_interval: None, + }, + ) + .unwrap(); + + let ones_8 = layer_8 + .features() + .filter_map(|f| f.field_as_integer(0)) + .filter(|v| *v == 1) + .count(); + assert_eq!(ones_8, 1); + + crate::vsi::unlink_mem_file(api, gpkg_path).unwrap(); + } + + #[cfg(feature = "gdal-sys")] + #[test] + fn test_polygonize_with_mask_band_restricts_output() { + use crate::dataset::LayerOptions; + use crate::driver::DriverManager; + use crate::raster::Buffer; + use crate::register::get_global_gdal_api; + use crate::vector::FieldDefn; + + let api = get_global_gdal_api().unwrap(); + let mem_driver = DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let raster_ds = mem_driver.create("", 3, 3, 2).unwrap(); + + let value_band = raster_ds.rasterband(1).unwrap(); + let mask_band = raster_ds.rasterband(2).unwrap(); + + // Value band: all 7s. + let mut values = Buffer::new((3, 3), vec![7u8; 9]); + value_band.write((0, 0), (3, 3), &mut values).unwrap(); + + // Mask: only the center pixel is included. + let mut mask = Buffer::new((3, 3), vec![0u8, 0, 0, 0, 1, 0, 0, 0, 0]); + mask_band.write((0, 0), (3, 3), &mut mask).unwrap(); + + let gpkg_path = "/vsimem/test_polygonize_mask.gpkg"; + let gpkg_driver = DriverManager::get_driver_by_name(api, "GPKG").unwrap(); + let vector_ds = gpkg_driver.create_vector_only(gpkg_path).unwrap(); + + let layer = vector_ds + .create_layer(LayerOptions { + name: "masked", + srs: None, + ty: OGRwkbGeometryType::wkbPolygon, + options: None, + }) + .unwrap(); + let field_defn = FieldDefn::new(api, "val", OGRFieldType::OFTInteger).unwrap(); + layer.create_field(&field_defn).unwrap(); + + polygonize( + api, + &value_band, + Some(&mask_band), + &layer, + 0, + &PolygonizeOptions::default(), + ) + .unwrap(); + + assert_eq!(layer.feature_count(true), 1); + let only_val = layer.features().next().unwrap().field_as_integer(0); + assert_eq!(only_val, Some(7)); + + crate::vsi::unlink_mem_file(api, gpkg_path).unwrap(); + } +} diff --git a/c/sedona-gdal/src/raster/rasterband.rs b/c/sedona-gdal/src/raster/rasterband.rs new file mode 100644 index 000000000..c7e0ec590 --- /dev/null +++ b/c/sedona-gdal/src/raster/rasterband.rs @@ -0,0 +1,355 @@ +// 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. + +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::*, 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::raster::types::ResampleAlg; + use crate::register::get_global_gdal_api; + + fn fixture(name: &str) -> String { + sedona_testing::data::test_raster(name).unwrap() + } + + #[test] + fn test_read_raster() { + let api = get_global_gdal_api().unwrap(); + 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]); + } + + #[test] + fn test_read_raster_with_default_resample() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_read_raster_with_average_resample() { + let api = get_global_gdal_api().unwrap(); + 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); + } + + #[test] + fn test_get_no_data_value() { + let api = get_global_gdal_api().unwrap(); + + // 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)); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_set_no_data_value() { + let api = get_global_gdal_api().unwrap(); + 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); + } +} diff --git a/c/sedona-gdal/src/raster/rasterize.rs b/c/sedona-gdal/src/raster/rasterize.rs new file mode 100644 index 000000000..e800df711 --- /dev/null +++ b/c/sedona-gdal/src/raster/rasterize.rs @@ -0,0 +1,255 @@ +// 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. + +use std::ptr; + +use crate::cpl::CslStringList; +use crate::dataset::Dataset; +use crate::errors::Result; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::vector::Geometry; + +/// Source of burn values. +#[derive(Copy, Clone, Debug)] +pub enum BurnSource { + /// Use whatever `burn_values` argument is supplied to `rasterize`. + UserSupplied, + + /// Add the geometry's Z value to whatever `burn_values` argument + /// is supplied to `rasterize`. + Z, +} + +/// Algorithm for merging new raster values with existing values. +#[derive(Copy, Clone, Debug)] +pub enum MergeAlgorithm { + /// Overwrite existing value (default). + Replace, + /// Add new value to existing value (useful for heatmaps). + Add, +} + +/// Optimization mode for rasterization. +#[derive(Copy, Clone, Debug)] +pub enum OptimizeMode { + /// Let GDAL decide (default). + Automatic, + /// Force raster-based scan (iterates over pixels). + Raster, + /// Force vector-based scan (iterates over geometry edges). + Vector, +} + +/// Options that specify how to rasterize geometries. +#[derive(Copy, Clone, Debug)] +pub struct RasterizeOptions { + /// Set to `true` to set all pixels touched by the line or polygons, + /// not just those whose center is within the polygon or that are + /// selected by Bresenham's line algorithm. Defaults to `false`. + pub all_touched: bool, + + /// May be set to `BurnSource::Z` to use the Z values of the geometries. + /// `burn_value` is added to this before burning. Defaults to + /// `BurnSource::UserSupplied` in which case just the `burn_value` is burned. + pub source: BurnSource, + + /// May be `MergeAlgorithm::Replace` (default) or `MergeAlgorithm::Add`. + /// `Replace` overwrites existing values; `Add` adds to them. + pub merge_algorithm: MergeAlgorithm, + + /// The height in lines of the chunk to operate on. `0` (default) lets GDAL + /// choose based on cache size. Not used in `OPTIM=RASTER` mode. + pub chunk_y_size: usize, + + /// Optimization mode for rasterization. + pub optimize: OptimizeMode, +} + +impl Default for RasterizeOptions { + fn default() -> Self { + RasterizeOptions { + all_touched: false, + source: BurnSource::UserSupplied, + merge_algorithm: MergeAlgorithm::Replace, + chunk_y_size: 0, + optimize: OptimizeMode::Automatic, + } + } +} + +impl RasterizeOptions { + /// Build a [`CslStringList`] from this options struct. + pub fn to_options_list(self) -> Result { + let mut options = CslStringList::with_capacity(5); + + options.set_name_value( + "ALL_TOUCHED", + if self.all_touched { "TRUE" } else { "FALSE" }, + )?; + + options.set_name_value( + "MERGE_ALG", + match self.merge_algorithm { + MergeAlgorithm::Replace => "REPLACE", + MergeAlgorithm::Add => "ADD", + }, + )?; + + options.set_name_value("CHUNKYSIZE", &self.chunk_y_size.to_string())?; + + options.set_name_value( + "OPTIM", + match self.optimize { + OptimizeMode::Automatic => "AUTO", + OptimizeMode::Raster => "RASTER", + OptimizeMode::Vector => "VECTOR", + }, + )?; + + if let BurnSource::Z = self.source { + options.set_name_value("BURN_VALUE_FROM", "Z")?; + } + + Ok(options) + } +} + +/// Rasterize geometries onto a dataset. +/// +/// There must be one burn value for every geometry. Each burn value is +/// replicated across all bands internally, matching the +/// `GDALRasterizeGeometries` contract of `nGeomCount * nBandCount` entries. +pub fn rasterize( + api: &'static GdalApi, + dataset: &Dataset, + band_list: &[i32], + geometries: &[&Geometry], + burn_values: &[f64], + options: Option, +) -> Result<()> { + if band_list.is_empty() { + return Err(crate::errors::GdalError::BadArgument( + "`band_list` must not be empty".to_string(), + )); + } + if burn_values.len() != geometries.len() { + return Err(crate::errors::GdalError::BadArgument(format!( + "burn_values length ({}) must match geometries length ({})", + burn_values.len(), + geometries.len() + ))); + } + let raster_count = dataset.raster_count(); + for &band in band_list { + let is_good = band > 0 && (band as usize) <= raster_count; + if !is_good { + return Err(crate::errors::GdalError::BadArgument(format!( + "Band index {} is out of bounds", + band + ))); + } + } + + let geom_handles: Vec = geometries.iter().map(|g| g.c_geometry()).collect(); + + // Replicate each burn value across all bands, matching the GDAL C API + // contract that expects nGeomCount * nBandCount burn values. + let expanded_burn_values: Vec = burn_values + .iter() + .flat_map(|burn| std::iter::repeat_n(burn, band_list.len())) + .copied() + .collect(); + + let opts = options.unwrap_or_default(); + let csl = opts.to_options_list()?; + + let n_band_count: i32 = band_list.len().try_into()?; + let n_geom_count: i32 = geom_handles.len().try_into()?; + + let rv = unsafe { + call_gdal_api!( + api, + GDALRasterizeGeometries, + dataset.c_dataset(), + n_band_count, + band_list.as_ptr(), + n_geom_count, + geom_handles.as_ptr(), + ptr::null_mut(), // pfnTransformer + ptr::null_mut(), // pTransformArg + expanded_burn_values.as_ptr(), + csl.as_ptr(), + ptr::null_mut(), // pfnProgress + ptr::null_mut() // pProgressData + ) + }; + if rv != CE_None { + return Err(api.last_cpl_err(rv as u32)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rasterizeoptions_as_ptr() { + let c_options = RasterizeOptions::default().to_options_list().unwrap(); + assert_eq!( + c_options.fetch_name_value("ALL_TOUCHED"), + Some("FALSE".to_string()) + ); + assert_eq!(c_options.fetch_name_value("BURN_VALUE_FROM"), None); + assert_eq!( + c_options.fetch_name_value("MERGE_ALG"), + Some("REPLACE".to_string()) + ); + assert_eq!( + c_options.fetch_name_value("CHUNKYSIZE"), + Some("0".to_string()) + ); + assert_eq!( + c_options.fetch_name_value("OPTIM"), + Some("AUTO".to_string()) + ); + } + + #[cfg(feature = "gdal-sys")] + #[test] + fn test_rasterize() { + let api = crate::register::get_global_gdal_api().unwrap(); + let wkt = "POLYGON ((2 2, 2 4.25, 4.25 4.25, 4.25 2, 2 2))"; + let poly = Geometry::from_wkt(api, wkt).unwrap(); + + let driver = crate::driver::DriverManager::get_driver_by_name(api, "MEM").unwrap(); + let dataset = driver.create("", 5, 5, 1).unwrap(); + + let bands = [1]; + let geometries = [&poly]; + let burn_values = [1.0]; + rasterize(api, &dataset, &bands, &geometries, &burn_values, None).unwrap(); + + let rb = dataset.rasterband(1).unwrap(); + let values = rb.read_as::((0, 0), (5, 5), (5, 5), None).unwrap(); + assert_eq!( + values.data(), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0] + ); + } +} diff --git a/c/sedona-gdal/src/raster/rasterize_affine.rs b/c/sedona-gdal/src/raster/rasterize_affine.rs new file mode 100644 index 000000000..232579f16 --- /dev/null +++ b/c/sedona-gdal/src/raster/rasterize_affine.rs @@ -0,0 +1,351 @@ +// 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. + +//! Fast affine-transformer rasterize wrapper. +//! +//! GDALRasterizeGeometries() will internally call GDALCreateGenImgProjTransformer2() +//! if pfnTransformer is NULL, even in the common case where only a GeoTransform-based +//! affine conversion from georeferenced coords to pixel/line is needed. +//! +//! This module supplies a minimal GDALTransformerFunc that applies the dataset +//! GeoTransform (and its inverse), avoiding expensive transformer creation. + +use std::ffi::{c_int, c_void}; +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::{CE_Failure, CE_None}; +use crate::geo_transform::{GeoTransform, GeoTransformEx}; +use crate::vector::Geometry; + +#[repr(C)] +struct AffineTransformArg { + gt: GeoTransform, + inv_gt: GeoTransform, +} + +unsafe extern "C" fn affine_transformer( + p_transformer_arg: *mut c_void, + b_dst_to_src: c_int, + n_point_count: c_int, + x: *mut f64, + y: *mut f64, + _z: *mut f64, + pan_success: *mut c_int, +) -> c_int { + if p_transformer_arg.is_null() || x.is_null() || y.is_null() || pan_success.is_null() { + return 0; + } + if n_point_count < 0 { + return 0; + } + + // Treat transformer arg as immutable. + let arg = &*(p_transformer_arg as *const AffineTransformArg); + let affine = if b_dst_to_src == 0 { + &arg.inv_gt + } else { + &arg.gt + }; + + let n = n_point_count as usize; + for i in 0..n { + // SAFETY: x/y/pan_success are assumed to point to arrays of length n_point_count. + let xin = unsafe { *x.add(i) }; + let yin = unsafe { *y.add(i) }; + let (xout, yout) = affine.apply(xin, yin); + unsafe { + *x.add(i) = xout; + *y.add(i) = yout; + *pan_success.add(i) = 1; + } + } + + 1 +} + +/// Rasterize geometries with an affine transformer derived from the destination dataset. +/// +/// This mirrors [`rasterize()`](super::rasterize) but avoids GDAL's slow default +/// transformer creation. +/// +/// Assumptions: +/// - Geometry coordinates are already in the destination dataset georeferenced coordinate space. +/// - Only GeoTransform-based affine conversion is supported (no GCP/RPC/geolocs). +pub fn rasterize_affine( + api: &'static GdalApi, + dataset: &Dataset, + bands: &[usize], + geometries: &[Geometry], + burn_values: &[f64], + all_touched: bool, +) -> Result<()> { + if bands.is_empty() { + return Err(GdalError::BadArgument( + "`bands` must not be empty".to_string(), + )); + } + if burn_values.len() != geometries.len() { + return Err(GdalError::BadArgument(format!( + "Burn values length ({}) must match geometries length ({})", + burn_values.len(), + geometries.len() + ))); + } + + let raster_count = dataset.raster_count(); + for band in bands { + let is_good = *band > 0 && *band <= raster_count; + if !is_good { + return Err(GdalError::BadArgument(format!( + "Band index {} is out of bounds", + *band + ))); + } + } + + let bands_i32: Vec = bands.iter().map(|&band| band as c_int).collect(); + + let c_options = if all_touched { + [c"ALL_TOUCHED=TRUE".as_ptr(), ptr::null_mut()] + } else { + [c"ALL_TOUCHED=FALSE".as_ptr(), ptr::null_mut()] + }; + + let geometries_c: Vec<_> = geometries.iter().map(|geo| geo.c_geometry()).collect(); + let burn_values_expanded: Vec = burn_values + .iter() + .flat_map(|burn| std::iter::repeat_n(burn, bands_i32.len())) + .copied() + .collect(); + + let gt = dataset.geo_transform().map_err(|_e| { + GdalError::BadArgument( + "Missing geotransform: only geotransform-based affine rasterize is supported" + .to_string(), + ) + })?; + let inv_gt = gt.invert().map_err(|_e| { + GdalError::BadArgument( + "Non-invertible geotransform: only geotransform-based affine rasterize is supported" + .to_string(), + ) + })?; + let mut arg = AffineTransformArg { gt, inv_gt }; + + unsafe { + let error = call_gdal_api!( + api, + GDALRasterizeGeometries, + dataset.c_dataset(), + bands_i32.len() as c_int, + bands_i32.as_ptr(), + geometries_c.len() as c_int, + geometries_c.as_ptr(), + (affine_transformer as *const ()).cast::() as *mut c_void, + (&mut arg as *mut AffineTransformArg).cast::(), + burn_values_expanded.as_ptr(), + c_options.as_ptr() as *mut *mut i8, + ptr::null_mut(), + ptr::null_mut() + ); + if error != CE_None { + return Err(api.last_cpl_err(CE_Failure as u32)); + } + } + Ok(()) +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use super::*; + + use crate::driver::DriverManager; + use crate::raster::rasterize; + use crate::raster::types::Buffer; + use crate::register::get_global_gdal_api; + use crate::Driver; + + fn api() -> &'static GdalApi { + get_global_gdal_api().unwrap() + } + + fn mem_driver() -> Driver { + DriverManager::get_driver_by_name(api(), "MEM").unwrap() + } + + fn make_dataset_u8(width: usize, height: usize, gt: GeoTransform) -> Result { + let driver = mem_driver(); + let ds = driver.create_with_band_type::("", width, height, 1)?; + ds.set_geo_transform(>)?; + let band = ds.rasterband(1)?; + let mut buf = Buffer::new((width, height), vec![0u8; width * height]); + band.write((0, 0), (width, height), &mut buf)?; + Ok(ds) + } + + fn read_u8(ds: &Dataset, width: usize, height: usize) -> Vec { + let band = ds.rasterband(1).unwrap(); + let buf = band + .read_as::((0, 0), (width, height), (width, height), None) + .unwrap(); + buf.data().to_vec() + } + + fn poly_from_pixel_rect(gt: &GeoTransform, x0: f64, y0: f64, x1: f64, y1: f64) -> Geometry { + let api = api(); + let (wx0, wy0) = gt.apply(x0, y0); + let (wx1, wy1) = gt.apply(x1, y0); + let (wx2, wy2) = gt.apply(x1, y1); + let (wx3, wy3) = gt.apply(x0, y1); + let wkt = + format!("POLYGON (({wx0} {wy0}, {wx1} {wy1}, {wx2} {wy2}, {wx3} {wy3}, {wx0} {wy0}))"); + Geometry::from_wkt(api, &wkt).unwrap() + } + + fn line_from_pixel_points(gt: &GeoTransform, pts: &[(f64, f64)]) -> Geometry { + let api = api(); + assert!(pts.len() >= 2); + let mut s = String::from("LINESTRING ("); + for (i, (px, py)) in pts.iter().copied().enumerate() { + let (wx, wy) = gt.apply(px, py); + if i > 0 { + s.push_str(", "); + } + s.push_str(&format!("{wx} {wy}")); + } + s.push(')'); + Geometry::from_wkt(api, &s).unwrap() + } + + #[test] + fn test_rasterize_affine_matches_baseline_north_up() { + let api = api(); + let (w, h) = (32usize, 24usize); + let gt: GeoTransform = [100.0, 2.0, 0.0, 200.0, 0.0, -2.0]; + + let geom_baseline = poly_from_pixel_rect(>, 3.2, 4.7, 20.4, 18.1); + let geom_affine = poly_from_pixel_rect(>, 3.2, 4.7, 20.4, 18.1); + + let ds_baseline = make_dataset_u8(w, h, gt).unwrap(); + let ds_affine = make_dataset_u8(w, h, gt).unwrap(); + + let geom_refs: Vec<&Geometry> = vec![&geom_baseline]; + rasterize(api, &ds_baseline, &[1], &geom_refs, &[1.0], None).unwrap(); + rasterize_affine(api, &ds_affine, &[1], &[geom_affine], &[1.0], false).unwrap(); + + assert_eq!(read_u8(&ds_affine, w, h), read_u8(&ds_baseline, w, h)); + } + + #[test] + fn test_rasterize_affine_matches_baseline_rotated_gt_all_touched() { + let api = api(); + let (w, h) = (40usize, 28usize); + // Rotated/skewed GeoTransform. + let gt: GeoTransform = [10.0, 1.2, 0.15, 50.0, -0.1, -1.1]; + + let geom_baseline = poly_from_pixel_rect(>, 5.25, 4.5, 25.75, 20.25); + let geom_affine = poly_from_pixel_rect(>, 5.25, 4.5, 25.75, 20.25); + + let ds_baseline = make_dataset_u8(w, h, gt).unwrap(); + let ds_affine = make_dataset_u8(w, h, gt).unwrap(); + + let geom_refs: Vec<&Geometry> = vec![&geom_baseline]; + rasterize( + api, + &ds_baseline, + &[1], + &geom_refs, + &[1.0], + Some(crate::raster::RasterizeOptions { + all_touched: true, + ..Default::default() + }), + ) + .unwrap(); + rasterize_affine(api, &ds_affine, &[1], &[geom_affine], &[1.0], true).unwrap(); + + assert_eq!(read_u8(&ds_affine, w, h), read_u8(&ds_baseline, w, h)); + } + + #[test] + fn test_rasterize_affine_matches_baseline_linestring() { + let api = api(); + let (w, h) = (64usize, 48usize); + // Rotated/skewed GeoTransform. + let gt: GeoTransform = [5.0, 1.0, 0.2, 100.0, -0.15, -1.05]; + + // A polyline with many vertices, defined in pixel/line space. + let mut pts: Vec<(f64, f64)> = Vec::new(); + for i in 0..200 { + let t = i as f64 / 199.0; + let x = 2.625 + t * ((w as f64) - 5.25); + let y = 5.25 + (t * 6.0).sin() * 8.0 + t * ((h as f64) - 12.25); + pts.push((x, y)); + } + let geom_baseline = line_from_pixel_points(>, &pts); + let geom_affine = line_from_pixel_points(>, &pts); + + let ds_baseline = make_dataset_u8(w, h, gt).unwrap(); + let ds_affine = make_dataset_u8(w, h, gt).unwrap(); + + let geom_refs: Vec<&Geometry> = vec![&geom_baseline]; + rasterize(api, &ds_baseline, &[1], &geom_refs, &[1.0], None).unwrap(); + rasterize_affine(api, &ds_affine, &[1], &[geom_affine], &[1.0], false).unwrap(); + + let got = read_u8(&ds_affine, w, h); + let expected = read_u8(&ds_baseline, w, h); + if got != expected { + let mut diffs = Vec::new(); + for (i, (a, b)) in got + .iter() + .copied() + .zip(expected.iter().copied()) + .enumerate() + { + if a != b { + let x = i % w; + let y = i / w; + diffs.push((x, y, a, b)); + } + } + panic!( + "raster mismatch: {} differing pixels; first 10: {:?}", + diffs.len(), + &diffs[..diffs.len().min(10)] + ); + } + } + + #[test] + fn test_rasterize_affine_fails_on_noninvertible_gt() { + let api = api(); + let (w, h) = (8usize, 8usize); + let gt: GeoTransform = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + let ds = make_dataset_u8(w, h, gt).unwrap(); + let geom = Geometry::from_wkt(api, "POINT (0 0)").unwrap(); + let err = rasterize_affine(api, &ds, &[1], &[geom], &[1.0], true).unwrap_err(); + match err { + GdalError::BadArgument(msg) => { + assert!(msg.contains("Non-invertible geotransform")); + } + other => panic!("Unexpected error: {other:?}"), + } + } +} diff --git a/c/sedona-gdal/src/raster/types.rs b/c/sedona-gdal/src/raster/types.rs new file mode 100644 index 000000000..b2d2e8325 --- /dev/null +++ b/c/sedona-gdal/src/raster/types.rs @@ -0,0 +1,242 @@ +// 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. + +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", + } + } +} diff --git a/c/sedona-gdal/src/register.rs b/c/sedona-gdal/src/register.rs new file mode 100644 index 000000000..d1b7a645e --- /dev/null +++ b/c/sedona-gdal/src/register.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. + +use crate::errors::GdalInitLibraryError; +use crate::gdal_api::GdalApi; +use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; + +/// Minimum GDAL version required by sedona-gdal. +#[cfg(feature = "gdal-sys")] +const MIN_GDAL_VERSION_MAJOR: i32 = 3; +#[cfg(feature = "gdal-sys")] +const MIN_GDAL_VERSION_MINOR: i32 = 4; + +static GDAL_API: OnceLock = OnceLock::new(); +static GDAL_API_INIT_LOCK: Mutex<()> = Mutex::new(()); + +fn init_gdal_api(init: F) -> Result<&'static GdalApi, GdalInitLibraryError> +where + F: FnOnce() -> Result, +{ + if let Some(api) = GDAL_API.get() { + return Ok(api); + } + + let _guard = GDAL_API_INIT_LOCK + .lock() + .map_err(|_| GdalInitLibraryError::Invalid("GDAL API init lock poisoned".to_string()))?; + + if let Some(api) = GDAL_API.get() { + return Ok(api); + } + + let api = init()?; + + // Register all GDAL drivers once, immediately after loading symbols. + // This mirrors georust/gdal's `_register_drivers()` pattern where + // `GDALAllRegister` is called via `std::sync::Once` before any driver + // lookup or dataset open. Here the `OnceLock` + `Mutex` already + // guarantees this runs exactly once. + unsafe { + let Some(gdal_all_register) = api.inner.GDALAllRegister else { + return Err(GdalInitLibraryError::LibraryError( + "GDALAllRegister symbol not loaded".to_string(), + )); + }; + gdal_all_register(); + } + + let _ = GDAL_API.set(api); + Ok(GDAL_API.get().expect("GDAL API should be set")) +} + +pub fn configure_global_gdal_api(shared_library: PathBuf) -> Result<(), GdalInitLibraryError> { + init_gdal_api(|| GdalApi::try_from_shared_library(shared_library))?; + Ok(()) +} + +pub fn is_gdal_api_configured() -> bool { + GDAL_API.get().is_some() +} + +pub fn with_global_gdal_api(func: F) -> Result +where + F: FnOnce(&'static GdalApi) -> Result, +{ + let api = get_global_gdal_api()?; + func(api) +} + +/// Get a reference to the global GDAL API, initializing from the current process +/// if not already done. +pub fn get_global_gdal_api() -> Result<&'static GdalApi, GdalInitLibraryError> { + init_gdal_api(|| { + #[cfg(feature = "gdal-sys")] + check_gdal_version()?; + GdalApi::try_from_current_process() + }) +} + +/// Verify that the compile-time-linked GDAL library meets the minimum version +/// requirement. Calling into `gdal-sys` also forces the linker to include GDAL +/// symbols, so that `try_from_current_process` (which resolves function pointers +/// via `dlsym` on the current process) can find them at runtime. +/// +/// We use `GDALVersionInfo("VERSION_NUM")` instead of `GDALCheckVersion` because +/// the latter performs an **exact** major.minor match and rejects newer versions +/// (e.g. GDAL 3.12 fails a check for 3.4), whereas we need a **minimum** version +/// check (>=). +#[cfg(feature = "gdal-sys")] +fn check_gdal_version() -> Result<(), GdalInitLibraryError> { + use std::ffi::CStr; + + // Matches the GDAL_COMPUTE_VERSION(maj,min,rev) macro: maj*1000000 + min*10000 + rev*100 + let min_version_num = MIN_GDAL_VERSION_MAJOR * 1_000_000 + MIN_GDAL_VERSION_MINOR * 10_000; + + let version_ptr = unsafe { gdal_sys::GDALVersionInfo(c"VERSION_NUM".as_ptr()) }; + if version_ptr.is_null() { + return Err(GdalInitLibraryError::LibraryError( + "GDALVersionInfo(\"VERSION_NUM\") returned null".to_string(), + )); + } + + let version_cstr = unsafe { CStr::from_ptr(version_ptr) }; + let version_num: i32 = version_cstr + .to_str() + .map_err(|e| { + GdalInitLibraryError::LibraryError(format!( + "GDAL version string is not valid UTF-8: {e}" + )) + })? + .trim() + .parse() + .map_err(|e| { + GdalInitLibraryError::LibraryError(format!( + "Failed to parse GDAL version number {:?}: {e}", + version_cstr + )) + })?; + + if version_num < min_version_num { + // Get the human-readable release name for the error message. + let release_ptr = unsafe { gdal_sys::GDALVersionInfo(c"RELEASE_NAME".as_ptr()) }; + let release_name = if release_ptr.is_null() { + format!("version_num={version_num}") + } else { + unsafe { CStr::from_ptr(release_ptr) } + .to_string_lossy() + .into_owned() + }; + return Err(GdalInitLibraryError::LibraryError(format!( + "GDAL >= {MIN_GDAL_VERSION_MAJOR}.{MIN_GDAL_VERSION_MINOR} required \ + for sedona-gdal (found {release_name})" + ))); + } + + Ok(()) +} diff --git a/c/sedona-gdal/src/spatial_ref.rs b/c/sedona-gdal/src/spatial_ref.rs new file mode 100644 index 000000000..b2027d58b --- /dev/null +++ b/c/sedona-gdal/src/spatial_ref.rs @@ -0,0 +1,143 @@ +// 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. + +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::register::get_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() { + let api = get_global_gdal_api().unwrap(); + let srs = SpatialRef::from_wkt(api, WGS84_WKT).unwrap(); + assert!(!srs.c_srs().is_null()); + } + + #[test] + fn test_from_wkt_invalid() { + let api = get_global_gdal_api().unwrap(); + let err = SpatialRef::from_wkt(api, "WGS\u{0}84"); + assert!(matches!(err, Err(GdalError::FfiNulError(_)))); + } + + #[test] + fn test_to_projjson() { + let api = get_global_gdal_api().unwrap(); + let srs = SpatialRef::from_wkt(api, WGS84_WKT).unwrap(); + let projjson = srs.to_projjson().unwrap(); + assert!( + projjson.contains("WGS 84"), + "unexpected projjson: {projjson}" + ); + } + + #[test] + fn test_from_c_srs_clone() { + let api = get_global_gdal_api().unwrap(); + 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()); + } +} diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs new file mode 100644 index 000000000..870684fe5 --- /dev/null +++ b/c/sedona-gdal/src/vector/feature.rs @@ -0,0 +1,212 @@ +// 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. + +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 { + 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> { + 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 using the provided API. + pub fn wkb(&self, api: &'static GdalApi) -> Result> { + let size = unsafe { call_gdal_api!(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!( + 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, api: &'static GdalApi) -> crate::vector::Envelope { + let mut env = OGREnvelope { + MinX: 0.0, + MaxX: 0.0, + MinY: 0.0, + MaxY: 0.0, + }; + unsafe { call_gdal_api!(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/geometry.rs b/c/sedona-gdal/src/vector/geometry.rs new file mode 100644 index 000000000..a21e8e76b --- /dev/null +++ b/c/sedona-gdal/src/vector/geometry.rs @@ -0,0 +1,158 @@ +// 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. + +use std::ffi::CString; +use std::ptr; + +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; + +/// An OGR geometry. +pub struct Geometry { + api: &'static GdalApi, + c_geom: OGRGeometryH, +} + +unsafe impl Send for Geometry {} + +impl Drop for Geometry { + fn drop(&mut self) { + if !self.c_geom.is_null() { + unsafe { call_gdal_api!(self.api, OGR_G_DestroyGeometry, self.c_geom) }; + } + } +} + +impl Geometry { + /// Create a geometry from WKB bytes. + pub fn from_wkb(api: &'static GdalApi, wkb: &[u8]) -> Result { + let wkb_len: i32 = wkb.len().try_into()?; + let mut c_geom: OGRGeometryH = ptr::null_mut(); + let rv = unsafe { + call_gdal_api!( + api, + OGR_G_CreateFromWkb, + wkb.as_ptr() as *const std::ffi::c_void, + ptr::null_mut(), // hSRS + &mut c_geom, + wkb_len + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkb", + }); + } + if c_geom.is_null() { + return Err(GdalError::NullPointer { + method_name: "OGR_G_CreateFromWkb", + msg: "returned null geometry".to_string(), + }); + } + Ok(Self { api, c_geom }) + } + + /// Create a geometry from WKT string. + pub fn from_wkt(api: &'static GdalApi, wkt: &str) -> Result { + let c_wkt = CString::new(wkt)?; + let mut wkt_ptr = c_wkt.as_ptr() as *mut std::os::raw::c_char; + let mut c_geom: OGRGeometryH = ptr::null_mut(); + let rv = unsafe { + call_gdal_api!( + api, + OGR_G_CreateFromWkt, + &mut wkt_ptr, + ptr::null_mut(), // hSRS + &mut c_geom + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_CreateFromWkt", + }); + } + if c_geom.is_null() { + return Err(GdalError::NullPointer { + method_name: "OGR_G_CreateFromWkt", + msg: "returned null geometry".to_string(), + }); + } + Ok(Self { api, c_geom }) + } + + /// Return the raw C geometry handle. + pub fn c_geometry(&self) -> OGRGeometryH { + self.c_geom + } + + /// Get the bounding envelope. + pub fn envelope(&self) -> Envelope { + let mut env = OGREnvelope { + MinX: 0.0, + MaxX: 0.0, + MinY: 0.0, + MaxY: 0.0, + }; + unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut env) }; + Envelope { + MinX: env.MinX, + MaxX: env.MaxX, + MinY: env.MinY, + MaxY: env.MaxY, + } + } + + /// Export to ISO WKB. + pub fn wkb(&self) -> Result> { + let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize, self.c_geom) }; + if size < 0 { + return Err(GdalError::BadArgument(format!( + "OGR_G_WkbSize returned negative size: {size}" + ))); + } + let mut buf = vec![0u8; size as usize]; + let rv = unsafe { + call_gdal_api!( + self.api, + OGR_G_ExportToIsoWkb, + self.c_geom, + wkbNDR, // little-endian + buf.as_mut_ptr() + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_G_ExportToIsoWkb", + }); + } + Ok(buf) + } +} + +/// Bounding envelope. +#[derive(Debug, Clone, Copy)] +#[allow(non_snake_case)] +pub struct Envelope { + pub MinX: f64, + pub MaxX: f64, + pub MinY: f64, + pub MaxY: f64, +} diff --git a/c/sedona-gdal/src/vector/layer.rs b/c/sedona-gdal/src/vector/layer.rs new file mode 100644 index 000000000..4ba626a4d --- /dev/null +++ b/c/sedona-gdal/src/vector/layer.rs @@ -0,0 +1,114 @@ +// 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. + +use std::marker::PhantomData; + +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::gdal_dyn_bindgen::*; +use crate::vector::feature::{Feature, FieldDefn}; + +/// An OGR layer (borrowed from a Dataset). +pub struct Layer<'a> { + api: &'static GdalApi, + c_layer: OGRLayerH, + _dataset: PhantomData<&'a Dataset>, +} + +impl<'a> Layer<'a> { + pub(crate) fn new(api: &'static GdalApi, c_layer: OGRLayerH, _dataset: &'a Dataset) -> Self { + Self { + api, + c_layer, + _dataset: PhantomData, + } + } + + /// Return the raw C layer handle. + pub fn c_layer(&self) -> OGRLayerH { + self.c_layer + } + + /// Reset reading to the first feature. + pub fn reset_reading(&self) { + unsafe { call_gdal_api!(self.api, OGR_L_ResetReading, self.c_layer) }; + } + + /// Get the next feature (returns None when exhausted). + pub fn next_feature(&self) -> Option> { + let c_feature = unsafe { call_gdal_api!(self.api, OGR_L_GetNextFeature, self.c_layer) }; + if c_feature.is_null() { + None + } else { + Some(Feature::new(self.api, c_feature)) + } + } + + /// Create a field on this layer. + pub fn create_field(&self, field_defn: &FieldDefn) -> Result<()> { + let rv = unsafe { + call_gdal_api!( + self.api, + OGR_L_CreateField, + self.c_layer, + field_defn.c_field_defn(), + 1 // bApproxOK + ) + }; + if rv != OGRERR_NONE { + return Err(GdalError::OgrError { + err: rv, + method_name: "OGR_L_CreateField", + }); + } + Ok(()) + } + + /// Get the number of features in this layer. + /// + /// If `force` is true, the count will be computed even if it is expensive. + pub fn feature_count(&self, force: bool) -> i64 { + unsafe { + call_gdal_api!( + self.api, + OGR_L_GetFeatureCount, + self.c_layer, + if force { 1 } else { 0 } + ) + } + } + + /// Iterate over all features. + pub fn features(&self) -> FeatureIterator<'_> { + self.reset_reading(); + FeatureIterator { layer: self } + } +} + +/// Iterator over features in a layer. +pub struct FeatureIterator<'a> { + layer: &'a Layer<'a>, +} + +impl<'a> Iterator for FeatureIterator<'a> { + type Item = Feature<'a>; + + fn next(&mut self) -> Option { + self.layer.next_feature() + } +} diff --git a/c/sedona-gdal/src/vector/mod.rs b/c/sedona-gdal/src/vector/mod.rs new file mode 100644 index 000000000..e93cac98f --- /dev/null +++ b/c/sedona-gdal/src/vector/mod.rs @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub mod feature; +pub mod geometry; +pub mod layer; + +pub use feature::{Feature, FieldDefn}; +pub use geometry::{Envelope, Geometry}; +pub use layer::Layer; diff --git a/c/sedona-gdal/src/vrt.rs b/c/sedona-gdal/src/vrt.rs new file mode 100644 index 000000000..32d4137d4 --- /dev/null +++ b/c/sedona-gdal/src/vrt.rs @@ -0,0 +1,324 @@ +// 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. + +//! GDAL VRT (Virtual Raster) API wrappers. + +use std::ffi::CString; +use std::ops::{Deref, DerefMut}; +use std::ptr::null_mut; + +use crate::cpl::CslStringList; +use crate::dataset::Dataset; +use crate::errors::{GdalError, Result}; +use crate::gdal_api::{call_gdal_api, GdalApi}; +use crate::raster::RasterBand; +use crate::{gdal_dyn_bindgen::*, GdalDataType}; + +/// Special value indicating that nodata is not set for a VRT source. +/// Matches `VRT_NODATA_UNSET` from GDAL's `gdal_vrt.h`. +pub const NODATA_UNSET: f64 = -1234.56; + +/// A VRT (Virtual Raster) dataset. +pub struct VrtDataset { + dataset: Dataset, +} + +unsafe impl Send for VrtDataset {} + +impl VrtDataset { + /// Create a new empty VRT dataset with the given dimensions. + pub fn create(api: &'static GdalApi, x_size: usize, y_size: usize) -> Result { + let x: i32 = x_size.try_into()?; + let y: i32 = y_size.try_into()?; + let c_dataset = unsafe { call_gdal_api!(api, VRTCreate, x, y) }; + + if c_dataset.is_null() { + return Err(GdalError::NullPointer { + method_name: "VRTCreate", + msg: String::new(), + }); + } + + Ok(VrtDataset { + dataset: Dataset::new_owned(api, c_dataset), + }) + } + + /// Consume this VRT and return the underlying `Dataset`, transferring ownership. + pub fn as_dataset(self) -> Dataset { + let VrtDataset { dataset } = self; + dataset + } + + /// Add a new band to the VRT dataset. + /// + /// Returns the 1-based index of the newly created band on success. + pub fn add_band(&mut self, data_type: GdalDataType, options: Option<&[&str]>) -> Result { + let csl = CslStringList::try_from_iter(options.unwrap_or(&[]).iter().copied())?; + + // Preserve null semantics: pass null when no options given. + let opts_ptr = if csl.is_empty() { + null_mut() + } else { + csl.as_ptr() + }; + + let rv = unsafe { + call_gdal_api!( + self.dataset.api(), + GDALAddBand, + self.dataset.c_dataset(), + data_type.to_c(), + opts_ptr + ) + }; + + if rv != CE_None { + return Err(self.dataset.api().last_cpl_err(rv as u32)); + } + + Ok(self.raster_count()) + } + + /// Fetch a band from the VRT dataset as a `VrtRasterBand`. + pub fn rasterband(&self, band_index: usize) -> Result> { + let band = self.dataset.rasterband(band_index)?; + Ok(VrtRasterBand { band }) + } +} + +impl Deref for VrtDataset { + type Target = Dataset; + + fn deref(&self) -> &Self::Target { + &self.dataset + } +} + +impl DerefMut for VrtDataset { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.dataset + } +} + +impl AsRef for VrtDataset { + fn as_ref(&self) -> &Dataset { + &self.dataset + } +} + +/// A raster band within a VRT dataset. +pub struct VrtRasterBand<'a> { + band: RasterBand<'a>, +} + +impl<'a> VrtRasterBand<'a> { + /// Returns the raw GDAL raster band handle. + pub fn c_rasterband(&self) -> GDALRasterBandH { + self.band.c_rasterband() + } + + /// Adds a simple source to this VRT band. + /// + /// # Arguments + /// * `source_band` - The source raster band to read from + /// * `src_window` - Source window as `(x_offset, y_offset, x_size, y_size)` in pixels + /// * `dst_window` - Destination window as `(x_offset, y_offset, x_size, y_size)` in pixels + /// * `resampling` - Optional resampling method (e.g. "near", "bilinear", "cubic"). + /// * `nodata` - Optional nodata value for the source. If None, uses `NODATA_UNSET`. + pub fn add_simple_source( + &self, + source_band: &RasterBand<'a>, + src_window: (i32, i32, i32, i32), + dst_window: (i32, i32, i32, i32), + resampling: Option<&str>, + nodata: Option, + ) -> Result<()> { + let c_resampling = resampling.and_then(|s| CString::new(s).ok()); + + let resampling_ptr = c_resampling + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(null_mut()); + + let nodata_value = nodata.unwrap_or(NODATA_UNSET); + + let rv = unsafe { + call_gdal_api!( + self.band.api(), + VRTAddSimpleSource, + self.band.c_rasterband(), + source_band.c_rasterband(), + src_window.0, + src_window.1, + src_window.2, + src_window.3, + dst_window.0, + dst_window.1, + dst_window.2, + dst_window.3, + resampling_ptr, + nodata_value + ) + }; + + if rv != CE_None { + return Err(self.band.api().last_cpl_err(rv as u32)); + } + Ok(()) + } + + /// Sets the nodata value for this VRT band. + pub fn set_no_data_value(&self, nodata: f64) -> Result<()> { + let rv = unsafe { + call_gdal_api!( + self.band.api(), + GDALSetRasterNoDataValue, + self.band.c_rasterband(), + nodata + ) + }; + + if rv != CE_None { + return Err(self.band.api().last_cpl_err(rv as u32)); + } + Ok(()) + } +} + +impl<'a> Deref for VrtRasterBand<'a> { + type Target = RasterBand<'a>; + + fn deref(&self) -> &Self::Target { + &self.band + } +} + +impl<'a> DerefMut for VrtRasterBand<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.band + } +} + +impl<'a> AsRef> for VrtRasterBand<'a> { + fn as_ref(&self) -> &RasterBand<'a> { + &self.band + } +} + +#[cfg(all(test, feature = "gdal-sys"))] +mod tests { + use crate::dataset::Dataset; + use crate::gdal_dyn_bindgen::GDAL_OF_READONLY; + use crate::register::get_global_gdal_api; + use crate::vrt::{VrtDataset, NODATA_UNSET}; + use crate::GdalDataType; + + fn fixture(name: &str) -> String { + sedona_testing::data::test_raster(name).unwrap() + } + + #[test] + fn test_vrt_create() { + let api = get_global_gdal_api().unwrap(); + let vrt = VrtDataset::create(api, 100, 100).unwrap(); + assert_eq!(vrt.raster_count(), 0); + assert!(!vrt.c_dataset().is_null()); + } + + #[test] + fn test_vrt_add_band() { + let api = get_global_gdal_api().unwrap(); + let mut vrt = VrtDataset::create(api, 100, 100).unwrap(); + let band_idx = vrt.add_band(GdalDataType::Float32, None).unwrap(); + assert_eq!(band_idx, 1); + assert_eq!(vrt.raster_count(), 1); + + let band_idx = vrt.add_band(GdalDataType::UInt8, None).unwrap(); + assert_eq!(band_idx, 2); + assert_eq!(vrt.raster_count(), 2); + } + + #[test] + fn test_vrt_set_geo_transform() { + let api = get_global_gdal_api().unwrap(); + let vrt = VrtDataset::create(api, 100, 100).unwrap(); + let transform = [0.0, 1.0, 0.0, 100.0, 0.0, -1.0]; + vrt.set_geo_transform(&transform).unwrap(); + assert_eq!(vrt.geo_transform().unwrap(), transform); + } + + #[test] + fn test_vrt_set_projection() { + let api = get_global_gdal_api().unwrap(); + let vrt = VrtDataset::create(api, 100, 100).unwrap(); + vrt.set_projection("EPSG:4326").unwrap(); + assert!(vrt.projection().contains("4326")); + } + + #[test] + fn test_vrt_add_simple_source() { + let api = get_global_gdal_api().unwrap(); + let source = Dataset::open_ex( + api, + &fixture("tinymarble.tif"), + GDAL_OF_READONLY, + None, + None, + None, + ) + .unwrap(); + let source_band_type = source.rasterband(1).unwrap().band_type(); + + let mut vrt = VrtDataset::create(api, 1, 1).unwrap(); + vrt.add_band(source_band_type, None).unwrap(); + + let source_band = source.rasterband(1).unwrap(); + let vrt_band = vrt.rasterband(1).unwrap(); + + vrt_band + .add_simple_source(&source_band, (0, 0, 1, 1), (0, 0, 1, 1), None, None) + .unwrap(); + + let source_px = source_band + .read_as::((0, 0), (1, 1), (1, 1), None) + .unwrap() + .data()[0]; + let vrt_px = vrt_band + .read_as::((0, 0), (1, 1), (1, 1), None) + .unwrap() + .data()[0]; + + assert_eq!(vrt_px, source_px); + } + + #[test] + fn test_vrt_nodata_unset() { + assert_eq!(NODATA_UNSET, -1234.56); + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_vrt_set_no_data_value() { + let api = get_global_gdal_api().unwrap(); + let mut vrt = VrtDataset::create(api, 1, 1).unwrap(); + vrt.add_band(GdalDataType::UInt8, None).unwrap(); + let band = vrt.rasterband(1).unwrap(); + band.set_no_data_value(-9999.0).unwrap(); + assert_eq!(band.no_data_value(), Some(-9999.0)); + } +} diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs new file mode 100644 index 000000000..495f4b671 --- /dev/null +++ b/c/sedona-gdal/src/vsi.rs @@ -0,0 +1,177 @@ +// 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. + +//! 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::register::get_global_gdal_api; + + #[test] + fn create_and_retrieve_mem_file() { + let file_name = "/vsimem/525ebf24-a030-4677-bb4e-a921741cabe0"; + + let api = get_global_gdal_api().unwrap(); + 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 + } + if file_name == file_name + )); + } + + #[test] + fn create_and_unlink_mem_file() { + let file_name = "/vsimem/bbf5f1d6-c1e9-4469-a33b-02cd9173132d"; + + let api = get_global_gdal_api().unwrap(); + create_mem_file(api, file_name, vec![1_u8, 2, 3, 4]).unwrap(); + + unlink_mem_file(api, file_name).unwrap(); + } + + #[test] + fn no_mem_file() { + let api = get_global_gdal_api().unwrap(); + assert!(matches!( + get_vsi_mem_file_bytes_owned(api, "foobar").unwrap_err(), + GdalError::NullPointer { + method_name: "VSIGetMemFileBuffer", + msg, + } + if msg.is_empty() + )); + } +} From aa8d091eb29df7d00c33fccffdfacfe3a929c7a8 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Mon, 2 Mar 2026 18:06:14 +0800 Subject: [PATCH 2/8] feat(sedona): expose gdal-sys feature flag --- Cargo.lock | 1 + rust/sedona/Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a7b5bd24b..9c7702fd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5110,6 +5110,7 @@ dependencies = [ "sedona-datasource", "sedona-expr", "sedona-functions", + "sedona-gdal", "sedona-geo", "sedona-geometry", "sedona-geoparquet", diff --git a/rust/sedona/Cargo.toml b/rust/sedona/Cargo.toml index eefc5b937..1550ff3d9 100644 --- a/rust/sedona/Cargo.toml +++ b/rust/sedona/Cargo.toml @@ -41,6 +41,7 @@ tg = ["dep:sedona-tg"] http = ["object_store/http"] pointcloud = ["dep:sedona-pointcloud"] proj = ["sedona-proj/proj-sys"] +gdal = ["sedona-gdal/gdal-sys"] spatial-join = ["dep:sedona-spatial-join"] s2geography = ["dep:sedona-s2geography"] @@ -77,6 +78,7 @@ sedona-geoparquet = { workspace = true } sedona-geos = { workspace = true, optional = true } sedona-pointcloud = { workspace = true, optional = true } sedona-proj = { workspace = true } +sedona-gdal = { workspace = true } sedona-raster-functions = { workspace = true } sedona-schema = { workspace = true } sedona-spatial-join = { workspace = true, optional = true } From fd8a2c97380c112dc460e3195fc9be987af5ed9e Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 3 Mar 2026 19:09:20 +0800 Subject: [PATCH 3/8] Fix compilation after rebasing --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 118b84fcb..0c577c443 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,6 @@ geo-types = "0.7.17" geojson = "0.24.2" geos = { version = "11.0.1", features = ["geo", "v3_12_0"] } glam = "0.32.0" -libloading = "0.9" libmimalloc-sys = { version = "0.1", default-features = false } log = "^0.4" libloading = "0.9" From 7987307e9f052257ba5ae1ad48d448ce53ab70ba Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 3 Mar 2026 20:52:17 +0800 Subject: [PATCH 4/8] Don't need to enable the bindgen feature for gdal-sys --- Cargo.lock | 1 - c/sedona-gdal/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c7702fd1..f9414a7c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2742,7 +2742,6 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cceef1cc08a1f031c5717cb645bb361a3114470cc142cc96bc5e62b79695632e" dependencies = [ - "bindgen", "pkg-config", "semver", ] diff --git a/c/sedona-gdal/Cargo.toml b/c/sedona-gdal/Cargo.toml index 1984d3e22..742f7c519 100644 --- a/c/sedona-gdal/Cargo.toml +++ b/c/sedona-gdal/Cargo.toml @@ -33,7 +33,7 @@ rust-version.workspace = true crate-type = ["staticlib", "cdylib", "lib"] [dependencies] -gdal-sys = { version = "0.12.0", features = ["bindgen"], optional = true } +gdal-sys = { version = "0.12.0", optional = true } libloading = { workspace = true } thiserror = { workspace = true } From e55d1761443e276fa6b0b9d210628143484321d7 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 3 Mar 2026 21:18:22 +0800 Subject: [PATCH 5/8] ci: add libgdal-dev to rust workflow dependencies for gdal-sys feature --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 03b6b5f79..d79d7a23a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -128,7 +128,7 @@ jobs: - name: Install dependencies shell: bash run: | - sudo apt-get update && sudo apt-get install -y libgeos-dev + sudo apt-get update && sudo apt-get install -y libgeos-dev libgdal-dev - name: Check if: matrix.name == 'check' From 2510b8822399f479bd39859eb92f3bb2c78356dc Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 3 Mar 2026 22:08:01 +0800 Subject: [PATCH 6/8] fix: use 1x1 dataset in create_vector_only for GDAL < 3.7 compat --- c/sedona-gdal/src/driver.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs index 9bddb58dd..0c96c83cb 100644 --- a/c/sedona-gdal/src/driver.rs +++ b/c/sedona-gdal/src/driver.rs @@ -133,8 +133,8 @@ impl Driver { GDALCreate, self.c_driver, c_filename.as_ptr(), - 0, - 0, + 1, + 1, 0, GDALDataType::GDT_Unknown, ptr::null_mut() @@ -225,6 +225,6 @@ mod tests { 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(), (0, 0)); + assert_eq!(ds.raster_size(), (1, 1)); } } From e133fed0d778ee253cbb1a502d72140fe17c8493 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Tue, 3 Mar 2026 23:12:15 +0800 Subject: [PATCH 7/8] Fix some of the review comments --- c/sedona-gdal/src/mem.rs | 18 +++++++++++++----- c/sedona-gdal/src/vrt.rs | 2 +- c/sedona-gdal/src/vsi.rs | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/c/sedona-gdal/src/mem.rs b/c/sedona-gdal/src/mem.rs index 254e02dc5..783d1d7c8 100644 --- a/c/sedona-gdal/src/mem.rs +++ b/c/sedona-gdal/src/mem.rs @@ -222,9 +222,9 @@ impl MemDatasetBuilder { api, MEMDatasetCreate, empty_filename.as_ptr(), - self.width as i32, - self.height as i32, - self.n_owned_bands as i32, + self.width.try_into()?, + self.height.try_into()?, + self.n_owned_bands.try_into()?, owned_bands_data_type, std::ptr::null_mut() ) @@ -258,7 +258,7 @@ impl MemDatasetBuilder { // Set per-band nodata values. for (i, band_spec) in self.bands.iter().enumerate() { if let Some(nodata) = &band_spec.nodata { - let raster_band = dataset.rasterband(i + 1)?; + let raster_band = dataset.rasterband(i + 1 + self.n_owned_bands)?; match nodata { Nodata::F64(v) => raster_band.set_no_data_value(Some(*v))?, Nodata::I64(v) => raster_band.set_no_data_value_i64(Some(*v))?, @@ -405,7 +405,13 @@ mod tests { let external_band = [0u8; 8 * 8]; let dataset = unsafe { MemDatasetBuilder::new_with_owned_bands(8, 8, 1, GdalDataType::Float32) - .add_band(GdalDataType::UInt8, external_band.as_ptr()) + .add_band_with_options( + GdalDataType::UInt8, + external_band.as_ptr(), + None, + None, + Some(Nodata::U64(255)), + ) .build() .unwrap() }; @@ -418,5 +424,7 @@ mod tests { dataset.rasterband(2).unwrap().band_type(), GdalDataType::UInt8 ); + let nodata = dataset.rasterband(2).unwrap().no_data_value(); + assert_eq!(nodata, Some(255.0)); } } diff --git a/c/sedona-gdal/src/vrt.rs b/c/sedona-gdal/src/vrt.rs index 32d4137d4..80be65f7b 100644 --- a/c/sedona-gdal/src/vrt.rs +++ b/c/sedona-gdal/src/vrt.rs @@ -148,7 +148,7 @@ impl<'a> VrtRasterBand<'a> { resampling: Option<&str>, nodata: Option, ) -> Result<()> { - let c_resampling = resampling.and_then(|s| CString::new(s).ok()); + let c_resampling = resampling.map(CString::new).transpose()?; let resampling_ptr = c_resampling .as_ref() diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs index 495f4b671..3c8f099b5 100644 --- a/c/sedona-gdal/src/vsi.rs +++ b/c/sedona-gdal/src/vsi.rs @@ -146,9 +146,9 @@ mod tests { assert!(matches!( unlink_mem_file(api, file_name).unwrap_err(), GdalError::UnlinkMemFile { - file_name + file_name: err_file_name } - if file_name == file_name + if err_file_name == file_name )); } From 8b852d90c36f1b150f5758f1a105dab1d6944c9f Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Wed, 4 Mar 2026 00:25:06 +0800 Subject: [PATCH 8/8] Add georust/gdal attribution comments to sedona-gdal safe API files Add file-level doc comments to all safe GDAL API wrappers in c/sedona-gdal/src that were ported from georust/gdal v0.19.0, noting the original source URL and MIT license. --- c/sedona-gdal/src/config.rs | 4 ++++ c/sedona-gdal/src/cpl.rs | 4 ++++ c/sedona-gdal/src/dataset.rs | 4 ++++ c/sedona-gdal/src/driver.rs | 4 ++++ c/sedona-gdal/src/errors.rs | 4 ++++ c/sedona-gdal/src/geo_transform.rs | 4 ++++ c/sedona-gdal/src/raster/rasterband.rs | 4 ++++ c/sedona-gdal/src/raster/rasterize.rs | 4 ++++ c/sedona-gdal/src/raster/types.rs | 4 ++++ c/sedona-gdal/src/spatial_ref.rs | 4 ++++ c/sedona-gdal/src/vector/feature.rs | 4 ++++ c/sedona-gdal/src/vector/geometry.rs | 4 ++++ c/sedona-gdal/src/vector/layer.rs | 4 ++++ c/sedona-gdal/src/vsi.rs | 4 ++++ 14 files changed, 56 insertions(+) diff --git a/c/sedona-gdal/src/config.rs b/c/sedona-gdal/src/config.rs index 2f9b1910b..2735d6b56 100644 --- a/c/sedona-gdal/src/config.rs +++ b/c/sedona-gdal/src/config.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/cpl.rs b/c/sedona-gdal/src/cpl.rs index 15257285c..6cae7673b 100644 --- a/c/sedona-gdal/src/cpl.rs +++ b/c/sedona-gdal/src/cpl.rs @@ -15,6 +15,10 @@ // 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 diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs index 49084ec55..2bba50a25 100644 --- a/c/sedona-gdal/src/dataset.rs +++ b/c/sedona-gdal/src/dataset.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs index 0c96c83cb..037fa9d41 100644 --- a/c/sedona-gdal/src/driver.rs +++ b/c/sedona-gdal/src/driver.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs index 3e990d80e..03aa3d124 100644 --- a/c/sedona-gdal/src/errors.rs +++ b/c/sedona-gdal/src/errors.rs @@ -15,6 +15,10 @@ // 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::NulError; use std::num::TryFromIntError; diff --git a/c/sedona-gdal/src/geo_transform.rs b/c/sedona-gdal/src/geo_transform.rs index 78fb0d19d..5504e8501 100644 --- a/c/sedona-gdal/src/geo_transform.rs +++ b/c/sedona-gdal/src/geo_transform.rs @@ -15,6 +15,10 @@ // 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) diff --git a/c/sedona-gdal/src/raster/rasterband.rs b/c/sedona-gdal/src/raster/rasterband.rs index c7e0ec590..dcc975f52 100644 --- a/c/sedona-gdal/src/raster/rasterband.rs +++ b/c/sedona-gdal/src/raster/rasterband.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/raster/rasterize.rs b/c/sedona-gdal/src/raster/rasterize.rs index e800df711..f85286a42 100644 --- a/c/sedona-gdal/src/raster/rasterize.rs +++ b/c/sedona-gdal/src/raster/rasterize.rs @@ -15,6 +15,10 @@ // 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::ptr; use crate::cpl::CslStringList; diff --git a/c/sedona-gdal/src/raster/types.rs b/c/sedona-gdal/src/raster/types.rs index b2d2e8325..c2cf7f1d8 100644 --- a/c/sedona-gdal/src/raster/types.rs +++ b/c/sedona-gdal/src/raster/types.rs @@ -15,6 +15,10 @@ // 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. diff --git a/c/sedona-gdal/src/spatial_ref.rs b/c/sedona-gdal/src/spatial_ref.rs index b2027d58b..78153d25a 100644 --- a/c/sedona-gdal/src/spatial_ref.rs +++ b/c/sedona-gdal/src/spatial_ref.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/vector/feature.rs b/c/sedona-gdal/src/vector/feature.rs index 870684fe5..5afc61057 100644 --- a/c/sedona-gdal/src/vector/feature.rs +++ b/c/sedona-gdal/src/vector/feature.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/vector/geometry.rs b/c/sedona-gdal/src/vector/geometry.rs index a21e8e76b..aefc153a6 100644 --- a/c/sedona-gdal/src/vector/geometry.rs +++ b/c/sedona-gdal/src/vector/geometry.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/vector/layer.rs b/c/sedona-gdal/src/vector/layer.rs index 4ba626a4d..0e23a4246 100644 --- a/c/sedona-gdal/src/vector/layer.rs +++ b/c/sedona-gdal/src/vector/layer.rs @@ -15,6 +15,10 @@ // 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; diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs index 3c8f099b5..f00c5c1f3 100644 --- a/c/sedona-gdal/src/vsi.rs +++ b/c/sedona-gdal/src/vsi.rs @@ -15,6 +15,10 @@ // 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;