package com.hyperling.weather import androidx.compose.ui.graphics.Color import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin /** * The 8 positions on the Celtic Season Wheel. * Based on solar events arranged clockwise starting from Summer at top (12 o'clock). */ enum class Season( val label: String, val fullName: String, val color: Color, /** * Start of season expressed as month (1-12) and day (1-31). * Used for the hand pointer position. */ val startMonth: Int, val startDay: Int, ) { // Summer solstice area - top of wheel (12 o'clock = 0 degrees in Compose arc coords) SUMMER1("V1", "Summer Cross Quarter to Fall Equinox", Color(0xFFFBBC04), 6, 21), SUMMER2("V2", "Fall Equinox to Winter Solstice (early)", Color(0xFFFF8F00), 8, 1), FALL1("F1", "Fall Equinox to Fall Cross Quarter", Color(0xFFE65100), 9, 21), FALL2("F2", "Winter Solstice to Winter Cross Quarter (early)", Color(0xFF6D4C41), 11, 1), WINTER1("W1", "Winter Cross Quarter to Spring Equinox (late)", Color(0xFF546E7A), 12, 21), WINTER2("W2", "Spring Equinox to Summer Solstice (early)", Color(0xFF37474F), 2, 1), SPRING1("S1", "Summer Cross Quarter to Fall Equinox", Color(0xFF4CAF50), 3, 21), SPRING2("S2", "Cross Quarter to Summer Solstice", Color(0xFF81C784), 5, 1); companion object { val All = values() val Segments = All.size // 8 /** * Get the current season based on today's date. */ fun current(): Season { val calendar = java.util.Calendar.getInstance() val month = calendar.get(java.util.Calendar.MONTH) + 1 // 1-12 val day = calendar.get(java.util.Calendar.DAY_OF_MONTH) return All.find { season -> val startStart = season.startMonth * 100 + season.startDay // Calculate end as next season's start val idx = season.ordinal val nextSeason = if (idx < All.size - 1) All[idx + 1] else All[0] val endStart = nextSeason.startMonth * 100 + nextSeason.startDay val currentPos = month * 100 + day when { startStart <= endStart -> currentPos in startStart..endStart else -> currentPos >= startStart || currentPos <= endStart } } ?: SPRING1 } /** * Get the hand angle (in radians) for the current date. * 0 radians = top of wheel (Summer Solstice), increasing clockwise. */ fun handAngle(): Double { val calendar = java.util.Calendar.getInstance() val month = calendar.get(java.util.Calendar.MONTH) + 1 val day = calendar.get(java.util.Calendar.DAY_OF_MONTH) // Calculate which season we're in and how far through it val currentSeason = current() val idx = currentSeason.ordinal val nextIdx = (idx + 1) % All.size val nextSeason = All[nextIdx] val startPos = currentSeason.startMonth * 100 + currentSeason.startDay val endPos = nextSeason.startMonth * 100 + nextSeason.startDay // Handle year wrap for seasons that cross January var totalDays = if (endPos > startPos) { daysBetween(currentSeason.startMonth, currentSeason.startDay, nextSeason.startMonth, nextSeason.startDay) } else { // Crosses year boundary val daysInCurrentMonth = daysInMonth(currentSeason.startMonth, currentSeason.startDay) val daysRemaining = daysInCurrentMonth - currentSeason.startDay + 1 val daysFromStartNext = nextSeason.startMonth * 30 + nextSeason.startDay - 15 daysRemaining + daysFromStartNext } if (totalDays <= 0) totalDays = 90 // fallback val currentPos = month * 100 + day var elapsedDays: Int if (endPos > startPos) { elapsedDays = daysBetween(currentSeason.startMonth, currentSeason.startDay, month, day) } else { val daysInCurrentMonth = daysInMonth(currentSeason.startMonth, 1) elapsedDays = (daysInCurrentMonth - currentSeason.startDay + 1) + (day - 1) } if (elapsedDays < 0) elapsedDays = 0 val fraction = elapsedDays.toDouble() / totalDays.coerceAtLeast(1) // Map to angle: 0 at top (Summer Solstice), clockwise return fraction * 2 * PI } private fun daysInMonth(month: Int, day: Int): Int { val cal = java.util.Calendar.getInstance().apply { set(java.util.Calendar.MONTH, month - 1) } return cal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) } private fun daysBetween(m1: Int, d1: Int, m2: Int, d2: Int): Int { val start = java.util.GregorianCalendar(2024, m1 - 1, d1).timeInMillis val end = java.util.GregorianCalendar(2024, m2 - 1, d2).timeInMillis return ((end - start) / (1000 * 60 * 60 * 24)).toInt() } /** * Get the angle for a season's center on the wheel. * 0 radians = top of wheel, increasing clockwise. */ fun angleFor(idx: Int): Double { return (idx.toDouble() / Segments) * 2 * PI } /** * Get the sweep angle for each wedge (360 degrees / 8 = 45 degrees). */ val wedgeSweep: Double get() = 2 * PI / Segments } } /** * Color palette for the Celtic Wheel UI. */ object SeasonColors { val Background = Color(0xFF1A1A2E) val CenterBg = Color(0xFF16213E) val TextPrimary = Color(0xFFFFFFFF) val TextSecondary = Color(0xFFB0BEC5) val Divider = Color(0xFF37474F) val Shadow = Color(0x4D000000) } /** * Current date/time info for the wheel hand. */ data class HandInfo( val dayOfYear: Int, val totalDaysInYear: Int, val fractionThroughSeason: Double, val currentSeason: Season ) { val angle: Double get() = (dayOfYear.toDouble() / totalDaysInYear) * 2 * Math.PI }