oxedyne/fe2o3/fe2o3_geom/tests/world.rs
8.9 KiB, 2 runs
created by r1870400018:59724, 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 | //! The world file and the spherical simplifier. |
| 2 | //! |
| 3 | //! The external check is byte identity with Ochre's `world.bin`, which Python wrote: run |
| 4 | //! `cargo run --release -p oxedyne_fe2o3_geom --example world_gen -- --compare |
| 5 | //! <ochre>/src/web/assets/world.bin` with Natural Earth cached. It needs the downloads, so it |
| 6 | //! is not a unit test; what is here needs nothing but the coarse fixture that example wrote. |
| 7 | |
| 8 | use oxedyne_fe2o3_geom::{ |
| 9 | proj::{ |
| 10 | EARTH_RADIUS_M, |
| 11 | unit_vec, |
| 12 | }, |
| 13 | world::{ |
| 14 | self, |
| 15 | Label, |
| 16 | Layer, |
| 17 | LayerKind, |
| 18 | World, |
| 19 | MAGIC, |
| 20 | Q, |
| 21 | }, |
| 22 | }; |
| 23 | |
| 24 | use oxedyne_fe2o3_core::prelude::*; |
| 25 | |
| 26 | use std::collections::HashSet; |
| 27 | |
| 28 | const WORLD: &[u8] = include_bytes!("data/world_coarse.bin"); |
| 29 | |
| 30 | #[test] |
| 31 | fn test_the_coarse_world_reads_back_as_the_world_00() -> Outcome<()> { |
| 32 | let w = res!(world::read(WORLD)); |
| 33 | let named: Vec<&str> = w.layers.iter().map(|l| l.name.as_str()).collect(); |
| 34 | req!(named, vec!["land", "lakes", "borders"]); |
| 35 | req!(w.layers[0].kind, LayerKind::Fill); |
| 36 | req!(w.layers[2].kind, LayerKind::Stroke); |
| 37 | req!(w.layers[0].tol_m, 30_000.0); |
| 38 | req!(w.details(), 1); |
| 39 | // Ochre's report for this level: 190 land rings of 4202 vertices, 11 lakes of 129, 183 |
| 40 | // borders of 559. |
| 41 | for (layer, rings, verts) in [(0usize, 190usize, 4202usize), (1, 11, 129), (2, 183, 559)] { |
| 42 | let l = &w.layers[layer]; |
| 43 | req!(l.rings.len(), rings, "{} has {} rings.", l.name, l.rings.len()); |
| 44 | let n: usize = l.rings.iter().map(|r| r.len()).sum(); |
| 45 | req!(n, verts, "{} has {} vertices.", l.name, n); |
| 46 | for r in &l.rings { |
| 47 | for (lat, lng) in r { |
| 48 | let real = lat.abs() <= 90.0 && lng.abs() <= 540.0; |
| 49 | req!(real, true, "{}, {} is not on the Earth.", lat, lng); |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | // And writing what was read gives the same bytes. |
| 54 | let again = res!(world::write(&w)); |
| 55 | req!((again == WORLD), true, "The world did not write back as it was read."); |
| 56 | // Reading one level alone gives that level. |
| 57 | let only = res!(world::read_detail(WORLD, Some(0))); |
| 58 | req!(only, w); |
| 59 | let none = res!(world::read_detail(WORLD, Some(1))); |
| 60 | req!(none.layers.len(), 0); |
| 61 | Ok(()) |
| 62 | } |
| 63 | |
| 64 | #[test] |
| 65 | fn test_every_position_written_is_a_source_position_01() -> Outcome<()> { |
| 66 | // A ring with steps long enough to need a jump, one across the antimeridian, and a vertex |
| 67 | // that rounds onto its neighbour. |
| 68 | let ring = vec![ |
| 69 | (10.0, 170.0), (10.4, 179.9), (10.4, -179.9), (-5.0, -170.0), |
| 70 | (-5.000_001, -170.000_001), (-20.0, 150.0), (10.0, 170.0), |
| 71 | ]; |
| 72 | let w = World { layers: vec![Layer { |
| 73 | name: "land".to_string(), kind: LayerKind::Fill, detail: 0, tol_m: 1_000.0, |
| 74 | rings: vec![ring.clone()], labels: Vec::new(), |
| 75 | }]}; |
| 76 | let bytes = res!(world::write(&w)); |
| 77 | req!(&bytes[..8], &MAGIC[..]); |
| 78 | req!(u16::from_le_bytes([bytes[8], bytes[9]]), world::VERSION_RINGS); |
| 79 | let back = res!(world::read(&bytes)); |
| 80 | let source: HashSet<(i64, i64)> = ring.iter() |
| 81 | .map(|(a, b)| ((a * Q).round() as i64, (b * Q).round() as i64)) |
| 82 | .collect(); |
| 83 | let got = &back.layers[0].rings[0]; |
| 84 | req!(got.len(), ring.len() - 1, "The duplicate vertex was not dropped."); |
| 85 | for (lat, lng) in got { |
| 86 | let q = ((lat * Q).round() as i64, (lng * Q).round() as i64); |
| 87 | // Longitude may come back a turn away, walked the short way round. |
| 88 | let turned = (q.0, (q.1 + 180 * 100_000).rem_euclid(360 * 100_000) - 180 * 100_000); |
| 89 | let known = source.contains(&q) || source.contains(&turned); |
| 90 | req!(known, true, "{}, {} is not a position the ring was given.", lat, lng); |
| 91 | } |
| 92 | Ok(()) |
| 93 | } |
| 94 | |
| 95 | #[test] |
| 96 | fn test_labels_round_trip_in_version_2_02() -> Outcome<()> { |
| 97 | let labels = vec![ |
| 98 | Label { lat: -31.9535, lng: 115.8571, rank: 0, name: "Perth".to_string() }, |
| 99 | Label { lat: 35.6895, lng: 139.6917, rank: 0, name: "東京".to_string() }, |
| 100 | Label { lat: 64.8378, lng: -147.7164, rank: 4, name: "Fairbanks".to_string() }, |
| 101 | ]; |
| 102 | let mut w = res!(world::read(WORLD)); |
| 103 | w.layers.push(Layer { |
| 104 | name: "places".to_string(), kind: LayerKind::Label, detail: 0, tol_m: 0.0, |
| 105 | rings: Vec::new(), labels: labels.clone(), |
| 106 | }); |
| 107 | let bytes = res!(world::write(&w)); |
| 108 | req!(u16::from_le_bytes([bytes[8], bytes[9]]), world::VERSION_LABELS); |
| 109 | let back = res!(world::read(&bytes)); |
| 110 | req!(back, w); |
| 111 | let again = res!(world::write(&back)); |
| 112 | req!((again == bytes), true, "A labelled world did not write back as it was read."); |
| 113 | let places = res!(back.layer("places", 0).ok_or_else(|| err!("No places."; Test))); |
| 114 | req!(places.labels[1].name.as_str(), "東京"); |
| 115 | Ok(()) |
| 116 | } |
| 117 | |
| 118 | #[test] |
| 119 | fn test_a_world_file_that_is_not_one_is_refused_03() -> Outcome<()> { |
| 120 | req!(world::read(b"nothing at all").is_err(), true, "Rubbish was accepted."); |
| 121 | let mut ochre = WORLD.to_vec(); |
| 122 | ochre[..8].copy_from_slice(b"OCHRWRLD"); |
| 123 | req!(world::read(&ochre).is_err(), true, "Another magic was accepted."); |
| 124 | let mut later = WORLD.to_vec(); |
| 125 | later[8] = 9; |
| 126 | req!(world::read(&later).is_err(), true, "A later version was accepted."); |
| 127 | let cut = &WORLD[..WORLD.len() / 2]; |
| 128 | req!(world::read(cut).is_err(), true, "A truncated file was accepted."); |
| 129 | let mut long = WORLD.to_vec(); |
| 130 | long.push(0); |
| 131 | req!(world::read(&long).is_err(), true, "A file with a trailing byte was accepted."); |
| 132 | // A label layer in a version 1 file. |
| 133 | let mut mislabelled = WORLD.to_vec(); |
| 134 | mislabelled[12] = 2; |
| 135 | req!(world::read(&mislabelled).is_err(), true, "Labels in version 1 were accepted."); |
| 136 | let named = World { layers: vec![Layer { |
| 137 | name: "coastline".to_string(), kind: LayerKind::Stroke, detail: 0, tol_m: 0.0, |
| 138 | rings: Vec::new(), labels: Vec::new(), |
| 139 | }]}; |
| 140 | req!(world::write(&named).is_err(), true, "A name longer than seven bytes was written."); |
| 141 | Ok(()) |
| 142 | } |
| 143 | |
| 144 | #[test] |
| 145 | fn test_the_simplifier_keeps_every_edge_within_its_tolerance_04() -> Outcome<()> { |
| 146 | // A wobbly line round a quarter of the equator: every dropped vertex lies within the |
| 147 | // tolerance of the chord between the kept vertices either side of it. |
| 148 | let n = 4_000; |
| 149 | let pts: Vec<[f64; 3]> = (0..n).map(|k| { |
| 150 | let t = k as f64 / n as f64; |
| 151 | unit_vec(0.3 * (t * 157.0).sin() + 0.05 * (t * 1_031.0).cos(), 90.0 * t) |
| 152 | }).collect(); |
| 153 | for eps_m in [1_000.0, 6_000.0, 30_000.0] { |
| 154 | let keep = world::simplify_sphere(&pts, eps_m); |
| 155 | req!(keep[0], 0); |
| 156 | req!(keep[keep.len() - 1], n - 1); |
| 157 | let fewer = keep.len() < n && keep.len() > 2; |
| 158 | req!(fewer, true, "At {} m, {} of {} vertices were kept.", eps_m, keep.len(), n); |
| 159 | let eps = eps_m / EARTH_RADIUS_M; |
| 160 | for pair in keep.windows(2) { |
| 161 | let (a, b) = (pts[pair[0]], pts[pair[1]]); |
| 162 | let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; |
| 163 | let dd = d[0] * d[0] + d[1] * d[1] + d[2] * d[2]; |
| 164 | for k in pair[0] + 1..pair[1] { |
| 165 | let p = pts[k]; |
| 166 | let t = (((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1] + (p[2] - a[2]) * d[2]) / dd) |
| 167 | .clamp(0.0, 1.0); |
| 168 | let off = ((p[0] - a[0] - t * d[0]).powi(2) + (p[1] - a[1] - t * d[1]).powi(2) |
| 169 | + (p[2] - a[2] - t * d[2]).powi(2)).sqrt(); |
| 170 | req!((off <= eps), true, "At {} m a dropped vertex is {} m off.", |
| 171 | eps_m, off * EARTH_RADIUS_M); |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | // Nothing to do below three vertices, or at no tolerance. |
| 176 | req!(world::simplify_sphere(&pts[..2], 1_000.0), vec![0, 1]); |
| 177 | req!(world::simplify_sphere(&pts[..5], 0.0), vec![0, 1, 2, 3, 4]); |
| 178 | Ok(()) |
| 179 | } |
| 180 | |
| 181 | #[test] |
| 182 | fn test_the_detail_drawn_follows_the_scale_and_worlds_merge_05() -> Outcome<()> { |
| 183 | let ring = |detail: u8, tol_m: f64, name: &str| Layer { |
| 184 | name: name.to_string(), kind: LayerKind::Fill, detail, tol_m, |
| 185 | rings: vec![vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]], labels: Vec::new(), |
| 186 | }; |
| 187 | let places = |n: usize| Layer { |
| 188 | name: "places".to_string(), kind: LayerKind::Label, detail: 0, tol_m: 0.0, |
| 189 | rings: Vec::new(), |
| 190 | labels: (0..n).map(|k| Label { lat: 0.0, lng: k as f64, rank: 0, name: fmt!("{}", k) }).collect(), |
| 191 | }; |
| 192 | let mut w = World { layers: vec![ring(0, 30_000.0, "land"), places(2)] }; |
| 193 | req!(World::default().detail_for(1_000.0, 2.5), None::<u8>); |
| 194 | req!(w.detail_for(1_000.0, 2.5), Some(0)); |
| 195 | let fine = World { layers: vec![ring(1, 6_000.0, "land"), ring(1, 6_000.0, "lakes"), places(5)] }; |
| 196 | w.merge(fine); |
| 197 | req!(w.details(), 2); |
| 198 | req!(w.layers.len(), 4); |
| 199 | // The finer file's places replace the coarse file's rather than doubling them. |
| 200 | req!(w.layer("places", 0).map(|l| l.labels.len()), Some(5)); |
| 201 | // 30 km at 2.5 px wants 12 km a pixel; 6 km wants 2.4 km; finer than that, the finest. |
| 202 | req!(w.detail_for(12_000.0, 2.5), Some(0)); |
| 203 | req!(w.detail_for(11_999.0, 2.5), Some(1)); |
| 204 | req!(w.detail_for(2_400.0, 2.5), Some(1)); |
| 205 | req!(w.detail_for(10.0, 2.5), Some(1)); |
| 206 | Ok(()) |
| 207 | } |