From 5a4e23797e74c2989cdea86128350de53c0b15e4 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 26 Aug 2025 21:32:23 +0200 Subject: properly clamp Coordinates::to_mercator --- hittekaart/src/gpx.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/hittekaart/src/gpx.rs b/hittekaart/src/gpx.rs index 68bd6fb..86cb88b 100644 --- a/hittekaart/src/gpx.rs +++ b/hittekaart/src/gpx.rs @@ -38,15 +38,32 @@ impl Coordinates { /// projection](https://en.wikipedia.org/wiki/Web_Mercator_projection) of the coordinates. /// /// Returns the `(x, y)` projection, where both are in the range `[0, 256 * 2^zoom)`. + /// + /// Note that coordinates outside the range are silently truncated to the limits. pub fn web_mercator(self, zoom: u32) -> (u64, u64) { const WIDTH: f64 = super::layer::TILE_WIDTH as f64; const HEIGHT: f64 = super::layer::TILE_HEIGHT as f64; - let lambda = self.longitude.to_radians(); - let phi = self.latitude.to_radians(); - let x = 2u64.pow(zoom) as f64 / (2.0 * PI) * WIDTH * (lambda + PI); - let y = - 2u64.pow(zoom) as f64 / (2.0 * PI) * HEIGHT * (PI - (PI / 4.0 + phi / 2.0).tan().ln()); + // We support longitudes from 180° W/E, which is represented as PI in radians. + // For latitudes, we actually don't go all the way up to 90° N/S, see + // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames + // To quote: + // This value will fall in the range (-π, π) for latitudes between 85.0511 °S and + // 85.0511 °N. + // For the curious, the number 85.0511 is the result of arctan(sinh(π)). By using + // this bound, the entire map becomes a (very large) square. + // We compute the exact bound here. + let max_lon: f64 = PI; + let max_lat: f64 = PI.sinh().atan(); + + let xmax = 2u64.pow(zoom) as f64 * WIDTH; + let ymax = 2u64.pow(zoom) as f64 * HEIGHT; + + let lambda = self.longitude.to_radians().clamp(-max_lon, max_lon); + let phi = self.latitude.to_radians().clamp(-max_lat, max_lat); + let phi = (phi.tan() + (1.0 / phi.cos())).ln(); + let x = xmax / (2.0 * PI) * (lambda + PI); + let y = ymax * (1.0 - phi / PI) / 2.0; (x.floor() as u64, y.floor() as u64) } } -- cgit v1.2.3 From 6ec81bd17e18eed3de705a07a86bd1bf239e6ba3 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 26 Aug 2025 21:49:03 +0200 Subject: add first tests --- Cargo.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ hittekaart/Cargo.toml | 1 + hittekaart/src/gpx.rs | 33 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0594ef5..d67ed63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,6 +558,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "half" version = "2.6.0" @@ -615,6 +621,7 @@ dependencies = [ "num-traits", "rayon", "roxmltree", + "rstest", "rusqlite", "thiserror", ] @@ -1310,6 +1317,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "roxmltree" version = "0.18.1" @@ -1319,6 +1332,32 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rusqlite" version = "0.29.0" @@ -1339,6 +1378,15 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusttype" version = "0.9.3" @@ -1379,6 +1427,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" diff --git a/hittekaart/Cargo.toml b/hittekaart/Cargo.toml index 62ec6e9..651aec4 100644 --- a/hittekaart/Cargo.toml +++ b/hittekaart/Cargo.toml @@ -27,3 +27,4 @@ thiserror = "2.0.12" [dev-dependencies] criterion = "0.5.0" +rstest = { version = "0.26.1", default-features = false } diff --git a/hittekaart/src/gpx.rs b/hittekaart/src/gpx.rs index 86cb88b..8bd076d 100644 --- a/hittekaart/src/gpx.rs +++ b/hittekaart/src/gpx.rs @@ -60,6 +60,8 @@ impl Coordinates { let ymax = 2u64.pow(zoom) as f64 * HEIGHT; let lambda = self.longitude.to_radians().clamp(-max_lon, max_lon); + // The coordinate +180° is the same as -180°, so we want to map them to the same point + let lambda = if lambda == PI { -PI } else { lambda }; let phi = self.latitude.to_radians().clamp(-max_lat, max_lat); let phi = (phi.tan() + (1.0 / phi.cos())).ln(); let x = xmax / (2.0 * PI) * (lambda + PI); @@ -171,3 +173,34 @@ pub fn extract_from_file>( }; extract_from_str(&content) } + +#[cfg(test)] +mod test { + use super::*; + use rstest::*; + + #[rstest] + #[case((0.0, 0.0), 0, (128, 128))] + #[case((-180.0, 0.0), 0, (0, 128))] + #[case((180.0, 0.0), 0, (0, 128))] + #[case((179.99, 0.0), 0, (255, 128))] + #[case((0.0, 90.0), 0, (128, 0))] + #[case((0.0, -90.0), 0, (128, 255))] + #[case((0.0, 0.0), 4, (2048, 2048))] + #[case((-180.0, 0.0), 4, (0, 2048))] + #[case((180.0, 0.0), 4, (0, 2048))] + #[case((179.99, 0.0), 4, (4095, 2048))] + #[case((0.0, 90.0), 4, (2048, 0))] + #[case((0.0, -90.0), 4, (2048, 4095))] + #[case((-90.0, 0.0), 0, (64, 128))] + #[case((90.0, 0.0), 0, (192, 128))] + // Example taken from + // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Example:_Convert_a_GPS_coordinate_to_a_pixel_position_in_a_Web_Mercator_tile + #[case((139.7006793, 35.6590699), 18, (232798 * 256 + 238, 103246 * 256 + 105))] + fn web_mercator(#[case] input: (f64, f64), #[case] zoom: u32, #[case] expected: (u64, u64)) { + assert_eq!( + Coordinates::new(input.0, input.1).web_mercator(zoom), + expected + ); + } +} -- cgit v1.2.3 From 0f55c686ef050abdb57fccd196d7208858ad34c8 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 27 Nov 2025 22:37:09 +0100 Subject: add preliminary test for heatmap_prepare --- hittekaart/src/renderer/mod.rs | 47 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/hittekaart/src/renderer/mod.rs b/hittekaart/src/renderer/mod.rs index 40fd5fa..696c1e5 100644 --- a/hittekaart/src/renderer/mod.rs +++ b/hittekaart/src/renderer/mod.rs @@ -3,10 +3,7 @@ use std::thread; use crossbeam_channel::Sender; -use super::{ - error::Result, - gpx::Coordinates, -}; +use super::{error::Result, gpx::Coordinates}; pub mod heatmap; pub mod marktile; @@ -99,3 +96,45 @@ pub fn colorize Result<()>>( colorizer.join().unwrap() }) } + +#[cfg(test)] +mod test { + use rstest::rstest; + use super::*; + + #[rstest] + fn test_heatmap_prepare() { + let tracks = &[ + vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], + vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], + vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], + ]; + let mut prep = prepare(&heatmap::Renderer, 0, &tracks[..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, &tracks[..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, &tracks[..3], || Ok(())).unwrap(); + for (x, y) in ones { + assert_eq!(prep.tile_mut(0, 0).get_pixel(*x, *y).0[0], 3); + } + } +} -- cgit v1.2.3 From a7c9cf08061d695f73a7e8bc44050d289030d8af Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 12:28:13 +0100 Subject: more tests for heatmap renderer --- hittekaart/src/renderer/mod.rs | 67 +++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/hittekaart/src/renderer/mod.rs b/hittekaart/src/renderer/mod.rs index 696c1e5..f9236f2 100644 --- a/hittekaart/src/renderer/mod.rs +++ b/hittekaart/src/renderer/mod.rs @@ -99,42 +99,75 @@ pub fn colorize Result<()>>( #[cfg(test)] mod test { - use rstest::rstest; use super::*; + use rstest::rstest; + + fn tracks() -> Vec> { + 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() { - let tracks = &[ - vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], - vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], - vec![Coordinates { latitude: 52.520008, longitude: 13.404954 }], - ]; - let mut prep = prepare(&heatmap::Renderer, 0, &tracks[..1], || Ok(())).unwrap(); + 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), - ]; + 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, &tracks[..2], || Ok(())).unwrap(); + 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, &tracks[..3], || Ok(())).unwrap(); + 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); + } + } } -- cgit v1.2.3