From 886358e93fc88b2f67262a308172bc70b1d1efa6 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 15 Dec 2023 01:11:31 -0500 Subject: [PATCH] Fix bbox to xyz roundtrip computation (#1059) Add `tile-grid` as a dependency to use its webmercator tile math. --------- Co-authored-by: sharkAndshark --- Cargo.lock | 83 +++++++++++++++-- Cargo.toml | 1 + martin-tile-utils/Cargo.toml | 1 + martin-tile-utils/src/lib.rs | 92 +++++++++++++++---- mbtiles/src/summary.rs | 74 +++++++-------- .../martin-cp/flat-with-hash_summary.txt | 2 +- tests/expected/martin-cp/flat_summary.txt | 4 +- 7 files changed, 190 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc8d9215..aabc6024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "brotli", "bytes", @@ -429,6 +429,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.5" @@ -582,8 +588,10 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1881,7 +1889,7 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_with", + "serde_with 3.4.0", "serde_yaml", "spreet", "subst", @@ -1896,6 +1904,7 @@ name = "martin-tile-utils" version = "0.2.0" dependencies = [ "approx", + "tile-grid", ] [[package]] @@ -1916,7 +1925,7 @@ dependencies = [ "rstest", "serde", "serde_json", - "serde_with", + "serde_with 3.4.0", "serde_yaml", "size_format", "sqlite-hashes", @@ -2412,7 +2421,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64", + "base64 0.21.5", "byteorder", "bytes", "fallible-iterator 0.2.0", @@ -2883,7 +2892,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.5", ] [[package]] @@ -3022,6 +3031,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.41", +] + [[package]] name = "serde_tuple" version = "0.5.0" @@ -3055,23 +3075,51 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros 2.3.3", + "time", +] + [[package]] name = "serde_with" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.1.0", "serde", "serde_json", - "serde_with_macros", + "serde_with_macros 3.4.0", "time", ] +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.41", +] + [[package]] name = "serde_with_macros" version = "3.4.0" @@ -3373,7 +3421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "byteorder", "bytes", @@ -3415,7 +3463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "byteorder", "crc", @@ -3617,6 +3665,21 @@ dependencies = [ "syn 2.0.41", ] +[[package]] +name = "tile-grid" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b57cef548fda6f42e870ed9106b6c15af81f34e4b7f679d8620b3be21d02039" +dependencies = [ + "chrono", + "once_cell", + "serde", + "serde_json", + "serde_repr", + "serde_with 2.3.3", + "thiserror", +] + [[package]] name = "tilejson" version = "0.4.1" @@ -3992,7 +4055,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51daa774fe9ee5efcf7b4fec13019b8119cda764d9a8b5b06df02bb1445c656" dependencies = [ - "base64", + "base64 0.21.5", "log", "pico-args", "usvg-parser", diff --git a/Cargo.toml b/Cargo.toml index dcfff749..292b8d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ sqlite-hashes = { version = "0.5", default-features = false, features = ["md5", sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] } subst = { version = "0.3", features = ["yaml"] } thiserror = "1" +tile-grid = "0.5" tilejson = "0.4" tokio = { version = "1", features = ["macros"] } tokio-postgres-rustls = "0.10" diff --git a/martin-tile-utils/Cargo.toml b/martin-tile-utils/Cargo.toml index a61a8509..6ed71e7d 100644 --- a/martin-tile-utils/Cargo.toml +++ b/martin-tile-utils/Cargo.toml @@ -17,6 +17,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] +tile-grid.workspace = true [dev-dependencies] approx.workspace = true diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index f41d685d..efbbfa9c 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -6,10 +6,18 @@ use std::f64::consts::PI; use std::fmt::Display; +use tile_grid::{tms, Tms, Xyz}; + pub const EARTH_CIRCUMFERENCE: f64 = 40_075_016.685_578_5; pub const EARTH_RADIUS: f64 = EARTH_CIRCUMFERENCE / 2.0 / PI; pub const MAX_ZOOM: u8 = 30; +use std::sync::OnceLock; + +fn web_merc() -> &'static Tms { + static TMS: OnceLock = OnceLock::new(); + TMS.get_or_init(|| tms().lookup("WebMercatorQuad").unwrap()) +} #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Format { @@ -190,27 +198,22 @@ impl Display for TileInfo { #[must_use] #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn tile_index(lon: f64, lat: f64, zoom: u8) -> (u32, u32) { - let n = f64::from(1_u32 << zoom); - let x = ((lon + 180.0) / 360.0 * n).floor() as u32; - let y = ((1.0 - (lat.to_radians().tan() + 1.0 / lat.to_radians().cos()).ln() / PI) / 2.0 * n) - .floor() as u32; - let max_value = (1_u32 << zoom) - 1; - (x.min(max_value), y.min(max_value)) + assert!(zoom <= MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); + let tile = web_merc().tile(lon, lat, zoom).unwrap(); + let max_value = (1_u64 << zoom) - 1; + (tile.x.min(max_value) as u32, tile.y.min(max_value) as u32) } /// Convert min/max XYZ tile coordinates to a bounding box values. /// The result is `[min_lng, min_lat, max_lng, max_lat]` #[must_use] -pub fn xyz_to_bbox(zoom: u8, min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> [f64; 4] { - let tile_size = EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom); - let (min_lng, min_lat) = webmercator_to_wgs84( - -0.5 * EARTH_CIRCUMFERENCE + f64::from(min_x) * tile_size, - -0.5 * EARTH_CIRCUMFERENCE + f64::from(min_y) * tile_size, - ); - let (max_lng, max_lat) = webmercator_to_wgs84( - -0.5 * EARTH_CIRCUMFERENCE + f64::from(max_x + 1) * tile_size, - -0.5 * EARTH_CIRCUMFERENCE + f64::from(max_y + 1) * tile_size, - ); +pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> [f64; 4] { + assert!(zoom <= MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); + let left_top_bounds = web_merc().xy_bounds(&Xyz::new(u64::from(min_x), u64::from(min_y), zoom)); + let right_bottom_bounds = + web_merc().xy_bounds(&Xyz::new(u64::from(max_x), u64::from(max_y), zoom)); + let (min_lng, min_lat) = webmercator_to_wgs84(left_top_bounds.left, right_bottom_bounds.bottom); + let (max_lng, max_lat) = webmercator_to_wgs84(right_bottom_bounds.right, left_top_bounds.top); [min_lng, min_lat, max_lng, max_lat] } @@ -228,6 +231,7 @@ pub fn bbox_to_xyz(left: f64, bottom: f64, right: f64, top: f64, zoom: u8) -> (u #[must_use] #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn get_zoom_precision(zoom: u8) -> usize { + assert!(zoom < MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); let lng_delta = webmercator_to_wgs84(EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom), 0.0).0; let log = lng_delta.log10() - 0.5; if log > 0.0 { @@ -246,6 +250,8 @@ pub fn webmercator_to_wgs84(x: f64, y: f64) -> (f64, f64) { #[cfg(test)] mod tests { + #![allow(clippy::unreadable_literal)] + use std::fs::read; use approx::assert_relative_eq; @@ -293,7 +299,59 @@ mod tests { } #[test] - #[allow(clippy::unreadable_literal)] + fn test_xyz_to_bbox() { + // you could easily get test cases from maptiler: https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection/#4/-118.82/71.02 + let bbox = xyz_to_bbox(0, 0, 0, 0, 0); + assert_relative_eq!(bbox[0], -179.99999999999955, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[1], -85.0511287798066, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[2], 179.99999999999986, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[3], 85.05112877980655, epsilon = f64::EPSILON * 2.0); + + let xyz = bbox_to_xyz(bbox[0], bbox[1], bbox[2], bbox[3], 0); + assert_eq!(xyz, (0, 0, 0, 0)); + + let bbox = xyz_to_bbox(1, 0, 0, 0, 0); + assert_relative_eq!(bbox[0], -179.99999999999955, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[1], 2.007891127734306e-13, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!( + bbox[2], + -2.007891127734306e-13, + epsilon = f64::EPSILON * 2.0 + ); + assert_relative_eq!(bbox[3], 85.05112877980655, epsilon = f64::EPSILON * 2.0); + + let xyz = bbox_to_xyz(bbox[0], bbox[1], bbox[2], bbox[3], 1); + assert!(xyz.0 == 0 || xyz.0 == 1); + assert!(xyz.1 == 0); + assert!(xyz.2 == 0 || xyz.2 == 1); + assert!(xyz.3 == 0 || xyz.3 == 1); + + let bbox = xyz_to_bbox(5, 1, 1, 2, 2); + assert_relative_eq!(bbox[0], -168.74999999999955, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[1], 81.09321385260832, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[2], -146.2499999999996, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[3], 83.979259498862, epsilon = f64::EPSILON * 2.0); + + let xyz = bbox_to_xyz(bbox[0], bbox[1], bbox[2], bbox[3], 5); + assert!(xyz.0 == 1 || xyz.0 == 2); + assert!(xyz.1 == 0 || xyz.1 == 1); + assert!(xyz.2 == 2 || xyz.2 == 3); + assert!(xyz.3 == 2 || xyz.3 == 3); + + let bbox = xyz_to_bbox(5, 1, 3, 2, 5); + assert_relative_eq!(bbox[0], -168.74999999999955, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[1], 74.01954331150218, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[2], -146.2499999999996, epsilon = f64::EPSILON * 2.0); + assert_relative_eq!(bbox[3], 81.09321385260832, epsilon = f64::EPSILON * 2.0); + + let xyz = bbox_to_xyz(bbox[0], bbox[1], bbox[2], bbox[3], 5); + assert!(xyz.0 == 1 || xyz.0 == 2); + assert!(xyz.1 == 2 || xyz.1 == 3); + assert!(xyz.2 == 2 || xyz.2 == 3); + assert!(xyz.3 == 5 || xyz.3 == 6); + } + + #[test] fn meter_to_lng_lat() { let (lng, lat) = webmercator_to_wgs84(-20037508.34, -20037508.34); assert_relative_eq!(lng, -179.9999999749437, epsilon = f64::EPSILON * 2.0); diff --git a/mbtiles/src/summary.rs b/mbtiles/src/summary.rs index ae4298f5..b7eb474d 100644 --- a/mbtiles/src/summary.rs +++ b/mbtiles/src/summary.rs @@ -14,7 +14,7 @@ use size_format::SizeFormatterBinary; use sqlx::{query, SqliteExecutor}; use tilejson::Bounds; -use crate::{MbtResult, MbtType, Mbtiles}; +use crate::{invert_y_value, MbtResult, MbtType, Mbtiles}; #[derive(Clone, Debug, PartialEq, Serialize)] pub struct ZoomInfo { @@ -154,10 +154,10 @@ impl Mbtiles { avg_tile_size: r.average.unwrap_or(0.0), bbox: xyz_to_bbox( zoom, - r.min_tile_x.unwrap(), - r.min_tile_y.unwrap(), - r.max_tile_x.unwrap(), - r.max_tile_y.unwrap(), + r.min_tile_x.unwrap() as u32, + invert_y_value(zoom, r.max_tile_y.unwrap() as u32), + r.max_tile_x.unwrap() as u32, + invert_y_value(zoom, r.min_tile_y.unwrap() as u32), ) .into(), } @@ -239,10 +239,10 @@ mod tests { max_tile_size: 1107 avg_tile_size: 96.2295918367347 bbox: - - -180 - - -85.0511287798066 - - 180 - - 85.0511287798066 + - -179.99999999999955 + - -85.05112877980659 + - 180.00000000000028 + - 85.05112877980655 min_zoom: 0 max_zoom: 6 zoom_info: @@ -252,70 +252,70 @@ mod tests { max_tile_size: 1107 avg_tile_size: 1107 bbox: - - -180 - - -85.0511287798066 - - 180 - - 85.0511287798066 + - -179.99999999999955 + - -85.05112877980659 + - 179.99999999999986 + - 85.05112877980655 - zoom: 1 tile_count: 4 min_tile_size: 160 max_tile_size: 650 avg_tile_size: 366.5 bbox: - - -180 - - -85.0511287798066 - - 180 - - 85.0511287798066 + - -179.99999999999955 + - -85.05112877980652 + - 179.99999999999915 + - 85.05112877980655 - zoom: 2 tile_count: 7 min_tile_size: 137 max_tile_size: 495 avg_tile_size: 239.57142857142858 bbox: - - -180 - - -66.51326044311186 - - 180 - - 66.51326044311186 + - -179.99999999999955 + - -66.51326044311165 + - 179.99999999999915 + - 66.51326044311182 - zoom: 3 tile_count: 17 min_tile_size: 67 max_tile_size: 246 avg_tile_size: 134 bbox: - - -135 - - -40.97989806962013 - - 180 - - 66.51326044311186 + - -134.99999999999957 + - -40.979898069620376 + - 180.00000000000028 + - 66.51326044311169 - zoom: 4 tile_count: 38 min_tile_size: 64 max_tile_size: 175 avg_tile_size: 86 bbox: - - -135 - - -40.97989806962013 - - 180 - - 66.51326044311186 + - -134.99999999999963 + - -40.979898069620106 + - 179.99999999999966 + - 66.51326044311175 - zoom: 5 tile_count: 57 min_tile_size: 64 max_tile_size: 107 avg_tile_size: 72.7719298245614 bbox: - - -123.75000000000001 - - -40.97989806962013 - - 180 - - 61.60639637138628 + - -123.74999999999966 + - -40.979898069620106 + - 179.99999999999966 + - 61.606396371386154 - zoom: 6 tile_count: 72 min_tile_size: 64 max_tile_size: 97 avg_tile_size: 68.29166666666667 bbox: - - -123.75000000000001 - - -40.97989806962013 - - 180 - - 61.60639637138628 + - -123.74999999999957 + - -40.979898069620305 + - 180.00000000000009 + - 61.606396371386104 "###); Ok(()) diff --git a/tests/expected/martin-cp/flat-with-hash_summary.txt b/tests/expected/martin-cp/flat-with-hash_summary.txt index fedde321..25b81ef4 100644 --- a/tests/expected/martin-cp/flat-with-hash_summary.txt +++ b/tests/expected/martin-cp/flat-with-hash_summary.txt @@ -7,7 +7,7 @@ Page size: 512B 1 | 4 | 474B | 983B | 609B | -180,-85,180,85 2 | 5 | 150B | 865B | 451B | -90,-67,180,67 3 | 8 | 57B | 839B | 264B | -45,-41,180,67 - 4 | 13 | 57B | 751B | 216B | -23,-22,158,56 + 4 | 13 | 57B | 751B | 216B | -22,-22,157,56 5 | 27 | 57B | 666B | 167B | -11,-11,146,49 6 | 69 | 57B | 636B | 127B | -6,-6,146,45 all | 127 | 57B | 983B | 187B | -180,-85,180,85 diff --git a/tests/expected/martin-cp/flat_summary.txt b/tests/expected/martin-cp/flat_summary.txt index 992a63bb..538d464e 100644 --- a/tests/expected/martin-cp/flat_summary.txt +++ b/tests/expected/martin-cp/flat_summary.txt @@ -4,10 +4,10 @@ Page size: 512B Zoom | Count | Smallest | Largest | Average | Bounding Box 0 | 1 | 643B | 643B | 643B | -180,-85,180,85 - 1 | 2 | 150B | 172B | 161B | -180,-85,0,85 + 1 | 2 | 150B | 172B | 161B | -180,-85,-0,85 2 | 4 | 291B | 690B | 414B | -90,-67,90,67 3 | 7 | 75B | 727B | 263B | -45,-41,90,67 - 4 | 13 | 75B | 684B | 225B | -23,-22,158,56 + 4 | 13 | 75B | 684B | 225B | -22,-22,157,56 5 | 27 | 75B | 659B | 195B | -11,-11,146,49 6 | 69 | 75B | 633B | 155B | -6,-6,146,45 all | 123 | 75B | 727B | 190B | -180,-85,180,85