@yachteye/signalk-makkah-plugin
Version:
Add Salah and Sun times to the SignalK graph
700 lines (699 loc) • 24.5 kB
JavaScript
"use strict";
/**
* Based on: http://praytimes.org/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrayTime = void 0;
/** Adjustment method for high latitudes. */
var Adjustment;
(function (Adjustment) {
Adjustment["None"] = "None";
Adjustment["MidNight"] = "NightMiddle";
Adjustment["OneSeventh"] = "OneSeventh";
Adjustment["AngleBased"] = "AngleBased";
})(Adjustment || (Adjustment = {}));
var PrayerCalculationMethod;
(function (PrayerCalculationMethod) {
/** Jafari - Shia Ithna-Ashari, Leva Institute, Qum */
PrayerCalculationMethod["Jafari"] = "Jafari";
/** Karachi - University of Islamic Sciences, Karachi */
PrayerCalculationMethod["Karachi"] = "Karachi";
/** ISNA- Islamic Society of North America (ISNA) */
PrayerCalculationMethod["ISNA"] = "ISNA";
/** MWL - Muslim World League */
PrayerCalculationMethod["MWL"] = "MWL";
/** Makkah - Umm Al-Qura University, Makkah */
PrayerCalculationMethod["Makkah"] = "Makkah";
/** Egypt - Egyptian General Authority of Survey */
PrayerCalculationMethod["Egypt"] = "Egypt";
/** Tehran - Institute of Geophysics, University of Tehran */
PrayerCalculationMethod["Tehran"] = "Tehran";
})(PrayerCalculationMethod || (PrayerCalculationMethod = {}));
/** For Asr calculation. */
var AsrJuristic;
(function (AsrJuristic) {
/** Shafii, Maliki, Jafari and Hanbali (shadow factor = 1) */
AsrJuristic[AsrJuristic["Standard"] = 0] = "Standard";
/** Hanafi school of tought (shadow factor = 2) */
AsrJuristic[AsrJuristic["Hanafi"] = 1] = "Hanafi";
})(AsrJuristic || (AsrJuristic = {}));
class PrayTime {
constructor() {
/** Name and parameters for each calculation method, keyed by the method-name. */
this.methods = {
'MWL': {
displayName: 'Muslim World League',
params: { fajr: 18, isha: 17 },
},
'ISNA': {
displayName: 'Islamic Society of North America (ISNA)',
params: { fajr: 15, isha: 15 },
},
'Egypt': {
displayName: 'Egyptian General Authority of Survey',
params: { fajr: 19.5, isha: 17.5 },
},
'Makkah': {
displayName: 'Umm Al-Qura University, Makkah',
params: { fajr: 18.5, isha: '90 min' }, // fajr was 19 degrees before 1430 hijri
},
'Karachi': {
displayName: 'University of Islamic Sciences, Karachi',
params: { fajr: 18, isha: 18 },
},
'Tehran': {
displayName: 'Institute of Geophysics, University of Tehran',
params: { fajr: 17.7, isha: 14, maghrib: 4.5, midnight: 'Jafari' }, // isha is not explicitly specified in this method
},
'Jafari': {
displayName: 'Shia Ithna-Ashari, Leva Institute, Qum',
params: { fajr: 16, isha: 14, maghrib: 4, midnight: 'Jafari' },
},
};
/** The settings that are used. */
this.setting = {
imsak: '10 min',
dhuhr: '0 min',
asr: 'Standard',
highLats: Adjustment.MidNight,
};
/** Not implemented. */
this.offset = {};
/** The prayer time calculation methods to use. */
this.calcMethod = PrayerCalculationMethod.MWL;
/** String to denote an invalid time. */
this.InvalidTime = '----';
/** Either '24h', '12h', '12hNS' (12-hour format with no suffix) or 'Float'. */
this.timeFormat = '24h';
/** Number of iterations used to compute (prayer) times. */
this.numIterations = 1;
/** latitude (decimal degrees). */
this.lat = 0;
/** longitude (decimal degrees). */
this.lng = 0;
/** elevation (meters) - not used. */
this.elv = 0;
/** Time zone (hours). */
this.timeZone = 0;
/** Julian date, with the longitude applied. */
this.JDate = 0;
/** Default calculation parameters. */
const defaultParams = {
maghrib: '0 min',
midnight: 'Standard',
};
// Merge the defaultParams with the parameters that are defined in this.methods.
Object.keys(this.methods).forEach((methodName) => {
const params = this.methods[methodName].params;
Object.keys(defaultParams).forEach((val) => {
if (typeof params[val] === 'undefined') {
params[val] = defaultParams[val];
}
});
});
// Initialize settings
const params = this.methods[this.calcMethod].params;
Object.keys(params).forEach((val) => {
this.setting[val] = params[val];
});
}
/**
* Set calculation method.
* @param methodName Name of the method (e.g. 'MWL').
*/
setMethod(methodName) {
if (this.methods[methodName]) {
this.adjust(this.methods[methodName].params);
this.calcMethod = PrayerCalculationMethod[methodName];
}
else {
throw new Error('setMethod(): invalid methodName: ' + methodName);
}
}
/**
* Set calculating parameters.
* @param params
*/
adjust(params) {
Object.keys(params).forEach((val) => {
this.setting[val] = params[val];
});
}
/**
* Set time offsets.
* @param timeOffsets
*/
tune(timeOffsets) {
// not implemented.
// for (var i in timeOffsets) {
// this.offset[i] = timeOffsets[i];
// }
}
/**
* Get current calculation method.
* @returns
*/
getMethod() {
return this.calcMethod;
}
/**
* Get current setting
* @returns
*/
getSetting() {
return this.setting;
}
/**
* Get current time offsets.
* @returns
*/
getOffsets() {
return this.offset;
}
/**
* get default calc parametrs
* @returns
*/
getDefaults() {
return this.methods;
}
/**
* Calculate julian date from a calendar date.
* JD is a continuous count of days and fractions since noon Universal Time on January 1, 4713 BC.
* Ref: Astronomical Algorithms by Jean Meeus
* @param year 4 digits
* @param month 1 - 12
* @param day 1 - 31
* @returns days since January 1, 4713 BC
*/
JulianDate(year, month, day) {
if (month <= 2) {
year -= 1;
month += 12;
}
const A = Math.floor(year / 100);
const B = 2 - A + Math.floor(A / 4);
const JD = Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
return JD;
}
/**
* Compute time (hours) for a given angle G.
* (compute the time at which sun reaches a specific angle below horizon)
* @param G Angle to calculate (degrees).
* @param t Time as portion of a 24 hour day.
* @returns Time in hours (can be a NaN)
*/
computeTime(G, t) {
// console.log('computeTime()', 'G=', G, 't=', t);
const D = this.sunDeclination(this.JDate + t);
const Z = this.computeMidDay(t);
// NB: V can become a NaN at high latitudes!
const V = (1.0 / 15.0) * this.darccos((-this.dsin(G) - this.dsin(D) * this.dsin(this.lat)) / (this.dcos(D) * this.dcos(this.lat)));
return Z + (G > 90 ? -V : V);
}
/**
* Return prayer times for a given date, position and time zone.
* @param year Four digit year.
* @param month The month component, expressed as a value between 1 and 12.
* @param day The day component (value between 1 and 31).
* @param latitude In decimal degrees.
* @param longitude In decimal degrees.
* @param timeZone Time zone (hours).
* @returns An object with prayer times in the correct format (default 24 hours 'hh:mm'). In case a time can not be calculated the properties are NULL!
*/
getDatePrayerTimes(year, month, day, latitude, longitude, timeZone) {
// console.log('getDatePrayerTimes()', year, month, day, latitude, longitude, 'tz=', timeZone);
this.lat = latitude;
this.lng = longitude;
this.elv = 0; // Not used at the moment.
this.timeZone = timeZone;
this.JDate = this.JulianDate(year, month, day) - longitude / (15 * 24);
return this.computeDayTimes();
}
// /**
// * Set the calculation method
// * @param methodID
// */
// public setCalcMethod(methodID: number): void {
// this.calcMethod = methodID;
// }
// public setAsrMethod(m: AsrJuristic): void {
// this.asrJuristic = m;
// }
/**
* compute declination angle of sun and equation of time
* Ref: http://aa.usno.navy.mil/faq/docs/SunApprox.php
* @param jd Julian date.
* @returns
*/
sunPosition(jd) {
const D = jd - 2451545.0;
const g = this.fixAngle(357.529 + 0.98560028 * D);
const q = this.fixAngle(280.459 + 0.98564736 * D);
const L = this.fixAngle(q + 1.915 * this.dsin(g) + 0.02 * this.dsin(2 * g));
//double R = 1.00014 - 0.01671 * this.dcos(g) - 0.00014 * this.dcos(2 * g);
const e = 23.439 - 0.00000036 * D;
const d = this.darcsin(this.dsin(e) * this.dsin(L));
let RA = this.darctan2(this.dcos(e) * this.dsin(L), this.dcos(L)) / 15;
RA = this.fixHour(RA);
const EqT = q / 15 - RA;
return { declination: d, equation: EqT };
}
/**
* Compute equation of time.
* @param jd Julian date.
* @returns
*/
equationOfTime(jd) {
return this.sunPosition(jd).equation;
}
/**
* Compute the declination angle of the sun (in decimal degrees).
* @param jd Julian date.
* @returns
*/
sunDeclination(jd) {
return this.sunPosition(jd).declination;
}
/**
* Compute mid-day (Dhuhr, Zawal) time
* @param t Time as fraction of 24 hrs.
* @returns
*/
computeMidDay(t) {
const T = this.equationOfTime(this.JDate + t);
const Z = this.fixHour(12 - T);
return Z;
}
/**
* Compute the time of Asr.
* @param step Majority of schools (including Shafi'i, Maliki, Ja'fari, and Hanbali): step=1, Hanafi school: step=2.
* @param t Time as fraction of the day.
* @returns
*/
computeAsr(step, t) {
// console.log('computeAsr()', 'step=', step, 't=', t);
const D = this.sunDeclination(this.JDate + t);
const G = -this.darccot(step + this.dtan(Math.abs(this.lat - D)));
return this.computeTime(G, t);
}
/**
* Compute prayer times at given julian date.
* @param times Default or start values to use in the calculation (hours).
* @returns
*/
computeTimes(times) {
const t = this.dayPortion(times);
const Imsak = this.computeTime(180 - this.paramValue(this.setting.imsak), t.imsak);
const Fajr = this.computeTime(180 - this.paramValue(this.setting.fajr), t.fajr);
const Sunrise = this.computeTime(180 - 0.833, t.sunrise);
const Dhuhr = this.computeMidDay(t.dhuhr);
var Asr = this.computeAsr(this.paramValueToAsrFactor(this.setting.asr), times.asr);
const Sunset = this.computeTime(0.833, t.sunset);
const Maghrib = this.computeTime(this.paramValue(this.setting.maghrib), t.maghrib);
const Isha = this.computeTime(this.paramValue(this.setting.isha), t.isha);
return {
imsak: Imsak, fajr: Fajr, sunrise: Sunrise, dhuhr: Dhuhr,
asr: Asr, sunset: Sunset, maghrib: Maghrib, isha: Isha, midnight: times.midnight,
};
}
/**
* Get the parameter value as a number.
* It is up to the caller to check if the parameter is a number or an offset (e.g. '10 min').
* @param v The parameter value to use.
* @returns Parameter value as a number.
*/
paramValue(v) {
if (v === undefined) {
throw new Error('Parameter value is undefined');
}
if (typeof v === 'number') {
return v;
}
if (typeof v === 'string') {
const arr = v.split(' ');
if (arr.length === 2 && arr[1] === 'min') {
// This is a 'X min' value, denoting a time offset (minutes) that is handled later on.
return parseInt(arr[0], 10);
}
throw new Error('Parameter value is unexpected string: ' + v);
}
throw new Error('Parameter value is unexpected: ' + v);
}
/**
* Return true in case the parameter value denotes a time offset (minutes).
* @param v The parameter value to check.
* @returns
*/
paramValueIsMinuteOffset(v) {
if (v !== undefined && typeof v === 'string') {
const arr = v.split(' ');
if (arr.length === 2 && arr[1] === 'min') {
// This is a 'X min' value, denoting a time offset (minutes).
return true;
}
}
return false;
}
/**
* Get the parameter value as a Asr factor.
* @param v The parameter value to check.
* @returns The factor value: either 1 (Standard) or 2 (Hanafi).
*/
paramValueToAsrFactor(v) {
if (v !== undefined) {
if (typeof v === 'string') {
var factor = { Standard: 1, Hanafi: 2 }[v];
if (factor !== undefined) {
return factor;
}
}
if (typeof v === 'number' && v >= 1 && v <= 2) {
return v;
}
}
throw new Error('Parameter value for Asr is incorrect: ' + v);
}
/**
* Adjust Imsak, Fajr, Isha and Maghrib times for locations in higher latitudes.
* @description Note: at higher latitudes the sunrise/sunset values are NaN, so these calculations do not seem to make sense.
* Therefore a check on isNaN(nightTime) is added.
* @param times The prayer times (in hours).
* @returns
*/
adjustHighLatTimes(times) {
const nightTime = this.getTimeDifference(times.sunset, times.sunrise); // sunset to sunrise.
// console.log('adjustHighLatTimes()', 'nightTime=', nightTime, times.sunset, times.sunrise);
if (false === isNaN(nightTime)) {
// Imsak.
// Skip if we use an offset for Imsak, the value will not be used anyway (see adjustTimes()).
if (false === this.paramValueIsMinuteOffset(this.setting.imsak)) {
const nightPortion = this.nightPortion(this.paramValue(this.setting.imsak), nightTime);
const timeDiff = this.getTimeDifference(times.imsak, times.sunrise);
if (isNaN(times.imsak) || timeDiff > nightPortion) {
times.imsak = times.sunrise - nightPortion;
}
}
// Adjust Fajr.
{
const nightPortion = this.nightPortion(this.paramValue(this.setting.fajr), nightTime);
const timeDiff = this.getTimeDifference(times.fajr, times.sunrise);
if (isNaN(times.fajr) || timeDiff > nightPortion) {
times.fajr = times.sunrise - nightPortion;
}
}
// Adjust Isha.
// Skip if we use an offset for Isha, the value will not be used anyway (see adjustTimes()).
if (false === this.paramValueIsMinuteOffset(this.setting.isha)) {
const nightPortion = this.nightPortion(this.paramValue(this.setting.isha), nightTime);
const timeDiff = this.getTimeDifference(times.sunset, times.isha);
if (isNaN(times.isha) || timeDiff > nightPortion) {
times.isha = times.sunset + nightPortion;
}
}
// Adjust Maghrib.
// Skip if we use an offset for Maghrib, the value will not be used anyway (see adjustTimes()).
if (false === this.paramValueIsMinuteOffset(this.setting.maghrib)) {
const nightPortion = this.nightPortion(this.paramValue(this.setting.maghrib), nightTime);
const timeDiff = this.getTimeDifference(times.sunset, times.maghrib);
if (isNaN(times.maghrib) || timeDiff > nightPortion) {
times.maghrib = times.sunset + nightPortion;
}
}
}
return times;
}
/**
* The night portion used for adjusting times in higher latitudes.
* @param angle
* @param nightTime
* @returns
*/
nightPortion(angle, nightTime) {
let portion = 1 / 2; // MidNight.
if (this.setting.highLats === Adjustment.AngleBased) {
portion = 1 / 60 * angle;
}
else if (this.setting.highLats === Adjustment.MidNight) {
portion = 1 / 2;
}
else if (this.setting.highLats === Adjustment.OneSeventh) {
portion = 1 / 7;
}
return portion * nightTime;
}
/**
* Convert hours to day portions (e.g. 12 hours becomes 0.5).
* @param times
* @returns
*/
dayPortion(times) {
Object.keys(times).forEach((value) => {
times[value] /= 24;
});
return times;
}
/**
* Compute the Salah times.
* @returns Salah times in the correct format (default 24 hours 'hh:mm').
*/
computeDayTimes() {
// Default times:
let times = {
imsak: 5,
fajr: 5,
sunrise: 6,
dhuhr: 12,
asr: 13,
sunset: 18,
maghrib: 18,
isha: 18,
midnight: 0,
};
for (let i = 0; i < this.numIterations; i++) {
times = this.computeTimes(times);
}
times = this.adjustTimes(times);
// Add midnight time.
times.midnight = (this.setting.midnight === 'Jafari') ?
times.sunset + this.getTimeDifference(times.sunset, times.fajr) / 2 :
times.sunset + this.getTimeDifference(times.sunset, times.sunrise) / 2;
// times = this.tuneTimes(times); // Not supported.
return this.adjustTimesFormat(times);
}
/**
* Adjust prayer times for: longitude, time zone, high latitudes and settings.
* @param times The prayer times (in hours).
* @returns Adjusted prayer times.
*/
adjustTimes(times) {
Object.keys(times).forEach((value) => {
times[value] += this.timeZone - this.lng / 15;
});
if (this.setting.highLats !== Adjustment.None) {
times = this.adjustHighLatTimes(times);
}
// Imsak.
if (this.paramValueIsMinuteOffset(this.setting.imsak)) {
times.imsak = times.fajr - this.paramValue(this.setting.imsak) / 60;
}
// Maghrib.
if (this.paramValueIsMinuteOffset(this.setting.maghrib)) {
times.maghrib = times.sunset + this.paramValue(this.setting.maghrib) / 60;
}
// Isha.
if (this.paramValueIsMinuteOffset(this.setting.isha)) {
times.isha = times.maghrib + this.paramValue(this.setting.isha) / 60;
}
// Dhuhr.
if (this.paramValueIsMinuteOffset(this.setting.dhuhr)) {
times.dhuhr += this.paramValue(this.setting.dhuhr) / 60;
}
return times;
}
/**
* Format the prayer times.
* @param times The prayer times in hours.
* @returns The prayer times according to the defined string format.
*/
adjustTimesFormat(times) {
const formatted = {
imsak: null,
fajr: null,
sunrise: null,
dhuhr: null,
asr: null,
sunset: null,
maghrib: null,
isha: null,
midnight: null
};
if (this.timeFormat === 'Float') {
Object.keys(times).forEach((val) => {
if (isNaN(times[val])) {
formatted[val] = this.InvalidTime;
}
else {
formatted[val] = times[val].toFixed(4);
}
});
return formatted;
}
Object.keys(times).forEach((val) => {
if (this.timeFormat === '12h') {
formatted[val] = this.floatToTime12(times[val], true);
}
else if (this.timeFormat === '12hNS') {
formatted[val] = this.floatToTime12NS(times[val]);
}
else {
formatted[val] = this.floatToTime24(times[val]);
}
});
return formatted;
}
/**
* Compute the difference between two times.
* @param c1
* @param c2
* @returns
*/
getTimeDifference(c1, c2) {
const diff = this.fixHour(c2 - c1);
return diff;
}
twoDigitsFormat(num) {
return num < 10 ? '0' + num : num + '';
}
/**
* Convert time to 'hh:mm' format (0-24 hours).
* @param time Time in hours.
* @returns Time in 'hh:mm' format (0-24 hours) or null.
*/
floatToTime24(time) {
if (time === null || isNaN(time)) {
return null;
}
if (time < 0) {
time += 24;
}
time = this.fixHour(time + 0.5 / 60); // add 0.5 minutes to round
const hours = Math.floor(time);
const minutes = Math.floor((time - hours) * 60);
return this.twoDigitsFormat(hours) + ':' + this.twoDigitsFormat(minutes);
}
floatToTime12NS(time) {
return this.floatToTime12(time, true);
}
floatToTime12(time, noSuffix) {
if (isNaN(time)) {
return null;
}
if (time < 0) {
return this.InvalidTime;
}
time = this.fixHour(time + 0.5 / 60); // add 0.5 minutes to round
let hours = Math.floor(time);
const minutes = Math.floor((time - hours) * 60);
const suffix = hours >= 12 ? ' pm' : ' am';
hours = ((hours + 12 - 1) % 12) + 1;
return hours + ':' + this.twoDigitsFormat(minutes) + (noSuffix ? '' : suffix);
}
/**
* Return the sine of the angle.
* @param degrees angle (degrees).
* @returns
*/
dsin(degrees) {
return Math.sin(degreeToRadian(degrees));
}
/**
* Return the cosine of the angle.
* @param degrees angle (degrees).
* @returns
*/
dcos(degrees) {
return Math.cos(degreeToRadian(degrees));
}
/**
* Return the tangent of the angle.
* @param degrees angle (degrees).
* @returns
*/
dtan(degrees) {
return Math.tan(degreeToRadian(degrees));
}
/**
* Return the arcsine of x, in degrees.
* @param x
* @returns
*/
darcsin(x) {
return radianToDegree(Math.asin(x));
}
/**
* Return the arc cosine of x, in degrees.
* @param x
* @returns
*/
darccos(x) {
return radianToDegree(Math.acos(x));
}
darctan(x) {
return radianToDegree(Math.atan(x));
}
/**
* Return the atan2 of (y,x) in degrees.
* @param y
* @param x
* @returns
*/
darctan2(y, x) {
return radianToDegree(Math.atan2(y, x));
}
/**
* Return the arctangent of 1/x, in degrees.
* @param x
* @returns
*/
darccot(x) {
return radianToDegree(Math.atan(1 / x));
}
/**
* Return a value between 0 (inclusive) and 360 (exclusive).
* @param angle Angle in degrees.
* @returns
*/
fixAngle(angle) {
angle = angle - 360.0 * Math.floor(angle / 360.0);
angle = angle < 0 ? angle + 360.0 : angle;
return angle;
}
/**
* Range reduce hours to 0..23
* @param hour
* @returns
*/
fixHour(hour) {
hour = hour - 24.0 * Math.floor(hour / 24.0);
hour = hour < 0 ? hour + 24.0 : hour;
return hour;
}
}
exports.PrayTime = PrayTime;
/**
*
* @param degree
* @returns
*/
const degreeToRadian = (degree) => {
return (degree * Math.PI) / 180.0;
};
/**
*
* @param radian
* @returns
*/
const radianToDegree = (radian) => {
return (radian * 180.0) / Math.PI;
};