import os, datetime, math
import numpy as np
import customtkinter as ctk
from skyfield.api import Loader, wgs84
from tkinter import messagebox
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def safe_monthrange(year, month):
is_leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
days_in_month = [31, 29 if is_leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
return 0, days_in_month[month - 1]
class Menu12App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Prayer Times (Modul 12)")
self.geometry("1100x600")
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 setup_ui(self):
self.sidebar = ctk.CTkFrame(self, width=250)
self.sidebar.pack(side="left", fill="y", padx=10, pady=10)
now = datetime.datetime.now()
self.entry_y = ctk.CTkEntry(self.sidebar, placeholder_text="Tahun"); self.entry_y.insert(0, str(now.year)); self.entry_y.pack(pady=2)
self.entry_m = ctk.CTkEntry(self.sidebar, placeholder_text="Bulan"); self.entry_m.insert(0, str(now.month)); self.entry_m.pack(pady=2)
self.entry_d = ctk.CTkEntry(self.sidebar, placeholder_text="Tanggal"); self.entry_d.insert(0, str(now.day)); self.entry_d.pack(pady=2)
self.entry_lat = ctk.CTkEntry(self.sidebar, placeholder_text="Latitude"); self.entry_lat.insert(0, "-7.0667"); self.entry_lat.pack(pady=2)
self.entry_lon = ctk.CTkEntry(self.sidebar, placeholder_text="Longitude"); self.entry_lon.insert(0, "110.4100"); self.entry_lon.pack(pady=2)
self.entry_tz = ctk.CTkEntry(self.sidebar, placeholder_text="Timezone"); self.entry_tz.insert(0, "7.0"); self.entry_tz.pack(pady=2)
self.entry_elev = ctk.CTkEntry(self.sidebar, placeholder_text="Elevasi"); self.entry_elev.insert(0, "230"); self.entry_elev.pack(pady=2)
self.combo_method = ctk.CTkOptionMenu(self.sidebar, values=["Kemenag RI (Fajr 20°, Isha 18°)", "MUHAMMADIYAH (Fajr 18°, Isha 18°)"])
self.combo_method.pack(pady=5)
ctk.CTkButton(self.sidebar, text="▶ HITUNG JADWAL", command=self.hitung).pack(pady=15)
self.textbox = ctk.CTkTextbox(self, font=("Consolas", 12), wrap="none")
self.textbox.pack(side="right", fill="both", expand=True, padx=10, pady=10)
def hitung(self):
self.textbox.delete("1.0", "end")
try:
y, m, d = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
lat, lon, tz, elev = float(self.entry_lat.get()), float(self.entry_lon.get()), float(self.entry_tz.get()), float(self.entry_elev.get())
except ValueError:
return
method = self.combo_method.get()
if "Kemenag" in method: f_ang, i_ang = -20.0, -18.0
else: f_ang, i_ang = -18.0, -18.0
loc = wgs84.latlon(lat, lon, elevation_m=elev)
earth, sun = self.eph['earth'], self.eph['sun']
days_to_calc = [d] # Mode Harian
pad = 8
self.textbox.insert("end", f"{'Date':<12} {'Fajer':^{pad}} {'Shuroq':^{pad}} {'Dhuha':^{pad}} {'Dhohur':^{pad}} {'Aser':^{pad}} {'Maghreb':^{pad}} {'Isha':^{pad}}\n{'-'*90}\n")
def find_crossing(alt_arr, tt_arr, target_alt, direction='up'):
diffs = alt_arr - target_alt
for i in range(len(diffs)-1):
if direction == 'up' and diffs[i] <= 0 and diffs[i+1] > 0:
f = abs(diffs[i]) / (abs(diffs[i]) + abs(diffs[i+1]) + 1e-9)
return tt_arr[i] + f * (tt_arr[i+1] - tt_arr[i])
elif direction == 'down' and diffs[i] >= 0 and diffs[i+1] < 0:
f = abs(diffs[i]) / (abs(diffs[i]) + abs(diffs[i+1]) + 1e-9)
return tt_arr[i] + f * (tt_arr[i+1] - tt_arr[i])
return None
for d_c in days_to_calc:
t0 = self.ts.utc(y, m, d_c, -int(tz))
t1 = self.ts.utc(y, m, d_c, 24 - int(tz))
tt_array = np.linspace(t0.tt, t1.tt, 2880) # Resolusi tinggi per menit
alt_deg = (earth + loc).at(self.ts.tt_jd(tt_array)).observe(sun).apparent().altaz(temperature_C=25, pressure_mbar=1010)[0].degrees
idx_noon = np.argmax(alt_deg)
t_dhohur = tt_array[idx_noon]
alt_am, tt_am = alt_deg[:idx_noon], tt_array[:idx_noon]
alt_pm, tt_pm = alt_deg[idx_noon:], tt_array[idx_noon:]
shadow_asr = 1.0 + math.tan(math.radians(max(0, 90.0 - alt_deg[idx_noon]))) # Standard Shafii
alt_asr_tgt = math.degrees(math.atan(1.0 / shadow_asr))
v_fajr = find_crossing(alt_am, tt_am, f_ang, 'up')
v_shu = find_crossing(alt_am, tt_am, -0.833, 'up')
v_dhu = find_crossing(alt_am, tt_am, 4.5, 'up')
v_asr = find_crossing(alt_pm, tt_pm, alt_asr_tgt, 'down')
v_mag = find_crossing(alt_pm, tt_pm, -0.833, 'down')
v_ish = find_crossing(alt_pm, tt_pm, i_ang, 'down')
def fmt(val, is_sh=False):
if val is None: return "----"
adj = (16 / 86400.0) # Ikhtiyat 16 detik
val += (-adj if is_sh else adj)
dt = self.ts.tt_jd(val + (tz/24.0))
return f"{int(dt.utc[3]):02d}:{int(dt.utc[4]):02d}:{int(dt.utc[5]):02d}"
line = f"{d_c:02d}/{m:02d}/{y} {fmt(v_fajr):^{pad}} {fmt(v_shu, True):^{pad}} {fmt(v_dhu):^{pad}} {fmt(t_dhohur):^{pad}} {fmt(v_asr):^{pad}} {fmt(v_mag):^{pad}} {fmt(v_ish):^{pad}}\n"
self.textbox.insert("end", line)
if __name__ == "__main__":
app = Menu12App()
app.mainloop()