use core::f64::consts::PI;
pub const RING_N_290: [usize; 18] = [
1, 4, 8, 12, 16, 20, 24, 28, 32, 32, 28, 24, 20, 16, 12, 8, 4, 1,
];
pub const RING_BASE_290: [usize; 18] = [
1, 2, 6, 14, 26, 42, 62, 86, 114, 146, 178, 206, 230, 250, 266, 278, 286, 290,
];
pub const DEC_BOUNDARIES_290: [f64; 19] = [
-90.0_f64 * PI / 180.0,
-85.232244043 * PI / 180.0,
-75.663487557 * PI / 180.0,
-65.992866371 * PI / 180.0,
-56.144973872 * PI / 180.0,
-46.031630674 * PI / 180.0,
-35.543077453 * PI / 180.0,
-24.533481154 * PI / 180.0,
-12.794405888 * PI / 180.0,
0.0,
12.794405888 * PI / 180.0,
24.533481154 * PI / 180.0,
35.543077453 * PI / 180.0,
46.031630674 * PI / 180.0,
56.144973872 * PI / 180.0,
65.992866371 * PI / 180.0,
75.663487557 * PI / 180.0,
85.232244043 * PI / 180.0,
90.0_f64 * PI / 180.0,
];
fn ring_of_dec(dec: f64) -> usize {
for ring in (0usize..18).rev() {
if dec >= DEC_BOUNDARIES_290[ring] {
return ring;
}
}
0
}
#[must_use]
pub fn area_nr_290(ra: f64, dec: f64) -> usize {
let ring = ring_of_dec(dec);
let n_ra = RING_N_290[ring];
let rot = ra.rem_euclid(2.0 * PI) * n_ra as f64 / (2.0 * PI);
RING_BASE_290[ring] + (rot.floor() as usize).min(n_ra - 1)
}
#[must_use]
pub fn filename_290(area_nr: usize) -> String {
let area = area_nr.clamp(1, 290);
let mut ring = 17usize;
for r in 0..18 {
if area >= RING_BASE_290[r] && area < RING_BASE_290[r] + RING_N_290[r] {
ring = r;
break;
}
}
let cell = area - RING_BASE_290[ring] + 1;
format!("{:02}{:02}.290", ring + 1, cell)
}
#[must_use]
pub fn find_areas_290(ra: f64, dec: f64, fov: f64) -> Vec<usize> {
let half = (fov * 0.5).clamp(0.0, PI);
let dec_lo = (dec - half).max(-PI / 2.0);
let dec_hi = (dec + half).min(PI / 2.0);
let ring_lo = ring_of_dec(dec_lo);
let ring_hi = ring_of_dec(dec_hi);
let touches_pole = dec_hi >= DEC_BOUNDARIES_290[18] - 1e-12
|| dec_lo <= DEC_BOUNDARIES_290[0] + 1e-12
|| dec + half >= PI / 2.0
|| dec - half <= -PI / 2.0;
let mut out = Vec::new();
for ring in ring_lo..=ring_hi {
let n_ra = RING_N_290[ring];
let base = RING_BASE_290[ring];
if n_ra == 1 || touches_pole {
for c in 0..n_ra {
out.push(base + c);
}
continue;
}
let d_near = dec.clamp(DEC_BOUNDARIES_290[ring], DEC_BOUNDARIES_290[ring + 1]);
let cos_d = d_near.cos();
if cos_d < 1e-6 {
for c in 0..n_ra {
out.push(base + c);
}
continue;
}
let ra_half = half / cos_d;
if ra_half >= PI {
for c in 0..n_ra {
out.push(base + c);
}
continue;
}
let step = 2.0 * PI / n_ra as f64;
let c_lo = ((ra - ra_half).rem_euclid(2.0 * PI) / step).floor() as i64;
let c_hi = ((ra + ra_half).rem_euclid(2.0 * PI) / step).floor() as i64;
let span = (c_hi - c_lo).rem_euclid(n_ra as i64);
for k in 0..=span {
let c = (c_lo + k).rem_euclid(n_ra as i64) as usize;
out.push(base + c);
}
}
out.sort_unstable();
out.dedup();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn deg(d: f64) -> f64 {
d * PI / 180.0
}
#[test]
fn ring_table_totals_290() {
assert_eq!(RING_N_290.iter().sum::<usize>(), 290);
for r in 0..18 {
assert_eq!(
RING_BASE_290[r] + RING_N_290[r],
if r == 17 { 291 } else { RING_BASE_290[r + 1] },
"ring {r} base/count are inconsistent"
);
}
}
#[test]
fn dec_boundaries_are_equal_area() {
let weights: Vec<f64> = (0..18)
.map(|r| {
if r == 0 || r == 17 {
0.5
} else {
RING_N_290[r] as f64
}
})
.collect();
let total: f64 = weights.iter().sum();
let mut cum = 0.0;
for r in 0..18 {
cum += weights[r];
let want = -1.0 + 2.0 * cum / total;
let got = DEC_BOUNDARIES_290[r + 1].sin();
assert!(
(want - got).abs() < 1e-9,
"ring {r} boundary: sin want {want}, got {got}"
);
}
}
#[test]
fn poles_and_equator_land_in_the_right_areas() {
assert_eq!(area_nr_290(0.0, deg(-89.9)), 1);
assert_eq!(area_nr_290(0.0, deg(89.9)), 290);
assert_eq!(area_nr_290(0.0, deg(-0.001)), 114);
assert_eq!(area_nr_290(0.0, 0.0), 146);
}
#[test]
fn filenames_round_trip() {
for area in 1..=290usize {
let f = filename_290(area);
assert!(f.ends_with(".290"));
let ring: usize = f[0..2].parse().unwrap();
let cell: usize = f[2..4].parse().unwrap();
assert!((1..=18).contains(&ring), "area {area} -> {f}");
assert!(
cell >= 1 && cell <= RING_N_290[ring - 1],
"area {area} -> {f}"
);
assert_eq!(RING_BASE_290[ring - 1] + cell - 1, area);
}
assert_eq!(filename_290(1), "0101.290");
assert_eq!(filename_290(290), "1801.290");
assert_eq!(filename_290(2), "0201.290");
assert_eq!(filename_290(5), "0204.290");
}
#[test]
fn small_field_takes_few_areas_and_contains_its_own_centre() {
for &(ra, dec) in &[
(0.0, 0.0),
(deg(180.0), deg(30.0)),
(deg(275.0), deg(-45.0)),
(deg(10.0), deg(70.0)),
] {
let areas = find_areas_290(ra, dec, deg(4.0));
assert!(
areas.contains(&area_nr_290(ra, dec)),
"field at ({ra}, {dec}) does not include its own centre cell"
);
assert!(areas.len() <= 8, "4° field took {} areas", areas.len());
}
}
#[test]
fn wide_field_spans_many_rings() {
let areas = find_areas_290(deg(180.0), 0.0, deg(60.0));
assert!(
areas.len() > 20,
"60° field took only {} areas",
areas.len()
);
assert!(areas.contains(&area_nr_290(deg(180.0), 0.0)));
}
#[test]
fn polar_field_takes_whole_rings() {
let areas = find_areas_290(0.0, deg(88.0), deg(20.0));
assert!(areas.contains(&290), "must include the north polar cap");
for c in 0..RING_N_290[16] {
assert!(
areas.contains(&(RING_BASE_290[16] + c)),
"polar field missed cell {c} of ring 17"
);
}
}
#[test]
fn ra_wrap_is_handled() {
let areas = find_areas_290(deg(0.5), 0.0, deg(8.0));
let n_ra = RING_N_290[9];
let base = RING_BASE_290[9];
assert!(areas.contains(&base), "missing the first cell of the ring");
assert!(
areas.contains(&(base + n_ra - 1)),
"missing the last cell of the ring (RA wrap)"
);
}
#[test]
fn all_sky_field_takes_every_area() {
let areas = find_areas_290(0.0, 0.0, deg(180.0));
assert_eq!(areas.len(), 290);
}
}