package com.github.praytimes;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import static com.github.praytimes.StaticUtils.deg;
import static com.github.praytimes.StaticUtils.dtr;
import static com.github.praytimes.StaticUtils.fixHour;
import static com.github.praytimes.StaticUtils.min;
import static com.github.praytimes.StaticUtils.rtd;
public class PrayTimesCalculator {
// default times
private static Map<PrayTime, Double> _defaultTimes;
static {
_defaultTimes = new HashMap<>();
_defaultTimes.put(PrayTime.IMSAK, 5d / 24);
_defaultTimes.put(PrayTime.FAJR, 5d / 24);
_defaultTimes.put(PrayTime.SUNRISE, 6d / 24);
_defaultTimes.put(PrayTime.DHUHR, 12d / 24);
_defaultTimes.put(PrayTime.ASR, 13d / 24);
_defaultTimes.put(PrayTime.SUNSET, 18d / 24);
_defaultTimes.put(PrayTime.MAGHRIB, 18d / 24);
_defaultTimes.put(PrayTime.ISHA, 18d / 24);
_defaultTimes = Collections.unmodifiableMap(_defaultTimes); // immutable
}
private final MinuteOrAngleDouble _imsak = min(10);
private final MinuteOrAngleDouble _dhuhr = min(0);
private final CalculationMethod.AsrJuristics _asr = CalculationMethod.AsrJuristics.Standard;
private final CalculationMethod.HighLatMethods _highLats = CalculationMethod.HighLatMethods.NightMiddle;
private final CalculationMethod _method;
private boolean _dst;
private double _timeZone;
private double _jDate;
private Coordinate _coordinate;
public PrayTimesCalculator() {
this(CalculationMethod.MWL); // default method
}
public PrayTimesCalculator(CalculationMethod method) {
_method = method; // default method
}
//
// Calculation Logic
//
//
Map<PrayTime, Clock> calculate(Date date, Coordinate coordinate,
Double timeZone, Boolean dst) {
_coordinate = coordinate;
_timeZone = timeZone != null ? timeZone : getTimeZone(date);
_dst = dst != null ? dst : getDst(date);
if (_dst) {
_timeZone++;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
_jDate = julian(year, month, day) - _coordinate.getLongitude() / (15d * 24d);
// compute prayer times at given julian date
Map<PrayTime, Double> times = new HashMap<>();
times.put(PrayTime.IMSAK, sunAngleTime(_imsak, _defaultTimes.get(PrayTime.IMSAK), true));
times.put(PrayTime.FAJR, sunAngleTime(_method.getFajr(), _defaultTimes.get(PrayTime.FAJR), true));
times.put(PrayTime.SUNRISE, sunAngleTime(riseSetAngle(), _defaultTimes.get(PrayTime.SUNRISE), true));
times.put(PrayTime.DHUHR, midDay(_defaultTimes.get(PrayTime.DHUHR)));
times.put(PrayTime.ASR, asrTime(asrFactor(), _defaultTimes.get(PrayTime.ASR)));
times.put(PrayTime.SUNSET, sunAngleTime(riseSetAngle(), _defaultTimes.get(PrayTime.SUNSET)));
times.put(PrayTime.MAGHRIB, sunAngleTime(_method.getMaghrib(), _defaultTimes.get(PrayTime.MAGHRIB)));
times.put(PrayTime.ISHA, sunAngleTime(_method.getIsha(), _defaultTimes.get(PrayTime.ISHA)));
times = adjustTimes(times);
// add midnight time
times.put(PrayTime.MIDNIGHT, (_method.getMidnight() == CalculationMethod.MidnightType.Jafari)
? times.get(PrayTime.SUNSET) + timeDiff(times.get(PrayTime.SUNSET), times.get(PrayTime.FAJR)) / 2
: times.get(PrayTime.SUNSET) + timeDiff(times.get(PrayTime.SUNSET), times.get(PrayTime.SUNRISE)) / 2);
Map<PrayTime, Clock> result = new HashMap<>();
for (Map.Entry<PrayTime, Double> i : times.entrySet()) {
result.put(i.getKey(), Clock.fromDouble(i.getValue()));
}
return result;
}
Map<PrayTime, Clock> calculate(Date date, Coordinate coordinate,
Double timeZone) {
return calculate(date, coordinate, timeZone, null);
}
public Map<PrayTime, Clock> calculate(Date date, Coordinate coordinate) {
return calculate(date, coordinate, null);
}
// compute mid-day time
private double midDay(double time) {
double eqt = sunPosition(_jDate + time).getEquation();
double noon = fixHour(12 - eqt);
return noon;
}
// compute the time at which sun reaches a specific angle below horizon
private double sunAngleTime(MinuteOrAngleDouble angle, double time,
boolean ccw) {
// TODO: I must enable below line!
// if (angle.isMin()) throw new IllegalArgumentException("angle argument must be degree, not minute!");
double decl = sunPosition(_jDate + time).getDeclination();
double noon = dtr(midDay(time));
double t = Math.acos((-Math.sin(dtr(angle.getValue())) - Math.sin(decl)
* Math.sin(dtr(_coordinate.getLatitude())))
/ (Math.cos(decl) * Math.cos(dtr(_coordinate.getLatitude())))) / 15d;
return rtd(noon + (ccw ? -t : t));
}
private double sunAngleTime(MinuteOrAngleDouble angle, double time) {
return sunAngleTime(angle, time, false);
}
// compute asr time
private double asrTime(double factor, double time) {
double decl = sunPosition(_jDate + time).getDeclination();
double angle = -Math.atan(1 / (factor + Math.tan(dtr(_coordinate.getLatitude()) - decl)));
return sunAngleTime(deg(rtd(angle)), time);
}
// compute declination angle of sun and equation of time
// Ref: http://aa.usno.navy.mil/faq/docs/SunApprox.php
private DeclEqt sunPosition(double jd) {
double D = jd - 2451545d;
double g = (357.529 + 0.98560028 * D) % 360;
double q = (280.459 + 0.98564736 * D) % 360;
double L = (q + 1.915 * Math.sin(dtr(g)) + 0.020 * Math.sin(dtr(2d * g))) % 360;
// weird!
// double R = 1.00014 - 0.01671 * Math.cos(dtr(g)) - 0.00014 *
// Math.cos(dtr(2d * g));
double e = 23.439 - 0.00000036 * D;
double RA = rtd(Math.atan2(Math.cos(dtr(e)) * Math.sin(dtr(L)), Math.cos(dtr(L)))) / 15d;
double eqt = q / 15d - fixHour(RA);
double decl = Math.asin(Math.sin(dtr(e)) * Math.sin(dtr(L)));
return new DeclEqt(decl, eqt);
}
// convert Gregorian date to Julian day
// Ref: Astronomical Algorithms by Jean Meeus
private double julian(int year, int month, int day) {
if (month <= 2) {
year -= 1;
month += 12;
}
double A = Math.floor((double) year / 100);
double B = 2 - A + Math.floor(A / 4);
double JD = Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
return JD;
}
// adjust times
private Map<PrayTime, Double> adjustTimes(Map<PrayTime, Double> times) {
Map<PrayTime, Double> result = new HashMap<PrayTime, Double>();
for (Map.Entry<PrayTime, Double> i : times.entrySet()) {
result.put(i.getKey(), i.getValue() + _timeZone - _coordinate.getLongitude() / 15d);
}
if (_highLats != CalculationMethod.HighLatMethods.None) {
result = adjustHighLats(result);
}
if (_imsak.isMin()) {
result.put(PrayTime.IMSAK, result.get(PrayTime.FAJR) - _imsak.getValue() / 60);
}
if (_method.getMaghrib().isMin()) {
result.put(PrayTime.MAGHRIB, result.get(PrayTime.SUNSET) + _method.getMaghrib().getValue() / 60d);
}
if (_method.getIsha().isMin()) {
result.put(PrayTime.ISHA, result.get(PrayTime.MAGHRIB) + _method.getIsha().getValue() / 60d);
}
result.put(PrayTime.DHUHR, result.get(PrayTime.DHUHR) + _dhuhr.getValue() / 60d);
return result;
}
// Section 2!! (Compute Prayer Time in JS code)
//
// get asr shadow factor
private double asrFactor() {
return _asr == CalculationMethod.AsrJuristics.Hanafi ? 2d : 1d;
}
// return sun angle for sunset/sunrise
private MinuteOrAngleDouble riseSetAngle() {
// var earthRad = 6371009; // in meters
// var angle = DMath.arccos(earthRad/(earthRad+ elv));
double angle = 0.0347 * Math.sqrt(_coordinate.getElevation()); // an approximation
return deg(0.833 + angle);
}
// adjust times for locations in higher latitudes
private Map<PrayTime, Double> adjustHighLats(Map<PrayTime, Double> times) {
double nightTime = timeDiff(times.get(PrayTime.SUNSET),
times.get(PrayTime.SUNRISE));
times.put(PrayTime.IMSAK, adjustHLTime(times.get(PrayTime.IMSAK),
times.get(PrayTime.SUNRISE), _imsak.getValue(), nightTime, true));
times.put(PrayTime.FAJR, adjustHLTime(times.get(PrayTime.FAJR), times.get(PrayTime.SUNRISE),
_method.getFajr().getValue(), nightTime, true));
times.put(PrayTime.ISHA, adjustHLTime(times.get(PrayTime.ISHA), times.get(PrayTime.SUNSET),
_method.getIsha().getValue(), nightTime));
times.put(PrayTime.MAGHRIB, adjustHLTime(times.get(PrayTime.MAGHRIB),
times.get(PrayTime.SUNSET), _method.getMaghrib().getValue(), nightTime));
return times;
}
// adjust a time for higher latitudes
private double adjustHLTime(double time, double bbase, double angle,
double night, boolean ccw) {
double portion = nightPortion(angle, night);
double timeDiff = ccw ? timeDiff(time, bbase) : timeDiff(bbase, time);
if (Double.isNaN(time) || timeDiff > portion)
time = bbase + (ccw ? -portion : portion);
return time;
}
private double adjustHLTime(double time, double bbase, double angle,
double night) {
return adjustHLTime(time, bbase, angle, night, false);
}
// the night portion used for adjusting times in higher latitudes
private double nightPortion(double angle, double night) {
double portion = 1d / 2d;
if (_highLats == CalculationMethod.HighLatMethods.AngleBased) {
portion = 1d / 60d * angle;
}
if (_highLats == CalculationMethod.HighLatMethods.OneSeventh) {
portion = 1 / 7;
}
return portion * night;
}
// get local time zone
private double getTimeZone(Date date) {
return TimeZone.getDefault().getRawOffset() / (60 * 60 * 1000.0);
}
//
// Time Zone Functions
//
//
// get daylight saving for a given date
private boolean getDst(Date date) {
return TimeZone.getDefault().inDaylightTime(date);
}
// compute the difference between two times
private double timeDiff(double time1, double time2) {
return fixHour(time2 - time1);
}
//
// Misc Functions
//
//
private class DeclEqt {
private final double declination;
private final double equation;
public DeclEqt(double declination, double equation) {
super();
this.declination = declination;
this.equation = equation;
}
public double getDeclination() {
return declination;
}
public double getEquation() {
return equation;
}
}
}