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