Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/src/adhan_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export 'madhab.dart' show Madhab;
export 'calculation_method.dart';
export 'calculation_parameters.dart';
export 'high_latitude_rule.dart';
export 'polar_circle_resolution.dart';
export 'prayer_adjustments.dart';
export 'qibla.dart';
export 'sunnah_times.dart';
Expand Down
5 changes: 5 additions & 0 deletions lib/src/calculation_parameters.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'calculation_method.dart';
import 'high_latitude_rule.dart';
import 'madhab.dart';
import 'polar_circle_resolution.dart';
import 'prayer_adjustments.dart';

/// Parameters used for PrayerTime calculation customization
Expand Down Expand Up @@ -30,6 +31,9 @@ class CalculationParameters {
/// Rules for placing bounds on Fajr and Isha for high latitude areas
HighLatitudeRule highLatitudeRule;

/// Strategy for resolving prayer times in polar regions where the sun may not rise or set
PolarCircleResolution polarCircleResolution;

/// Used to optionally add or subtract a set amount of time from each prayer time
PrayerAdjustments adjustments;

Expand All @@ -44,6 +48,7 @@ class CalculationParameters {
this.ishaInterval = 0,
this.madhab = Madhab.shafi,
this.highLatitudeRule = HighLatitudeRule.middle_of_the_night,
this.polarCircleResolution = PolarCircleResolution.unresolved,
PrayerAdjustments? adjustments,
PrayerAdjustments? methodAdjustments})
: adjustments = adjustments ?? PrayerAdjustments(),
Expand Down
90 changes: 90 additions & 0 deletions lib/src/polar_circle_resolution.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'coordinates.dart';
import 'internal/solar_time.dart';

/// Resolution strategies for handling prayer time calculations in polar regions
/// where the sun may not rise or set for extended periods.
enum PolarCircleResolution {
/// Find the nearest location (by latitude) where valid sunrise/sunset times can be calculated.
/// This strategy adjusts the latitude in 0.5° steps toward the equator until valid solar times are found.
aqrabBalad,

/// Find the nearest date (forward or backward in time) where valid sunrise/sunset times can be calculated.
/// This strategy searches for dates when the original location experiences normal day/night cycles.
aqrabYaum,

/// Leave prayer times undefined when they cannot be calculated due to polar conditions.
/// This maintains the original behavior before polar circle resolution was introduced.
unresolved
}

/// Constants for polar circle resolution calculations
class _PolarCircleConstants {
/// Degrees to add/remove at each resolution step for AqrabBalad
static const double latitudeVariationStep = 0.5;

/// Latitude threshold for polar circle calculations (65° based on midnight sun research)
static const double unsafeLatitude = 65.0;

/// Maximum days to search for AqrabYaum (half a year for practicality)
static const int maxDaysToSearch = 182;
}

/// Result of polar circle resolution containing resolved calculation data
class _PolarCircleResolutionResult {
final DateTime date;
final DateTime tomorrow;
final Coordinates coordinates;
final SolarTime solarTime;
final SolarTime tomorrowSolarTime;

_PolarCircleResolutionResult({
required this.date,
required this.tomorrow,
required this.coordinates,
required this.solarTime,
required this.tomorrowSolarTime,
});
}

/// Utility functions for polar circle resolution
class PolarCircleResolutionUtils {
/// Check if solar time values are valid (not NaN or infinite)
static bool isValidSolarTime(SolarTime solarTime) {
bool _valid(double value) => !(value.isInfinite || value.isNaN);
return _valid(solarTime.sunrise) && _valid(solarTime.sunset);
}

/// Check if both current and tomorrow's solar times are valid
static bool isValidSolarTimePair(SolarTime solarTime, SolarTime tomorrowSolarTime) {
return isValidSolarTime(solarTime) && isValidSolarTime(tomorrowSolarTime);
}
}

/// Constants for polar circle resolution calculations
class PolarCircleConstants {
/// Degrees to add/remove at each resolution step for AqrabBalad
static const double latitudeVariationStep = 0.5;

/// Latitude threshold for polar circle calculations (65° based on midnight sun research)
static const double unsafeLatitude = 65.0;

/// Maximum days to search for AqrabYaum (half a year for practicality)
static const int maxDaysToSearch = 182;
}

/// Result of polar circle resolution containing resolved calculation data
class PolarCircleResolutionResult {
final DateTime date;
final DateTime tomorrow;
final Coordinates coordinates;
final SolarTime solarTime;
final SolarTime tomorrowSolarTime;

PolarCircleResolutionResult({
required this.date,
required this.tomorrow,
required this.coordinates,
required this.solarTime,
required this.tomorrowSolarTime,
});
}
102 changes: 102 additions & 0 deletions lib/src/polar_circle_resolvers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'data/date_components.dart';
import 'internal/solar_time.dart';
import 'polar_circle_resolution.dart';
import 'coordinates.dart';

/// Implementation of AqrabBalad resolution strategy
/// Finds the nearest location (by latitude) where valid sunrise/sunset times can be calculated
class _AqrabBaladResolver {
static PolarCircleResolutionResult? resolve(
Coordinates originalCoordinates,
DateTime date,
double latitude,
) {
// Calculate solar times at the given latitude
final coordinates = Coordinates(latitude, originalCoordinates.longitude);
final solarTime = SolarTime(date, coordinates);
final tomorrow = date.add(Duration(days: 1));
final tomorrowSolarTime = SolarTime(tomorrow, coordinates);

// If solar times are invalid and we're still in polar region
if (!PolarCircleResolutionUtils.isValidSolarTimePair(solarTime, tomorrowSolarTime)) {
// Check if we've reached a safe latitude
if (latitude.abs() >= PolarCircleConstants.unsafeLatitude) {
// Recursively try 0.5° closer to equator
final newLatitude = latitude - (latitude >= 0 ? 1 : -1) * PolarCircleConstants.latitudeVariationStep;
return resolve(originalCoordinates, date, newLatitude);
} else {
// Reached safe latitude but still no valid solar times
return null;
}
}

// Return resolved values
return PolarCircleResolutionResult(
date: date,
tomorrow: tomorrow,
coordinates: coordinates,
solarTime: solarTime,
tomorrowSolarTime: tomorrowSolarTime,
);
}
}

/// Implementation of AqrabYaum resolution strategy
/// Finds the nearest date (forward or backward in time) where valid sunrise/sunset times can be calculated
class _AqrabYaumResolver {
static PolarCircleResolutionResult? resolve(
Coordinates coordinates,
DateTime originalDate,
int daysAdded,
int direction,
) {
// Safety check: don't search more than half a year
if (daysAdded > PolarCircleConstants.maxDaysToSearch) {
return null;
}

// Try calculating for offset date
final testDate = originalDate.add(Duration(days: direction * daysAdded));
final tomorrow = testDate.add(Duration(days: 1));
final solarTime = SolarTime(testDate, coordinates);
final tomorrowSolarTime = SolarTime(tomorrow, coordinates);

// If still invalid, reverse direction and continue searching
if (!PolarCircleResolutionUtils.isValidSolarTimePair(solarTime, tomorrowSolarTime)) {
// Determine next search parameters
final nextDaysAdded = daysAdded + (direction > 0 ? 0 : 1);
final nextDirection = -direction;

return resolve(coordinates, originalDate, nextDaysAdded, nextDirection);
}

// Return resolved values (using original date for consistency, but resolved solar times)
return PolarCircleResolutionResult(
date: originalDate, // Keep original date for prayer time calculation
tomorrow: originalDate.add(Duration(days: 1)),
coordinates: coordinates,
solarTime: solarTime,
tomorrowSolarTime: tomorrowSolarTime,
);
}
}

/// Main resolver that dispatches to the appropriate strategy
class PolarCircleResolver {
static PolarCircleResolutionResult? resolve(
PolarCircleResolution strategy,
Coordinates coordinates,
DateTime date,
) {
switch (strategy) {
case PolarCircleResolution.aqrabBalad:
return _AqrabBaladResolver.resolve(coordinates, date, coordinates.latitude);

case PolarCircleResolution.aqrabYaum:
return _AqrabYaumResolver.resolve(coordinates, date, 1, 1);

case PolarCircleResolution.unresolved:
return null;
}
}
}
96 changes: 85 additions & 11 deletions lib/src/prayer_times.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'data/date_components.dart';
import 'data/time_components.dart';
import 'internal/solar_time.dart';
import 'madhab.dart';
import 'polar_circle_resolution.dart';
import 'polar_circle_resolvers.dart';
import 'prayer.dart';

class PrayerTimes {
Expand Down Expand Up @@ -41,6 +43,12 @@ class PrayerTimes {

final CalculationParameters calculationParameters;

late bool _polarResolutionApplied;
bool get polarResolutionApplied => _polarResolutionApplied;

late PolarCircleResolution? _polarResolutionStrategy;
PolarCircleResolution? get polarResolutionStrategy => _polarResolutionStrategy;

/// Calculate PrayerTimes and Output Local Times By Default.
/// If you provide utcOffset then it will Output UTC with Offset Applied Times.
///
Expand Down Expand Up @@ -125,21 +133,85 @@ class PrayerTimes {
final year = date.year;
final dayOfYear = date.dayOfYear;

final solarTime = SolarTime(date, coordinates);
// Initialize solar time calculation
late SolarTime solarTime;
late SolarTime tomorrowSolarTime;
late DateTime tomorrow;
late Coordinates calculationCoordinates;

// Apply polar circle resolution if needed
final initialSolarTime = SolarTime(date, coordinates);
final initialTomorrowSolarTime = SolarTime(date.add(Duration(days: 1)), coordinates);

bool resolutionApplied = false;
PolarCircleResolution? usedStrategy;

if (!PolarCircleResolutionUtils.isValidSolarTimePair(initialSolarTime, initialTomorrowSolarTime) &&
calculationParameters.polarCircleResolution != PolarCircleResolution.unresolved) {
// Apply resolution strategy
final resolutionResult = PolarCircleResolver.resolve(
calculationParameters.polarCircleResolution,
coordinates,
date,
);

if (resolutionResult != null) {
solarTime = resolutionResult.solarTime;
tomorrowSolarTime = resolutionResult.tomorrowSolarTime;
tomorrow = resolutionResult.tomorrow;
calculationCoordinates = resolutionResult.coordinates;
resolutionApplied = true;
usedStrategy = calculationParameters.polarCircleResolution;
} else {
// Resolution failed, use original coordinates (will result in undefined times)
solarTime = initialSolarTime;
tomorrowSolarTime = initialTomorrowSolarTime;
tomorrow = date.add(Duration(days: 1));
calculationCoordinates = coordinates;
}
} else {
// No resolution needed or configured
solarTime = initialSolarTime;
tomorrowSolarTime = initialTomorrowSolarTime;
tomorrow = date.add(Duration(days: 1));
calculationCoordinates = coordinates;
}

// Store resolution info in instance variables (using private fields and getters)
_polarResolutionApplied = resolutionApplied;
_polarResolutionStrategy = usedStrategy;

var timeComponents = TimeComponents.fromDouble(solarTime.transit);
final transit = timeComponents.dateComponents(date);

timeComponents = TimeComponents.fromDouble(solarTime.sunrise);
final sunriseComponents = timeComponents.dateComponents(date);
// Calculate sunrise and sunset
DateTime sunriseComponents;
DateTime sunsetComponents;

timeComponents = TimeComponents.fromDouble(solarTime.sunset);
final sunsetComponents = timeComponents.dateComponents(date);
if (_valid(solarTime.sunrise)) {
timeComponents = TimeComponents.fromDouble(solarTime.sunrise);
sunriseComponents = timeComponents.dateComponents(date);
} else {
// Sunrise cannot be calculated (polar night)
sunriseComponents = date; // Placeholder, will be handled by safe bounds
}

final tomorrow = date.add(Duration(days: 1));
final tomorrowSolarTime = SolarTime(tomorrow, coordinates);
final tomorrowSunriseComponents =
TimeComponents.fromDouble(tomorrowSolarTime.sunrise);
if (_valid(solarTime.sunset)) {
timeComponents = TimeComponents.fromDouble(solarTime.sunset);
sunsetComponents = timeComponents.dateComponents(date);
} else {
// Sunset cannot be calculated (midnight sun)
sunsetComponents = date; // Placeholder, will be handled by safe bounds
}

// Calculate tomorrow's sunrise for night length calculation
DateTime tomorrowSunriseComponents;
if (_valid(tomorrowSolarTime.sunrise)) {
tomorrowSunriseComponents = TimeComponents.fromDouble(tomorrowSolarTime.sunrise).dateComponents(tomorrow);
} else {
// Tomorrow's sunrise cannot be calculated, use approximation
tomorrowSunriseComponents = transit.add(Duration(hours: 24));
}

tempDhuhr = transit;
tempSunrise = sunriseComponents;
Expand All @@ -149,12 +221,12 @@ class PrayerTimes {
tempAsr = timeComponents.dateComponents(date);

// get night length
final tomorrowSunrise = tomorrowSunriseComponents.dateComponents(tomorrow);
final tomorrowSunrise = tomorrowSunriseComponents;
final night = tomorrowSunrise.millisecondsSinceEpoch -
sunsetComponents.millisecondsSinceEpoch;

// Handle Fajr calculation
_value = solarTime.hourAngle(-calculationParameters.fajrAngle, false);

if (_valid(_value)) {
timeComponents = TimeComponents.fromDouble(_value);
tempFajr = timeComponents.dateComponents(date);
Expand Down Expand Up @@ -188,6 +260,7 @@ class PrayerTimes {
tempIsha = sunsetComponents
.add(Duration(seconds: calculationParameters.ishaInterval * 60));
} else {
// Handle Isha calculation
_value = solarTime.hourAngle(-calculationParameters.ishaAngle!, true);

if (calculationParameters.ishaAngle != null && _valid(_value)) {
Expand Down Expand Up @@ -221,6 +294,7 @@ class PrayerTimes {

tempMaghrib = sunsetComponents;
if (calculationParameters.maghribAngle != null) {
// Handle angle-based Maghrib calculation
final angleBasedMaghrib = TimeComponents.fromDouble(solarTime.hourAngle(
-1 * calculationParameters.maghribAngle!, true))
.dateComponents(date);
Expand Down
Loading
Loading