use oxigdal::DatasetFormat;
use oxigdal::convert::{
ConversionPlan, ConversionStep, ConvertOptions, can_convert, detect_format,
supported_conversions,
};
#[test]
fn detect_tif_extension() {
assert_eq!(
detect_format("elevation.tif").ok(),
Some(DatasetFormat::GeoTiff)
);
}
#[test]
fn detect_tiff_extension() {
assert_eq!(detect_format("dem.tiff").ok(), Some(DatasetFormat::GeoTiff));
}
#[test]
fn detect_geojson_extension() {
assert_eq!(
detect_format("cities.geojson").ok(),
Some(DatasetFormat::GeoJson)
);
}
#[test]
fn detect_gpkg_extension() {
assert_eq!(
detect_format("admin.gpkg").ok(),
Some(DatasetFormat::GeoPackage)
);
}
#[test]
fn detect_pmtiles_extension() {
assert_eq!(
detect_format("basemap.pmtiles").ok(),
Some(DatasetFormat::PMTiles)
);
}
#[test]
fn detect_mbtiles_extension() {
assert_eq!(
detect_format("osm.mbtiles").ok(),
Some(DatasetFormat::MBTiles)
);
}
#[test]
fn detect_shp_extension() {
assert_eq!(
detect_format("roads.shp").ok(),
Some(DatasetFormat::Shapefile)
);
}
#[test]
fn detect_fgb_extension() {
assert_eq!(
detect_format("buildings.fgb").ok(),
Some(DatasetFormat::FlatGeobuf)
);
}
#[test]
fn detect_parquet_extension() {
assert_eq!(
detect_format("census.parquet").ok(),
Some(DatasetFormat::GeoParquet)
);
}
#[test]
fn detect_zarr_extension() {
assert_eq!(
detect_format("climate.zarr").ok(),
Some(DatasetFormat::Zarr)
);
}
#[test]
fn detect_copc_laz_compound() {
assert_eq!(
detect_format("lidar.copc.laz").ok(),
Some(DatasetFormat::Copc)
);
}
#[test]
fn detect_laz_extension() {
assert_eq!(detect_format("scan.laz").ok(), Some(DatasetFormat::Copc));
}
#[test]
fn detect_las_extension() {
assert_eq!(detect_format("scan.las").ok(), Some(DatasetFormat::Copc));
}
#[test]
fn detect_jp2_extension() {
assert_eq!(
detect_format("imagery.jp2").ok(),
Some(DatasetFormat::Jpeg2000)
);
}
#[test]
fn detect_grib_extension() {
assert_eq!(
detect_format("forecast.grib2").ok(),
Some(DatasetFormat::Grib)
);
}
#[test]
fn detect_empty_path_returns_error() {
assert!(detect_format("").is_err());
}
#[test]
fn detect_unknown_extension_returns_error() {
assert!(detect_format("readme.md").is_err());
}
#[test]
fn detect_no_extension_returns_error() {
assert!(detect_format("Makefile").is_err());
}
#[test]
fn detect_dot_only_returns_error() {
assert!(detect_format(".").is_err());
}
#[test]
fn detect_path_with_directories() {
assert_eq!(
detect_format("/data/project/world.tif").ok(),
Some(DatasetFormat::GeoTiff)
);
}
#[test]
fn detect_uppercase_extension() {
assert_eq!(
detect_format("IMAGE.TIF").ok(),
Some(DatasetFormat::GeoTiff)
);
}
#[test]
fn can_convert_identity_all_formats() {
let formats = [
DatasetFormat::GeoTiff,
DatasetFormat::GeoJson,
DatasetFormat::Shapefile,
DatasetFormat::GeoParquet,
DatasetFormat::FlatGeobuf,
DatasetFormat::Zarr,
DatasetFormat::PMTiles,
DatasetFormat::MBTiles,
DatasetFormat::Copc,
DatasetFormat::GeoPackage,
];
for fmt in &formats {
assert!(
can_convert(*fmt, *fmt),
"identity should be true for {fmt:?}"
);
}
}
#[test]
fn can_convert_raster_to_raster_pairs() {
let raster_formats = [
DatasetFormat::GeoTiff,
DatasetFormat::Zarr,
DatasetFormat::NetCdf,
DatasetFormat::Hdf5,
DatasetFormat::Jpeg2000,
DatasetFormat::PMTiles,
DatasetFormat::MBTiles,
DatasetFormat::Copc,
];
for &a in &raster_formats {
for &b in &raster_formats {
assert!(
can_convert(a, b),
"raster-to-raster should work: {a:?} -> {b:?}"
);
}
}
}
#[test]
fn can_convert_vector_to_vector_pairs() {
let vector_formats = [
DatasetFormat::GeoJson,
DatasetFormat::Shapefile,
DatasetFormat::GeoParquet,
DatasetFormat::FlatGeobuf,
DatasetFormat::GeoPackage,
];
for &a in &vector_formats {
for &b in &vector_formats {
assert!(
can_convert(a, b),
"vector-to-vector should work: {a:?} -> {b:?}"
);
}
}
}
#[test]
fn cannot_convert_raster_to_pure_vector() {
assert!(!can_convert(DatasetFormat::GeoTiff, DatasetFormat::GeoJson));
assert!(!can_convert(DatasetFormat::Zarr, DatasetFormat::Shapefile));
assert!(!can_convert(
DatasetFormat::PMTiles,
DatasetFormat::FlatGeobuf
));
}
#[test]
fn cannot_convert_vector_to_pure_raster() {
assert!(!can_convert(DatasetFormat::GeoJson, DatasetFormat::GeoTiff));
assert!(!can_convert(DatasetFormat::Shapefile, DatasetFormat::Zarr));
}
#[test]
fn can_convert_anything_to_geopackage() {
assert!(can_convert(
DatasetFormat::GeoTiff,
DatasetFormat::GeoPackage
));
assert!(can_convert(
DatasetFormat::GeoJson,
DatasetFormat::GeoPackage
));
assert!(can_convert(
DatasetFormat::PMTiles,
DatasetFormat::GeoPackage
));
}
#[test]
fn can_convert_geopackage_to_anything() {
assert!(can_convert(
DatasetFormat::GeoPackage,
DatasetFormat::GeoTiff
));
assert!(can_convert(
DatasetFormat::GeoPackage,
DatasetFormat::GeoJson
));
}
#[test]
fn cannot_convert_unknown() {
assert!(!can_convert(DatasetFormat::Unknown, DatasetFormat::GeoTiff));
assert!(!can_convert(DatasetFormat::GeoTiff, DatasetFormat::Unknown));
assert!(can_convert(DatasetFormat::Unknown, DatasetFormat::Unknown));
}
#[test]
fn supported_conversions_has_many_pairs() {
let pairs = supported_conversions();
assert!(pairs.len() > 50, "expected many pairs, got {}", pairs.len());
}
#[test]
fn supported_conversions_excludes_identity() {
for (from, to) in supported_conversions() {
assert_ne!(from, to);
}
}
#[test]
fn reexport_core_types_accessible() {
let _ = oxigdal::version();
let count = oxigdal::driver_count();
assert!(count >= 3, "default features should enable >= 3 drivers");
}
#[test]
fn dataset_format_display() {
assert_eq!(DatasetFormat::GeoTiff.to_string(), "GTiff");
assert_eq!(DatasetFormat::PMTiles.to_string(), "PMTiles");
assert_eq!(DatasetFormat::MBTiles.to_string(), "MBTiles");
assert_eq!(DatasetFormat::Copc.to_string(), "COPC");
assert_eq!(DatasetFormat::GeoPackage.to_string(), "GPKG");
}
#[test]
fn dataset_format_driver_name() {
assert_eq!(DatasetFormat::GeoTiff.driver_name(), "GTiff");
assert_eq!(DatasetFormat::GeoJson.driver_name(), "GeoJSON");
assert_eq!(DatasetFormat::PMTiles.driver_name(), "PMTiles");
}
#[test]
fn plan_simple_conversion_has_expected_steps() {
let plan = ConversionPlan::build(
DatasetFormat::GeoJson,
DatasetFormat::Shapefile,
ConvertOptions::new(),
)
.expect("plan");
assert_eq!(plan.source, DatasetFormat::GeoJson);
assert_eq!(plan.target, DatasetFormat::Shapefile);
assert!(plan.step_count() >= 3);
assert!(!plan.has_reprojection());
assert!(!plan.has_bbox_filter());
}
#[test]
fn plan_with_all_options() {
let opts = ConvertOptions::new()
.with_bbox(-10.0, -10.0, 10.0, 10.0)
.with_target_crs("EPSG:3857")
.with_feature_limit(100)
.with_compression("lz4");
let plan = ConversionPlan::build(DatasetFormat::Shapefile, DatasetFormat::GeoParquet, opts)
.expect("plan");
assert!(plan.has_reprojection());
assert!(plan.has_bbox_filter());
assert!(plan.step_count() >= 5);
}
#[test]
fn plan_unsupported_returns_error() {
let result = ConversionPlan::build(
DatasetFormat::GeoTiff,
DatasetFormat::GeoJson,
ConvertOptions::new(),
);
assert!(result.is_err());
}
#[test]
fn plan_first_step_is_read_source() {
let plan = ConversionPlan::build(
DatasetFormat::GeoTiff,
DatasetFormat::Zarr,
ConvertOptions::new(),
)
.expect("plan");
assert!(matches!(
plan.steps.first(),
Some(ConversionStep::ReadSource(DatasetFormat::GeoTiff))
));
}
#[test]
fn plan_last_step_is_write_target() {
let plan = ConversionPlan::build(
DatasetFormat::GeoTiff,
DatasetFormat::Zarr,
ConvertOptions::new(),
)
.expect("plan");
assert!(matches!(
plan.steps.last(),
Some(ConversionStep::WriteTarget(DatasetFormat::Zarr))
));
}
#[test]
fn plan_identity_no_transcode() {
let plan = ConversionPlan::build(
DatasetFormat::GeoJson,
DatasetFormat::GeoJson,
ConvertOptions::new(),
)
.expect("plan");
assert_eq!(plan.step_count(), 2);
let has_transcode = plan.steps.iter().any(|s| {
matches!(
s,
ConversionStep::TranscodeRaster(_) | ConversionStep::TranscodeVector(_)
)
});
assert!(!has_transcode, "identity should not have transcode step");
}
#[cfg(feature = "geotiff")]
fn write_synthetic_tiff(name: &str) -> std::path::PathBuf {
use oxigdal::core_types::types::{GeoTransform, NoDataValue, RasterDataType};
use oxigdal::geotiff::{
GeoTiffWriter, GeoTiffWriterOptions, OverviewResampling, WriterConfig,
tiff::{Compression, PhotometricInterpretation, Predictor},
};
let dir = std::env::temp_dir();
let path = dir.join(name);
let config = WriterConfig {
width: 8,
height: 8,
band_count: 1,
data_type: RasterDataType::Float32,
compression: Compression::None,
predictor: Predictor::None,
tile_width: None,
tile_height: None,
photometric: PhotometricInterpretation::BlackIsZero,
geo_transform: Some(GeoTransform::north_up(10.0, 50.0, 1.0, -1.0)),
epsg_code: Some(4326),
nodata: NoDataValue::None,
use_bigtiff: false,
generate_overviews: false,
overview_resampling: OverviewResampling::Average,
overview_levels: vec![],
};
let pixel_data: Vec<u8> = (1u32..=64u32)
.flat_map(|v| (v as f32).to_le_bytes())
.collect();
let mut writer =
GeoTiffWriter::create(&path, config, GeoTiffWriterOptions::default()).expect("create tiff");
writer.write(&pixel_data).expect("write tiff");
path
}
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_statistics_geotiff() {
let path = write_synthetic_tiff("test_ds_statistics_8x8.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let stats = ds.statistics(0).expect("statistics band 0");
assert!(
stats.min <= stats.mean && stats.mean <= stats.max,
"expected min <= mean <= max, got min={} mean={} max={}",
stats.min,
stats.mean,
stats.max
);
assert_eq!(stats.valid_count, 64, "all 64 pixels should be valid");
assert_eq!(stats.band, 0);
assert!(
(stats.min - 1.0).abs() < 1e-3,
"min should be ~1.0, got {}",
stats.min
);
assert!(
(stats.max - 64.0).abs() < 1e-3,
"max should be ~64.0, got {}",
stats.max
);
assert!(
(stats.mean - 32.5).abs() < 1e-2,
"mean should be ~32.5, got {}",
stats.mean
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_statistics_band_out_of_range() {
let path = write_synthetic_tiff("test_ds_statistics_oor.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let result = ds.statistics(5);
assert!(
result.is_err(),
"expected error for out-of-range band, got {:?}",
result
);
}
#[test]
fn test_dataset_clip_raster() {
use oxigdal::{BoundingBox, Dataset, DatasetFormat};
#[cfg(feature = "geotiff")]
{
use oxigdal::core_types::types::{GeoTransform, NoDataValue, RasterDataType};
use oxigdal::geotiff::{
GeoTiffWriter, GeoTiffWriterOptions, OverviewResampling, WriterConfig,
tiff::{Compression, PhotometricInterpretation, Predictor},
};
let dir = std::env::temp_dir();
let path = dir.join("test_ds_clip_16x16.tif");
let config = WriterConfig {
width: 16,
height: 16,
band_count: 1,
data_type: RasterDataType::Float32,
compression: Compression::None,
predictor: Predictor::None,
tile_width: None,
tile_height: None,
photometric: PhotometricInterpretation::BlackIsZero,
geo_transform: Some(GeoTransform::north_up(0.0, 16.0, 1.0, -1.0)),
epsg_code: Some(4326),
nodata: NoDataValue::None,
use_bigtiff: false,
generate_overviews: false,
overview_resampling: OverviewResampling::Average,
overview_levels: vec![],
};
let pixel_data = vec![0u8; 16 * 16 * 4]; let mut writer =
GeoTiffWriter::create(&path, config, GeoTiffWriterOptions::default()).expect("create");
writer.write(&pixel_data).expect("write");
let ds = Dataset::open(path.to_str().expect("path str")).expect("open");
assert_eq!(ds.width(), 16);
assert_eq!(ds.height(), 16);
let gt = ds
.geotransform()
.expect("16×16 TIFF should have a geotransform");
let (wx_min, wy_min) = gt.pixel_to_world(2.0, 2.0);
let (wx_max, wy_max) = gt.pixel_to_world(10.0, 10.0);
let (bmin_x, bmax_x) = if wx_min <= wx_max {
(wx_min, wx_max)
} else {
(wx_max, wx_min)
};
let (bmin_y, bmax_y) = if wy_min <= wy_max {
(wy_min, wy_max)
} else {
(wy_max, wy_min)
};
let bbox = BoundingBox::new(bmin_x, bmin_y, bmax_x, bmax_y).expect("valid derived bbox");
let clipped = ds.clip(bbox).expect("clip");
assert!(
clipped.width() < ds.width(),
"clipped width {} should be < original {}",
clipped.width(),
ds.width()
);
assert!(
clipped.height() < ds.height(),
"clipped height {} should be < original {}",
clipped.height(),
ds.height()
);
}
#[cfg(feature = "geojson")]
{
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_ds_clip_vector.geojson");
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(b"{\"type\":\"FeatureCollection\",\"features\":[]}"))
.expect("write");
let ds = Dataset::open(path.to_str().expect("path str")).expect("open");
let bbox = BoundingBox::new(-10.0, -10.0, 10.0, 10.0).expect("valid bbox");
let clipped = ds.clip(bbox).expect("clip vector");
assert_eq!(clipped.format(), DatasetFormat::GeoJson);
}
}
#[cfg(not(feature = "proj"))]
#[test]
fn test_dataset_reproject_returns_err_without_proj_feature() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_ds_reproject_noproj.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 ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let result = ds.reproject(3857);
assert!(
result.is_err(),
"reproject() should return Err without 'proj' feature"
);
match result {
Err(oxigdal::OxiGdalError::NotSupported { .. }) => {}
other => panic!("expected NotSupported, got {other:?}"),
}
}
#[cfg(feature = "proj")]
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_reproject_with_proj_feature() {
let path = write_synthetic_tiff("test_ds_reproject_proj.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let reprojected = ds.reproject(3857).expect("reproject to 3857");
let new_crs = reprojected
.crs()
.expect("reprojected dataset should have CRS");
assert!(
new_crs.contains("3857"),
"CRS should mention 3857, got {new_crs}"
);
assert_eq!(reprojected.width(), ds.width());
assert_eq!(reprojected.height(), ds.height());
let orig_gt = ds.geotransform().expect("orig gt");
let new_gt = reprojected.geotransform().expect("new gt");
let changed = (new_gt.origin_x - orig_gt.origin_x).abs() > 1.0
|| (new_gt.origin_y - orig_gt.origin_y).abs() > 1.0;
assert!(
changed,
"geo-transform should change after reprojection, orig={orig_gt:?} new={new_gt:?}"
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_bands_iter_single_band() {
let path = write_synthetic_tiff("test_bands_iter_single.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
assert_eq!(ds.band_count(), 1, "synthetic tiff has 1 band");
let bands: Vec<_> = ds.bands().collect();
assert_eq!(bands.len(), 1, "iterator should yield exactly 1 band");
let buf = bands
.into_iter()
.next()
.expect("one band")
.expect("read ok");
assert_eq!(
buf.as_bytes().len(),
8 * 8 * 4, "single band should be 8×8×4 bytes"
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_bands_iter_out_of_range_errors() {
let path = write_synthetic_tiff("test_bands_oor.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let result = ds.read_band(5);
assert!(
result.is_err(),
"band index 5 should fail for a 1-band TIFF, got {:?}",
result
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_bands_iter_size_hint() {
let path = write_synthetic_tiff("test_bands_size_hint.tif");
let ds = oxigdal::Dataset::open(path.to_str().expect("path str")).expect("open");
let iter = ds.bands();
let (lo, hi) = iter.size_hint();
assert_eq!(lo, 1, "size_hint low should equal band_count");
assert_eq!(hi, Some(1), "size_hint high should equal band_count");
}
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_convert_raster_identity() {
use oxigdal::ConversionOptions;
let src_path = write_synthetic_tiff("test_convert_src.tif");
let dst_path = std::env::temp_dir().join("test_convert_dst.tif");
let src_ds = oxigdal::Dataset::open(src_path.to_str().expect("path")).expect("open src");
let dst_ds = src_ds
.convert(
&dst_path,
oxigdal::DatasetFormat::GeoTiff,
ConversionOptions::default(),
)
.expect("convert GeoTiff→GeoTiff");
assert_eq!(dst_ds.format(), oxigdal::DatasetFormat::GeoTiff);
assert!(dst_path.exists(), "output TIFF file should exist on disk");
}
#[test]
fn test_dataset_convert_unsupported_pair_errors() {
use oxigdal::{ConversionOptions, DatasetFormat};
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_convert_unsupported.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 ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open");
let output = dir.join("test_convert_unsupported_out.geojson");
let result = ds.convert(
&output,
DatasetFormat::GeoJson,
ConversionOptions::default(),
);
assert!(
result.is_err(),
"raster→vector conversion should return Err, got {:?}",
result
);
}
#[test]
fn test_cloud_uri_detection() {
assert!(oxigdal::is_cloud_uri("s3://bucket/key.tif"));
assert!(oxigdal::is_cloud_uri("gs://bucket/key.geojson"));
assert!(oxigdal::is_cloud_uri("az://container/blob.tif"));
assert!(oxigdal::is_cloud_uri(
"abfs://container@account.dfs.core.windows.net/path"
));
assert!(oxigdal::is_cloud_uri("http://example.com/file.tif"));
assert!(oxigdal::is_cloud_uri("https://example.com/file.tif"));
}
#[test]
fn test_open_bare_path_still_works() {
assert!(!oxigdal::is_cloud_uri("/local/path/to/file.tif"));
assert!(!oxigdal::is_cloud_uri("relative/path/file.tif"));
assert!(!oxigdal::is_cloud_uri("file.tif"));
}
#[cfg(not(feature = "cloud"))]
#[test]
fn test_open_s3_uri_parses_without_cloud_feature() {
let result = oxigdal::Dataset::open("s3://my-bucket/data/elevation.tif");
assert!(
result.is_err(),
"s3:// open should fail without 'cloud' feature"
);
match result {
Err(oxigdal::OxiGdalError::NotSupported { .. }) => {}
other => panic!("expected NotSupported, got {other:?}"),
}
}
#[cfg(feature = "geojson")]
#[test]
fn test_info_geojson_populated() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_info_geojson.geojson");
let content = br#"{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{}},
{"type":"Feature","geometry":{"type":"Point","coordinates":[1,1]},"properties":{}},
{"type":"Feature","geometry":{"type":"Point","coordinates":[2,2]},"properties":{}}
]}"#;
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(content))
.expect("write geojson");
let ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open");
let info = ds.info();
assert_eq!(info.layer_count, 1, "GeoJSON FeatureCollection has 1 layer");
assert_eq!(
info.feature_count,
Some(3),
"should count 3 features, got {:?}",
info.feature_count
);
}
#[cfg(feature = "geojson")]
#[test]
fn test_info_geojson_empty_collection() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_info_geojson_empty.geojson");
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(b"{\"type\":\"FeatureCollection\",\"features\":[]}"))
.expect("write geojson");
let ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open");
assert_eq!(
ds.info().feature_count,
None,
"empty FeatureCollection should yield feature_count=None"
);
}
#[cfg(feature = "geojson")]
#[test]
fn test_info_geojson_bbox_parsed() {
use std::io::Write;
let dir = std::env::temp_dir();
let path = dir.join("test_info_geojson_bbox.geojson");
let content = br#"{"type":"FeatureCollection","bbox":[-180,-90,180,90],"features":[]}"#;
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(content))
.expect("write geojson");
let ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open");
let bounds = ds.bounds();
assert!(
bounds.is_some(),
"bounds should be populated from GeoJSON bbox"
);
let b = bounds.expect("bounds");
assert!((b.min_x + 180.0).abs() < 1e-6, "min_x should be -180");
assert!((b.max_x - 180.0).abs() < 1e-6, "max_x should be 180");
}
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_convert_geotiff_cog() {
use oxigdal::ConversionOptions;
use oxigdal::core_types::io::FileDataSource;
use oxigdal::geotiff::CogReader;
let src_path = write_synthetic_tiff("test_cog_src.tif");
let dst_path = std::env::temp_dir().join("test_cog_dst.tif");
let src_ds = oxigdal::Dataset::open(src_path.to_str().expect("src path")).expect("open src");
let opts = ConversionOptions {
cog: true,
overviews: vec![2],
..ConversionOptions::default()
};
let dst_ds = src_ds
.convert(&dst_path, oxigdal::DatasetFormat::GeoTiff, opts)
.expect("COG conversion should succeed");
assert_eq!(dst_ds.format(), oxigdal::DatasetFormat::GeoTiff);
assert!(dst_path.exists(), "COG output should exist on disk");
assert_eq!(dst_ds.width(), src_ds.width());
assert_eq!(dst_ds.height(), src_ds.height());
let source = FileDataSource::open(&dst_path).expect("open COG source");
let cog_reader = CogReader::open(source).expect("output should be a valid COG");
assert!(
cog_reader.overview_count() > 0,
"COG output should have at least one overview level (requested [2])"
);
assert!(
cog_reader.tile_size().is_some(),
"COG output should be tiled (tile_size must be set)"
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_dataset_convert_geotiff_with_lzw_compression() {
use oxigdal::core_types::io::FileDataSource;
use oxigdal::geotiff::tiff::{Compression as TiffCompression, ImageInfo, TiffFile};
use oxigdal::{Compression, ConversionOptions};
let src_path = write_synthetic_tiff("test_convert_lzw_src.tif");
let dst_path = std::env::temp_dir().join("test_convert_lzw_dst.tif");
let src_ds = oxigdal::Dataset::open(src_path.to_str().expect("path")).expect("open src");
let opts = ConversionOptions {
compression: Some(Compression::Lzw),
..ConversionOptions::default()
};
let dst_ds = src_ds
.convert(&dst_path, oxigdal::DatasetFormat::GeoTiff, opts)
.expect("convert with LZW");
assert_eq!(dst_ds.format(), oxigdal::DatasetFormat::GeoTiff);
assert!(
dst_path.exists(),
"LZW-compressed TIFF should exist on disk"
);
assert_eq!(dst_ds.width(), src_ds.width());
assert_eq!(dst_ds.height(), src_ds.height());
let source = FileDataSource::open(&dst_path).expect("open dst source");
let tiff = TiffFile::parse(&source).expect("parse TIFF");
let variant = tiff.header.variant;
let img_info = ImageInfo::from_ifd::<FileDataSource>(
tiff.primary_ifd(),
&source,
tiff.byte_order(),
variant,
)
.expect("read image info");
assert_eq!(
img_info.compression,
TiffCompression::Lzw,
"output TIFF compression should be LZW (code 5), got {:?}",
img_info.compression,
);
}
#[cfg(feature = "geojson")]
#[test]
fn test_dataset_convert_geojson_to_geojson() {
use oxigdal::ConversionOptions;
use std::io::Write;
let dir = std::env::temp_dir();
let src_path = dir.join("test_convert_gj_src.geojson");
let dst_path = dir.join("test_convert_gj_dst.geojson");
let content = br#"{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Point","coordinates":[10,20]},"properties":{"name":"A"}}
]}"#;
std::fs::File::create(&src_path)
.and_then(|mut f| f.write_all(content))
.expect("write src geojson");
let src_ds = oxigdal::Dataset::open(src_path.to_str().expect("path")).expect("open src");
let dst_ds = src_ds
.convert(
&dst_path,
oxigdal::DatasetFormat::GeoJson,
ConversionOptions::default(),
)
.expect("convert GeoJSON→GeoJSON");
assert_eq!(dst_ds.format(), oxigdal::DatasetFormat::GeoJson);
assert!(dst_path.exists(), "output GeoJSON should exist on disk");
}
#[cfg(all(feature = "geojson", feature = "shapefile"))]
#[test]
fn test_dataset_convert_geojson_to_shapefile() {
use oxigdal::ConversionOptions;
use std::io::Write;
let dir = std::env::temp_dir();
let src_path = dir.join("test_convert_gj2shp_src.geojson");
let dst_path = dir.join("test_convert_gj2shp_dst.shp");
let content = br#"{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Point","coordinates":[10.0,20.0]},"properties":{"name":"Alpha"}},
{"type":"Feature","geometry":{"type":"Point","coordinates":[30.0,40.0]},"properties":{"name":"Beta"}}
]}"#;
std::fs::File::create(&src_path)
.and_then(|mut f| f.write_all(content))
.expect("write src geojson");
let src_ds = oxigdal::Dataset::open(src_path.to_str().expect("src path")).expect("open src");
let dst_ds = src_ds
.convert(
&dst_path,
oxigdal::DatasetFormat::Shapefile,
ConversionOptions::default(),
)
.expect("convert GeoJSON→Shapefile");
assert_eq!(dst_ds.format(), oxigdal::DatasetFormat::Shapefile);
assert!(dst_path.exists(), "output Shapefile should exist on disk");
let reopened =
oxigdal::Dataset::open(dst_path.to_str().expect("dst path")).expect("reopen shp");
let info = reopened.info();
assert_eq!(
info.feature_count,
Some(2),
"Shapefile produced from 2-feature GeoJSON should report feature_count=2, got {:?}",
info.feature_count,
);
}
#[cfg(feature = "shapefile")]
#[test]
fn test_info_shapefile_populated() {
use oxigdal_core::vector::{Coordinate, Feature as CoreFeature, FieldValue, Geometry, Point};
use oxigdal_shapefile::{
ShapeType, ShapefileWriter,
dbf::{FieldDescriptor, FieldType},
};
let dir = std::env::temp_dir();
let base = dir.join("test_info_shapefile");
let field = FieldDescriptor::new("name".to_string(), FieldType::Character, 20, 0)
.expect("field descriptor");
let mut writer = ShapefileWriter::new(&base, ShapeType::Point, vec![field])
.expect("create shapefile writer");
let mut f1 = CoreFeature::new(Geometry::Point(Point::from_coord(Coordinate {
x: 10.0,
y: 20.0,
z: None,
m: None,
})));
f1.set_property("name", FieldValue::String("A".to_string()));
let mut f2 = CoreFeature::new(Geometry::Point(Point::from_coord(Coordinate {
x: 30.0,
y: 40.0,
z: None,
m: None,
})));
f2.set_property("name", FieldValue::String("B".to_string()));
writer
.write_oxigdal_features(&[f1, f2])
.expect("write features");
let shp_path = base.with_extension("shp");
let ds = oxigdal::Dataset::open(shp_path.to_str().expect("path")).expect("open shp");
let info = ds.info();
assert_eq!(
info.feature_count,
Some(2),
"shapefile with 2 features should report feature_count=2"
);
assert!(
info.bounds.is_some(),
"shapefile bounds should be populated"
);
let b = info.bounds.expect("bounds");
assert!((b.min_x - 10.0).abs() < 1e-6, "min_x should be 10.0");
assert!((b.max_x - 30.0).abs() < 1e-6, "max_x should be 30.0");
}
#[cfg(feature = "geoparquet")]
#[test]
fn test_info_geoparquet_populated() {
use oxigdal_geoparquet::geometry::{Geometry as ParquetGeom, Point as ParquetPoint};
use oxigdal_geoparquet::{GeoParquetWriter, GeometryColumnMetadata};
let dir = std::env::temp_dir();
let path = dir.join("test_info_geoparquet.parquet");
{
let meta = GeometryColumnMetadata::new_wkb();
let mut writer =
GeoParquetWriter::new(&path, "geometry", meta).expect("create parquet writer");
for i in 0u8..3 {
let point = ParquetGeom::Point(ParquetPoint::new_2d(i as f64, i as f64));
writer.add_geometry(&point).expect("add geometry");
}
writer.finish().expect("finish writer");
}
let ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open parquet");
let info = ds.info();
assert_eq!(
info.feature_count,
Some(3),
"GeoParquet with 3 rows should report feature_count=3, got {:?}",
info.feature_count
);
}
#[cfg(feature = "flatgeobuf")]
#[test]
fn test_info_flatgeobuf_populated() {
use oxigdal_core::vector::{Coordinate, Feature as CoreFeature, FieldValue, Geometry, Point};
use oxigdal_flatgeobuf::{FlatGeobufWriterBuilder, GeometryType as FgbGeomType};
let dir = std::env::temp_dir();
let path = dir.join("test_info_flatgeobuf.fgb");
{
let file = std::fs::File::create(&path).expect("create fgb");
let mut writer = FlatGeobufWriterBuilder::new(FgbGeomType::Point)
.with_index()
.build(std::io::BufWriter::new(file))
.expect("create writer");
let mut f1 = CoreFeature::new(Geometry::Point(Point::from_coord(Coordinate {
x: 5.0,
y: 15.0,
z: None,
m: None,
})));
f1.set_property("id", FieldValue::Integer(1));
let mut f2 = CoreFeature::new(Geometry::Point(Point::from_coord(Coordinate {
x: 25.0,
y: 35.0,
z: None,
m: None,
})));
f2.set_property("id", FieldValue::Integer(2));
writer.add_feature(&f1).expect("add feature 1");
writer.add_feature(&f2).expect("add feature 2");
writer.finish().expect("finish writer");
}
let ds = oxigdal::Dataset::open(path.to_str().expect("path")).expect("open fgb");
let info = ds.info();
assert_eq!(
info.feature_count,
Some(2),
"FlatGeobuf with 2 features should report feature_count=2, got {:?}",
info.feature_count
);
assert!(
info.bounds.is_some(),
"FlatGeobuf bounds should be populated"
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_build_vrt_single_source() {
use oxigdal::vrt_builder::VrtOptions;
let src_path = write_synthetic_tiff("test_vrt_single_src.tif");
let vrt_path = std::env::temp_dir().join("test_vrt_single.vrt");
let ds = oxigdal::Dataset::build_vrt(&[src_path.as_path()], &vrt_path, VrtOptions::default())
.expect("build_vrt");
assert_eq!(ds.format(), oxigdal::DatasetFormat::Vrt);
assert!(vrt_path.exists(), "VRT file should exist on disk");
assert!(ds.width() > 0, "VRT width should be non-zero");
assert!(ds.height() > 0, "VRT height should be non-zero");
let xml = std::fs::read_to_string(&vrt_path).expect("read vrt");
assert!(
xml.contains("<VRTDataset"),
"VRT file should have VRTDataset element"
);
assert!(
xml.contains("VRTRasterBand"),
"VRT should have VRTRasterBand"
);
}
#[cfg(feature = "geotiff")]
#[test]
fn test_build_vrt_two_tiffs_union_extent() {
use oxigdal::core_types::types::{GeoTransform, NoDataValue, RasterDataType};
use oxigdal::geotiff::{
GeoTiffWriter, GeoTiffWriterOptions, OverviewResampling, WriterConfig,
tiff::{Compression, PhotometricInterpretation, Predictor},
};
use oxigdal::vrt_builder::VrtOptions;
let dir = std::env::temp_dir();
let config_a = WriterConfig {
width: 8,
height: 8,
band_count: 1,
data_type: RasterDataType::Float32,
compression: Compression::None,
predictor: Predictor::None,
tile_width: None,
tile_height: None,
photometric: PhotometricInterpretation::BlackIsZero,
geo_transform: Some(GeoTransform::north_up(0.0, 8.0, 1.0, 1.0)),
epsg_code: Some(4326),
nodata: NoDataValue::None,
use_bigtiff: false,
generate_overviews: false,
overview_resampling: OverviewResampling::Average,
overview_levels: vec![],
};
let config_b = WriterConfig {
geo_transform: Some(GeoTransform::north_up(8.0, 8.0, 1.0, 1.0)),
..config_a.clone()
};
let path_a = dir.join("test_vrt_union_a.tif");
let path_b = dir.join("test_vrt_union_b.tif");
let vrt_path = dir.join("test_vrt_union.vrt");
let pixel_data = vec![0u8; 8 * 8 * 4];
let mut w_a = GeoTiffWriter::create(&path_a, config_a, GeoTiffWriterOptions::default())
.expect("create a");
w_a.write(&pixel_data).expect("write a");
let mut w_b = GeoTiffWriter::create(&path_b, config_b, GeoTiffWriterOptions::default())
.expect("create b");
w_b.write(&pixel_data).expect("write b");
let ds = oxigdal::Dataset::build_vrt(
&[path_a.as_path(), path_b.as_path()],
&vrt_path,
VrtOptions::default(),
)
.expect("build_vrt");
assert!(ds.width() >= 8, "union width >= 8");
assert!(ds.height() >= 8, "union height >= 8");
let xml = std::fs::read_to_string(&vrt_path).expect("read vrt");
assert!(
xml.contains("SourceFilename"),
"VRT should reference source files"
);
let src_a = path_a.file_name().expect("a filename").to_string_lossy();
let src_b = path_b.file_name().expect("b filename").to_string_lossy();
assert!(
xml.contains(src_a.as_ref()),
"VRT should reference source file A"
);
assert!(
xml.contains(src_b.as_ref()),
"VRT should reference source file B"
);
}
#[test]
fn test_build_vrt_empty_sources_errors() {
use oxigdal::vrt_builder::VrtOptions;
let vrt_path = std::env::temp_dir().join("test_vrt_empty.vrt");
let result = oxigdal::Dataset::build_vrt(&[], &vrt_path, VrtOptions::default());
assert!(
result.is_err(),
"build_vrt with empty sources should return Err"
);
}
#[cfg(feature = "gdal-compat")]
#[test]
fn test_gdal_compat_version_and_register() {
oxigdal::gdal_compat::GDALAllRegister();
let v = oxigdal::gdal_compat::GDALVersionInfo();
assert!(!v.is_empty());
}
#[cfg(feature = "gdal-compat")]
#[test]
fn test_gdal_compat_open_nonexistent_errors() {
let result = oxigdal::gdal_compat::GDALOpen("/nonexistent/path/file.tif");
assert!(
result.is_err(),
"GDALOpen on nonexistent path should return Err"
);
}