setiastrosuitepro 1.6.2.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +945 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +624 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +602 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +213 -0
- setiastro/saspro/clahe.py +368 -0
- setiastro/saspro/comet_stacking.py +1442 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +973 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8810 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +236 -0
- setiastro/saspro/isophote.py +1182 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1841 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +931 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3831 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1293 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1473 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1071 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +501 -0
- setiastro/saspro/rgb_combination.py +208 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +73 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +18470 -0
- setiastro/saspro/star_alignment.py +7435 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3328 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +3749 -0
- setiastro/saspro/translations/es_translations.py +3939 -0
- setiastro/saspro/translations/fr_translations.py +3858 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +270 -0
- setiastro/saspro/translations/it_translations.py +3678 -0
- setiastro/saspro/translations/ja_translations.py +3601 -0
- setiastro/saspro/translations/pt_translations.py +3869 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +3675 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +492 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
- setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
setiastro/saspro/wims.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# whatsinmysky.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# --- stdlib ---
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
import warnings
|
|
9
|
+
import webbrowser
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from decimal import getcontext
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# --- third-party ---
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import pytz
|
|
18
|
+
from astropy import units as u
|
|
19
|
+
from astropy.coordinates import SkyCoord, EarthLocation, AltAz, get_sun, get_body
|
|
20
|
+
from astropy.time import Time
|
|
21
|
+
|
|
22
|
+
# --- Qt / PyQt6 ---
|
|
23
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings
|
|
24
|
+
from PyQt6.QtGui import QIcon, QPixmap
|
|
25
|
+
from PyQt6.QtWidgets import (
|
|
26
|
+
QDialog, QLabel, QLineEdit, QComboBox, QCheckBox, QRadioButton, QButtonGroup,
|
|
27
|
+
QPushButton, QGridLayout, QTreeWidget, QTreeWidgetItem, QHeaderView, QFileDialog,
|
|
28
|
+
QScrollArea, QInputDialog, QMessageBox, QWidget, QHBoxLayout
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------
|
|
32
|
+
# paths / globals
|
|
33
|
+
# ---------------------------------------------------
|
|
34
|
+
def _app_root() -> str:
|
|
35
|
+
# this file sits next to setiastrosuitepro.py and imgs/
|
|
36
|
+
return getattr(sys, "_MEIPASS", os.path.dirname(__file__))
|
|
37
|
+
|
|
38
|
+
def imgs_path(*parts) -> str:
|
|
39
|
+
return os.path.join(_app_root(), "imgs", *parts)
|
|
40
|
+
|
|
41
|
+
getcontext().prec = 24
|
|
42
|
+
warnings.filterwarnings("ignore")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------
|
|
46
|
+
# Worker thread
|
|
47
|
+
# ---------------------------------------------------
|
|
48
|
+
class CalculationThread(QThread):
|
|
49
|
+
calculation_complete = pyqtSignal(pd.DataFrame, str)
|
|
50
|
+
lunar_phase_calculated = pyqtSignal(int, str) # phase_percentage, phase_image_name
|
|
51
|
+
lst_calculated = pyqtSignal(str)
|
|
52
|
+
status_update = pyqtSignal(str)
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
latitude: float,
|
|
57
|
+
longitude: float,
|
|
58
|
+
date: str,
|
|
59
|
+
time: str,
|
|
60
|
+
timezone: str,
|
|
61
|
+
min_altitude: float,
|
|
62
|
+
catalog_filters: list[str],
|
|
63
|
+
object_limit: int,
|
|
64
|
+
):
|
|
65
|
+
super().__init__()
|
|
66
|
+
self.latitude = float(latitude)
|
|
67
|
+
self.longitude = float(longitude)
|
|
68
|
+
self.date = date
|
|
69
|
+
self.time = time
|
|
70
|
+
self.timezone = timezone
|
|
71
|
+
self.min_altitude = float(min_altitude)
|
|
72
|
+
self.catalog_filters = list(catalog_filters or [])
|
|
73
|
+
self.object_limit = int(object_limit)
|
|
74
|
+
|
|
75
|
+
self.catalog_file = self.get_catalog_file_path()
|
|
76
|
+
|
|
77
|
+
def get_catalog_file_path(self) -> str:
|
|
78
|
+
user_catalog_path = os.path.join(os.path.expanduser("~"), "celestial_catalog.csv")
|
|
79
|
+
if not os.path.exists(user_catalog_path):
|
|
80
|
+
bundled = os.path.join(_app_root(), "data", "catalogs", "celestial_catalog.csv")
|
|
81
|
+
if os.path.exists(bundled):
|
|
82
|
+
try: shutil.copyfile(bundled, user_catalog_path)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
import logging
|
|
85
|
+
logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
|
|
86
|
+
return user_catalog_path
|
|
87
|
+
|
|
88
|
+
def run(self):
|
|
89
|
+
try:
|
|
90
|
+
# local date/time → astropy Time
|
|
91
|
+
local_tz = pytz.timezone(self.timezone)
|
|
92
|
+
naive = datetime.strptime(f"{self.date} {self.time}", "%Y-%m-%d %H:%M")
|
|
93
|
+
local_dt = local_tz.localize(naive)
|
|
94
|
+
t = Time(local_dt)
|
|
95
|
+
|
|
96
|
+
# observer + LST
|
|
97
|
+
loc = EarthLocation(lat=self.latitude * u.deg, lon=self.longitude * u.deg, height=0 * u.m)
|
|
98
|
+
lst = t.sidereal_time("apparent", self.longitude * u.deg)
|
|
99
|
+
self.lst_calculated.emit(f"Local Sidereal Time: {lst.to_string(unit=u.hour, precision=3)}")
|
|
100
|
+
|
|
101
|
+
# moon phase + icon
|
|
102
|
+
phase_pct, phase_icon = self.calculate_lunar_phase(t, loc)
|
|
103
|
+
self.lunar_phase_calculated.emit(phase_pct, phase_icon)
|
|
104
|
+
|
|
105
|
+
# load catalog
|
|
106
|
+
catalog_file = self.catalog_file
|
|
107
|
+
if not os.path.exists(catalog_file):
|
|
108
|
+
self.calculation_complete.emit(pd.DataFrame(), "Catalog file not found.")
|
|
109
|
+
return
|
|
110
|
+
df = pd.read_csv(catalog_file, encoding="ISO-8859-1")
|
|
111
|
+
|
|
112
|
+
if self.catalog_filters:
|
|
113
|
+
df = df[df["Catalog"].isin(self.catalog_filters)]
|
|
114
|
+
df.dropna(subset=["RA", "Dec"], inplace=True)
|
|
115
|
+
df.reset_index(drop=True, inplace=True)
|
|
116
|
+
|
|
117
|
+
# coordinates → AltAz
|
|
118
|
+
sky = SkyCoord(ra=df["RA"].to_numpy() * u.deg, dec=df["Dec"].to_numpy() * u.deg, frame="icrs")
|
|
119
|
+
altaz_frame = AltAz(obstime=t, location=loc)
|
|
120
|
+
altaz = sky.transform_to(altaz_frame)
|
|
121
|
+
df["Altitude"] = np.round(altaz.alt.deg, 1)
|
|
122
|
+
df["Azimuth"] = np.round(altaz.az.deg, 1)
|
|
123
|
+
|
|
124
|
+
# separation from Moon
|
|
125
|
+
moon_altaz = get_body("moon", t, loc).transform_to(altaz_frame)
|
|
126
|
+
df["Degrees from Moon"] = np.round(altaz.separation(moon_altaz).deg, 2)
|
|
127
|
+
|
|
128
|
+
# altitude gate
|
|
129
|
+
df = df[df["Altitude"] >= self.min_altitude]
|
|
130
|
+
|
|
131
|
+
# minutes to transit
|
|
132
|
+
ra_hours = df["RA"].to_numpy() * (24.0 / 360.0)
|
|
133
|
+
minutes = ((ra_hours - lst.hour) * u.hour) % (24 * u.hour)
|
|
134
|
+
mins = minutes.to_value(u.hour) * 60.0
|
|
135
|
+
df["Minutes to Transit"] = np.round(mins, 1)
|
|
136
|
+
df["Before/After Transit"] = np.where(df["Minutes to Transit"] > 720, "After", "Before")
|
|
137
|
+
df["Minutes to Transit"] = np.where(df["Minutes to Transit"] > 720,
|
|
138
|
+
1440 - df["Minutes to Transit"],
|
|
139
|
+
df["Minutes to Transit"])
|
|
140
|
+
|
|
141
|
+
# pick N nearest
|
|
142
|
+
df = df.nsmallest(self.object_limit, "Minutes to Transit")
|
|
143
|
+
self.calculation_complete.emit(df, "Calculation complete.")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.calculation_complete.emit(pd.DataFrame(), f"Error: {e!s}")
|
|
146
|
+
|
|
147
|
+
def calculate_lunar_phase(self, t: Time, loc: EarthLocation):
|
|
148
|
+
moon = get_body("moon", t, loc)
|
|
149
|
+
sun = get_sun(t)
|
|
150
|
+
elong = moon.separation(sun).deg
|
|
151
|
+
|
|
152
|
+
phase_pct = int(round((1 - np.cos(np.radians(elong))) / 2 * 100))
|
|
153
|
+
|
|
154
|
+
future = t + (6 * u.hour)
|
|
155
|
+
is_waxing = get_body("moon", future, loc).separation(get_sun(future)).deg > elong
|
|
156
|
+
|
|
157
|
+
name = "new_moon.png"
|
|
158
|
+
if 0 <= elong < 9: name = "new_moon.png"
|
|
159
|
+
elif 9 <= elong < 18: name = "waxing_crescent_1.png" if is_waxing else "waning_crescent_5.png"
|
|
160
|
+
elif 18 <= elong < 27: name = "waxing_crescent_2.png" if is_waxing else "waning_crescent_4.png"
|
|
161
|
+
elif 27 <= elong < 36: name = "waxing_crescent_3.png" if is_waxing else "waning_crescent_3.png"
|
|
162
|
+
elif 36 <= elong < 45: name = "waxing_crescent_4.png" if is_waxing else "waning_crescent_2.png"
|
|
163
|
+
elif 45 <= elong < 54: name = "waxing_crescent_5.png" if is_waxing else "waning_crescent_1.png"
|
|
164
|
+
elif 54 <= elong < 90: name = "first_quarter.png"
|
|
165
|
+
elif 90 <= elong < 108: name = "waxing_gibbous_1.png" if is_waxing else "waning_gibbous_4.png"
|
|
166
|
+
elif 108 <= elong < 126: name = "waxing_gibbous_2.png" if is_waxing else "waning_gibbous_3.png"
|
|
167
|
+
elif 126 <= elong < 144: name = "waxing_gibbous_3.png" if is_waxing else "waning_gibbous_2.png"
|
|
168
|
+
elif 144 <= elong < 162: name = "waxing_gibbous_4.png" if is_waxing else "waning_gibbous_1.png"
|
|
169
|
+
elif 162 <= elong <= 180: name = "full_moon.png"
|
|
170
|
+
|
|
171
|
+
return phase_pct, name
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------
|
|
175
|
+
# UI dialog
|
|
176
|
+
# ---------------------------------------------------
|
|
177
|
+
class SortableTreeWidgetItem(QTreeWidgetItem):
|
|
178
|
+
def __lt__(self, other):
|
|
179
|
+
col = self.treeWidget().sortColumn()
|
|
180
|
+
numeric_cols = [3, 4, 5, 7, 10] # Alt, Az, Minutes, Sep, Mag
|
|
181
|
+
if col in numeric_cols:
|
|
182
|
+
try:
|
|
183
|
+
return float(self.text(col)) < float(other.text(col))
|
|
184
|
+
except ValueError:
|
|
185
|
+
return self.text(col) < other.text(col)
|
|
186
|
+
return self.text(col) < other.text(col)
|
|
187
|
+
|
|
188
|
+
# ---------- coordinate parsing / formatting ----------
|
|
189
|
+
def _parse_deg_with_suffix(txt: str, kind: str) -> float:
|
|
190
|
+
"""
|
|
191
|
+
Parse latitude/longitude accepting:
|
|
192
|
+
30.1, -111, "30.1N", "111W", " -30.0 s ", etc.
|
|
193
|
+
kind: "lat" or "lon" (for range checks and suffix semantics)
|
|
194
|
+
Returns signed decimal degrees (E+, W-, N+, S-).
|
|
195
|
+
Raises ValueError on bad input.
|
|
196
|
+
"""
|
|
197
|
+
if txt is None:
|
|
198
|
+
raise ValueError("empty")
|
|
199
|
+
t = str(txt).strip().replace("°", "")
|
|
200
|
+
if not t:
|
|
201
|
+
raise ValueError("empty")
|
|
202
|
+
|
|
203
|
+
# extract trailing letter (N/S/E/W), case-insensitive
|
|
204
|
+
suffix = ""
|
|
205
|
+
if t and t[-1].upper() in ("N", "S", "E", "W"):
|
|
206
|
+
suffix = t[-1].upper()
|
|
207
|
+
t = t[:-1].strip()
|
|
208
|
+
|
|
209
|
+
val = float(t) # may be signed already
|
|
210
|
+
|
|
211
|
+
# apply suffix to sign if present
|
|
212
|
+
if suffix:
|
|
213
|
+
if kind == "lat":
|
|
214
|
+
if suffix == "N":
|
|
215
|
+
val = abs(val)
|
|
216
|
+
elif suffix == "S":
|
|
217
|
+
val = -abs(val)
|
|
218
|
+
else:
|
|
219
|
+
raise ValueError("Latitude suffix must be N or S")
|
|
220
|
+
elif kind == "lon":
|
|
221
|
+
if suffix == "E":
|
|
222
|
+
val = abs(val) # E is positive
|
|
223
|
+
elif suffix == "W":
|
|
224
|
+
val = -abs(val) # W is negative
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError("Longitude suffix must be E or W")
|
|
227
|
+
|
|
228
|
+
# clamp / validate ranges
|
|
229
|
+
if kind == "lat":
|
|
230
|
+
if not (-90.0 <= val <= 90.0):
|
|
231
|
+
raise ValueError("Latitude must be in [-90, 90]")
|
|
232
|
+
else:
|
|
233
|
+
if not (-180.0 <= val <= 180.0):
|
|
234
|
+
raise ValueError("Longitude must be in [-180, 180]")
|
|
235
|
+
|
|
236
|
+
return val
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _format_with_suffix(val: float, kind: str) -> str:
|
|
240
|
+
"""
|
|
241
|
+
Render signed degrees with hemisphere suffix.
|
|
242
|
+
e.g. lat -33.5 -> '33.5S'
|
|
243
|
+
lon -111 -> '111W'
|
|
244
|
+
"""
|
|
245
|
+
v = float(val)
|
|
246
|
+
if kind == "lat":
|
|
247
|
+
hemi = "N" if v >= 0 else "S"
|
|
248
|
+
else:
|
|
249
|
+
hemi = "E" if v >= 0 else "W"
|
|
250
|
+
return f"{abs(v):g}{hemi}"
|
|
251
|
+
|
|
252
|
+
def _tz_vs_longitude_hint(tz_name: str, date_str: str, time_str: str, lon_deg: float):
|
|
253
|
+
"""
|
|
254
|
+
Compare timezone UTC offset to longitude.
|
|
255
|
+
Heuristic:
|
|
256
|
+
• sign check: West longitudes (~W) usually have negative UTC offsets; East longitudes (~E) positive
|
|
257
|
+
• central meridian check: |lon| should be near |offset_hours*15|; flag if > 45°
|
|
258
|
+
Returns (should_warn: bool, human_msg: str, utc_str: str, central_meridian: float)
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
local_tz = pytz.timezone(tz_name)
|
|
262
|
+
naive = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
|
|
263
|
+
local_dt = local_tz.localize(naive)
|
|
264
|
+
off_hours = (local_dt.utcoffset() or pd.Timedelta(0)).total_seconds() / 3600.0
|
|
265
|
+
except Exception:
|
|
266
|
+
return (False, "", "", 0.0)
|
|
267
|
+
|
|
268
|
+
# UTC string like UTC−7 or UTC+5:30
|
|
269
|
+
hours = int(off_hours)
|
|
270
|
+
mins = int(round(abs(off_hours - hours) * 60))
|
|
271
|
+
sign = "−" if off_hours < 0 else "+"
|
|
272
|
+
if mins:
|
|
273
|
+
utc_str = f"UTC{sign}{abs(hours)}:{mins:02d}"
|
|
274
|
+
else:
|
|
275
|
+
utc_str = f"UTC{sign}{abs(hours)}"
|
|
276
|
+
|
|
277
|
+
central = off_hours * 15.0 # “central meridian” for that offset
|
|
278
|
+
sign_ok = (abs(off_hours) < 1e-9) or (lon_deg == 0) or ((lon_deg > 0) == (off_hours > 0))
|
|
279
|
+
far = abs(abs(lon_deg) - abs(central)) > 45.0
|
|
280
|
+
|
|
281
|
+
if (not sign_ok) or far:
|
|
282
|
+
msg = (f"Timezone {tz_name} ({utc_str}) looks inconsistent with longitude "
|
|
283
|
+
f"{abs(lon_deg):g}{'E' if lon_deg>0 else 'W'} "
|
|
284
|
+
f"(central meridian ≈ {abs(central):.0f}°{'E' if central>0 else 'W'}).")
|
|
285
|
+
return (True, msg, utc_str, central)
|
|
286
|
+
return (False, "", utc_str, central)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class WhatsInMySkyDialog(QDialog):
|
|
290
|
+
def __init__(self, parent=None, wims_path: Optional[str] = None, wrench_path: Optional[str] = None):
|
|
291
|
+
super().__init__(parent)
|
|
292
|
+
self.setWindowTitle("What's In My Sky")
|
|
293
|
+
if wims_path:
|
|
294
|
+
self.setWindowIcon(QIcon(wims_path))
|
|
295
|
+
|
|
296
|
+
self.settings = QSettings()
|
|
297
|
+
self.object_limit = int(self.settings.value("object_limit", 100, int))
|
|
298
|
+
|
|
299
|
+
self._build_ui(wrench_path)
|
|
300
|
+
self._load_settings_into_ui()
|
|
301
|
+
|
|
302
|
+
self.calc_thread: Optional[CalculationThread] = None
|
|
303
|
+
self.catalog_file: Optional[str] = None
|
|
304
|
+
|
|
305
|
+
# ---------- UI ----------
|
|
306
|
+
def _build_ui(self, wrench_path: Optional[str]):
|
|
307
|
+
layout = QGridLayout(self)
|
|
308
|
+
fixed_w = 150
|
|
309
|
+
|
|
310
|
+
self.latitude_entry = QLineEdit(); self.latitude_entry.setFixedWidth(fixed_w)
|
|
311
|
+
self.longitude_entry = QLineEdit(); self.longitude_entry.setFixedWidth(fixed_w)
|
|
312
|
+
self.date_entry = QLineEdit(); self.date_entry.setFixedWidth(fixed_w)
|
|
313
|
+
self.time_entry = QLineEdit(); self.time_entry.setFixedWidth(fixed_w)
|
|
314
|
+
|
|
315
|
+
self.timezone_combo = QComboBox(); self.timezone_combo.addItems(pytz.all_timezones)
|
|
316
|
+
self.timezone_combo.setFixedWidth(fixed_w)
|
|
317
|
+
|
|
318
|
+
r = 0
|
|
319
|
+
layout.addWidget(QLabel("Latitude:"), r, 0); layout.addWidget(self.latitude_entry, r, 1); r += 1
|
|
320
|
+
layout.addWidget(QLabel("Longitude (E+, W−):"), r, 0); layout.addWidget(self.longitude_entry, r, 1); r += 1
|
|
321
|
+
layout.addWidget(QLabel("Date (YYYY-MM-DD):"), r, 0); layout.addWidget(self.date_entry, r, 1); r += 1
|
|
322
|
+
layout.addWidget(QLabel("Time (HH:MM):"), r, 0); layout.addWidget(self.time_entry, r, 1); r += 1
|
|
323
|
+
layout.addWidget(QLabel("Time Zone:"), r, 0); layout.addWidget(self.timezone_combo, r, 1); r += 1
|
|
324
|
+
|
|
325
|
+
self.min_altitude_entry = QLineEdit(); self.min_altitude_entry.setFixedWidth(fixed_w)
|
|
326
|
+
layout.addWidget(QLabel("Min Altitude (0–90°):"), r, 0); layout.addWidget(self.min_altitude_entry, r, 1); r += 1
|
|
327
|
+
|
|
328
|
+
# catalogs
|
|
329
|
+
catalog_frame = QScrollArea()
|
|
330
|
+
cat_widget = QWidget(); cat_layout = QGridLayout(cat_widget)
|
|
331
|
+
self.catalog_vars: dict[str, QCheckBox] = {}
|
|
332
|
+
for i, name in enumerate(["Messier","NGC","IC","Caldwell","Abell","Sharpless","LBN","LDN","PNG","User"]):
|
|
333
|
+
cb = QCheckBox(name); cb.setChecked(False)
|
|
334
|
+
cat_layout.addWidget(cb, i // 5, i % 5)
|
|
335
|
+
self.catalog_vars[name] = cb
|
|
336
|
+
catalog_frame.setWidget(cat_widget); catalog_frame.setFixedWidth(fixed_w + 250)
|
|
337
|
+
layout.addWidget(QLabel("Catalog Filters:"), r, 0); layout.addWidget(catalog_frame, r, 1); r += 1
|
|
338
|
+
|
|
339
|
+
# RA/Dec format
|
|
340
|
+
self.ra_dec_degrees = QRadioButton("Degrees")
|
|
341
|
+
self.ra_dec_hms = QRadioButton("H:M:S / D:M:S")
|
|
342
|
+
self.ra_dec_degrees.setChecked(True)
|
|
343
|
+
g = QButtonGroup(self); g.addButton(self.ra_dec_degrees); g.addButton(self.ra_dec_hms)
|
|
344
|
+
ra_row = QHBoxLayout(); ra_row.addWidget(self.ra_dec_degrees); ra_row.addWidget(self.ra_dec_hms)
|
|
345
|
+
layout.addWidget(QLabel("RA/Dec Format:"), r, 0); layout.addLayout(ra_row, r, 1); r += 1
|
|
346
|
+
self.ra_dec_degrees.toggled.connect(self.update_ra_dec_format)
|
|
347
|
+
self.ra_dec_hms.toggled.connect(self.update_ra_dec_format)
|
|
348
|
+
|
|
349
|
+
# action buttons / status
|
|
350
|
+
calc_btn = QPushButton("Calculate"); calc_btn.setFixedWidth(fixed_w); calc_btn.clicked.connect(self.start_calculation)
|
|
351
|
+
layout.addWidget(calc_btn, r, 0); r += 1
|
|
352
|
+
|
|
353
|
+
self.status_label = QLabel("Status: Idle"); layout.addWidget(self.status_label, r, 0, 1, 2); r += 1
|
|
354
|
+
self.lst_label = QLabel("Local Sidereal Time: 0.000"); layout.addWidget(self.lst_label, r, 0, 1, 2); r += 1
|
|
355
|
+
|
|
356
|
+
# moon phase preview
|
|
357
|
+
self.lunar_phase_image_label = QLabel()
|
|
358
|
+
layout.addWidget(self.lunar_phase_image_label, 0, 2, 4, 1)
|
|
359
|
+
self.lunar_phase_label = QLabel("Lunar Phase: N/A")
|
|
360
|
+
layout.addWidget(self.lunar_phase_label, 4, 2)
|
|
361
|
+
|
|
362
|
+
# results tree
|
|
363
|
+
self.tree = QTreeWidget()
|
|
364
|
+
self.tree.setHeaderLabels([
|
|
365
|
+
"Name","RA","Dec","Altitude","Azimuth","Minutes to Transit","Before/After Transit",
|
|
366
|
+
"Degrees from Moon","Alt Name","Type","Magnitude","Size (arcmin)"
|
|
367
|
+
])
|
|
368
|
+
self.tree.setSortingEnabled(True)
|
|
369
|
+
hdr = self.tree.header()
|
|
370
|
+
hdr.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
371
|
+
hdr.setStretchLastSection(False)
|
|
372
|
+
self.tree.sortByColumn(5, Qt.SortOrder.AscendingOrder)
|
|
373
|
+
self.tree.itemDoubleClicked.connect(self.on_row_double_click)
|
|
374
|
+
layout.addWidget(self.tree, r, 0, 1, 3); r += 1
|
|
375
|
+
|
|
376
|
+
# bottom row
|
|
377
|
+
add_btn = QPushButton("Add Custom Object"); add_btn.setFixedWidth(fixed_w); add_btn.clicked.connect(self.add_custom_object)
|
|
378
|
+
layout.addWidget(add_btn, r, 0)
|
|
379
|
+
|
|
380
|
+
save_btn = QPushButton("Save to CSV"); save_btn.setFixedWidth(fixed_w); save_btn.clicked.connect(self.save_to_csv)
|
|
381
|
+
layout.addWidget(save_btn, r, 1)
|
|
382
|
+
|
|
383
|
+
settings_btn = QPushButton(); settings_btn.setFixedWidth(fixed_w)
|
|
384
|
+
if wrench_path and os.path.exists(wrench_path):
|
|
385
|
+
settings_btn.setIcon(QIcon(wrench_path))
|
|
386
|
+
settings_btn.clicked.connect(self.open_settings)
|
|
387
|
+
layout.addWidget(settings_btn, r, 2)
|
|
388
|
+
|
|
389
|
+
layout.setColumnStretch(2, 1)
|
|
390
|
+
|
|
391
|
+
# ---------- settings ----------
|
|
392
|
+
def _load_settings_into_ui(self):
|
|
393
|
+
def cast(v, typ, default):
|
|
394
|
+
try: return typ(v)
|
|
395
|
+
except Exception: return default
|
|
396
|
+
lat = cast(self.settings.value("latitude", 0.0), float, 0.0)
|
|
397
|
+
lon = cast(self.settings.value("longitude", 0.0), float, 0.0)
|
|
398
|
+
date = self.settings.value("date", datetime.now().strftime("%Y-%m-%d"))
|
|
399
|
+
time = self.settings.value("time", "00:00")
|
|
400
|
+
tz = self.settings.value("timezone", "UTC")
|
|
401
|
+
min_alt = cast(self.settings.value("min_altitude", 0.0), float, 0.0)
|
|
402
|
+
self.object_limit = cast(self.settings.value("object_limit", 100), int, 100)
|
|
403
|
+
|
|
404
|
+
self.latitude_entry.setText(str(lat))
|
|
405
|
+
self.longitude_entry.setText(str(lon))
|
|
406
|
+
self.date_entry.setText(date)
|
|
407
|
+
self.time_entry.setText(time)
|
|
408
|
+
self.timezone_combo.setCurrentText(tz)
|
|
409
|
+
self.min_altitude_entry.setText(str(min_alt))
|
|
410
|
+
|
|
411
|
+
def _save_settings(self, latitude, longitude, date, time, timezone, min_altitude):
|
|
412
|
+
self.settings.setValue("latitude", latitude)
|
|
413
|
+
self.settings.setValue("longitude", longitude)
|
|
414
|
+
self.settings.setValue("date", date)
|
|
415
|
+
self.settings.setValue("time", time)
|
|
416
|
+
self.settings.setValue("timezone", timezone)
|
|
417
|
+
self.settings.setValue("min_altitude", min_altitude)
|
|
418
|
+
|
|
419
|
+
# ---------- actions ----------
|
|
420
|
+
def start_calculation(self):
|
|
421
|
+
try:
|
|
422
|
+
orig_lat_txt = self.latitude_entry.text()
|
|
423
|
+
orig_lon_txt = self.longitude_entry.text()
|
|
424
|
+
|
|
425
|
+
latitude = _parse_deg_with_suffix(orig_lat_txt, "lat")
|
|
426
|
+
longitude = _parse_deg_with_suffix(orig_lon_txt, "lon")
|
|
427
|
+
|
|
428
|
+
# Pretty-print back with suffixes
|
|
429
|
+
self.latitude_entry.setText(_format_with_suffix(latitude, "lat"))
|
|
430
|
+
self.longitude_entry.setText(_format_with_suffix(longitude, "lon"))
|
|
431
|
+
|
|
432
|
+
date_str = self.date_entry.text().strip()
|
|
433
|
+
time_str = self.time_entry.text().strip()
|
|
434
|
+
tz_str = self.timezone_combo.currentText()
|
|
435
|
+
min_alt = float(self.min_altitude_entry.text())
|
|
436
|
+
except ValueError as e:
|
|
437
|
+
self.update_status(f"Invalid input: {e}")
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Heuristic warning (and gentle auto-fix if user probably forgot the suffix)
|
|
441
|
+
warn, msg, utc_str, central = _tz_vs_longitude_hint(tz_str, date_str, time_str, longitude)
|
|
442
|
+
if warn:
|
|
443
|
+
# If the user typed a bare number (no N/S/E/W) and sign mismatches TZ, suggest flip
|
|
444
|
+
bare_lon = (orig_lon_txt.strip() and orig_lon_txt.strip()[-1].upper() not in ("E","W"))
|
|
445
|
+
sign_mismatch = not ((longitude > 0) == (central > 0) or abs(central) < 1e-6 or longitude == 0)
|
|
446
|
+
|
|
447
|
+
if bare_lon and sign_mismatch:
|
|
448
|
+
# Flip once, write back, and tell the user.
|
|
449
|
+
longitude = -longitude
|
|
450
|
+
self.longitude_entry.setText(_format_with_suffix(longitude, "lon"))
|
|
451
|
+
self.update_status(f"{msg} → Assuming you meant {_format_with_suffix(longitude, 'lon')} (auto-corrected).")
|
|
452
|
+
else:
|
|
453
|
+
self.update_status(msg + " Please verify your longitude/timezone.")
|
|
454
|
+
else:
|
|
455
|
+
self.update_status("Inputs look consistent.")
|
|
456
|
+
|
|
457
|
+
# Persist settings (numeric)
|
|
458
|
+
self._save_settings(latitude, longitude, date_str, time_str, tz_str, min_alt)
|
|
459
|
+
|
|
460
|
+
catalogs = [name for name, cb in self.catalog_vars.items() if cb.isChecked()]
|
|
461
|
+
self.calc_thread = CalculationThread(latitude, longitude, date_str, time_str, tz_str,
|
|
462
|
+
min_alt, catalogs, self.object_limit)
|
|
463
|
+
self.catalog_file = self.calc_thread.catalog_file
|
|
464
|
+
|
|
465
|
+
self.calc_thread.calculation_complete.connect(self.on_calculation_complete)
|
|
466
|
+
self.calc_thread.lunar_phase_calculated.connect(self.update_lunar_phase)
|
|
467
|
+
self.calc_thread.lst_calculated.connect(self.update_lst)
|
|
468
|
+
self.calc_thread.status_update.connect(self.update_status)
|
|
469
|
+
|
|
470
|
+
self.update_status("Calculating…")
|
|
471
|
+
self.calc_thread.start()
|
|
472
|
+
|
|
473
|
+
def update_lunar_phase(self, phase_percentage: int, phase_image_name: str):
|
|
474
|
+
self.lunar_phase_label.setText(f"Lunar Phase: {phase_percentage}% illuminated")
|
|
475
|
+
pth = imgs_path(phase_image_name)
|
|
476
|
+
if os.path.exists(pth):
|
|
477
|
+
pm = QPixmap(pth).scaled(100, 100, Qt.AspectRatioMode.KeepAspectRatio,
|
|
478
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
479
|
+
self.lunar_phase_image_label.setPixmap(pm)
|
|
480
|
+
|
|
481
|
+
def on_calculation_complete(self, df: pd.DataFrame, message: str):
|
|
482
|
+
self.update_status(message)
|
|
483
|
+
self.tree.clear()
|
|
484
|
+
if df.empty:
|
|
485
|
+
return
|
|
486
|
+
for _, row in df.iterrows():
|
|
487
|
+
ra_disp, dec_disp = row["RA"], row["Dec"]
|
|
488
|
+
if self.ra_dec_hms.isChecked():
|
|
489
|
+
sc = SkyCoord(ra=row["RA"] * u.deg, dec=row["Dec"] * u.deg)
|
|
490
|
+
ra_disp = sc.ra.to_string(unit=u.hour, sep=":")
|
|
491
|
+
dec_disp = sc.dec.to_string(unit=u.deg, sep=":")
|
|
492
|
+
size_arcmin = row.get("Info", "")
|
|
493
|
+
if pd.notna(size_arcmin):
|
|
494
|
+
size_arcmin = str(size_arcmin)
|
|
495
|
+
vals = [
|
|
496
|
+
str(row.get("Name","") or ""),
|
|
497
|
+
str(ra_disp),
|
|
498
|
+
str(dec_disp),
|
|
499
|
+
str(row.get("Altitude","")),
|
|
500
|
+
str(row.get("Azimuth","")),
|
|
501
|
+
str(int(row.get("Minutes to Transit",0))) if pd.notna(row.get("Minutes to Transit", np.nan)) else "",
|
|
502
|
+
str(row.get("Before/After Transit","")),
|
|
503
|
+
str(round(row.get("Degrees from Moon", 0.0), 2)) if pd.notna(row.get("Degrees from Moon", np.nan)) else "",
|
|
504
|
+
row.get("Alt Name","") if pd.notna(row.get("Alt Name","")) else "",
|
|
505
|
+
row.get("Type","") if pd.notna(row.get("Type","")) else "",
|
|
506
|
+
str(row.get("Magnitude","")) if pd.notna(row.get("Magnitude","")) else "",
|
|
507
|
+
str(size_arcmin) if pd.notna(size_arcmin) else "",
|
|
508
|
+
]
|
|
509
|
+
self.tree.addTopLevelItem(SortableTreeWidgetItem(vals))
|
|
510
|
+
|
|
511
|
+
def update_status(self, msg: str):
|
|
512
|
+
self.status_label.setText(f"Status: {msg}")
|
|
513
|
+
|
|
514
|
+
def update_lst(self, msg: str):
|
|
515
|
+
self.lst_label.setText(msg)
|
|
516
|
+
|
|
517
|
+
def open_settings(self):
|
|
518
|
+
n, ok = QInputDialog.getInt(self, "Settings", "Enter number of objects to display:",
|
|
519
|
+
value=int(self.object_limit), min=1, max=1000)
|
|
520
|
+
if ok:
|
|
521
|
+
self.object_limit = int(n)
|
|
522
|
+
self.settings.setValue("object_limit", int(n))
|
|
523
|
+
|
|
524
|
+
def on_row_double_click(self, item: QTreeWidgetItem, column: int):
|
|
525
|
+
name = item.text(0).replace(" ", "")
|
|
526
|
+
webbrowser.open(f"https://www.astrobin.com/search/?q={name}")
|
|
527
|
+
|
|
528
|
+
def add_custom_object(self):
|
|
529
|
+
name, ok = QInputDialog.getText(self, "Add Custom Object", "Enter object name:")
|
|
530
|
+
if not ok or not name:
|
|
531
|
+
return
|
|
532
|
+
ra, ok = QInputDialog.getDouble(self, "Add Custom Object", "Enter RA (deg):", decimals=3)
|
|
533
|
+
if not ok: return
|
|
534
|
+
dec, ok = QInputDialog.getDouble(self, "Add Custom Object", "Enter Dec (deg):", decimals=3)
|
|
535
|
+
if not ok: return
|
|
536
|
+
|
|
537
|
+
entry = {"Name": name, "RA": ra, "Dec": dec, "Catalog": "User",
|
|
538
|
+
"Alt Name": "User Defined", "Type": "Custom", "Magnitude": "", "Info": ""}
|
|
539
|
+
|
|
540
|
+
catalog_csv = self.catalog_file or os.path.join(os.path.expanduser("~"), "celestial_catalog.csv")
|
|
541
|
+
try:
|
|
542
|
+
df = pd.read_csv(catalog_csv, encoding="ISO-8859-1") if os.path.exists(catalog_csv) else pd.DataFrame()
|
|
543
|
+
df = pd.concat([df, pd.DataFrame([entry])], ignore_index=True)
|
|
544
|
+
df.to_csv(catalog_csv, index=False, encoding="ISO-8859-1")
|
|
545
|
+
self.update_status(f"Added custom object: {name}")
|
|
546
|
+
except Exception as e:
|
|
547
|
+
QMessageBox.warning(self, "Add Custom Object", f"Could not update catalog:\n{e}")
|
|
548
|
+
|
|
549
|
+
def update_ra_dec_format(self):
|
|
550
|
+
use_deg = self.ra_dec_degrees.isChecked()
|
|
551
|
+
for i in range(self.tree.topLevelItemCount()):
|
|
552
|
+
it = self.tree.topLevelItem(i)
|
|
553
|
+
ra_txt, dec_txt = it.text(1), it.text(2)
|
|
554
|
+
try:
|
|
555
|
+
if use_deg:
|
|
556
|
+
if ":" in ra_txt:
|
|
557
|
+
sc = SkyCoord(ra=ra_txt, dec=dec_txt, unit=(u.hourangle, u.deg))
|
|
558
|
+
it.setText(1, f"{sc.ra.deg:.3f}")
|
|
559
|
+
it.setText(2, f"{sc.dec.deg:.3f}")
|
|
560
|
+
else:
|
|
561
|
+
if ":" not in ra_txt:
|
|
562
|
+
sc = SkyCoord(ra=float(ra_txt) * u.deg, dec=float(dec_txt) * u.deg)
|
|
563
|
+
it.setText(1, sc.ra.to_string(unit=u.hour, sep=":"))
|
|
564
|
+
it.setText(2, sc.dec.to_string(unit=u.deg, sep=":"))
|
|
565
|
+
except Exception:
|
|
566
|
+
pass
|
|
567
|
+
|
|
568
|
+
def save_to_csv(self):
|
|
569
|
+
path, _ = QFileDialog.getSaveFileName(self, "Save CSV File", "", "CSV files (*.csv);;All Files (*)")
|
|
570
|
+
if not path:
|
|
571
|
+
return
|
|
572
|
+
cols = [self.tree.headerItem().text(i) for i in range(self.tree.columnCount())]
|
|
573
|
+
rows = []
|
|
574
|
+
for i in range(self.tree.topLevelItemCount()):
|
|
575
|
+
it = self.tree.topLevelItem(i)
|
|
576
|
+
rows.append([it.text(j) for j in range(self.tree.columnCount())])
|
|
577
|
+
pd.DataFrame(rows, columns=cols).to_csv(path, index=False)
|
|
578
|
+
self.update_status(f"Data saved to {path}")
|