import os, datetime, math, random
import customtkinter as ctk
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk, ImageDraw
import ephem
from skyfield.api import Loader, wgs84
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class Menu15App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Live Animasi 2D Tracker (Modul 15)")
self.geometry("1100x700")
ctk.set_appearance_mode("Dark")
self.load_obj = Loader(BASE_DIR)
self.eph = self.load_obj('de421.bsp')
self.ts = self.load_obj.timescale()
self.setup_ui()
self.anim_is_live = True
self.anim_custom_time = None
self.anim_running = True
self.draw_static_background()
self.update_animation()
def setup_ui(self):
self.sidebar = ctk.CTkFrame(self, width=280)
self.sidebar.pack(side="left", fill="y", padx=10, pady=10)
ctk.CTkLabel(self.sidebar, text="LIVE ANIMASI 2D", font=("Segoe UI", 16, "bold")).pack(pady=20)
# Form Lokasi
self.entry_lat = ctk.CTkEntry(self.sidebar, placeholder_text="Latitude"); self.entry_lat.insert(0, "-7.0667"); self.entry_lat.pack(pady=5)
self.entry_lon = ctk.CTkEntry(self.sidebar, placeholder_text="Longitude"); self.entry_lon.insert(0, "110.4100"); self.entry_lon.pack(pady=5)
self.entry_tz = ctk.CTkEntry(self.sidebar, placeholder_text="Timezone"); self.entry_tz.insert(0, "7.0"); self.entry_tz.pack(pady=5)
now = datetime.datetime.now()
self.entry_y = ctk.CTkEntry(self.sidebar); self.entry_y.insert(0, str(now.year)); self.entry_y.pack(pady=5)
self.entry_m = ctk.CTkEntry(self.sidebar); self.entry_m.insert(0, f"{now.month:02d}"); self.entry_m.pack(pady=5)
self.entry_d = ctk.CTkEntry(self.sidebar); self.entry_d.insert(0, f"{now.day:02d}"); self.entry_d.pack(pady=5)
self.entry_time = ctk.CTkEntry(self.sidebar); self.entry_time.insert(0, now.strftime("%H:%M:%S")); self.entry_time.pack(pady=5)
ctk.CTkButton(self.sidebar, text="Terapkan Kustom", command=self.set_kustom).pack(pady=10)
ctk.CTkButton(self.sidebar, text="🌅 Ke Waktu Sunset", fg_color="#F57C00", command=self.set_sunset).pack(pady=5)
ctk.CTkButton(self.sidebar, text="🔴 Kembali Live", fg_color="#2E7D32", command=self.set_live).pack(pady=5)
# Main Canvas Area
self.main_frame = ctk.CTkFrame(self, fg_color="#050510")
self.main_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)
header = ctk.CTkFrame(self.main_frame, fg_color="#181818", corner_radius=8)
header.pack(fill="x", padx=15, pady=10)
ctk.CTkLabel(header, text="🔴 REAL-TIME TRACKER", font=("Segoe UI", 16, "bold"), text_color="#FF5252").pack(side="left", padx=15)
self.is_anim_zoomed = False
self.btn_anim_zoom = ctk.CTkButton(header, text="🔍 Zoom Hilal", fg_color="#673AB7", command=self.toggle_zoom)
self.btn_anim_zoom.pack(side="right", padx=15, pady=10)
self.anim_canvas = tk.Canvas(self.main_frame, bg='#030514', highlightthickness=0)
self.anim_canvas.pack(fill="both", expand=True, padx=15, pady=(0, 15))
self.anim_canvas.bind("<Configure>", self.draw_static_background)
def set_live(self):
self.anim_is_live = True
messagebox.showinfo("Live", "Dikembalikan ke Waktu Real-Time.")
def set_kustom(self):
self.anim_is_live = False
try:
y, m, d = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
h, mn, s = map(int, self.entry_time.get().split(":"))
tz = float(self.entry_tz.get())
dt_lokal = datetime.datetime(y, m, d, h, mn, s)
dt_utc = dt_lokal - datetime.timedelta(hours=tz)
import pytz
self.anim_custom_time = self.ts.from_datetime(dt_utc.replace(tzinfo=pytz.utc))
except Exception as e:
messagebox.showerror("Error", f"Format tidak valid: {e}")
self.set_live()
def set_sunset(self):
self.anim_is_live = False
try:
y, m, d = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
lat, lon, tz = float(self.entry_lat.get()), float(self.entry_lon.get()), float(self.entry_tz.get())
pengamat = ephem.Observer()
pengamat.lat, pengamat.lon = str(lat), str(lon)
pengamat.elevation, pengamat.pressure, pengamat.temp = 0, 1010, 25
waktu_mulai = datetime.datetime(y, m, d, 12, 0, 0) - datetime.timedelta(hours=tz)
pengamat.date = ephem.Date(waktu_mulai)
waktu_sunset_utc = pengamat.next_setting(ephem.Sun()).datetime()
dt_lokal = waktu_sunset_utc + datetime.timedelta(hours=tz)
import pytz
self.anim_custom_time = self.ts.from_datetime(waktu_sunset_utc.replace(tzinfo=pytz.utc))
self.entry_time.delete(0, 'end')
self.entry_time.insert(0, dt_lokal.strftime("%H:%M:%S"))
except Exception as e:
messagebox.showerror("Error", f"Gagal mencari sunset: {e}")
def toggle_zoom(self):
self.is_anim_zoomed = not getattr(self, 'is_anim_zoomed', False)
if self.is_anim_zoomed:
self.btn_anim_zoom.configure(text="🔍 Normal View", fg_color="#D32F2F")
self.anim_canvas.delete("static")
else:
self.btn_anim_zoom.configure(text="🔍 Zoom Hilal", fg_color="#673AB7")
self.draw_static_background()
def draw_static_background(self, event=None):
if getattr(self, 'is_anim_zoomed', False):
self.anim_canvas.delete("static")
return
self.anim_canvas.delete("static")
width = self.anim_canvas.winfo_width()
height = self.anim_canvas.winfo_height()
if height <= 10: return
horizon_y = height / 2
pad_x = 45
usable_width = width - (2 * pad_x)
self.anim_canvas.create_rectangle(0, horizon_y, width, height, fill="#0A150A", outline="", tags="static")
self.anim_canvas.create_line(0, horizon_y, width, horizon_y, fill="#00E676", width=2, dash=(4, 2), tags="static")
# Bintang
for _ in range(150):
x = random.randint(0, width)
y = random.randint(0, int(horizon_y))
color = random.choice(["#FFFFFF", "#E0F7FA", "#FFFDE7"])
self.anim_canvas.create_oval(x-1, y-1, x+1, y+1, fill=color, outline="", tags="static")
# Kompas
kompas = {0: "U (0°)", 45: "TL (45°)", 90: "T (90°)", 135: "TG (135°)", 180: "S (180°)", 225: "BD (225°)", 270: "B (270°)", 315: "BL (315°)", 360: "U (360°)"}
for az, label in kompas.items():
x = pad_x + (az / 360.0) * usable_width
self.anim_canvas.create_text(x, horizon_y + 15, text=label, fill="white", font=("Consolas", 9), tags="static")
self.anim_canvas.create_line(x, horizon_y, x, horizon_y + 6, fill="white", tags="static")
def update_animation(self):
if not self.anim_running: return
width = self.anim_canvas.winfo_width()
height = self.anim_canvas.winfo_height()
if width < 10 or height < 10:
self.after(500, self.update_animation)
return
self.anim_canvas.delete("dyn")
try:
lat, lon, tz = float(self.entry_lat.get()), float(self.entry_lon.get()), float(self.entry_tz.get())
if self.anim_is_live: t_sim = self.ts.now()
else: t_sim = self.anim_custom_time
dt_lokal = t_sim.utc_datetime() + datetime.timedelta(hours=tz)
earth, sun, moon = self.eph['earth'], self.eph['sun'], self.eph['moon']
observer = earth + wgs84.latlon(lat, lon)
astro_sun = observer.at(t_sim).observe(sun).apparent()
alt_s, az_s, _ = astro_sun.altaz()
alt_s_deg, az_s_deg = alt_s.degrees, az_s.degrees
astro_moon = observer.at(t_sim).observe(moon).apparent()
alt_m, az_m, _ = astro_moon.altaz()
alt_m_deg, az_m_deg = alt_m.degrees, az_m.degrees
elong_geo = earth.at(t_sim).observe(sun).apparent().separation_from(earth.at(t_sim).observe(moon).apparent()).degrees
pad_x = 45
usable_width = width - (2 * pad_x)
horizon_y = height / 2
if self.is_anim_zoomed:
# Mode Zoom (Fokus Selisih Azimut)
diff_az = az_m_deg - az_s_deg
if diff_az > 180: diff_az -= 360
elif diff_az < -180: diff_az += 360
center_az = az_s_deg + (diff_az / 2.0)
center_alt = (alt_s_deg + alt_m_deg) / 2.0
px_per_deg_y = 60.0
px_per_deg_x = 60.0
horizon_y = (height / 2.0) + (center_alt * px_per_deg_y)
r_sun, r_moon = 40, 30
def get_zoomed_x(az_target):
d_az = az_target - center_az
if d_az > 180: d_az -= 360
elif d_az < -180: d_az += 360
return (width / 2.0) + (d_az * px_per_deg_x)
x_sun, x_moon = get_zoomed_x(az_s_deg), get_zoomed_x(az_m_deg)
self.anim_canvas.create_line(0, horizon_y, width, horizon_y, fill="#00E676", width=3, tags="dyn")
else:
# Mode Normal (360 Derajat)
px_per_deg_y = horizon_y / 90.0
px_per_deg_x = usable_width / 360.0
r_sun = max(36, int(3.6 * px_per_deg_y))
r_moon = max(22, int(2.2 * px_per_deg_y))
x_sun = pad_x + (az_s_deg * px_per_deg_x)
x_moon = pad_x + (az_m_deg * px_per_deg_x)
y_sun = horizon_y - (alt_s_deg * px_per_deg_y)
y_moon = horizon_y - (alt_m_deg * px_per_deg_y)
# Gambar Garis Elongasi
self.anim_canvas.create_line(x_sun, y_sun, x_moon, y_moon, fill="#00E676", dash=(4, 2), width=2, tags="dyn")
# Menggambar Matahari (Fallback ke warna orange jika gambar gagal)
self.anim_canvas.create_oval(x_sun-r_sun, y_sun-r_sun, x_sun+r_sun, y_sun+r_sun, fill="#FFD54F", outline="#FFEA00", width=2, tags="dyn")
# Menggambar Bulan (Warna cyan redup sebagai ilustrasi hilal)
self.anim_canvas.create_oval(x_moon-r_moon, y_moon-r_moon, x_moon+r_moon, y_moon+r_moon, fill="#E0E0E0", outline="white", width=2, tags="dyn")
# Teks Info Koordinat
self.anim_canvas.create_text(x_sun, y_sun + r_sun + 15, text=f"SUN\nAlt:{alt_s_deg:.1f}°", fill="#FFD54F", font=("Consolas", 10), tags="dyn")
self.anim_canvas.create_text(x_moon, y_moon - r_moon - 15, text=f"MOON\nAlt:{alt_m_deg:.1f}°", fill="#00E5FF", font=("Consolas", 10), tags="dyn")
# Box Kiri Atas
box_text = f"Waktu: {dt_lokal.strftime('%d-%m-%Y %H:%M:%S')}\nElongasi Geo: {elong_geo:.2f}°"
self.anim_canvas.create_rectangle(15, 15, 300, 75, fill="#1A1A1A", outline="#00E5FF", tags="dyn")
self.anim_canvas.create_text(25, 25, text=box_text, fill="white", font=("Consolas", 12, "bold"), anchor="nw", tags="dyn")
except Exception as e:
self.anim_canvas.create_text(width/2, height/2, text=f"Updating Data... {e}", fill="gray", tags="dyn")
self.after(1000, self.update_animation)
if __name__ == "__main__":
app = Menu15App()
app.mainloop()