menu14.py

Download
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()