'use client'; import { useRef, useEffect, useCallback } from 'react'; import { useLocale } from './LocaleProvider'; import { ts } from '@/lib/i18n'; import { getMoonDeclination, getMoonRightAscension, getMoonPhaseInfo } from '@/lib/lunar'; export default function VisibilityMap() { const { locale } = useLocale(); const canvasRef = useRef(null); const mapImageRef = useRef(null); const dateToJD = useCallback((date: Date): number => { const y = date.getUTCFullYear(); const m = date.getUTCMonth() + 1; const d = date.getUTCDate() + date.getUTCHours() / 24; let yr = y; let mo = m; if (mo <= 2) { yr -= 1; mo += 12; } const A = Math.floor(yr / 100); const B = 2 - A + Math.floor(A / 4); return Math.floor(365.25 * (yr + 4716)) + Math.floor(30.6001 * (mo + 1)) + d + B - 1524.5; }, []); const drawMap = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const now = new Date(); const w = canvas.width; const h = canvas.height; ctx.clearRect(0, 0, w, h); const mapImage = mapImageRef.current; if (mapImage && mapImage.complete && mapImage.naturalWidth > 0) { ctx.drawImage(mapImage, 0, 0, w, h); // Slight dark layer for readability of overlays ctx.fillStyle = 'rgba(10, 14, 39, 0.25)'; ctx.fillRect(0, 0, w, h); } else { // Fallback background if image is missing ctx.fillStyle = '#0a0e27'; ctx.fillRect(0, 0, w, h); ctx.strokeStyle = 'rgba(99, 102, 241, 0.1)'; ctx.lineWidth = 0.5; for (let lat = -60; lat <= 60; lat += 30) { const y = h / 2 - (lat / 90) * (h / 2); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } for (let lon = -150; lon <= 180; lon += 30) { const x = w / 2 + (lon / 180) * (w / 2); ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } } // Moon position const dec = getMoonDeclination(now); const ra = getMoonRightAscension(now); // Convert RA to longitude (approximate, accounting for Earth's rotation) const gmst = (280.46061837 + 360.98564736629 * ((dateToJD(now) - 2451545.0))) % 360; let moonLon = ra - gmst; while (moonLon > 180) moonLon -= 360; while (moonLon < -180) moonLon += 360; const moonX = w / 2 + (moonLon / 180) * (w / 2); const moonY = h / 2 - (dec / 90) * (h / 2); // Visibility zone (large circle) const visibilityRadius = w * 0.25; const visGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, visibilityRadius); visGrad.addColorStop(0, 'rgba(251, 191, 36, 0.2)'); visGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.08)'); visGrad.addColorStop(1, 'rgba(251, 191, 36, 0)'); ctx.fillStyle = visGrad; ctx.beginPath(); ctx.arc(moonX, moonY, visibilityRadius, 0, Math.PI * 2); ctx.fill(); // Moon position marker const moonPhase = getMoonPhaseInfo(now); // Glow const glowGrad = ctx.createRadialGradient(moonX, moonY, 0, moonX, moonY, 25); glowGrad.addColorStop(0, 'rgba(251, 191, 36, 0.8)'); glowGrad.addColorStop(0.5, 'rgba(251, 191, 36, 0.3)'); glowGrad.addColorStop(1, 'rgba(251, 191, 36, 0)'); ctx.fillStyle = glowGrad; ctx.beginPath(); ctx.arc(moonX, moonY, 25, 0, Math.PI * 2); ctx.fill(); // Moon dot ctx.beginPath(); ctx.arc(moonX, moonY, 8, 0, Math.PI * 2); ctx.fillStyle = '#fbbf24'; ctx.fill(); ctx.strokeStyle = '#fef9c3'; ctx.lineWidth = 2; ctx.stroke(); // Label ctx.fillStyle = '#fef9c3'; ctx.font = '12px system-ui'; ctx.textAlign = 'center'; ctx.fillText(`${moonPhase.emoji} ${moonPhase.illumination}%`, moonX, moonY - 18); // Equator label ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.font = '10px system-ui'; ctx.textAlign = 'left'; ctx.fillText('0°', 4, h / 2 + 4); }, [dateToJD]); useEffect(() => { const mapImage = new Image(); mapImage.src = '/moon-visibility-map.webp'; mapImage.onload = () => { mapImageRef.current = mapImage; drawMap(); }; mapImage.onerror = () => { mapImageRef.current = null; drawMap(); }; drawMap(); const interval = setInterval(drawMap, 60000); // Update every minute return () => clearInterval(interval); }, [drawMap]); return (

{ts('visibility_title', locale)}

{ts('visibility_subtitle', locale)}

Moon Position Visibility Zone
); }