#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub use oxigdal_core::error::OxiGdalError;
pub use oxigdal_core::error::Result;
pub use oxigdal_core::types::{BoundingBox, GeoTransform, RasterDataType, RasterMetadata};
pub mod open;
pub mod builder;
pub mod streaming;
pub mod convert;
pub(crate) mod magic;
pub mod cloud_detect;
pub use cloud_detect::is_cloud_uri;
pub mod vrt_builder;
mod format;
pub use format::DatasetFormat;
mod dataset_ops;
mod convert_ops;
#[cfg(feature = "gdal-compat")]
#[cfg_attr(docsrs, doc(cfg(feature = "gdal-compat")))]
#[doc(hidden)]
pub mod gdal_compat;
pub use builder::{
CompressionType, CreateOptions, DatasetCreateBuilder, DatasetOpenBuilder, DatasetWriter,
OutputFormat,
};
pub use open::{CloudScheme, OpenedDataset, open};
pub use streaming::{FeatureStream, RasterTile, StreamingExt, StreamingFeature, TileStream};
pub use oxigdal_core as core_types;
#[cfg(feature = "geotiff")]
#[cfg_attr(docsrs, doc(cfg(feature = "geotiff")))]
pub use oxigdal_geotiff as geotiff;
#[cfg(feature = "geojson")]
#[cfg_attr(docsrs, doc(cfg(feature = "geojson")))]
pub use oxigdal_geojson as geojson;
#[cfg(feature = "shapefile")]
#[cfg_attr(docsrs, doc(cfg(feature = "shapefile")))]
pub use oxigdal_shapefile as shapefile;
#[cfg(feature = "geoparquet")]
#[cfg_attr(docsrs, doc(cfg(feature = "geoparquet")))]
pub use oxigdal_geoparquet as geoparquet;
#[cfg(feature = "netcdf")]
#[cfg_attr(docsrs, doc(cfg(feature = "netcdf")))]
pub use oxigdal_netcdf as netcdf;
#[cfg(feature = "hdf5")]
#[cfg_attr(docsrs, doc(cfg(feature = "hdf5")))]
pub use oxigdal_hdf5 as hdf5;
#[cfg(feature = "zarr")]
#[cfg_attr(docsrs, doc(cfg(feature = "zarr")))]
pub use oxigdal_zarr as zarr;
#[cfg(feature = "grib")]
#[cfg_attr(docsrs, doc(cfg(feature = "grib")))]
pub use oxigdal_grib as grib;
#[cfg(feature = "stac")]
#[cfg_attr(docsrs, doc(cfg(feature = "stac")))]
pub use oxigdal_stac as stac;
#[cfg(feature = "terrain")]
#[cfg_attr(docsrs, doc(cfg(feature = "terrain")))]
pub use oxigdal_terrain as terrain;
#[cfg(feature = "vrt")]
#[cfg_attr(docsrs, doc(cfg(feature = "vrt")))]
pub use oxigdal_vrt as vrt;
#[cfg(feature = "flatgeobuf")]
#[cfg_attr(docsrs, doc(cfg(feature = "flatgeobuf")))]
pub use oxigdal_flatgeobuf as flatgeobuf;
#[cfg(feature = "jpeg2000")]
#[cfg_attr(docsrs, doc(cfg(feature = "jpeg2000")))]
pub use oxigdal_jpeg2000 as jpeg2000;
#[cfg(feature = "cloud")]
#[cfg_attr(docsrs, doc(cfg(feature = "cloud")))]
pub use oxigdal_cloud as cloud;
#[cfg(feature = "proj")]
#[cfg_attr(docsrs, doc(cfg(feature = "proj")))]
pub use oxigdal_proj as proj;
#[cfg(feature = "algorithms")]
#[cfg_attr(docsrs, doc(cfg(feature = "algorithms")))]
pub use oxigdal_algorithms as algorithms;
#[cfg(feature = "analytics")]
#[cfg_attr(docsrs, doc(cfg(feature = "analytics")))]
pub use oxigdal_analytics as analytics;
#[cfg(feature = "streaming")]
#[cfg_attr(docsrs, doc(cfg(feature = "streaming")))]
pub use oxigdal_streaming as streaming_ext;
#[cfg(feature = "ml")]
#[cfg_attr(docsrs, doc(cfg(feature = "ml")))]
pub use oxigdal_ml as ml;
#[cfg(feature = "gpu")]
#[cfg_attr(docsrs, doc(cfg(feature = "gpu")))]
pub use oxigdal_gpu as gpu;
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use oxigdal_server as server;
#[cfg(feature = "temporal")]
#[cfg_attr(docsrs, doc(cfg(feature = "temporal")))]
pub use oxigdal_temporal as temporal;
#[cfg(feature = "gpkg")]
#[cfg_attr(docsrs, doc(cfg(feature = "gpkg")))]
pub use oxigdal_gpkg as gpkg;
#[cfg(feature = "pmtiles")]
#[cfg_attr(docsrs, doc(cfg(feature = "pmtiles")))]
pub use oxigdal_pmtiles as pmtiles;
#[cfg(feature = "mbtiles")]
#[cfg_attr(docsrs, doc(cfg(feature = "mbtiles")))]
pub use oxigdal_mbtiles as mbtiles;
#[cfg(feature = "copc")]
#[cfg_attr(docsrs, doc(cfg(feature = "copc")))]
pub use oxigdal_copc as copc;
#[cfg(feature = "index")]
#[cfg_attr(docsrs, doc(cfg(feature = "index")))]
pub use oxigdal_index as index;
#[cfg(feature = "noalloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "noalloc")))]
pub use oxigdal_noalloc as noalloc;
#[cfg(feature = "services")]
#[cfg_attr(docsrs, doc(cfg(feature = "services")))]
pub use oxigdal_services as services;
#[derive(Debug, Clone)]
pub struct DatasetInfo {
pub format: DatasetFormat,
pub path: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub band_count: u32,
pub layer_count: u32,
pub crs: Option<String>,
pub geotransform: Option<GeoTransform>,
pub feature_count: Option<u64>,
pub bounds: Option<BoundingBox>,
}
pub struct Dataset {
path: String,
info: DatasetInfo,
}
impl Dataset {
pub fn open(path: &str) -> Result<Self> {
if crate::cloud_detect::is_cloud_uri(path) {
return crate::cloud_detect::open_cloud_dataset(path);
}
let p = std::path::Path::new(path);
let format = if p.exists() {
DatasetFormat::detect(p).unwrap_or_else(|_| DatasetFormat::from_extension(path))
} else {
DatasetFormat::from_extension(path)
};
Self::open_with_format(path, format)
}
pub fn open_with_format(path: &str, format: DatasetFormat) -> Result<Self> {
match format {
#[cfg(feature = "geotiff")]
DatasetFormat::GeoTiff => Self::open_raster(path, DatasetFormat::GeoTiff),
#[cfg(feature = "geojson")]
DatasetFormat::GeoJson => Self::open_vector(path, DatasetFormat::GeoJson),
#[cfg(feature = "shapefile")]
DatasetFormat::Shapefile => Self::open_vector(path, DatasetFormat::Shapefile),
#[cfg(feature = "geoparquet")]
DatasetFormat::GeoParquet => Self::open_vector(path, DatasetFormat::GeoParquet),
#[cfg(feature = "netcdf")]
DatasetFormat::NetCdf => Self::open_raster(path, DatasetFormat::NetCdf),
#[cfg(feature = "hdf5")]
DatasetFormat::Hdf5 => Self::open_raster(path, DatasetFormat::Hdf5),
#[cfg(feature = "zarr")]
DatasetFormat::Zarr => Self::open_raster(path, DatasetFormat::Zarr),
#[cfg(feature = "grib")]
DatasetFormat::Grib => Self::open_raster(path, DatasetFormat::Grib),
#[cfg(feature = "flatgeobuf")]
DatasetFormat::FlatGeobuf => Self::open_vector(path, DatasetFormat::FlatGeobuf),
#[cfg(feature = "jpeg2000")]
DatasetFormat::Jpeg2000 => Self::open_raster(path, DatasetFormat::Jpeg2000),
#[cfg(feature = "vrt")]
DatasetFormat::Vrt => Self::open_raster(path, DatasetFormat::Vrt),
#[cfg(feature = "gpkg")]
DatasetFormat::GeoPackage => Self::open_vector(path, DatasetFormat::GeoPackage),
#[cfg(feature = "pmtiles")]
DatasetFormat::PMTiles => Self::open_raster(path, DatasetFormat::PMTiles),
#[cfg(feature = "mbtiles")]
DatasetFormat::MBTiles => Self::open_raster(path, DatasetFormat::MBTiles),
#[cfg(feature = "copc")]
DatasetFormat::Copc => Self::open_raster(path, DatasetFormat::Copc),
_ => Err(OxiGdalError::NotSupported {
operation: format!(
"Format '{}' for '{}' — enable the corresponding feature flag or check the file extension",
format.driver_name(),
path,
),
}),
}
}
fn open_raster(path: &str, format: DatasetFormat) -> Result<Self> {
let p = std::path::Path::new(path);
if !p.exists() {
return Err(OxiGdalError::Io(oxigdal_core::error::IoError::NotFound {
path: path.to_string(),
}));
}
let mut info = match format {
DatasetFormat::GeoTiff => {
crate::open::extract_tiff_info(p).unwrap_or_else(|| DatasetInfo {
format,
path: Some(path.to_string()),
width: None,
height: None,
band_count: 0,
layer_count: 0,
crs: None,
geotransform: None,
feature_count: None,
bounds: None,
})
}
_ => DatasetInfo {
format,
path: Some(path.to_string()),
width: None,
height: None,
band_count: 0,
layer_count: 0,
crs: None,
geotransform: None,
feature_count: None,
bounds: None,
},
};
info.path = Some(path.to_string());
Ok(Self {
path: path.to_string(),
info,
})
}
fn open_vector(path: &str, format: DatasetFormat) -> Result<Self> {
let p = std::path::Path::new(path);
if !p.exists() {
return Err(OxiGdalError::Io(oxigdal_core::error::IoError::NotFound {
path: path.to_string(),
}));
}
let empty_info = || DatasetInfo {
format,
path: Some(path.to_string()),
width: None,
height: None,
band_count: 0,
layer_count: 0,
crs: None,
geotransform: None,
feature_count: None,
bounds: None,
};
let mut info = match format {
DatasetFormat::GeoJson => {
crate::open::extract_geojson_info(p).unwrap_or_else(empty_info)
}
#[cfg(feature = "shapefile")]
DatasetFormat::Shapefile => {
crate::open::extract_shapefile_info(p).unwrap_or_else(empty_info)
}
#[cfg(feature = "flatgeobuf")]
DatasetFormat::FlatGeobuf => {
crate::open::extract_flatgeobuf_info(p).unwrap_or_else(empty_info)
}
#[cfg(feature = "geoparquet")]
DatasetFormat::GeoParquet => {
crate::open::extract_geoparquet_info(p).unwrap_or_else(empty_info)
}
_ => empty_info(),
};
info.path = Some(path.to_string());
Ok(Self {
path: path.to_string(),
info,
})
}
pub(crate) fn from_info(path: String, info: DatasetInfo) -> Self {
Self { path, info }
}
pub fn path(&self) -> &str {
&self.path
}
pub fn format(&self) -> DatasetFormat {
self.info.format
}
pub fn info(&self) -> &DatasetInfo {
&self.info
}
pub fn width(&self) -> u32 {
self.info.width.unwrap_or(0)
}
pub fn height(&self) -> u32 {
self.info.height.unwrap_or(0)
}
pub fn crs(&self) -> Option<&str> {
self.info.crs.as_deref()
}
pub fn band_count(&self) -> u32 {
self.info.band_count
}
pub fn layer_count(&self) -> u32 {
self.info.layer_count
}
pub fn geotransform(&self) -> Option<&GeoTransform> {
self.info.geotransform.as_ref()
}
pub fn feature_count(&self) -> Option<u64> {
self.info.feature_count
}
pub fn bounds(&self) -> Option<&BoundingBox> {
self.info.bounds.as_ref()
}
pub fn read_band(&self, band: u32) -> Result<oxigdal_core::buffer::RasterBuffer> {
self.read_band_impl(band)
}
pub fn bands(&self) -> BandIter<'_> {
BandIter {
dataset: self,
next_band: 0,
band_count: self.info.band_count,
}
}
fn read_band_impl(&self, band: u32) -> Result<oxigdal_core::buffer::RasterBuffer> {
if self.info.band_count > 0 && band >= self.info.band_count {
return Err(OxiGdalError::InvalidParameter {
parameter: "band",
message: format!(
"band index {} is out of range (dataset has {} bands)",
band, self.info.band_count
),
});
}
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
return self.read_band_geotiff(band);
}
Err(OxiGdalError::NotSupported {
operation: format!(
"read_band() is not supported for format '{}' (enable the 'geotiff' feature for GeoTIFF support)",
self.info.format.driver_name()
),
})
}
#[cfg(feature = "geotiff")]
fn read_band_geotiff(&self, band: u32) -> Result<oxigdal_core::buffer::RasterBuffer> {
use oxigdal_core::buffer::RasterBuffer;
use oxigdal_core::io::FileDataSource;
use oxigdal_core::types::NoDataValue;
use oxigdal_geotiff::GeoTiffReader;
let source = FileDataSource::open(&self.path).map_err(|e| {
OxiGdalError::Io(oxigdal_core::error::IoError::Read {
message: format!("failed to open '{}': {e}", self.path),
})
})?;
let reader = GeoTiffReader::open(source)?;
let width = reader.width();
let height = reader.height();
let raw_bytes = reader.read_band(0, band as usize)?;
let data_type = reader
.data_type()
.unwrap_or(oxigdal_core::types::RasterDataType::UInt8);
RasterBuffer::new(raw_bytes, width, height, data_type, NoDataValue::None).map_err(|e| {
OxiGdalError::Internal {
message: format!("failed to create RasterBuffer: {e}"),
}
})
}
pub fn statistics(&self, band: u32) -> Result<BandStatistics> {
self.compute_band_statistics(band)
}
pub fn clip(&self, bbox: BoundingBox) -> Result<Dataset> {
self.clip_to_bbox(bbox)
}
pub fn reproject(&self, target_epsg: u32) -> Result<Dataset> {
self.reproject_to_epsg(target_epsg)
}
fn compute_band_statistics(&self, band: u32) -> Result<BandStatistics> {
if self.info.band_count > 0 && band >= self.info.band_count {
return Err(OxiGdalError::InvalidParameter {
parameter: "band",
message: format!(
"band index {} is out of range (dataset has {} bands)",
band, self.info.band_count
),
});
}
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
return self.statistics_geotiff(band);
}
Err(OxiGdalError::NotSupported {
operation: format!(
"statistics() is not supported for format '{}' (enable the 'geotiff' feature for GeoTIFF support)",
self.info.format.driver_name()
),
})
}
#[cfg(feature = "geotiff")]
fn statistics_geotiff(&self, band: u32) -> Result<BandStatistics> {
use oxigdal_core::buffer::RasterBuffer;
use oxigdal_core::io::FileDataSource;
use oxigdal_core::types::NoDataValue;
use oxigdal_geotiff::GeoTiffReader;
let source = FileDataSource::open(&self.path).map_err(|e| {
OxiGdalError::Io(oxigdal_core::error::IoError::Read {
message: format!("failed to open '{}': {e}", self.path),
})
})?;
let reader = GeoTiffReader::open(source)?;
let width = reader.width();
let height = reader.height();
let raw_bytes = reader.read_band(0, band as usize)?;
let data_type = reader
.data_type()
.unwrap_or(oxigdal_core::types::RasterDataType::UInt8);
let buf = RasterBuffer::new(raw_bytes, width, height, data_type, NoDataValue::None)
.map_err(|e| OxiGdalError::Internal {
message: format!("failed to create RasterBuffer: {e}"),
})?;
let buf_stats = buf.compute_statistics()?;
Ok(BandStatistics {
band,
min: buf_stats.min,
max: buf_stats.max,
mean: buf_stats.mean,
std_dev: buf_stats.std_dev,
valid_count: buf_stats.valid_count,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BandStatistics {
pub band: u32,
pub min: f64,
pub max: f64,
pub mean: f64,
pub std_dev: f64,
pub valid_count: u64,
}
pub struct BandIter<'a> {
pub(crate) dataset: &'a Dataset,
pub(crate) next_band: u32,
pub(crate) band_count: u32,
}
impl<'a> Iterator for BandIter<'a> {
type Item = Result<oxigdal_core::buffer::RasterBuffer>;
fn next(&mut self) -> Option<Self::Item> {
if self.next_band >= self.band_count {
return None;
}
let band = self.next_band;
self.next_band += 1;
Some(self.dataset.read_band(band))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.band_count.saturating_sub(self.next_band)) as usize;
(remaining, Some(remaining))
}
}
impl<'a> core::iter::ExactSizeIterator for BandIter<'a> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Compression {
#[default]
None,
Deflate,
Lzw,
PackBits,
Zstd,
}
#[derive(Debug, Clone, Default)]
pub struct ConversionOptions {
pub compression: Option<Compression>,
pub compression_level: Option<u8>,
pub cog: bool,
pub overviews: Vec<u32>,
pub tile_size: Option<u32>,
pub creation_options: Vec<(String, String)>,
}
pub(crate) fn extract_epsg_from_crs_string(crs: &str) -> Option<u32> {
let upper = crs.to_uppercase();
let pos = upper.find("EPSG:")?;
let after_colon = &crs[pos + 5..];
let digits: String = after_colon
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
digits.parse().ok()
}
impl core::fmt::Debug for Dataset {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Dataset")
.field("path", &self.path)
.field("format", &self.info.format)
.field("width", &self.info.width)
.field("height", &self.info.height)
.field("band_count", &self.info.band_count)
.field("layer_count", &self.info.layer_count)
.finish()
}
}
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[allow(clippy::vec_init_then_push)]
pub fn drivers() -> Vec<&'static str> {
let mut list = Vec::new();
#[cfg(feature = "geotiff")]
list.push("GTiff");
#[cfg(feature = "geojson")]
list.push("GeoJSON");
#[cfg(feature = "shapefile")]
list.push("ESRI Shapefile");
#[cfg(feature = "geoparquet")]
list.push("GeoParquet");
#[cfg(feature = "netcdf")]
list.push("netCDF");
#[cfg(feature = "hdf5")]
list.push("HDF5");
#[cfg(feature = "zarr")]
list.push("Zarr");
#[cfg(feature = "grib")]
list.push("GRIB");
#[cfg(feature = "stac")]
list.push("STAC");
#[cfg(feature = "terrain")]
list.push("Terrain");
#[cfg(feature = "vrt")]
list.push("VRT");
#[cfg(feature = "flatgeobuf")]
list.push("FlatGeobuf");
#[cfg(feature = "jpeg2000")]
list.push("JPEG2000");
#[cfg(feature = "gpkg")]
list.push("GPKG");
#[cfg(feature = "pmtiles")]
list.push("PMTiles");
#[cfg(feature = "mbtiles")]
list.push("MBTiles");
#[cfg(feature = "copc")]
list.push("COPC");
list
}
pub fn driver_count() -> usize {
drivers().len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version() {
let v = version();
assert!(!v.is_empty());
assert!(v.starts_with("0."));
}
#[test]
fn test_default_drivers() {
let d = drivers();
assert!(d.contains(&"GTiff"), "GeoTIFF should be a default driver");
assert!(d.contains(&"GeoJSON"), "GeoJSON should be a default driver");
assert!(
d.contains(&"ESRI Shapefile"),
"Shapefile should be a default driver"
);
}
#[test]
fn test_driver_count() {
assert!(driver_count() >= 3, "At least 3 default drivers");
}
#[test]
fn test_format_detection() {
assert_eq!(
DatasetFormat::from_extension("world.tif"),
DatasetFormat::GeoTiff
);
assert_eq!(
DatasetFormat::from_extension("data.geojson"),
DatasetFormat::GeoJson
);
assert_eq!(
DatasetFormat::from_extension("map.shp"),
DatasetFormat::Shapefile
);
assert_eq!(
DatasetFormat::from_extension("cloud.zarr"),
DatasetFormat::Zarr
);
assert_eq!(
DatasetFormat::from_extension("output.parquet"),
DatasetFormat::GeoParquet
);
assert_eq!(
DatasetFormat::from_extension("scene.vrt"),
DatasetFormat::Vrt
);
assert_eq!(
DatasetFormat::from_extension("README.md"),
DatasetFormat::Unknown
);
}
#[test]
fn test_format_display() {
assert_eq!(DatasetFormat::GeoTiff.to_string(), "GTiff");
assert_eq!(DatasetFormat::GeoJson.to_string(), "GeoJSON");
}
#[test]
fn test_open_nonexistent() {
let result = Dataset::open("/nonexistent/file.tif");
assert!(result.is_err());
}
#[test]
fn test_open_unsupported_extension() {
let result = Dataset::open("data.xyz");
assert!(result.is_err());
}
#[test]
fn test_open_with_format() {
let result = Dataset::open_with_format("/no/such/file.tif", DatasetFormat::GeoTiff);
assert!(result.is_err());
}
#[test]
fn test_magic_bytes_tiff_le() {
let bytes = [0x49u8, 0x49, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::GeoTiff)
);
}
#[test]
fn test_magic_bytes_tiff_be() {
let bytes = [0x4Du8, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::GeoTiff)
);
}
#[test]
fn test_magic_bytes_jp2() {
let bytes: [u8; 12] = [
0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A,
];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::Jpeg2000)
);
}
#[test]
fn test_magic_bytes_hdf5() {
let bytes: [u8; 8] = [0x89, 0x48, 0x44, 0x46, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::Hdf5)
);
}
#[test]
fn test_magic_bytes_netcdf() {
let bytes = [0x43u8, 0x44, 0x46, 0x01];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::NetCdf)
);
}
#[test]
fn test_magic_bytes_flatgeobuf() {
let bytes: [u8; 8] = [0x66, 0x67, 0x62, 0x03, 0x66, 0x67, 0x62, 0x00];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::FlatGeobuf)
);
}
#[test]
fn test_magic_bytes_pmtiles() {
let bytes = b"PMTiles\x03";
assert_eq!(
DatasetFormat::detect_from_magic_bytes(bytes),
Some(DatasetFormat::PMTiles)
);
}
#[test]
fn test_magic_bytes_las() {
let bytes = b"LASF";
assert_eq!(
DatasetFormat::detect_from_magic_bytes(bytes),
Some(DatasetFormat::Copc)
);
}
#[test]
fn test_magic_bytes_grib() {
let bytes = b"GRIB";
assert_eq!(
DatasetFormat::detect_from_magic_bytes(bytes),
Some(DatasetFormat::Grib)
);
}
#[test]
fn test_magic_bytes_geoparquet() {
let bytes = b"PAR1";
assert_eq!(
DatasetFormat::detect_from_magic_bytes(bytes),
Some(DatasetFormat::GeoParquet)
);
}
#[test]
fn test_magic_bytes_sqlite() {
let bytes: [u8; 16] = [
0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20,
0x33, 0x00,
];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::GeoPackage)
);
}
#[test]
fn test_magic_bytes_zip() {
let bytes: [u8; 4] = [0x50, 0x4B, 0x03, 0x04];
assert_eq!(
DatasetFormat::detect_from_magic_bytes(&bytes),
Some(DatasetFormat::GeoPackage)
);
}
#[test]
fn test_magic_bytes_empty_returns_none() {
assert_eq!(DatasetFormat::detect_from_magic_bytes(&[]), None);
}
#[test]
fn test_magic_bytes_unknown_returns_none() {
let bytes = b"UNKNOWNFORMAT";
assert_eq!(DatasetFormat::detect_from_magic_bytes(bytes), None);
}
#[test]
fn test_detect_file_tiff() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_detect_tiff.tif");
let bytes: [u8; 8] = [0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(&bytes))
.expect("write tiff");
let fmt = DatasetFormat::detect(&path).expect("detect");
assert_eq!(fmt, DatasetFormat::GeoTiff);
}
#[test]
fn test_detect_file_las_as_copc() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_detect_las.las");
let mut bytes = Vec::new();
bytes.extend_from_slice(b"LASF");
bytes.extend_from_slice(&[0u8; 64]);
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(&bytes))
.expect("write las");
let fmt = DatasetFormat::detect(&path).expect("detect");
assert_eq!(fmt, DatasetFormat::Copc);
}
#[test]
fn test_detect_fallback_to_extension() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_detect_ext_fallback.geojson");
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(b"{}"))
.expect("write");
let fmt = DatasetFormat::detect(&path).expect("detect");
assert_eq!(fmt, DatasetFormat::GeoJson);
}
#[cfg(feature = "geojson")]
#[test]
fn test_open_geojson_layer_count() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_open_layer_count.geojson");
let content = br#"{"type":"FeatureCollection","features":[]}"#;
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(content))
.expect("write");
let ds = Dataset::open(path.to_str().expect("path str")).expect("open");
assert_eq!(ds.format(), DatasetFormat::GeoJson);
assert_eq!(
ds.layer_count(),
1,
"FeatureCollection should have layer_count=1"
);
assert_eq!(
ds.info().path,
Some(path.to_str().expect("path str").to_string())
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_open_tiff_wires_metadata() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_open_tiff_meta.tif");
let mut buf: Vec<u8> = vec![0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
buf.extend_from_slice(&3u16.to_le_bytes()); buf.extend_from_slice(&256u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&64u32.to_le_bytes());
buf.extend_from_slice(&257u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&32u32.to_le_bytes());
buf.extend_from_slice(&277u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&[0x00, 0x00]);
buf.extend_from_slice(&0u32.to_le_bytes());
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(&buf))
.expect("write tiff");
let ds = Dataset::open(path.to_str().expect("path str")).expect("open");
assert_eq!(ds.format(), DatasetFormat::GeoTiff);
assert_eq!(ds.width(), 64);
assert_eq!(ds.height(), 32);
assert_eq!(ds.band_count(), 4);
assert_eq!(
ds.info().path,
Some(path.to_str().expect("path str").to_string())
);
}
}