diff --git a/src/ConfigManager.cpp b/src/ConfigManager.cpp index 70ada2e..530a7da 100644 --- a/src/ConfigManager.cpp +++ b/src/ConfigManager.cpp @@ -1,7 +1,12 @@ #include "ConfigManager.hpp" +#include "SunCalc.hpp" +#include +#include #include +#include #include #include +#include #include #include #include "helpers/Log.hpp" @@ -19,6 +24,8 @@ CConfigManager::CConfigManager(std::string configPath) : void CConfigManager::init() { m_config.addConfigValue("max-gamma", Hyprlang::INT{100}); + m_config.addConfigValue("latitude", Hyprlang::FLOAT{std::numeric_limits::quiet_NaN()}); + m_config.addConfigValue("longitude", Hyprlang::FLOAT{std::numeric_limits::quiet_NaN()}); m_config.addSpecialCategory("profile", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); m_config.addSpecialConfigValue("profile", "time", Hyprlang::STRING{"00:00"}); @@ -40,6 +47,41 @@ std::vector CConfigManager::getSunsetProfiles() { auto keys = m_config.listKeysForSpecialCategory("profile"); result.reserve(keys.size()); + const double latitude = static_cast(std::any_cast(m_config.getConfigValue("latitude"))); + const double longitude = static_cast(std::any_cast(m_config.getConfigValue("longitude"))); + + bool sunTimesCached = false; + NSunCalc::SSunTimes cachedSunTimes{}; + auto ensureSunTimes = [&](const std::string& key, const std::string& descriptor) -> const NSunCalc::SSunTimes& { + if (std::isnan(latitude) || std::isnan(longitude)) + RASSERT(false, "Profile {} uses '{}' time, but latitude and longitude must be configured", key, descriptor); + + if (!sunTimesCached) { + const auto now = std::chrono::system_clock::now(); + const auto* zone = std::chrono::current_zone(); + const auto info = zone->get_info(now); + constexpr double SECONDS_PER_HOUR = 3600.0; + const double timezoneHours = static_cast(info.offset.count()) / SECONDS_PER_HOUR; + std::time_t nowT = std::chrono::system_clock::to_time_t(now); + std::tm localTm{}; + localtime_r(&nowT, &localTm); + const int year = localTm.tm_year + 1900; + const int month = localTm.tm_mon + 1; + const int day = localTm.tm_mday; + const NSunCalc::SLocation location{ + .latitude = latitude, + .longitude = longitude, + .timezone = timezoneHours, + }; + + NSunCalc::CSunCalculator calculator(location); + cachedSunTimes = calculator.computeWithFallback(year, month, day); + sunTimesCached = true; + } + + return cachedSunTimes; + }; + for (auto& key : keys) { std::string time; unsigned long temperature; @@ -57,8 +99,23 @@ std::vector CConfigManager::getSunsetProfiles() { RASSERT(false, "Missing property for Profile: {}", e.what()); // } - size_t separator = time.find(':'); + const bool wantsSunrise = time == "sunrise"; + const bool wantsSunset = time == "sunset"; + + if (wantsSunrise || wantsSunset) { + const auto& sunTimes = ensureSunTimes(key, time); + const double decimalHour = wantsSunrise ? sunTimes.sunrise : sunTimes.sunset; + if (decimalHour < 0) { + Debug::log(ERR, "Failed to compute {} time for profile {}, skipping", time, key); + continue; + } + const std::string formatted = NSunCalc::CSunCalculator::formatTime(decimalHour); + if (formatted == "--:--") + RASSERT(false, "Computed {} time invalid for profile {}", time, key); + time = formatted; + } + size_t separator = time.find(':'); if (separator == std::string::npos) RASSERT(false, "Invalid time format for profile {}", key); @@ -71,6 +128,12 @@ std::vector CConfigManager::getSunsetProfiles() { continue; } + for (const auto& existing : result) { + if (existing.time.hour == std::chrono::hours(hour) && existing.time.minute == std::chrono::minutes(minute)) { + Debug::log(WARN, "Profile {} has the same time {}:{} as an earlier profile; scheduling may delay switching.", key, hour, minute); + } + } + // clang-format off result.push_back(SSunsetProfile{ .time = { diff --git a/src/SunCalc.cpp b/src/SunCalc.cpp new file mode 100644 index 0000000..45f6df8 --- /dev/null +++ b/src/SunCalc.cpp @@ -0,0 +1,276 @@ +#include "SunCalc.hpp" +#include +#include +#include +#include +#include +#include + +namespace NSunCalc { + + // ------------------ Constructor ------------------ + + CSunCalculator::CSunCalculator(const SLocation& l) : m_location(l) {} + + // ------------------ Public API ------------------ + + SSunTimes CSunCalculator::compute(int year, int month, int day) { + // UTC sunrise/sunset (minutes from midnight) + double sunriseUTC = calcSunriseUTC(day, month, year, m_location.latitude, m_location.longitude); + + double sunsetUTC = calcSunsetUTC(day, month, year, m_location.latitude, m_location.longitude); + + SSunTimes times{}; + if (sunriseUTC == NO_EVENT_SENTINEL) { + times.sunrise = NO_EVENT_SENTINEL; + times.sunriseMissing = true; + } else { + double localMinutes = sunriseUTC + m_location.timezone * MINUTES_PER_HOUR; + localMinutes = std::fmod(localMinutes, MINUTES_PER_DAY); + if (localMinutes < 0) + localMinutes += MINUTES_PER_DAY; + times.sunrise = localMinutes / MINUTES_PER_HOUR; + } + + if (sunsetUTC == NO_EVENT_SENTINEL) { + times.sunset = NO_EVENT_SENTINEL; + times.sunsetMissing = true; + } else { + double localMinutes = sunsetUTC + m_location.timezone * MINUTES_PER_HOUR; + localMinutes = std::fmod(localMinutes, MINUTES_PER_DAY); + if (localMinutes < 0) + localMinutes += MINUTES_PER_DAY; + times.sunset = localMinutes / MINUTES_PER_HOUR; + } + + return times; + } + + SSunTimes CSunCalculator::compute() { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto offsetSeconds = seconds(static_cast(std::llround(m_location.timezone * SECONDS_PER_HOUR))); + const auto shifted = now + offsetSeconds; + std::time_t tt = system_clock::to_time_t(shifted); + + std::tm utc{}; + gmtime_r(&tt, &utc); + return compute(utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday); + } + + SSunTimes CSunCalculator::computeWithFallback(int year, int month, int day) { + SSunTimes times = compute(year, month, day); + applyFallback(times); + return times; + } + + SSunTimes CSunCalculator::computeWithFallback() { + SSunTimes times = compute(); + applyFallback(times); + return times; + } + + double CSunCalculator::currentLocalHours() const { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto offsetSeconds = seconds(static_cast(std::llround(m_location.timezone * SECONDS_PER_HOUR))); + const auto shifted = now + offsetSeconds; + std::time_t tt = system_clock::to_time_t(shifted); + + std::tm utc{}; + gmtime_r(&tt, &utc); + + const double hours = static_cast(utc.tm_hour); + const double minutes = static_cast(utc.tm_min) / MINUTES_PER_HOUR; + const double seconds = static_cast(utc.tm_sec) / SECONDS_PER_HOUR; + double local = std::fmod(hours + minutes + seconds, MINUTES_PER_DAY / MINUTES_PER_HOUR); + if (local < 0) + local += MINUTES_PER_DAY / MINUTES_PER_HOUR; + return local; + } + + void CSunCalculator::applyFallback(SSunTimes& times) const { + if (times.sunriseMissing) + times.sunrise = NO_EVENT_SENTINEL; + if (times.sunsetMissing) + times.sunset = NO_EVENT_SENTINEL; + } + + std::string CSunCalculator::formatTime(double decimalHours) { + if (decimalHours < 0) + return "--:--"; + + double totalMinutes = std::round(decimalHours * MINUTES_PER_HOUR); + totalMinutes = std::fmod(totalMinutes, MINUTES_PER_DAY); + if (totalMinutes < 0) + totalMinutes += MINUTES_PER_DAY; + + int h = static_cast(totalMinutes / MINUTES_PER_HOUR); + int m = static_cast(std::fmod(totalMinutes, MINUTES_PER_HOUR)); + + char buf[6]; + std::snprintf(buf, sizeof(buf), "%02d:%02d", h, m); + return std::string(buf); + } + + // ------------------ Math Helpers ------------------ + + double CSunCalculator::deg2rad(double deg) { + return deg * M_PI / HALF_CIRCLE_DEGREES; + } + double CSunCalculator::rad2deg(double rad) { + return rad * HALF_CIRCLE_DEGREES / M_PI; + } + + // ------------------ NOAA Core Functions ------------------ + // Based on NOAA solar position calculator reference implementation. + + double CSunCalculator::calcGeomMeanLongSun(double t) { + double L0 = GEOM_MEAN_LONG_BASE + t * (GEOM_MEAN_LONG_COEFF_PRIMARY + t * GEOM_MEAN_LONG_COEFF_SECONDARY); + L0 = std::fmod(L0, FULL_CIRCLE_DEGREES); + if (L0 < 0) + L0 += FULL_CIRCLE_DEGREES; + return L0; + } + + double CSunCalculator::calcGeomMeanAnomalySun(double t) { + return GEOM_MEAN_ANOMALY_BASE + t * (GEOM_MEAN_ANOMALY_COEFF_PRIMARY - GEOM_MEAN_ANOMALY_COEFF_SECONDARY * t); + } + + double CSunCalculator::calcEccentricityEarthOrbit(double t) { + return ECCENTRICITY_BASE - t * (ECCENTRICITY_COEFF_PRIMARY + ECCENTRICITY_COEFF_SECONDARY * t); + } + + double CSunCalculator::calcSunEqOfCenter(double t) { + double m = deg2rad(calcGeomMeanAnomalySun(t)); + double sinm = std::sin(m); + double sin2m = std::sin(2 * m); + double sin3m = std::sin(3 * m); + + return sinm * (SUN_EQ_CENTER_TERM1 - t * (SUN_EQ_CENTER_TERM1_T1 + SUN_EQ_CENTER_TERM1_T2 * t)) + sin2m * (SUN_EQ_CENTER_TERM2 - SUN_EQ_CENTER_TERM2_T1 * t) + + sin3m * SUN_EQ_CENTER_TERM3; + } + + double CSunCalculator::calcSunTrueLong(double t) { + return calcGeomMeanLongSun(t) + calcSunEqOfCenter(t); + } + + double CSunCalculator::calcSunApparentLong(double t) { + double omega = deg2rad(SUN_APP_LONG_OMEGA_BASE - SUN_APP_LONG_OMEGA_COEFF * t); + return calcSunTrueLong(t) - SUN_APP_LONG_CORR_PRIMARY - SUN_APP_LONG_CORR_SECONDARY * std::sin(omega); + } + + double CSunCalculator::calcMeanObliquityOfEcliptic(double t) { + double seconds = MEAN_OBLIQUITY_SECONDS - t * (MEAN_OBLIQUITY_COEFF1 + t * (MEAN_OBLIQUITY_COEFF2 - MEAN_OBLIQUITY_COEFF3 * t)); + return OBLIQUITY_BASE_DEGREES + (OBLIQUITY_ARCMINUTES + seconds / MINUTES_PER_HOUR) / MINUTES_PER_HOUR; + } + + double CSunCalculator::calcObliquityCorrection(double t) { + double omega = deg2rad(SUN_APP_LONG_OMEGA_BASE - SUN_APP_LONG_OMEGA_COEFF * t); + return calcMeanObliquityOfEcliptic(t) + OBLIQUITY_CORR_COEFF * std::cos(omega); + } + + double CSunCalculator::calcSunDeclination(double t) { + double eps = deg2rad(calcObliquityCorrection(t)); + double lambda = deg2rad(calcSunApparentLong(t)); + double sint = std::sin(eps) * std::sin(lambda); + return rad2deg(std::asin(sint)); + } + + double CSunCalculator::calcEquationOfTime(double t) { + double epsilon = deg2rad(calcObliquityCorrection(t)); + double L0 = deg2rad(calcGeomMeanLongSun(t)); + double e = calcEccentricityEarthOrbit(t); + double m = deg2rad(calcGeomMeanAnomalySun(t)); + + double y = std::tan(epsilon / 2.0); + y *= y; + + double sin2L0 = std::sin(2.0 * L0); + double sinm = std::sin(m); + double cos2L0 = std::cos(2.0 * L0); + double sin4L0 = std::sin(4.0 * L0); + double sin2m = std::sin(2.0 * m); + + double Etime = y * sin2L0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2L0 - EQUATION_OF_TIME_FACTOR1 * y * y * sin4L0 - EQUATION_OF_TIME_FACTOR2 * e * e * sin2m; + + return rad2deg(Etime) * 4.0; // minutes + } + + double CSunCalculator::calcHourAngleSunrise(double lat, double solarDec) { + double latRad = deg2rad(lat); + double sdRad = deg2rad(solarDec); + double cosHA = (std::cos(deg2rad(SOLAR_STANDARD_ALTITUDE)) / (std::cos(latRad) * std::cos(sdRad))) - std::tan(latRad) * std::tan(sdRad); + + if (cosHA > 1.0 + COSINE_TOLERANCE) + return std::numeric_limits::quiet_NaN(); // true polar night + if (cosHA < -1.0 - COSINE_TOLERANCE) + return std::numeric_limits::quiet_NaN(); // true midnight sun + + cosHA = std::clamp(cosHA, -1.0, 1.0); + return std::acos(cosHA); + } + + double CSunCalculator::calcHourAngleSunset(double lat, double solarDec) { + return calcHourAngleSunrise(lat, solarDec); + } + + // ------------------ Julian Date Helpers ------------------ + + double CSunCalculator::calcJD(int year, int month, int day) { + if (month <= 2) { + year -= 1; + month += MONTHS_IN_YEAR; + } + int A = year / CENTURY_DIVISOR; + int B = GREGORIAN_CORRECTION_NUMERATOR - A + (A / LEAP_DIVISOR); + return std::floor(JULIAN_DAYS_PER_YEAR * (year + JULIAN_YEAR_SHIFT)) + std::floor(JULIAN_DAYS_PER_MONTH * (month + 1)) + day + B - JULIAN_DAY_CORRECTION; + } + + double CSunCalculator::calcTimeJulianCent(double jd) { + return (jd - JULIAN_DAY_J2000) / JULIAN_CENTURY_DAYS; + } + + // ------------------ Sunrise / Sunset ------------------ + // Uses NOAA equations, returns minutes from midnight UTC. + + double CSunCalculator::calcSunriseUTC(int day, int month, int year, double latitude, double longitude) { + return calcSunEventUTC(day, month, year, latitude, longitude, true); + } + + double CSunCalculator::calcSunsetUTC(int day, int month, int year, double latitude, double longitude) { + return calcSunEventUTC(day, month, year, latitude, longitude, false); + } + + double CSunCalculator::calcSunEventUTC(int day, int month, int year, double latitude, double longitude, bool isSunrise) { + const double jd = calcJD(year, month, day); + double julianT = calcTimeJulianCent(jd); + double eventUTC = NO_EVENT_SENTINEL; + + for (int iteration = 0; iteration < 2; ++iteration) { + const double eqTime = calcEquationOfTime(julianT); + const double solarDec = calcSunDeclination(julianT); + const double ha = isSunrise ? calcHourAngleSunrise(latitude, solarDec) : calcHourAngleSunset(latitude, solarDec); + + if (std::isnan(ha)) + return NO_EVENT_SENTINEL; + + const double haDeg = rad2deg(ha); + const double solarNoonUTC = MINUTES_AT_NOON - MINUTES_PER_DEGREE * longitude - eqTime; + const double offset = MINUTES_PER_DEGREE * haDeg; + eventUTC = isSunrise ? solarNoonUTC - offset : solarNoonUTC + offset; + + eventUTC = std::fmod(eventUTC, MINUTES_PER_DAY); + if (eventUTC < 0) + eventUTC += MINUTES_PER_DAY; + + if (iteration == 0) { + const double newJD = jd + eventUTC / MINUTES_PER_DAY; + julianT = calcTimeJulianCent(newJD); + } + } + + return eventUTC; + } + +} // namespace NSunCalc diff --git a/src/SunCalc.hpp b/src/SunCalc.hpp new file mode 100644 index 0000000..85d0df6 --- /dev/null +++ b/src/SunCalc.hpp @@ -0,0 +1,116 @@ +#ifndef SUNCALC_HPP +#define SUNCALC_HPP + +#include + +namespace NSunCalc { + + struct SLocation { + double latitude; // degrees + double longitude; // degrees + double timezone; // UTC offset (e.g., -5 for EST) + }; + + struct SSunTimes { + double sunrise; // decimal hours (local time) + double sunset; // decimal hours (local time) + bool sunriseMissing{false}; + bool sunsetMissing{false}; + }; + + class CSunCalculator { + public: + explicit CSunCalculator(const SLocation& loc); + + SSunTimes compute(int year, int month, int day); + SSunTimes compute(); + SSunTimes computeWithFallback(int year, int month, int day); + SSunTimes computeWithFallback(); + static std::string formatTime(double decimalHours); + + private: + SLocation m_location; + double currentLocalHours() const; + void applyFallback(SSunTimes& times) const; + + // NOAA algorithm internal helpers + double calcSunriseUTC(int day, int month, int year, double latitude, double longitude); + + double calcSunsetUTC(int day, int month, int year, double latitude, double longitude); + + // Math helpers + static double deg2rad(double deg); + static double rad2deg(double rad); + + // NOAA internal + static double calcGeomMeanLongSun(double t); + static double calcGeomMeanAnomalySun(double t); + static double calcEccentricityEarthOrbit(double t); + static double calcSunEqOfCenter(double t); + static double calcSunTrueLong(double t); + static double calcSunApparentLong(double t); + static double calcMeanObliquityOfEcliptic(double t); + static double calcObliquityCorrection(double t); + static double calcSunDeclination(double t); + static double calcEquationOfTime(double t); + static double calcHourAngleSunrise(double lat, double solarDec); + static double calcHourAngleSunset(double lat, double solarDec); + + static double calcTimeJulianCent(double jd); + static double calcJD(int year, int month, int day); + static double calcSunEventUTC(int day, int month, int year, double latitude, double longitude, bool isSunrise); + + // Shared constants + static constexpr double MINUTES_PER_HOUR = 60.0; + static constexpr double MINUTES_PER_DAY = 1440.0; + static constexpr double MINUTES_AT_NOON = 720.0; + static constexpr double MINUTES_PER_DEGREE = 4.0; + static constexpr double SECONDS_PER_HOUR = 3600.0; + static constexpr double NO_EVENT_SENTINEL = -1.0; + static constexpr double FULL_CIRCLE_DEGREES = 360.0; + static constexpr double HALF_CIRCLE_DEGREES = 180.0; + static constexpr double SOLAR_STANDARD_ALTITUDE = 90.833; + static constexpr double COSINE_TOLERANCE = 1e-9; + static constexpr double JULIAN_DAYS_PER_YEAR = 365.25; + static constexpr double JULIAN_DAYS_PER_MONTH = 30.6001; + static constexpr int JULIAN_YEAR_SHIFT = 4716; + static constexpr int MONTHS_IN_YEAR = 12; + static constexpr int GREGORIAN_CORRECTION_NUMERATOR = 2; + static constexpr int CENTURY_DIVISOR = 100; + static constexpr int LEAP_DIVISOR = 4; + static constexpr double JULIAN_DAY_CORRECTION = 1524.5; + static constexpr double JULIAN_DAY_J2000 = 2451545.0; + static constexpr double JULIAN_CENTURY_DAYS = 36525.0; + static constexpr double GEOM_MEAN_LONG_BASE = 280.46646; + static constexpr double GEOM_MEAN_LONG_COEFF_PRIMARY = 36000.76983; + static constexpr double GEOM_MEAN_LONG_COEFF_SECONDARY = 0.0003032; + static constexpr double GEOM_MEAN_ANOMALY_BASE = 357.52911; + static constexpr double GEOM_MEAN_ANOMALY_COEFF_PRIMARY = 35999.05029; + static constexpr double GEOM_MEAN_ANOMALY_COEFF_SECONDARY = 0.0001537; + static constexpr double ECCENTRICITY_BASE = 0.016708634; + static constexpr double ECCENTRICITY_COEFF_PRIMARY = 0.000042037; + static constexpr double ECCENTRICITY_COEFF_SECONDARY = 0.0000001267; + static constexpr double SUN_EQ_CENTER_TERM1 = 1.914602; + static constexpr double SUN_EQ_CENTER_TERM1_T1 = 0.004817; + static constexpr double SUN_EQ_CENTER_TERM1_T2 = 0.000014; + static constexpr double SUN_EQ_CENTER_TERM2 = 0.019993; + static constexpr double SUN_EQ_CENTER_TERM2_T1 = 0.000101; + static constexpr double SUN_EQ_CENTER_TERM3 = 0.000289; + static constexpr double SUN_APP_LONG_OMEGA_BASE = 125.04; + static constexpr double SUN_APP_LONG_OMEGA_COEFF = 1934.136; + static constexpr double SUN_APP_LONG_CORR_PRIMARY = 0.00569; + static constexpr double SUN_APP_LONG_CORR_SECONDARY = 0.00478; + static constexpr double MEAN_OBLIQUITY_SECONDS = 21.448; + static constexpr double MEAN_OBLIQUITY_COEFF1 = 46.815; + static constexpr double MEAN_OBLIQUITY_COEFF2 = 0.00059; + static constexpr double MEAN_OBLIQUITY_COEFF3 = 0.001813; + static constexpr double OBLIQUITY_BASE_DEGREES = 23.0; + static constexpr double OBLIQUITY_ARCMINUTES = 26.0; + static constexpr double OBLIQUITY_CORR_COEFF = 0.00256; + static constexpr double EQUATION_OF_TIME_FACTOR1 = 0.5; + static constexpr double EQUATION_OF_TIME_FACTOR2 = 1.25; + }; + +} // namespace NSunCalc + +#endif // SUNCALC_HPP