Skip to main content

oxigdal/
lib.rs

1//! # OxiGDAL — Pure Rust Geospatial Data Abstraction Library
2//!
3//! OxiGDAL is the Rust-native alternative to [GDAL](https://gdal.org/),
4//! providing a comprehensive geospatial data abstraction layer
5//! with **zero C/Fortran dependencies**. 100% Pure Rust.
6//!
7//! ## Quick Start
8//!
9//! ```toml
10//! [dependencies]
11//! oxigdal = "0.1"  # includes GeoTIFF, GeoJSON, Shapefile by default
12//! ```
13//!
14//! ```rust
15//! use oxigdal::Dataset;
16//!
17//! # fn main() -> oxigdal::Result<()> {
18//! let drivers = oxigdal::drivers();
19//! println!("Enabled drivers: {:?}", drivers);
20//! println!("OxiGDAL version: {}", oxigdal::version());
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! ## Feature Flags
26//!
27//! | Feature | Default | Description |
28//! |---------|---------|-------------|
29//! | `geotiff` | ✅ | GeoTIFF raster format (COG support) |
30//! | `geojson` | ✅ | GeoJSON vector format |
31//! | `shapefile` | ✅ | ESRI Shapefile |
32//! | `geoparquet` | ❌ | GeoParquet (Apache Arrow columnar) |
33//! | `netcdf` | ❌ | NetCDF scientific data format |
34//! | `hdf5` | ❌ | HDF5 hierarchical data format |
35//! | `zarr` | ❌ | Zarr cloud-native arrays |
36//! | `grib` | ❌ | GRIB meteorological data format |
37//! | `stac` | ❌ | SpatioTemporal Asset Catalog |
38//! | `terrain` | ❌ | Terrain/elevation data |
39//! | `vrt` | ❌ | Virtual Raster Tiles |
40//! | `flatgeobuf` | ❌ | FlatGeobuf vector format |
41//! | `jpeg2000` | ❌ | JPEG2000 raster format |
42//! | `full` | ❌ | **All formats above** |
43//! | `cloud` | ❌ | Cloud storage (S3, GCS, Azure) |
44//! | `proj` | ❌ | CRS transformations (Pure Rust proj) |
45//! | `algorithms` | ❌ | Raster/vector algorithms |
46//! | `analytics` | ❌ | Geospatial analytics |
47//! | `streaming` | ❌ | Stream processing |
48//! | `ml` | ❌ | Machine learning integration |
49//! | `gpu` | ❌ | GPU-accelerated processing |
50//! | `server` | ❌ | OGC-compliant tile server |
51//! | `temporal` | ❌ | Temporal/time-series analysis |
52//!
53//! ## GDAL Compatibility
54//!
55//! OxiGDAL aims to provide familiar concepts for GDAL users:
56//!
57//! | GDAL (C/C++) | OxiGDAL (Rust) |
58//! |---|---|
59//! | `GDALOpen()` | [`Dataset::open()`] |
60//! | `GDALGetRasterBand()` | `dataset.raster_band(n)` |
61//! | `GDALGetGeoTransform()` | [`Dataset::geotransform()`] |
62//! | `GDALGetProjectionRef()` | [`Dataset::crs()`] |
63//! | `GDALAllRegister()` | [`drivers()`] |
64//! | `GDALVersionInfo()` | [`version()`] |
65//! | `GDALWarp()` | `oxigdal::algorithms::warp()` (feature `algorithms`) |
66//! | `ogr2ogr` | `oxigdal-cli convert` (crate `oxigdal-cli`) |
67//!
68//! ## Architecture
69//!
70//! ```text
71//! ┌──────────────────────────────────────────────────┐
72//! │  oxigdal (this crate) — Unified API              │
73//! │  Dataset::open() → auto-detect format            │
74//! ├──────────────────────────────────────────────────┤
75//! │  Drivers (feature-gated)                         │
76//! │  ┌──────────┐ ┌──────────┐ ┌─────────────┐      │
77//! │  │ GeoTIFF  │ │ GeoJSON  │ │  Shapefile  │ ...  │
78//! │  └──────────┘ └──────────┘ └─────────────┘      │
79//! ├──────────────────────────────────────────────────┤
80//! │  oxigdal-core — Types, Buffers, Error, I/O       │
81//! └──────────────────────────────────────────────────┘
82//! ```
83//!
84//! ## Crate Ecosystem
85//!
86//! OxiGDAL is a workspace of 65+ crates. This `oxigdal` crate serves as
87//! the **unified entry point**. Individual crates can also be used directly:
88//!
89//! ```toml
90//! # Use the unified API (recommended for most users)
91//! oxigdal = { version = "0.1", features = ["full", "cloud", "proj"] }
92//!
93//! # Or pick individual crates for minimal dependencies
94//! oxigdal-core = "0.1"
95//! oxigdal-geotiff = "0.1"
96//! ```
97//!
98//! ## Pure Rust — No C/Fortran Dependencies
99//!
100//! Unlike the original GDAL which requires C/C++ compilation and system
101//! libraries (PROJ, GEOS, etc.), OxiGDAL is **100% Pure Rust**:
102//!
103//! - No `bindgen`, no `cc`, no `cmake`
104//! - Cross-compiles to WASM, embedded, mobile
105//! - `cargo add oxigdal` — that's it
106//!
107//! Part of the [COOLJAPAN](https://github.com/cool-japan) ecosystem.
108
109#![cfg_attr(docsrs, feature(doc_cfg))]
110#![forbid(unsafe_code)]
111#![warn(missing_docs)]
112
113// Re-export core types — always available
114pub use oxigdal_core::error::OxiGdalError;
115pub use oxigdal_core::error::Result;
116pub use oxigdal_core::types::{BoundingBox, GeoTransform, RasterDataType, RasterMetadata};
117
118/// Re-export the core crate for advanced usage
119pub use oxigdal_core as core_types;
120
121// ─── Driver re-exports (feature-gated) ──────────────────────────────────────
122
123/// GeoTIFF raster driver (Cloud-Optimized GeoTIFF support)
124#[cfg(feature = "geotiff")]
125#[cfg_attr(docsrs, doc(cfg(feature = "geotiff")))]
126pub use oxigdal_geotiff as geotiff;
127
128/// GeoJSON vector driver
129#[cfg(feature = "geojson")]
130#[cfg_attr(docsrs, doc(cfg(feature = "geojson")))]
131pub use oxigdal_geojson as geojson;
132
133/// ESRI Shapefile driver
134#[cfg(feature = "shapefile")]
135#[cfg_attr(docsrs, doc(cfg(feature = "shapefile")))]
136pub use oxigdal_shapefile as shapefile;
137
138/// GeoParquet columnar format driver
139#[cfg(feature = "geoparquet")]
140#[cfg_attr(docsrs, doc(cfg(feature = "geoparquet")))]
141pub use oxigdal_geoparquet as geoparquet;
142
143/// NetCDF scientific format driver
144#[cfg(feature = "netcdf")]
145#[cfg_attr(docsrs, doc(cfg(feature = "netcdf")))]
146pub use oxigdal_netcdf as netcdf;
147
148/// HDF5 hierarchical data driver
149#[cfg(feature = "hdf5")]
150#[cfg_attr(docsrs, doc(cfg(feature = "hdf5")))]
151pub use oxigdal_hdf5 as hdf5;
152
153/// Zarr cloud-native array driver
154#[cfg(feature = "zarr")]
155#[cfg_attr(docsrs, doc(cfg(feature = "zarr")))]
156pub use oxigdal_zarr as zarr;
157
158/// GRIB meteorological data driver
159#[cfg(feature = "grib")]
160#[cfg_attr(docsrs, doc(cfg(feature = "grib")))]
161pub use oxigdal_grib as grib;
162
163/// SpatioTemporal Asset Catalog driver
164#[cfg(feature = "stac")]
165#[cfg_attr(docsrs, doc(cfg(feature = "stac")))]
166pub use oxigdal_stac as stac;
167
168/// Terrain/elevation data driver
169#[cfg(feature = "terrain")]
170#[cfg_attr(docsrs, doc(cfg(feature = "terrain")))]
171pub use oxigdal_terrain as terrain;
172
173/// Virtual Raster Tiles driver
174#[cfg(feature = "vrt")]
175#[cfg_attr(docsrs, doc(cfg(feature = "vrt")))]
176pub use oxigdal_vrt as vrt;
177
178/// FlatGeobuf vector format driver
179#[cfg(feature = "flatgeobuf")]
180#[cfg_attr(docsrs, doc(cfg(feature = "flatgeobuf")))]
181pub use oxigdal_flatgeobuf as flatgeobuf;
182
183/// JPEG2000 raster format driver
184#[cfg(feature = "jpeg2000")]
185#[cfg_attr(docsrs, doc(cfg(feature = "jpeg2000")))]
186pub use oxigdal_jpeg2000 as jpeg2000;
187
188// ─── Advanced capability re-exports (feature-gated) ─────────────────────────
189
190/// Cloud storage backends (S3, GCS, Azure Blob)
191#[cfg(feature = "cloud")]
192#[cfg_attr(docsrs, doc(cfg(feature = "cloud")))]
193pub use oxigdal_cloud as cloud;
194
195/// Coordinate reference system transformations (Pure Rust proj)
196#[cfg(feature = "proj")]
197#[cfg_attr(docsrs, doc(cfg(feature = "proj")))]
198pub use oxigdal_proj as proj;
199
200/// Raster and vector algorithms (resampling, reprojection, etc.)
201#[cfg(feature = "algorithms")]
202#[cfg_attr(docsrs, doc(cfg(feature = "algorithms")))]
203pub use oxigdal_algorithms as algorithms;
204
205/// Geospatial analytics and statistics
206#[cfg(feature = "analytics")]
207#[cfg_attr(docsrs, doc(cfg(feature = "analytics")))]
208pub use oxigdal_analytics as analytics;
209
210/// Stream processing for large datasets
211#[cfg(feature = "streaming")]
212#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
213pub use oxigdal_streaming as streaming;
214
215/// Machine learning integration
216#[cfg(feature = "ml")]
217#[cfg_attr(docsrs, doc(cfg(feature = "ml")))]
218pub use oxigdal_ml as ml;
219
220/// GPU-accelerated geospatial processing
221#[cfg(feature = "gpu")]
222#[cfg_attr(docsrs, doc(cfg(feature = "gpu")))]
223pub use oxigdal_gpu as gpu;
224
225/// OGC-compliant geospatial tile/feature server
226#[cfg(feature = "server")]
227#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
228pub use oxigdal_server as server;
229
230/// Temporal/time-series geospatial analysis
231#[cfg(feature = "temporal")]
232#[cfg_attr(docsrs, doc(cfg(feature = "temporal")))]
233pub use oxigdal_temporal as temporal;
234
235// ─── Unified Dataset API ────────────────────────────────────────────────────
236
237/// Detected format of a geospatial dataset.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum DatasetFormat {
240    /// GeoTIFF / Cloud-Optimized GeoTIFF (.tif, .tiff)
241    GeoTiff,
242    /// GeoJSON (.geojson, .json)
243    GeoJson,
244    /// ESRI Shapefile (.shp)
245    Shapefile,
246    /// GeoParquet (.parquet, .geoparquet)
247    GeoParquet,
248    /// NetCDF (.nc, .nc4)
249    NetCdf,
250    /// HDF5 (.h5, .hdf5, .he5)
251    Hdf5,
252    /// Zarr (.zarr directory)
253    Zarr,
254    /// GRIB/GRIB2 (.grib, .grib2, .grb, .grb2)
255    Grib,
256    /// STAC catalog (.json with STAC metadata)
257    Stac,
258    /// Terrain formats
259    Terrain,
260    /// Virtual Raster Tiles (.vrt)
261    Vrt,
262    /// FlatGeobuf (.fgb)
263    FlatGeobuf,
264    /// JPEG2000 (.jp2, .j2k)
265    Jpeg2000,
266    /// Unknown / user-specified
267    Unknown,
268}
269
270impl DatasetFormat {
271    /// Detect format from file extension.
272    ///
273    /// Returns `DatasetFormat::Unknown` if the extension is not recognized.
274    pub fn from_extension(path: &str) -> Self {
275        let ext = std::path::Path::new(path)
276            .extension()
277            .and_then(|e| e.to_str())
278            .map(|e| e.to_lowercase())
279            .unwrap_or_default();
280
281        match ext.as_str() {
282            "tif" | "tiff" => Self::GeoTiff,
283            "geojson" => Self::GeoJson,
284            "shp" => Self::Shapefile,
285            "parquet" | "geoparquet" => Self::GeoParquet,
286            "nc" | "nc4" => Self::NetCdf,
287            "h5" | "hdf5" | "he5" => Self::Hdf5,
288            "zarr" => Self::Zarr,
289            "grib" | "grib2" | "grb" | "grb2" => Self::Grib,
290            "vrt" => Self::Vrt,
291            "fgb" => Self::FlatGeobuf,
292            "jp2" | "j2k" => Self::Jpeg2000,
293            _ => Self::Unknown,
294        }
295    }
296
297    /// Human-readable driver name (matches GDAL naming convention).
298    pub fn driver_name(&self) -> &'static str {
299        match self {
300            Self::GeoTiff => "GTiff",
301            Self::GeoJson => "GeoJSON",
302            Self::Shapefile => "ESRI Shapefile",
303            Self::GeoParquet => "GeoParquet",
304            Self::NetCdf => "netCDF",
305            Self::Hdf5 => "HDF5",
306            Self::Zarr => "Zarr",
307            Self::Grib => "GRIB",
308            Self::Stac => "STAC",
309            Self::Terrain => "Terrain",
310            Self::Vrt => "VRT",
311            Self::FlatGeobuf => "FlatGeobuf",
312            Self::Jpeg2000 => "JPEG2000",
313            Self::Unknown => "Unknown",
314        }
315    }
316}
317
318impl core::fmt::Display for DatasetFormat {
319    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
320        f.write_str(self.driver_name())
321    }
322}
323
324/// Basic dataset metadata — analogous to `GDALDataset` info.
325#[derive(Debug, Clone)]
326pub struct DatasetInfo {
327    /// Detected format
328    pub format: DatasetFormat,
329    /// Width in pixels (raster) or `None` (vector-only)
330    pub width: Option<u32>,
331    /// Height in pixels (raster) or `None` (vector-only)
332    pub height: Option<u32>,
333    /// Number of raster bands
334    pub band_count: u32,
335    /// Number of vector layers
336    pub layer_count: u32,
337    /// Coordinate reference system (WKT, EPSG code, or PROJ string)
338    pub crs: Option<String>,
339    /// Geotransform: `[origin_x, pixel_width, rotation_x, origin_y, rotation_y, pixel_height]`
340    pub geotransform: Option<GeoTransform>,
341}
342
343/// Unified dataset handle — the central abstraction (analogous to `GDALDataset`).
344///
345/// Opens any supported geospatial format and provides uniform access
346/// to raster bands, vector layers, and metadata.
347///
348/// # Example
349///
350/// ```rust,no_run
351/// use oxigdal::Dataset;
352///
353/// let ds = Dataset::open("elevation.tif").expect("failed to open");
354/// println!("{}×{} pixels, {} bands", ds.width(), ds.height(), ds.band_count());
355/// println!("Format: {}", ds.format());
356/// if let Some(crs) = ds.crs() {
357///     println!("CRS: {crs}");
358/// }
359/// ```
360pub struct Dataset {
361    path: String,
362    info: DatasetInfo,
363}
364
365impl Dataset {
366    /// Open a geospatial dataset from a file path — the universal entry point.
367    ///
368    /// Format is auto-detected from file extension (and in the future, magic bytes),
369    /// just like `GDALOpen()` in C GDAL.
370    ///
371    /// # Supported Formats
372    ///
373    /// Which formats are available depends on enabled feature flags.
374    /// With default features: GeoTIFF, GeoJSON, Shapefile.
375    ///
376    /// # Errors
377    ///
378    /// Returns [`OxiGdalError::NotSupported`] if the format is not recognized
379    /// or the corresponding feature flag is not enabled.
380    ///
381    /// Returns [`OxiGdalError::Io`] if the file cannot be read.
382    pub fn open(path: &str) -> Result<Self> {
383        let format = DatasetFormat::from_extension(path);
384        Self::open_with_format(path, format)
385    }
386
387    /// Open a dataset with an explicitly specified format.
388    ///
389    /// Use this when auto-detection from extension is insufficient
390    /// (e.g., `.json` files that could be GeoJSON or STAC).
391    ///
392    /// # Errors
393    ///
394    /// Returns error if the format's feature flag is not enabled or file is unreadable.
395    pub fn open_with_format(path: &str, format: DatasetFormat) -> Result<Self> {
396        match format {
397            #[cfg(feature = "geotiff")]
398            DatasetFormat::GeoTiff => Self::open_raster_stub(path, DatasetFormat::GeoTiff),
399
400            #[cfg(feature = "geojson")]
401            DatasetFormat::GeoJson => Self::open_vector_stub(path, DatasetFormat::GeoJson),
402
403            #[cfg(feature = "shapefile")]
404            DatasetFormat::Shapefile => Self::open_vector_stub(path, DatasetFormat::Shapefile),
405
406            #[cfg(feature = "geoparquet")]
407            DatasetFormat::GeoParquet => Self::open_vector_stub(path, DatasetFormat::GeoParquet),
408
409            #[cfg(feature = "netcdf")]
410            DatasetFormat::NetCdf => Self::open_raster_stub(path, DatasetFormat::NetCdf),
411
412            #[cfg(feature = "hdf5")]
413            DatasetFormat::Hdf5 => Self::open_raster_stub(path, DatasetFormat::Hdf5),
414
415            #[cfg(feature = "zarr")]
416            DatasetFormat::Zarr => Self::open_raster_stub(path, DatasetFormat::Zarr),
417
418            #[cfg(feature = "grib")]
419            DatasetFormat::Grib => Self::open_raster_stub(path, DatasetFormat::Grib),
420
421            #[cfg(feature = "flatgeobuf")]
422            DatasetFormat::FlatGeobuf => Self::open_vector_stub(path, DatasetFormat::FlatGeobuf),
423
424            #[cfg(feature = "jpeg2000")]
425            DatasetFormat::Jpeg2000 => Self::open_raster_stub(path, DatasetFormat::Jpeg2000),
426
427            #[cfg(feature = "vrt")]
428            DatasetFormat::Vrt => Self::open_raster_stub(path, DatasetFormat::Vrt),
429
430            _ => Err(OxiGdalError::NotSupported {
431                operation: format!(
432                    "Format '{}' for '{}' — enable the corresponding feature flag or check the file extension",
433                    format.driver_name(),
434                    path,
435                ),
436            }),
437        }
438    }
439
440    // -- Stub openers (delegate to driver crates in the future) ---------------
441
442    fn open_raster_stub(path: &str, format: DatasetFormat) -> Result<Self> {
443        // Verify file exists
444        if !std::path::Path::new(path).exists() {
445            return Err(OxiGdalError::Io(oxigdal_core::error::IoError::NotFound {
446                path: path.to_string(),
447            }));
448        }
449
450        Ok(Self {
451            path: path.to_string(),
452            info: DatasetInfo {
453                format,
454                width: None,
455                height: None,
456                band_count: 0,
457                layer_count: 0,
458                crs: None,
459                geotransform: None,
460            },
461        })
462    }
463
464    fn open_vector_stub(path: &str, format: DatasetFormat) -> Result<Self> {
465        if !std::path::Path::new(path).exists() {
466            return Err(OxiGdalError::Io(oxigdal_core::error::IoError::NotFound {
467                path: path.to_string(),
468            }));
469        }
470
471        Ok(Self {
472            path: path.to_string(),
473            info: DatasetInfo {
474                format,
475                width: None,
476                height: None,
477                band_count: 0,
478                layer_count: 0,
479                crs: None,
480                geotransform: None,
481            },
482        })
483    }
484
485    // -- Accessors (GDAL-like API) ------------------------------------------
486
487    /// File path this dataset was opened from.
488    pub fn path(&self) -> &str {
489        &self.path
490    }
491
492    /// Detected dataset format.
493    pub fn format(&self) -> DatasetFormat {
494        self.info.format
495    }
496
497    /// Full dataset info.
498    pub fn info(&self) -> &DatasetInfo {
499        &self.info
500    }
501
502    /// Width in pixels (raster datasets). Returns 0 for vector-only datasets.
503    pub fn width(&self) -> u32 {
504        self.info.width.unwrap_or(0)
505    }
506
507    /// Height in pixels (raster datasets). Returns 0 for vector-only datasets.
508    pub fn height(&self) -> u32 {
509        self.info.height.unwrap_or(0)
510    }
511
512    /// Coordinate reference system (WKT, EPSG code, or PROJ string).
513    pub fn crs(&self) -> Option<&str> {
514        self.info.crs.as_deref()
515    }
516
517    /// Number of raster bands.
518    pub fn band_count(&self) -> u32 {
519        self.info.band_count
520    }
521
522    /// Number of vector layers.
523    pub fn layer_count(&self) -> u32 {
524        self.info.layer_count
525    }
526
527    /// Geotransform coefficients.
528    ///
529    /// `[origin_x, pixel_width, rotation_x, origin_y, rotation_y, pixel_height]`
530    pub fn geotransform(&self) -> Option<&GeoTransform> {
531        self.info.geotransform.as_ref()
532    }
533}
534
535impl core::fmt::Debug for Dataset {
536    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
537        f.debug_struct("Dataset")
538            .field("path", &self.path)
539            .field("format", &self.info.format)
540            .field("width", &self.info.width)
541            .field("height", &self.info.height)
542            .field("band_count", &self.info.band_count)
543            .field("layer_count", &self.info.layer_count)
544            .finish()
545    }
546}
547
548// ─── Top-level functions ────────────────────────────────────────────────────
549
550/// OxiGDAL version string.
551///
552/// Equivalent to `GDALVersionInfo("RELEASE_NAME")` in C GDAL.
553pub fn version() -> &'static str {
554    env!("CARGO_PKG_VERSION")
555}
556
557/// List all enabled format drivers.
558///
559/// Equivalent to `GDALAllRegister()` + iterating registered drivers in C GDAL.
560///
561/// Returns a list of human-readable driver names for all features
562/// currently compiled in.
563///
564/// # Example
565///
566/// ```rust
567/// let drivers = oxigdal::drivers();
568/// assert!(drivers.contains(&"GTiff"));     // default feature
569/// assert!(drivers.contains(&"GeoJSON"));   // default feature
570/// assert!(drivers.contains(&"ESRI Shapefile")); // default feature
571/// ```
572pub fn drivers() -> Vec<&'static str> {
573    let mut list = Vec::new();
574
575    #[cfg(feature = "geotiff")]
576    list.push("GTiff");
577    #[cfg(feature = "geojson")]
578    list.push("GeoJSON");
579    #[cfg(feature = "shapefile")]
580    list.push("ESRI Shapefile");
581    #[cfg(feature = "geoparquet")]
582    list.push("GeoParquet");
583    #[cfg(feature = "netcdf")]
584    list.push("netCDF");
585    #[cfg(feature = "hdf5")]
586    list.push("HDF5");
587    #[cfg(feature = "zarr")]
588    list.push("Zarr");
589    #[cfg(feature = "grib")]
590    list.push("GRIB");
591    #[cfg(feature = "stac")]
592    list.push("STAC");
593    #[cfg(feature = "terrain")]
594    list.push("Terrain");
595    #[cfg(feature = "vrt")]
596    list.push("VRT");
597    #[cfg(feature = "flatgeobuf")]
598    list.push("FlatGeobuf");
599    #[cfg(feature = "jpeg2000")]
600    list.push("JPEG2000");
601
602    list
603}
604
605/// Number of registered (enabled) format drivers.
606///
607/// Equivalent to `GDALGetDriverCount()` in C GDAL.
608pub fn driver_count() -> usize {
609    drivers().len()
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn test_version() {
618        let v = version();
619        assert!(!v.is_empty());
620        assert!(v.starts_with("0."));
621    }
622
623    #[test]
624    fn test_default_drivers() {
625        let d = drivers();
626        // Default features: geotiff, geojson, shapefile
627        assert!(d.contains(&"GTiff"), "GeoTIFF should be a default driver");
628        assert!(d.contains(&"GeoJSON"), "GeoJSON should be a default driver");
629        assert!(
630            d.contains(&"ESRI Shapefile"),
631            "Shapefile should be a default driver"
632        );
633    }
634
635    #[test]
636    fn test_driver_count() {
637        assert!(driver_count() >= 3, "At least 3 default drivers");
638    }
639
640    #[test]
641    fn test_format_detection() {
642        assert_eq!(DatasetFormat::from_extension("world.tif"), DatasetFormat::GeoTiff);
643        assert_eq!(DatasetFormat::from_extension("data.geojson"), DatasetFormat::GeoJson);
644        assert_eq!(DatasetFormat::from_extension("map.shp"), DatasetFormat::Shapefile);
645        assert_eq!(DatasetFormat::from_extension("cloud.zarr"), DatasetFormat::Zarr);
646        assert_eq!(DatasetFormat::from_extension("output.parquet"), DatasetFormat::GeoParquet);
647        assert_eq!(DatasetFormat::from_extension("scene.vrt"), DatasetFormat::Vrt);
648        assert_eq!(DatasetFormat::from_extension("README.md"), DatasetFormat::Unknown);
649    }
650
651    #[test]
652    fn test_format_display() {
653        assert_eq!(DatasetFormat::GeoTiff.to_string(), "GTiff");
654        assert_eq!(DatasetFormat::GeoJson.to_string(), "GeoJSON");
655    }
656
657    #[test]
658    fn test_open_nonexistent() {
659        let result = Dataset::open("/nonexistent/file.tif");
660        assert!(result.is_err());
661    }
662
663    #[test]
664    fn test_open_unsupported_extension() {
665        let result = Dataset::open("data.xyz");
666        assert!(result.is_err());
667    }
668
669    #[test]
670    fn test_open_with_format() {
671        // Opening with explicit format for a nonexistent file should give IoError
672        let result = Dataset::open_with_format("/no/such/file.tif", DatasetFormat::GeoTiff);
673        assert!(result.is_err());
674    }
675}