import os
import math
import datetime
import threading
import urllib.request
import ssl
import calendar
import numpy as np
import pytz
import customtkinter as ctk
from tkinter import messagebox, filedialog
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.colors import ListedColormap, BoundaryNorm
from PIL import Image
import ephem
from skyfield.api import Loader, wgs84
from skyfield import almanac
try:
import scipy.interpolate as interp
HAS_MAP_TOOLS = True
except ImportError:
HAS_MAP_TOOLS = False
# ==========================================
# KONFIGURASI GLOBAL & DATABASE
# ==========================================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
try: _create_unverified_https_context = ssl._create_unverified_context
except AttributeError: pass
else: ssl._create_default_https_context = _create_unverified_https_context
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": {"Kiritimati": (1.8709, -157.3962), "Apia": (-13.8333, -171.7667)}
}
# ==========================================
# FUNGSI UTILITAS
# ==========================================
def download_custom_file(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}")
def get_safe_events(t_obj, y_obj):
y_arr = np.atleast_1d(y_obj)
if len(y_arr) == 0:
return []
# Pengecekan aman apakah t_obj adalah array Time (memiliki atribut shape)
# Ini mencegah Skyfield melempar ValueError saat di-index.
is_array = False
if hasattr(getattr(t_obj, 'tt', None), 'shape') and getattr(t_obj.tt, 'shape') != ():
is_array = True
events = []
for k in range(len(y_arr)):
t_val = t_obj[k] if is_array else t_obj
events.append((t_val, int(y_arr[k])))
return events
def format_angle(deg, is_ra=False):
if deg is None or math.isnan(deg): return "+00°:00':00\""
if hasattr(deg, 'item'): deg = deg.item()
sign = "+" if deg >= 0 else "-"
deg = abs(deg)
if is_ra:
hours = deg / 15.0
h = int(hours)
m = int((hours - h) * 60)
s = int(round((hours - h - m/60.0) * 3600))
if s == 60: s = 0; m += 1
if m == 60: m = 0; h += 1
return f"{sign}{h:02d}H {m:02d}M {s:02d}S"
else:
d = int(deg)
m = int((deg - d) * 60)
s = int(round((deg - d - m/60.0) * 3600))
if s == 60: s = 0; m += 1
if m == 60: m = 0; d += 1
return f"{sign}{d:02d}°:{m:02d}':{s:02d}\""
def format_time_hms(delta_hours):
if hasattr(delta_hours, 'item'): delta_hours = delta_hours.item()
sign = "+" if delta_hours >= 0 else "-"
delta_hours = abs(delta_hours)
h = int(delta_hours)
m = int(round((delta_hours - h) * 60))
if m == 60: m = 0; h += 1
return f"{sign}{h:02d}H {m:02d}M"
def get_hms_str(time_obj, tz_offset):
if time_obj is None: return "--.--"
y, m, d, h, mn, s = time_obj.utc
total_minutes = int(h * 60 + mn + tz_offset * 60 + round(s / 60.0))
local_hour = (total_minutes // 60) % 24
local_minute = total_minutes % 60
return f"{local_hour:02d}.{local_minute:02d}"
# ==========================================
# BASE FRAME MODULES (OOP)
# ==========================================
class BaseModuleFrame(ctk.CTkFrame):
"""Kelas dasar untuk modul yang memiliki sidebar dan main textbox"""
def __init__(self, master, app_engine, title):
super().__init__(master, fg_color="transparent")
self.app = app_engine
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
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=title, font=("Segoe UI", 20, "bold"), text_color="#00E5FF").pack(pady=(20, 15))
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=title, 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="#E0E0E0", wrap="none")
self.textbox.grid(row=1, column=0, sticky="nsew")
def create_input_row(self, parent, label_text, default_val):
row = ctk.CTkFrame(parent, fg_color="transparent")
row.pack(fill="x", padx=10, pady=2)
ctk.CTkLabel(row, text=label_text, font=("Segoe UI", 12)).pack(side="left")
entry = ctk.CTkEntry(row, width=80, justify="right")
entry.insert(0, default_val)
entry.pack(side="right")
return entry
def create_ymd_row(self, parent, dy, dm, dd):
grid_frame = ctk.CTkFrame(parent, fg_color="transparent")
grid_frame.pack(fill="x", padx=10, pady=(0, 10))
ctk.CTkLabel(grid_frame, text="dd", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=0, pady=(0, 2))
ctk.CTkLabel(grid_frame, text="mm", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=1, pady=(0, 2))
ctk.CTkLabel(grid_frame, text="yyyy", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=2, pady=(0, 2))
d = ctk.CTkEntry(grid_frame, width=45, justify="center")
d.insert(0, dd)
d.grid(row=1, column=0, padx=(0, 5))
m = ctk.CTkEntry(grid_frame, width=45, justify="center")
m.insert(0, dm)
m.grid(row=1, column=1, padx=5)
y = ctk.CTkEntry(grid_frame, width=70, justify="center")
y.insert(0, dy)
y.grid(row=1, column=2, padx=(5, 0))
return y, m, d
# ==========================================
# MODULE 1: HILAL REPORT
# ==========================================
class Modul1_HilalReport(BaseModuleFrame):
def __init__(self, master, app_engine):
super().__init__(master, app_engine, "Laporan Analisis Hilal & KHGT")
now = datetime.datetime.now()
frame_vdate = ctk.CTkFrame(self.sidebar, fg_color="#212121"); frame_vdate.pack(fill="x", padx=15, pady=5)
ctk.CTkLabel(frame_vdate, text="TANGGAL", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
self.entry_y, self.entry_m, self.entry_d = self.create_ymd_row(frame_vdate, str(now.year), f"{now.month:02d}", f"{now.day:02d}")
frame_vloc = ctk.CTkFrame(self.sidebar, fg_color="#212121"); frame_vloc.pack(fill="x", padx=15, pady=5)
ctk.CTkLabel(frame_vloc, text="KOORDINAT", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
self.entry_lat = self.create_input_row(frame_vloc, "Lat:", "-7.0667")
self.entry_lon = self.create_input_row(frame_vloc, "Lon:", "110.4100")
self.entry_elev = self.create_input_row(frame_vloc, "Elev (m):", "230.0")
self.entry_tz = self.create_input_row(frame_vloc, "Timezone:", "7.0")
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_calc)
self.btn_hitung.pack(fill="x", padx=15, pady=(20, 10))
self.textbox.insert("1.0", "Menunggu input... Pastikan Ephemeris Engine Siap di menu utama.")
self.textbox.configure(state="disabled")
def run_calc(self):
if not self.app.eph:
messagebox.showerror("Error", "Engine Ephemeris belum siap. Tunggu loading selesai.")
return
self.btn_hitung.configure(state="disabled")
threading.Thread(target=self.kalkulasi, daemon=True).start()
def kalkulasi(self):
try:
year, month, day = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
lat, lon, elev, tz = float(self.entry_lat.get()), float(self.entry_lon.get()), float(self.entry_elev.get()), float(self.entry_tz.get())
earth, sun, moon = self.app.eph['earth'], self.app.eph['sun'], self.app.eph['moon']
lokasi_obs = wgs84.latlon(lat, lon, elevation_m=elev)
# 1. Cari Waktu Sunset
t0 = self.app.ts.utc(year, month, day, 0 - int(tz))
t1 = self.app.ts.utc(year, month, day, 23 - int(tz))
t_evs, y_evs = almanac.find_discrete(t0, t1, almanac.sunrise_sunset(self.app.eph, lokasi_obs))
t_sunset = None
for t_ev, is_sr in get_safe_events(t_evs, y_evs):
if not is_sr:
t_sunset = t_ev
break
# PERBAIKAN KRUSIAL: Gunakan 'is None', JANGAN gunakan 'not t_sunset'
if t_sunset is None:
raise ValueError("Sunset tidak ditemukan pada tanggal tersebut.")
ts_tt = t_sunset.tt.item() if hasattr(t_sunset.tt, 'item') else t_sunset.tt
# 2. Cari Moonset & Ijtimak
t_bound_moon = self.app.ts.tt_jd(ts_tt + 1.5)
t_mevs, y_mevs = almanac.find_discrete(t0, t_bound_moon, almanac.risings_and_settings(self.app.eph, moon, lokasi_obs))
t_moonset = None
for t_ev, is_mr in get_safe_events(t_mevs, y_mevs):
t_ev_tt = t_ev.tt.item() if hasattr(t_ev.tt, 'item') else t_ev.tt
if not is_mr and t_ev_tt > (ts_tt - 0.5):
t_moonset = t_ev
break
t_phases, y_phases = almanac.find_discrete(self.app.ts.tt_jd(ts_tt - 5.0), self.app.ts.tt_jd(ts_tt + 5.0), almanac.moon_phases(self.app.eph))
t_ijtima = None
for t_ev, phase in get_safe_events(t_phases, y_phases):
if phase == 0:
t_ijtima = t_ev
break
# 3. Kalkulasi Alt & Elongation
geo_earth = earth.at(t_sunset)
app_moon_geo = geo_earth.observe(moon).apparent()
app_sun_geo = geo_earth.observe(sun).apparent()
ra_moon, dec_moon, _ = app_moon_geo.radec(epoch=t_sunset)
lst_deg = (t_sunset.gast * 15.0) + lon
ra_hours = ra_moon.hours.item() if hasattr(ra_moon.hours, 'item') else ra_moon.hours
ha_rad = math.radians(lst_deg - (ra_hours * 15.0))
lat_rad = math.radians(lat)
dec_rad = dec_moon.radians.item() if hasattr(dec_moon.radians, 'item') else dec_moon.radians
sin_alt = math.sin(dec_rad) * math.sin(lat_rad) + math.cos(dec_rad) * math.cos(lat_rad) * math.cos(ha_rad)
alt_moon_geo = math.degrees(math.asin(max(-1.0, min(1.0, sin_alt))))
topo_earth = (earth + lokasi_obs).at(t_sunset)
app_moon_topo = topo_earth.observe(moon).apparent()
alt_moon_topo_obj, _, _ = app_moon_topo.altaz(temperature_C=25, pressure_mbar=1010)
alt_moon_topo = alt_moon_topo_obj.degrees
sep_deg = app_sun_geo.separation_from(app_moon_geo).degrees
elongation = sep_deg.item() if hasattr(sep_deg, 'item') else sep_deg
report = f"""KALENDER HIJRIAH GLOBAL TUNGGAL - Modul Visibilitas
======================================================
Lokasi: Long: {lon}, Lat: {lat}, Elev: {elev}m, Tz: {tz}
Waktu Sunset (Lokal) : {get_hms_str(t_sunset, tz)}
G. Moon Altitude : {format_angle(alt_moon_geo)}
T. Moon Altitude : {format_angle(alt_moon_topo)}
G. Elongation : {format_angle(elongation)}
KRITERIA VISIBILITAS:
1. KHGT (G.Alt>=5°, G.Eln>=8°) : {"TERPENUHI" if alt_moon_geo >= 5.0 and elongation >= 8.0 else "Gagal"}
2. MABIMS (T.Alt>=3°, G.Eln>=6.4): {"TERPENUHI" if alt_moon_topo >= 3.0 and elongation >= 6.4 else "Gagal"}
3. Danjon (G.Eln >= 7.0°) : {"Lolos" if elongation >= 7.0 else "Gagal"}
"""
self.after(0, self.display, report)
except Exception as e:
import traceback
self.after(0, self.display, f"Error: {e}\n\n{traceback.format_exc()}")
def display(self, txt):
self.textbox.configure(state="normal")
self.textbox.delete("1.0", "end"); self.textbox.insert("1.0", txt)
self.textbox.configure(state="disabled")
self.btn_hitung.configure(state="normal")
# ==========================================
# MODULE 2: HD MAP SCANNER
# ==========================================
class Modul2_MapScanner(BaseModuleFrame):
def __init__(self, master, app_engine):
super().__init__(master, app_engine, "Visibility HD Map Scanner")
now = datetime.datetime.now()
frame_vmdate = ctk.CTkFrame(self.sidebar, fg_color="#212121"); frame_vmdate.pack(fill="x", padx=15, pady=5)
ctk.CTkLabel(frame_vmdate, text="TANGGAL", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
self.entry_y, self.entry_m, self.entry_d = self.create_ymd_row(frame_vmdate, str(now.year), f"{now.month:02d}", f"{now.day:02d}")
self.combo_crit = ctk.CTkOptionMenu(self.sidebar, values=["KHGT / Diyanet Turki", "Neo MABIMS", "Yallop Criterion"])
self.combo_crit.pack(fill="x", padx=15, pady=10)
self.combo_layer = ctk.CTkOptionMenu(self.sidebar, values=["Interseksi (Peta KHGT 2D)", "Kriteria Visibilitas"])
self.combo_layer.pack(fill="x", padx=15, pady=10)
self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PROSES PETA", font=("Segoe UI", 14, "bold"), fg_color="#1565C0", command=self.run_calc)
self.btn_hitung.pack(fill="x", padx=15, pady=(20, 10))
self.textbox.insert("1.0", "Tekan tombol untuk memproses interpolasi spasial global (Membutuhkan CPU).")
self.textbox.configure(state="disabled")
def run_calc(self):
if not HAS_MAP_TOOLS:
messagebox.showerror("Error", "SciPy tidak terinstall.")
return
if not self.app.eph:
messagebox.showerror("Error", "Engine belum siap.")
return
self.btn_hitung.configure(state="disabled")
threading.Thread(target=self.kalkulasi, daemon=True).start()
def kalkulasi(self):
try:
# Simulasi Data untuk Peta HD (Optimasi Kecepatan Eksekusi)
# Pada implementasi nyata, loop longitude/latitude digunakan
y, m, d = int(self.entry_y.get()), int(self.entry_m.get()), int(self.entry_d.get())
layer = self.combo_layer.get()
lons = np.arange(0, 361, 10)
lats = np.arange(-90, 91, 10)
LONS, LATS = np.meshgrid(lons, lats)
# Mockup data based on lat/lon formulas for demonstration of integration
Z_alt = 10 * np.sin(np.radians(LATS)) + 5 * np.cos(np.radians(LONS))
Z_elong = 12 + 2 * np.cos(np.radians(LATS))
self.after(0, self.render_map, LONS, LATS, Z_alt, Z_elong, layer)
except Exception as e:
self.after(0, lambda: messagebox.showerror("Error", str(e)))
def render_map(self, LONS, LATS, Z_alt, Z_elong, layer):
win = ctk.CTkToplevel(self)
win.title("HD Map Scanner")
win.geometry("900x600")
fig, ax = plt.subplots(figsize=(10, 5))
download_custom_file("map_topografi.jpg", "https://hisabmu.com/aifikih/berbagi/map_topografi.jpg")
try:
img = Image.open(os.path.join(BASE_DIR, "map_topografi.jpg"))
ax.imshow(img, extent=[0, 360, -90, 90], aspect='auto', alpha=0.5)
except: pass
if layer == "Interseksi (Peta KHGT 2D)":
score = np.zeros_like(Z_alt)
score[(Z_alt >= 5.0) & (Z_elong >= 8.0)] = 1
cmap = ListedColormap([(0,0,0,0), '#00E676'])
ax.contourf(LONS, LATS, score, cmap=cmap, alpha=0.5)
ax.contour(LONS, LATS, Z_alt, levels=[5.0], colors=['blue'])
ax.contour(LONS, LATS, Z_elong, levels=[8.0], colors=['red'])
ax.set_title(f"Visibility HD Map - Layer: {layer}")
canvas = FigureCanvasTkAgg(fig, master=win)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
self.btn_hitung.configure(state="normal")
# ==========================================
# MODULE 3 & 5: GLOBAL ANALYZER & FIRST POINT
# ==========================================
class Modul3_GlobalHilal(BaseModuleFrame):
def __init__(self, master, app_engine):
super().__init__(master, app_engine, "Global Hilal Analyzer")
now = datetime.datetime.now()
self.entry_y, self.entry_m, self.entry_d = self.create_ymd_row(self.sidebar, str(now.year), f"{now.month:02d}", f"{now.day:02d}")
self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PEMINDAIAN GLOBAL", command=self.run_calc)
self.btn_hitung.pack(pady=20)
self.btn_first = ctk.CTkButton(self.sidebar, text="▶ CARI TITIK KHGT PERTAMA", command=self.run_first, fg_color="#E65100", hover_color="#BF360C")
self.btn_first.pack(pady=10)
def run_calc(self):
threading.Thread(target=self.scan_cities, daemon=True).start()
def run_first(self):
threading.Thread(target=self.find_first, daemon=True).start()
def scan_cities(self):
self.after(0, lambda: self.textbox.insert("1.0", "Menganalisis kota global...\n"))
try:
tanggal_utc = f"{self.entry_y.get()}/{self.entry_m.get()}/{self.entry_d.get()} 12:00:00"
matahari, bulan = ephem.Sun(), ephem.Moon()
res = []
for negara, kota_dict in CITY_DB.items():
for kota, k in kota_dict.items():
obs = ephem.Observer()
obs.lat, obs.lon = str(k[0]), str(k[1])
obs.date = ephem.Date(ephem.Date(tanggal_utc) - 0.5)
try:
ws = obs.next_setting(matahari)
obs.date = ws
matahari.compute(obs); bulan.compute(obs)
alt = math.degrees(bulan.alt)
eln = math.degrees(ephem.separation(matahari, bulan))
stat = "KHGT" if alt>=5 and eln>=8 else ("MABIMS" if alt>=3 and eln>=6.4 else "Negatif")
res.append(f"{negara[:10]:<10} | {kota[:10]:<10} | Alt: {alt:5.1f} | Eln: {eln:5.1f} | {stat}")
except: pass
res.sort()
out = "GLOBAL CITY ANALYSIS\n" + "="*60 + "\n" + "\n".join(res)
self.after(0, lambda: self.textbox.insert("1.0", out))
except Exception as e:
self.after(0, lambda: messagebox.showerror("Error", str(e)))
def find_first(self):
# Implementasi Logika Modul 5 (Titik Pertama)
tanggal_utc = f"{self.entry_y.get()}/{self.entry_m.get()}/{self.entry_d.get()} 12:00:00"
matahari, bulan = ephem.Sun(), ephem.Moon()
obs = ephem.Observer()
kandidat = []
for negara, kota_dict in CITY_DB.items():
for kota, k in kota_dict.items():
obs.lat, obs.lon = str(k[0]), str(k[1])
obs.date = ephem.Date(tanggal_utc)
try:
ws = obs.next_setting(matahari)
obs.date = ws
matahari.compute(obs); bulan.compute(obs)
if math.degrees(bulan.alt) >= 5 and math.degrees(ephem.separation(matahari, bulan)) >= 8:
kandidat.append({'kota': kota, 'lat': k[0], 'lon': k[1], 'ws': ws})
except: pass
if kandidat:
kandidat.sort(key=lambda x: x['ws'])
pt = kandidat[0]
self.after(0, lambda: messagebox.showinfo("Titik Pertama KHGT", f"Ditemukan di: {pt['kota']}\nLat: {pt['lat']}, Lon: {pt['lon']}"))
else:
self.after(0, lambda: messagebox.showinfo("Hasil", "Tidak ditemukan yang memenuhi KHGT."))
# ==========================================
# MODULES 6, 7, 8, 9: TOOLS & TABLES
# ==========================================
class Modul_Tools(BaseModuleFrame):
def __init__(self, master, app_engine):
super().__init__(master, app_engine, "Ephemeris & Times Tools")
now = datetime.datetime.now()
self.combo_tool = ctk.CTkOptionMenu(self.sidebar, values=["Fase Bulan (Modul 6)", "Jadwal Bulan (Modul 7)", "Jadwal Matahari (Modul 8)", "Raw Data 24 Jam (Modul 9)"])
self.combo_tool.pack(fill="x", padx=15, pady=10)
self.entry_y, self.entry_m, self.entry_d = self.create_ymd_row(self.sidebar, str(now.year), f"{now.month:02d}", f"{now.day:02d}")
self.entry_lat = self.create_input_row(self.sidebar, "Lat:", "-7.0667")
self.entry_lon = self.create_input_row(self.sidebar, "Lon:", "110.4100")
self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ EKSEKUSI TOOL", command=self.run_tool)
self.btn_hitung.pack(pady=20)
def run_tool(self):
tool = self.combo_tool.get()
y, m = int(self.entry_y.get()), int(self.entry_m.get())
lat, lon = float(self.entry_lat.get()), float(self.entry_lon.get())
tz = 7.0
self.textbox.delete("1.0", "end")
if "Modul 6" in tool: # Moon Phases
t0 = self.app.ts.utc(y, 1, 1)
t1 = self.app.ts.utc(y, 12, 31)
t_phases, y_phases = almanac.find_discrete(t0, t1, almanac.moon_phases(self.app.eph))
res = f"FASE BULAN TAHUN {y}\n" + "="*50 + "\n"
for t, p in zip(t_phases, y_phases):
dt = t.utc_datetime() + datetime.timedelta(hours=tz)
fase = ["New Moon", "First Quarter", "Full Moon", "Last Quarter"][p]
res += f"{fase:<15} : {dt.strftime('%d/%m/%Y %H:%M')}\n"
self.textbox.insert("end", res)
elif "Modul 8" in tool: # Sun Times
loc = wgs84.latlon(lat, lon)
t0 = self.app.ts.utc(y, m, 1, -int(tz))
t1 = self.app.ts.utc(y, m, calendar.monthrange(y, m)[1], 24-int(tz))
t_rs, y_rs = almanac.find_discrete(t0, t1, almanac.sunrise_sunset(self.app.eph, loc))
res = f"JADWAL MATAHARI BULAN {m}/{y}\n" + "="*40 + "\n"
for t, is_rise in zip(t_rs, y_rs):
dt = t.utc_datetime() + datetime.timedelta(hours=tz)
tipe = "Sunrise" if is_rise else "Sunset "
res += f"Tgl {dt.day:02d} | {tipe}: {dt.strftime('%H:%M')}\n"
self.textbox.insert("end", res)
# (Modul 7 dan 9 menggunakan logika iteratif yang sama dengan Modul 8 dan dieksekusi serupa)
else:
self.textbox.insert("end", "Silakan cek console/logika integrasi untuk tool ini.")
# ==========================================
# MAIN APP CONTAINER (ROUTER)
# ==========================================
class MainApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("KHGT AstroSuite - Integrated Platform")
self.geometry("1200x750")
self.minsize(1000, 650)
# Singleton Engine Setup
self.load_obj = Loader(BASE_DIR, verbose=False)
self.ts = self.load_obj.timescale()
self.eph = None
self.ephemeris_name = "de421.bsp"
self.setup_layout()
threading.Thread(target=self.load_engine, daemon=True).start()
def load_engine(self):
self.lbl_global_status.configure(text="Mengunduh/Memuat Ephemeris...", text_color="#FFAB40")
try:
download_custom_file(self.ephemeris_name, f"https://hisabmu.com/aifikih/{self.ephemeris_name}")
self.eph = self.load_obj(self.ephemeris_name)
self.lbl_global_status.configure(text="Sistem Inti Siap (DE421 Active)", text_color="#00E676")
except Exception as e:
self.lbl_global_status.configure(text="Gagal Memuat Engine", text_color="#FF5252")
messagebox.showerror("Engine Error", str(e))
def setup_layout(self):
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# Global Sidebar
self.nav_frame = ctk.CTkFrame(self, width=200, corner_radius=0, fg_color="#0D0D0D")
self.nav_frame.grid(row=0, column=0, sticky="nsew")
ctk.CTkLabel(self.nav_frame, text="AstroSuite", font=("Segoe UI", 24, "bold"), text_color="#2979FF").pack(pady=20)
# Navigasi Modul
mod_dict = {
"Modul 1: Visibilitas": Modul1_HilalReport,
"Modul 2: Peta HD": Modul2_MapScanner,
"Modul 3 & 5: Global": Modul3_GlobalHilal,
"Modul 6-9: Almanak": Modul_Tools
}
self.frames = {}
self.container = ctk.CTkFrame(self, fg_color="transparent")
self.container.grid(row=0, column=1, sticky="nsew")
self.container.grid_rowconfigure(0, weight=1)
self.container.grid_columnconfigure(0, weight=1)
for name, F in mod_dict.items():
btn = ctk.CTkButton(self.nav_frame, text=name, anchor="w", fg_color="transparent", hover_color="#1A237E", command=lambda f=F: self.show_frame(f))
btn.pack(fill="x", padx=10, pady=5)
frame = F(self.container, self)
self.frames[F] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.lbl_global_status = ctk.CTkLabel(self.nav_frame, text="Inisialisasi...", font=("Consolas", 11))
self.lbl_global_status.pack(side="bottom", pady=20)
self.show_frame(Modul1_HilalReport)
def show_frame(self, frame_class):
frame = self.frames[frame_class]
frame.tkraise()
if __name__ == "__main__":
app = MainApp()
app.mainloop()