From 3e694d68a685b6e22d6ab59f34090e4681849ebc Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 13 Mar 2023 22:08:48 +0100 Subject: implement "proper" tile hunter mode Now with fixed zoom level for the hunting. --- src/main.rs | 22 +++++--- src/renderer/heatmap.rs | 11 ++-- src/renderer/marktile.rs | 65 ++++++++++++++++++++++ src/renderer/mod.rs | 20 ++++--- src/renderer/tilehunt.rs | 137 +++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 src/renderer/marktile.rs 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::(args), - Mode::Tilehunt => run::(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(args: Args) -> Result<()> { +fn run(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(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::(zoom, &tracks, || { + let counter = renderer::prepare(&renderer, zoom, &tracks, || { bar.inc(1); Ok(()) })?; @@ -139,10 +145,10 @@ fn run(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::(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], tick: Sender<()>) -> Result { + fn prepare( + &self, + zoom: u32, + tracks: &[Vec], + tick: Sender<()>, + ) -> Result { 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) -> Result<()> { + fn colorize(&self, layer: HeatCounter, tx: Sender) -> 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 { + fn tile_count(&self, layer: &HeatCounter) -> Result { 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], + tick: Sender<()>, + ) -> Result { + 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) -> Result<()> { + // The tile is hand-crafted to be very small. See + // for a reference, and of course the actual + // PNG specification . + 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 { + 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], tick: Sender<()>) -> Result; + fn prepare( + &self, + zoom: u32, + tracks: &[Vec], + tick: Sender<()>, + ) -> Result; /// 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) -> Result<()>; + fn colorize(&self, prepared: Self::Prepared, saver: Sender) -> 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; + fn tile_count(&self, prepared: &Self::Prepared) -> Result; } /// 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 Result<()>>( + renderer: &R, zoom: u32, tracks: &[Vec], mut tick: F, @@ -59,7 +66,7 @@ pub fn prepare 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 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 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> { + // 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], tick: Sender<()>) -> Result { + fn prepare( + &self, + zoom: u32, + tracks: &[Vec], + tick: Sender<()>, + ) -> Result { 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) -> Result<()> { - // The tile is hand-crafted to be very small. See - // for a reference, and of course the actual - // PNG specification . - 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) -> 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 { - Ok(layer.len().try_into().unwrap()) + fn tile_count(&self, layer: &Self::Prepared) -> Result { + Ok(layer.1.len().try_into().unwrap()) } } -- cgit v1.2.3