diff options
author | Daniel Schadt <kingdread@gmx.de> | 2023-03-13 22:08:48 +0100 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2023-03-13 22:08:48 +0100 |
commit | 3e694d68a685b6e22d6ab59f34090e4681849ebc (patch) | |
tree | 0e9d817d38d8d3524b459c5e8ebe4c55876e322b /src/renderer | |
parent | 3bec9ff1bcb7fb8b93693c0c93b8d42797f95e1c (diff) | |
download | hittekaart-3e694d68a685b6e22d6ab59f34090e4681849ebc.tar.gz hittekaart-3e694d68a685b6e22d6ab59f34090e4681849ebc.tar.bz2 hittekaart-3e694d68a685b6e22d6ab59f34090e4681849ebc.zip |
implement "proper" tile hunter mode
Now with fixed zoom level for the hunting.
Diffstat (limited to 'src/renderer')
-rw-r--r-- | src/renderer/heatmap.rs | 11 | ||||
-rw-r--r-- | src/renderer/marktile.rs | 65 | ||||
-rw-r--r-- | src/renderer/mod.rs | 20 | ||||
-rw-r--r-- | src/renderer/tilehunt.rs | 137 |
4 files changed, 202 insertions, 31 deletions
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()) } } |