setiastrosuitepro 1.6.5.post3__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 (368) 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/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotatearbitrary.png +0 -0
  107. setiastro/images/rotateclockwise.png +0 -0
  108. setiastro/images/rotatecounterclockwise.png +0 -0
  109. setiastro/images/satellite.png +0 -0
  110. setiastro/images/script.png +0 -0
  111. setiastro/images/selectivecolor.png +0 -0
  112. setiastro/images/simbad.png +0 -0
  113. setiastro/images/slot0.png +0 -0
  114. setiastro/images/slot1.png +0 -0
  115. setiastro/images/slot2.png +0 -0
  116. setiastro/images/slot3.png +0 -0
  117. setiastro/images/slot4.png +0 -0
  118. setiastro/images/slot5.png +0 -0
  119. setiastro/images/slot6.png +0 -0
  120. setiastro/images/slot7.png +0 -0
  121. setiastro/images/slot8.png +0 -0
  122. setiastro/images/slot9.png +0 -0
  123. setiastro/images/spcc.png +0 -0
  124. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  125. setiastro/images/spinner.gif +0 -0
  126. setiastro/images/stacking.png +0 -0
  127. setiastro/images/staradd.png +0 -0
  128. setiastro/images/staralign.png +0 -0
  129. setiastro/images/starnet.png +0 -0
  130. setiastro/images/starregistration.png +0 -0
  131. setiastro/images/starspike.png +0 -0
  132. setiastro/images/starstretch.png +0 -0
  133. setiastro/images/statstretch.png +0 -0
  134. setiastro/images/supernova.png +0 -0
  135. setiastro/images/uhs.png +0 -0
  136. setiastro/images/undoicon.png +0 -0
  137. setiastro/images/upscale.png +0 -0
  138. setiastro/images/viewbundle.png +0 -0
  139. setiastro/images/whitebalance.png +0 -0
  140. setiastro/images/wimi_icon_256x256.png +0 -0
  141. setiastro/images/wimilogo.png +0 -0
  142. setiastro/images/wims.png +0 -0
  143. setiastro/images/wrench_icon.png +0 -0
  144. setiastro/images/xisfliberator.png +0 -0
  145. setiastro/qml/ResourceMonitor.qml +126 -0
  146. setiastro/saspro/__init__.py +20 -0
  147. setiastro/saspro/__main__.py +958 -0
  148. setiastro/saspro/_generated/__init__.py +7 -0
  149. setiastro/saspro/_generated/build_info.py +3 -0
  150. setiastro/saspro/abe.py +1346 -0
  151. setiastro/saspro/abe_preset.py +196 -0
  152. setiastro/saspro/aberration_ai.py +698 -0
  153. setiastro/saspro/aberration_ai_preset.py +224 -0
  154. setiastro/saspro/accel_installer.py +218 -0
  155. setiastro/saspro/accel_workers.py +30 -0
  156. setiastro/saspro/add_stars.py +624 -0
  157. setiastro/saspro/astrobin_exporter.py +1010 -0
  158. setiastro/saspro/astrospike.py +153 -0
  159. setiastro/saspro/astrospike_python.py +1841 -0
  160. setiastro/saspro/autostretch.py +198 -0
  161. setiastro/saspro/backgroundneutral.py +611 -0
  162. setiastro/saspro/batch_convert.py +328 -0
  163. setiastro/saspro/batch_renamer.py +522 -0
  164. setiastro/saspro/blemish_blaster.py +491 -0
  165. setiastro/saspro/blink_comparator_pro.py +3149 -0
  166. setiastro/saspro/bundles.py +61 -0
  167. setiastro/saspro/bundles_dock.py +114 -0
  168. setiastro/saspro/cheat_sheet.py +213 -0
  169. setiastro/saspro/clahe.py +368 -0
  170. setiastro/saspro/comet_stacking.py +1442 -0
  171. setiastro/saspro/common_tr.py +107 -0
  172. setiastro/saspro/config.py +38 -0
  173. setiastro/saspro/config_bootstrap.py +40 -0
  174. setiastro/saspro/config_manager.py +316 -0
  175. setiastro/saspro/continuum_subtract.py +1617 -0
  176. setiastro/saspro/convo.py +1400 -0
  177. setiastro/saspro/convo_preset.py +414 -0
  178. setiastro/saspro/copyastro.py +190 -0
  179. setiastro/saspro/cosmicclarity.py +1589 -0
  180. setiastro/saspro/cosmicclarity_preset.py +407 -0
  181. setiastro/saspro/crop_dialog_pro.py +983 -0
  182. setiastro/saspro/crop_preset.py +189 -0
  183. setiastro/saspro/curve_editor_pro.py +2562 -0
  184. setiastro/saspro/curves_preset.py +375 -0
  185. setiastro/saspro/debayer.py +673 -0
  186. setiastro/saspro/debug_utils.py +29 -0
  187. setiastro/saspro/dnd_mime.py +35 -0
  188. setiastro/saspro/doc_manager.py +2664 -0
  189. setiastro/saspro/exoplanet_detector.py +2166 -0
  190. setiastro/saspro/file_utils.py +284 -0
  191. setiastro/saspro/fitsmodifier.py +748 -0
  192. setiastro/saspro/fix_bom.py +32 -0
  193. setiastro/saspro/free_torch_memory.py +48 -0
  194. setiastro/saspro/frequency_separation.py +1349 -0
  195. setiastro/saspro/function_bundle.py +1596 -0
  196. setiastro/saspro/generate_translations.py +3092 -0
  197. setiastro/saspro/ghs_dialog_pro.py +663 -0
  198. setiastro/saspro/ghs_preset.py +284 -0
  199. setiastro/saspro/graxpert.py +637 -0
  200. setiastro/saspro/graxpert_preset.py +287 -0
  201. setiastro/saspro/gui/__init__.py +0 -0
  202. setiastro/saspro/gui/main_window.py +8792 -0
  203. setiastro/saspro/gui/mixins/__init__.py +33 -0
  204. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  205. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  206. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  207. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  208. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  209. setiastro/saspro/gui/mixins/menu_mixin.py +390 -0
  210. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  211. setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
  212. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  213. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  214. setiastro/saspro/gui/statistics_dialog.py +47 -0
  215. setiastro/saspro/halobgon.py +488 -0
  216. setiastro/saspro/header_viewer.py +448 -0
  217. setiastro/saspro/headless_utils.py +88 -0
  218. setiastro/saspro/histogram.py +756 -0
  219. setiastro/saspro/history_explorer.py +941 -0
  220. setiastro/saspro/i18n.py +168 -0
  221. setiastro/saspro/image_combine.py +417 -0
  222. setiastro/saspro/image_peeker_pro.py +1604 -0
  223. setiastro/saspro/imageops/__init__.py +37 -0
  224. setiastro/saspro/imageops/mdi_snap.py +292 -0
  225. setiastro/saspro/imageops/scnr.py +36 -0
  226. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  227. setiastro/saspro/imageops/stretch.py +236 -0
  228. setiastro/saspro/isophote.py +1182 -0
  229. setiastro/saspro/layers.py +208 -0
  230. setiastro/saspro/layers_dock.py +714 -0
  231. setiastro/saspro/lazy_imports.py +193 -0
  232. setiastro/saspro/legacy/__init__.py +2 -0
  233. setiastro/saspro/legacy/image_manager.py +2360 -0
  234. setiastro/saspro/legacy/numba_utils.py +3676 -0
  235. setiastro/saspro/legacy/xisf.py +1213 -0
  236. setiastro/saspro/linear_fit.py +537 -0
  237. setiastro/saspro/live_stacking.py +1854 -0
  238. setiastro/saspro/log_bus.py +5 -0
  239. setiastro/saspro/logging_config.py +460 -0
  240. setiastro/saspro/luminancerecombine.py +510 -0
  241. setiastro/saspro/main_helpers.py +201 -0
  242. setiastro/saspro/mask_creation.py +1086 -0
  243. setiastro/saspro/masks_core.py +56 -0
  244. setiastro/saspro/mdi_widgets.py +353 -0
  245. setiastro/saspro/memory_utils.py +666 -0
  246. setiastro/saspro/metadata_patcher.py +75 -0
  247. setiastro/saspro/mfdeconv.py +3909 -0
  248. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  249. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  250. setiastro/saspro/mfdeconvsport.py +2459 -0
  251. setiastro/saspro/minorbodycatalog.py +567 -0
  252. setiastro/saspro/morphology.py +407 -0
  253. setiastro/saspro/multiscale_decomp.py +1747 -0
  254. setiastro/saspro/nbtorgb_stars.py +541 -0
  255. setiastro/saspro/numba_utils.py +3145 -0
  256. setiastro/saspro/numba_warmup.py +141 -0
  257. setiastro/saspro/ops/__init__.py +9 -0
  258. setiastro/saspro/ops/command_help_dialog.py +623 -0
  259. setiastro/saspro/ops/command_runner.py +217 -0
  260. setiastro/saspro/ops/commands.py +1594 -0
  261. setiastro/saspro/ops/script_editor.py +1105 -0
  262. setiastro/saspro/ops/scripts.py +1476 -0
  263. setiastro/saspro/ops/settings.py +637 -0
  264. setiastro/saspro/parallel_utils.py +554 -0
  265. setiastro/saspro/pedestal.py +121 -0
  266. setiastro/saspro/perfect_palette_picker.py +1105 -0
  267. setiastro/saspro/pipeline.py +110 -0
  268. setiastro/saspro/pixelmath.py +1604 -0
  269. setiastro/saspro/plate_solver.py +2445 -0
  270. setiastro/saspro/project_io.py +797 -0
  271. setiastro/saspro/psf_utils.py +136 -0
  272. setiastro/saspro/psf_viewer.py +549 -0
  273. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  274. setiastro/saspro/remove_green.py +331 -0
  275. setiastro/saspro/remove_stars.py +1599 -0
  276. setiastro/saspro/remove_stars_preset.py +446 -0
  277. setiastro/saspro/resources.py +503 -0
  278. setiastro/saspro/rgb_combination.py +208 -0
  279. setiastro/saspro/rgb_extract.py +19 -0
  280. setiastro/saspro/rgbalign.py +723 -0
  281. setiastro/saspro/runtime_imports.py +7 -0
  282. setiastro/saspro/runtime_torch.py +754 -0
  283. setiastro/saspro/save_options.py +73 -0
  284. setiastro/saspro/selective_color.py +1611 -0
  285. setiastro/saspro/sfcc.py +1472 -0
  286. setiastro/saspro/shortcuts.py +3116 -0
  287. setiastro/saspro/signature_insert.py +1102 -0
  288. setiastro/saspro/stacking_suite.py +19066 -0
  289. setiastro/saspro/star_alignment.py +7380 -0
  290. setiastro/saspro/star_alignment_preset.py +329 -0
  291. setiastro/saspro/star_metrics.py +49 -0
  292. setiastro/saspro/star_spikes.py +765 -0
  293. setiastro/saspro/star_stretch.py +507 -0
  294. setiastro/saspro/stat_stretch.py +538 -0
  295. setiastro/saspro/status_log_dock.py +78 -0
  296. setiastro/saspro/subwindow.py +3407 -0
  297. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  298. setiastro/saspro/swap_manager.py +134 -0
  299. setiastro/saspro/torch_backend.py +89 -0
  300. setiastro/saspro/torch_rejection.py +434 -0
  301. setiastro/saspro/translations/all_source_strings.json +4726 -0
  302. setiastro/saspro/translations/ar_translations.py +4096 -0
  303. setiastro/saspro/translations/de_translations.py +3728 -0
  304. setiastro/saspro/translations/es_translations.py +4169 -0
  305. setiastro/saspro/translations/fr_translations.py +4090 -0
  306. setiastro/saspro/translations/hi_translations.py +3803 -0
  307. setiastro/saspro/translations/integrate_translations.py +271 -0
  308. setiastro/saspro/translations/it_translations.py +4728 -0
  309. setiastro/saspro/translations/ja_translations.py +3834 -0
  310. setiastro/saspro/translations/pt_translations.py +3847 -0
  311. setiastro/saspro/translations/ru_translations.py +3082 -0
  312. setiastro/saspro/translations/saspro_ar.qm +0 -0
  313. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  314. setiastro/saspro/translations/saspro_de.qm +0 -0
  315. setiastro/saspro/translations/saspro_de.ts +14548 -0
  316. setiastro/saspro/translations/saspro_es.qm +0 -0
  317. setiastro/saspro/translations/saspro_es.ts +16202 -0
  318. setiastro/saspro/translations/saspro_fr.qm +0 -0
  319. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  320. setiastro/saspro/translations/saspro_hi.qm +0 -0
  321. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  322. setiastro/saspro/translations/saspro_it.qm +0 -0
  323. setiastro/saspro/translations/saspro_it.ts +19046 -0
  324. setiastro/saspro/translations/saspro_ja.qm +0 -0
  325. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  326. setiastro/saspro/translations/saspro_pt.qm +0 -0
  327. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  328. setiastro/saspro/translations/saspro_ru.qm +0 -0
  329. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  330. setiastro/saspro/translations/saspro_sw.qm +0 -0
  331. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  332. setiastro/saspro/translations/saspro_uk.qm +0 -0
  333. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  334. setiastro/saspro/translations/saspro_zh.qm +0 -0
  335. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  336. setiastro/saspro/translations/sw_translations.py +3897 -0
  337. setiastro/saspro/translations/uk_translations.py +3929 -0
  338. setiastro/saspro/translations/zh_translations.py +3910 -0
  339. setiastro/saspro/versioning.py +77 -0
  340. setiastro/saspro/view_bundle.py +1558 -0
  341. setiastro/saspro/wavescale_hdr.py +645 -0
  342. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  343. setiastro/saspro/wavescalede.py +680 -0
  344. setiastro/saspro/wavescalede_preset.py +230 -0
  345. setiastro/saspro/wcs_update.py +374 -0
  346. setiastro/saspro/whitebalance.py +513 -0
  347. setiastro/saspro/widgets/__init__.py +48 -0
  348. setiastro/saspro/widgets/common_utilities.py +306 -0
  349. setiastro/saspro/widgets/graphics_views.py +122 -0
  350. setiastro/saspro/widgets/image_utils.py +518 -0
  351. setiastro/saspro/widgets/minigame/game.js +991 -0
  352. setiastro/saspro/widgets/minigame/index.html +53 -0
  353. setiastro/saspro/widgets/minigame/style.css +241 -0
  354. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  355. setiastro/saspro/widgets/resource_monitor.py +263 -0
  356. setiastro/saspro/widgets/spinboxes.py +290 -0
  357. setiastro/saspro/widgets/themed_buttons.py +13 -0
  358. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  359. setiastro/saspro/wimi.py +7996 -0
  360. setiastro/saspro/wims.py +578 -0
  361. setiastro/saspro/window_shelf.py +185 -0
  362. setiastro/saspro/xisf.py +1213 -0
  363. setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
  364. setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
  365. setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
  366. setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
  367. setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
  368. setiastrosuitepro-1.6.5.post3.dist-info/licenses/license.txt +2580 -0
@@ -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(self.tr("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(self.tr("Latitude:")), r, 0); layout.addWidget(self.latitude_entry, r, 1); r += 1
320
+ layout.addWidget(QLabel(self.tr("Longitude (E+, W−):")), r, 0); layout.addWidget(self.longitude_entry, r, 1); r += 1
321
+ layout.addWidget(QLabel(self.tr("Date (YYYY-MM-DD):")), r, 0); layout.addWidget(self.date_entry, r, 1); r += 1
322
+ layout.addWidget(QLabel(self.tr("Time (HH:MM):")), r, 0); layout.addWidget(self.time_entry, r, 1); r += 1
323
+ layout.addWidget(QLabel(self.tr("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(self.tr("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(self.tr("Catalog Filters:")), r, 0); layout.addWidget(catalog_frame, r, 1); r += 1
338
+
339
+ # RA/Dec format
340
+ self.ra_dec_degrees = QRadioButton(self.tr("Degrees"))
341
+ self.ra_dec_hms = QRadioButton(self.tr("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(self.tr("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(self.tr("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(self.tr("Status: Idle")); layout.addWidget(self.status_label, r, 0, 1, 2); r += 1
354
+ self.lst_label = QLabel(self.tr("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(self.tr("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
+ 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"),
366
+ self.tr("Degrees from Moon"),self.tr("Alt Name"),self.tr("Type"),self.tr("Magnitude"),self.tr("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(self.tr("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(self.tr("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(self.tr("Invalid input: {}").format(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 + self.tr(" Please verify your longitude/timezone."))
454
+ else:
455
+ self.update_status(self.tr("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(self.tr("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(self.tr("Lunar Phase: {}% illuminated").format(phase_percentage))
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(self.tr("Status: {}").format(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, self.tr("Settings"), self.tr("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, self.tr("Add Custom Object"), self.tr("Enter object name:"))
530
+ if not ok or not name:
531
+ return
532
+ ra, ok = QInputDialog.getDouble(self, self.tr("Add Custom Object"), self.tr("Enter RA (deg):"), decimals=3)
533
+ if not ok: return
534
+ dec, ok = QInputDialog.getDouble(self, self.tr("Add Custom Object"), self.tr("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(self.tr("Added custom object: {}").format(name))
546
+ except Exception as e:
547
+ QMessageBox.warning(self, self.tr("Add Custom Object"), self.tr("Could not update catalog:\n{}").format(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, self.tr("Save CSV File"), "", self.tr("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(self.tr("Data saved to {}").format(path))