oxedyne/fe2o3/fe2o3_geom/tests/tiles.rs
34.1 KiB, 2 runs
created by r1870400018:60838, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Map tiles: the grid, PMTiles and Mapbox Vector Tiles. |
| 2 | //! |
| 3 | //! The oracles come from outside this library: PROJ 9.7.1 for the tile corners, the vector |
| 4 | //! tile specification's own fixtures (`@mapbox/mvt-fixtures` 4.0.0, CC0) for the decoder, and |
| 5 | //! for real data the Protomaps planet build as read by the reference JavaScript readers |
| 6 | //! (`tests/data/protomaps/NOTICE.md`). |
| 7 | |
| 8 | use oxedyne_fe2o3_geom::{ |
| 9 | mvt::{ |
| 10 | self, |
| 11 | GeomKind, |
| 12 | StyleRule, |
| 13 | Value, |
| 14 | }, |
| 15 | planar::Pt, |
| 16 | proj::{ |
| 17 | Projection, |
| 18 | Viewport, |
| 19 | }, |
| 20 | tile::{ |
| 21 | self, |
| 22 | pmtiles::{ |
| 23 | self, |
| 24 | Archive, |
| 25 | Compression, |
| 26 | FileSource, |
| 27 | RangeSource, |
| 28 | TileType, |
| 29 | }, |
| 30 | TileId, |
| 31 | }, |
| 32 | }; |
| 33 | |
| 34 | use oxedyne_fe2o3_core::prelude::*; |
| 35 | use oxedyne_fe2o3_jdat::{ |
| 36 | prelude::*, |
| 37 | string::dec::DecoderConfig, |
| 38 | usr::{ |
| 39 | UsrKind, |
| 40 | UsrKindCode, |
| 41 | UsrKindId, |
| 42 | }, |
| 43 | }; |
| 44 | |
| 45 | use std::{ |
| 46 | collections::{ |
| 47 | BTreeMap, |
| 48 | HashSet, |
| 49 | }, |
| 50 | path::PathBuf, |
| 51 | }; |
| 52 | |
| 53 | fn data(rel: &str) -> PathBuf { |
| 54 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data").join(rel) |
| 55 | } |
| 56 | |
| 57 | fn json(text: &str) -> Outcome<Dat> { |
| 58 | let cfg = DecoderConfig::<BTreeMap<UsrKindCode, UsrKind>, BTreeMap<String, UsrKindId>>::json(None); |
| 59 | Dat::decode_string_with_config(text, &cfg) |
| 60 | } |
| 61 | |
| 62 | fn json_file(rel: &str) -> Outcome<Dat> { |
| 63 | let text = res!(std::fs::read_to_string(data(rel)), File, Read); |
| 64 | json(&text) |
| 65 | } |
| 66 | |
| 67 | fn get<'a>(d: &'a Dat, key: &str) -> Outcome<Option<&'a Dat>> { |
| 68 | d.map_get(&dat!(key)) |
| 69 | } |
| 70 | |
| 71 | fn list(d: &Dat) -> Outcome<&Vec<Dat>> { |
| 72 | match d { |
| 73 | Dat::List(v) => Ok(v), |
| 74 | other => Err(err!("Expected a list, found {:?}.", other.kind(); Test, Mismatch)), |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /// A JSON integer, exactly, whatever width the decoder gave it. |
| 79 | fn int(d: &Dat) -> Outcome<i128> { |
| 80 | match d { |
| 81 | Dat::U8(v) => Ok(*v as i128), |
| 82 | Dat::U16(v) => Ok(*v as i128), |
| 83 | Dat::U32(v) => Ok(*v as i128), |
| 84 | Dat::U64(v) => Ok(*v as i128), |
| 85 | Dat::I8(v) => Ok(*v as i128), |
| 86 | Dat::I16(v) => Ok(*v as i128), |
| 87 | Dat::I32(v) => Ok(*v as i128), |
| 88 | Dat::I64(v) => Ok(*v as i128), |
| 89 | Dat::U128(v) => Ok(*v as i128), |
| 90 | Dat::I128(v) => Ok(*v), |
| 91 | other => Err(err!("Expected an integer, found {:?}.", other; Test, Mismatch)), |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | fn num(d: &Dat) -> Outcome<f64> { |
| 96 | match d.get_float64() { |
| 97 | Some(f) => Ok(f.0), |
| 98 | None => Err(err!("Expected a number, found {:?}.", d; Test, Mismatch)), |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | fn string(d: &Dat) -> Outcome<String> { |
| 103 | match d { |
| 104 | Dat::Str(s) => Ok(s.clone()), |
| 105 | other => Err(err!("Expected a string, found {:?}.", other; Test, Mismatch)), |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | // --------------------------------------------------------------------------------------------- |
| 110 | // The grid |
| 111 | // --------------------------------------------------------------------------------------------- |
| 112 | |
| 113 | #[test] |
| 114 | fn test_tile_corners_agree_with_proj_00() -> Outcome<()> { |
| 115 | // proj -I -f "%.12f" +proj=webmerc +R=1 of each tile's north-west and south-east corners, |
| 116 | // as lng, lat. |
| 117 | for (z, x, y, nw, se) in [ |
| 118 | (0u8, 0u32, 0u32, (-180.0, 85.051128779807), (180.0, -85.051128779807)), |
| 119 | (15, 26929, 19456, (115.850830078125, -31.952162238025), (115.861816406250, -31.961483557269)), |
| 120 | (12, 3768, 2457, (151.171875000000, -33.797408767572), (151.259765625000, -33.870415550942)), |
| 121 | (7, 0, 127, (-180.0, -84.802473724335), (-177.1875, -85.051128779807)), |
| 122 | (20, 1048575, 0, (179.999656677246, 85.051128779807), (180.0, 85.051099162384)), |
| 123 | ] { |
| 124 | let t = res!(TileId::new(z, x, y)); |
| 125 | for ((u, v), (lng, lat)) in [((0.0, 0.0), nw), ((1.0, 1.0), se)] { |
| 126 | let (a, b) = t.point(u, v); |
| 127 | let near = (a - lat).abs() < 1.0e-9 && (b - lng).abs() < 1.0e-9; |
| 128 | req!(near, true, "{} at ({}, {}) is {}, {}; PROJ says {}, {}.", t, u, v, a, b, lat, lng); |
| 129 | } |
| 130 | } |
| 131 | // proj -f "%.15f" +proj=webmerc +R=1, and the grid position that follows from it at z15. |
| 132 | let n = (1u64 << 15) as f64; |
| 133 | for (lat, lng, xm, ym) in [ |
| 134 | (-31.9535, 115.8571, 2.022087856812322, -0.589076140272944), |
| 135 | (-33.8688, 151.2093, 2.639100144635862, -0.628898163728307), |
| 136 | (0.0, 0.0, 0.0, 0.0), |
| 137 | ] { |
| 138 | let tx = (xm + std::f64::consts::PI) / std::f64::consts::TAU * n; |
| 139 | let ty = (std::f64::consts::PI - ym) / std::f64::consts::TAU * n; |
| 140 | let near = (tile::lng_to_tx(lng, 15) - tx).abs() < 1.0e-6 && (tile::lat_to_ty(lat, 15) - ty).abs() < 1.0e-6; |
| 141 | req!(near, true, "{}, {} lands at {}, {}; PROJ says {}, {}.", lat, lng, |
| 142 | tile::lng_to_tx(lng, 15), tile::lat_to_ty(lat, 15), tx, ty); |
| 143 | let t = res!(TileId::at(lat, lng, 15)); |
| 144 | req!((t.x, t.y), (tx.floor() as u32, ty.floor() as u32)); |
| 145 | } |
| 146 | // A pole lands on the edge row, and the antimeridian wraps. |
| 147 | req!(res!(TileId::at(90.0, 0.0, 3)).y, 0); |
| 148 | req!(res!(TileId::at(-90.0, 0.0, 3)).y, 7); |
| 149 | req!(res!(TileId::at(0.0, 180.0, 3)).x, 0); |
| 150 | req!(res!(TileId::at(0.0, -180.0, 3)).x, 0); |
| 151 | req!(TileId::new(3, 8, 0).is_err(), true, "A tile off the grid was made."); |
| 152 | Ok(()) |
| 153 | } |
| 154 | |
| 155 | #[test] |
| 156 | fn test_overzoom_finds_the_part_of_an_ancestor_01() -> Outcome<()> { |
| 157 | let t = res!(TileId::new(17, 107718, 77826)); |
| 158 | let a = res!(t.ancestor(15)); |
| 159 | req!((a.z, a.x, a.y), (15, 26929, 19456)); |
| 160 | let (u, v, span) = res!(t.within(&a).ok_or_else(|| err!("Not within."; Test))); |
| 161 | req!((u, v, span), (0.5, 0.5, 4.0)); |
| 162 | req!(t.within(&res!(TileId::new(15, 0, 0))).is_none(), true, "A stranger contained it."); |
| 163 | let kids = res!(a.children()); |
| 164 | for k in kids.iter() { |
| 165 | req!(res!(k.ancestor(15)), a); |
| 166 | } |
| 167 | Ok(()) |
| 168 | } |
| 169 | |
| 170 | #[test] |
| 171 | fn test_the_screen_is_covered_by_its_tiles_02() -> Outcome<()> { |
| 172 | for (lat, lng, heading, m, z) in [ |
| 173 | (-31.9535, 115.8571, 0.0, 5.0, 15u8), |
| 174 | (-31.9535, 115.8571, 30.0, 5.0, 15), |
| 175 | (51.5, -0.12, 200.0, 40.0, 13), |
| 176 | (0.5, 179.99, 0.0, 20_000.0, 3), |
| 177 | (70.0, 20.0, 0.0, 800.0, 9), |
| 178 | ] { |
| 179 | let v = res!(Viewport::new(Projection::WebMercator, lat, lng, heading, m, 390.0, 700.0)); |
| 180 | let got = res!(v.tiles_covering(z, 10_000)); |
| 181 | let set: HashSet<TileId> = got.iter().cloned().collect(); |
| 182 | req!(set.len(), got.len(), "A tile was listed twice."); |
| 183 | req!(got[0], res!(TileId::at(lat, lng, z)), "The first tile is not the centre's."); |
| 184 | // Every pixel's tile is listed. |
| 185 | for i in 0..=39 { |
| 186 | for j in 0..=70 { |
| 187 | let pt = Pt::new(390.0 * i as f64 / 39.0, 700.0 * j as f64 / 70.0); |
| 188 | if let Some((a, b)) = v.inverse(pt) { |
| 189 | let t = res!(TileId::at(a, b, z)); |
| 190 | req!(set.contains(&t), true, "{} under ({}, {}) is not listed.", t, pt.x, pt.y); |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | // And every listed tile reaches the screen. |
| 195 | for t in &got { |
| 196 | let (aff, _) = res!(v.tile_affine(t).ok_or_else(|| err!("No transform."; Test))); |
| 197 | let mut xs = Vec::new(); |
| 198 | let mut ys = Vec::new(); |
| 199 | for (u, w) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] { |
| 200 | let (x, y) = aff.apply(u, w); |
| 201 | xs.push(x); |
| 202 | ys.push(y); |
| 203 | } |
| 204 | let reach = xs.iter().cloned().fold(f64::MIN, f64::max) >= 0.0 |
| 205 | && xs.iter().cloned().fold(f64::MAX, f64::min) <= 390.0 |
| 206 | && ys.iter().cloned().fold(f64::MIN, f64::max) >= 0.0 |
| 207 | && ys.iter().cloned().fold(f64::MAX, f64::min) <= 700.0; |
| 208 | // A wrapped copy may lie a world away from the one the transform picks. |
| 209 | let wraps = v.bounding_cap().1 >= std::f64::consts::PI; |
| 210 | req!(reach || wraps, true, "{} is listed but off the screen.", t); |
| 211 | } |
| 212 | } |
| 213 | // The globe: every pixel's tile is listed. |
| 214 | for (lat, lng, m, z) in [(-31.9535, 115.8571, 50.0, 12u8), (60.0, -150.0, 20_000.0, 4), |
| 215 | (-89.0, 0.0, 5_000.0, 6)] |
| 216 | { |
| 217 | let v = res!(Viewport::new(Projection::Orthographic, lat, lng, 25.0, m, 390.0, 700.0)); |
| 218 | let got = res!(v.tiles_covering(z, 20_000)); |
| 219 | let set: HashSet<TileId> = got.iter().cloned().collect(); |
| 220 | for i in 0..=39 { |
| 221 | for j in 0..=70 { |
| 222 | let pt = Pt::new(390.0 * i as f64 / 39.0, 700.0 * j as f64 / 70.0); |
| 223 | if let Some((a, b)) = v.inverse(pt) { |
| 224 | if a.abs() > 85.05 { |
| 225 | continue; |
| 226 | } |
| 227 | let t = res!(TileId::at(a, b, z)); |
| 228 | req!(set.contains(&t), true, "Globe: {} under ({}, {}) is not listed.", t, pt.x, pt.y); |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | } |
| 233 | // Too many tiles are refused. |
| 234 | let v = res!(Viewport::new(Projection::WebMercator, 0.0, 0.0, 0.0, 20_000.0, 390.0, 700.0)); |
| 235 | req!(v.tiles_covering(15, 1000).is_err(), true, "A world of zoom-15 tiles was listed."); |
| 236 | Ok(()) |
| 237 | } |
| 238 | |
| 239 | #[test] |
| 240 | fn test_a_tile_bitmap_lands_where_the_projection_does_03() -> Outcome<()> { |
| 241 | // On the flat map exactly, at any heading. |
| 242 | let v = res!(Viewport::new(Projection::WebMercator, -31.9535, 115.8571, 37.0, 3.0, 390.0, 700.0)); |
| 243 | for t in res!(v.tiles_covering(15, 1000)) { |
| 244 | let (aff, err) = res!(v.tile_affine(&t).ok_or_else(|| err!("No transform."; Test))); |
| 245 | req!(err, 0.0); |
| 246 | for (u, w) in [(0.0, 0.0), (1.0, 0.0), (0.3, 0.8), (1.0, 1.0)] { |
| 247 | let (lat, lng) = t.point(u, w); |
| 248 | let p = res!(v.forward(lat, lng).ok_or_else(|| err!("Lost."; Test))); |
| 249 | let (x, y) = aff.apply(u, w); |
| 250 | let near = (p.x - x).abs() < 1.0e-6 && (p.y - y).abs() < 1.0e-6; |
| 251 | req!(near, true, "{} at ({}, {}) paints at {}, {}, the map at {}, {}.", t, u, w, x, y, p.x, p.y); |
| 252 | } |
| 253 | } |
| 254 | // On the globe the error grows with the view, as the square of its size: under half a |
| 255 | // pixel for a 12 km view at Perth, over a pixel at 100 km. |
| 256 | let mut worst = Vec::new(); |
| 257 | for km in [12.0, 20.0, 100.0] { |
| 258 | let m = km * 1000.0 / 700.0; |
| 259 | let v = res!(Viewport::new(Projection::Orthographic, -31.9535, 115.8571, 0.0, m, 390.0, 700.0)); |
| 260 | let z = 15 - (km / 12.0f64).log2().ceil().max(0.0) as u8; |
| 261 | let mut e: f64 = 0.0; |
| 262 | for t in res!(v.tiles_covering(z, 1000)) { |
| 263 | if let Some((_, err)) = v.tile_affine(&t) { |
| 264 | // Only the part of a tile on the screen matters. |
| 265 | let (aff, _) = res!(v.tile_affine(&t).ok_or_else(|| err!("No transform."; Test))); |
| 266 | let (x, y) = aff.apply(0.5, 0.5); |
| 267 | if x >= 0.0 && x <= 390.0 && y >= 0.0 && y <= 700.0 { |
| 268 | e = e.max(err); |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | worst.push(e); |
| 273 | } |
| 274 | println!("globe against map at Perth: 12 km {:.3} px, 20 km {:.3} px, 100 km {:.3} px", |
| 275 | worst[0], worst[1], worst[2]); |
| 276 | req!((worst[0] < 0.5), true, "A 12 km globe view strays {} px.", worst[0]); |
| 277 | let grows = worst[0] < worst[1] && worst[1] < worst[2]; |
| 278 | req!(grows, true, "The error does not grow with the view: {:?}.", worst); |
| 279 | req!((worst[2] > 1.0), true, "A 100 km globe view strays only {} px.", worst[2]); |
| 280 | Ok(()) |
| 281 | } |
| 282 | |
| 283 | // --------------------------------------------------------------------------------------------- |
| 284 | // PMTiles |
| 285 | // --------------------------------------------------------------------------------------------- |
| 286 | |
| 287 | /// The byte ranges the reference reader read, served back; any other range is refused, so |
| 288 | /// the reader under test cannot have found a tile the reference did not. |
| 289 | struct Sparse { |
| 290 | ranges: Vec<(u64, Vec<u8>)>, |
| 291 | } |
| 292 | |
| 293 | impl Sparse { |
| 294 | fn load(rel: &str) -> Outcome<Self> { |
| 295 | let b = res!(std::fs::read(data(rel)), File, Read); |
| 296 | let mut ranges = Vec::new(); |
| 297 | let mut i = 0usize; |
| 298 | while i + 12 <= b.len() { |
| 299 | let mut o = [0u8; 8]; |
| 300 | o.copy_from_slice(&b[i..i + 8]); |
| 301 | let n = u32::from_le_bytes([b[i + 8], b[i + 9], b[i + 10], b[i + 11]]) as usize; |
| 302 | ranges.push((u64::from_le_bytes(o), b[i + 12..i + 12 + n].to_vec())); |
| 303 | i += 12 + n; |
| 304 | } |
| 305 | Ok(Self { ranges }) |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | impl RangeSource for Sparse { |
| 310 | fn read(&self, offset: u64, len: u64) -> Outcome<Vec<u8>> { |
| 311 | for (o, b) in &self.ranges { |
| 312 | if offset >= *o && offset + len <= *o + b.len() as u64 { |
| 313 | let a = (offset - o) as usize; |
| 314 | return Ok(b[a..a + len as usize].to_vec()); |
| 315 | } |
| 316 | } |
| 317 | Err(err!("Bytes {}+{} were never read by the reference reader.", offset, len; Test, Missing)) |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | fn fnv1a64(b: &[u8]) -> String { |
| 322 | let mut h: u64 = 0xcbf29ce484222325; |
| 323 | for x in b { |
| 324 | h ^= *x as u64; |
| 325 | h = h.wrapping_mul(0x100000001b3); |
| 326 | } |
| 327 | fmt!("{:016x}", h) |
| 328 | } |
| 329 | |
| 330 | #[test] |
| 331 | fn test_pmtiles_reads_the_planet_as_the_reference_does_04() -> Outcome<()> { |
| 332 | let expect = res!(json_file("protomaps/expect.json")); |
| 333 | let head = res!(res!(get(&expect, "header")).ok_or_else(|| err!("No header."; Test))); |
| 334 | let a = res!(Archive::open(res!(Sparse::load("protomaps/ocean_ranges.bin")))); |
| 335 | let h = a.header().clone(); |
| 336 | for (key, got) in [ |
| 337 | ("rootDirectoryOffset", h.root_offset), ("rootDirectoryLength", h.root_length), |
| 338 | ("jsonMetadataOffset", h.metadata_offset), ("jsonMetadataLength", h.metadata_length), |
| 339 | ("leafDirectoryOffset", h.leaf_offset), ("leafDirectoryLength", h.leaf_length), |
| 340 | ("tileDataOffset", h.data_offset), ("tileDataLength", h.data_length), |
| 341 | ("numAddressedTiles", h.addressed_tiles), ("numTileEntries", h.tile_entries), |
| 342 | ("numTileContents", h.tile_contents), |
| 343 | ("minZoom", h.min_zoom as u64), ("maxZoom", h.max_zoom as u64), |
| 344 | ] { |
| 345 | let want = res!(int(res!(res!(get(head, key)).ok_or_else(|| err!("No {}.", key; Test))))); |
| 346 | req!(got as i128, want, "Header {} is {}, the reference says {}.", key, got, want); |
| 347 | } |
| 348 | req!(h.internal_compression, Compression::Gzip); |
| 349 | req!(h.tile_compression, Compression::Gzip); |
| 350 | req!(h.tile_type, TileType::Mvt); |
| 351 | req!(h.clustered, true); |
| 352 | let b = h.bounds(); |
| 353 | for (key, got) in [("minLon", b[0]), ("minLat", b[1]), ("maxLon", b[2]), ("maxLat", b[3])] { |
| 354 | let want = res!(num(res!(res!(get(head, key)).ok_or_else(|| err!("No {}.", key; Test))))); |
| 355 | req!(got, want, "Header {} is {}, the reference says {}.", key, got, want); |
| 356 | } |
| 357 | // Fifty tiles' ids, and the open-ocean ones in full: both are found, share one stored |
| 358 | // content through the run-length entries, and decompress to the reference's bytes. |
| 359 | let tiles = res!(list(res!(res!(get(&expect, "tiles")).ok_or_else(|| err!("No tiles."; Test))))); |
| 360 | req!(tiles.len(), 50); |
| 361 | let mut fetched = 0; |
| 362 | for t in tiles { |
| 363 | let z = res!(int(res!(res!(get(t, "z")).ok_or_else(|| err!("z"; Test))))) as u8; |
| 364 | let x = res!(int(res!(res!(get(t, "x")).ok_or_else(|| err!("x"; Test))))) as u32; |
| 365 | let y = res!(int(res!(res!(get(t, "y")).ok_or_else(|| err!("y"; Test))))) as u32; |
| 366 | let id = res!(int(res!(res!(get(t, "id")).ok_or_else(|| err!("id"; Test))))); |
| 367 | req!(res!(pmtiles::zxy_to_id(z, x, y)) as i128, id, "{}/{}/{} has the wrong id.", z, x, y); |
| 368 | req!(res!(pmtiles::id_to_zxy(id as u64)), (z, x, y)); |
| 369 | let len = res!(int(res!(res!(get(t, "len")).ok_or_else(|| err!("len"; Test))))); |
| 370 | if len >= 0 && len < 200 { |
| 371 | let tile = res!(res!(a.tile_decoded(z, x, y)).ok_or_else(|| err!("{}/{}/{} is missing.", z, x, y; Test))); |
| 372 | req!(tile.len() as i128, len); |
| 373 | let fnv = res!(string(res!(res!(get(t, "fnv")).ok_or_else(|| err!("fnv"; Test))))); |
| 374 | req!(fnv1a64(&tile), fnv, "{}/{}/{} decompressed to other bytes.", z, x, y); |
| 375 | fetched += 1; |
| 376 | } |
| 377 | } |
| 378 | req!(fetched, 2, "Expected the two ocean tiles."); |
| 379 | // Ids the other way, at the corners of grids and deep zooms. |
| 380 | let ids = res!(list(res!(res!(get(&expect, "ids")).ok_or_else(|| err!("No ids."; Test))))); |
| 381 | for t in ids { |
| 382 | let z = res!(int(res!(res!(get(t, "z")).ok_or_else(|| err!("z"; Test))))) as u8; |
| 383 | let x = res!(int(res!(res!(get(t, "x")).ok_or_else(|| err!("x"; Test))))) as u32; |
| 384 | let y = res!(int(res!(res!(get(t, "y")).ok_or_else(|| err!("y"; Test))))) as u32; |
| 385 | let id = res!(int(res!(res!(get(t, "id")).ok_or_else(|| err!("id"; Test))))); |
| 386 | req!(res!(pmtiles::zxy_to_id(z, x, y)) as i128, id, "{}/{}/{} has the wrong id.", z, x, y); |
| 387 | req!(res!(pmtiles::id_to_zxy(id as u64)), (z, x, y), "Id {} names the wrong tile.", id); |
| 388 | } |
| 389 | // A tile beyond the archive's zooms is simply absent. |
| 390 | req!(res!(a.tile(16, 0, 0)), None::<Vec<u8>>); |
| 391 | Ok(()) |
| 392 | } |
| 393 | |
| 394 | #[test] |
| 395 | fn test_pmtiles_reads_an_archive_the_reference_writer_wrote_09() -> Outcome<()> { |
| 396 | let a = res!(Archive::open(res!(FileSource::open(data("protomaps/sample.pmtiles"))))); |
| 397 | let h = a.header().clone(); |
| 398 | req!(h.tile_type, TileType::Mvt); |
| 399 | req!(h.tile_compression, Compression::Gzip); |
| 400 | req!(h.internal_compression, Compression::Gzip); |
| 401 | req!((h.min_zoom, h.max_zoom), (12, 15)); |
| 402 | req!(h.bounds_e7(), [1_156_000_000, -326_000_000, 1_163_000_000, -316_000_000]); |
| 403 | req!((h.addressed_tiles, h.tile_entries, h.tile_contents), (5, 3, 3)); |
| 404 | for name in ["15_26930_19457", "13_6729_4865"] { |
| 405 | let parts: Vec<u32> = name.split('_').map(|p| p.parse::<u32>().unwrap_or(0)).collect(); |
| 406 | let want = res!(std::fs::read(data(&fmt!("protomaps/{}.mvt", name))), File, Read); |
| 407 | let got = res!(a.tile_decoded(parts[0] as u8, parts[1], parts[2])); |
| 408 | req!(got, Some(want.clone()), "{} differs from its tile.", name); |
| 409 | } |
| 410 | // The run of three: consecutive Hilbert ids sharing one stored content. |
| 411 | let ocean = res!(res!(a.tile(12, 2958, 2545)).ok_or_else(|| err!("No ocean."; Test))); |
| 412 | for (x, y) in [(2957, 2545), (2957, 2544)] { |
| 413 | req!(res!(a.tile(12, x, y)), Some(ocean.clone()), "12/{}/{} is not in the run.", x, y); |
| 414 | } |
| 415 | req!(res!(a.tile(12, 2956, 2544)), None::<Vec<u8>>); |
| 416 | req!(res!(a.tile(11, 0, 0)), None::<Vec<u8>>); |
| 417 | let meta = res!(a.metadata()); |
| 418 | req!(meta.contains("OpenStreetMap"), true, "The metadata is {:?}.", meta); |
| 419 | Ok(()) |
| 420 | } |
| 421 | |
| 422 | /// Appends a varint. |
| 423 | fn put(out: &mut Vec<u8>, mut v: u64) { |
| 424 | while v >= 0x80 { |
| 425 | out.push((v as u8 & 0x7f) | 0x80); |
| 426 | v >>= 7; |
| 427 | } |
| 428 | out.push(v as u8); |
| 429 | } |
| 430 | |
| 431 | fn directory(entries: &[(u64, u64, u32, u32)]) -> Vec<u8> { |
| 432 | let mut b = Vec::new(); |
| 433 | put(&mut b, entries.len() as u64); |
| 434 | let mut last = 0; |
| 435 | for e in entries { |
| 436 | put(&mut b, e.0 - last); |
| 437 | last = e.0; |
| 438 | } |
| 439 | for e in entries { |
| 440 | put(&mut b, e.3 as u64); |
| 441 | } |
| 442 | for e in entries { |
| 443 | put(&mut b, e.2 as u64); |
| 444 | } |
| 445 | for (i, e) in entries.iter().enumerate() { |
| 446 | // "Straight after the last" is written as nought. |
| 447 | let follows = i > 0 && entries[i - 1].1 + entries[i - 1].2 as u64 == e.1; |
| 448 | put(&mut b, if follows { 0 } else { e.1 + 1 }); |
| 449 | } |
| 450 | b |
| 451 | } |
| 452 | |
| 453 | #[test] |
| 454 | fn test_pmtiles_reads_a_small_archive_from_a_file_05() -> Outcome<()> { |
| 455 | // Built here to the specification: tiles 0/0/0 and a run of the four zoom-1 tiles sharing |
| 456 | // one content, in the root; zoom 2 behind a leaf directory, one tile of it stored. |
| 457 | let t0 = b"world".to_vec(); |
| 458 | let t1 = b"four of a kind".to_vec(); |
| 459 | let t2 = b"one at zoom two".to_vec(); |
| 460 | let id2 = res!(pmtiles::zxy_to_id(2, 1, 2)); |
| 461 | let leaf = directory(&[(id2, (t0.len() + t1.len()) as u64, t2.len() as u32, 1)]); |
| 462 | let root = directory(&[ |
| 463 | (0, 0, t0.len() as u32, 1), |
| 464 | (1, t0.len() as u64, t1.len() as u32, 4), |
| 465 | (5, 0, leaf.len() as u32, 0), |
| 466 | ]); |
| 467 | let root_off = 127u64; |
| 468 | let leaf_off = root_off + root.len() as u64; |
| 469 | let data_off = leaf_off + leaf.len() as u64; |
| 470 | let mut h = vec![0u8; 127]; |
| 471 | h[..7].copy_from_slice(b"PMTiles"); |
| 472 | h[7] = 3; |
| 473 | for (at, v) in [(8, root_off), (16, root.len() as u64), (24, 0), (32, 0), (40, leaf_off), |
| 474 | (48, leaf.len() as u64), (56, data_off), (64, (t0.len() + t1.len() + t2.len()) as u64), |
| 475 | (72, 21), (80, 3), (88, 3)] |
| 476 | { |
| 477 | h[at..at + 8].copy_from_slice(&(v as u64).to_le_bytes()); |
| 478 | } |
| 479 | h[97] = 1; // internal compression: none |
| 480 | h[98] = 1; // tile compression: none |
| 481 | h[99] = 1; // mvt |
| 482 | h[100] = 0; |
| 483 | h[101] = 2; |
| 484 | let mut file = h; |
| 485 | file.extend_from_slice(&root); |
| 486 | file.extend_from_slice(&leaf); |
| 487 | file.extend_from_slice(&t0); |
| 488 | file.extend_from_slice(&t1); |
| 489 | file.extend_from_slice(&t2); |
| 490 | let dir = std::env::temp_dir().join(fmt!("fe2o3_geom_pmtiles_{}", std::process::id())); |
| 491 | res!(std::fs::create_dir_all(&dir), File, Write); |
| 492 | let path = dir.join("small.pmtiles"); |
| 493 | res!(std::fs::write(&path, &file), File, Write); |
| 494 | let a = res!(Archive::open(res!(FileSource::open(&path)))); |
| 495 | req!(res!(a.tile(0, 0, 0)), Some(t0.clone())); |
| 496 | for (x, y) in [(0, 0), (0, 1), (1, 1), (1, 0)] { |
| 497 | req!(res!(a.tile(1, x, y)), Some(t1.clone()), "1/{}/{} was not the shared content.", x, y); |
| 498 | } |
| 499 | req!(res!(a.tile(2, 1, 2)), Some(t2.clone())); |
| 500 | req!(res!(a.tile(2, 0, 0)), None::<Vec<u8>>); |
| 501 | req!(res!(a.tile(3, 0, 0)), None::<Vec<u8>>); |
| 502 | // The same bytes from memory. |
| 503 | let m = res!(Archive::open(&file[..])); |
| 504 | req!(res!(m.tile(2, 1, 2)), Some(t2.clone())); |
| 505 | let _ = std::fs::remove_dir_all(&dir); |
| 506 | // Not an archive, and a directory whose count lies. |
| 507 | req!(pmtiles::Header::parse(&file[1..]).is_err(), true, "A shifted header was read."); |
| 508 | req!(pmtiles::decode_directory(&[0xff, 0xff, 0x03]).is_err(), true, "A lying count was believed."); |
| 509 | Ok(()) |
| 510 | } |
| 511 | |
| 512 | // --------------------------------------------------------------------------------------------- |
| 513 | // Mapbox Vector Tiles |
| 514 | // --------------------------------------------------------------------------------------------- |
| 515 | |
| 516 | /// Does a decoded tile say what the fixture's JSON reading of its protocol buffer says? |
| 517 | fn same_as_fixture(t: &mvt::Tile, want: &Dat, name: &str) -> Outcome<()> { |
| 518 | let layers = match res!(get(want, "layers")) { |
| 519 | Some(l) => res!(list(l)).clone(), |
| 520 | None => Vec::new(), |
| 521 | }; |
| 522 | req!(t.layers.len(), layers.len(), "{}: layer count.", name); |
| 523 | for (l, w) in t.layers.iter().zip(layers.iter()) { |
| 524 | req!(l.name.clone(), res!(string(res!(res!(get(w, "name")).ok_or_else(|| err!("name"; Test)))))); |
| 525 | if let Some(v) = res!(get(w, "version")) { |
| 526 | req!(l.version as i128, res!(int(v)), "{}: version.", name); |
| 527 | } |
| 528 | let extent = match res!(get(w, "extent")) { Some(e) => res!(int(e)), None => 4096 }; |
| 529 | req!(l.extent as i128, extent, "{}: extent.", name); |
| 530 | let keys: Vec<String> = match res!(get(w, "keys")) { |
| 531 | Some(k) => { let mut o = Vec::new(); for s in res!(list(k)) { o.push(res!(string(s))); } o }, |
| 532 | None => Vec::new(), |
| 533 | }; |
| 534 | req!(l.keys.clone(), keys, "{}: keys.", name); |
| 535 | let values = match res!(get(w, "values")) { Some(v) => res!(list(v)).clone(), None => Vec::new() }; |
| 536 | req!(l.values.len(), values.len(), "{}: value count.", name); |
| 537 | for (v, wv) in l.values.iter().zip(values.iter()) { |
| 538 | let ok = match v { |
| 539 | // The fixtures are generated from this JSON, and 076's generator wrote a number |
| 540 | // into a string field, which went into the tile as its decimal text. |
| 541 | Value::Str(s) => res!(get(wv, "string_value")).map(|d| string(d).ok() == Some(s.clone()) |
| 542 | || int(d).ok().map(|i| i.to_string()) == Some(s.clone())), |
| 543 | Value::Float(f) => res!(get(wv, "float_value")).map(|d| num(d).ok().map(|x| x as f32) == Some(*f)), |
| 544 | Value::Double(f)=> res!(get(wv, "double_value")).map(|d| num(d).ok() == Some(*f)), |
| 545 | Value::Int(i) => res!(get(wv, "int_value")).map(|d| int(d).ok() == Some(*i as i128)), |
| 546 | Value::Uint(u) => res!(get(wv, "uint_value")).map(|d| int(d).ok() == Some(*u as i128)), |
| 547 | Value::Sint(i) => res!(get(wv, "sint_value")).map(|d| int(d).ok() == Some(*i as i128)), |
| 548 | Value::Bool(b) => res!(get(wv, "bool_value")).map(|d| matches!(d, Dat::Bool(x) if x == b)), |
| 549 | }; |
| 550 | req!(ok, Some(true), "{}: value {:?} against {:?}.", name, v, wv); |
| 551 | } |
| 552 | let feats = match res!(get(w, "features")) { Some(f) => res!(list(f)).clone(), None => Vec::new() }; |
| 553 | req!(l.features.len(), feats.len(), "{}: feature count.", name); |
| 554 | for (f, wf) in l.features.iter().zip(feats.iter()) { |
| 555 | let id = match res!(get(wf, "id")) { Some(d) => Some(res!(int(d)) as u64), None => None }; |
| 556 | // A proto2 id of nought is indistinguishable from none. |
| 557 | req!(f.id.filter(|v| *v != 0), id.filter(|v| *v != 0), "{}: id.", name); |
| 558 | let ty = match res!(get(wf, "type")) { Some(d) => res!(int(d)), None => 0 }; |
| 559 | let ty = if ty > 3 { 0 } else { ty }; |
| 560 | req!(f.kind.code() as i128, ty, "{}: type.", name); |
| 561 | let tags: Vec<i128> = match res!(get(wf, "tags")) { |
| 562 | Some(d) => { let mut o = Vec::new(); for x in res!(list(d)) { o.push(res!(int(x))); } o }, |
| 563 | None => Vec::new(), |
| 564 | }; |
| 565 | req!(f.tags.iter().map(|v| *v as i128).collect::<Vec<_>>(), tags, "{}: tags.", name); |
| 566 | let geom: Vec<i128> = match res!(get(wf, "geometry")) { |
| 567 | Some(d) => { let mut o = Vec::new(); for x in res!(list(d)) { o.push(res!(int(x))); } o }, |
| 568 | None => Vec::new(), |
| 569 | }; |
| 570 | req!(f.geometry.iter().map(|v| *v as i128).collect::<Vec<_>>(), geom, "{}: geometry.", name); |
| 571 | } |
| 572 | } |
| 573 | Ok(()) |
| 574 | } |
| 575 | |
| 576 | /// Decoding in full: the structure, then every feature's geometry and properties. |
| 577 | fn decode_fully(bytes: &[u8]) -> Outcome<mvt::Tile> { |
| 578 | let t = res!(mvt::decode(bytes)); |
| 579 | for l in &t.layers { |
| 580 | for f in &l.features { |
| 581 | res!(f.geometry()); |
| 582 | res!(f.properties(l)); |
| 583 | } |
| 584 | } |
| 585 | Ok(t) |
| 586 | } |
| 587 | |
| 588 | #[test] |
| 589 | fn test_the_specification_fixtures_decode_as_specified_06() -> Outcome<()> { |
| 590 | let root = data("mvt-fixtures"); |
| 591 | let mut names: Vec<String> = Vec::new(); |
| 592 | for e in res!(std::fs::read_dir(&root), File, Read) { |
| 593 | let e = res!(e, File, Read); |
| 594 | if e.path().is_dir() { |
| 595 | names.push(e.file_name().to_string_lossy().into_owned()); |
| 596 | } |
| 597 | } |
| 598 | names.sort(); |
| 599 | req!(names.len(), 74, "Expected the 74 fixtures of mvt-fixtures 4.0.0."); |
| 600 | let (mut valid, mut fatal) = (0, 0); |
| 601 | for name in &names { |
| 602 | let info = res!(json_file(&fmt!("mvt-fixtures/{}/info.json", name))); |
| 603 | let validity = res!(res!(get(&info, "validity")).ok_or_else(|| err!("validity"; Test))); |
| 604 | let v2 = matches!(res!(get(validity, "v2")), Some(Dat::Bool(true))); |
| 605 | let error = match res!(get(validity, "error")) { Some(d) => res!(string(d)), None => String::new() }; |
| 606 | let bytes = res!(std::fs::read(root.join(name).join("tile.mvt")), File, Read); |
| 607 | if v2 { |
| 608 | let t = res!(mvt::decode(&bytes)); |
| 609 | let want = res!(json_file(&fmt!("mvt-fixtures/{}/tile.json", name))); |
| 610 | res!(same_as_fixture(&t, &want, name)); |
| 611 | // 057 is a MoveTo whose count asks for half a billion points and supplies one; |
| 612 | // a count is never trusted, so its geometry is refused rather than allocated. |
| 613 | if name != "057" { |
| 614 | res!(decode_fully(&bytes)); |
| 615 | } |
| 616 | valid += 1; |
| 617 | } else if error == "fatal" { |
| 618 | let refused = decode_fully(&bytes).is_err(); |
| 619 | req!(refused, true, "Fixture {} is fatally malformed and decoded.", name); |
| 620 | fatal += 1; |
| 621 | } else { |
| 622 | // Recoverable: either answer will do, so long as it is an answer. |
| 623 | let _ = decode_fully(&bytes); |
| 624 | } |
| 625 | } |
| 626 | req!(valid, 46); |
| 627 | let plenty = fatal >= 20; |
| 628 | req!(plenty, true, "Only {} fatal fixtures.", fatal); |
| 629 | Ok(()) |
| 630 | } |
| 631 | |
| 632 | #[test] |
| 633 | fn test_real_tiles_decode_as_the_reference_decoder_reads_them_07() -> Outcome<()> { |
| 634 | for name in ["15_26930_19457", "13_6729_4865"] { |
| 635 | let bytes = res!(std::fs::read(data(&fmt!("protomaps/{}.mvt", name))), File, Read); |
| 636 | let gz = res!(std::fs::read(data(&fmt!("protomaps/{}.json.gz", name))), File, Read); |
| 637 | let text = match String::from_utf8(res!(pmtiles::decompress(&gz, Compression::Gzip))) { |
| 638 | Ok(s) => s, |
| 639 | Err(_) => return Err(err!("The reference dump is not text."; Test)), |
| 640 | }; |
| 641 | let want = res!(json(&text)); |
| 642 | let t = res!(mvt::decode(&bytes)); |
| 643 | let layers = res!(list(&want)); |
| 644 | req!(t.layers.len(), layers.len(), "{}: layer count.", name); |
| 645 | let mut features = 0; |
| 646 | for (l, w) in t.layers.iter().zip(layers.iter()) { |
| 647 | req!(l.name.clone(), res!(string(res!(res!(get(w, "name")).ok_or_else(|| err!("name"; Test)))))); |
| 648 | req!(l.extent as i128, res!(int(res!(res!(get(w, "extent")).ok_or_else(|| err!("extent"; Test)))))); |
| 649 | let wf = res!(list(res!(res!(get(w, "features")).ok_or_else(|| err!("features"; Test))))); |
| 650 | req!(l.features.len(), wf.len(), "{} {}: feature count.", name, l.name); |
| 651 | for (f, w) in l.features.iter().zip(wf.iter()) { |
| 652 | features += 1; |
| 653 | match res!(get(w, "id")) { |
| 654 | Some(Dat::Empty) | Some(Dat::Opt(_)) | None => req!(f.id, None::<u64>), |
| 655 | Some(d) => req!(f.id.map(|v| v as i128), Some(res!(int(d))), "{} {}: id.", name, l.name), |
| 656 | } |
| 657 | req!(f.kind.code() as i128, res!(int(res!(res!(get(w, "type")).ok_or_else(|| err!("type"; Test)))))); |
| 658 | // Properties, as a map: a JavaScript object orders integer-like keys first. |
| 659 | let props = res!(f.properties(l)); |
| 660 | let wp = res!(res!(get(w, "props")).ok_or_else(|| err!("props"; Test))); |
| 661 | let mut count = 0; |
| 662 | for (k, v) in &props { |
| 663 | count += 1; |
| 664 | let d = res!(res!(get(wp, k)).ok_or_else(|| err!("{} {}: no {} in the reference.", name, l.name, k; Test))); |
| 665 | let ok = match (v, d) { |
| 666 | (Value::Str(s), Dat::Str(r)) => s == r, |
| 667 | (Value::Bool(b), Dat::Bool(r)) => b == r, |
| 668 | (v, d) => match (v.as_f64(), d.get_float64()) { |
| 669 | (Some(a), Some(b)) => a == b.0, |
| 670 | _ => false, |
| 671 | }, |
| 672 | }; |
| 673 | req!(ok, true, "{} {}: {} is {:?}, the reference {:?}.", name, l.name, k, v, d); |
| 674 | } |
| 675 | let wlen = match wp { Dat::Map(m) => m.len(), Dat::OrdMap(m) => m.len(), _ => 0 }; |
| 676 | req!(count, wlen, "{} {}: property count.", name, l.name); |
| 677 | // Geometry, point for point. |
| 678 | let g = res!(f.geometry()); |
| 679 | let wg = res!(list(res!(res!(get(w, "geom")).ok_or_else(|| err!("geom"; Test))))); |
| 680 | req!(g.len(), wg.len(), "{} {}: ring count.", name, l.name); |
| 681 | for (r, wr) in g.iter().zip(wg.iter()) { |
| 682 | let wr = res!(list(wr)); |
| 683 | req!(r.len(), wr.len(), "{} {}: point count.", name, l.name); |
| 684 | for (p, wp) in r.iter().zip(wr.iter()) { |
| 685 | let wp = res!(list(wp)); |
| 686 | req!((p.0 as i128, p.1 as i128), (res!(int(&wp[0])), res!(int(&wp[1]))), |
| 687 | "{} {}: a point differs.", name, l.name); |
| 688 | } |
| 689 | } |
| 690 | } |
| 691 | } |
| 692 | let plenty = features > 20; |
| 693 | req!(plenty, true, "{} had only {} features.", name, features); |
| 694 | } |
| 695 | Ok(()) |
| 696 | } |
| 697 | |
| 698 | #[test] |
| 699 | fn test_features_sort_into_style_classes_08() -> Outcome<()> { |
| 700 | let bytes = res!(std::fs::read(data("protomaps/13_6729_4865.mvt")), File, Read); |
| 701 | let t = res!(mvt::decode(&bytes)); |
| 702 | let names: Vec<&str> = t.layers.iter().map(|l| l.name.as_str()).collect(); |
| 703 | req!(names.contains(&"roads"), true, "No roads layer in {:?}.", names); |
| 704 | let rules = vec![ |
| 705 | StyleRule { layer: "water".into(), key: "kind".into(), values: vec![], min_zoom: 0, class: 1 }, |
| 706 | StyleRule { layer: "roads".into(), key: "kind".into(), values: vec!["highway".into(), "major_road".into()], min_zoom: 0, class: 2 }, |
| 707 | StyleRule { layer: "roads".into(), key: "kind".into(), values: vec![], min_zoom: 14, class: 3 }, |
| 708 | ]; |
| 709 | let at13 = mvt::classify(&t, &rules, 13, Some("min_zoom")); |
| 710 | let at14 = mvt::classify(&t, &rules, 14, Some("min_zoom")); |
| 711 | // Rule order is paint order, and the catch-all minor-road rule only opens at 14. |
| 712 | let order_ok = at13.windows(2).all(|p| p[0].0 <= p[1].0); |
| 713 | req!(order_ok, true, "Classes out of rule order."); |
| 714 | req!(at13.iter().any(|h| h.0 == 3), false, "Class 3 drawn below its zoom."); |
| 715 | let more = at14.len() >= at13.len(); |
| 716 | req!(more, true, "Zooming in drew less."); |
| 717 | for (class, li, fi) in &at14 { |
| 718 | let l = &t.layers[*li]; |
| 719 | let f = &l.features[*fi]; |
| 720 | if *class == 2 { |
| 721 | let k = l.property(f, "kind").and_then(|v| v.as_str()).unwrap_or(""); |
| 722 | req!((k == "highway" || k == "major_road"), true, "A {} road in class 2.", k); |
| 723 | } |
| 724 | if let Some(mz) = l.property(f, "min_zoom").and_then(|v| v.as_f64()) { |
| 725 | req!((mz <= 14.0), true, "A feature of min_zoom {} drawn at 14.", mz); |
| 726 | } |
| 727 | } |
| 728 | // Geometry types are what the layers imply. |
| 729 | for l in &t.layers { |
| 730 | for f in &l.features { |
| 731 | if l.name == "roads" { |
| 732 | req!(f.kind, GeomKind::LineString); |
| 733 | } |
| 734 | } |
| 735 | } |
| 736 | Ok(()) |
| 737 | } |
| 738 | |
| 739 | #[test] |
| 740 | fn test_the_tile_zoom_draws_a_tile_pixel_to_a_screen_pixel_10() -> Outcome<()> { |
| 741 | let world_m = 2.0 * std::f64::consts::PI * oxedyne_fe2o3_geom::proj::EARTH_RADIUS_M; |
| 742 | // At the equator one 256-pixel tile holds the world at zoom 0. |
| 743 | let vp = res!(Viewport::new(Projection::WebMercator, 0.0, 0.0, 0.0, world_m / 256.0, 390.0, 700.0)); |
| 744 | req!((vp.tile_zoom(256.0).abs() < 1.0e-9), true); |
| 745 | for (kind, lat, z) in [(Projection::WebMercator, -33.87, 15u8), (Projection::Orthographic, 51.5, 9), |
| 746 | (Projection::WebMercator, 70.0, 4)] |
| 747 | { |
| 748 | // Find the scale at which zoom z is exact, then check a tile of z is 256 pixels across. |
| 749 | let probe = res!(Viewport::new(kind, lat, 151.2, 30.0, 1.0, 390.0, 700.0)); |
| 750 | let m_per_px = 2f64.powf(probe.tile_zoom(256.0) - z as f64); |
| 751 | let vp = res!(Viewport::new(kind, lat, 151.2, 30.0, m_per_px, 390.0, 700.0)); |
| 752 | req!(((vp.tile_zoom(256.0) - z as f64).abs() < 1.0e-9), true); |
| 753 | let t = res!(TileId::at(lat, 151.2, z)); |
| 754 | let (aff, _) = res!(vp.tile_affine(&t).ok_or_else(|| err!("no affine"; Missing))); |
| 755 | req!(((aff.a.hypot(aff.b) - 256.0).abs() < 1.0e-6), true); |
| 756 | } |
| 757 | Ok(()) |
| 758 | } |