Oregami
Repositories/oxedyne/fe2o3

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
8use 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
34use oxedyne_fe2o3_core::prelude::*;
35use oxedyne_fe2o3_jdat::{
36 prelude::*,
37 string::dec::DecoderConfig,
38 usr::{
39 UsrKind,
40 UsrKindCode,
41 UsrKindId,
42 },
43};
44
45use std::{
46 collections::{
47 BTreeMap,
48 HashSet,
49 },
50 path::PathBuf,
51};
52
53fn data(rel: &str) -> PathBuf {
54 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data").join(rel)
55}
56
57fn 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
62fn json_file(rel: &str) -> Outcome<Dat> {
63 let text = res!(std::fs::read_to_string(data(rel)), File, Read);
64 json(&text)
65}
66
67fn get<'a>(d: &'a Dat, key: &str) -> Outcome<Option<&'a Dat>> {
68 d.map_get(&dat!(key))
69}
70
71fn 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.
79fn 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
95fn 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
102fn 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]
114fn 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]
156fn 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]
171fn 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]
240fn 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.
289struct Sparse {
290 ranges: Vec<(u64, Vec<u8>)>,
291}
292
293impl 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
309impl 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
321fn 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]
331fn 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]
395fn 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.
423fn 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
431fn 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]
454fn 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?
517fn 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.
577fn 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]
589fn 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]
633fn 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]
699fn 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]
740fn 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}