use serde::Deserialize; #[derive(Debug, Deserialize)] struct FeatureCollection { features: Vec, } #[derive(Debug, Deserialize)] struct Feature { properties: Properties, geometry: Geometry, } #[derive(Debug, Deserialize)] struct Properties { #[serde(rename = "ISO_A2_EH")] iso_a2: String, #[serde(rename = "NAME")] name: String, } #[derive(Debug, Deserialize)] #[serde(tag = "type")] enum Geometry { Polygon { coordinates: Vec>, }, MultiPolygon { coordinates: Vec>>, }, } #[allow(dead_code)] pub struct Country { pub iso_a2: String, pub name: String, /// Each polygon is a list of rings; ring 0 = outer, rest = holes pub polygons: Vec>>, pub bbox: (f64, f64, f64, f64), // (min_lon, min_lat, max_lon, max_lat) pub label_pos: (f64, f64), // (lon, lat) — centroid of largest polygon } /// Signed area of a ring (positive = CCW). fn ring_signed_area(ring: &[[f64; 2]]) -> f64 { let n = ring.len(); if n < 3 { return 0.0; } let mut area = 0.0; let mut j = n - 1; for i in 0..n { area += (ring[j][0] - ring[i][0]) * (ring[j][1] + ring[i][1]); j = i; } area / 2.0 } /// Ray-casting point-in-ring test. fn point_in_ring(lon: f64, lat: f64, ring: &[[f64; 2]]) -> bool { let mut inside = false; let n = ring.len(); if n < 3 { return false; } let mut j = n - 1; for i in 0..n { let xi = ring[i][0]; let yi = ring[i][1]; let xj = ring[j][0]; let yj = ring[j][1]; if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) { inside = !inside; } j = i; } inside } /// Find horizontal interior spans at a given latitude. /// Returns sorted pairs of (enter_lon, exit_lon). fn horizontal_spans(lat: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> { let n = ring.len(); if n < 3 { return Vec::new(); } let mut crossings = Vec::new(); let mut j = n - 1; for i in 0..n { let yi = ring[i][1]; let yj = ring[j][1]; if (yi > lat) != (yj > lat) { let xi = ring[i][0]; let xj = ring[j][0]; crossings.push((xj - xi) * (lat - yi) / (yj - yi) + xi); } j = i; } crossings.sort_by(|a, b| a.partial_cmp(b).unwrap()); crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect() } /// Find vertical interior spans at a given longitude. /// Returns sorted pairs of (enter_lat, exit_lat). fn vertical_spans(lon: f64, ring: &[[f64; 2]]) -> Vec<(f64, f64)> { let n = ring.len(); if n < 3 { return Vec::new(); } let mut crossings = Vec::new(); let mut j = n - 1; for i in 0..n { let xi = ring[i][0]; let xj = ring[j][0]; if (xi > lon) != (xj > lon) { let yi = ring[i][1]; let yj = ring[j][1]; crossings.push((yj - yi) * (lon - xi) / (xj - xi) + yi); } j = i; } crossings.sort_by(|a, b| a.partial_cmp(b).unwrap()); crossings.chunks_exact(2).map(|p| (p[0], p[1])).collect() } /// Find a good interior label point for a ring. /// Scans a grid of candidate points and picks the one that maximizes /// min(half_width, half_height) — the "most interior" point. fn ring_label_point(ring: &[[f64; 2]]) -> (f64, f64) { let min_lon = ring.iter().map(|c| c[0]).fold(f64::MAX, f64::min); let max_lon = ring.iter().map(|c| c[0]).fold(f64::MIN, f64::max); let min_lat = ring.iter().map(|c| c[1]).fold(f64::MAX, f64::min); let max_lat = ring.iter().map(|c| c[1]).fold(f64::MIN, f64::max); let steps = 24; let mut best = ((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0); let mut best_score = 0.0f64; for row in 1..steps { let lat = min_lat + (max_lat - min_lat) * row as f64 / steps as f64; let h_spans = horizontal_spans(lat, ring); for &(span_left, span_right) in &h_spans { let mid_lon = (span_left + span_right) / 2.0; let half_w = (span_right - span_left) / 2.0; // Measure vertical extent at this longitude let v_spans = vertical_spans(mid_lon, ring); for &(span_bot, span_top) in &v_spans { if lat >= span_bot && lat <= span_top { let half_h = ((lat - span_bot).min(span_top - lat)).min(half_w); let score = half_w.min(half_h); if score > best_score { best_score = score; best = (mid_lon, lat); } break; } } } } best } pub fn load_countries(geojson: &str) -> Vec { let fc: FeatureCollection = serde_json::from_str(geojson).expect("Failed to parse GeoJSON"); fc.features .into_iter() .map(|f| { let polygons = match f.geometry { Geometry::Polygon { coordinates } => vec![coordinates], Geometry::MultiPolygon { coordinates } => coordinates, }; let mut min_lon = f64::MAX; let mut min_lat = f64::MAX; let mut max_lon = f64::MIN; let mut max_lat = f64::MIN; for poly in &polygons { for ring in poly { for coord in ring { let lon = coord[0]; let lat = coord[1]; if lon < min_lon { min_lon = lon; } if lon > max_lon { max_lon = lon; } if lat < min_lat { min_lat = lat; } if lat > max_lat { max_lat = lat; } } } } // Label inside the largest polygon let label_pos = polygons .iter() .filter(|p| !p.is_empty() && p[0].len() >= 3) .max_by(|a, b| { ring_signed_area(&a[0]) .abs() .partial_cmp(&ring_signed_area(&b[0]).abs()) .unwrap() }) .map(|p| ring_label_point(&p[0])) .unwrap_or(((min_lon + max_lon) / 2.0, (min_lat + max_lat) / 2.0)); Country { iso_a2: f.properties.iso_a2, name: f.properties.name, polygons, bbox: (min_lon, min_lat, max_lon, max_lat), label_pos, } }) .collect() } /// Check if a point is inside a polygon (outer ring minus holes). fn point_in_polygon(lon: f64, lat: f64, rings: &[Vec<[f64; 2]>]) -> bool { if rings.is_empty() || !point_in_ring(lon, lat, &rings[0]) { return false; } // Must be outside all holes !rings[1..].iter().any(|hole| point_in_ring(lon, lat, hole)) } /// Check if a point falls inside a country. pub fn point_in_country(lon: f64, lat: f64, country: &Country) -> bool { let (min_lon, min_lat, max_lon, max_lat) = country.bbox; if lon < min_lon || lon > max_lon || lat < min_lat || lat > max_lat { return false; } country .polygons .iter() .any(|poly| point_in_polygon(lon, lat, poly)) } /// Find which country contains the given point, with a nearest-country /// fallback for when low-res coastlines cause a near miss. pub fn find_country(lon: f64, lat: f64, countries: &[Country]) -> Option { // Exact hit if let Some(idx) = countries.iter().position(|c| point_in_country(lon, lat, c)) { return Some(idx); } // Search in expanding rings up to ~1 degree for &offset in &[0.25, 0.5, 1.0] { for &(dlon, dlat) in &[ (offset, 0.0), (-offset, 0.0), (0.0, offset), (0.0, -offset), (offset, offset), (offset, -offset), (-offset, offset), (-offset, -offset), ] { if let Some(idx) = countries .iter() .position(|c| point_in_country(lon + dlon, lat + dlat, c)) { return Some(idx); } } } None }