aboutsummaryrefslogtreecommitdiff
path: root/src/renderer/tilehunt.rs
blob: 9081523ee7bd8fa4beb5c48cbea12e1cc027d5d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! 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<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(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<Coordinates>],
        tick: Sender<()>,
    ) -> Result<Self::Prepared> {
        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<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(&self, layer: &Self::Prepared) -> Result<u64> {
        Ok(layer.1.len().try_into().unwrap())
    }
}