mirror of
https://github.com/acamarata/pray-calc-dart.git
synced 2026-06-30 19:04:25 +00:00
156 lines
4.8 KiB
Dart
156 lines
4.8 KiB
Dart
/// Moonsighting Committee Worldwide (MCW) seasonal algorithm.
|
|
///
|
|
/// Computes Fajr and Isha as time offsets from sunrise/sunset using the
|
|
/// empirical piecewise-linear seasonal functions developed by the Moonsighting
|
|
/// Committee Worldwide (Khalid Shaukat).
|
|
///
|
|
/// Reference: moonsighting.com/isha_fajr.html
|
|
library;
|
|
|
|
import 'dart:math';
|
|
|
|
import 'types.dart';
|
|
|
|
bool _isLeapYear(int year) {
|
|
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
|
|
}
|
|
|
|
/// Compute the MCW seasonal index (dyy) and days in year.
|
|
({int dyy, int daysInYear}) _computeDyy(DateTime date, double latitude) {
|
|
final year = date.year;
|
|
final daysInYear = _isLeapYear(year) ? 366 : 365;
|
|
|
|
// Reference solstice: Dec 21 for Northern, Jun 21 for Southern
|
|
final refMonth = latitude >= 0 ? 12 : 6; // Dec = 12, Jun = 6 (1-based)
|
|
const refDay = 21;
|
|
|
|
final zeroDate = DateTime.utc(year, refMonth, refDay);
|
|
final inputUtc = DateTime.utc(date.year, date.month, date.day);
|
|
|
|
int diffDays = inputUtc.difference(zeroDate).inDays;
|
|
if (diffDays < 0) diffDays += daysInYear;
|
|
|
|
return (dyy: diffDays, daysInYear: daysInYear);
|
|
}
|
|
|
|
/// Piecewise-linear seasonal interpolation over 6 segments.
|
|
double _interpolateSegment(
|
|
int dyy,
|
|
int daysInYear,
|
|
double a,
|
|
double b,
|
|
double c,
|
|
double d,
|
|
) {
|
|
if (dyy < 91) {
|
|
return a + ((b - a) / 91) * dyy;
|
|
} else if (dyy < 137) {
|
|
return b + ((c - b) / 46) * (dyy - 91);
|
|
} else if (dyy < 183) {
|
|
return c + ((d - c) / 46) * (dyy - 137);
|
|
} else if (dyy < 229) {
|
|
return d + ((c - d) / 46) * (dyy - 183);
|
|
} else if (dyy < 275) {
|
|
return c + ((b - c) / 46) * (dyy - 229);
|
|
} else {
|
|
final len = daysInYear - 275;
|
|
return b + ((a - b) / len) * (dyy - 275);
|
|
}
|
|
}
|
|
|
|
/// Compute Fajr offset in minutes before sunrise using the MCW algorithm.
|
|
///
|
|
/// Returns minutes before sunrise (rounded to nearest minute).
|
|
double getMscFajr(DateTime date, double latitude) {
|
|
final latAbs = latitude.abs();
|
|
final (:dyy, :daysInYear) = _computeDyy(date, latitude);
|
|
|
|
final a = 75 + (28.65 / 55) * latAbs;
|
|
final b = 75 + (19.44 / 55) * latAbs;
|
|
final c = 75 + (32.74 / 55) * latAbs;
|
|
final d = 75 + (48.1 / 55) * latAbs;
|
|
|
|
return _interpolateSegment(dyy, daysInYear, a, b, c, d).roundToDouble();
|
|
}
|
|
|
|
/// Compute Isha offset in minutes after sunset using the MCW algorithm.
|
|
///
|
|
/// [shafaq] selects the twilight mode: general (default), ahmer, or abyad.
|
|
/// Returns minutes after sunset (rounded to nearest minute).
|
|
double getMscIsha(
|
|
DateTime date,
|
|
double latitude, [
|
|
ShafaqMode shafaq = ShafaqMode.general,
|
|
]) {
|
|
final latAbs = latitude.abs();
|
|
final (:dyy, :daysInYear) = _computeDyy(date, latitude);
|
|
|
|
double a, b, c, d;
|
|
|
|
switch (shafaq) {
|
|
case ShafaqMode.ahmer:
|
|
a = 62 + (17.4 / 55) * latAbs;
|
|
b = 62 - (7.16 / 55) * latAbs;
|
|
c = 62 + (5.12 / 55) * latAbs;
|
|
d = 62 + (19.44 / 55) * latAbs;
|
|
case ShafaqMode.abyad:
|
|
a = 75 + (25.6 / 55) * latAbs;
|
|
b = 75 + (7.16 / 55) * latAbs;
|
|
c = 75 + (36.84 / 55) * latAbs;
|
|
d = 75 + (81.84 / 55) * latAbs;
|
|
case ShafaqMode.general:
|
|
a = 75 + (25.6 / 55) * latAbs;
|
|
b = 75 + (2.05 / 55) * latAbs;
|
|
c = 75 - (9.21 / 55) * latAbs;
|
|
d = 75 + (6.14 / 55) * latAbs;
|
|
}
|
|
|
|
return _interpolateSegment(dyy, daysInYear, a, b, c, d).roundToDouble();
|
|
}
|
|
|
|
/// Convert MCW minutes-before-sunrise to an equivalent solar depression angle
|
|
/// in degrees, using exact spherical trigonometry.
|
|
///
|
|
/// Returns [double.nan] if the geometry is unreachable (polar day/night).
|
|
double minutesToDepression(double minutes, double latDeg, double declDeg) {
|
|
final phi = latDeg * (pi / 180);
|
|
final delta = declDeg * (pi / 180);
|
|
|
|
final cosPhi = cos(phi);
|
|
final sinPhi = sin(phi);
|
|
final cosDelta = cos(delta);
|
|
final sinDelta = sin(delta);
|
|
|
|
// Standard sunrise/sunset: h = -0.833° (includes refraction + semi-diameter)
|
|
final h0 = -0.833 * (pi / 180);
|
|
final sinH0 = sin(h0);
|
|
|
|
final denominator = cosPhi * cosDelta;
|
|
if (denominator.abs() < 1e-10) return double.nan;
|
|
|
|
// Hour angle at standard sunrise
|
|
final cosHRise = (sinH0 - sinPhi * sinDelta) / denominator;
|
|
|
|
if (cosHRise < -1) return double.nan; // polar night
|
|
if (cosHRise > 1) return double.nan; // polar day
|
|
|
|
final hRise = acos(cosHRise); // radians
|
|
|
|
// Hour angle at the prayer time (further from solar noon)
|
|
final deltaH = (minutes / 60) * 15 * (pi / 180);
|
|
final hPrayer = hRise + deltaH;
|
|
|
|
// Cap at π (midnight)
|
|
if (hPrayer > pi) {
|
|
final sinHMidnight = sinPhi * sinDelta + cosPhi * cosDelta * cos(pi);
|
|
final hMidnight = asin(sinHMidnight.clamp(-1.0, 1.0));
|
|
return -hMidnight / (pi / 180);
|
|
}
|
|
|
|
// Solar altitude at hPrayer
|
|
final sinHPrayer = sinPhi * sinDelta + cosPhi * cosDelta * cos(hPrayer);
|
|
final hPrayerAlt = asin(sinHPrayer.clamp(-1.0, 1.0));
|
|
|
|
// Depression angle: positive when sun is below horizon
|
|
return -hPrayerAlt / (pi / 180);
|
|
}
|