import os
import math
import datetime
import threading
import urllib.request
import ssl
import numpy as np
import pytz
import customtkinter as ctk
from tkinter import messagebox
import ephem
from skyfield.api import Loader
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# ==========================================
# FUNGSI UTILITAS & UNDUHAN
# ==========================================
def download_custom_bsp(filename, url):
filepath = os.path.join(BASE_DIR, filename)
if not os.path.exists(filepath):
try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req) as response, open(filepath, 'wb') as out_file:
out_file.write(response.read())
except Exception as e:
print(f"Gagal mengunduh {filename}: {e}")
# ==========================================
# DATABASE KOTA (Dari Master Code)
# ==========================================
CITY_DB = {
"Aceh": {"Banda Aceh": (5.5483, 95.3238), "Sabang": (5.8942, 95.3184), "Lhokseumawe": (5.1801, 97.1507)},
"Jawa Tengah": {"Semarang": (-7.0667, 110.4100), "Surakarta": (-7.5703, 110.8297)},
"DKI Jakarta": {"Jakarta Pusat": (-6.1865, 106.8270)},
"Jawa Timur": {"Surabaya": (-7.2575, 112.7521), "Malang": (-7.9797, 112.6304)},
"Jawa Barat": {"Bandung": (-6.9175, 107.6191)},
"Sumatera Barat": {"Padang": (-0.9471, 100.4172)},
"Papua": {"Jayapura": (-2.5916, 140.6690)},
"Papua Barat Daya": {"Sorong": (-0.8765, 131.2558)},
"Sulawesi Selatan": {"Makassar": (-5.1476, 119.4327)},
"Kalimantan Timur": {"Balikpapan": (-1.2654, 116.8312)},
"Selandia Baru": {"Gisborne": (-38.6623, 178.0176), "Wellington": (-41.2865, 174.7762), "Waitangi": (-35.2681, 174.0803)},
"Australia": {"Sydney": (-33.8688, 151.2093), "Perth": (-31.9505, 115.8605)},
"Jepang": {"Tokyo": (35.6762, 139.6503)},
"China": {"Beijing": (39.9042, 116.4074), "Kashgar": (39.4677, 75.9938)},
"Asia Selatan & Timteng": {
"New Delhi": (28.6139, 77.2090), "Karachi": (24.8607, 67.0011),
"Tehran": (35.6892, 51.3890), "Kabul": (34.5553, 69.2075)
},
"Arab Saudi": {"Makkah": (21.4225, 39.8262), "Riyadh": (24.7136, 46.6753)},
"Mesir": {"Kairo": (30.0444, 31.2357)},
"Maroko": {"Rabat": (34.0209, -6.8416)},
"Turki": {"Istanbul": (41.0082, 28.9784)},
"Eropa Daratan": {"Paris": (48.8566, 2.3522), "Berlin": (52.5200, 13.4050), "Roma": (41.9028, 12.4964)},
"Inggris Raya": {"London": (51.5074, -0.1278)},
"Rusia": {"Moskow": (55.7558, 37.6173), "Vladivostok": (43.1198, 131.8869)},
"Afrika Selatan": {"Cape Town": (-33.9249, 18.4241)},
"Afrika Barat": {"Dakar": (14.7167, -17.4677), "Lagos": (6.5244, 3.3792)},
"Amerika Serikat": {
"New York City": (40.7128, -74.0060), "Los Angeles": (34.0522, -118.2437),
"Anchorage": (61.2181, -149.9003)
},
"Amerika Selatan": {
"Lima": (-12.0464, -77.0428), "Santiago": (-33.4489, -70.6693),
"Sao Paulo": (-23.5505, -46.6333), "Buenos Aires": (-34.6037, -58.3816)
},
"Kepulauan Pasifik (Timur Jauh)": {"Kiritimati": (1.8709, -157.3962), "Apia": (-13.8333, -171.7667)}
}
# ==========================================
# KELAS UTAMA APLIKASI (MODUL 3)
# ==========================================
class Menu3App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Analisis Hilal Global (Modul 3)")
self.geometry("1150x700")
self.minsize(900, 600)
try: _create_unverified_https_context = ssl._create_unverified_context
except AttributeError: pass
else: ssl._create_default_https_context = _create_unverified_https_context
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
# Inisialisasi Skyfield
self.load_obj = Loader(BASE_DIR, verbose=False)
self.ts = self.load_obj.timescale()
self.eph = None
self.ephemeris_name = "de421.bsp"
self.setup_ui()
threading.Thread(target=self.load_ephemeris, daemon=True).start()
def get_header(self, width):
lines = [
"By the Name of Allah",
"KALENDER HIJRIAH GLOBAL TUNGGAL",
"KHGT Times 7.2 - Global Hilal Analyzer"
]
return "\n".join(line.center(width) for line in lines)
def load_ephemeris(self):
try:
self.lbl_status.configure(text=f"Mengunduh/Memuat ephemeris ({self.ephemeris_name})...", text_color="#00E5FF")
download_custom_bsp(self.ephemeris_name, f"https://hisabmu.com/aifikih/{self.ephemeris_name}")
self.eph = self.load_obj(self.ephemeris_name)
self.lbl_status.configure(text=f"Sistem Siap ({self.ephemeris_name} Dimuat)", text_color="#00E676")
self.btn_hitung.configure(state="normal")
except Exception as e:
self.lbl_status.configure(text="Gagal memuat ephemeris!", text_color="#FF1744")
messagebox.showerror("Error", f"Gagal memuat file ephemeris.\nDetail: {str(e)}")
def auto_switch_ephemeris(self, target_year):
ephemeris_priority = [
("de421.bsp", 1900, 2050),
("de442.bsp", 1550, 2650),
("de406.bsp", -3000, 3000)
]
best_bsp = None
for filename, min_yr, max_yr in ephemeris_priority:
if min_yr <= target_year <= max_yr and os.path.exists(os.path.join(BASE_DIR, filename)):
best_bsp = filename
break
if best_bsp is None:
for filename, _, _ in ephemeris_priority:
if os.path.exists(os.path.join(BASE_DIR, filename)):
best_bsp = filename
break
if best_bsp is None:
best_bsp = "de421.bsp"
if self.ephemeris_name != best_bsp:
try:
self.eph = self.load_obj(best_bsp)
self.ephemeris_name = best_bsp
print(f"[Engine] Berhasil beralih ke ephemeris {best_bsp} untuk tahun {target_year}")
except Exception as e:
raise RuntimeError(str(e))
def setup_ui(self):
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
now = datetime.datetime.now()
curr_y, curr_m, curr_d = str(now.year), f"{now.month:02d}", f"{now.day:02d}"
# ================= SIDEBAR KIRI =================
self.sidebar = ctk.CTkScrollableFrame(self, width=320, corner_radius=0, fg_color="#181818")
self.sidebar.grid(row=0, column=0, sticky="nsew")
ctk.CTkLabel(self.sidebar, text="GLOBAL ANALYZER", font=("Segoe UI", 24, "bold"), text_color="#00E5FF").pack(pady=(20, 15))
# 1. Tanggal Referensi UTC
frame_gha_date = ctk.CTkFrame(self.sidebar, fg_color="#212121")
frame_gha_date.pack(fill="x", padx=15, pady=5)
ctk.CTkLabel(frame_gha_date, text="TANGGAL REFERENSI (UTC)", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
gha_date_grid = ctk.CTkFrame(frame_gha_date, fg_color="transparent")
gha_date_grid.pack(fill="x", padx=10, pady=(0, 5))
ctk.CTkLabel(gha_date_grid, text="dd", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=0, pady=(0, 2))
ctk.CTkLabel(gha_date_grid, text="mm", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=1, pady=(0, 2))
ctk.CTkLabel(gha_date_grid, text="yyyy", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=2, pady=(0, 2))
self.entry_gha_day = ctk.CTkEntry(gha_date_grid, width=45, justify="center")
self.entry_gha_day.insert(0, curr_d)
self.entry_gha_day.grid(row=1, column=0, padx=(0, 5))
self.entry_gha_month = ctk.CTkEntry(gha_date_grid, width=45, justify="center")
self.entry_gha_month.insert(0, curr_m)
self.entry_gha_month.grid(row=1, column=1, padx=5)
self.entry_gha_year = ctk.CTkEntry(gha_date_grid, width=70, justify="center")
self.entry_gha_year.insert(0, curr_y)
self.entry_gha_year.grid(row=1, column=2, padx=(5, 0))
ctk.CTkLabel(frame_gha_date, text="hh:mm:ss", font=("Segoe UI", 10), text_color="#9E9E9E").pack(anchor="w", padx=12, pady=(5, 0))
self.entry_gha_time = ctk.CTkEntry(frame_gha_date, placeholder_text="Waktu HH:MM:SS (Def: 12:00:00)")
self.entry_gha_time.insert(0, "12:00:00")
self.entry_gha_time.pack(fill="x", padx=10, pady=(0, 10))
# 2. Tombol Proses
self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PROSES DATA", font=("Segoe UI", 14, "bold"), height=45, fg_color="#1565C0", hover_color="#0D47A1", command=self.run_calculation, state="disabled")
self.btn_hitung.pack(fill="x", padx=15, pady=(20, 10))
self.lbl_status = ctk.CTkLabel(self.sidebar, text="Menyiapkan Sistem...", font=("Consolas", 11), text_color="#FFAB40")
self.lbl_status.pack(pady=5)
# ================= MAIN AREA KANAN =================
self.main_frame = ctk.CTkFrame(self, fg_color="transparent")
self.main_frame.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
self.main_frame.grid_rowconfigure(1, weight=1)
self.main_frame.grid_columnconfigure(0, weight=1)
self.lbl_main_title = ctk.CTkLabel(self.main_frame, text="Global Hilal Visibility Analyzer (Iterasi Ephem)", font=("Segoe UI", 20, "bold"))
self.lbl_main_title.grid(row=0, column=0, sticky="w", pady=(0, 10))
self.textbox = ctk.CTkTextbox(self.main_frame, font=("Consolas", 13), fg_color="#101010", text_color="#00E676", wrap="none")
self.textbox.grid(row=1, column=0, sticky="nsew")
self.textbox.insert("1.0", "Silakan klik 'PROSES DATA' untuk memulai pemindaian global ke ratusan kota di seluruh benua.\n\nSistem akan mengurutkan hasilnya berdasarkan abjad negara dan kota secara otomatis.")
self.textbox.configure(state="disabled")
def run_calculation(self):
self.lbl_status.configure(text="Memindai Data Global...", text_color="#FFAB40")
self.btn_hitung.configure(state="disabled")
self.textbox.configure(state="normal")
self.textbox.delete("1.0", "end")
self.textbox.insert("1.0", "Mengeksekusi iterasi Ephemeris ke seluruh koordinat dunia...\n")
self.textbox.configure(state="disabled")
threading.Thread(target=self.calculate_global_hilal, daemon=True).start()
def calculate_global_hilal(self):
try:
y = int(self.entry_gha_year.get())
m = int(self.entry_gha_month.get())
d = int(self.entry_gha_day.get())
time_str = self.entry_gha_time.get()
if not time_str: time_str = "12:00:00"
# Pastikan Ephemeris ter-load sesuai tahun yang diminta
self.auto_switch_ephemeris(y)
tanggal_referensi_utc = f"{y}/{m}/{d} {time_str}"
matahari = ephem.Sun()
bulan = ephem.Moon()
waktu_ijtimak = ephem.previous_new_moon(tanggal_referensi_utc)
str_ijtimak = str(ephem.Date(waktu_ijtimak))
output_lines = []
header = f"{self.get_header(138)}\n"
header += f"{'[ Analisis Hilal Global (KHGT, MABIMS, Wujudul Hilal) - Ephem ]'.center(138)}\n\n"
header += f"* Tanggal Referensi Pencarian (UTC): {tanggal_referensi_utc}\n"
header += f"* Waktu Ijtimak/Konjungsi Terdekat (UTC): {str_ijtimak}\n"
header += f"* Catatan: Diurutkan Berdasarkan Abjad Negara, lalu Abjad Kota.\n"
header += "="*138 + "\n"
header += f"{'Negara':<15} | {'Kota':<14} | {'Sunset (UTC)':<16} | {'Umur Bln':<9} | {'Alt(Geo)':<8} | {'Eln(Geo)':<8} | {'Alt(Top)':<8} | {'Eln(Top)':<8} | Status\n"
header += "-"*138
output_lines.append(header)
hasil_kalkulasi = []
for negara, kota_dict in CITY_DB.items():
for nama_kota, koordinat in kota_dict.items():
lintang, bujur = koordinat
pengamat = ephem.Observer()
pengamat.lat = math.radians(lintang)
pengamat.lon = math.radians(bujur)
pengamat.elevation = 0
pengamat.pressure = 1010
pengamat.temp = 25
try:
pengamat.date = ephem.Date(ephem.Date(tanggal_referensi_utc) - 0.5)
waktu_sunset = pengamat.next_setting(matahari)
except (ephem.AlwaysUpError, ephem.NeverUpError):
hasil_kalkulasi.append({
'negara': negara,
'kota': nama_kota,
'waktu_sort': 999999.0,
'baris_teks': f"{negara[:15]:<15} | {nama_kota[:14]:<14} | {'Anomali Ekstrem':<16} | {'-':<9} | {'-':<8} | {'-':<8} | {'-':<8} | {'-':<8} | Lintang Tinggi (Midnight Sun)"
})
continue
pengamat.date = waktu_sunset
matahari.compute(pengamat)
bulan.compute(pengamat)
# 1. Parameter Toposentris (Bawaan Ephem)
alt_topo = math.degrees(bulan.alt)
elong_topo = math.degrees(ephem.separation(matahari, bulan))
# 2. Parameter Geosentris (DISELELARASKAN DENGAN SKYFIELD)
t_sunset_sky = self.ts.from_datetime(waktu_sunset.datetime().replace(tzinfo=pytz.utc))
obs_center = self.eph['earth'].at(t_sunset_sky)
s_app_sky = obs_center.observe(self.eph['sun']).apparent()
m_app_sky = obs_center.observe(self.eph['moon']).apparent()
elong_geo = s_app_sky.separation_from(m_app_sky).degrees
gmst = t_sunset_sky.gast
lst_deg = (gmst * 15.0) + bujur
ra_m, dec_m, _ = m_app_sky.radec(epoch=t_sunset_sky)
ha_deg = lst_deg - (ra_m.hours * 15.0)
ha_rad = math.radians(ha_deg)
lat_rad = math.radians(lintang)
d_rad = dec_m.radians
sin_alt_geo = math.sin(d_rad) * math.sin(lat_rad) + math.cos(d_rad) * math.cos(lat_rad) * math.cos(ha_rad)
alt_geo = math.degrees(math.asin(max(-1.0, min(1.0, sin_alt_geo))))
umur_bulan_desimal = (waktu_sunset - waktu_ijtimak) * 24
if umur_bulan_desimal < 0:
format_umur = "B. Ijtimak"
status_visibilitas = "Negatif (Bulan Tua)"
else:
jam = int(umur_bulan_desimal)
menit = int((umur_bulan_desimal - jam) * 60)
format_umur = f"{jam}j {menit}m"
# EVALUASI STATUS BERJENJANG
if alt_geo >= 5.0 and elong_geo >= 8.0:
status_visibilitas = "Memenuhi Kriteria KHGT"
elif alt_topo >= 3.0 and elong_topo >= 6.4:
status_visibilitas = "Memenuhi Kriteria MABIMS"
elif alt_geo > 0 or alt_topo > 0:
status_visibilitas = "Wujudul Hilal"
else:
status_visibilitas = "Negatif (Bawah Ufuk)"
alt_g_str = f"{alt_geo:.2f}°"
elong_g_str = f"{elong_geo:.2f}°"
alt_t_str = f"{alt_topo:.2f}°"
elong_t_str = f"{elong_topo:.2f}°"
sunset_str = str(ephem.Date(waktu_sunset))
baris_teks = f"{negara[:15]:<15} | {nama_kota[:14]:<14} | {sunset_str[:16]:<16} | {format_umur[:9]:<9} | {alt_g_str[:8]:<8} | {elong_g_str[:8]:<8} | {alt_t_str[:8]:<8} | {elong_t_str[:8]:<8} | {status_visibilitas}"
hasil_kalkulasi.append({
'negara': negara,
'kota': nama_kota,
'waktu_sort': waktu_sunset,
'baris_teks': baris_teks
})
# Sorting berdasarkan abjad Negara dan Kota
hasil_kalkulasi.sort(key=lambda x: (x['negara'].lower(), x['kota'].lower()))
for hasil in hasil_kalkulasi:
output_lines.append(hasil['baris_teks'])
output_lines.append("="*138)
self.after(0, self.display_result, "\n".join(output_lines))
except Exception as e:
import traceback
self.after(0, self.display_error, f"{str(e)}\n\n{traceback.format_exc()}")
def display_result(self, report_text):
self.textbox.configure(state="normal")
self.textbox.delete("1.0", "end")
self.textbox.insert("1.0", report_text)
self.textbox.configure(state="disabled")
self.lbl_status.configure(text="Kalkulasi Selesai", text_color="#00E676")
self.btn_hitung.configure(state="normal")
def display_error(self, error_msg):
self.textbox.configure(state="normal")
self.textbox.delete("1.0", "end")
self.textbox.insert("1.0", f"TERJADI KESALAHAN:\n{error_msg}")
self.textbox.configure(state="disabled")
self.lbl_status.configure(text="Error Kalkulasi", text_color="#FF1744")
self.btn_hitung.configure(state="normal")
if __name__ == "__main__":
app = Menu3App()
app.mainloop()