diff options
-rw-r--r-- | benches/hittebench.rs | 7 | ||||
-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 |
4 files changed, 175 insertions, 85 deletions
diff --git a/benches/hittebench.rs b/benches/hittebench.rs index 15ca3ff..163fd1e 100644 --- a/benches/hittebench.rs +++ b/benches/hittebench.rs @@ -1,6 +1,9 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use hittekaart::{gpx, renderer}; +use hittekaart::{ + gpx, + renderer::{self, heatmap}, +}; static BENCH_DATA: &str = include_str!("bench.gpx"); @@ -12,7 +15,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let data = vec![gpx::extract_from_str(BENCH_DATA).unwrap()]; c.bench_function("render_heatcounter", |b| { - b.iter(|| renderer::render_heatcounter(19, &data, |_| ())); + b.iter(|| renderer::prepare::<heatmap::Renderer, _>(19, &data, || Ok(()))); }); } 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() + }) +} |