use std::{ fs, io::{self, ErrorKind}, path::{Path, PathBuf}, }; use clap::Parser; use color_eyre::eyre::{bail, eyre, Context, Result}; use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use is_terminal::IsTerminal; use rayon::ThreadPoolBuilder; mod gpx; mod layer; mod renderer; use gpx::Compression; #[derive(Parser, Debug, Clone)] #[command(author, version, about)] struct Args { /// The GPX files to parse. #[arg(required = true)] files: Vec, /// Minimum zoom level to generate tiles for. #[arg(long, default_value_t = 0)] min_zoom: u32, /// Maximum zoom level to generate tiles for. #[arg(long, default_value_t = 19)] max_zoom: u32, /// Number of threads to use. Set to 0 to use all available CPU cores. #[arg(long, short, default_value_t = 0)] threads: usize, /// The output directory. Will be created if it does not exist. #[arg(long, short, default_value = "tiles")] output_directory: PathBuf, } fn main() -> Result<()> { color_eyre::install()?; let args = Args::parse(); if args.max_zoom < args.min_zoom { bail!("Max zoom cannot be smaller than min zoom!"); } ThreadPoolBuilder::new() .num_threads(args.threads) .build_global()?; let progress_style = ProgressStyle::with_template("[{elapsed}] {prefix:.cyan} {wide_bar} {pos:.green}/{len}")?; let zoom_style = ProgressStyle::with_template("[{elapsed}] {prefix:.yellow} {wide_bar} {pos:.green}/{len}")?; let use_progress_bars = io::stdout().is_terminal(); let make_bar = |len| { if use_progress_bars { ProgressBar::new(len) } else { ProgressBar::hidden() } }; let bar = make_bar(args.files.len().try_into().unwrap()).with_style(progress_style.clone()); bar.set_prefix("Reading GPX files"); let mut tracks = Vec::new(); for file in &args.files { let compression = Compression::suggest_from_path(file) .ok_or_else(|| eyre!("Could not determine format for {file:?}"))?; let data = gpx::extract_from_file(file, compression)?; tracks.push(data); bar.inc(1); } bar.finish(); ensure_output_directory(&args.output_directory)?; let multibar = MultiProgress::new(); if !use_progress_bars { multibar.set_draw_target(ProgressDrawTarget::hidden()) } let zoom_bar = make_bar((args.max_zoom - args.min_zoom + 1).into()).with_style(zoom_style); multibar.add(zoom_bar.clone()); zoom_bar.set_prefix("Zoom levels"); for zoom in args.min_zoom..=args.max_zoom { let bar = make_bar(tracks.len().try_into().unwrap()).with_style(progress_style.clone()); multibar.insert_from_back(1, bar.clone()); bar.set_prefix("Rendering heat zones"); let counter = renderer::render_heatcounter(zoom, &tracks, |x| bar.inc(x.try_into().unwrap())); bar.finish(); multibar.remove(&bar); let target = [&args.output_directory, &zoom.to_string().into()] .iter() .collect::(); fs::create_dir(&target)?; let bar = make_bar(counter.tile_count().try_into().unwrap()).with_style(progress_style.clone()); multibar.insert_from_back(1, bar.clone()); bar.set_prefix("Saving heat tiles"); renderer::lazy_colorization(counter, &target, |x| bar.inc(x.try_into().unwrap()))?; bar.finish(); multibar.remove(&bar); zoom_bar.inc(1); } zoom_bar.finish(); Ok(()) } fn ensure_output_directory>(path: P) -> Result<()> { let path = path.as_ref(); let metadata = fs::metadata(path); match metadata { Err(e) if e.kind() == ErrorKind::NotFound => { let parent = path.parent().unwrap_or(Path::new("/")); fs::create_dir(path) .context(format!("Could not create output directory at {parent:?}"))? } Err(e) => Err(e).context("Error while checking output directory")?, Ok(m) if m.is_dir() => (), Ok(_) => bail!("Output directory is not a directory"), } Ok(()) }