aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main.rs22
-rw-r--r--src/renderer/heatmap.rs11
-rw-r--r--src/renderer/marktile.rs65
-rw-r--r--src/renderer/mod.rs20
-rw-r--r--src/renderer/tilehunt.rs137
5 files changed, 216 insertions, 39 deletions
diff --git a/src/main.rs b/src/main.rs
index 7f7c532..11133af 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,7 +7,7 @@ use color_eyre::{
};
use hittekaart::{
gpx::{self, Compression},
- renderer::{self, heatmap, tilehunt, Renderer},
+ renderer::{self, heatmap, marktile, tilehunt, Renderer},
storage::{Folder, Sqlite, Storage},
};
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
@@ -21,7 +21,8 @@ use rayon::{
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Mode {
Heatmap,
- Tilehunt,
+ Marktile,
+ Tilehunter,
}
#[derive(Parser, Debug, Clone)]
@@ -56,6 +57,10 @@ struct Args {
/// Generation mode.
#[arg(value_enum, long, short, default_value_t = Mode::Heatmap)]
mode: Mode,
+
+ /// Zoom level for the tilehunter mode.
+ #[arg(long, default_value_t = 14)]
+ tilehunter_zoom: u32,
}
fn main() -> Result<()> {
@@ -72,12 +77,13 @@ fn main() -> Result<()> {
.build_global()?;
match args.mode {
- Mode::Heatmap => run::<heatmap::Renderer>(args),
- Mode::Tilehunt => run::<tilehunt::Renderer>(args),
+ Mode::Heatmap => run(heatmap::Renderer, args),
+ Mode::Marktile => run(marktile::Renderer, args),
+ Mode::Tilehunter => run(tilehunt::Renderer::new(args.tilehunter_zoom), args),
}
}
-fn run<R: Renderer>(args: Args) -> Result<()> {
+fn run<R: Renderer>(renderer: R, args: Args) -> Result<()> {
let progress_style =
ProgressStyle::with_template("[{elapsed}] {prefix:.cyan} {wide_bar} {pos:.green}/{len}")?;
let zoom_style =
@@ -130,7 +136,7 @@ fn run<R: Renderer>(args: Args) -> 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::prepare::<R, _>(zoom, &tracks, || {
+ let counter = renderer::prepare(&renderer, zoom, &tracks, || {
bar.inc(1);
Ok(())
})?;
@@ -139,10 +145,10 @@ fn run<R: Renderer>(args: Args) -> Result<()> {
storage.prepare_zoom(zoom)?;
- let bar = make_bar(R::tile_count(&counter)?).with_style(progress_style.clone());
+ let bar = make_bar(renderer.tile_count(&counter)?).with_style(progress_style.clone());
multibar.insert_from_back(1, bar.clone());
bar.set_prefix("Saving heat tiles");
- renderer::colorize::<R, _>(counter, |rendered_tile| {
+ renderer::colorize(&renderer, counter, |rendered_tile| {
storage.store(zoom, rendered_tile.x, rendered_tile.y, &rendered_tile.data)?;
bar.inc(1);
Ok(())
diff --git a/src/renderer/heatmap.rs b/src/renderer/heatmap.rs
index c4af7a6..0c4f93f 100644
--- a/src/renderer/heatmap.rs
+++ b/src/renderer/heatmap.rs
@@ -150,7 +150,12 @@ impl super::Renderer for Renderer {
/// 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> {
+ fn prepare(
+ &self,
+ zoom: u32,
+ tracks: &[Vec<Coordinates>],
+ tick: Sender<()>,
+ ) -> Result<HeatCounter> {
let mut heatcounter = TileLayer::from_pixel([0].into());
for track in tracks {
@@ -183,7 +188,7 @@ impl super::Renderer for Renderer {
///
/// 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<()> {
+ fn colorize(&self, layer: HeatCounter, tx: Sender<RenderedTile>) -> Result<()> {
let max = layer.pixels().map(|l| l.0[0]).max().unwrap_or_default();
if max == 0 {
return Ok(());
@@ -203,7 +208,7 @@ impl super::Renderer for Renderer {
})
}
- fn tile_count(layer: &HeatCounter) -> Result<u64> {
+ fn tile_count(&self, layer: &HeatCounter) -> Result<u64> {
Ok(layer.tile_count().try_into().unwrap())
}
}
diff --git a/src/renderer/marktile.rs b/src/renderer/marktile.rs
new file mode 100644
index 0000000..1e3020f
--- /dev/null
+++ b/src/renderer/marktile.rs
@@ -0,0 +1,65 @@
+//! Actual rendering functions for tile hunts.
+//!
+//! This renders a tile as "transparent green" if any track passes through it.
+//!
+//! Note that is version of "tile hunt" is a bit silly, as the tile size changes with the zoom
+//! level. For a better version, the "tile hunt size" should be fixed to a given zoom.
+use color_eyre::eyre::Result;
+use crossbeam_channel::Sender;
+use fnv::FnvHashSet;
+
+use super::{
+ super::{
+ gpx::Coordinates,
+ layer::{TILE_HEIGHT, TILE_WIDTH},
+ },
+ RenderedTile,
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Renderer;
+
+impl super::Renderer for Renderer {
+ type Prepared = FnvHashSet<(u64, u64)>;
+
+ fn prepare(
+ &self,
+ zoom: u32,
+ tracks: &[Vec<Coordinates>],
+ tick: Sender<()>,
+ ) -> Result<Self::Prepared> {
+ let mut marked = FnvHashSet::default();
+
+ for track in tracks {
+ for point in track {
+ let merc = point.web_mercator(zoom);
+ let tile_x = merc.0 / TILE_WIDTH;
+ let tile_y = merc.1 / TILE_HEIGHT;
+ marked.insert((tile_x, tile_y));
+ }
+
+ tick.send(()).unwrap();
+ }
+
+ Ok(marked)
+ }
+
+ fn colorize(&self, layer: Self::Prepared, tx: Sender<RenderedTile>) -> Result<()> {
+ // The tile is hand-crafted to be very small. See
+ // <https://www.mjt.me.uk/posts/smallest-png/> for a reference, and of course the actual
+ // PNG specification <http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html>.
+ static IMAGE_DATA: &[u8] = include_bytes!("tile-marked.png");
+ for (tile_x, tile_y) in layer {
+ tx.send(RenderedTile {
+ x: tile_x,
+ y: tile_y,
+ data: IMAGE_DATA.to_vec(),
+ })?;
+ }
+ Ok(())
+ }
+
+ fn tile_count(&self, layer: &Self::Prepared) -> Result<u64> {
+ Ok(layer.len().try_into().unwrap())
+ }
+}
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index f109872..73c2e87 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -7,6 +7,7 @@ use crossbeam_channel::Sender;
use super::gpx::Coordinates;
pub mod heatmap;
+pub mod marktile;
pub mod tilehunt;
const CHANNEL_SIZE: usize = 30;
@@ -26,25 +27,30 @@ pub struct RenderedTile {
///
/// This is done in two steps, preparation and actual rendering. This allows different feedback for
/// the user.
-pub trait Renderer {
+pub trait Renderer: Send + Sync {
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>;
+ fn prepare(
+ &self,
+ 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<()>;
+ fn colorize(&self, 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>;
+ fn tile_count(&self, prepared: &Self::Prepared) -> Result<u64>;
}
/// A convenience wrapper to call [`Renderer::prepare`].
@@ -52,6 +58,7 @@ pub trait Renderer {
/// 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<()>>(
+ renderer: &R,
zoom: u32,
tracks: &[Vec<Coordinates>],
mut tick: F,
@@ -59,7 +66,7 @@ pub fn prepare<R: Renderer, F: FnMut() -> Result<()>>(
thread::scope(|s| {
let (sender, receiver) = crossbeam_channel::bounded(CHANNEL_SIZE);
- let preparer = s.spawn(|| R::prepare(zoom, tracks, sender));
+ let preparer = s.spawn(|| renderer.prepare(zoom, tracks, sender));
for _ in receiver {
tick()?;
@@ -74,13 +81,14 @@ pub fn prepare<R: Renderer, F: FnMut() -> Result<()>>(
/// 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<()>>(
+ renderer: &R,
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));
+ let colorizer = s.spawn(|| renderer.colorize(prepared, sender));
for tile in receiver {
saver(tile)?;
diff --git a/src/renderer/tilehunt.rs b/src/renderer/tilehunt.rs
index 55b30af..9081523 100644
--- a/src/renderer/tilehunt.rs
+++ b/src/renderer/tilehunt.rs
@@ -4,30 +4,76 @@
//!
//! Note that is version of "tile hunt" is a bit silly, as the tile size changes with the zoom
//! level. For a better version, the "tile hunt size" should be fixed to a given zoom.
+use std::cmp::Ordering;
+
use color_eyre::eyre::Result;
use crossbeam_channel::Sender;
-use fnv::FnvHashSet;
+use fnv::{FnvHashMap, FnvHashSet};
+use image::RgbaImage;
+use imageproc::{drawing::draw_filled_rect_mut, rect::Rect};
+use rayon::iter::{IntoParallelIterator, ParallelIterator};
use super::{
super::{
gpx::Coordinates,
- layer::{TILE_HEIGHT, TILE_WIDTH},
+ layer::{self, TILE_HEIGHT, TILE_WIDTH},
},
RenderedTile,
};
+fn render_squares(grid: u32, inner: Vec<(u8, u8)>) -> Result<Vec<u8>> {
+ // We re-use the tiny PNG if possible
+ static FULL_TILE: &[u8] = include_bytes!("tile-marked.png");
+ if grid == 1 && !inner.is_empty() {
+ return Ok(FULL_TILE.to_vec());
+ }
+ let mut base =
+ RgbaImage::from_pixel(TILE_WIDTH as u32, TILE_HEIGHT as u32, [0, 0, 0, 0].into());
+ let patch_size = TILE_WIDTH as u32 / grid;
+
+ for (patch_x, patch_y) in inner {
+ draw_filled_rect_mut(
+ &mut base,
+ Rect::at(
+ patch_x as i32 * patch_size as i32,
+ patch_y as i32 * patch_size as i32,
+ )
+ .of_size(patch_size, patch_size),
+ [0, 255, 0, 128].into(),
+ );
+ }
+
+ layer::compress_png_as_bytes(&base)
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct Renderer;
+pub struct Renderer(u32);
+
+impl Renderer {
+ pub fn new(hunter_zoom: u32) -> Self {
+ Renderer(hunter_zoom)
+ }
+
+ #[inline]
+ pub fn hunter_zoom(&self) -> u32 {
+ self.0
+ }
+}
impl super::Renderer for Renderer {
- type Prepared = FnvHashSet<(u64, u64)>;
+ type Prepared = (u32, FnvHashMap<(u64, u64), Vec<(u8, u8)>>);
- fn prepare(zoom: u32, tracks: &[Vec<Coordinates>], tick: Sender<()>) -> Result<Self::Prepared> {
+ fn prepare(
+ &self,
+ zoom: u32,
+ tracks: &[Vec<Coordinates>],
+ tick: Sender<()>,
+ ) -> Result<Self::Prepared> {
let mut marked = FnvHashSet::default();
for track in tracks {
for point in track {
- let merc = point.web_mercator(zoom);
+ let merc = point.web_mercator(self.hunter_zoom());
let tile_x = merc.0 / TILE_WIDTH;
let tile_y = merc.1 / TILE_HEIGHT;
marked.insert((tile_x, tile_y));
@@ -36,25 +82,72 @@ impl super::Renderer for Renderer {
tick.send(()).unwrap();
}
- Ok(marked)
- }
+ let scale = i32::try_from(zoom).unwrap() - i32::try_from(self.hunter_zoom()).unwrap();
+ let grid = if scale >= 0 {
+ 1
+ } else {
+ 2u64.pow(scale.abs().min(8) as u32)
+ };
+
+ let mut result = FnvHashMap::<(u64, u64), Vec<(u8, u8)>>::default();
- fn colorize(layer: Self::Prepared, tx: Sender<RenderedTile>) -> Result<()> {
- // The tile is hand-crafted to be very small. See
- // <https://www.mjt.me.uk/posts/smallest-png/> for a reference, and of course the actual
- // PNG specification <http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html>.
- static IMAGE_DATA: &[u8] = include_bytes!("tile-marked.png");
- for (tile_x, tile_y) in layer {
- tx.send(RenderedTile {
- x: tile_x,
- y: tile_y,
- data: IMAGE_DATA.to_vec(),
- })?;
+ for (tile_x, tile_y) in marked {
+ match scale.cmp(&0) {
+ Ordering::Equal =>
+ // The current zoom level is the same as the hunter level, so the tiles have a 1:1
+ // mapping
+ {
+ result.entry((tile_x, tile_y)).or_default().push((0u8, 0u8))
+ }
+ Ordering::Less =>
+ // In this case we are "zoomed out" further than the hunter level, so a marked tile
+ // has to be scaled down and we need to figure out where in the "big tile" our
+ // marked tile is
+ {
+ result
+ .entry((tile_x / grid, tile_y / grid))
+ .or_default()
+ .push((
+ (tile_x % grid).try_into().unwrap(),
+ (tile_y % grid).try_into().unwrap(),
+ ))
+ }
+ Ordering::Greater => {
+ // In this case, we are zoomed in more than the hunter level. Each marked tile
+ // expands to multiple tiles.
+ let multiplier = 2u64.pow(scale as u32);
+ for dx in 0..multiplier {
+ for dy in 0..multiplier {
+ result
+ .entry((tile_x * multiplier + dx, tile_y * multiplier + dy))
+ .or_default()
+ .push((0u8, 0u8));
+ }
+ }
+ }
+ }
}
- Ok(())
+
+ Ok((grid.try_into().unwrap(), result))
+ }
+
+ fn colorize(&self, layer: Self::Prepared, tx: Sender<RenderedTile>) -> Result<()> {
+ let grid = layer.0;
+ layer
+ .1
+ .into_par_iter()
+ .try_for_each_with(tx, |tx, ((tile_x, tile_y), inner)| {
+ let data = render_squares(grid, inner)?;
+ tx.send(RenderedTile {
+ x: tile_x,
+ y: tile_y,
+ data,
+ })?;
+ Ok(())
+ })
}
- fn tile_count(layer: &Self::Prepared) -> Result<u64> {
- Ok(layer.len().try_into().unwrap())
+ fn tile_count(&self, layer: &Self::Prepared) -> Result<u64> {
+ Ok(layer.1.len().try_into().unwrap())
}
}