//! 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 std::cmp::Ordering; use color_eyre::eyre::Result; use crossbeam_channel::Sender; 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::{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(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 = (u32, FnvHashMap<(u64, u64), Vec<(u8, u8)>>); 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(self.hunter_zoom()); let tile_x = merc.0 / TILE_WIDTH; let tile_y = merc.1 / TILE_HEIGHT; marked.insert((tile_x, tile_y)); } tick.send(()).unwrap(); } 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(); 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((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(&self, layer: &Self::Prepared) -> Result { Ok(layer.1.len().try_into().unwrap()) } }