1use anyhow::{Context, Result};
10use oxigeo_core::io::FileDataSource;
11use oxigeo_core::types::RasterDataType;
12use oxigeo_geojson::GeoJsonReader;
13use oxigeo_geotiff::GeoTiffReader;
14use serde::Serialize;
15use std::fs::File;
16use std::io::BufReader;
17use std::path::Path;
18
19#[derive(Debug, Clone, Serialize)]
21pub struct InspectionReport {
22 pub path: String,
24 pub file_size: u64,
26 pub format: String,
28 pub extension: String,
30 pub is_cloud: bool,
32 pub crs: Option<String>,
34 pub raster: Option<RasterSummary>,
36 pub vector: Option<VectorSummary>,
38}
39
40#[derive(Debug, Clone, Serialize)]
42pub struct RasterSummary {
43 pub width: u32,
45 pub height: u32,
47 pub band_count: u32,
49 pub data_types: Vec<String>,
51 pub geo_transform: Option<[f64; 6]>,
53 pub nodata: Option<f64>,
55}
56
57#[derive(Debug, Clone, Serialize)]
59pub struct VectorSummary {
60 pub feature_count: Option<u64>,
62 pub layer_count: u32,
64 pub bounds: Option<[f64; 4]>,
66}
67
68pub fn inspect_file(path: &str, detailed: bool) -> Result<InspectionReport> {
82 let is_cloud = crate::util::cloud::is_cloud_uri(path);
83
84 let resolved: &str = path.strip_prefix("file://").unwrap_or(path);
86 let resolved_path = Path::new(resolved);
87
88 let file_size = if is_cloud {
90 0
91 } else {
92 if !resolved_path.exists() {
93 anyhow::bail!("File not found: {}", resolved);
94 }
95 std::fs::metadata(resolved_path)
96 .with_context(|| format!("Failed to read file metadata: {}", resolved))?
97 .len()
98 };
99
100 let extension = resolved_path
102 .extension()
103 .and_then(|ext| ext.to_str())
104 .map(|ext| ext.to_lowercase())
105 .unwrap_or_default();
106
107 let format = crate::util::detect_format(resolved_path)
109 .map(|f| f.to_string())
110 .unwrap_or_else(|| "Unknown".to_string());
111
112 let mut report = InspectionReport {
113 path: path.to_string(),
114 file_size,
115 format: format.clone(),
116 extension,
117 is_cloud,
118 crs: None,
119 raster: None,
120 vector: None,
121 };
122
123 if is_cloud || format == "Unknown" {
126 return Ok(report);
127 }
128
129 match format.as_str() {
130 "GeoTIFF" => {
131 let (summary, crs) = inspect_geotiff(resolved_path, detailed)?;
132 report.crs = crs;
133 report.raster = Some(summary);
134 }
135 "GeoJSON" => {
136 let (summary, crs) = inspect_geojson(resolved_path, detailed)?;
137 report.crs = crs;
138 report.vector = Some(summary);
139 }
140 _ => {}
144 }
145
146 Ok(report)
147}
148
149fn inspect_geotiff(path: &Path, detailed: bool) -> Result<(RasterSummary, Option<String>)> {
151 let source = FileDataSource::open(path)
152 .map_err(|e| anyhow::anyhow!("Failed to open file {}: {e}", path.display()))?;
153 let reader = GeoTiffReader::open(source)
154 .map_err(|e| anyhow::anyhow!("Failed to read GeoTIFF {}: {e}", path.display()))?;
155
156 let width = u32::try_from(reader.width()).unwrap_or(u32::MAX);
157 let height = u32::try_from(reader.height()).unwrap_or(u32::MAX);
158 let band_count = reader.band_count();
159
160 let data_types = if detailed {
162 let type_name = reader
163 .data_type()
164 .map_or_else(|| "Unknown".to_string(), data_type_name);
165 vec![type_name; band_count as usize]
166 } else {
167 Vec::new()
168 };
169
170 let geo_transform = if detailed {
172 reader.geo_transform().map(|gt| {
173 [
174 gt.origin_x,
175 gt.pixel_width,
176 gt.row_rotation,
177 gt.origin_y,
178 gt.col_rotation,
179 gt.pixel_height,
180 ]
181 })
182 } else {
183 None
184 };
185
186 let nodata = if detailed {
187 reader.nodata().as_f64()
188 } else {
189 None
190 };
191
192 let crs = reader.epsg_code().map(|code| format!("EPSG:{code}"));
194
195 Ok((
196 RasterSummary {
197 width,
198 height,
199 band_count,
200 data_types,
201 geo_transform,
202 nodata,
203 },
204 crs,
205 ))
206}
207
208fn inspect_geojson(path: &Path, detailed: bool) -> Result<(VectorSummary, Option<String>)> {
210 let file =
211 File::open(path).with_context(|| format!("Failed to open file: {}", path.display()))?;
212 let mut reader = GeoJsonReader::new(BufReader::new(file));
213 let collection = reader
214 .read_feature_collection()
215 .map_err(|e| anyhow::anyhow!("Failed to read GeoJSON {}: {e}", path.display()))?;
216
217 let feature_count = Some(collection.features.len() as u64);
218
219 let layer_count = 1;
223
224 let bounds = collection.bbox.as_ref().and_then(|bbox| {
226 if bbox.len() >= 4 {
227 Some([bbox[0], bbox[1], bbox[2], bbox[3]])
228 } else {
229 None
230 }
231 });
232
233 let _ = detailed;
235 let crs = collection.crs.as_ref().and_then(|c| c.name());
236
237 Ok((
238 VectorSummary {
239 feature_count,
240 layer_count,
241 bounds,
242 },
243 crs,
244 ))
245}
246
247fn data_type_name(dt: RasterDataType) -> String {
249 match dt {
250 RasterDataType::UInt8 => "UInt8",
251 RasterDataType::UInt16 => "UInt16",
252 RasterDataType::UInt32 => "UInt32",
253 RasterDataType::UInt64 => "UInt64",
254 RasterDataType::Int8 => "Int8",
255 RasterDataType::Int16 => "Int16",
256 RasterDataType::Int32 => "Int32",
257 RasterDataType::Int64 => "Int64",
258 RasterDataType::Float32 => "Float32",
259 RasterDataType::Float64 => "Float64",
260 RasterDataType::CFloat32 => "CFloat32",
261 RasterDataType::CFloat64 => "CFloat64",
262 }
263 .to_string()
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_data_type_name() {
272 assert_eq!(data_type_name(RasterDataType::UInt8), "UInt8");
273 assert_eq!(data_type_name(RasterDataType::Float64), "Float64");
274 assert_eq!(data_type_name(RasterDataType::CFloat64), "CFloat64");
275 }
276
277 #[test]
278 fn test_inspect_unknown_extension_no_summary() -> Result<()> {
279 let dir = std::env::temp_dir();
280 let path = dir.join(format!(
281 "oxigeo_inspector_unit_{}_{}.xyz",
282 std::process::id(),
283 "unknown"
284 ));
285 std::fs::write(&path, b"not a geospatial file")?;
286
287 let report = inspect_file(path.to_string_lossy().as_ref(), false)?;
288 assert_eq!(report.format, "Unknown");
289 assert!(report.raster.is_none());
290 assert!(report.vector.is_none());
291 assert!(!report.is_cloud);
292
293 let _ = std::fs::remove_file(&path);
294 Ok(())
295 }
296}