This is an automated email from the ASF dual-hosted git repository.
kontinuation pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 2822b306 feat(sedona-gdal): add dataset and vector/raster wrappers
(#699)
2822b306 is described below
commit 2822b30680a7b46ddd12fd1f32eacb576a16abac
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Mar 26 21:57:00 2026 +0800
feat(sedona-gdal): add dataset and vector/raster wrappers (#699)
## Summary
- add safe wrappers for GDAL datasets, drivers, raster bands, features, and
layers
- wire the core raster/vector modules that higher-level operations build on
- keep the wrapper surface explicit by removing module re-export aliases
from this layer
---
c/sedona-gdal/src/dataset.rs | 501 +++++++++++++++++++++++++++++++++
c/sedona-gdal/src/driver.rs | 242 ++++++++++++++++
c/sedona-gdal/src/errors.rs | 3 +
c/sedona-gdal/src/lib.rs | 2 +
c/sedona-gdal/src/raster.rs | 1 +
c/sedona-gdal/src/raster/rasterband.rs | 372 ++++++++++++++++++++++++
c/sedona-gdal/src/vector.rs | 2 +
c/sedona-gdal/src/vector/feature.rs | 213 ++++++++++++++
c/sedona-gdal/src/vector/layer.rs | 208 ++++++++++++++
c/sedona-gdal/src/vsi.rs | 2 +-
10 files changed, 1545 insertions(+), 1 deletion(-)
diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs
new file mode 100644
index 00000000..36832d86
--- /dev/null
+++ b/c/sedona-gdal/src/dataset.rs
@@ -0,0 +1,501 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/dataset.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::{CStr, CString};
+use std::ptr;
+
+use crate::cpl::CslStringList;
+use crate::driver::Driver;
+use crate::errors::Result;
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::raster::rasterband::RasterBand;
+use crate::raster::types::{DatasetOptions, GdalDataType as RustGdalDataType};
+use crate::spatial_ref::SpatialRef;
+use crate::vector::layer::Layer;
+
+/// A GDAL dataset.
+pub struct Dataset {
+ api: &'static GdalApi,
+ c_dataset: GDALDatasetH,
+}
+
+// SAFETY: `Dataset` has unique ownership of its GDAL dataset handle and only
moves
+// that ownership across threads. The handle is closed exactly once on drop,
and this
+// wrapper does not provide shared concurrent access, so `Send` is sound while
`Sync`
+// remains intentionally unimplemented.
+unsafe impl Send for Dataset {}
+
+impl Drop for Dataset {
+ fn drop(&mut self) {
+ if !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<Self> {
+ 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 })
+ }
+
+ /// Create a new Dataset from an owned C handle.
+ pub(crate) fn new(api: &'static GdalApi, c_dataset: GDALDatasetH) -> Self {
+ Self { api, c_dataset }
+ }
+
+ /// 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 }
+ }
+
+ /// Fetch a raster band by 1-indexed band number.
+ /// Band numbers start at 1, as in GDAL.
+ pub fn rasterband(&self, band_index: usize) -> Result<RasterBand<'_>> {
+ 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(self.api.last_null_pointer_err("GDALGetRasterBand"));
+ }
+ Ok(RasterBand::new(self.api, c_band, self))
+ }
+
+ /// Fetch the dataset geotransform coefficients.
+ /// Return an error if no geotransform is available.
+ 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(())
+ }
+
+ /// Fetch the projection definition string for this dataset.
+ /// Return an empty string if no projection is available.
+ 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 definition string for this dataset.
+ 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(())
+ }
+
+ /// Fetch the spatial reference for this dataset.
+ /// GDAL returns a borrowed handle; this method clones it.
+ pub fn spatial_ref(&self) -> Result<SpatialRef> {
+ let c_srs = unsafe { call_gdal_api!(self.api, GDALGetSpatialRef,
self.c_dataset) };
+ if c_srs.is_null() {
+ return Err(self.api.last_null_pointer_err("GDALGetSpatialRef"));
+ }
+ // GDALGetSpatialRef returns a borrowed reference — clone it via
OSRClone.
+ unsafe { SpatialRef::from_c_srs_clone(self.api, c_srs) }
+ }
+
+ /// Set the spatial reference.
+ pub fn set_spatial_ref(&self, srs: &SpatialRef) -> Result<()> {
+ let rv =
+ unsafe { call_gdal_api!(self.api, GDALSetSpatialRef,
self.c_dataset, srs.c_srs()) };
+ if rv != CE_None {
+ return Err(self.api.last_cpl_err(rv as u32));
+ }
+ Ok(())
+ }
+
+ /// Create a copy of this dataset to a new file using the given driver.
+ pub fn create_copy(
+ &self,
+ driver: &Driver,
+ filename: &str,
+ options: &[&str],
+ ) -> Result<Dataset> {
+ 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::new(self.api, c_ds))
+ }
+
+ /// Create a new vector layer.
+ pub fn create_layer(&self, options: LayerOptions<'_>) -> Result<Layer<'_>>
{
+ 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(self.api.last_null_pointer_err("GDALDatasetCreateLayer"));
+ }
+ 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> {
+ Self::open_ex(
+ api,
+ path,
+ options.open_flags,
+ options.allowed_drivers,
+ options.open_options,
+ options.sibling_files,
+ )
+ }
+
+ /// Add a band backed by an existing memory buffer.
+ /// Pass `DATAPOINTER`, `PIXELOFFSET`, and `LINEOFFSET` to `GDALAddBand`.
+ ///
+ /// # Safety
+ ///
+ /// `data_ptr` must point to valid band data that outlives this dataset.
+ pub unsafe fn add_band_with_data(
+ &self,
+ data_type: RustGdalDataType,
+ data_ptr: *const u8,
+ pixel_offset: Option<i64>,
+ line_offset: Option<i64>,
+ ) -> 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(())
+ }
+}
+
+/// 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 gdal_sys::{GDALDatasetGetLayer, GDALDatasetGetLayerCount};
+
+ use crate::driver::DriverManager;
+ use crate::gdal_dyn_bindgen::{OGRwkbGeometryType, GDAL_OF_READONLY,
GDAL_OF_VECTOR};
+ use crate::global::with_global_gdal_api;
+ use crate::vector::layer::Layer;
+ use crate::vsi::unlink_mem_file;
+
+ use super::{Dataset, LayerOptions};
+
+ #[test]
+ fn test_geo_transform_roundtrip() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create("", 256, 256, 1).unwrap();
+
+ let gt = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0];
+ ds.set_geo_transform(>).unwrap();
+ let got = ds.geo_transform().unwrap();
+ assert_eq!(gt, got);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_geo_transform_unset() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create("", 256, 256, 1).unwrap();
+
+ // MEM driver without an explicit set_geo_transform returns an
error
+ assert!(ds.geo_transform().is_err());
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_set_projection_roundtrip() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create("", 256, 256, 1).unwrap();
+
+ let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS
84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#;
+ ds.set_projection(wkt).unwrap();
+ let got = ds.projection();
+ // The returned WKT may be reformatted by GDAL, so just check it
contains WGS 84
+ assert!(got.contains("WGS 84"), "Expected WGS 84 in: {got}");
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_dataset_raster_count() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+
+ let ds1 = driver.create("", 64, 64, 1).unwrap();
+ assert_eq!(ds1.raster_count(), 1);
+
+ let ds3 = driver.create("", 64, 64, 3).unwrap();
+ assert_eq!(ds3.raster_count(), 3);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_dataset_raster_size() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create("", 123, 456, 1).unwrap();
+ assert_eq!(ds.raster_size(), (123, 456));
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_create_vector_layer() {
+ with_global_gdal_api(|api| {
+ let path = "/vsimem/test_dataset_create_vector_layer.gpkg";
+ let driver = DriverManager::get_driver_by_name(api,
"GPKG").unwrap();
+ let dataset = driver.create_vector_only(path).unwrap();
+
+ let layer = dataset
+ .create_layer(LayerOptions {
+ name: "points",
+ srs: None,
+ ty: OGRwkbGeometryType::wkbPoint,
+ options: None,
+ })
+ .unwrap();
+
+ assert_eq!(dataset.raster_count(), 0);
+ assert!(!layer.c_layer().is_null());
+ assert_eq!(layer.feature_count(true), 0);
+
+ unlink_mem_file(api, path).unwrap();
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_open_vector_dataset_with_open_ex() {
+ with_global_gdal_api(|api| {
+ let path = "/vsimem/test_dataset_open_vector.gpkg";
+ let driver = DriverManager::get_driver_by_name(api,
"GPKG").unwrap();
+ {
+ let dataset = driver.create_vector_only(path).unwrap();
+
+ dataset
+ .create_layer(LayerOptions {
+ name: "points",
+ srs: None,
+ ty: OGRwkbGeometryType::wkbPoint,
+ options: None,
+ })
+ .unwrap();
+ }
+
+ let reopened = Dataset::open_ex(
+ api,
+ path,
+ GDAL_OF_VECTOR | GDAL_OF_READONLY,
+ None,
+ None,
+ None,
+ )
+ .unwrap();
+
+ let layer_count = unsafe {
GDALDatasetGetLayerCount(reopened.c_dataset()) };
+ assert_eq!(layer_count, 1);
+
+ let c_layer = unsafe { GDALDatasetGetLayer(reopened.c_dataset(),
0) };
+ assert!(!c_layer.is_null());
+
+ let reopened_layer = Layer::new(api, c_layer, &reopened);
+ assert_eq!(reopened_layer.feature_count(true), 0);
+
+ unlink_mem_file(api, path).unwrap();
+ })
+ .unwrap();
+ }
+}
diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs
new file mode 100644
index 00000000..cced1754
--- /dev/null
+++ b/c/sedona-gdal/src/driver.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.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/driver.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::CString;
+use std::ptr;
+
+use crate::dataset::Dataset;
+use crate::errors::Result;
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::raster::types::GdalDataType as RustGdalDataType;
+use crate::raster::types::GdalType;
+
+/// A GDAL driver.
+pub struct Driver {
+ api: &'static GdalApi,
+ c_driver: GDALDriverH,
+}
+
+impl Driver {
+ /// Wrap an existing C driver handle.
+ ///
+ /// # Safety
+ ///
+ /// The caller must ensure the handle is valid.
+ pub unsafe fn from_c_driver(api: &'static GdalApi, c_driver: GDALDriverH)
-> Self {
+ Self { api, c_driver }
+ }
+
+ /// Return the raw C driver handle.
+ pub fn c_driver(&self) -> GDALDriverH {
+ self.c_driver
+ }
+
+ /// Create a new raster dataset (with u8 band type).
+ pub fn create(
+ &self,
+ filename: &str,
+ size_x: usize,
+ size_y: usize,
+ bands: usize,
+ ) -> Result<Dataset> {
+ self.create_with_band_type::<u8>(filename, size_x, size_y, bands)
+ }
+
+ /// Create a new raster dataset with a specific band type.
+ pub fn create_with_band_type<T: GdalType>(
+ &self,
+ filename: &str,
+ size_x: usize,
+ size_y: usize,
+ bands: usize,
+ ) -> Result<Dataset> {
+ 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(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<Dataset> {
+ 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(self.api, c_ds))
+ }
+
+ /// Create a new dataset (vector-only, no raster bands).
+ pub fn create_vector_only(&self, filename: &str) -> Result<Dataset> {
+ let c_filename = CString::new(filename)?;
+ let c_ds = unsafe {
+ call_gdal_api!(
+ self.api,
+ GDALCreate,
+ self.c_driver,
+ c_filename.as_ptr(),
+ 1,
+ 1,
+ 0,
+ GDALDataType::GDT_Unknown,
+ ptr::null_mut()
+ )
+ };
+ if c_ds.is_null() {
+ return Err(self.api.last_cpl_err(CE_Failure as u32));
+ }
+ Ok(Dataset::new(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<Driver> {
+ let c_name = CString::new(name)?;
+ let c_driver = unsafe { call_gdal_api!(api, GDALGetDriverByName,
c_name.as_ptr()) };
+ if c_driver.is_null() {
+ return Err(api.last_null_pointer_err("GDALGetDriverByName"));
+ }
+ Ok(Driver { api, c_driver })
+ }
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+ use crate::driver::DriverManager;
+ use crate::errors::GdalError;
+ use crate::global::with_global_gdal_api;
+ use crate::raster::types::GdalDataType;
+
+ #[test]
+ fn test_get_driver_by_name() {
+ with_global_gdal_api(|api| {
+ let gtiff = DriverManager::get_driver_by_name(api,
"GTiff").unwrap();
+ assert!(!gtiff.c_driver().is_null());
+ let mem = DriverManager::get_driver_by_name(api, "MEM").unwrap();
+ assert!(!mem.c_driver().is_null());
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_get_driver_by_name_invalid() {
+ with_global_gdal_api(|api| {
+ let err = DriverManager::get_driver_by_name(api, "NO_SUCH_DRIVER");
+ assert!(matches!(err, Err(GdalError::NullPointer { .. })));
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_driver_create() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create("", 32, 16, 2).unwrap();
+ assert_eq!(ds.raster_size(), (32, 16));
+ assert_eq!(ds.raster_count(), 2);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_driver_create_with_band_type() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create_with_band_type::<u8>("", 10, 20,
1).unwrap();
+ assert_eq!(ds.raster_count(), 1);
+ let ds = driver.create_with_band_type::<f32>("", 10, 20,
2).unwrap();
+ assert_eq!(ds.raster_count(), 2);
+ let ds = driver.create_with_band_type::<i16>("", 10, 20,
3).unwrap();
+ assert_eq!(ds.raster_count(), 3);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_driver_create_with_data_type() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver
+ .create_with_data_type("", 8, 8, 1, GdalDataType::UInt16)
+ .unwrap();
+ assert_eq!(ds.raster_count(), 1);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_driver_create_vector_only() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let ds = driver.create_vector_only("").unwrap();
+ assert_eq!(ds.raster_count(), 0);
+ assert_eq!(ds.raster_size(), (1, 1));
+ })
+ .unwrap();
+ }
+}
diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs
index f344667e..3ff5d03d 100644
--- a/c/sedona-gdal/src/errors.rs
+++ b/c/sedona-gdal/src/errors.rs
@@ -63,6 +63,9 @@ pub enum GdalError {
#[error(transparent)]
IntConversionError(#[from] TryFromIntError),
+
+ #[error("Buffer length {0} does not match raster size {1:?}")]
+ BufferSizeMismatch(usize, (usize, usize)),
}
pub type Result<T> = std::result::Result<T, GdalError>;
diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs
index 0646a241..c331f5df 100644
--- a/c/sedona-gdal/src/lib.rs
+++ b/c/sedona-gdal/src/lib.rs
@@ -29,6 +29,8 @@ pub mod global;
// --- High-level wrappers ---
pub mod config;
pub mod cpl;
+pub mod dataset;
+pub mod driver;
pub mod geo_transform;
pub mod raster;
pub mod spatial_ref;
diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs
index 1ddc9b2e..389d9d73 100644
--- a/c/sedona-gdal/src/raster.rs
+++ b/c/sedona-gdal/src/raster.rs
@@ -15,4 +15,5 @@
// specific language governing permissions and limitations
// under the License.
+pub mod rasterband;
pub mod types;
diff --git a/c/sedona-gdal/src/raster/rasterband.rs
b/c/sedona-gdal/src/raster/rasterband.rs
new file mode 100644
index 00000000..9e537669
--- /dev/null
+++ b/c/sedona-gdal/src/raster/rasterband.rs
@@ -0,0 +1,372 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/raster/rasterband.rs>.
+//! Original code is licensed under MIT.
+
+use std::marker::PhantomData;
+
+use crate::dataset::Dataset;
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::raster::types::{Buffer, GdalType, ResampleAlg};
+use crate::{gdal_dyn_bindgen::*, raster::types::GdalDataType};
+
+/// A raster band of a dataset.
+pub struct RasterBand<'a> {
+ api: &'static GdalApi,
+ c_rasterband: GDALRasterBandH,
+ _dataset: PhantomData<&'a Dataset>,
+}
+
+impl<'a> RasterBand<'a> {
+ pub(crate) fn new(
+ api: &'static GdalApi,
+ c_rasterband: GDALRasterBandH,
+ _dataset: &'a Dataset,
+ ) -> Self {
+ Self {
+ api,
+ c_rasterband,
+ _dataset: PhantomData,
+ }
+ }
+
+ /// Return the raw C raster band handle.
+ pub fn c_rasterband(&self) -> GDALRasterBandH {
+ self.c_rasterband
+ }
+
+ /// Read a window of this band into a typed buffer.
+ /// If `e_resample_alg` is `None`, use nearest-neighbour resampling.
+ pub fn read_as<T: GdalType + Copy>(
+ &self,
+ window: (isize, isize),
+ window_size: (usize, usize),
+ size: (usize, usize),
+ e_resample_alg: Option<ResampleAlg>,
+ ) -> Result<Buffer<T>> {
+ 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<T> = 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<T: GdalType + Copy>(
+ &self,
+ window: (isize, isize),
+ window_size: (usize, usize),
+ buffer: &mut Buffer<T>,
+ ) -> 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(())
+ }
+
+ /// Fetch this band's data type.
+ pub fn band_type(&self) -> GdalDataType {
+
GdalDataType::from_c(self.c_band_type()).unwrap_or(GdalDataType::Unknown)
+ }
+
+ /// Fetch this band's raw GDAL data type.
+ pub fn c_band_type(&self) -> GDALDataType {
+ unsafe { call_gdal_api!(self.api, GDALGetRasterDataType,
self.c_rasterband) }
+ }
+
+ /// Fetch 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)
+ }
+
+ /// Fetch the natural 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)
+ }
+
+ /// Fetch the band's nodata value.
+ /// Return `None` if no nodata value is set.
+ pub fn no_data_value(&self) -> Option<f64> {
+ 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 the band's nodata value.
+ /// Pass `None` to clear any existing nodata value.
+ pub fn set_no_data_value(&self, value: Option<f64>) -> 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 the band's nodata value as `u64`.
+ /// Pass `None` to clear any existing nodata value.
+ pub fn set_no_data_value_u64(&self, value: Option<u64>) -> 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 the band's nodata value as `i64`.
+ /// Pass `None` to clear any existing nodata value.
+ pub fn set_no_data_value_i64(&self, value: Option<i64>) -> 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
+ }
+}
+
+/// Return the actual block size for a block index.
+/// Clamp edge blocks to the raster extent.
+pub fn actual_block_size(
+ band: &RasterBand<'_>,
+ block_index: (usize, usize),
+) -> Result<(usize, usize)> {
+ let (block_x, block_y) = band.block_size();
+ let (raster_x, raster_y) = band.size();
+ let x_off = block_index.0 * block_x;
+ let y_off = block_index.1 * block_y;
+ if x_off >= raster_x || y_off >= raster_y {
+ return Err(GdalError::BadArgument(format!(
+ "block index ({}, {}) is out of bounds for raster size ({}, {})",
+ block_index.0, block_index.1, raster_x, raster_y
+ )));
+ }
+ let actual_x = if x_off + block_x > raster_x {
+ raster_x - x_off
+ } else {
+ block_x
+ };
+ let actual_y = if y_off + block_y > raster_y {
+ raster_y - y_off
+ } else {
+ block_y
+ };
+ Ok((actual_x, actual_y))
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+ use crate::dataset::Dataset;
+ use crate::driver::DriverManager;
+ use crate::gdal_dyn_bindgen::*;
+ use crate::global::with_global_gdal_api;
+ use crate::raster::types::ResampleAlg;
+
+ fn fixture(name: &str) -> String {
+ sedona_testing::data::test_raster(name).unwrap()
+ }
+
+ #[test]
+ fn test_read_raster() {
+ with_global_gdal_api(|api| {
+ let path = fixture("tinymarble.tif");
+ let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None,
None, None).unwrap();
+ let rb = dataset.rasterband(1).unwrap();
+ let rv = rb.read_as::<u8>((20, 30), (2, 3), (2, 3), None).unwrap();
+ assert_eq!(rv.shape, (2, 3));
+ assert_eq!(rv.data(), [7, 7, 7, 10, 8, 12]);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_read_raster_with_default_resample() {
+ with_global_gdal_api(|api| {
+ let path = fixture("tinymarble.tif");
+ let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None,
None, None).unwrap();
+ let rb = dataset.rasterband(1).unwrap();
+ let rv = rb.read_as::<u8>((20, 30), (4, 4), (2, 2), None).unwrap();
+ assert_eq!(rv.shape, (2, 2));
+ // Default is NearestNeighbour; exact values are
GDAL-version-dependent
+ // when downsampling from 4x4 to 2x2. Just verify shape and
non-emptiness.
+ assert_eq!(rv.data().len(), 4);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_read_raster_with_average_resample() {
+ with_global_gdal_api(|api| {
+ let path = fixture("tinymarble.tif");
+ let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None,
None, None).unwrap();
+ let rb = dataset.rasterband(1).unwrap();
+ let rv = rb
+ .read_as::<u8>((20, 30), (4, 4), (2, 2),
Some(ResampleAlg::Average))
+ .unwrap();
+ assert_eq!(rv.shape, (2, 2));
+ // Average resampling; exact values are GDAL-version-dependent, so
just
+ // verify that the downsampled result has the expected shape and
length.
+ assert_eq!(rv.data().len(), 4);
+ })
+ .unwrap();
+ }
+
+ #[test]
+ fn test_get_no_data_value() {
+ with_global_gdal_api(|api| {
+ // tinymarble.tif has no nodata
+ let path = fixture("tinymarble.tif");
+ let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None,
None, None).unwrap();
+ let rb = dataset.rasterband(1).unwrap();
+ assert!(rb.no_data_value().is_none());
+
+ // labels.tif has nodata=255
+ let path = fixture("labels.tif");
+ let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None,
None, None).unwrap();
+ let rb = dataset.rasterband(1).unwrap();
+ assert_eq!(rb.no_data_value(), Some(255.0));
+ })
+ .unwrap();
+ }
+
+ #[test]
+ #[allow(clippy::float_cmp)]
+ fn test_set_no_data_value() {
+ with_global_gdal_api(|api| {
+ let driver = DriverManager::get_driver_by_name(api,
"MEM").unwrap();
+ let dataset = driver.create("", 20, 10, 1).unwrap();
+ let rasterband = dataset.rasterband(1).unwrap();
+ assert_eq!(rasterband.no_data_value(), None);
+ assert!(rasterband.set_no_data_value(Some(1.23)).is_ok());
+ assert_eq!(rasterband.no_data_value(), Some(1.23));
+ assert!(rasterband.set_no_data_value(None).is_ok());
+ assert_eq!(rasterband.no_data_value(), None);
+ })
+ .unwrap();
+ }
+}
diff --git a/c/sedona-gdal/src/vector.rs b/c/sedona-gdal/src/vector.rs
index 10c038ed..52ff4e1b 100644
--- a/c/sedona-gdal/src/vector.rs
+++ b/c/sedona-gdal/src/vector.rs
@@ -15,4 +15,6 @@
// specific language governing permissions and limitations
// under the License.
+pub mod feature;
pub mod geometry;
+pub mod layer;
diff --git a/c/sedona-gdal/src/vector/feature.rs
b/c/sedona-gdal/src/vector/feature.rs
new file mode 100644
index 00000000..a80648b1
--- /dev/null
+++ b/c/sedona-gdal/src/vector/feature.rs
@@ -0,0 +1,213 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/vector/feature.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::CString;
+use std::marker::PhantomData;
+
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::vector::geometry::Envelope;
+
+/// An OGR feature.
+pub struct Feature<'a> {
+ api: &'static GdalApi,
+ c_feature: OGRFeatureH,
+ _lifetime: PhantomData<&'a ()>,
+}
+
+impl Drop for Feature<'_> {
+ fn drop(&mut self) {
+ if !self.c_feature.is_null() {
+ unsafe { call_gdal_api!(self.api, OGR_F_Destroy, self.c_feature) };
+ }
+ }
+}
+
+impl<'a> Feature<'a> {
+ pub(crate) fn new(api: &'static GdalApi, c_feature: OGRFeatureH) -> Self {
+ Self {
+ api,
+ c_feature,
+ _lifetime: PhantomData,
+ }
+ }
+
+ /// Fetch the feature geometry.
+ /// The returned geometry is borrowed; return `None` if no geometry is set.
+ pub fn geometry(&self) -> Option<BorrowedGeometry<'_>> {
+ let c_geom = unsafe { call_gdal_api!(self.api, OGR_F_GetGeometryRef,
self.c_feature) };
+ if c_geom.is_null() {
+ None
+ } else {
+ Some(BorrowedGeometry {
+ api: self.api,
+ c_geom,
+ _lifetime: PhantomData,
+ })
+ }
+ }
+
+ /// Fetch the index of a field by name.
+ /// Return an error if the field is not found.
+ pub fn field_index(&self, name: &str) -> Result<i32> {
+ let c_name = CString::new(name)?;
+ let idx = unsafe {
+ call_gdal_api!(
+ self.api,
+ OGR_F_GetFieldIndex,
+ self.c_feature,
+ c_name.as_ptr()
+ )
+ };
+ if idx < 0 {
+ return Err(GdalError::BadArgument(format!("field '{name}' not
found")));
+ }
+ Ok(idx)
+ }
+
+ /// Fetch 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
+ )
+ }
+ }
+
+ /// Fetch a field value as `i32`.
+ /// Return `None` if the field is unset or null.
+ pub fn field_as_integer(&self, field_index: i32) -> Option<i32> {
+ let is_set = unsafe {
+ call_gdal_api!(
+ self.api,
+ OGR_F_IsFieldSetAndNotNull,
+ self.c_feature,
+ field_index
+ )
+ };
+ if is_set != 0 {
+ Some(unsafe {
+ call_gdal_api!(
+ self.api,
+ OGR_F_GetFieldAsInteger,
+ self.c_feature,
+ field_index
+ )
+ })
+ } else {
+ None
+ }
+ }
+}
+
+/// A geometry borrowed from a feature (not owned — will NOT be destroyed).
+pub struct BorrowedGeometry<'a> {
+ api: &'static GdalApi,
+ c_geom: OGRGeometryH,
+ _lifetime: PhantomData<&'a ()>,
+}
+
+impl<'a> BorrowedGeometry<'a> {
+ /// Return the raw C geometry handle.
+ pub fn c_geometry(&self) -> OGRGeometryH {
+ self.c_geom
+ }
+
+ /// Export to ISO WKB.
+ pub fn wkb(&self) -> Result<Vec<u8>> {
+ let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize,
self.c_geom) };
+ if size < 0 {
+ return Err(GdalError::BadArgument(format!(
+ "OGR_G_WkbSize returned negative size: {size}"
+ )));
+ }
+ let mut buf = vec![0u8; size as usize];
+ let rv = unsafe {
+ call_gdal_api!(
+ self.api,
+ OGR_G_ExportToIsoWkb,
+ self.c_geom,
+ wkbNDR,
+ buf.as_mut_ptr()
+ )
+ };
+ if rv != OGRERR_NONE {
+ return Err(GdalError::OgrError {
+ err: rv,
+ method_name: "OGR_G_ExportToIsoWkb",
+ });
+ }
+ Ok(buf)
+ }
+
+ /// Fetch the 2D envelope of this geometry.
+ pub fn envelope(&self) -> Envelope {
+ let mut env = OGREnvelope {
+ MinX: 0.0,
+ MaxX: 0.0,
+ MinY: 0.0,
+ MaxY: 0.0,
+ };
+ unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut
env) };
+ Envelope {
+ MinX: env.MinX,
+ MaxX: env.MaxX,
+ MinY: env.MinY,
+ MaxY: env.MaxY,
+ }
+ }
+}
+
+/// An OGR field definition.
+pub struct FieldDefn {
+ api: &'static GdalApi,
+ c_field_defn: OGRFieldDefnH,
+}
+
+impl Drop for FieldDefn {
+ fn drop(&mut self) {
+ if !self.c_field_defn.is_null() {
+ unsafe { call_gdal_api!(self.api, OGR_Fld_Destroy,
self.c_field_defn) };
+ }
+ }
+}
+
+impl FieldDefn {
+ /// Create a new field definition.
+ pub fn new(api: &'static GdalApi, name: &str, field_type: OGRFieldType) ->
Result<Self> {
+ 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(api.last_null_pointer_err("OGR_Fld_Create"));
+ }
+ Ok(Self { api, c_field_defn })
+ }
+
+ /// Return the raw C handle.
+ pub fn c_field_defn(&self) -> OGRFieldDefnH {
+ self.c_field_defn
+ }
+}
diff --git a/c/sedona-gdal/src/vector/layer.rs
b/c/sedona-gdal/src/vector/layer.rs
new file mode 100644
index 00000000..8111f231
--- /dev/null
+++ b/c/sedona-gdal/src/vector/layer.rs
@@ -0,0 +1,208 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/vector/layer.rs>.
+//! Original code is licensed under MIT.
+
+use std::marker::PhantomData;
+
+use crate::dataset::Dataset;
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::vector::feature::{Feature, FieldDefn};
+
+/// An OGR layer (borrowed from a Dataset).
+pub struct Layer<'a> {
+ api: &'static GdalApi,
+ c_layer: OGRLayerH,
+ _dataset: PhantomData<&'a Dataset>,
+}
+
+impl<'a> Layer<'a> {
+ pub(crate) fn new(api: &'static GdalApi, c_layer: OGRLayerH, _dataset: &'a
Dataset) -> Self {
+ Self {
+ api,
+ c_layer,
+ _dataset: PhantomData,
+ }
+ }
+
+ /// Return the raw C layer handle.
+ pub fn c_layer(&self) -> OGRLayerH {
+ self.c_layer
+ }
+
+ /// Reset reading to the first feature.
+ pub fn reset_reading(&self) {
+ unsafe { call_gdal_api!(self.api, OGR_L_ResetReading, self.c_layer) };
+ }
+
+ /// Fetch the next feature from the current read cursor.
+ /// Return `None` when no more features are available.
+ pub fn next_feature(&self) -> Option<Feature<'_>> {
+ 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 in this layer.
+ /// Allow the driver to approximate the definition if needed.
+ 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(())
+ }
+
+ /// Fetch the feature count for this layer.
+ /// Return `-1` if the count is unknown and `force` is `false`.
+ 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 features from the start of the layer.
+ /// This resets the layer read cursor before iteration.
+ pub fn features(&mut 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::Item> {
+ self.layer.next_feature()
+ }
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+ use gdal_sys::{
+ GDALDatasetGetLayer, GDALDatasetGetLayerCount, OGR_F_Create,
OGR_F_SetGeometry,
+ OGR_L_CreateFeature, OGR_L_GetLayerDefn,
+ };
+
+ use super::Layer;
+ use crate::dataset::{Dataset, LayerOptions};
+ use crate::driver::DriverManager;
+ use crate::gdal_dyn_bindgen::OGRwkbGeometryType;
+ use crate::global::with_global_gdal_api;
+ use crate::vector::geometry::Geometry;
+ use crate::vsi::unlink_mem_file;
+
+ #[test]
+ fn test_layer_iteration_and_reset() {
+ with_global_gdal_api(|api| {
+ let path = "/vsimem/test_layer_iteration.gpkg";
+ let driver = DriverManager::get_driver_by_name(api,
"GPKG").unwrap();
+ let dataset = driver.create_vector_only(path).unwrap();
+
+ let layer = dataset
+ .create_layer(LayerOptions {
+ name: "features",
+ srs: None,
+ ty: OGRwkbGeometryType::wkbPoint,
+ options: None,
+ })
+ .unwrap();
+
+ let layer_defn = unsafe { OGR_L_GetLayerDefn(layer.c_layer()) };
+ assert!(!layer_defn.is_null());
+
+ for x in [1.0_f64, 2.0, 3.0] {
+ let feature = unsafe { OGR_F_Create(layer_defn) };
+ assert!(!feature.is_null());
+
+ let geometry = Geometry::from_wkt(api, &format!("POINT ({x}
0)")).unwrap();
+ let set_geometry_err = unsafe { OGR_F_SetGeometry(feature,
geometry.c_geometry()) };
+ assert_eq!(set_geometry_err, gdal_sys::OGRErr::OGRERR_NONE);
+
+ let create_feature_err = unsafe {
OGR_L_CreateFeature(layer.c_layer(), feature) };
+ assert_eq!(create_feature_err, gdal_sys::OGRErr::OGRERR_NONE);
+
+ unsafe { gdal_sys::OGR_F_Destroy(feature) };
+ }
+
+ let write_count = unsafe {
GDALDatasetGetLayerCount(dataset.c_dataset()) };
+ assert_eq!(write_count, 1);
+
+ let read_dataset = Dataset::open_ex(
+ api,
+ path,
+ crate::gdal_dyn_bindgen::GDAL_OF_VECTOR |
crate::gdal_dyn_bindgen::GDAL_OF_READONLY,
+ None,
+ None,
+ None,
+ )
+ .unwrap();
+
+ let read_count = unsafe {
GDALDatasetGetLayerCount(read_dataset.c_dataset()) };
+ assert_eq!(read_count, 1);
+
+ let c_layer = unsafe {
GDALDatasetGetLayer(read_dataset.c_dataset(), 0) };
+ assert!(!c_layer.is_null());
+ let mut read_layer = Layer::new(api, c_layer, &read_dataset);
+
+ assert_eq!(read_layer.feature_count(true), 3);
+
+ let mut iter = read_layer.features();
+ assert!(iter.next().is_some());
+ assert!(iter.next().is_some());
+ assert!(iter.next().is_some());
+ assert!(iter.next().is_none());
+
+ read_layer.reset_reading();
+ assert!(read_layer.next_feature().is_some());
+
+ assert_eq!(read_layer.features().count(), 3);
+ assert_eq!(read_layer.features().count(), 3);
+
+ unlink_mem_file(api, path).unwrap();
+ })
+ .unwrap();
+ }
+}
diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs
index 80022c7c..20a60e11 100644
--- a/c/sedona-gdal/src/vsi.rs
+++ b/c/sedona-gdal/src/vsi.rs
@@ -276,7 +276,7 @@ mod tests {
let buffer = get_vsi_mem_file_buffer_owned(api,
file_name).unwrap();
assert!(buffer.is_empty());
- assert_eq!(buffer.as_ref(), &[]);
+ assert_eq!(buffer.as_ref(), b"");
})
.unwrap();
}