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