mirror of
https://github.com/acamarata/pray-calc.git
synced 2026-06-30 19:04:26 +00:00
feat: improve Asr solver, dynamic twilight angles, and MSC algorithm
This commit is contained in:
parent
eb68c97b42
commit
b5ee3ca8b8
7 changed files with 264 additions and 124 deletions
10
.npmignore
Normal file
10
.npmignore
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Ignore Git history
|
||||
.git
|
||||
# Ignore local dev/test files
|
||||
/tests/
|
||||
/bin/
|
||||
**/*.c
|
||||
**/*.h
|
||||
getMoonVisibility.js
|
||||
getMSC.js
|
||||
test*.js
|
||||
70
getAngles.js
70
getAngles.js
|
|
@ -1,27 +1,63 @@
|
|||
// getAngles.js
|
||||
'use strict';
|
||||
|
||||
const PI = Math.PI;
|
||||
|
||||
/**
|
||||
* Calculate dynamic Fajr and Isha depression angles based on the current date (season)
|
||||
* plus refraction and elevation adjustments.
|
||||
*
|
||||
* @param {number} [elevation=0] Observer elevation in meters.
|
||||
* @param {number} [pressure=1013.25] Atmospheric pressure in mbar.
|
||||
* @param {number} [temperature=15] Temperature in °C.
|
||||
* @returns {{ fajrAngle: number, ishaAngle: number }} Depression angles in degrees.
|
||||
*/
|
||||
function getAngles(elevation = 0, pressure = 1013.25, temperature = 15) {
|
||||
const baseAngle = 18; // Base angle for astronomical twilight
|
||||
// 1) Compute day of year (1–365/366)
|
||||
const today = new Date();
|
||||
const start = Date.UTC(today.getFullYear(), 0, 0);
|
||||
const diffMs = today - start;
|
||||
const dayOfYear = Math.floor(diffMs / 86400000);
|
||||
|
||||
// Calculate refraction adjusted angle at the horizon (altitude = 0)
|
||||
const refraction = calculateAtmosphericRefraction(0, pressure, temperature);
|
||||
// 2) Approximate solar declination δ (radians):
|
||||
// δ = 23.44° * sin(2π * (day + 284) / 365)
|
||||
const declRad = (23.44 * Math.sin(2 * PI * (dayOfYear + 284) / 365)) * (PI / 180);
|
||||
|
||||
// Elevation adjustment (approximate)
|
||||
const elevationAdjustment = elevation / 1000 * 0.1; // Rough estimate
|
||||
// 3) Seasonal factor (−1 to +1)
|
||||
const seasonalFactor = Math.sin(declRad);
|
||||
|
||||
// Adjust angles for Fajr and Isha
|
||||
const fajrAngle = baseAngle + refraction + elevationAdjustment;
|
||||
const ishaAngle = baseAngle - refraction - elevationAdjustment;
|
||||
// 4) Base and amplitude for twilight angle (±2° variation)
|
||||
const baseAngle = 18.0;
|
||||
const amplitude = 2.0;
|
||||
const seasonalAngle = baseAngle + amplitude * seasonalFactor;
|
||||
|
||||
return { fajrAngle, ishaAngle };
|
||||
// 5) Refraction correction at horizon (altitude = 0)
|
||||
const refraction = calculateAtmosphericRefraction(0, pressure, temperature);
|
||||
|
||||
// 6) Elevation adjustment (0.1° per 1000 m)
|
||||
const elevationAdjustment = (elevation / 1000) * 0.1;
|
||||
|
||||
// 7) Final angles
|
||||
const fajrAngle = seasonalAngle + refraction + elevationAdjustment;
|
||||
const ishaAngle = seasonalAngle - refraction - elevationAdjustment;
|
||||
|
||||
return { fajrAngle, ishaAngle };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute atmospheric refraction correction at a given altitude.
|
||||
*
|
||||
* @param {number} altitude Solar altitude in degrees.
|
||||
* @param {number} pressure Atmospheric pressure in mbar.
|
||||
* @param {number} temperature Temperature in °C.
|
||||
* @returns {number} Refraction correction in degrees.
|
||||
*/
|
||||
function calculateAtmosphericRefraction(altitude, pressure = 1013.25, temperature = 10) {
|
||||
const altInRadians = altitude * Math.PI / 180;
|
||||
let R = 1.0 / Math.tan(altInRadians + 7.31 / (altInRadians + 0.077));
|
||||
R = R / 60; // Convert from arcminutes to degrees
|
||||
R = (pressure / 1010) * (283 / (273 + temperature)) * R;
|
||||
return R;
|
||||
const altRad = altitude * PI / 180;
|
||||
let R = 1.0 / Math.tan(altRad + 7.31 / (altRad + 0.077));
|
||||
R = R / 60; // arcminutes → degrees
|
||||
R = (pressure / 1010) * (283 / (273 + temperature)) * R;
|
||||
return R;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAngles
|
||||
};
|
||||
module.exports = { getAngles };
|
||||
82
getAsr.js
82
getAsr.js
|
|
@ -1,28 +1,66 @@
|
|||
const { getSpa, fractalTime } = require('nrel-spa');
|
||||
// getAsr.js
|
||||
'use strict';
|
||||
|
||||
function getAsr(solarNoonDate, latitude, longitude, tz, standard = true) {
|
||||
const solarNoonAltitude = getSpa(solarNoonDate, latitude, longitude, tz).zenith;
|
||||
const shadowLengthAtNoon = 1 / Math.tan((90 - solarNoonAltitude) * Math.PI / 180);
|
||||
const { SpaData, spa_calculate, SPA_ZA_RTS } = require('nrel-spa/dist/spa');
|
||||
|
||||
// For standard method: Shadow length = object height + shadow length at noon
|
||||
// For Hanafi method: Shadow length = 2 * object height + shadow length at noon
|
||||
const targetShadowLength = standard ? 1 + shadowLengthAtNoon : 2 + shadowLengthAtNoon;
|
||||
/**
|
||||
* Compute Asr time (fractional hours) for a given date and location.
|
||||
*
|
||||
* @param {Date} date - Local date/time for the calculation.
|
||||
* @param {number} latitude - Observer latitude in decimal degrees.
|
||||
* @param {number} longitude - Observer longitude in decimal degrees.
|
||||
* @param {number} timezone - Timezone offset from UTC in hours (negative west).
|
||||
* @param {boolean}[standard] - true for Shāfiʿī (shadow=1), false for Ḥanafī (shadow=2).
|
||||
* @returns {number|null} Fractional‐hour Asr time (local), or null if unreachable.
|
||||
*/
|
||||
function getAsr(date, latitude, longitude, timezone, standard = true) {
|
||||
// Load inputs into SPA struct
|
||||
const data = new SpaData();
|
||||
data.year = date.getFullYear();
|
||||
data.month = date.getMonth() + 1;
|
||||
data.day = date.getDate();
|
||||
data.hour = date.getHours();
|
||||
data.minute = date.getMinutes();
|
||||
data.second = date.getSeconds();
|
||||
data.delta_ut1 = 0.0;
|
||||
data.delta_t = 67.0;
|
||||
data.timezone = timezone;
|
||||
data.longitude = longitude;
|
||||
data.latitude = latitude;
|
||||
data.elevation = 0.0;
|
||||
data.pressure = 1013.0;
|
||||
data.temperature = 15.0;
|
||||
data.slope = 0.0;
|
||||
data.azm_rotation = 0.0;
|
||||
data.atmos_refract = 0.5667;
|
||||
data.function = SPA_ZA_RTS;
|
||||
|
||||
// Increment time from noon to find Asr time
|
||||
let asrTime;
|
||||
for (let i = 0; i < 720; i++) { // Check next 12 hours
|
||||
const testTime = new Date(solarNoonDate.getTime() + i * 60000); // Increment by one minute
|
||||
const currentAltitude = getSpa(testTime, latitude, longitude, tz).zenith;
|
||||
const currentShadowLength = 1 / Math.tan((90 - currentAltitude) * Math.PI / 180);
|
||||
if (currentShadowLength >= targetShadowLength) {
|
||||
asrTime = testTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Perform SPA calculation
|
||||
if (spa_calculate(data) !== 0) return null;
|
||||
|
||||
return asrTime ? asrTime.getHours() + asrTime.getMinutes() / 60 + asrTime.getSeconds() / 3600 : null;
|
||||
// Convert angles to radians
|
||||
const φ = latitude * Math.PI / 180;
|
||||
const δ = data.delta * Math.PI / 180;
|
||||
const transit = data.suntransit; // fractional‐hour solar noon
|
||||
|
||||
// Compute required solar elevation A for Asr:
|
||||
const shadowFactor = standard ? 1 : 2;
|
||||
const X = Math.abs(φ - δ);
|
||||
const opp = 1;
|
||||
const adj = shadowFactor + Math.tan(X);
|
||||
const hyp = Math.hypot(opp, adj);
|
||||
const sinA = opp / hyp;
|
||||
|
||||
// Solve hour‐angle H0: cos(H0) = (sinA - sinφ·sinδ) / (cosφ·cosδ)
|
||||
const cosH0 = (sinA - Math.sin(φ) * Math.sin(δ)) /
|
||||
(Math.cos(φ) * Math.cos(δ));
|
||||
if (cosH0 < -1 || cosH0 > 1) return null; // sun never reaches A
|
||||
|
||||
// Convert H0 (rad) to hours
|
||||
const H0h = (Math.acos(cosH0) * 180 / Math.PI) / 15;
|
||||
|
||||
// Asr time = solar noon + H0h
|
||||
return transit + H0h;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAsr
|
||||
};
|
||||
module.exports = { getAsr };
|
||||
170
getMSC.js
170
getMSC.js
|
|
@ -1,95 +1,125 @@
|
|||
// getMSC.js
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Base class for Moonsighting.com seasonal interpolation algorithm.
|
||||
* Computes a day-of-year offset (dyy) from the nearest solstice and
|
||||
* interpolates minutes for Fajr (before sunrise) or Isha (after sunset).
|
||||
*/
|
||||
class PrayerTimes {
|
||||
constructor(date, latitude) {
|
||||
this.date = new Date(date);
|
||||
this.latitude = latitude;
|
||||
this.getDyy();
|
||||
}
|
||||
constructor(date, latitude) {
|
||||
this.date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
this.latitude = latitude;
|
||||
this.year = this.date.getFullYear();
|
||||
this.daysInYear = isLeapYear(this.year) ? 366 : 365;
|
||||
this.computeDyy();
|
||||
}
|
||||
|
||||
getDyy() {
|
||||
const year = this.date.getFullYear();
|
||||
const northDate = new Date(`${year}-12-21`);
|
||||
const southDate = new Date(`${year}-06-21`);
|
||||
const zeroDate = this.latitude > 0 ? northDate : southDate;
|
||||
this.dyy = Math.floor((this.date - zeroDate) / (1000 * 60 * 60 * 24));
|
||||
if (this.dyy < 0) {
|
||||
this.dyy += 365;
|
||||
}
|
||||
}
|
||||
computeDyy() {
|
||||
// Reference solstice: Dec 21 (Northern Hemisphere) or Jun 21 (Southern)
|
||||
const northSolstice = new Date(this.year, 11, 21);
|
||||
const southSolstice = new Date(this.year, 5, 21);
|
||||
const zeroDate = this.latitude >= 0 ? northSolstice : southSolstice;
|
||||
|
||||
getMinutes() {
|
||||
if (this.dyy < 91)
|
||||
return this.a + ((this.b - this.a) / 91) * this.dyy;
|
||||
if (this.dyy < 137)
|
||||
return this.b + ((this.c - this.b) / 46) * (this.dyy - 91);
|
||||
if (this.dyy < 183)
|
||||
return this.c + ((this.d - this.c) / 46) * (this.dyy - 137);
|
||||
if (this.dyy < 229)
|
||||
return this.d + ((this.c - this.d) / 46) * (this.dyy - 183);
|
||||
if (this.dyy < 275)
|
||||
return this.c + ((this.b - this.c) / 46) * (this.dyy - 229);
|
||||
return this.b + ((this.a - this.b) / 91) * (this.dyy - 275);
|
||||
let diffDays = Math.floor((this.date - zeroDate) / 86400000);
|
||||
if (diffDays < 0) diffDays += this.daysInYear;
|
||||
this.dyy = diffDays;
|
||||
}
|
||||
|
||||
getMinutesSegment() {
|
||||
const { a, b, c, d, dyy, daysInYear } = this;
|
||||
|
||||
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 {
|
||||
const len = daysInYear - 275;
|
||||
return b + ((a - b) / len) * (dyy - 275);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fajr: returns minutes before sunrise.
|
||||
*/
|
||||
class Fajr extends PrayerTimes {
|
||||
constructor(date, latitude) {
|
||||
super(date, latitude);
|
||||
this.a = 75 + (28.65 / 55) * Math.abs(latitude);
|
||||
this.b = 75 + (19.44 / 55) * Math.abs(latitude);
|
||||
this.c = 75 + (32.74 / 55) * Math.abs(latitude);
|
||||
this.d = 75 + (48.1 / 55) * Math.abs(latitude);
|
||||
}
|
||||
constructor(date, latitude) {
|
||||
super(date, latitude);
|
||||
const latAbs = Math.abs(latitude);
|
||||
this.a = 75 + (28.65 / 55) * latAbs;
|
||||
this.b = 75 + (19.44 / 55) * latAbs;
|
||||
this.c = 75 + (32.74 / 55) * latAbs;
|
||||
this.d = 75 + (48.10 / 55) * latAbs;
|
||||
}
|
||||
|
||||
getMinutesBeforeSunrise() {
|
||||
return Math.round(this.getMinutes());
|
||||
}
|
||||
getMinutesBeforeSunrise() {
|
||||
return Math.round(this.getMinutesSegment());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Isha: returns minutes after sunset.
|
||||
*/
|
||||
class Isha extends PrayerTimes {
|
||||
static SHAFAQ_AHMER = 'ahmer';
|
||||
static SHAFAQ_ABYAD = 'abyad';
|
||||
static SHAFAQ_GENERAL = 'general';
|
||||
static SHAFAQ_GENERAL = 'general';
|
||||
static SHAFAQ_AHMER = 'ahmer';
|
||||
static SHAFAQ_ABYAD = 'abyad';
|
||||
|
||||
constructor(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) {
|
||||
super(date, latitude);
|
||||
this.setShafaq(shafaq);
|
||||
constructor(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) {
|
||||
super(date, latitude);
|
||||
this.setShafaq(shafaq);
|
||||
}
|
||||
|
||||
setShafaq(shafaq) {
|
||||
this.shafaq = shafaq;
|
||||
const latAbs = Math.abs(this.latitude);
|
||||
|
||||
switch (shafaq) {
|
||||
case Isha.SHAFAQ_AHMER:
|
||||
this.a = 62 + (17.4 / 55) * latAbs;
|
||||
this.b = 62 - (7.16 / 55) * latAbs;
|
||||
this.c = 62 + (5.12 / 55) * latAbs;
|
||||
this.d = 62 + (19.44 / 55) * latAbs;
|
||||
break;
|
||||
|
||||
case Isha.SHAFAQ_ABYAD:
|
||||
this.a = 75 + (25.6 / 55) * latAbs;
|
||||
this.b = 75 + (7.16 / 55) * latAbs;
|
||||
this.c = 75 + (36.84 / 55) * latAbs;
|
||||
this.d = 75 + (81.84 / 55) * latAbs;
|
||||
break;
|
||||
|
||||
default: // general
|
||||
this.a = 75 + (25.6 / 55) * latAbs;
|
||||
this.b = 75 + (2.05 / 55) * latAbs;
|
||||
this.c = 75 - (9.21 / 55) * latAbs;
|
||||
this.d = 75 + (6.14 / 55) * latAbs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setShafaq(shafaq) {
|
||||
this.shafaq = shafaq;
|
||||
getMinutesAfterSunset() {
|
||||
return Math.round(this.getMinutesSegment());
|
||||
}
|
||||
}
|
||||
|
||||
if (shafaq === Isha.SHAFAQ_AHMER) {
|
||||
this.a = 62 + (17.4 / 55) * Math.abs(this.latitude);
|
||||
this.b = 62 - (7.16 / 55) * Math.abs(this.latitude);
|
||||
this.c = 62 + (5.12 / 55) * Math.abs(this.latitude);
|
||||
this.d = 62 + (19.44 / 55) * Math.abs(this.latitude);
|
||||
} else if (shafaq === Isha.SHAFAQ_ABYAD) {
|
||||
this.a = 75 + (25.6 / 55) * Math.abs(this.latitude);
|
||||
this.b = 75 + (7.16 / 55) * Math.abs(this.latitude);
|
||||
this.c = 75 + (36.84 / 55) * Math.abs(this.latitude);
|
||||
this.d = 75 + (81.84 / 55) * Math.abs(this.latitude);
|
||||
} else {
|
||||
this.a = 75 + (25.6 / 55) * Math.abs(this.latitude);
|
||||
this.c = 75 - (9.21 / 55) * Math.abs(this.latitude);
|
||||
this.b = 75 + (2.05 / 55) * Math.abs(this.latitude);
|
||||
this.d = 75 + (6.14 / 55) * Math.abs(this.latitude);
|
||||
}
|
||||
}
|
||||
|
||||
getMinutesAfterSunset() {
|
||||
return Math.round(this.getMinutes());
|
||||
}
|
||||
function isLeapYear(year) {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
||||
}
|
||||
|
||||
function getFajr(date, latitude) {
|
||||
const fajr = new Fajr(date, latitude);
|
||||
return fajr.getMinutesBeforeSunrise();
|
||||
return new Fajr(date, latitude).getMinutesBeforeSunrise();
|
||||
}
|
||||
|
||||
function getIsha(date, latitude, shafaq = Isha.SHAFAQ_GENERAL) {
|
||||
const isha = new Isha(date, latitude, shafaq);
|
||||
return isha.getMinutesAfterSunset();
|
||||
return new Isha(date, latitude, shafaq).getMinutesAfterSunset();
|
||||
}
|
||||
|
||||
module.exports = { getFajr, getIsha, Isha, Fajr };
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -1,22 +1,23 @@
|
|||
{
|
||||
"name": "pray-calc",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pray-calc",
|
||||
"version": "1.5.0",
|
||||
"license": "ISC",
|
||||
"version": "1.6.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nrel-spa": "^1.2.2",
|
||||
"nrel-spa": "^1.3.0",
|
||||
"suncalc": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nrel-spa": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/nrel-spa/-/nrel-spa-1.2.2.tgz",
|
||||
"integrity": "sha512-2mycHd7PP0l+pPjPvrT6vr+PRHufCFOxsPcpaIOQ9GwhhfzgyTvSsLKPsqC6ZENNU65yq4PetT5XlWj3CoLjiQ=="
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/nrel-spa/-/nrel-spa-1.3.0.tgz",
|
||||
"integrity": "sha512-vZPD4TjCxGQsrstyyZGtnJtU8y1bJghBki5MR9HbDyhGsO+mHM+l3ffI7l3rIjTJsJBoQP7gyDwQntiPy3vUHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/suncalc": {
|
||||
"version": "1.9.0",
|
||||
|
|
|
|||
36
package.json
36
package.json
|
|
@ -1,19 +1,43 @@
|
|||
{
|
||||
"name": "pray-calc",
|
||||
"version": "1.5.1",
|
||||
"description": "Prayer times calculator using nrel-spa and custom formula for Fajr and Isha angles (as well as traditional static angle methods in the All function)",
|
||||
"version": "1.6.0",
|
||||
"description": "Prayer times calculator using nrel-spa and custom formula for Fajr and Isha angles (as well as traditional static angle methods)",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
},
|
||||
"author": "Ummat",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ummeco/pray-calc.git"
|
||||
},
|
||||
"homepage": "https://github.com/ummeco/pray-calc#readme",
|
||||
"keywords": [
|
||||
"prayer",
|
||||
"islam",
|
||||
"fajr",
|
||||
"dhuhr",
|
||||
"asr",
|
||||
"maghrib",
|
||||
"isha",
|
||||
"nrel-spa",
|
||||
"solar-position"
|
||||
],
|
||||
"author": "Ummat",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ummeco/pray-calc/issues"
|
||||
}
|
||||
},
|
||||
"homepage": "https://github.com/ummeco/pray-calc#readme",
|
||||
"dependencies": {
|
||||
"nrel-spa": "^1.3.0",
|
||||
"suncalc": "^1.9.0"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"getTimes.js",
|
||||
"getAsr.js",
|
||||
"getAngles.js",
|
||||
"dist/",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
5
test.js
5
test.js
|
|
@ -1,13 +1,14 @@
|
|||
const { getTimes, calcTimesAll } = require('./index');
|
||||
|
||||
// Manually setting the date to January 1, 2024
|
||||
const date = new Date('2024-01-01T00:00:00Z');
|
||||
//const date = new Date('2024-01-01T00:00:00Z');
|
||||
const date = new Date();
|
||||
|
||||
// NYC - minimum params
|
||||
const city = "New York"
|
||||
const lat = 40.7128;
|
||||
const lng = -74.006;
|
||||
const tz = -5
|
||||
const tz = -4
|
||||
const elevation = null
|
||||
const temperature = null
|
||||
const pressure = null
|
||||
|
|
|
|||
Loading…
Reference in a new issue