menu19.py

Download
import os, datetime, math, calendar
import numpy as np
import pytz  # <--- PERBAIKAN: Import pytz ditambahkan
import customtkinter as ctk
from tkinter import messagebox
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import ephem
from skyfield.api import Loader, wgs84

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

class Menu19App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Simulasi Ephemeris 3D (Modul 19)")
        self.geometry("1200x750")
        ctk.set_appearance_mode("Dark")
        
        # --- PERBAIKAN: Mencegah error loop saat jendela ditutup ---
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        
        self.load_obj = Loader(BASE_DIR)
        self.eph = self.load_obj('de421.bsp')
        self.ts = self.load_obj.timescale()
        
        self.pe_obs = ephem.Observer()
        self.pe_sun = ephem.Sun()
        self.pe_moon = ephem.Moon()
        
        self.eph3d_updating = False
        self.eph3d_is_live = False

        self.setup_ui()
        self.setup_eph3d_out_frame()
        self.eph3d_start_live()

    def on_closing(self):
        """Fungsi untuk menghentikan animasi secara bersih saat aplikasi ditutup"""
        self.eph3d_is_live = False
        self.destroy()
        import sys
        sys.exit(0)

    def get_tz_from_lon(self, lon_val):
        return float(int(round(lon_val / 15.0)))

    def create_eph3d_slider(self, parent, label_text, from_, to_, valinit, res=1):
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=10, pady=2)
        ctk.CTkLabel(row, text=label_text, width=80, anchor="w", font=("Segoe UI", 12)).pack(side="left")
        val_label = ctk.CTkLabel(row, text=str(valinit), width=40, anchor="e", font=("Consolas", 12))
        var = ctk.DoubleVar(value=valinit)
        
        def on_slide(val):
            fmt = "{:.0f}" if res >= 1 else "{:.2f}"
            val_label.configure(text=fmt.format(val))
            if not getattr(self, 'eph3d_updating', False):
                self.eph3d_is_live = False
                self.eph3d_update_plot()
                
        slider = ctk.CTkSlider(row, from_=from_, to=to_, variable=var, number_of_steps=int((to_-from_)/res), command=on_slide)
        slider.pack(side="left", fill="x", expand=True, padx=(5, 10))
        val_label.pack(side="right")
        return var, val_label

    def setup_ui(self):
        self.grid_columnconfigure(0, weight=0)
        self.grid_columnconfigure(1, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self.sidebar = ctk.CTkFrame(self, width=320)
        self.sidebar.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)

        ctk.CTkLabel(self.sidebar, text="SIMULASI 3D\n(Toposentris)", font=("Segoe UI", 16, "bold")).pack(pady=20)
        
        # Lokasi
        frame_loc = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_loc.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_loc, text="KOORDINAT OBSERVASI").pack(pady=5)
        
        def create_input(parent, label, default):
            r = ctk.CTkFrame(parent, fg_color="transparent")
            r.pack(fill="x", padx=10, pady=2)
            ctk.CTkLabel(r, text=label).pack(side="left")
            ent = ctk.CTkEntry(r, width=80); ent.insert(0, default); ent.pack(side="right")
            return ent
            
        self.entry_eph3d_lat = create_input(frame_loc, "Lat:", "-7.0667")
        self.entry_eph3d_lon = create_input(frame_loc, "Lon:", "110.4100")

        # Kontrol Waktu
        frame_waktu = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_waktu.pack(fill="x", padx=15, pady=15)
        
        now = datetime.datetime.now()
        self.var_eph3d_tahun, self.lbl_eph3d_tahun = self.create_eph3d_slider(frame_waktu, "Tahun", 1900, 2100, now.year, 1)
        self.var_eph3d_bulan, self.lbl_eph3d_bulan = self.create_eph3d_slider(frame_waktu, "Bulan", 1, 12, now.month, 1)
        self.var_eph3d_hari, self.lbl_eph3d_hari = self.create_eph3d_slider(frame_waktu, "Tanggal", 1, 31, now.day, 1)
        self.var_eph3d_jam, self.lbl_eph3d_jam = self.create_eph3d_slider(frame_waktu, "Jam (Lokal)", 0, 23.99, now.hour, 0.01)
        self.var_eph3d_elev, self.lbl_eph3d_elev = self.create_eph3d_slider(frame_waktu, "Elevasi (m)", 0, 5000, 230, 1)

        # Tombol Aksi
        ctk.CTkButton(self.sidebar, text="🌅 Cari Sunset", fg_color="#C62828", command=self.eph3d_cari_sunset).pack(pady=5, fill="x", padx=20)
        ctk.CTkButton(self.sidebar, text="🔴 Waktu Live", fg_color="#2E7D32", command=self.eph3d_start_live).pack(pady=5, fill="x", padx=20)

        self.main_frame = ctk.CTkFrame(self, fg_color="#000000")
        self.main_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)

    def setup_eph3d_out_frame(self):
        self.lbl_eph3d_info = ctk.CTkLabel(self.main_frame, text="Memuat data...", font=("Courier", 12), text_color="#00E676", justify="left", anchor="nw")
        self.lbl_eph3d_info.pack(fill="x", padx=15, pady=10)

        plt.style.use('dark_background')
        self.fig_eph3d = plt.figure(figsize=(8, 8), facecolor='#000000')
        self.ax_eph3d = self.fig_eph3d.add_subplot(111, projection='3d')
        self.fig_eph3d.subplots_adjust(left=0, right=1, bottom=0, top=1)

        self.canvas_eph3d = FigureCanvasTkAgg(self.fig_eph3d, master=self.main_frame)
        self.canvas_eph3d.get_tk_widget().pack(fill="both", expand=True)

        # Grid Ufuk
        u = np.linspace(0, 2 * np.pi, 60)
        v = np.linspace(0, np.pi/2, 30)
        self.ax_eph3d.plot_wireframe(10 * np.outer(np.cos(u), np.sin(v)), 10 * np.outer(np.sin(u), np.sin(v)), 10 * np.outer(np.ones(np.size(u)), np.cos(v)), color='gray', alpha=0.1)
        
        xx, yy = np.meshgrid(np.linspace(-10, 10, 2), np.linspace(-10, 10, 2))
        self.ax_eph3d.plot_surface(xx, yy, np.zeros_like(xx), color='green', alpha=0.3)

        self.ax_eph3d.text(0, 11, 0, 'Utara', color='white', ha='center')
        self.ax_eph3d.text(0, -11, 0, 'Selatan', color='white', ha='center')
        self.ax_eph3d.text(11, 0, 0, 'Timur', color='white', ha='center')
        self.ax_eph3d.text(-11, 0, 0, 'Barat', color='white', ha='center')

        self.titik_matahari, = self.ax_eph3d.plot([], [], [], 'o', color='yellow', markersize=15)
        self.titik_bulan, = self.ax_eph3d.plot([], [], [], 'o', color='white', markersize=10)
        self.garis_matahari, = self.ax_eph3d.plot([], [], [], color='yellow', linestyle='--', alpha=0.5)
        self.garis_bulan, = self.ax_eph3d.plot([], [], [], color='white', linestyle='--', alpha=0.5)
        
        self.ax_eph3d.set_xlim([-10, 10]); self.ax_eph3d.set_ylim([-10, 10]); self.ax_eph3d.set_zlim([-2, 10])
        self.ax_eph3d.set_axis_off()

    def eph3d_r_ke_xyz(self, alt, az, r=10):
        return r * np.sin(az) * np.cos(alt), r * np.cos(az) * np.cos(alt), r * np.sin(alt)

    def eph3d_update_plot(self):
        try:
            lat_val, lon_val = float(self.entry_eph3d_lat.get()), float(self.entry_eph3d_lon.get())
            elev_val = float(self.var_eph3d_elev.get())
            tz_offset = int(self.get_tz_from_lon(lon_val))
            
            y, m = int(self.var_eph3d_tahun.get()), int(self.var_eph3d_bulan.get())
            d = min(int(self.var_eph3d_hari.get()), calendar.monthrange(y, m)[1])
            jam_desimal = self.var_eph3d_jam.get()
            
            jam = int(jam_desimal)
            menit = int((jam_desimal - jam) * 60)
            detik = min(59, max(0, int((((jam_desimal - jam) * 60) - menit) * 60)))
            
            waktu_lokal = datetime.datetime(y, m, d, jam, menit, detik)
            waktu_utc = waktu_lokal - datetime.timedelta(hours=tz_offset)
            
            # PyEphem
            self.pe_obs.lat, self.pe_obs.lon = str(lat_val), str(lon_val)
            self.pe_obs.elevation = elev_val
            self.pe_obs.date = waktu_utc
            self.pe_sun.compute(self.pe_obs)
            self.pe_moon.compute(self.pe_obs)
            
            alt_topo_sun, az_topo_sun = math.degrees(float(self.pe_sun.alt)), math.degrees(float(self.pe_sun.az))
            alt_topo_moon, az_topo_moon = math.degrees(float(self.pe_moon.alt)), math.degrees(float(self.pe_moon.az))
            elong_topo = math.degrees(ephem.separation(self.pe_sun, self.pe_moon))
            
            # Skyfield (Geocentric)
            t_skyfield = self.ts.from_datetime(waktu_utc.replace(tzinfo=pytz.utc))
            bumi, matahari, bulan = self.eph['earth'], self.eph['sun'], self.eph['moon']
            sun_geo = bumi.at(t_skyfield).observe(matahari).apparent()
            moon_geo = bumi.at(t_skyfield).observe(bulan).apparent()

            elong_geo = sun_geo.separation_from(moon_geo).degrees
            ra_sun, dec_sun, _ = sun_geo.radec()
            ra_moon, dec_moon, _ = moon_geo.radec()
            
            lst = t_skyfield.gast + (lon_val / 15.0)
            lat_rad = math.radians(lat_val)

            def get_geo_alt(ra, dec):
                ha_rad = math.radians((lst - ra.hours) * 15.0)
                sin_alt = math.sin(lat_rad) * math.sin(dec.radians) + math.cos(lat_rad) * math.cos(dec.radians) * math.cos(ha_rad)
                return math.degrees(math.asin(max(-1.0, min(1.0, sin_alt))))

            alt_geo_sun = get_geo_alt(ra_sun, dec_sun)
            alt_geo_moon = get_geo_alt(ra_moon, dec_moon)
            
            # Update 3D Visual
            sx, sy, sz = self.eph3d_r_ke_xyz(float(self.pe_sun.alt), float(self.pe_sun.az))
            mx, my, mz = self.eph3d_r_ke_xyz(float(self.pe_moon.alt), float(self.pe_moon.az))
            
            self.titik_matahari.set_data([sx], [sy]); self.titik_matahari.set_3d_properties([sz])
            self.garis_matahari.set_data([0, sx], [0, sy]); self.garis_matahari.set_3d_properties([0, sz])
            self.titik_bulan.set_data([mx], [my]); self.titik_bulan.set_3d_properties([mz])
            self.garis_bulan.set_data([0, mx], [0, my]); self.garis_bulan.set_3d_properties([0, mz])
            
            status_waktu = "🔴 SEDANG LIVE" if self.eph3d_is_live else "⏸️ WAKTU KUSTOM"
            teks = (
                f"Lokasi: Lat {lat_val} | Lon {lon_val} | Elev: {elev_val}m\n"
                f"Waktu Lokal: {waktu_lokal.strftime('%d-%b-%Y %H:%M:%S')} ({status_waktu})\n\n"
                f"[MATAHARI]\n"
                f" ├ Toposentrik -> Tinggi: {alt_topo_sun:>6.2f}° | Azimuth: {az_topo_sun:>6.2f}°\n"
                f" └ Geosentrik  -> Tinggi: {alt_geo_sun:>6.2f}°\n"
                f"[BULAN / HILAL]\n"
                f" ├ Toposentrik -> Tinggi: {alt_topo_moon:>6.2f}° | Azimuth: {az_topo_moon:>6.2f}°\n"
                f" └ Geosentrik  -> Tinggi: {alt_geo_moon:>6.2f}°\n"
                f"[RELASI]\n"
                f" ├ Elongasi Toposentrik : {elong_topo:>6.2f}°\n"
                f" ├ Elongasi Geosentrik  : {elong_geo:>6.2f}°\n"
            )
            self.lbl_eph3d_info.configure(text=teks)
            self.canvas_eph3d.draw_idle()
            
        except Exception as e: print(f"Error 3D Plot: {e}")

    def eph3d_cari_sunset(self):
        self.eph3d_is_live = False
        try:
            lat_val, lon_val = float(self.entry_eph3d_lat.get()), float(self.entry_eph3d_lon.get())
            tz_offset = int(self.get_tz_from_lon(lon_val))
            y, m, d = int(self.var_eph3d_tahun.get()), int(self.var_eph3d_bulan.get()), int(self.var_eph3d_hari.get())
            
            self.pe_obs.lat, self.pe_obs.lon = str(lat_val), str(lon_val)
            self.pe_obs.date = datetime.datetime(y, m, d, 12, 0, 0) - datetime.timedelta(hours=tz_offset)
            
            waktu_sunset_lokal = self.pe_obs.next_setting(self.pe_sun).datetime() + datetime.timedelta(hours=tz_offset)
            jam_desimal = waktu_sunset_lokal.hour + (waktu_sunset_lokal.minute / 60.0) + (waktu_sunset_lokal.second / 3600.0)
            
            self.eph3d_updating = True
            self.var_eph3d_tahun.set(waktu_sunset_lokal.year)
            self.var_eph3d_bulan.set(waktu_sunset_lokal.month)
            self.var_eph3d_hari.set(waktu_sunset_lokal.day)
            self.var_eph3d_jam.set(jam_desimal)
            
            self.eph3d_updating = False
            self.eph3d_update_plot()
        except Exception as e: messagebox.showerror("Error", f"Gagal mencari sunset: {e}")

    def eph3d_start_live(self):
        self.eph3d_is_live = True
        self.eph3d_update_plot() 
        self.eph3d_live_update_loop()

    def eph3d_live_update_loop(self):
        # --- PERBAIKAN: Mencegah error jika jendela sudah ditutup ---
        if not self.winfo_exists(): return 
        
        if self.eph3d_is_live:
            self.eph3d_updating = True
            try:
                lon_val = float(self.entry_eph3d_lon.get())
                tz_offset = int(self.get_tz_from_lon(lon_val))
                
                now_utc = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
                now_target = now_utc + datetime.timedelta(hours=tz_offset)
                
                self.var_eph3d_tahun.set(now_target.year)
                self.var_eph3d_bulan.set(now_target.month)
                self.var_eph3d_hari.set(now_target.day)
                jam_des = now_target.hour + (now_target.minute / 60.0) + (now_target.second / 3600.0)
                self.var_eph3d_jam.set(jam_des)
                
                self.eph3d_update_plot()
            except Exception: pass
            finally: self.eph3d_updating = False
            self.after(1000, self.eph3d_live_update_loop)

if __name__ == "__main__":
    app = Menu19App()
    app.mainloop()