mirror of
https://github.com/acamarata/nrel-spa.git
synced 2026-06-30 19:04:25 +00:00
118 lines
No EOL
3.6 KiB
JavaScript
118 lines
No EOL
3.6 KiB
JavaScript
// index.js
|
|
const spa = require('./dist/spa');
|
|
|
|
/**
|
|
* Convert fractional hours to HH:MM:SS.mmm (rounding total seconds)
|
|
*/
|
|
function fractalTime(fractionalHour) {
|
|
const totalSec = Math.round(fractionalHour * 3600);
|
|
const H = Math.floor(totalSec / 3600);
|
|
const rem = totalSec - H * 3600;
|
|
const M = Math.floor(rem / 60);
|
|
const S = rem - M * 60;
|
|
const ms = Math.round((fractionalHour * 3600 - Math.floor(fractionalHour * 3600)) * 1000);
|
|
return `${H.toString().padStart(2,'0')}:` +
|
|
`${M.toString().padStart(2,'0')}:` +
|
|
`${S.toString().padStart(2,'0')}.` +
|
|
`${ms.toString().padStart(3,'0')}`;
|
|
}
|
|
|
|
/**
|
|
* Re-solve hour-angle for a custom zenith angle Zdeg (in degrees)
|
|
*/
|
|
function adjustForCustomAngle(base, Zdeg) {
|
|
const φ = base.latitude * Math.PI/180;
|
|
const δ = base.delta * Math.PI/180;
|
|
const Z = Zdeg * Math.PI/180;
|
|
const cosH0 = (Math.cos(Z) - Math.sin(φ) * Math.sin(δ)) /
|
|
(Math.cos(φ) * Math.cos(δ));
|
|
if (cosH0 < -1 || cosH0 > 1) {
|
|
return { ...base, sunrise: NaN, sunset: NaN };
|
|
}
|
|
const H0h = (Math.acos(cosH0) * 180/Math.PI) / 15;
|
|
return {
|
|
...base,
|
|
sunrise: base.suntransit - H0h,
|
|
sunset: base.suntransit + H0h
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Core SPA data calculation (raw fractional hours)
|
|
* @param {Date} date - JavaScript Date (UTC)
|
|
* @param {number} lat
|
|
* @param {number} lng
|
|
* @param {number} tz - timezone offset in hours (e.g. -4 for EDT)
|
|
* @param {object} params - { elevation, pressure, temperature, delta_ut1, delta_t, slope, azm_rotation, atmos_refract }
|
|
* @param {number[]} angles - custom zenith angles (deg) for twilight
|
|
*/
|
|
function getSpa(date, lat, lng, tz = 0, params = {}, angles = []) {
|
|
const d = new spa.SpaData();
|
|
// Use UTC components and explicit tz
|
|
d.year = date.getUTCFullYear();
|
|
d.month = date.getUTCMonth() + 1;
|
|
d.day = date.getUTCDate();
|
|
d.hour = date.getUTCHours();
|
|
d.minute = date.getUTCMinutes();
|
|
d.second = date.getUTCSeconds();
|
|
d.longitude = lng;
|
|
d.latitude = lat;
|
|
d.timezone = tz;
|
|
|
|
// Align defaults to reference C code
|
|
d.elevation = params.elevation ?? 0;
|
|
d.pressure = params.pressure ?? 1013;
|
|
d.temperature = params.temperature ?? 15;
|
|
d.delta_ut1 = params.delta_ut1 ?? 0;
|
|
d.delta_t = params.delta_t ?? 67;
|
|
d.slope = params.slope ?? 0;
|
|
d.azm_rotation = params.azm_rotation ?? 0;
|
|
d.atmos_refract= params.atmos_refract?? 0.5667;
|
|
|
|
// Only compute ZA and rise/transit/set
|
|
d.function = spa.SPA_ZA_RTS;
|
|
|
|
const rc = spa.spa_calculate(d);
|
|
if (rc !== 0) {
|
|
throw new Error(`SPA calculation failed with code ${rc}`);
|
|
}
|
|
|
|
// Base outputs
|
|
const output = {
|
|
zenith: d.zenith,
|
|
azimuth: d.azimuth,
|
|
sunrise: d.sunrise,
|
|
solarNoon: d.suntransit,
|
|
sunset: d.sunset
|
|
};
|
|
|
|
// Custom angles (twilight)
|
|
if (angles.length) {
|
|
output.angles = angles.map(Z => {
|
|
const c = adjustForCustomAngle(d, Z);
|
|
return { sunrise: c.sunrise, sunset: c.sunset };
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Same as getSpa, but formats sunrise/noon/sunset to strings
|
|
*/
|
|
function calcSpa(date, lat, lng, tz = 0, params = {}, angles = []) {
|
|
const raw = getSpa(date, lat, lng, tz, params, angles);
|
|
return {
|
|
zenith: raw.zenith,
|
|
azimuth: raw.azimuth,
|
|
sunrise: fractalTime(raw.sunrise),
|
|
solarNoon: fractalTime(raw.solarNoon),
|
|
sunset: fractalTime(raw.sunset),
|
|
angles: raw.angles ? raw.angles.map(a => ({
|
|
sunrise: fractalTime(a.sunrise),
|
|
sunset: fractalTime(a.sunset)
|
|
})) : undefined
|
|
};
|
|
}
|
|
|
|
module.exports = { getSpa, calcSpa, fractalTime, adjustForCustomAngle }; |