hittekaart 0.1.0

Generates OSM heatmap tiles from GPX tracks
Documentation
//! Generic "tile rendering" methods.
use std::thread;

use crossbeam_channel::Sender;

use super::{error::Result, gpx::Coordinates};

pub mod heatmap;
pub mod marktile;
pub mod tilehunt;

const CHANNEL_SIZE: usize = 30;

/// Represents a fully rendered tile.
#[derive(Debug, Clone)]
pub struct RenderedTile {
    /// The `x` coordinate of the tile.
    pub x: u64,
    /// The `y` coordinate of the tile.
    pub y: u64,
    /// The encoded (PNG) image data, ready to be saved to disk.
    pub data: Vec<u8>,
}

/// An object that is responsible for turning raw GPX tracks into a representation.
///
/// This is done in two steps, preparation and actual rendering. This allows different feedback for
/// the user.
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(
        &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(&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(&self, prepared: &Self::Prepared) -> Result<u64>;
}

/// A convenience wrapper to call [`Renderer::prepare`].
///
/// 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,
) -> Result<R::Prepared> {
    thread::scope(|s| {
        let (sender, receiver) = crossbeam_channel::bounded(CHANNEL_SIZE);

        let preparer = s.spawn(|| renderer.prepare(zoom, tracks, sender));

        for _ in receiver {
            tick()?;
        }

        preparer.join().unwrap()
    })
}

/// A convenience wrapper to call [`Renderer::colorize`].
///
/// 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(|| renderer.colorize(prepared, sender));

        for tile in receiver {
            saver(tile)?;
        }

        colorizer.join().unwrap()
    })
}

#[cfg(test)]
mod test {
    use super::*;
    use rstest::rstest;

    fn tracks() -> Vec<Vec<Coordinates>> {
        vec![
            vec![Coordinates {
                latitude: 52.520008,
                longitude: 13.404954,
            }],
            vec![Coordinates {
                latitude: 52.520008,
                longitude: 13.404954,
            }],
            vec![Coordinates {
                latitude: 52.520008,
                longitude: 13.404954,
            }],
        ]
    }

    #[rstest]
    fn test_heatmap_prepare_zoom_0() {
        let ts = tracks();
        let mut prep = prepare(&heatmap::Renderer, 0, &ts[..1], || Ok(())).unwrap();

        assert_eq!(prep.tile_count(), 1);
        assert_eq!(prep.tile_mut(0, 0).get_pixel(0, 0).0[0], 0);
        assert_eq!(prep.tile_mut(0, 0).get_pixel(255, 255).0[0], 0);

        let ones: &[(u32, u32)] = &[(137, 82), (136, 83), (137, 83), (138, 83), (137, 84)];

        for (x, y) in ones {
            assert_eq!(prep.tile_mut(0, 0).get_pixel(*x, *y).0[0], 1);
        }

        let mut prep = prepare(&heatmap::Renderer, 0, &ts[..2], || Ok(())).unwrap();
        for (x, y) in ones {
            assert_eq!(prep.tile_mut(0, 0).get_pixel(*x, *y).0[0], 2);
        }

        let mut prep = prepare(&heatmap::Renderer, 0, &ts[..3], || Ok(())).unwrap();
        for (x, y) in ones {
            assert_eq!(prep.tile_mut(0, 0).get_pixel(*x, *y).0[0], 3);
        }
    }

    #[rstest]
    fn test_heatmap_prepare_zoom_1() {
        let ts = tracks();
        let mut prep = prepare(&heatmap::Renderer, 1, &ts[..1], || Ok(())).unwrap();
        for (tx, ty) in [(0, 0), (0, 1), (1, 1)] {
            assert!(prep.tile_mut(tx, ty).pixels().all(|px| px.0[0] == 0));
        }
        let ones: &[(u32, u32)] = &[(19, 166), (18, 167), (19, 167), (20, 167), (19, 168)];
        for (x, y) in ones {
            assert_eq!(prep.tile_mut(1, 0).get_pixel(*x, *y).0[0], 1);
        }
    }

    #[rstest]
    fn test_heatmap_prepare_zoom_2() {
        let ts = tracks();
        let mut prep = prepare(&heatmap::Renderer, 2, &ts[..1], || Ok(())).unwrap();
        for (tx, ty) in [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1)] {
            assert!(prep.tile_mut(tx, ty).pixels().all(|px| px.0[0] == 0));
        }
        let ones: &[(u32, u32)] = &[(38, 78), (37, 79), (38, 79), (39, 79), (38, 80)];
        for (x, y) in ones {
            assert_eq!(prep.tile_mut(2, 1).get_pixel(*x, *y).0[0], 1);
        }
    }
}