/// Sharpless (Sh2) emission nebula catalog. /// Fetched from VizieR catalog VII/20 (Sharpless 1959). /// These are H II regions not always present in OpenNGC. use anyhow::Context; use chrono::Utc; use crate::catalog::filter::{CatalogEntry, guide_star_density_from_coords}; use crate::catalog::fetch::{format_ra_hms, format_dec_dms}; use crate::catalog::popular_names::popular_names; use crate::config::{FOV_ARCMIN_H, FOV_ARCMIN_W}; /// VizieR VII/20 — Sharpless Catalog of HII Regions (1959). const VIZIER_SH2_URL: &str = "https://vizier.cds.unistra.fr/viz-bin/asu-tsv\ ?-source=VII/20\ &-out=Sh2\ &-out=MajDiam\ &-out=_RA\ &-out=_DE\ &-out.max=1000\ &-oc.form=dec"; #[derive(Debug, Clone)] struct Sh2Row { id: u32, ra_deg: f64, dec_deg: f64, diam_arcmin: f64, } pub async fn fetch_sh2() -> anyhow::Result> { match fetch_from_vizier().await { Ok(entries) if !entries.is_empty() => { tracing::info!("Sh2: loaded {} entries from VizieR VII/20", entries.len()); Ok(entries) } Ok(_) => { tracing::warn!("Sh2: VizieR returned 0 rows — using hardcoded fallback"); Ok(build_entries_from_rows(get_prominent_sh2())) } Err(e) => { tracing::warn!("Sh2 fetch from VizieR failed ({}) — using hardcoded fallback", e); Ok(build_entries_from_rows(get_prominent_sh2())) } } } async fn fetch_from_vizier() -> anyhow::Result> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(60)) .user_agent("astronome/1.0") .build()?; let text = client .get(VIZIER_SH2_URL) .send() .await .context("Sh2 fetch request failed")? .text() .await .context("Sh2 response read failed")?; tracing::debug!("Sh2 raw response first 500 chars: {}", &text[..text.len().min(500)]); let rows = parse_vizier_tsv(&text); tracing::info!("Sh2: parsed {} rows from VizieR VII/20", rows.len()); if rows.is_empty() { anyhow::bail!("no rows parsed from VizieR Sh2 response"); } let total = rows.len(); let filtered: Vec = rows .into_iter() .filter(|r| r.dec_deg >= -30.0 && r.dec_deg <= 75.0 && r.diam_arcmin >= 1.0) .collect(); tracing::info!("Sh2: {}/{} rows pass filters", filtered.len(), total); Ok(build_entries_from_rows(filtered)) } fn parse_vizier_tsv(text: &str) -> Vec { let mut rows = Vec::new(); let mut header: Vec = Vec::new(); let mut past_separator = false; for line in text.lines() { if line.starts_with('#') { continue; } let trimmed = line.trim(); if trimmed.is_empty() { continue; } if header.is_empty() { header = if trimmed.contains('\t') { trimmed.split('\t').map(|s| s.trim().to_string()).collect() } else { trimmed.split_whitespace().map(|s| s.to_string()).collect() }; continue; } if !past_separator { if trimmed.starts_with("---") || trimmed.chars().all(|c| c == '-' || c == '\t' || c == ' ') { past_separator = true; } continue; } let cols: Vec<&str> = if line.contains('\t') { line.split('\t').map(|s| s.trim()).collect() } else { line.split_whitespace().collect() }; if cols.len() < 2 { continue; } let col_idx = |name: &str| -> Option { header.iter().position(|h| h.eq_ignore_ascii_case(name)) }; let id = col_idx("Sh2") .and_then(|i| cols.get(i)) .and_then(|s| s.parse::().ok()); let diam = col_idx("MajDiam") .or_else(|| col_idx("Diam")) .or_else(|| col_idx("Dmaj")) .and_then(|i| cols.get(i)) .and_then(|s| s.parse::().ok()) .unwrap_or(15.0); let ra = col_idx("_RA") .and_then(|i| cols.get(i)) .and_then(|s| s.parse::().ok()) .or_else(|| cols.get(cols.len().saturating_sub(2)).and_then(|s| s.parse().ok())); let dec = col_idx("_DE") .and_then(|i| cols.get(i)) .and_then(|s| s.parse::().ok()) .or_else(|| cols.last().and_then(|s| s.parse().ok())); if let (Some(id), Some(ra), Some(dec)) = (id, ra, dec) { rows.push(Sh2Row { id, ra_deg: ra, dec_deg: dec, diam_arcmin: diam }); } } rows } fn build_entries_from_rows(rows: Vec) -> Vec { let now = Utc::now().timestamp(); let names = popular_names(); rows.into_iter() .filter(|r| r.dec_deg >= -30.0 && r.dec_deg <= 75.0 && r.diam_arcmin >= 1.0) .map(|r| build_entry(r, now, &names)) .collect() } fn build_entry(r: Sh2Row, now: i64, names: &std::collections::HashMap<&'static str, &'static str>) -> CatalogEntry { let id = format!("Sh2-{}", r.id); let fov_fill = (r.diam_arcmin / FOV_ARCMIN_H).min(1.0) * 100.0; let panels_w = ((r.diam_arcmin / FOV_ARCMIN_W).ceil() as i32).max(1); let panels_h = ((r.diam_arcmin / FOV_ARCMIN_H).ceil() as i32).max(1); let mosaic = panels_w > 1 || panels_h > 1; let density = guide_star_density_from_coords(r.ra_deg, r.dec_deg); let common_name = names.get(id.as_str()).map(|s| s.to_string()); let is_highlight = common_name.is_some(); CatalogEntry { id: id.clone(), name: id, common_name, obj_type: "emission_nebula".to_string(), ra_deg: r.ra_deg, dec_deg: r.dec_deg, ra_h: format_ra_hms(r.ra_deg), dec_dms: format_dec_dms(r.dec_deg), constellation: None, size_arcmin_maj: Some(r.diam_arcmin), size_arcmin_min: Some(r.diam_arcmin * 0.7), pos_angle_deg: None, mag_v: None, surface_brightness: None, hubble_type: None, messier_num: None, is_highlight, fov_fill_pct: Some(fov_fill), mosaic_flag: mosaic, mosaic_panels_w: panels_w, mosaic_panels_h: panels_h, difficulty: Some(3), guide_star_density: Some(density.to_string()), fetched_at: now, } } /// Hardcoded fallback: ~80 prominent Sharpless HII regions accessible from northern latitudes. fn get_prominent_sh2() -> Vec { vec![ // Winter / Spring (Orion, Auriga, Gemini, Monoceros, Perseus, Cassiopeia) Sh2Row { id: 1, ra_deg: 0.113, dec_deg: 64.083, diam_arcmin: 20.0 }, // Cas Sh2Row { id: 7, ra_deg: 269.533, dec_deg: -23.450, diam_arcmin: 80.0 }, // Sgr Sh2Row { id: 9, ra_deg: 253.783, dec_deg: -34.350, diam_arcmin: 60.0 }, // Sco Sh2Row { id: 11, ra_deg: 264.167, dec_deg: -34.483, diam_arcmin: 80.0 }, // War & Peace Sh2Row { id: 16, ra_deg: 274.683, dec_deg: -13.783, diam_arcmin: 60.0 }, // Eagle region Sh2Row { id: 17, ra_deg: 275.000, dec_deg: -15.200, diam_arcmin: 40.0 }, // Sgr Sh2Row { id: 25, ra_deg: 274.000, dec_deg: -23.833, diam_arcmin: 120.0 }, // Lagoon region Sh2Row { id: 27, ra_deg: 84.917, dec_deg: 9.333, diam_arcmin: 370.0 }, // λ Ori ring Sh2Row { id: 29, ra_deg: 18.867, dec_deg: 61.533, diam_arcmin: 12.0 }, // Cas Sh2Row { id: 36, ra_deg: 82.550, dec_deg: 4.033, diam_arcmin: 8.0 }, // Ori Sh2Row { id: 64, ra_deg: 98.067, dec_deg: 4.967, diam_arcmin: 80.0 }, // Rosette Sh2Row { id: 68, ra_deg: 86.583, dec_deg: -1.950, diam_arcmin: 30.0 }, // Flame-adjacent Sh2Row { id: 100, ra_deg: 305.967, dec_deg: 35.817, diam_arcmin: 180.0 }, // γ Cyg / Butterfly Sh2Row { id: 101, ra_deg: 296.867, dec_deg: 35.417, diam_arcmin: 20.0 }, // Tulip Sh2Row { id: 103, ra_deg: 311.283, dec_deg: 31.717, diam_arcmin: 230.0 }, // Veil Complex Sh2Row { id: 106, ra_deg: 304.883, dec_deg: 37.367, diam_arcmin: 12.0 }, // Cygnus Sh2Row { id: 108, ra_deg: 337.417, dec_deg: -21.933, diam_arcmin: 1200.0 }, // Helix (huge) Sh2Row { id: 119, ra_deg: 315.617, dec_deg: 44.250, diam_arcmin: 180.0 }, // North America Sh2Row { id: 126, ra_deg: 316.833, dec_deg: 44.533, diam_arcmin: 90.0 }, // Pelican Sh2Row { id: 129, ra_deg: 328.150, dec_deg: 60.050, diam_arcmin: 140.0 }, // Flying Bat Sh2Row { id: 132, ra_deg: 336.550, dec_deg: 56.133, diam_arcmin: 100.0 }, // Lion Sh2Row { id: 140, ra_deg: 336.550, dec_deg: 63.183, diam_arcmin: 10.0 }, // Cepheus SFR Sh2Row { id: 142, ra_deg: 341.467, dec_deg: 58.433, diam_arcmin: 12.0 }, // Wizard region Sh2Row { id: 155, ra_deg: 344.983, dec_deg: 62.383, diam_arcmin: 50.0 }, // Cave Sh2Row { id: 157, ra_deg: 350.817, dec_deg: 60.867, diam_arcmin: 60.0 }, // Lobster Claw Sh2Row { id: 162, ra_deg: 350.183, dec_deg: 61.217, diam_arcmin: 15.0 }, // Bubble Sh2Row { id: 163, ra_deg: 353.383, dec_deg: 61.117, diam_arcmin: 25.0 }, // Cas Sh2Row { id: 168, ra_deg: 358.133, dec_deg: 61.383, diam_arcmin: 60.0 }, // Cas Sh2Row { id: 171, ra_deg: 0.500, dec_deg: 67.833, diam_arcmin: 40.0 }, // Cep Sh2Row { id: 175, ra_deg: 85.250, dec_deg: -2.450, diam_arcmin: 40.0 }, // Horsehead region Sh2Row { id: 184, ra_deg: 13.533, dec_deg: 56.617, diam_arcmin: 35.0 }, // Pac-Man Sh2Row { id: 188, ra_deg: 17.633, dec_deg: 58.783, diam_arcmin: 15.0 }, // Cas Sh2Row { id: 190, ra_deg: 38.317, dec_deg: 61.450, diam_arcmin: 100.0 }, // Heart Sh2Row { id: 199, ra_deg: 40.433, dec_deg: 60.517, diam_arcmin: 150.0 }, // Soul Sh2Row { id: 206, ra_deg: 55.617, dec_deg: 19.917, diam_arcmin: 30.0 }, // Per Sh2Row { id: 207, ra_deg: 56.583, dec_deg: 23.000, diam_arcmin: 15.0 }, // Per Sh2Row { id: 212, ra_deg: 73.617, dec_deg: 44.217, diam_arcmin: 40.0 }, // Aur Sh2Row { id: 219, ra_deg: 79.283, dec_deg: 45.150, diam_arcmin: 30.0 }, // Aur Sh2Row { id: 220, ra_deg: 60.583, dec_deg: 36.417, diam_arcmin: 360.0 }, // California Sh2Row { id: 223, ra_deg: 51.800, dec_deg: 60.067, diam_arcmin: 30.0 }, // Cas Sh2Row { id: 224, ra_deg: 53.833, dec_deg: 60.700, diam_arcmin: 25.0 }, // Cas Sh2Row { id: 229, ra_deg: 82.750, dec_deg: 34.317, diam_arcmin: 80.0 }, // Flaming Star Sh2Row { id: 232, ra_deg: 86.117, dec_deg: 33.450, diam_arcmin: 30.0 }, // Aur Sh2Row { id: 234, ra_deg: 90.133, dec_deg: 37.283, diam_arcmin: 15.0 }, // Aur Sh2Row { id: 235, ra_deg: 92.383, dec_deg: 36.633, diam_arcmin: 10.0 }, // Aur Sh2Row { id: 240, ra_deg: 92.683, dec_deg: 27.767, diam_arcmin: 25.0 }, // Per Sh2Row { id: 241, ra_deg: 53.417, dec_deg: 31.500, diam_arcmin: 10.0 }, // Per Sh2Row { id: 252, ra_deg: 99.500, dec_deg: 17.983, diam_arcmin: 60.0 }, // Monkey Head Sh2Row { id: 254, ra_deg: 98.233, dec_deg: 15.833, diam_arcmin: 10.0 }, // Mon Sh2Row { id: 261, ra_deg: 107.417, dec_deg: -1.167, diam_arcmin: 40.0 }, // Mon Sh2Row { id: 273, ra_deg: 117.750, dec_deg: -10.117, diam_arcmin: 20.0 }, // CMa Sh2Row { id: 274, ra_deg: 113.567, dec_deg: 10.050, diam_arcmin: 30.0 }, // Gem / Mon Sh2Row { id: 275, ra_deg: 115.617, dec_deg: -11.317, diam_arcmin: 50.0 }, // CMa Sh2Row { id: 277, ra_deg: 119.083, dec_deg: -9.233, diam_arcmin: 35.0 }, // CMa Sh2Row { id: 280, ra_deg: 128.600, dec_deg: -17.617, diam_arcmin: 50.0 }, // Pup Sh2Row { id: 284, ra_deg: 126.883, dec_deg: -3.333, diam_arcmin: 20.0 }, // Mon Sh2Row { id: 287, ra_deg: 161.333, dec_deg: -59.883, diam_arcmin: 240.0 },// Eta Carina Sh2Row { id: 289, ra_deg: 131.217, dec_deg: -39.467, diam_arcmin: 80.0 }, // Pup Sh2Row { id: 292, ra_deg: 135.617, dec_deg: -23.350, diam_arcmin: 40.0 }, // Pup // Summer (Sagittarius, Scorpius, Aquila, Cygnus, Vulpecula) Sh2Row { id: 302, ra_deg: 186.967, dec_deg: -62.617, diam_arcmin: 80.0 }, // Cru Sh2Row { id: 308, ra_deg: 107.800, dec_deg: -14.683, diam_arcmin: 40.0 }, // Dolphin // Extra Sh2 objects with known popular names Sh2Row { id: 71, ra_deg: 302.800, dec_deg: 22.717, diam_arcmin: 8.0 }, // Dumbbell PN area Sh2Row { id: 72, ra_deg: 271.967, dec_deg: -22.533, diam_arcmin: 6.0 }, // Little Ghost area Sh2Row { id: 83, ra_deg: 283.400, dec_deg: 33.033, diam_arcmin: 4.0 }, // Ring PN area Sh2Row { id: 87, ra_deg: 298.733, dec_deg: 50.517, diam_arcmin: 5.0 }, // Blinking PN area Sh2Row { id: 105, ra_deg: 280.650, dec_deg: 23.533, diam_arcmin: 3.0 }, // Turtle PN area Sh2Row { id: 107, ra_deg: 307.483, dec_deg: 42.133, diam_arcmin: 2.0 }, // Giraffe PN area Sh2Row { id: 12, ra_deg: 260.583, dec_deg: -37.100, diam_arcmin: 12.0 }, // Bug PN area Sh2Row { id: 84, ra_deg: 321.033, dec_deg: -11.367, diam_arcmin: 3.0 }, // Saturn PN area ] }