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())      }  }  | 
