Fix bbox to xyz roundtrip computation (#1059)

Add `tile-grid` as a dependency to use its webmercator tile math.

---------

Co-authored-by: sharkAndshark <zhangyijunmetro@hotmail.com>
This commit is contained in:
Yuri Astrakhan 2023-12-15 01:11:31 -05:00 committed by GitHub
parent ce89a44a05
commit 886358e93f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 67 deletions

83
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -17,6 +17,7 @@ repository.workspace = true
rust-version.workspace = true
[dependencies]
tile-grid.workspace = true
[dev-dependencies]
approx.workspace = true

View File

@ -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<Tms> = 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);

View File

@ -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(())

View File

@ -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

View File

@ -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