use std::sync::OnceLock;
const ANTIMERIDIAN_WRAP_THRESHOLD_DEG: f32 = 180.0;
struct RawLandmass {
rings: Vec<Vec<(f32, f32)>>,
triangles: Vec<usize>,
vertex_count: usize,
}
pub(crate) struct Landmasses {
pub(crate) triangles: Vec<[egui::Pos2; 3]>,
pub(crate) outlines: Vec<Vec<egui::Pos2>>,
}
static RAW: OnceLock<Vec<RawLandmass>> = OnceLock::new();
static LANDMASSES: OnceLock<Landmasses> = OnceLock::new();
fn raw_landmasses() -> &'static [RawLandmass] {
RAW.get_or_init(|| {
bywind::landmass::raw_polygons()
.iter()
.map(|p| triangulate_polygon(p.rings.clone()))
.collect()
})
}
fn triangulate_polygon(rings: Vec<Vec<(f64, f64)>>) -> RawLandmass {
if rings.is_empty() {
return RawLandmass {
rings: Vec::new(),
triangles: Vec::new(),
vertex_count: 0,
};
}
let rings_f32: Vec<Vec<(f32, f32)>> = rings
.into_iter()
.map(|r| {
let mut r = r;
if r.len() >= 2 && r.first() == r.last() {
r.pop();
}
r.into_iter()
.map(|(lon, lat)| (lon as f32, lat as f32))
.collect()
})
.collect();
let mut coords: Vec<f64> = Vec::new();
let mut hole_indices: Vec<usize> = Vec::new();
for (i, ring) in rings_f32.iter().enumerate() {
if i > 0 {
hole_indices.push(coords.len() / 2);
}
for &(lon, lat) in ring {
coords.push(f64::from(lon));
coords.push(f64::from(lat));
}
}
let triangles = earcutr::earcut(&coords, &hole_indices, 2).unwrap_or_else(|e| {
log::warn!("earcutr failed on a landmass polygon: {e:?}");
Vec::new()
});
let vertex_count = coords.len() / 2;
RawLandmass {
rings: rings_f32,
triangles,
vertex_count,
}
}
pub(crate) fn landmasses() -> &'static Landmasses {
LANDMASSES.get_or_init(|| {
let mut triangles: Vec<[egui::Pos2; 3]> = Vec::new();
let mut outlines: Vec<Vec<egui::Pos2>> = Vec::new();
for landmass in raw_landmasses() {
let mut verts: Vec<egui::Pos2> = Vec::with_capacity(landmass.vertex_count);
for ring in &landmass.rings {
for &(lon, lat) in ring {
verts.push(egui::Pos2::new(lon, lat));
}
}
for tri_idx in landmass.triangles.chunks_exact(3) {
let (Some(&ai), Some(&bi), Some(&ci)) =
(tri_idx.first(), tri_idx.get(1), tri_idx.get(2))
else {
continue;
};
let (Some(&a), Some(&b), Some(&c)) = (verts.get(ai), verts.get(bi), verts.get(ci))
else {
continue;
};
triangles.push([a, b, c]);
}
let mut cursor = verts.iter();
for ring in &landmass.rings {
let mut strip: Vec<egui::Pos2> =
cursor.by_ref().take(ring.len()).copied().collect();
if let Some(&first) = strip.first() {
strip.push(first);
}
for sub in split_long_edges(&strip) {
if sub.len() >= 2 {
outlines.push(sub);
}
}
}
}
Landmasses {
triangles,
outlines,
}
})
}
fn split_long_edges(strip: &[egui::Pos2]) -> Vec<Vec<egui::Pos2>> {
let mut out = Vec::new();
let mut current: Vec<egui::Pos2> = Vec::new();
for &p in strip {
if let Some(&prev) = current.last()
&& (p.x - prev.x).abs() > ANTIMERIDIAN_WRAP_THRESHOLD_DEG
{
if current.len() >= 2 {
out.push(std::mem::take(&mut current));
} else {
current.clear();
}
}
current.push(p);
}
if current.len() >= 2 {
out.push(current);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_landmasses_parses_at_least_one_polygon() {
let landmasses = raw_landmasses();
assert!(
!landmasses.is_empty(),
"expected polygons from bundled GeoJSON"
);
assert!(
landmasses
.iter()
.any(|l| l.rings.first().is_some_and(|r| r.len() >= 3) && !l.triangles.is_empty())
);
}
#[test]
fn triangulate_polygon_handles_simple_quad() {
let rings = vec![vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]];
let lm = triangulate_polygon(rings);
assert_eq!(lm.rings.first().expect("one ring").len(), 4);
assert_eq!(lm.triangles.len(), 2 * 3);
assert_eq!(lm.vertex_count, 4);
}
#[test]
fn triangulate_polygon_strips_closing_vertex() {
let rings = vec![vec![
(0.0, 0.0),
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(0.0, 0.0),
]];
let lm = triangulate_polygon(rings);
assert_eq!(lm.rings.first().expect("one ring").len(), 4);
assert_eq!(lm.vertex_count, 4);
}
#[test]
fn split_long_edges_breaks_antimeridian_jump() {
let strip = vec![egui::Pos2::new(-179.0, 0.0), egui::Pos2::new(179.0, 0.0)];
let parts = split_long_edges(&strip);
assert!(parts.is_empty() || parts.iter().all(|p| p.len() < 2));
}
#[test]
fn landmasses_yields_some_triangles() {
let lm = landmasses();
assert!(
!lm.triangles.is_empty(),
"expected non-empty fill triangles"
);
assert!(
!lm.outlines.is_empty(),
"expected non-empty outline polylines"
);
}
}