jyotish-calc 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/index.js +4 -1
- package/package.json +2 -2
- package/src/panchanga/constants.js +266 -0
- package/src/panchanga/index.js +330 -0
- package/src/panchanga/karana.js +185 -0
- package/src/panchanga/lunar-calendar.js +207 -0
- package/src/panchanga/muhurtas.js +370 -0
- package/src/panchanga/nakshatra.js +180 -0
- package/src/panchanga/solar-calendar.js +301 -0
- package/src/panchanga/sunrise-sunset.js +400 -0
- package/src/panchanga/tithi.js +234 -0
- package/src/panchanga/utils.js +320 -0
- package/src/panchanga/vaara.js +182 -0
- package/src/panchanga/yogam.js +196 -0
- package/get-full-results.js +0 -140
- package/test-chennai-1998.js +0 -162
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solar Calendar Functions
|
|
3
|
+
* Solar month, Sankranti, Tamil calendar, Samvatsara
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const sunriseSunset = require('./sunrise-sunset');
|
|
7
|
+
const utils = require('./utils');
|
|
8
|
+
const { SOLAR_MONTH_NAMES, TAMIL_MONTH_NAMES, SAMVATSARA_NAMES, SIGN_NAMES } = require('./constants');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get solar month (Raasi) for given date
|
|
12
|
+
* @param {number} jd - Julian Day Number
|
|
13
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
14
|
+
* @returns {object} Solar month details
|
|
15
|
+
*/
|
|
16
|
+
function raasi(jd, place) {
|
|
17
|
+
const jdUTC = utils.toUTC(jd, place.timezone);
|
|
18
|
+
const rise = sunriseSunset.sunrise(jd, place);
|
|
19
|
+
|
|
20
|
+
if (!rise) return null;
|
|
21
|
+
|
|
22
|
+
const riseJDUTC = utils.toUTC(rise.jd, place.timezone);
|
|
23
|
+
const sunLong = sunriseSunset.solarLongitude(riseJDUTC);
|
|
24
|
+
const raasiNo = Math.floor(sunLong / 30) + 1;
|
|
25
|
+
const degreesInRaasi = sunLong % 30;
|
|
26
|
+
const degreesLeft = 30 - degreesInRaasi;
|
|
27
|
+
|
|
28
|
+
// Calculate end time using interpolation
|
|
29
|
+
const offsets = [0.0, 0.25, 0.5, 0.75, 1.0];
|
|
30
|
+
const sunLongs = offsets.map(t => sunriseSunset.solarLongitude(riseJDUTC + t));
|
|
31
|
+
const unwrapped = utils.unwrapAngles(sunLongs);
|
|
32
|
+
|
|
33
|
+
let endTime;
|
|
34
|
+
const targetDegree = raasiNo * 30;
|
|
35
|
+
try {
|
|
36
|
+
const jdAtStart = utils.dateToJD(
|
|
37
|
+
utils.jdToDate(jd).year,
|
|
38
|
+
utils.jdToDate(jd).month,
|
|
39
|
+
utils.jdToDate(jd).day
|
|
40
|
+
);
|
|
41
|
+
const approxEnd = utils.inverseLagrange(offsets, unwrapped, targetDegree);
|
|
42
|
+
endTime = (rise.jd + approxEnd - jdAtStart) * 24;
|
|
43
|
+
} catch {
|
|
44
|
+
// Sun moves ~1 degree per day
|
|
45
|
+
endTime = rise.hours + degreesLeft * 24;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
number: raasiNo,
|
|
50
|
+
name: SOLAR_MONTH_NAMES[raasiNo - 1],
|
|
51
|
+
signName: SIGN_NAMES[raasiNo - 1],
|
|
52
|
+
sunLongitude: sunLong,
|
|
53
|
+
degreesInRaasi,
|
|
54
|
+
degreesLeft,
|
|
55
|
+
endTime,
|
|
56
|
+
endTimeString: utils.formatTime(endTime > 24 ? endTime - 24 : endTime)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get Tamil solar month and date
|
|
62
|
+
* @param {number} jd - Julian Day Number
|
|
63
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
64
|
+
* @returns {object} Tamil month and date
|
|
65
|
+
*/
|
|
66
|
+
function tamilSolarMonthAndDate(jd, place) {
|
|
67
|
+
const set = sunriseSunset.sunset(jd, place);
|
|
68
|
+
if (!set) return null;
|
|
69
|
+
|
|
70
|
+
const setJDUTC = utils.toUTC(set.jd, place.timezone);
|
|
71
|
+
const sunLong = sunriseSunset.solarLongitude(setJDUTC);
|
|
72
|
+
const tamilMonth = Math.floor(sunLong / 30);
|
|
73
|
+
|
|
74
|
+
// Count days since sun entered this raasi
|
|
75
|
+
let dayCount = 1;
|
|
76
|
+
let searchJD = set.jd;
|
|
77
|
+
let searchLong = sunLong;
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const remainder = searchLong % 30;
|
|
81
|
+
if (remainder < 1 && remainder > 0) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
searchJD -= 1;
|
|
85
|
+
const searchSetJDUTC = utils.toUTC(searchJD, place.timezone);
|
|
86
|
+
searchLong = sunriseSunset.solarLongitude(searchSetJDUTC);
|
|
87
|
+
dayCount++;
|
|
88
|
+
|
|
89
|
+
// Safety limit
|
|
90
|
+
if (dayCount > 32) break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
month: tamilMonth,
|
|
95
|
+
monthNumber: tamilMonth + 1,
|
|
96
|
+
monthName: TAMIL_MONTH_NAMES[tamilMonth],
|
|
97
|
+
date: dayCount,
|
|
98
|
+
sunLongitude: sunLong
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get Samvatsara (60-year cycle) for given date
|
|
104
|
+
* @param {number} jd - Julian Day Number
|
|
105
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
106
|
+
* @returns {object} Samvatsara details
|
|
107
|
+
*/
|
|
108
|
+
function samvatsara(jd, place) {
|
|
109
|
+
const tamilDate = tamilSolarMonthAndDate(jd, place);
|
|
110
|
+
if (!tamilDate) return null;
|
|
111
|
+
|
|
112
|
+
// Find previous Mesha Sankranti (start of year)
|
|
113
|
+
const prevSankranti = previousSankranti(jd, place, 0); // 0 = Mesha
|
|
114
|
+
const year = prevSankranti ? prevSankranti.date.year : utils.jdToDate(jd).year;
|
|
115
|
+
|
|
116
|
+
// Calculate samvatsara index
|
|
117
|
+
// Using the traditional formula
|
|
118
|
+
const adjustedYear = year > 0 ? year - 1 : year;
|
|
119
|
+
const samvatsaraIndex = ((adjustedYear - 1926) % 60 + 60) % 60;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
index: samvatsaraIndex,
|
|
123
|
+
number: samvatsaraIndex + 1,
|
|
124
|
+
name: SAMVATSARA_NAMES[samvatsaraIndex],
|
|
125
|
+
year
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get previous Sankranti (sun entering a sign)
|
|
131
|
+
* @param {number} jd - Julian Day Number
|
|
132
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
133
|
+
* @param {number} targetSign - Target sign (0-11), null for any
|
|
134
|
+
* @returns {object} Sankranti details
|
|
135
|
+
*/
|
|
136
|
+
function previousSankranti(jd, place, targetSign = null) {
|
|
137
|
+
const set = sunriseSunset.sunset(jd, place);
|
|
138
|
+
if (!set) return null;
|
|
139
|
+
|
|
140
|
+
let searchJD = set.jd;
|
|
141
|
+
let searchJDUTC = utils.toUTC(searchJD, place.timezone);
|
|
142
|
+
let sunLong = sunriseSunset.solarLongitude(searchJDUTC);
|
|
143
|
+
let currentSign = Math.floor(sunLong / 30);
|
|
144
|
+
|
|
145
|
+
// Search backwards for sign change
|
|
146
|
+
let maxDays = 35;
|
|
147
|
+
while (maxDays > 0) {
|
|
148
|
+
searchJD -= 1;
|
|
149
|
+
searchJDUTC = utils.toUTC(searchJD, place.timezone);
|
|
150
|
+
const newSunLong = sunriseSunset.solarLongitude(searchJDUTC);
|
|
151
|
+
const newSign = Math.floor(newSunLong / 30);
|
|
152
|
+
|
|
153
|
+
if (newSign !== currentSign) {
|
|
154
|
+
// Found a sign change
|
|
155
|
+
if (targetSign === null || currentSign === targetSign) {
|
|
156
|
+
// Refine the time
|
|
157
|
+
const riseJD = sunriseSunset.sunrise(searchJD + 1, place).jd;
|
|
158
|
+
const riseJDUTC = utils.toUTC(riseJD, place.timezone);
|
|
159
|
+
const offsets = [0.0, 0.25, 0.5, 0.75, 1.0];
|
|
160
|
+
const sunLongs = offsets.map(t => sunriseSunset.solarLongitude(riseJDUTC + t));
|
|
161
|
+
|
|
162
|
+
const sankrantiDegree = currentSign * 30;
|
|
163
|
+
let sankrantiTime;
|
|
164
|
+
try {
|
|
165
|
+
sankrantiTime = utils.inverseLagrange(offsets, sunLongs, sankrantiDegree);
|
|
166
|
+
} catch {
|
|
167
|
+
sankrantiTime = 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const date = utils.jdToDate(searchJD + 1);
|
|
171
|
+
return {
|
|
172
|
+
sign: currentSign,
|
|
173
|
+
signName: SIGN_NAMES[currentSign],
|
|
174
|
+
solarMonth: SOLAR_MONTH_NAMES[currentSign],
|
|
175
|
+
tamilMonth: TAMIL_MONTH_NAMES[currentSign],
|
|
176
|
+
date: {
|
|
177
|
+
year: date.year,
|
|
178
|
+
month: date.month,
|
|
179
|
+
day: Math.floor(date.day)
|
|
180
|
+
},
|
|
181
|
+
time: sankrantiTime,
|
|
182
|
+
jd: searchJD + 1 + sankrantiTime
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
currentSign = newSign;
|
|
188
|
+
sunLong = newSunLong;
|
|
189
|
+
maxDays--;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get next Sankranti
|
|
197
|
+
* @param {number} jd - Julian Day Number
|
|
198
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
199
|
+
* @param {number} targetSign - Target sign (0-11), null for any
|
|
200
|
+
* @returns {object} Sankranti details
|
|
201
|
+
*/
|
|
202
|
+
function nextSankranti(jd, place, targetSign = null) {
|
|
203
|
+
const set = sunriseSunset.sunset(jd, place);
|
|
204
|
+
if (!set) return null;
|
|
205
|
+
|
|
206
|
+
let searchJD = set.jd;
|
|
207
|
+
let searchJDUTC = utils.toUTC(searchJD, place.timezone);
|
|
208
|
+
let sunLong = sunriseSunset.solarLongitude(searchJDUTC);
|
|
209
|
+
let currentSign = Math.floor(sunLong / 30);
|
|
210
|
+
|
|
211
|
+
// Search forwards for sign change
|
|
212
|
+
let maxDays = 35;
|
|
213
|
+
while (maxDays > 0) {
|
|
214
|
+
searchJD += 1;
|
|
215
|
+
searchJDUTC = utils.toUTC(searchJD, place.timezone);
|
|
216
|
+
const newSunLong = sunriseSunset.solarLongitude(searchJDUTC);
|
|
217
|
+
const newSign = Math.floor(newSunLong / 30);
|
|
218
|
+
|
|
219
|
+
if (newSign !== currentSign) {
|
|
220
|
+
if (targetSign === null || newSign === targetSign) {
|
|
221
|
+
const riseJD = sunriseSunset.sunrise(searchJD, place).jd;
|
|
222
|
+
const riseJDUTC = utils.toUTC(riseJD, place.timezone);
|
|
223
|
+
const offsets = [0.0, 0.25, 0.5, 0.75, 1.0];
|
|
224
|
+
const sunLongs = offsets.map(t => sunriseSunset.solarLongitude(riseJDUTC + t));
|
|
225
|
+
|
|
226
|
+
const sankrantiDegree = newSign * 30;
|
|
227
|
+
let sankrantiTime;
|
|
228
|
+
try {
|
|
229
|
+
sankrantiTime = utils.inverseLagrange(offsets, sunLongs, sankrantiDegree);
|
|
230
|
+
} catch {
|
|
231
|
+
sankrantiTime = 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const date = utils.jdToDate(searchJD);
|
|
235
|
+
return {
|
|
236
|
+
sign: newSign,
|
|
237
|
+
signName: SIGN_NAMES[newSign],
|
|
238
|
+
solarMonth: SOLAR_MONTH_NAMES[newSign],
|
|
239
|
+
tamilMonth: TAMIL_MONTH_NAMES[newSign],
|
|
240
|
+
date: {
|
|
241
|
+
year: date.year,
|
|
242
|
+
month: date.month,
|
|
243
|
+
day: Math.floor(date.day)
|
|
244
|
+
},
|
|
245
|
+
time: sankrantiTime,
|
|
246
|
+
jd: searchJD + sankrantiTime
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
currentSign = newSign;
|
|
252
|
+
sunLong = newSunLong;
|
|
253
|
+
maxDays--;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get number of days in Tamil month
|
|
261
|
+
* @param {number} jd - Julian Day Number
|
|
262
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
263
|
+
* @returns {number} Days in the month
|
|
264
|
+
*/
|
|
265
|
+
function daysInTamilMonth(jd, place) {
|
|
266
|
+
const current = tamilSolarMonthAndDate(jd, place);
|
|
267
|
+
if (!current) return 30;
|
|
268
|
+
|
|
269
|
+
// Find next sankranti
|
|
270
|
+
const next = nextSankranti(jd, place);
|
|
271
|
+
if (!next) return 30;
|
|
272
|
+
|
|
273
|
+
// Count days
|
|
274
|
+
const daysPassed = current.date;
|
|
275
|
+
let searchJD = jd;
|
|
276
|
+
let daysRemaining = 0;
|
|
277
|
+
|
|
278
|
+
while (true) {
|
|
279
|
+
searchJD += 1;
|
|
280
|
+
const checkDate = tamilSolarMonthAndDate(searchJD, place);
|
|
281
|
+
if (!checkDate || checkDate.month !== current.month) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
daysRemaining++;
|
|
285
|
+
if (daysRemaining > 35) break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return daysPassed + daysRemaining;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
raasi,
|
|
293
|
+
tamilSolarMonthAndDate,
|
|
294
|
+
samvatsara,
|
|
295
|
+
previousSankranti,
|
|
296
|
+
nextSankranti,
|
|
297
|
+
daysInTamilMonth,
|
|
298
|
+
SOLAR_MONTH_NAMES,
|
|
299
|
+
TAMIL_MONTH_NAMES,
|
|
300
|
+
SAMVATSARA_NAMES
|
|
301
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sunrise/Sunset Calculations
|
|
3
|
+
* Calculate sun and moon rise/set times using Swiss Ephemeris
|
|
4
|
+
* With simplified astronomical formulas for reliability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const swe = require('swisseph-v2');
|
|
8
|
+
const utils = require('./utils');
|
|
9
|
+
|
|
10
|
+
// Set ephemeris path
|
|
11
|
+
swe.swe_set_ephe_path('./node_modules/swisseph-v2/ephe');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calculate sunrise for given date and place
|
|
15
|
+
* @param {number} jd - Julian Day Number (local time)
|
|
16
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
17
|
+
* @returns {object} {hours, string, jd} - sunrise time
|
|
18
|
+
*/
|
|
19
|
+
function sunrise(jd, place) {
|
|
20
|
+
const { year, month, day } = utils.jdToDate(jd);
|
|
21
|
+
|
|
22
|
+
// Use simple sunrise formula
|
|
23
|
+
const result = calculateSunTime(year, month, day, place.latitude, place.longitude, place.timezone, true);
|
|
24
|
+
|
|
25
|
+
if (result === null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const jdAtStart = utils.dateToJD(year, month, day);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
hours: result,
|
|
33
|
+
string: utils.formatTime(result),
|
|
34
|
+
string24: utils.formatTime(result, true),
|
|
35
|
+
jd: jdAtStart + result / 24
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate sunset for given date and place
|
|
41
|
+
* @param {number} jd - Julian Day Number (local time)
|
|
42
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
43
|
+
* @returns {object} {hours, string, jd} - sunset time
|
|
44
|
+
*/
|
|
45
|
+
function sunset(jd, place) {
|
|
46
|
+
const { year, month, day } = utils.jdToDate(jd);
|
|
47
|
+
|
|
48
|
+
const result = calculateSunTime(year, month, day, place.latitude, place.longitude, place.timezone, false);
|
|
49
|
+
|
|
50
|
+
if (result === null) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const jdAtStart = utils.dateToJD(year, month, day);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
hours: result,
|
|
58
|
+
string: utils.formatTime(result),
|
|
59
|
+
string24: utils.formatTime(result, true),
|
|
60
|
+
jd: jdAtStart + result / 24
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Simple and reliable sunrise/sunset calculation
|
|
66
|
+
* Based on Meeus "Astronomical Algorithms"
|
|
67
|
+
*/
|
|
68
|
+
function calculateSunTime(year, month, day, lat, lng, tz, isSunrise) {
|
|
69
|
+
// Day of year
|
|
70
|
+
const N1 = Math.floor(275 * month / 9);
|
|
71
|
+
const N2 = Math.floor((month + 9) / 12);
|
|
72
|
+
const N3 = 1 + Math.floor((year - 4 * Math.floor(year / 4) + 2) / 3);
|
|
73
|
+
const N = N1 - (N2 * N3) + day - 30;
|
|
74
|
+
|
|
75
|
+
// Convert longitude to hour value
|
|
76
|
+
const lngHour = lng / 15;
|
|
77
|
+
|
|
78
|
+
// Approximate time
|
|
79
|
+
let t;
|
|
80
|
+
if (isSunrise) {
|
|
81
|
+
t = N + (6 - lngHour) / 24;
|
|
82
|
+
} else {
|
|
83
|
+
t = N + (18 - lngHour) / 24;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sun's mean anomaly
|
|
87
|
+
const M = (0.9856 * t) - 3.289;
|
|
88
|
+
|
|
89
|
+
// Sun's true longitude
|
|
90
|
+
let L = M + (1.916 * Math.sin(utils.toRadians(M))) +
|
|
91
|
+
(0.020 * Math.sin(utils.toRadians(2 * M))) + 282.634;
|
|
92
|
+
L = ((L % 360) + 360) % 360;
|
|
93
|
+
|
|
94
|
+
// Sun's right ascension
|
|
95
|
+
let RA = utils.toDegrees(Math.atan(0.91764 * Math.tan(utils.toRadians(L))));
|
|
96
|
+
RA = ((RA % 360) + 360) % 360;
|
|
97
|
+
|
|
98
|
+
// RA in same quadrant as L
|
|
99
|
+
const Lquadrant = Math.floor(L / 90) * 90;
|
|
100
|
+
const RAquadrant = Math.floor(RA / 90) * 90;
|
|
101
|
+
RA = RA + (Lquadrant - RAquadrant);
|
|
102
|
+
|
|
103
|
+
// RA to hours
|
|
104
|
+
RA = RA / 15;
|
|
105
|
+
|
|
106
|
+
// Sun's declination
|
|
107
|
+
const sinDec = 0.39782 * Math.sin(utils.toRadians(L));
|
|
108
|
+
const cosDec = Math.cos(Math.asin(sinDec));
|
|
109
|
+
|
|
110
|
+
// Sun's local hour angle
|
|
111
|
+
const zenith = 90.833; // Official zenith with refraction correction
|
|
112
|
+
const cosH = (Math.cos(utils.toRadians(zenith)) -
|
|
113
|
+
(sinDec * Math.sin(utils.toRadians(lat)))) /
|
|
114
|
+
(cosDec * Math.cos(utils.toRadians(lat)));
|
|
115
|
+
|
|
116
|
+
// Check if sun rises/sets
|
|
117
|
+
if (cosH > 1) {
|
|
118
|
+
return null; // Sun never rises
|
|
119
|
+
}
|
|
120
|
+
if (cosH < -1) {
|
|
121
|
+
return null; // Sun never sets
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Hour angle
|
|
125
|
+
let H;
|
|
126
|
+
if (isSunrise) {
|
|
127
|
+
H = 360 - utils.toDegrees(Math.acos(cosH));
|
|
128
|
+
} else {
|
|
129
|
+
H = utils.toDegrees(Math.acos(cosH));
|
|
130
|
+
}
|
|
131
|
+
H = H / 15; // Convert to hours
|
|
132
|
+
|
|
133
|
+
// Local mean time
|
|
134
|
+
const T = H + RA - (0.06571 * t) - 6.622;
|
|
135
|
+
|
|
136
|
+
// UTC time
|
|
137
|
+
let UT = T - lngHour;
|
|
138
|
+
UT = ((UT % 24) + 24) % 24;
|
|
139
|
+
|
|
140
|
+
// Local time
|
|
141
|
+
const localTime = UT + tz;
|
|
142
|
+
|
|
143
|
+
return ((localTime % 24) + 24) % 24;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculate moonrise for given date and place
|
|
148
|
+
* @param {number} jd - Julian Day Number (local time)
|
|
149
|
+
* @param {object} place - {latitude, longitude, timezone}
|
|
150
|
+
* @returns {object} {hours, string, jd} - moonrise time
|
|
151
|
+
*/
|
|
152
|
+
function moonrise(jd, place) {
|
|
153
|
+
const { year, month, day } = utils.jdToDate(jd);
|
|
154
|
+
const jdNoon = utils.dateToJD(year, month, day, 12);
|
|
155
|
+
const jdNoonUTC = jdNoon - place.timezone / 24;
|
|
156
|
+
|
|
157
|
+
let moonriseTime = null;
|
|
158
|
+
|
|
159
|
+
for (let hour = 0; hour < 24; hour += 0.5) {
|
|
160
|
+
const testJD = jdNoonUTC - 12 / 24 + hour / 24;
|
|
161
|
+
const alt = getMoonAltitude(testJD, place.latitude, place.longitude);
|
|
162
|
+
const nextAlt = getMoonAltitude(testJD + 0.5 / 24, place.latitude, place.longitude);
|
|
163
|
+
|
|
164
|
+
if (alt < 0 && nextAlt >= 0) {
|
|
165
|
+
moonriseTime = hour;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (moonriseTime === null) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const jdAtStart = utils.dateToJD(year, month, day);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
hours: moonriseTime,
|
|
178
|
+
string: utils.formatTime(moonriseTime),
|
|
179
|
+
string24: utils.formatTime(moonriseTime, true),
|
|
180
|
+
jd: jdAtStart + moonriseTime / 24
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calculate moonset for given date and place
|
|
186
|
+
*/
|
|
187
|
+
function moonset(jd, place) {
|
|
188
|
+
const { year, month, day } = utils.jdToDate(jd);
|
|
189
|
+
const jdNoon = utils.dateToJD(year, month, day, 12);
|
|
190
|
+
const jdNoonUTC = jdNoon - place.timezone / 24;
|
|
191
|
+
|
|
192
|
+
let moonsetTime = null;
|
|
193
|
+
|
|
194
|
+
for (let hour = 0; hour < 24; hour += 0.5) {
|
|
195
|
+
const testJD = jdNoonUTC - 12 / 24 + hour / 24;
|
|
196
|
+
const alt = getMoonAltitude(testJD, place.latitude, place.longitude);
|
|
197
|
+
const nextAlt = getMoonAltitude(testJD + 0.5 / 24, place.latitude, place.longitude);
|
|
198
|
+
|
|
199
|
+
if (alt >= 0 && nextAlt < 0) {
|
|
200
|
+
moonsetTime = hour;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (moonsetTime === null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const jdAtStart = utils.dateToJD(year, month, day);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
hours: moonsetTime,
|
|
213
|
+
string: utils.formatTime(moonsetTime),
|
|
214
|
+
string24: utils.formatTime(moonsetTime, true),
|
|
215
|
+
jd: jdAtStart + moonsetTime / 24
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get approximate moon altitude
|
|
221
|
+
*/
|
|
222
|
+
function getMoonAltitude(jdUTC, lat, lng) {
|
|
223
|
+
const flags = swe.SEFLG_SWIEPH;
|
|
224
|
+
const result = swe.swe_calc_ut(jdUTC, swe.SE_MOON, flags);
|
|
225
|
+
|
|
226
|
+
if (result.error) {
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const moonLong = result.longitude;
|
|
231
|
+
const moonLat = result.latitude;
|
|
232
|
+
|
|
233
|
+
const obliquity = 23.44;
|
|
234
|
+
const oblRad = utils.toRadians(obliquity);
|
|
235
|
+
const longRad = utils.toRadians(moonLong);
|
|
236
|
+
const latRad = utils.toRadians(moonLat);
|
|
237
|
+
|
|
238
|
+
const sinRA = Math.sin(longRad) * Math.cos(oblRad) - Math.tan(latRad) * Math.sin(oblRad);
|
|
239
|
+
const cosRA = Math.cos(longRad);
|
|
240
|
+
const RA = Math.atan2(sinRA, cosRA);
|
|
241
|
+
|
|
242
|
+
const sinDec = Math.sin(latRad) * Math.cos(oblRad) +
|
|
243
|
+
Math.cos(latRad) * Math.sin(oblRad) * Math.sin(longRad);
|
|
244
|
+
const dec = Math.asin(sinDec);
|
|
245
|
+
|
|
246
|
+
const LST = getLST(jdUTC, lng);
|
|
247
|
+
const LSTrad = utils.toRadians(LST);
|
|
248
|
+
|
|
249
|
+
const HA = LSTrad - RA;
|
|
250
|
+
|
|
251
|
+
const latRadObs = utils.toRadians(lat);
|
|
252
|
+
const sinAlt = Math.sin(dec) * Math.sin(latRadObs) +
|
|
253
|
+
Math.cos(dec) * Math.cos(latRadObs) * Math.cos(HA);
|
|
254
|
+
const altitude = utils.toDegrees(Math.asin(sinAlt));
|
|
255
|
+
|
|
256
|
+
return altitude;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get Local Sidereal Time
|
|
261
|
+
*/
|
|
262
|
+
function getLST(jdUTC, lng) {
|
|
263
|
+
const T = (jdUTC - 2451545.0) / 36525.0;
|
|
264
|
+
let GMST = 280.46061837 + 360.98564736629 * (jdUTC - 2451545.0) +
|
|
265
|
+
0.000387933 * T * T - T * T * T / 38710000.0;
|
|
266
|
+
GMST = ((GMST % 360) + 360) % 360;
|
|
267
|
+
|
|
268
|
+
const LST = GMST + lng;
|
|
269
|
+
return ((LST % 360) + 360) % 360;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Calculate midday time (sun at highest point)
|
|
274
|
+
*/
|
|
275
|
+
function midday(jd, place) {
|
|
276
|
+
const rise = sunrise(jd, place);
|
|
277
|
+
const set = sunset(jd, place);
|
|
278
|
+
|
|
279
|
+
if (!rise || !set) return null;
|
|
280
|
+
|
|
281
|
+
const hours = (rise.hours + set.hours) / 2;
|
|
282
|
+
const middayJD = (rise.jd + set.jd) / 2;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
hours,
|
|
286
|
+
string: utils.formatTime(hours),
|
|
287
|
+
string24: utils.formatTime(hours, true),
|
|
288
|
+
jd: middayJD
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Calculate midnight time (sun at lowest point)
|
|
294
|
+
*/
|
|
295
|
+
function midnight(jd, place) {
|
|
296
|
+
const set = sunset(jd, place);
|
|
297
|
+
const nextRise = sunrise(jd + 1, place);
|
|
298
|
+
|
|
299
|
+
if (!set || !nextRise) return null;
|
|
300
|
+
|
|
301
|
+
let hours = (set.hours + 24 + nextRise.hours) / 2;
|
|
302
|
+
if (hours > 24) hours -= 24;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
hours,
|
|
306
|
+
string: utils.formatTime(hours),
|
|
307
|
+
string24: utils.formatTime(hours, true)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Calculate day length (sunrise to sunset)
|
|
313
|
+
*/
|
|
314
|
+
function dayLength(jd, place) {
|
|
315
|
+
const rise = sunrise(jd, place);
|
|
316
|
+
const set = sunset(jd, place);
|
|
317
|
+
|
|
318
|
+
if (!rise || !set) return 12;
|
|
319
|
+
|
|
320
|
+
let length = set.hours - rise.hours;
|
|
321
|
+
if (length < 0) length += 24;
|
|
322
|
+
return length;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Calculate night length (sunset to next sunrise)
|
|
327
|
+
*/
|
|
328
|
+
function nightLength(jd, place) {
|
|
329
|
+
const day = dayLength(jd, place);
|
|
330
|
+
return 24 - day;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get sidereal longitude of a planet/point
|
|
335
|
+
*/
|
|
336
|
+
function siderealLongitude(jdUTC, planet, ayanamsaMode = 'LAHIRI') {
|
|
337
|
+
swe.swe_set_sid_mode(swe.SE_SIDM_LAHIRI, 0, 0);
|
|
338
|
+
|
|
339
|
+
const flags = swe.SEFLG_SWIEPH | swe.SEFLG_SIDEREAL | swe.SEFLG_SPEED;
|
|
340
|
+
const result = swe.swe_calc_ut(jdUTC, planet, flags);
|
|
341
|
+
|
|
342
|
+
if (result.error) {
|
|
343
|
+
console.warn('Planet calculation error:', result.error);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return utils.norm360(result.longitude);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get solar longitude (sidereal)
|
|
352
|
+
*/
|
|
353
|
+
function solarLongitude(jdUTC) {
|
|
354
|
+
return siderealLongitude(jdUTC, swe.SE_SUN);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get lunar longitude (sidereal)
|
|
359
|
+
*/
|
|
360
|
+
function lunarLongitude(jdUTC) {
|
|
361
|
+
return siderealLongitude(jdUTC, swe.SE_MOON);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get daily motion of Moon
|
|
366
|
+
*/
|
|
367
|
+
function lunarDailyMotion(jdUTC) {
|
|
368
|
+
const today = lunarLongitude(jdUTC);
|
|
369
|
+
const tomorrow = lunarLongitude(jdUTC + 1);
|
|
370
|
+
let motion = tomorrow - today;
|
|
371
|
+
if (motion < 0) motion += 360;
|
|
372
|
+
return motion;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get daily motion of Sun
|
|
377
|
+
*/
|
|
378
|
+
function solarDailyMotion(jdUTC) {
|
|
379
|
+
const today = solarLongitude(jdUTC);
|
|
380
|
+
const tomorrow = solarLongitude(jdUTC + 1);
|
|
381
|
+
let motion = tomorrow - today;
|
|
382
|
+
if (motion < 0) motion += 360;
|
|
383
|
+
return motion;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
sunrise,
|
|
388
|
+
sunset,
|
|
389
|
+
moonrise,
|
|
390
|
+
moonset,
|
|
391
|
+
midday,
|
|
392
|
+
midnight,
|
|
393
|
+
dayLength,
|
|
394
|
+
nightLength,
|
|
395
|
+
siderealLongitude,
|
|
396
|
+
solarLongitude,
|
|
397
|
+
lunarLongitude,
|
|
398
|
+
lunarDailyMotion,
|
|
399
|
+
solarDailyMotion
|
|
400
|
+
};
|