aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-11-29 12:28:29 +0100
committerDaniel Schadt <kingdread@gmx.de>2025-11-29 12:28:29 +0100
commit96c8a9cfe0565f76060c369fb98542c4cf6e7d00 (patch)
tree1dec973861a0221af4a7a568319769f1c006d477
parent38b533918afe4c12de1042d0cb8bf690c4ed0b8b (diff)
parenta7c9cf08061d695f73a7e8bc44050d289030d8af (diff)
downloadhittekaart-96c8a9cfe0565f76060c369fb98542c4cf6e7d00.tar.gz
hittekaart-96c8a9cfe0565f76060c369fb98542c4cf6e7d00.tar.bz2
hittekaart-96c8a9cfe0565f76060c369fb98542c4cf6e7d00.zip
Merge branch 'tests'
-rw-r--r--Cargo.lock54
-rw-r--r--hittekaart/Cargo.toml1
-rw-r--r--hittekaart/src/gpx.rs60
-rw-r--r--hittekaart/src/renderer/mod.rs80
4 files changed, 186 insertions, 9 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1f1a483..0a85c6b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -559,6 +559,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -615,6 +621,7 @@ dependencies = [
"num-traits",
"rayon",
"roxmltree",
+ "rstest",
"rusqlite",
"thiserror",
]
@@ -1309,6 +1316,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1318,6 +1331,32 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1338,6 +1377,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1378,6 +1426,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 68bd6fb..8bd076d 100644
--- a/hittekaart/src/gpx.rs
+++ b/hittekaart/src/gpx.rs
@@ -38,15 +38,34 @@ 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);
+ // 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);
+ let y = ymax * (1.0 - phi / PI) / 2.0;
(x.floor() as u64, y.floor() as u64)
}
}
@@ -154,3 +173,34 @@ pub fn extract_from_file<P: AsRef<Path>>(
};
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
+ );
+ }
+}
diff --git a/hittekaart/src/renderer/mod.rs b/hittekaart/src/renderer/mod.rs
index 40fd5fa..f9236f2 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,78 @@ pub fn colorize<R: Renderer, F: FnMut(RenderedTile) -> Result<()>>(
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);
+ }
+ }
+}