nrel-spa/index.js

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 };