use anyhow::{Context, Result};
use oxigdal_core::io::FileDataSource;
use oxigdal_core::types::RasterDataType;
use oxigdal_geojson::GeoJsonReader;
use oxigdal_geotiff::GeoTiffReader;
use serde::Serialize;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
#[derive(Debug, Clone, Serialize)]
pub struct InspectionReport {
pub path: String,
pub file_size: u64,
pub format: String,
pub extension: String,
pub is_cloud: bool,
pub crs: Option<String>,
pub raster: Option<RasterSummary>,
pub vector: Option<VectorSummary>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RasterSummary {
pub width: u32,
pub height: u32,
pub band_count: u32,
pub data_types: Vec<String>,
pub geo_transform: Option<[f64; 6]>,
pub nodata: Option<f64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct VectorSummary {
pub feature_count: Option<u64>,
pub layer_count: u32,
pub bounds: Option<[f64; 4]>,
}
pub fn inspect_file(path: &str, detailed: bool) -> Result<InspectionReport> {
let is_cloud = crate::util::cloud::is_cloud_uri(path);
let resolved: &str = path.strip_prefix("file://").unwrap_or(path);
let resolved_path = Path::new(resolved);
let file_size = if is_cloud {
0
} else {
if !resolved_path.exists() {
anyhow::bail!("File not found: {}", resolved);
}
std::fs::metadata(resolved_path)
.with_context(|| format!("Failed to read file metadata: {}", resolved))?
.len()
};
let extension = resolved_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
.unwrap_or_default();
let format = crate::util::detect_format(resolved_path)
.map(|f| f.to_string())
.unwrap_or_else(|| "Unknown".to_string());
let mut report = InspectionReport {
path: path.to_string(),
file_size,
format: format.clone(),
extension,
is_cloud,
crs: None,
raster: None,
vector: None,
};
if is_cloud || format == "Unknown" {
return Ok(report);
}
match format.as_str() {
"GeoTIFF" => {
let (summary, crs) = inspect_geotiff(resolved_path, detailed)?;
report.crs = crs;
report.raster = Some(summary);
}
"GeoJSON" => {
let (summary, crs) = inspect_geojson(resolved_path, detailed)?;
report.crs = crs;
report.vector = Some(summary);
}
_ => {}
}
Ok(report)
}
fn inspect_geotiff(path: &Path, detailed: bool) -> Result<(RasterSummary, Option<String>)> {
let source = FileDataSource::open(path)
.map_err(|e| anyhow::anyhow!("Failed to open file {}: {e}", path.display()))?;
let reader = GeoTiffReader::open(source)
.map_err(|e| anyhow::anyhow!("Failed to read GeoTIFF {}: {e}", path.display()))?;
let width = u32::try_from(reader.width()).unwrap_or(u32::MAX);
let height = u32::try_from(reader.height()).unwrap_or(u32::MAX);
let band_count = reader.band_count();
let data_types = if detailed {
let type_name = reader
.data_type()
.map_or_else(|| "Unknown".to_string(), data_type_name);
vec![type_name; band_count as usize]
} else {
Vec::new()
};
let geo_transform = if detailed {
reader.geo_transform().map(|gt| {
[
gt.origin_x,
gt.pixel_width,
gt.row_rotation,
gt.origin_y,
gt.col_rotation,
gt.pixel_height,
]
})
} else {
None
};
let nodata = if detailed {
reader.nodata().as_f64()
} else {
None
};
let crs = reader.epsg_code().map(|code| format!("EPSG:{code}"));
Ok((
RasterSummary {
width,
height,
band_count,
data_types,
geo_transform,
nodata,
},
crs,
))
}
fn inspect_geojson(path: &Path, detailed: bool) -> Result<(VectorSummary, Option<String>)> {
let file =
File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
let mut reader = GeoJsonReader::new(BufReader::new(file));
let collection = reader
.read_feature_collection()
.map_err(|e| anyhow::anyhow!("Failed to read GeoJSON {}: {e}", path.display()))?;
let feature_count = Some(collection.features.len() as u64);
let layer_count = 1;
let bounds = collection.bbox.as_ref().and_then(|bbox| {
if bbox.len() >= 4 {
Some([bbox[0], bbox[1], bbox[2], bbox[3]])
} else {
None
}
});
let _ = detailed;
let crs = collection.crs.as_ref().and_then(|c| c.name());
Ok((
VectorSummary {
feature_count,
layer_count,
bounds,
},
crs,
))
}
fn data_type_name(dt: RasterDataType) -> String {
match dt {
RasterDataType::UInt8 => "UInt8",
RasterDataType::UInt16 => "UInt16",
RasterDataType::UInt32 => "UInt32",
RasterDataType::UInt64 => "UInt64",
RasterDataType::Int8 => "Int8",
RasterDataType::Int16 => "Int16",
RasterDataType::Int32 => "Int32",
RasterDataType::Int64 => "Int64",
RasterDataType::Float32 => "Float32",
RasterDataType::Float64 => "Float64",
RasterDataType::CFloat32 => "CFloat32",
RasterDataType::CFloat64 => "CFloat64",
}
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_type_name() {
assert_eq!(data_type_name(RasterDataType::UInt8), "UInt8");
assert_eq!(data_type_name(RasterDataType::Float64), "Float64");
assert_eq!(data_type_name(RasterDataType::CFloat64), "CFloat64");
}
#[test]
fn test_inspect_unknown_extension_no_summary() -> Result<()> {
let dir = std::env::temp_dir();
let path = dir.join(format!(
"oxigdal_inspector_unit_{}_{}.xyz",
std::process::id(),
"unknown"
));
std::fs::write(&path, b"not a geospatial file")?;
let report = inspect_file(path.to_string_lossy().as_ref(), false)?;
assert_eq!(report.format, "Unknown");
assert!(report.raster.is_none());
assert!(report.vector.is_none());
assert!(!report.is_cloud);
let _ = std::fs::remove_file(&path);
Ok(())
}
}