diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 30 | ||||
| -rw-r--r-- | src/renderer/heatmap.rs (renamed from src/renderer.rs) | 133 | ||||
| -rw-r--r-- | src/renderer/mod.rs | 90 | 
3 files changed, 170 insertions, 83 deletions
| diff --git a/src/main.rs b/src/main.rs index e5b835e..6c110e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@  use std::{io, path::PathBuf}; -use clap::Parser; +use clap::{Parser, ValueEnum};  use color_eyre::{      eyre::{bail, eyre, Result},      Report,  };  use hittekaart::{      gpx::{self, Compression}, -    renderer, +    renderer::{self, heatmap, Renderer},      storage::{Folder, Sqlite, Storage},  };  use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; @@ -17,6 +17,13 @@ use rayon::{      ThreadPoolBuilder,  }; +/// Tile generation mode. +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Mode { +    Heatmap, +    Tilehunt, +} +  #[derive(Parser, Debug, Clone)]  #[command(author, version, about)]  struct Args { @@ -45,6 +52,10 @@ struct Args {      /// filename.      #[arg(long)]      sqlite: bool, + +    /// Generation mode. +    #[arg(value_enum, long, short, default_value_t = Mode::Heatmap)] +    mode: Mode,  }  fn main() -> Result<()> { @@ -60,6 +71,10 @@ fn main() -> Result<()> {          .num_threads(args.threads)          .build_global()?; +    run::<heatmap::Renderer>(args) +} + +fn run<R: Renderer>(args: Args) -> Result<()> {      let progress_style =          ProgressStyle::with_template("[{elapsed}] {prefix:.cyan} {wide_bar} {pos:.green}/{len}")?;      let zoom_style = @@ -112,18 +127,19 @@ fn main() -> Result<()> {          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())); +        let counter = renderer::prepare::<R, _>(zoom, &tracks, || { +            bar.inc(1); +            Ok(()) +        })?;          bar.finish();          multibar.remove(&bar);          storage.prepare_zoom(zoom)?; -        let bar = -            make_bar(counter.tile_count().try_into().unwrap()).with_style(progress_style.clone()); +        let bar = make_bar(R::tile_count(&counter)?).with_style(progress_style.clone());          multibar.insert_from_back(1, bar.clone());          bar.set_prefix("Saving heat tiles"); -        renderer::lazy_colorization(counter, |rendered_tile| { +        renderer::colorize::<R, _>(counter, |rendered_tile| {              storage.store(zoom, rendered_tile.x, rendered_tile.y, &rendered_tile.data)?;              bar.inc(1);              Ok(()) diff --git a/src/renderer.rs b/src/renderer/heatmap.rs index d7aefe4..c4af7a6 100644 --- a/src/renderer.rs +++ b/src/renderer/heatmap.rs @@ -6,29 +6,20 @@  //!  //! We then render the colored heatmap tiles using [`lazy_colorization`], which provides us with  //! colorful PNG data. -use std::thread; -  use color_eyre::{eyre::Result, Report}; +use crossbeam_channel::Sender;  use image::{ImageBuffer, Luma, Pixel, RgbaImage};  use nalgebra::{vector, Vector2};  use rayon::iter::ParallelIterator;  use super::{ -    gpx::Coordinates, -    layer::{self, TileLayer}, +    super::{ +        gpx::Coordinates, +        layer::{self, TileLayer}, +    }, +    RenderedTile,  }; -/// Represents a fully rendered tile. -#[derive(Debug, Clone)] -pub struct RenderedTile { -    /// The `x` coordinate of the tile. -    pub x: u64, -    /// The `y` coordinate of the tile. -    pub y: u64, -    /// The encoded (PNG) image data, ready to be saved to disk. -    pub data: Vec<u8>, -} -  /// Type for the intermediate heat counters.  pub type HeatCounter = TileLayer<Luma<u8>>; @@ -148,30 +139,55 @@ fn colorize_tile(tile: &ImageBuffer<Luma<u8>, Vec<u8>>, max: u32) -> RgbaImage {      result  } -/// Lazily colorizes a [`HeatCounter`] by colorizing it tile-by-tile and saving a tile before -/// rendering the next one. -/// -/// This function calls the given callback with each rendered tile, and the function is responsible -/// for saving it. If the callback returns an `Err(...)`, the error is passed through. -/// -/// Note that this function internally uses `rayon` for parallization. If you want to limit the -/// number of threads used, set up the global [`rayon::ThreadPool`] first. -pub fn lazy_colorization<F: FnMut(RenderedTile) -> Result<()> + Send>( -    layer: HeatCounter, -    mut save_callback: F, -) -> Result<()> { -    let max = layer.pixels().map(|l| l.0[0]).max().unwrap_or_default(); -    if max == 0 { -        return Ok(()); -    } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Renderer; + +impl super::Renderer for Renderer { +    type Prepared = HeatCounter; + +    /// Renders the heat counter for the given zoom level and track points. +    /// +    /// The given callback will be called when a track has been rendered and merged into the +    /// accumulator, to allow for UI feedback. The passed parameter is the number of tracks that have +    /// been rendered since the last call. +    fn prepare(zoom: u32, tracks: &[Vec<Coordinates>], tick: Sender<()>) -> Result<HeatCounter> { +        let mut heatcounter = TileLayer::from_pixel([0].into()); -    let (tx, rx) = crossbeam_channel::bounded::<RenderedTile>(30); +        for track in tracks { +            let mut layer = TileLayer::from_pixel([0].into()); -    thread::scope(|s| { -        let saver = s.spawn(move || loop { -            let Ok(tile) = rx.recv() else { return Ok::<_, Report>(()) }; -            save_callback(tile)?; -        }); +            let points = track +                .iter() +                .map(|coords| coords.web_mercator(zoom)) +                .collect::<Vec<_>>(); + +            for point in points.iter() { +                render_circle(&mut layer, *point, (zoom as u64 / 4).max(2) - 1, [1].into()); +            } + +            for (a, b) in points.iter().zip(points.iter().skip(1)) { +                render_line(&mut layer, *a, *b, (zoom as u64 / 4).max(1), [1].into()); +            } + +            merge_heat_counter(&mut heatcounter, &layer); +            tick.send(()).unwrap(); +        } +        Ok(heatcounter) +    } + +    /// Lazily colorizes a [`HeatCounter`] by colorizing it tile-by-tile and saving a tile before +    /// rendering the next one. +    /// +    /// This function calls the given callback with each rendered tile, and the function is responsible +    /// for saving it. If the callback returns an `Err(...)`, the error is passed through. +    /// +    /// Note that this function internally uses `rayon` for parallization. If you want to limit the +    /// number of threads used, set up the global [`rayon::ThreadPool`] first. +    fn colorize(layer: HeatCounter, tx: Sender<RenderedTile>) -> Result<()> { +        let max = layer.pixels().map(|l| l.0[0]).max().unwrap_or_default(); +        if max == 0 { +            return Ok(()); +        }          layer              .into_parallel_tiles() @@ -184,45 +200,10 @@ pub fn lazy_colorization<F: FnMut(RenderedTile) -> Result<()> + Send>(                      data,                  })?;                  Ok::<(), Report>(()) -            })?; - -        saver.join().unwrap()?; -        Ok::<_, Report>(()) -    })?; - -    Ok(()) -} - -/// Renders the heat counter for the given zoom level and track points. -/// -/// The given callback will be called when a track has been rendered and merged into the -/// accumulator, to allow for UI feedback. The passed parameter is the number of tracks that have -/// been rendered since the last call. -pub fn render_heatcounter<F: Fn(usize) + Send + Sync>( -    zoom: u32, -    tracks: &[Vec<Coordinates>], -    progress_callback: F, -) -> HeatCounter { -    let mut heatcounter = TileLayer::from_pixel([0].into()); - -    for track in tracks { -        let mut layer = TileLayer::from_pixel([0].into()); - -        let points = track -            .iter() -            .map(|coords| coords.web_mercator(zoom)) -            .collect::<Vec<_>>(); - -        for point in points.iter() { -            render_circle(&mut layer, *point, (zoom as u64 / 4).max(2) - 1, [1].into()); -        } - -        for (a, b) in points.iter().zip(points.iter().skip(1)) { -            render_line(&mut layer, *a, *b, (zoom as u64 / 4).max(1), [1].into()); -        } +            }) +    } -        merge_heat_counter(&mut heatcounter, &layer); -        progress_callback(1); +    fn tile_count(layer: &HeatCounter) -> Result<u64> { +        Ok(layer.tile_count().try_into().unwrap())      } -    heatcounter  } diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..927c6ed --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,90 @@ +//! Generic "tile rendering" methods. +use std::thread; + +use color_eyre::Result; +use crossbeam_channel::Sender; + +use super::gpx::Coordinates; + +pub mod heatmap; + +const CHANNEL_SIZE: usize = 30; + +/// Represents a fully rendered tile. +#[derive(Debug, Clone)] +pub struct RenderedTile { +    /// The `x` coordinate of the tile. +    pub x: u64, +    /// The `y` coordinate of the tile. +    pub y: u64, +    /// The encoded (PNG) image data, ready to be saved to disk. +    pub data: Vec<u8>, +} + +/// An object that is responsible for turning raw GPX tracks into a representation. +/// +/// This is done in two steps, preparation and actual rendering. This allows different feedback for +/// the user. +pub trait Renderer { +    type Prepared: Send; + +    /// Prepare the rendered data. +    /// +    /// The `tick` channel is used to provide user-feedback, for every finished track a tick should +    /// be sent. +    fn prepare(zoom: u32, tracks: &[Vec<Coordinates>], tick: Sender<()>) -> Result<Self::Prepared>; + +    /// Actually produce the colored tiles, using the previously prepared data. +    /// +    /// The `saver` channel is used to send the finished tiles to a thread that is responsible for +    /// saving them. +    fn colorize(prepared: Self::Prepared, saver: Sender<RenderedTile>) -> Result<()>; + +    /// Returns the tile count of the prepared data. +    /// +    /// This is used for the user interface, to scale progress bars appropriately. +    fn tile_count(prepared: &Self::Prepared) -> Result<u64>; +} + +/// A convenience wrapper to call [`Renderer::prepare`]. +/// +/// This function takes the same arguments, but provides the ability to use a callback closure +/// instead of having to set up a channel. The callback is always called on the same thread. +pub fn prepare<R: Renderer, F: FnMut() -> Result<()>>( +    zoom: u32, +    tracks: &[Vec<Coordinates>], +    mut tick: F, +) -> Result<R::Prepared> { +    thread::scope(|s| { +        let (sender, receiver) = crossbeam_channel::bounded(CHANNEL_SIZE); + +        let preparer = s.spawn(|| R::prepare(zoom, tracks, sender)); + +        for _ in receiver { +            tick()?; +        } + +        preparer.join().unwrap() +    }) +} + +/// A convenience wrapper to call [`Renderer::colorize`]. +/// +/// This function takes the same arguments, but provides the ability to use a callback closure +/// instead of having to set up a channel. The callback is always called on the same thread. +pub fn colorize<R: Renderer, F: FnMut(RenderedTile) -> Result<()>>( +    prepared: R::Prepared, +    mut saver: F, +) -> Result<()> { +    thread::scope(|s| { +        let (sender, receiver) = crossbeam_channel::bounded(CHANNEL_SIZE); + +        let colorizer = s.spawn(|| R::colorize(prepared, sender)); + +        for tile in receiver { +            saver(tile)?; +        } + +        colorizer.join().unwrap() +    }) +} | 
