1use std::fs;
5use std::path::{Path, PathBuf};
6
7pub const ASTAP_EXTS: &[(&str, usize)] = &[(".1476", 1476), (".290", 290), (".001", 1)];
10
11#[must_use]
20pub fn default_db_path() -> PathBuf {
21 let managed = super::default_catalog_dir();
22 if has_star_database(&managed) {
23 managed
24 } else {
25 PathBuf::from(".")
26 }
27}
28
29#[must_use]
32pub fn has_star_database(dir: &Path) -> bool {
33 DB_FOV_RANGES.iter().any(|(prefix, ..)| {
34 ASTAP_EXTS
35 .iter()
36 .any(|(ext, _)| dir.join(format!("{prefix}_0101{ext}")).exists())
37 })
38}
39
40pub const DB_FOV_RANGES: &[(&str, f64, f64, u8)] = &[
47 ("d80", 0.15, 6.0, 8),
49 ("v50", 0.20, 6.0, 6),
50 ("d50", 0.20, 6.0, 5),
51 ("d20", 0.30, 6.0, 4),
52 ("v05", 0.60, 6.0, 3),
53 ("d05", 0.60, 6.0, 2),
54 ("g05", 3.00, 20.0, 7),
55 ("w08", 20.0, 80.0, 7),
56];
57
58fn db_prefix(file_name: &str) -> Option<&str> {
62 if ASTAP_EXTS.iter().any(|(ext, _)| file_name.ends_with(ext)) {
63 file_name.split('_').next()
64 } else {
65 None
66 }
67}
68
69#[must_use]
71pub fn available_dbs(db_path: &Path) -> Vec<String> {
72 let mut out: Vec<String> = fs::read_dir(db_path)
73 .into_iter()
74 .flatten()
75 .flatten()
76 .filter_map(|e| db_prefix(&e.file_name().to_string_lossy()).map(str::to_string))
77 .collect();
78 out.sort();
79 out.dedup();
80 out
81}
82
83#[must_use]
87pub fn select_db_for_fov(db_path: &Path, fov_deg: f64) -> Option<String> {
88 select_from(&available_dbs(db_path), fov_deg)
89}
90
91fn select_from(present: &[String], fov_deg: f64) -> Option<String> {
97 if present.is_empty() {
98 return None;
99 }
100 let installed = DB_FOV_RANGES
101 .iter()
102 .filter(|(prefix, ..)| present.iter().any(|p| p == prefix));
103
104 let mut best: Option<(u8, &str)> = None;
105 for &(prefix, lo, hi, pref) in installed.clone() {
106 if fov_deg >= lo && fov_deg <= hi && best.is_none_or(|(bp, _)| pref > bp) {
107 best = Some((pref, prefix));
108 }
109 }
110 if let Some((_, prefix)) = best {
111 return Some(prefix.to_string());
112 }
113
114 let mut fallback: Option<(f64, &str)> = None;
116 for &(prefix, lo, hi, _) in installed {
117 let dist = if fov_deg < lo {
118 lo - fov_deg
119 } else {
120 fov_deg - hi
121 };
122 if fallback.is_none_or(|(bd, _)| dist < bd) {
123 fallback = Some((dist, prefix));
124 }
125 }
126 fallback
127 .map(|(_, p)| p.to_string())
128 .or_else(|| present.first().cloned())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn owned(v: &[&str]) -> Vec<String> {
136 v.iter().map(|s| (*s).to_string()).collect()
137 }
138
139 #[test]
140 fn prefixes_come_from_every_database_format() {
141 assert_eq!(db_prefix("d50_0101.1476"), Some("d50"));
142 assert_eq!(db_prefix("g05_0101.290"), Some("g05"));
143 assert_eq!(db_prefix("w08_0101.001"), Some("w08"));
144 assert_eq!(db_prefix("index-4107.fits"), None);
145 assert_eq!(db_prefix("readme.txt"), None);
146 }
147
148 #[test]
149 fn the_densest_covering_database_wins() {
150 let all = owned(&["d05", "d50", "d80", "g05", "w08"]);
151 assert_eq!(select_from(&all, 1.0).as_deref(), Some("d80"));
152 assert_eq!(select_from(&all, 10.0).as_deref(), Some("g05"));
153 assert_eq!(select_from(&all, 40.0).as_deref(), Some("w08"));
154 assert_eq!(select_from(&all, 4.0).as_deref(), Some("d80"));
156 }
157
158 #[test]
159 fn an_uncovered_field_gets_the_nearest_range() {
160 let d = owned(&["d50", "g05"]);
161 assert_eq!(select_from(&d, 0.05).as_deref(), Some("d50"));
162 assert_eq!(select_from(&d, 30.0).as_deref(), Some("g05"));
163 }
164
165 #[test]
166 fn unknown_prefixes_are_a_last_resort() {
167 assert_eq!(select_from(&owned(&["h18"]), 1.0).as_deref(), Some("h18"));
168 assert_eq!(select_from(&[], 1.0), None);
169 }
170}