use anyhow::Result;
use clap::Parser;
use rayon::ThreadPoolBuilder;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{error, info};
use tracing_subscriber;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(value_name = "INPUT")]
input: PathBuf,
#[arg(short, long, value_name = "DIR")]
output: PathBuf,
#[arg(short, long)]
threads: Option<usize>,
#[arg(long)]
merge: bool,
#[arg(long)]
terrain_rgb: bool,
#[arg(long)]
min_elevation: Option<f32>,
#[arg(long)]
max_elevation: Option<f32>,
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
let start_time = std::time::Instant::now();
if let Some(threads) = args.threads {
ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
.expect("Failed to build thread pool");
}
fs::create_dir_all(&args.output)?;
if args.input.is_file() {
let ext = args
.input
.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
match ext {
"zip" => {
info!("Processing ZIP file: {:?}", args.input);
process_zip_file(&args.input, &args)?;
}
"xml" => {
info!("Processing XML file: {:?}", args.input);
process_file(&args.input, &args)?;
}
_ => {
error!("Unsupported file type: {:?}", args.input);
anyhow::bail!("Input file must be .xml or .zip");
}
}
} else if args.input.is_dir() {
info!("Processing directory: {:?}", args.input);
process_directory(&args.input, &args)?;
} else {
error!("Invalid input path: {:?}", args.input);
anyhow::bail!("Input path must be a file or directory");
}
let elapsed = start_time.elapsed();
info!("Total processing time: {:?}", elapsed);
Ok(())
}
fn process_file(path: &Path, args: &Args) -> Result<()> {
info!("Processing file: {:?}", path);
use japan_dem::parser::parse_dem_xml;
use japan_dem::writer::GeoTiffWriter;
use japan_dem::TerrainRgbConfig;
use std::fs::File;
use std::io::BufReader;
let file = File::open(path)?;
let reader = BufReader::new(file);
let dem_tile = parse_dem_xml(reader)?;
info!(
"Parsed successfully: {} ({}x{})",
dem_tile.metadata.meshcode, dem_tile.rows, dem_tile.cols
);
if args.terrain_rgb {
let output_filename = format!("{}_terrain_rgb.tif", dem_tile.metadata.meshcode);
let output_path = args.output.join(&output_filename);
let config = TerrainRgbConfig {
min_elevation: args.min_elevation,
max_elevation: args.max_elevation,
};
let writer = GeoTiffWriter::new();
writer.write_terrain_rgb(&dem_tile, &output_path, &config)?;
info!("Written Terrain-RGB: {:?}", output_path);
} else {
let output_filename = format!("{}.tif", dem_tile.metadata.meshcode);
let output_path = args.output.join(&output_filename);
let writer = GeoTiffWriter::new();
writer.write(&dem_tile, &output_path)?;
info!("Written GeoTIFF: {:?}", output_path);
}
Ok(())
}
fn process_directory(dir: &Path, args: &Args) -> Result<()> {
use rayon::prelude::*;
let input_files = collect_input_files(dir)?;
info!("Found {} input files (XML/ZIP)", input_files.len());
let results: Vec<Result<()>> = input_files
.par_iter()
.map(|(path, file_type)| match file_type {
FileType::Xml => process_file(path, args),
FileType::Zip => process_zip_file(path, args),
})
.collect();
let mut errors = Vec::new();
for (i, result) in results.into_iter().enumerate() {
if let Err(e) = result {
errors.push(format!("{}: {}", input_files[i].0.display(), e));
}
}
if !errors.is_empty() {
error!("Failed to process {} files:", errors.len());
for err in &errors {
error!(" {}", err);
}
anyhow::bail!("{} files failed to process", errors.len());
}
Ok(())
}
fn collect_input_files(dir: &Path) -> Result<Vec<(std::path::PathBuf, FileType)>> {
use rayon::prelude::*;
use std::sync::{Arc, Mutex};
let files = Arc::new(Mutex::new(Vec::new()));
let entries: Result<Vec<_>, _> = fs::read_dir(dir)?.collect();
let entries = entries?;
entries.into_par_iter().try_for_each(|entry| -> Result<()> {
let path = entry.path();
if path.is_dir() {
let sub_files = collect_input_files(&path)?;
if !sub_files.is_empty() {
let mut files_guard = files.lock().unwrap();
files_guard.extend(sub_files);
}
} else {
match path.extension().and_then(|s| s.to_str()) {
Some("xml") => {
let mut files_guard = files.lock().unwrap();
files_guard.push((path, FileType::Xml));
},
Some("zip") => {
let mut files_guard = files.lock().unwrap();
files_guard.push((path, FileType::Zip));
},
_ => {}
}
}
Ok(())
})?;
let files = Arc::try_unwrap(files).unwrap().into_inner().unwrap();
Ok(files)
}
#[derive(Debug, Clone, Copy)]
enum FileType {
Xml,
Zip,
}
fn process_zip_file(path: &Path, args: &Args) -> Result<()> {
use japan_dem::writer::GeoTiffWriter;
use japan_dem::{MergedDemTile, TerrainRgbConfig, ZipHandler};
let handler = ZipHandler::new(path);
let tiles = handler.process_all_tiles()?;
if args.merge && tiles.len() > 1 {
info!("Merging {} tiles", tiles.len());
let merged = MergedDemTile::from_tiles(tiles)?;
let dem_tile = merged.to_dem_tile();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("merged");
if args.terrain_rgb {
let output_filename = format!("{}_terrain_rgb.tif", stem);
let output_path = args.output.join(&output_filename);
let config = TerrainRgbConfig {
min_elevation: args.min_elevation,
max_elevation: args.max_elevation,
};
let writer = GeoTiffWriter::new();
writer.write_terrain_rgb(&dem_tile, &output_path, &config)?;
info!("Written merged Terrain-RGB: {:?}", output_path);
} else {
let output_filename = format!("{}.tif", stem);
let output_path = args.output.join(&output_filename);
let writer = GeoTiffWriter::new();
writer.write(&dem_tile, &output_path)?;
info!("Written merged GeoTIFF: {:?}", output_path);
}
} else {
info!("Writing {} tiles individually", tiles.len());
if args.terrain_rgb {
let config = TerrainRgbConfig {
min_elevation: args.min_elevation,
max_elevation: args.max_elevation,
};
for tile in tiles {
let output_filename = format!("{}_terrain_rgb.tif", tile.metadata.meshcode);
let output_path = args.output.join(&output_filename);
let writer = GeoTiffWriter::new();
writer.write_terrain_rgb(&tile, &output_path, &config)?;
info!("Written Terrain-RGB: {:?}", output_path);
}
} else {
let writer = GeoTiffWriter::new();
for tile in tiles {
let output_filename = format!("{}.tif", tile.metadata.meshcode);
let output_path = args.output.join(&output_filename);
writer.write(&tile, &output_path)?;
info!("Written GeoTIFF: {:?}", output_path);
}
}
}
Ok(())
}