use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write};
use std::path::Path;
use crate::error::{Result, RasterError};
use crate::io_utils::*;
use crate::raster::{DataType, Raster, RasterConfig};
pub fn read(path: &str) -> Result<Raster> {
let grid_dir = resolve_grid_dir(path)?;
read_from_dir(&grid_dir)
}
pub fn write(raster: &Raster, path: &str) -> Result<()> {
let dir = if path.ends_with(".adf") {
Path::new(path).parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string())
} else {
path.trim_end_matches('/').to_string()
};
if raster.bands != 1 {
return Err(RasterError::UnsupportedDataType(
"Esri Binary Grid writer currently supports single-band rasters only".into(),
));
}
write_to_dir(raster, &dir)
}
const HDR_ADF_MAGIC: [u8; 8] = [0x00, 0x00, 0x27, 0x0A, 0xFF, 0xFF, 0xFB, 0xF8];
const DATA_MAGIC: [u8; 8] = [0x00, 0x00, 0x27, 0x0A, 0xFF, 0xFF, 0xFB, 0xF8];
fn resolve_grid_dir(path: &str) -> Result<String> {
let p = Path::new(path);
if p.is_dir() {
return Ok(path.trim_end_matches('/').to_string());
}
if p.is_file() {
return Ok(p.parent()
.map(|d| d.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string()));
}
if Path::new(path).exists() {
return Ok(path.to_string());
}
Err(RasterError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("grid directory not found: {path}"),
)))
}
fn read_from_dir(dir: &str) -> Result<Raster> {
let hdr_path = format!("{dir}/hdr.adf");
let hdr = read_hdr_adf(&hdr_path)?;
let (x_min, y_min, x_max, y_max) = read_dblbnd(dir)?;
let cell_size_x = (x_max - x_min) / hdr.cols as f64;
let cell_size_y = (y_max - y_min) / hdr.rows as f64;
let crs = read_prj_adf(dir);
let tile_path = format!("{dir}/w001001.adf");
let data = read_tile_data(&tile_path, hdr.cols, hdr.rows, hdr.nodata)?;
let cfg = RasterConfig {
cols: hdr.cols,
rows: hdr.rows,
x_min,
y_min,
cell_size: cell_size_x,
cell_size_y: Some(cell_size_y),
nodata: hdr.nodata,
data_type: DataType::F32,
crs: crs, ..Default::default()
};
Raster::from_data(cfg, data)
}
struct HdrAdf {
cols: usize,
rows: usize,
nodata: f64,
}
fn read_hdr_adf(path: &str) -> Result<HdrAdf> {
let buf = fs::read(path)?;
if buf.len() < 50 {
return Err(RasterError::CorruptData(
format!("hdr.adf too short ({} bytes)", buf.len())
));
}
let cols = read_i32_be(&buf, 16) as usize;
let rows = read_i32_be(&buf, 20) as usize;
let nodata_present = buf.get(48).copied().unwrap_or(0);
let nodata = if nodata_present != 0 {
read_f64_be(&buf, 40)
} else {
-9999.0
};
Ok(HdrAdf { cols, rows, nodata })
}
fn read_dblbnd(dir: &str) -> Result<(f64, f64, f64, f64)> {
let path = format!("{dir}/dblbnd.adf");
let buf = fs::read(&path)?;
if buf.len() < 32 {
return Err(RasterError::CorruptData(
format!("dblbnd.adf too short ({} bytes)", buf.len())
));
}
let x_min = read_f64_be(&buf, 0);
let y_min = read_f64_be(&buf, 8);
let x_max = read_f64_be(&buf, 16);
let y_max = read_f64_be(&buf, 24);
Ok((x_min, y_min, x_max, y_max))
}
fn read_prj_adf(dir: &str) -> crate::crs_info::CrsInfo {
use crate::crs_info::CrsInfo;
let path = format!("{dir}/prj.adf");
match fs::read_to_string(&path) {
Ok(wkt) if !wkt.trim().is_empty() => CrsInfo::from_wkt(wkt.trim()),
_ => CrsInfo::default(),
}
}
fn read_tile_data(path: &str, cols: usize, rows: usize, nodata: f64) -> Result<Vec<f64>> {
let mut file = BufReader::with_capacity(512 * 1024, File::open(path)?);
let expected_bytes = cols * rows * 4;
let file_meta = fs::metadata(path)?;
let file_size = file_meta.len() as usize;
let data_offset = if file_size == expected_bytes { 0 }
else if file_size == expected_bytes + 8 { 8 }
else if file_size >= expected_bytes + 28 { 28 }
else if file_size > expected_bytes { file_size - expected_bytes }
else {
return Err(RasterError::CorruptData(format!(
"tile data file {path} is {} bytes; expected at least {expected_bytes}",
file_size
)));
};
file.seek(SeekFrom::Start(data_offset as u64))?;
let n = cols * rows;
let mut data = Vec::with_capacity(n);
for _ in 0..n {
let v = read_f32_be_stream(&mut file)? as f64;
let v = if (v - 1.175_494_e-38_f32 as f64).abs() < 1e-40 { nodata } else { v };
data.push(v);
}
Ok(data)
}
fn write_to_dir(raster: &Raster, dir: &str) -> Result<()> {
fs::create_dir_all(dir)?;
write_hdr_adf(raster, dir)?;
write_dblbnd(raster, dir)?;
write_tile_data(raster, dir)?;
write_tile_index(raster, dir)?;
write_sta_adf(raster, dir)?;
if !raster.crs.is_unknown() {
write_prj_adf(raster, dir)?;
}
Ok(())
}
fn write_hdr_adf(raster: &Raster, dir: &str) -> Result<()> {
let path = format!("{dir}/hdr.adf");
let mut w = BufWriter::new(File::create(&path)?);
w.write_all(&HDR_ADF_MAGIC)?;
w.write_all(&[0u8; 8])?;
w.write_all(&(raster.cols as i32).to_be_bytes())?;
w.write_all(&(raster.rows as i32).to_be_bytes())?;
w.write_all(&(raster.cols as i32).to_be_bytes())?;
w.write_all(&(raster.rows as i32).to_be_bytes())?;
w.write_all(&[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])?;
w.write_all(&raster.nodata.to_be_bytes())?;
w.write_all(&[0x01])?;
w.write_all(&[0x00])?;
w.flush()?;
Ok(())
}
fn write_dblbnd(raster: &Raster, dir: &str) -> Result<()> {
let path = format!("{dir}/dblbnd.adf");
let mut w = BufWriter::new(File::create(&path)?);
w.write_all(&raster.x_min.to_be_bytes())?;
w.write_all(&raster.y_min.to_be_bytes())?;
w.write_all(&raster.x_max().to_be_bytes())?;
w.write_all(&raster.y_max().to_be_bytes())?;
w.flush()?;
Ok(())
}
fn write_tile_data(raster: &Raster, dir: &str) -> Result<()> {
let path = format!("{dir}/w001001.adf");
let mut w = BufWriter::with_capacity(512 * 1024, File::create(&path)?);
w.write_all(&DATA_MAGIC)?;
for v in raster.data.iter_f64() {
let f = v as f32;
w.write_all(&f.to_be_bytes())?;
}
w.flush()?;
Ok(())
}
fn write_tile_index(raster: &Raster, dir: &str) -> Result<()> {
let path = format!("{dir}/w001001x.adf");
let mut w = BufWriter::new(File::create(&path)?);
let offset: u32 = 8; let size: u32 = (raster.cols * raster.rows * 4) as u32;
w.write_all(&offset.to_be_bytes())?;
w.write_all(&size.to_be_bytes())?;
w.flush()?;
Ok(())
}
fn write_sta_adf(raster: &Raster, dir: &str) -> Result<()> {
let path = format!("{dir}/sta.adf");
let mut w = BufWriter::new(File::create(&path)?);
let stats = raster.statistics();
w.write_all(&stats.min.to_be_bytes())?;
w.write_all(&stats.max.to_be_bytes())?;
w.write_all(&stats.mean.to_be_bytes())?;
w.write_all(&stats.std_dev.to_be_bytes())?;
for _ in 0..4 {
w.write_all(&0.0_f64.to_be_bytes())?;
}
w.flush()?;
Ok(())
}
fn write_prj_adf(raster: &Raster, dir: &str) -> Result<()> {
if let Some(ref wkt) = raster.crs.wkt {
let path = format!("{dir}/prj.adf");
fs::write(&path, wkt.as_bytes())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::raster::RasterConfig;
use tempfile_helper::TempDir;
mod tempfile_helper {
use std::path::PathBuf;
pub struct TempDir(pub PathBuf);
impl TempDir {
pub fn new() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos();
let p = std::env::temp_dir().join(format!("gis_raster_test_{ts}"));
std::fs::create_dir_all(&p).unwrap();
TempDir(p)
}
pub fn path(&self) -> &str { self.0.to_str().unwrap() }
}
impl Drop for TempDir {
fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); }
}
}
#[test]
fn roundtrip_esri_binary() {
let td = TempDir::new();
let dir = format!("{}/testgrid", td.path());
let cfg = RasterConfig {
cols: 3, rows: 2,
x_min: 0.0, y_min: 0.0,
cell_size: 10.0, nodata: -9999.0,
..Default::default()
};
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let r = Raster::from_data(cfg, data).unwrap();
write_to_dir(&r, &dir).unwrap();
let r2 = read_from_dir(&dir).unwrap();
assert_eq!(r2.cols, 3);
assert_eq!(r2.rows, 2);
assert!((r2.get(0, 0, 0) - 1.0).abs() < 1e-4);
assert!((r2.get(0, 1, 2) - 6.0).abs() < 1e-4);
}
}