aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--benches/hittebench.rs7
-rw-r--r--src/main.rs30
-rw-r--r--src/renderer/heatmap.rs (renamed from src/renderer.rs)133
-rw-r--r--src/renderer/mod.rs90
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()
+ })
+}