UNPKG

@yachteye/signalk-makkah-plugin

Version:
700 lines (699 loc) 24.5 kB
"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; };