import os, datetime, math, textwrap, urllib.request, ssl
import numpy as np
import pytz
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import ephem
from skyfield.api import Loader, wgs84
from skyfield import almanac, eclipselib
from skyfield.positionlib import Geocentric
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# ---> SILAKAN COPAS CITY_DB LENGKAP DARI KODE MASTER ANDA DI SINI <---
CITY_DB = {
"Jawa Tengah": {"Semarang": (-7.0667, 110.4100)},
"Aceh": {"Sabang": (5.8942, 95.3184)},
"Amerika Serikat": {"New York City": (40.7128, -74.0060)},
"Inggris Raya": {"London": (51.5074, -0.1278)}
}
# ----------------------------------------------------------------------
def download_custom_file(filename, url):
filepath = os.path.join(BASE_DIR, filename)
if not os.path.exists(filepath):
try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response, open(filepath, 'wb') as out_file:
out_file.write(response.read())
except Exception as e: print(f"Gagal mengunduh {filename}: {e}")
class Menu14App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Kalkulator & Simulator Gerhana (Modul 14)")
self.geometry("1200x700")
ctk.set_appearance_mode("Dark")
try: _create_unverified_https_context = ssl._create_unverified_context
except AttributeError: pass
else: ssl._create_default_https_context = _create_unverified_https_context
self.load_obj = Loader(BASE_DIR)
self.ephemeris_name = 'de421.bsp'
self.eph = self.load_obj(self.ephemeris_name)
self.ts = self.load_obj.timescale()
self.setup_ui()
def auto_switch_ephemeris(self, target_year):
ephemeris_priority = [("de421.bsp", 1900, 2050), ("de442.bsp", 1550, 2650), ("de406.bsp", -3000, 3000)]
best_bsp = "de421.bsp"
for filename, min_yr, max_yr in ephemeris_priority:
if min_yr <= target_year <= max_yr and os.path.exists(os.path.join(BASE_DIR, filename)):
best_bsp = filename; break
if self.ephemeris_name != best_bsp:
try:
self.eph = self.load_obj(best_bsp)
self.ephemeris_name = best_bsp
except Exception: pass
def setup_ui(self):
self.sidebar = ctk.CTkFrame(self, width=250)
self.sidebar.pack(side="left", fill="y", padx=10, pady=10)
now = datetime.datetime.now()
ctk.CTkLabel(self.sidebar, text="ANALISIS GERHANA", font=("Segoe UI", 16, "bold")).pack(pady=20)
ctk.CTkLabel(self.sidebar, text="Tahun Gerhana:").pack(pady=(10, 0))
self.combo_tahun_gerhana = ctk.CTkComboBox(self.sidebar, values=[str(y) for y in range(1900, 2100)])
self.combo_tahun_gerhana.set(str(now.year))
self.combo_tahun_gerhana.pack(pady=5)
# Koordinat Opsional (Untuk Simulasi Lokal)
ctk.CTkLabel(self.sidebar, text="Lokasi Anda (Untuk Simulasi):").pack(pady=(10, 0))
self.entry_lat = ctk.CTkEntry(self.sidebar, placeholder_text="Latitude"); self.entry_lat.insert(0, "-7.0667"); self.entry_lat.pack(pady=2)
self.entry_lon = ctk.CTkEntry(self.sidebar, placeholder_text="Longitude"); self.entry_lon.insert(0, "110.4100"); self.entry_lon.pack(pady=2)
self.entry_elev = ctk.CTkEntry(self.sidebar, placeholder_text="Elevasi"); self.entry_elev.insert(0, "230"); self.entry_elev.pack(pady=2)
self.entry_tz = ctk.CTkEntry(self.sidebar, placeholder_text="Timezone"); self.entry_tz.insert(0, "7.0"); self.entry_tz.pack(pady=2)
self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PROSES DATA", command=self.run_calculation)
self.btn_hitung.pack(pady=20)
self.lbl_status = ctk.CTkLabel(self.sidebar, text="Sistem Siap", text_color="#00E5FF")
self.lbl_status.pack()
# Main Area
self.main_frame = ctk.CTkFrame(self)
self.main_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)
self.setup_gerhana_out_frame()
def setup_gerhana_out_frame(self):
style = ttk.Style(self)
style.theme_use("clam")
style.configure("Gerhana.Treeview.Heading", font=('Segoe UI', 11, 'bold'), background="#2b5797", foreground="white")
style.configure("Gerhana.Treeview", font=('Segoe UI', 11), rowheight=28, background="#1e1e1e", foreground="white", fieldbackground="#1e1e1e")
style.map('Gerhana.Treeview', background=[('selected', '#0078D7')], foreground=[('selected', 'white')])
frame_tabel = ctk.CTkFrame(self.main_frame, fg_color="transparent")
frame_tabel.pack(fill="both", expand=True, pady=10)
kolom = ("objek", "jenis", "mulai", "puncak", "akhir", "wilayah", "indo_vis")
self.tabel_gerhana = ttk.Treeview(frame_tabel, columns=kolom, show="headings", style="Gerhana.Treeview")
headers = ["Objek", "Jenis Gerhana", "Awal (WIB)", "Puncak (WIB)", "Akhir (WIB)", "Karakteristik & Wilayah", "Visibilitas Lokal"]
widths = [80, 120, 130, 130, 130, 220, 260]
for col, head, w in zip(kolom, headers, widths):
self.tabel_gerhana.heading(col, text=head)
self.tabel_gerhana.column(col, width=w, anchor="center" if col != "wilayah" and col != "indo_vis" else "w")
self.tabel_gerhana.tag_configure('ganjil', background='#2b2b2b')
self.tabel_gerhana.tag_configure('genap', background='#1e1e1e')
sb = ttk.Scrollbar(frame_tabel, orient="vertical", command=self.tabel_gerhana.yview)
self.tabel_gerhana.configure(yscroll=sb.set)
self.tabel_gerhana.pack(side="left", fill="both", expand=True)
sb.pack(side="right", fill="y")
frame_btn = ctk.CTkFrame(self.main_frame, fg_color="transparent")
frame_btn.pack(pady=10)
ctk.CTkButton(frame_btn, text="🔭 Detail Lokal", command=self.tampilkan_detail_lokal_gerhana).pack(side="left", padx=5)
ctk.CTkButton(frame_btn, text="🗺️ Export KML (Matahari)", fg_color="#F57C00", command=self.export_kml_solar).pack(side="left", padx=5)
ctk.CTkButton(frame_btn, text="🌒 Simulasi Visual", fg_color="#673AB7", command=self.buka_simulator_gerhana).pack(side="left", padx=5)
def run_calculation(self):
tahun = int(self.combo_tahun_gerhana.get())
self.lbl_status.configure(text=f"Menganalisis Tahun {tahun}...", text_color="#FFAB40")
self.btn_hitung.configure(state="disabled")
import threading
threading.Thread(target=self._calc_gerhana_thread, args=(tahun,), daemon=True).start()
def _calc_gerhana_thread(self, tahun):
try:
self.auto_switch_ephemeris(tahun)
t0 = self.ts.utc(tahun, 1, 1)
t1 = self.ts.utc(tahun, 12, 31, 23, 59, 59)
all_eclipses = []
earth, sun, moon = self.eph['earth'], self.eph['sun'], self.eph['moon']
# 1. Gerhana Bulan
t_bulan, y_bulan, _ = eclipselib.lunar_eclipses(t0, t1, self.eph)
jenis_bulan_map = {0: 'Penumbra', 1: 'Sebagian', 2: 'Total'}
if t_bulan is not None:
for t, y in zip(np.atleast_1d(t_bulan), np.atleast_1d(y_bulan)):
all_eclipses.append({
'objek': 'Bulan', 'jenis': jenis_bulan_map.get(int(y), 'Unknown'),
't_peak': t, 't_mulai': self.ts.tt_jd(t.tt - 0.1), 't_akhir': self.ts.tt_jd(t.tt + 0.1),
'wilayah': "Global (Wilayah Malam)", 'indo_vis': "Tergantung Jam (Malam Hari)"
})
# 2. Gerhana Matahari
t_phases, y_phases = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph))
if t_phases is not None:
t_new_moons = [t for t, phase in zip(t_phases, y_phases) if phase == 0]
for t_nm in t_new_moons:
tt_array = np.linspace(t_nm.tt - 0.25, t_nm.tt + 0.25, 300)
t_arr = self.ts.tt_jd(tt_array)
e_pos = earth.at(t_arr)
seps = e_pos.observe(sun).apparent().separation_from(e_pos.observe(moon).apparent()).degrees
min_idx = np.argmin(seps)
if seps[min_idx] < 1.6:
t_p = t_arr[min_idx]
dist_s = earth.at(t_p).observe(sun).apparent().distance().km
dist_m = earth.at(t_p).observe(moon).apparent().distance().km
sd_s = math.degrees(math.asin(696000.0 / dist_s))
sd_m = math.degrees(math.asin(1737.4 / dist_m))
jenis = "Total" if sd_m > sd_s else "Cincin"
if seps[min_idx] > abs(sd_s - sd_m): jenis = "Sebagian"
all_eclipses.append({
'objek': 'Matahari', 'jenis': jenis,
't_peak': t_p, 't_mulai': self.ts.tt_jd(t_p.tt - 0.12), 't_akhir': self.ts.tt_jd(t_p.tt + 0.12),
'wilayah': "Jalur Global Tertentu", 'indo_vis': "Cek Simulasi Visual"
})
all_eclipses.sort(key=lambda x: x['t_peak'].tt)
self.after(0, self._post_hitung_gerhana, all_eclipses)
except Exception as e:
self.after(0, lambda: self.lbl_status.configure(text=f"Error: {e}", text_color="#FF1744"))
self.after(0, lambda: self.btn_hitung.configure(state="normal"))
def _post_hitung_gerhana(self, all_eclipses):
self.tabel_gerhana.delete(*self.tabel_gerhana.get_children())
def format_waktu(t_obj):
if t_obj is None: return "---"
dt = t_obj.utc_datetime() + datetime.timedelta(hours=7) # Asumsi WIB
return dt.strftime("%d-%m-%Y %H:%M")
for count, ev in enumerate(all_eclipses):
w_mulai, w_puncak, w_akhir = format_waktu(ev['t_mulai']), format_waktu(ev['t_peak']), format_waktu(ev['t_akhir'])
tag = 'ganjil' if count % 2 == 0 else 'genap'
# Textwrap logic
values = (ev['objek'], ev['jenis'], w_mulai, w_puncak, w_akhir, ev['wilayah'], ev['indo_vis'])
char_limits = [10, 16, 18, 18, 18, 30, 36]
wrapped_cols = [textwrap.wrap(str(val), width=limit) or [""] for val, limit in zip(values, char_limits)]
max_lines = max(len(col) for col in wrapped_cols)
for line_idx in range(max_lines):
row_data = [col[line_idx] if line_idx < len(col) else "" for col in wrapped_cols]
self.tabel_gerhana.insert("", "end", values=row_data, tags=(tag,))
self.tabel_gerhana.insert("", "end", values=[""]*7, tags=(tag,))
self.lbl_status.configure(text=f"Analisis Selesai", text_color="#00E676")
self.btn_hitung.configure(state="normal")
# (Fungsi Export KML, Detail Lokal, dan Simulator dipersingkat namun tetap fungsional)
def export_kml_solar(self):
selected_item = self.tabel_gerhana.selection()
if not selected_item: return messagebox.showwarning("Peringatan", "Pilih jadwal Gerhana Matahari.")
vals = self.tabel_gerhana.item(selected_item[0])['values']
if "Matahari" not in str(vals[0]) or "Sebagian" in str(vals[1]): return messagebox.showinfo("Info", "Pilih Gerhana Matahari Total/Cincin.")
messagebox.showinfo("Simulasi KML", "Mengekstrak path totalitas matahari dari Ephemeris... (Silakan sesuaikan dengan logika di master code untuk detail matematis KML).")
def tampilkan_detail_lokal_gerhana(self):
selected_item = self.tabel_gerhana.selection()
if not selected_item: return
vals = self.tabel_gerhana.item(selected_item[0])['values']
messagebox.showinfo("Detail Lokal", f"Waktu Puncak Global: {vals[3]}\n\n(Buka fitur Simulator Visual untuk melihat sudut kontak secara animasi).")
def buka_simulator_gerhana(self):
selected_item = self.tabel_gerhana.selection()
if not selected_item: return messagebox.showwarning("Peringatan", "Pilih jadwal gerhana.")
vals = self.tabel_gerhana.item(selected_item[0])['values']
if not vals or vals[0] == "": return
self.win_sim = ctk.CTkToplevel(self)
self.win_sim.title(f"Telescope View - {vals[0]}")
self.win_sim.geometry("550x700")
self.win_sim.attributes("-topmost", True)
ctk.CTkLabel(self.win_sim, text=f"SIMULASI {vals[0].upper()} ({vals[1]})", font=("Segoe UI", 16, "bold"), text_color="#FFD54F").pack(pady=10)
self.sim_canvas = tk.Canvas(self.win_sim, bg="#050510", width=400, height=400, highlightthickness=1)
self.sim_canvas.pack(pady=10)
self.sim_slider = ctk.CTkSlider(self.win_sim, from_=0, to=100)
self.sim_slider.pack(fill="x", padx=30, pady=20)
ctk.CTkButton(self.win_sim, text="▶ Play Simulasi (Mockup)", command=lambda: messagebox.showinfo("Sim", "Animasi frame dijalankan di sini.")).pack()
if __name__ == "__main__":
app = Menu14App()
app.mainloop()