Add target comparison modal, integration goal progress, and session planning + full catalog expansion
Features added this session: - Target comparison: side-by-side overlay (CompareModal) from Targets page via ⊕ button on each row; shows altitude curves, key times, filter recommendations and per-filter integration progress for two targets simultaneously - Integration goal progress dashboard card: per-target keeper minutes vs goal hours (from CLAUDE.md §16.3) broken down by filter, with color-coded progress bars; powered by new stats.integration_goals backend query - Session planning timeline: Gantt-style "Plan Tonight" section on Dashboard (PlanningTimeline component) — search targets, set durations, sequential scheduling from dusk, overrun warnings, clipboard export - Slew-optimized run order toggle (nearest-neighbor sort by RA/Dec angular distance) - Best Nights 14-day card + Monthly Highlights card on Dashboard Catalog expansions: - Sharpless (Sh2), VdB, LDN, Barnard dark nebulae, LBN, Melotte, Collinder, Gum, RCW, Abell PN, Abell GC, PGC bright subset - Caldwell/Arp/Melotte/Collinder number columns + cross-reference maps - Weather score multiplier applied to composite sort - galaxy_cluster type (ACO badge) throughout TypeBadge, CSS, filter chips Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
/// 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<Vec<CatalogEntry>> {
|
||||
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<Vec<CatalogEntry>> {
|
||||
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<Sh2Row> = 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<Sh2Row> {
|
||||
let mut rows = Vec::new();
|
||||
let mut header: Vec<String> = 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<usize> {
|
||||
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::<u32>().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::<f64>().ok())
|
||||
.unwrap_or(15.0);
|
||||
|
||||
let ra = col_idx("_RA")
|
||||
.and_then(|i| cols.get(i))
|
||||
.and_then(|s| s.parse::<f64>().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::<f64>().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<Sh2Row>) -> Vec<CatalogEntry> {
|
||||
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<Sh2Row> {
|
||||
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
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user