import sys
import subprocess
import json
import threading
import time
import math
from datetime import datetime, timedelta
import warnings
import customtkinter as ctk
import astropy.units as u
from astropy.time import Time
from astropy.coordinates import EarthLocation, AltAz, get_body, SkyCoord, solar_system_ephemeris, GeocentricTrueEcliptic
from tkinter import messagebox
# Abaikan peringatan IERS
warnings.filterwarnings('ignore')
# ==========================================
# KONFIGURASI TEMA GLOBAL
# ==========================================
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("dark-blue")
COLORS = {
"bg": "#101010", # Background lebih gelap
"card_bg": "#1E1E1E", # Card abu-abu gelap
"sun_accent": "#FFD700", # Emas cerah
"sun_glow": "#FFF176",
"moon_accent": "#00E5FF", # Cyan neon
"moon_glow": "#B3E5FC",
"text": "#FFFFFF",
"subtext": "#AAAAAA",
"success": "#00C853", # Hijau Matrix
"danger": "#FF1744", # Merah Alert
"warning": "#FF9100",
"header_bg": "#252525",
"hijri_text": "#E040FB" # Ungu Neon untuk Tanggal Komariyah
}
# ==========================================
# BAGIAN 1: LOGIKA ASTRONOMIS (SHARED ENGINE)
# ==========================================
class AstroEngine:
"""Kelas inti untuk perhitungan Astronomi yang digunakan oleh kedua Tab"""
@staticmethod
def calculate_positions(location, obstime):
"""Menghitung posisi Sun & Moon, Elongasi, Iluminasi & Moon Age"""
try:
# Frame AltAz Lokal
frame_topo = AltAz(obstime=obstime, location=location, pressure=0*u.bar)
results = {}
# Gunakan ephemeris bawaan
with solar_system_ephemeris.set('builtin'):
# --- A. AMBIL OBJEK GCRS (Geosentris) ---
sun_gcrs = get_body('sun', obstime)
moon_gcrs = get_body('moon', obstime)
# --- B. HITUNG ELONGASI ---
elongation = sun_gcrs.separation(moon_gcrs).degree
results['elongation'] = elongation
# --- C. HITUNG ILUMINASI BULAN ---
elong_rad = math.radians(elongation)
illumination_percent = 0.5 * (1 - math.cos(elong_rad)) * 100
results['illumination'] = illumination_percent
# --- D. HITUNG MOON AGE (TAMBAHAN FITUR) ---
ecliptic_frame = GeocentricTrueEcliptic(obstime=obstime)
sun_ecl = sun_gcrs.transform_to(ecliptic_frame)
moon_ecl = moon_gcrs.transform_to(ecliptic_frame)
lon_diff = (moon_ecl.lon.degree - sun_ecl.lon.degree) % 360
moon_age = (lon_diff / 360.0) * 29.53059
results['moon_age'] = moon_age
# --- E. HITUNG POSISI SUN & MOON ---
bodies = {'sun': sun_gcrs, 'moon': moon_gcrs}
for name, obj_gcrs in bodies.items():
# 1. HITUNGAN TOPOSENTRIS (Parallax ON)
obj_topo_raw = get_body(name, obstime, location=location)
pos_topo = obj_topo_raw.transform_to(frame_topo)
# 2. HITUNGAN GEOSENTRIS (Parallax OFF)
obj_geo_proxy = SkyCoord(
ra=obj_gcrs.ra,
dec=obj_gcrs.dec,
distance=1000*u.AU,
frame='gcrs',
obstime=obstime
)
pos_geo = obj_geo_proxy.transform_to(frame_topo)
results[name] = {
"alt_topo": pos_topo.alt.degree,
"alt_geo": pos_geo.alt.degree,
"az_topo": pos_topo.az.degree,
"az_geo": pos_geo.az.degree,
"dist": pos_topo.distance.to(u.km).value
}
return results
except Exception as e:
print(f"Engine Error: {e}")
return None
# ==========================================
# BAGIAN 2: LOGIKA GPS (SHARED)
# ==========================================
class GPSBackend:
def __init__(self):
self.location = None
self.lat = 0
self.lon = 0
self.source = "Mencari..."
self.is_ready = False
def get_windows_gps(self):
"""Mengakses Sensor Lokasi via PowerShell"""
ps_script = """
Add-Type -AssemblyName System.Device
$watcher = New-Object System.Device.Location.GeoCoordinateWatcher
$watcher.Start()
$start = Get-Date
while (($watcher.Status -ne 'Ready') -and ((Get-Date) -lt $start.AddSeconds(3))) {
Start-Sleep -Milliseconds 500
}
if ($watcher.Status -eq 'Ready') {
$loc = $watcher.Position.Location
if ($loc.IsUnknown -ne $true) {
$result = @{lat=$loc.Latitude; lon=$loc.Longitude}
Write-Output ($result | ConvertTo-Json)
}
}
"""
try:
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
process = subprocess.run(
["powershell", "-Command", ps_script],
capture_output=True, text=True, startupinfo=si
)
output = process.stdout.strip()
if not output: return None
data = json.loads(output)
return float(data['lat']), float(data['lon'])
except:
return None
def initialize_location(self):
gps = self.get_windows_gps()
if gps:
self.lat, self.lon = gps
self.source = "GPS Satelit"
else:
# Fallback Jakarta
self.lat, self.lon = -6.1754, 106.8272
self.source = "Default (Jakarta)"
self.location = EarthLocation(lat=self.lat*u.deg, lon=self.lon*u.deg, height=20*u.m)
self.is_ready = True
# ==========================================
# BAGIAN 3: GUI - TAB 1 (REAL-TIME TRACKER)
# ==========================================
class TabTracker(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master, fg_color="transparent")
self.backend = GPSBackend()
self.engine = AstroEngine()
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(2, weight=1)
# --- HEADER ---
self.header_frame = ctk.CTkFrame(self, corner_radius=15, fg_color=COLORS["header_bg"])
self.header_frame.grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 10), sticky="ew")
self.header_frame.columnconfigure(0, weight=1)
self.header_frame.columnconfigure(1, weight=1)
self.title_box = ctk.CTkFrame(self.header_frame, fg_color="transparent")
self.title_box.grid(row=0, column=0, padx=20, pady=10, sticky="w")
ctk.CTkLabel(self.title_box, text="ASTRO MONITOR", font=("Montserrat", 22, "bold"), text_color="white").pack(anchor="w")
self.lbl_loc = ctk.CTkLabel(self.title_box, text="Mencari Lokasi...", font=("Consolas", 11), text_color=COLORS["subtext"])
self.lbl_loc.pack(anchor="w")
self.time_box = ctk.CTkFrame(self.header_frame, fg_color="transparent")
self.time_box.grid(row=0, column=1, padx=20, pady=10, sticky="e")
self.lbl_time = ctk.CTkLabel(self.time_box, text="--:--:--", font=("Consolas", 24, "bold"), text_color=COLORS["moon_accent"])
self.lbl_time.pack(anchor="e")
self.lbl_elong = ctk.CTkLabel(self.time_box, text="Elongation: --", font=("Segoe UI", 12, "bold"), text_color=COLORS["sun_accent"])
self.lbl_elong.pack(anchor="e")
# --- CARDS ---
self.sun_frame = self.create_instrument_card(0, "SUN", "☀", COLORS["sun_accent"], COLORS["sun_glow"])
self.moon_frame = self.create_instrument_card(1, "MOON", "☾", COLORS["moon_accent"], COLORS["moon_glow"], is_moon=True)
# --- STATUS ---
self.status_frame = ctk.CTkFrame(self, fg_color="transparent")
self.status_frame.grid(row=3, column=0, columnspan=2, pady=10, sticky="ew")
self.lbl_status = ctk.CTkLabel(self.status_frame, text="Initializing System...", text_color="gray", font=("Consolas", 10))
self.lbl_status.pack()
threading.Thread(target=self.start_backend, daemon=True).start()
self.update_ui()
def create_instrument_card(self, col, title, icon, accent_color, glow_color, is_moon=False):
frame = ctk.CTkFrame(self, corner_radius=20, fg_color=COLORS["card_bg"], border_width=1, border_color="#333")
frame.grid(row=1, column=col, padx=10, pady=10, sticky="nsew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
header = ctk.CTkFrame(frame, fg_color="transparent")
header.grid(row=0, column=0, columnspan=2, pady=(15, 5), padx=15, sticky="ew")
ctk.CTkLabel(header, text=icon, font=("Segoe UI", 24), text_color=accent_color).pack(side="left")
ctk.CTkLabel(header, text=f" {title}", font=("Montserrat", 16, "bold"), text_color="white").pack(side="left")
status_pill = ctk.CTkLabel(header, text="...", font=("Segoe UI", 10, "bold"), text_color="white", fg_color="#333", corner_radius=8, width=80, height=24)
status_pill.pack(side="right")
alt_frame = ctk.CTkFrame(frame, fg_color="transparent")
alt_frame.grid(row=1, column=0, columnspan=2, pady=(10, 5))
alt_val = ctk.CTkLabel(alt_frame, text="--°", font=("Consolas", 48, "bold"), text_color=accent_color)
alt_val.pack()
ctk.CTkLabel(alt_frame, text="TOPO ALTITUDE", font=("Segoe UI", 9, "bold"), text_color="gray").pack()
prog_bar = ctk.CTkProgressBar(frame, height=6, progress_color=accent_color, fg_color="#333")
prog_bar.set(0)
prog_bar.grid(row=2, column=0, columnspan=2, padx=20, pady=(5, 15), sticky="ew")
data_grid = ctk.CTkFrame(frame, fg_color="#252525", corner_radius=10)
data_grid.grid(row=3, column=0, columnspan=2, padx=15, pady=(0, 15), sticky="ew")
data_grid.columnconfigure((0, 1), weight=1)
labels = {}
def add_data_row(parent, r, label_txt, key, val_color="white"):
ctk.CTkLabel(parent, text=label_txt, font=("Segoe UI", 12, "bold"), text_color="gray").grid(row=r, column=0, padx=15, pady=3, sticky="w")
lbl_val = ctk.CTkLabel(parent, text="--", font=("Consolas", 20, "bold"), text_color=val_color)
lbl_val.grid(row=r, column=1, padx=15, pady=3, sticky="e")
labels[key] = lbl_val
add_data_row(data_grid, 0, "AZIMUTH", "az")
add_data_row(data_grid, 1, "GEO ALTITUDE", "geo_alt", val_color=COLORS["subtext"])
add_data_row(data_grid, 2, "DISTANCE (km)", "dist")
if is_moon:
ctk.CTkFrame(data_grid, height=1, fg_color="#444").grid(row=3, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
add_data_row(data_grid, 4, "ILLUMINATION", "illum", val_color=COLORS["moon_accent"])
add_data_row(data_grid, 5, "MOON AGE", "age", val_color="#E1BEE7")
add_data_row(data_grid, 6, "EST. KOMARIYAH", "hijri", val_color=COLORS["hijri_text"])
labels['main_alt'] = alt_val
labels['status'] = status_pill
labels['bar'] = prog_bar
frame.labels = labels
return frame
def start_backend(self):
self.backend.initialize_location()
def update_ui(self):
current_time = datetime.now().strftime("%H:%M:%S")
if self.backend.is_ready:
self.lbl_loc.configure(text=f"📍 {self.backend.lat:.4f}, {self.backend.lon:.4f} | {datetime.now().strftime('%d %b %Y')}")
try:
now = Time.now()
data = self.engine.calculate_positions(self.backend.location, now)
if data:
self.lbl_elong.configure(text=f"∠ Elongation: {data['elongation']:.2f}°")
self.update_card(self.sun_frame, data['sun'])
self.update_card(self.moon_frame, data['moon'])
self.moon_frame.labels['illum'].configure(text=f"{data.get('illumination', 0):.1f}%")
age = data.get('moon_age', 0)
self.moon_frame.labels['age'].configure(text=f"{age:.2f} d")
komariyah = int(age)
self.moon_frame.labels['hijri'].configure(text="New Moon" if komariyah == 0 else f"Tgl {komariyah}")
self.lbl_status.configure(text=f"Sync: {now.iso} (UTC) | Source: {self.backend.source}")
except Exception as e:
self.lbl_status.configure(text=f"Error: {e}")
else:
self.lbl_status.configure(text="Mencari sinyal GPS...")
self.lbl_time.configure(text=current_time)
self.after(500, self.update_ui)
def update_card(self, frame, data):
alt = data['alt_topo']
frame.labels['main_alt'].configure(text=f"{alt:.2f}°")
frame.labels['az'].configure(text=f"{data['az_topo']:.2f}°")
frame.labels['geo_alt'].configure(text=f"{data['alt_geo']:.2f}°")
frame.labels['dist'].configure(text=f"{data['dist']:,.0f}")
frame.labels['bar'].set(max(0, min(1, alt / 90)))
if alt > 0: frame.labels['status'].configure(text="VISIBLE", fg_color=COLORS["success"])
elif alt > -18: frame.labels['status'].configure(text="TWILIGHT", fg_color=COLORS["warning"])
else: frame.labels['status'].configure(text="SET", fg_color=COLORS["danger"])
# ==========================================
# BAGIAN 4: GUI - TAB 2 (MANUAL ALASKA + 2 BUTTONS)
# ==========================================
class TabAlaska(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master, fg_color="transparent")
self.engine = AstroEngine()
self.gps_backend = GPSBackend()
# Konfigurasi Default Data Alaska
self.ALASKA_DEFAULTS = {
"Latitude": "56.8135",
"Longitude": "-158.8622",
"Tanggal (YYYY-MM-DD)": "2026-02-17",
"Waktu (HH:MM:SS)": "16:42:21",
"UTC Offset": "-11"
}
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=3)
self.grid_rowconfigure(0, weight=1)
self.setup_left_panel()
self.setup_right_panel()
def setup_left_panel(self):
input_frame = ctk.CTkFrame(self, corner_radius=0, fg_color=COLORS["card_bg"])
input_frame.grid(row=0, column=0, sticky="nsew")
ctk.CTkLabel(input_frame, text="MANUAL INPUT", font=("Segoe UI", 16, "bold")).pack(pady=(20, 10))
self.entries = {}
for lbl_txt, val in self.ALASKA_DEFAULTS.items():
ctk.CTkLabel(input_frame, text=lbl_txt, anchor="w", font=("Arial", 11)).pack(fill="x", padx=15, pady=(5,0))
ent = ctk.CTkEntry(input_frame, height=30)
ent.insert(0, val)
ent.pack(fill="x", padx=15, pady=(0,5))
self.entries[lbl_txt] = ent
# --- CONTAINER TOMBOL ISI DATA ---
btn_box = ctk.CTkFrame(input_frame, fg_color="transparent")
btn_box.pack(fill="x", padx=10, pady=(20, 5))
# Tombol 1: Real Time (Kiri)
self.btn_realtime = ctk.CTkButton(btn_box, text="📍 REAL TIME",
command=self.fill_realtime_data,
fg_color=COLORS["success"], width=100)
self.btn_realtime.pack(side="left", padx=5, expand=True, fill="x")
# Tombol 2: Default Alaska (Kanan)
self.btn_default = ctk.CTkButton(btn_box, text="❄ DEFAULT",
command=self.fill_default_data,
fg_color="#546E7A", width=100)
self.btn_default.pack(side="right", padx=5, expand=True, fill="x")
# Label Status GPS/Reset
self.lbl_gps_status = ctk.CTkLabel(input_frame, text="", font=("Arial", 10), text_color="gray")
self.lbl_gps_status.pack(pady=2)
# Tombol Hitung (Bawah)
ctk.CTkButton(input_frame, text="HITUNG POSISI", command=self.on_calculate,
fg_color=COLORS["moon_accent"], text_color="black", height=40).pack(padx=15, pady=(10, 20), fill="x")
def setup_right_panel(self):
output_frame = ctk.CTkFrame(self, fg_color="transparent")
output_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
self.lbl_result_title = ctk.CTkLabel(output_frame, text="HASIL KALKULASI", font=("Segoe UI", 20, "bold"))
self.lbl_result_title.pack(pady=(10, 5))
self.lbl_result_time = ctk.CTkLabel(output_frame, text="-- Menunggu Input --", font=("Consolas", 12), text_color="gray")
self.lbl_result_time.pack(pady=(0, 15))
self.grid_frame = ctk.CTkFrame(output_frame, fg_color="transparent")
self.grid_frame.pack()
headers = ["OBJEK", "ALT (TOPO)", "ALT (GEO)", "AZIMUTH", "STATUS"]
for i, h in enumerate(headers):
ctk.CTkLabel(self.grid_frame, text=h, font=("Segoe UI", 13, "bold"),
width=140, height=40, fg_color=COLORS["header_bg"], corner_radius=5).grid(row=0, column=i, padx=3, pady=3)
self.cells = {}
for row, name in enumerate(["sun", "moon"], start=1):
disp_name = "MATAHARI" if name == "sun" else "BULAN"
name_color = COLORS["sun_accent"] if name == "sun" else COLORS["moon_accent"]
ctk.CTkLabel(self.grid_frame, text=disp_name, font=("Consolas", 14, "bold"),
text_color=name_color, width=140, height=50, fg_color="#3a3a3a", corner_radius=5).grid(row=row, column=0, padx=3, pady=3)
for col, key in enumerate(["alt_topo", "alt_geo", "az", "stat"], start=1):
lbl = ctk.CTkLabel(self.grid_frame, text="--", font=("Consolas", 20, "bold"),
width=140, height=50, fg_color="#3a3a3a", corner_radius=5)
lbl.grid(row=row, column=col, padx=3, pady=3)
self.cells[f"{name}_{key}"] = lbl
self.elong_frame = ctk.CTkFrame(output_frame, fg_color=COLORS["card_bg"])
self.elong_frame.pack(pady=20, fill="x", padx=20)
for i in range(4): self.elong_frame.columnconfigure(i, weight=1)
self.create_stat_box(0, "ELONGASI", "elong", "#FFA726")
self.create_stat_box(1, "ILUMINASI", "illum", "#4FC3F7")
self.create_stat_box(2, "UMUR BULAN", "age", "#E1BEE7")
self.create_stat_box(3, "EST. KOMARIYAH", "hijri", COLORS["hijri_text"])
def create_stat_box(self, col, title, suffix, color):
ctk.CTkLabel(self.elong_frame, text=title, font=("Arial", 12, "bold"), text_color="gray").grid(row=0, column=col, pady=(15,0))
lbl = ctk.CTkLabel(self.elong_frame, text="--", font=("Consolas", 26, "bold"), text_color=color)
lbl.grid(row=1, column=col, pady=(0,15))
setattr(self, f"lbl_{suffix}_val", lbl)
def fill_realtime_data(self):
self.btn_realtime.configure(state="disabled", text="Scan GPS...")
self.lbl_gps_status.configure(text="Mendeteksi...", text_color=COLORS["warning"])
now = datetime.now()
utc_offset_hr = int(now.astimezone().utcoffset().total_seconds() / 3600)
self.set_entry("Tanggal (YYYY-MM-DD)", now.strftime("%Y-%m-%d"))
self.set_entry("Waktu (HH:MM:SS)", now.strftime("%H:%M:%S"))
self.set_entry("UTC Offset", str(utc_offset_hr))
threading.Thread(target=self._fetch_gps_thread, daemon=True).start()
def fill_default_data(self):
"""Reset input ke data asli Alaska"""
for key, val in self.ALASKA_DEFAULTS.items():
self.set_entry(key, val)
self.lbl_gps_status.configure(text="Data di-reset ke Default (Alaska)", text_color=COLORS["moon_accent"])
def _fetch_gps_thread(self):
gps_data = self.gps_backend.get_windows_gps()
lat, lon = gps_data if gps_data else (-6.1754, 106.8272)
msg = "Lokasi: GPS Satelit" if gps_data else "GPS Gagal (Pakai Default JKT)"
col = COLORS["success"] if gps_data else COLORS["warning"]
self.after(0, lambda: self._update_gps_ui(lat, lon, msg, col))
def _update_gps_ui(self, lat, lon, msg, color):
self.set_entry("Latitude", str(lat))
self.set_entry("Longitude", str(lon))
self.lbl_gps_status.configure(text=msg, text_color=color)
self.btn_realtime.configure(state="normal", text="📍 REAL TIME")
def set_entry(self, key, value):
self.entries[key].delete(0, "end")
self.entries[key].insert(0, value)
def on_calculate(self):
try:
lat = float(self.entries["Latitude"].get())
lon = float(self.entries["Longitude"].get())
date_s = self.entries["Tanggal (YYYY-MM-DD)"].get()
time_s = self.entries["Waktu (HH:MM:SS)"].get()
utc_off = float(self.entries["UTC Offset"].get())
dt = datetime.strptime(f"{date_s} {time_s}", "%Y-%m-%d %H:%M:%S")
astro_time = Time(dt - timedelta(hours=utc_off))
loc = EarthLocation(lat=lat*u.deg, lon=lon*u.deg, height=0*u.m)
data = self.engine.calculate_positions(loc, astro_time)
if data:
self.lbl_result_time.configure(text=f"Ref: {dt} (UTC{'+' if utc_off>=0 else ''}{utc_off})")
self.lbl_elong_val.configure(text=f"{data['elongation']:.4f}°")
self.lbl_illum_val.configure(text=f"{data.get('illumination', 0):.1f}%")
age = data.get('moon_age', 0)
self.lbl_age_val.configure(text=f"{age:.2f} hr")
self.lbl_hijri_val.configure(text="New Moon" if int(age) == 0 else f"Tgl {int(age)}")
for name in ['sun', 'moon']:
d = data[name]
self.cells[f"{name}_alt_topo"].configure(text=f"{d['alt_topo']:.4f}°")
self.cells[f"{name}_alt_geo"].configure(text=f"{d['alt_geo']:.4f}°", text_color=COLORS["subtext"])
self.cells[f"{name}_az"].configure(text=f"{d['az_topo']:.4f}°")
stat_lbl = self.cells[f"{name}_stat"]
if d['alt_topo'] > 0: stat_lbl.configure(text="TERLIHAT", text_color=COLORS["success"])
elif d['alt_topo'] > -18: stat_lbl.configure(text="TWILIGHT", text_color=COLORS["warning"])
else: stat_lbl.configure(text="TERBENAM", text_color=COLORS["danger"])
except Exception as e:
messagebox.showerror("Error", f"Input error: {e}")
# ==========================================
# MAIN APP
# ==========================================
class IntegratedAstroApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Integrated Astro Tracker Pro")
self.geometry("900x650")
self.tabview = ctk.CTkTabview(self, width=880, height=630, fg_color=COLORS["bg"],
segmented_button_selected_color=COLORS["moon_accent"],
segmented_button_selected_hover_color=COLORS["moon_glow"],
segmented_button_unselected_color=COLORS["card_bg"])
self.tabview.pack(padx=10, pady=10, expand=True, fill="both")
self.tabview.add("Astro Tracker")
self.tabview.add("Astro Alaska")
TabTracker(self.tabview.tab("Astro Tracker")).pack(expand=True, fill="both")
TabAlaska(self.tabview.tab("Astro Alaska")).pack(expand=True, fill="both")
if __name__ == "__main__":
app = IntegratedAstroApp()
app.mainloop()