use crate::OutputFormat;
use crate::util::{progress, raster};
use anyhow::{Context, Result};
use clap::Args;
use std::path::PathBuf;
#[derive(Args, Debug)]
pub struct ContourArgs {
#[arg(value_name = "INPUT")]
input: PathBuf,
#[arg(value_name = "OUTPUT")]
output: PathBuf,
#[arg(short, long)]
interval: Option<f64>,
#[arg(long = "fl", value_delimiter = ',')]
fixed_levels: Option<Vec<f64>>,
#[arg(short, long, default_value = "0.0")]
base: f64,
#[arg(short, long, default_value = "elev")]
attribute: String,
#[arg(long)]
min_elev: Option<f64>,
#[arg(long)]
max_elev: Option<f64>,
#[arg(long)]
no_data: Option<f64>,
#[arg(long)]
overwrite: bool,
}
#[allow(dead_code)]
struct ContourResult {
input_file: String,
output_file: String,
contour_count: usize,
levels_generated: usize,
processing_time_ms: u128,
}
pub fn execute(args: ContourArgs, _format: OutputFormat) -> Result<()> {
let _start = std::time::Instant::now();
if !args.input.exists() {
anyhow::bail!("Input file not found: {}", args.input.display());
}
if args.output.exists() && !args.overwrite {
anyhow::bail!(
"Output file already exists: {}. Use --overwrite to replace.",
args.output.display()
);
}
if args.interval.is_none() && args.fixed_levels.is_none() {
anyhow::bail!("Must specify either --interval or --fixed-levels");
}
let pb = progress::create_spinner("Reading DEM");
let raster_info =
raster::read_raster_info(&args.input).context("Failed to read DEM metadata")?;
let dem_data =
raster::read_band_region(&args.input, 0, 0, 0, raster_info.width, raster_info.height)
.context("Failed to read DEM data")?;
pb.finish_and_clear();
let levels = if let Some(ref fixed) = args.fixed_levels {
fixed.clone()
} else if let Some(interval) = args.interval {
let dem_values = raster_buffer_to_f64(&dem_data)?;
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for &val in &dem_values {
if let Some(nd) = args.no_data {
if (val - nd).abs() < f64::EPSILON {
continue;
}
}
min_val = min_val.min(val);
max_val = max_val.max(val);
}
let min_elev = args.min_elev.unwrap_or(min_val);
let max_elev = args.max_elev.unwrap_or(max_val);
let mut levels = Vec::new();
let mut level = ((min_elev - args.base) / interval).ceil() * interval + args.base;
while level <= max_elev {
levels.push(level);
level += interval;
}
levels
} else {
unreachable!();
};
if levels.is_empty() {
anyhow::bail!("No contour levels to generate");
}
let _pb = progress::create_progress_bar(levels.len() as u64, "Generating contours");
let _features: Vec<()> = Vec::new();
anyhow::bail!("Contour generation is not yet implemented. This feature is coming soon!")
}
#[allow(dead_code)]
fn pixel_to_geo(px: f64, py: f64, gt: &oxigdal_core::types::GeoTransform) -> (f64, f64) {
let x = gt.origin_x + px * gt.pixel_width + py * gt.row_rotation;
let y = gt.origin_y + px * gt.col_rotation + py * gt.pixel_height;
(x, y)
}
fn raster_buffer_to_f64(buffer: &oxigdal_core::buffer::RasterBuffer) -> Result<Vec<f64>> {
let data = buffer.as_bytes();
let data_type = buffer.data_type();
let pixel_count = (buffer.width() * buffer.height()) as usize;
let mut values = Vec::with_capacity(pixel_count);
match data_type {
oxigdal_core::types::RasterDataType::UInt8 => {
for &byte in data {
values.push(byte as f64);
}
}
oxigdal_core::types::RasterDataType::Float64 => {
for chunk in data.chunks_exact(8) {
let value = f64::from_ne_bytes([
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
]);
values.push(value);
}
}
oxigdal_core::types::RasterDataType::Float32 => {
for chunk in data.chunks_exact(4) {
let value = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
values.push(value as f64);
}
}
_ => anyhow::bail!("Unsupported data type for contour: {:?}", data_type),
}
Ok(values)
}
#[cfg(test)]
mod tests {
use super::*;
use oxigdal_core::types::GeoTransform;
#[test]
fn test_pixel_to_geo() {
let gt = GeoTransform {
origin_x: 0.0,
pixel_width: 1.0,
row_rotation: 0.0,
origin_y: 100.0,
col_rotation: 0.0,
pixel_height: -1.0,
};
let (x, y) = pixel_to_geo(10.0, 20.0, >);
assert_eq!(x, 10.0);
assert_eq!(y, 80.0);
}
}