import os, datetime, threading, json
import customtkinter as ctk
from tkinter import messagebox
from skyfield.api import Loader
from skyfield import almanac
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BULAN_HIJRIAH = ["Muharam", "Safar", "Rabiulawal", "Rabiulakhir", "Jumadilawal", "Jumadilakhir", "Rajab", "Syakban", "Ramadan", "Syawal", "Zulkaidah", "Zulhijah"]
hari_nama = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Ahad"]
bulan_masehi_singkat = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
class Menu30App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("HIJRI_DB Auto-Builder (Modul 30)")
self.geometry("900x600")
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()
def get_approx_nm_tt(self, y, m):
abs_month = (y - 1) * 12 + m
return 2451550.0 + (abs_month - 17038) * 29.530588853
def get_new_moons_in_range(self, start_tt_float, end_tt_float):
t0 = self.ts.tt_jd(start_tt_float)
t1 = self.ts.tt_jd(end_tt_float)
t_obj, y_obj = almanac.find_discrete(t0, t1, almanac.moon_phases(self.eph))
if t_obj is None: return []
if getattr(t_obj, 'shape', ()) == ():
tt_list, y_list = [float(t_obj.tt)], [int(y_obj)]
else:
tt_list, y_list = t_obj.tt.tolist(), y_obj.tolist()
new_moons_tt = []
for i in range(len(y_list)):
if y_list[i] == 0: new_moons_tt.append(tt_list[i])
return new_moons_tt
def calculate_khgt_1st_of_month(self, nm_tt_float):
t_nm = self.ts.tt_jd(nm_tt_float)
y, m, d, h, mi, s = t_nm.utc
t_midnight = self.ts.utc(y, m, d)
# Logika PKG KHGT Murni (Sesuai batas Fajar Gisborne = Jam 15 UTC)
if h < 15: return t_midnight.tt + 1.0
else: return t_midnight.tt + 2.0
def setup_ui(self):
self.sidebar = ctk.CTkFrame(self, width=250)
self.sidebar.pack(side="left", fill="y", padx=10, pady=10)
ctk.CTkLabel(self.sidebar, text="DATABASE BUILDER\n(KHGT ENGINE)", font=("Segoe UI", 16, "bold"), text_color="#FFD54F").pack(pady=20)
ctk.CTkLabel(self.sidebar, text="Tahun Awal (H):").pack()
self.entry_start = ctk.CTkEntry(self.sidebar); self.entry_start.insert(0, "1447"); self.entry_start.pack(pady=5)
ctk.CTkLabel(self.sidebar, text="Tahun Akhir (H):").pack()
self.entry_end = ctk.CTkEntry(self.sidebar); self.entry_end.insert(0, "1496"); self.entry_end.pack(pady=5)
self.btn_hitung = ctk.CTkButton(self.sidebar, text="⚙️ GENERATE DB JSON", fg_color="#F57C00", hover_color="#E65100", command=self.mulai_build, height=45)
self.btn_hitung.pack(pady=30, fill="x", padx=15)
self.lbl_status = ctk.CTkLabel(self.sidebar, text="Sistem Siap", text_color="#00E676")
self.lbl_status.pack()
self.textbox = ctk.CTkTextbox(self, font=("Consolas", 14), wrap="none")
self.textbox.pack(side="right", fill="both", expand=True, padx=10, pady=10)
self.textbox.insert("1.0", "Klik tombol di samping untuk menghasilkan dataset Kalender KHGT selama 50 tahun ke depan.\n\nSistem akan merekonstruksi ulang penanggalan berbasis Ephemeris NASA, lalu mengekspornya sebagai 'db_hijriah.json'.")
self.textbox.configure(state="disabled")
def mulai_build(self):
threading.Thread(target=self._build_hijri_db_thread, daemon=True).start()
def _build_hijri_db_thread(self):
try:
start_year = int(self.entry_start.get())
end_year = int(self.entry_end.get())
except ValueError:
self.after(0, lambda: messagebox.showerror("Error", "Tahun harus berupa angka."))
return
self.after(0, lambda: self.lbl_status.configure(text=f"Membangun DB {start_year}-{end_year}...", text_color="#00E5FF"))
self.after(0, lambda: self.textbox.configure(state="normal"))
self.after(0, lambda: self.textbox.delete("1.0", "end"))
self.after(0, lambda: self.textbox.insert("end", f"Memulai iterasi mesin KHGT dari tahun {start_year} H hingga {end_year} H...\nProses ini memerlukan waktu pemrosesan CPU, mohon tunggu...\n\n"))
self.after(0, lambda: self.btn_hitung.configure(state="disabled"))
new_db = {}
try:
for y_h in range(start_year, end_year + 1):
year_data = []
for m_h in range(1, 13):
approx_tt_curr = self.get_approx_nm_tt(y_h, m_h)
nm_list_curr = self.get_new_moons_in_range(approx_tt_curr - 5.0, approx_tt_curr + 5.0)
if not nm_list_curr: raise ValueError(f"Fase Bulan Baru tidak ditemukan untuk {y_h}-{m_h}")
nm_curr = nm_list_curr[0]
start_tt_curr = self.calculate_khgt_1st_of_month(nm_curr)
next_m, next_y = m_h + 1, y_h
if next_m > 12: next_m, next_y = 1, y_h + 1
approx_tt_next = self.get_approx_nm_tt(next_y, next_m)
nm_list_next = self.get_new_moons_in_range(approx_tt_next - 5.0, approx_tt_next + 5.0)
nm_next = nm_list_next[0]
start_tt_next = self.calculate_khgt_1st_of_month(nm_next)
jumlah_hari = int(round(start_tt_next - start_tt_curr))
t_obj = self.ts.tt_jd(start_tt_curr)
gy, gm, gd, _, _, _ = t_obj.utc
nama_h = hari_nama[int(t_obj.whole % 7)]
tgl_str = f"{int(gd):02d}-{bulan_masehi_singkat[int(gm)-1]}-{int(gy)}"
year_data.append([BULAN_HIJRIAH[m_h-1], nama_h, tgl_str, jumlah_hari])
new_db[y_h] = year_data
self.after(0, lambda yr=y_h: self.textbox.insert("end", f"✓ Tahun {yr} H berhasil diproses...\n"))
self.after(0, lambda: self.textbox.see("end"))
db_path = os.path.join(BASE_DIR, "db_hijriah.json")
with open(db_path, "w", encoding="utf-8") as f:
json.dump(new_db, f, indent=4, ensure_ascii=False)
self.after(0, lambda: self.textbox.insert("end", f"\n[+] SUKSES! Database JSON KHGT berhasil dibangun dan disimpan ke:\n{db_path}\nData ini dapat dibaca oleh Modul Kalender (Menu 26 & 27)."))
self.after(0, lambda: self.lbl_status.configure(text="Selesai", text_color="#00E676"))
except Exception as e:
self.after(0, lambda err=str(e): self.textbox.insert("end", f"\nERROR: {err}"))
self.after(0, lambda: self.lbl_status.configure(text="Auto-Builder Gagal", text_color="#FF1744"))
finally:
self.after(0, lambda: self.btn_hitung.configure(state="normal"))
self.after(0, lambda: self.textbox.configure(state="disabled"))
if __name__ == "__main__":
app = Menu30App()
app.mainloop()