generated from me/template-mit
167 lines
6.1 KiB
Kotlin
167 lines
6.1 KiB
Kotlin
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
|
|
}
|