mirror of
https://github.com/acamarata/nrel-spa.git
synced 2026-07-01 11:24:25 +00:00
95 lines
4.8 KiB
Markdown
95 lines
4.8 KiB
Markdown
# Architecture
|
|
|
|
## Package Structure
|
|
|
|
```
|
|
nrel-spa/
|
|
├── src/
|
|
│ ├── index.ts <- Public API: getSpa, calcSpa, formatTime
|
|
│ └── types.ts <- TypeScript interfaces and SPA function code constants
|
|
├── lib/
|
|
│ └── spa.js <- Core SPA algorithm (JS port of NREL C source, tracked in git)
|
|
├── dist/ <- Built output (generated by tsup, gitignored)
|
|
│ ├── index.cjs <- CommonJS build
|
|
│ ├── index.mjs <- ESM build
|
|
│ ├── index.d.ts <- CJS type declarations
|
|
│ └── index.d.mts <- ESM type declarations
|
|
├── test.mjs <- ESM test suite (61 assertions)
|
|
└── test-cjs.cjs <- CJS test suite (17 assertions)
|
|
```
|
|
|
|
## Layers
|
|
|
|
**lib/spa.js** is the core mathematical engine. It is a direct JavaScript port of the NREL C source (`spa.c`), preserving function names, variable names, and the algorithm structure. The file is checked into git and shipped in the npm package.
|
|
|
|
**src/index.ts** is a thin TypeScript wrapper. It:
|
|
|
|
- Loads `lib/spa.js` at runtime
|
|
- Validates input parameters, throwing `TypeError` or `RangeError` for invalid values
|
|
- Validates `options.function` and throws `RangeError` for out-of-range codes
|
|
- Returns `NaN` for `sunrise`, `solarNoon`, and `sunset` when the function code does not include RTS (`SPA_ZA` or `SPA_ZA_INC`)
|
|
- Maps the flat SpaData structure to a clean output object
|
|
- Implements `adjustForCustomAngle()` internally for twilight calculations
|
|
- Provides `formatTime()` as a standalone export
|
|
|
|
**src/types.ts** exports all public TypeScript interfaces and constants:
|
|
|
|
- `SpaOptions`: atmospheric and calculation parameters
|
|
- `SpaResult` / `SpaFormattedResult`: base return types for `getSpa` / `calcSpa`
|
|
- `SpaAnglesResult` / `SpaFormattedAnglesResult`: per-angle entries in the `angles` array
|
|
- `SpaResultWithAngles` / `SpaFormattedResultWithAngles`: return types when `angles` is passed
|
|
- `SpaFunctionCode`: union type `0 | 1 | 2 | 3`
|
|
- `SPA_ZA`, `SPA_ZA_INC`, `SPA_ZA_RTS`, `SPA_ALL`: function code constants
|
|
|
|
**tsup** compiles `src/index.ts` to both CJS (`dist/index.cjs`) and ESM (`dist/index.mjs`) with TypeScript declarations. The `lib/spa.js` module is kept external (not bundled) and resolved at runtime via a `createRequire` shim in the ESM build.
|
|
|
|
## Loading Strategy
|
|
|
|
The ESM build uses a `createRequire` banner injected by tsup:
|
|
|
|
```javascript
|
|
import { createRequire as __cr } from 'node:module';
|
|
const __require = __cr(import.meta.url);
|
|
```
|
|
|
|
This anchors `require()` resolution to the file's own location rather than the calling context, so `../lib/spa.js` resolves correctly regardless of where the caller imports the package.
|
|
|
|
In the CJS build, standard `require()` is used directly.
|
|
|
|
## Why lib/spa.js Is External
|
|
|
|
Unlike a typical npm package where all source is compiled away, `lib/spa.js` ships as a separate file rather than being inlined into `dist/`. This is an intentional choice:
|
|
|
|
1. The algorithm is the canonical reference. Keeping it as a readable JS file allows inspection without source maps.
|
|
2. The file is large (989 lines). Bundling it into both CJS and ESM outputs would double its footprint for no functional gain.
|
|
3. It matches the structure of [solar-spa](https://www.npmjs.com/package/solar-spa), which keeps its Emscripten WASM module in a separate `wasm/` directory.
|
|
|
|
## Date Handling
|
|
|
|
The wrapper uses UTC components from the `Date` object (`getUTCFullYear()`, etc.) and accepts an explicit `timezone` offset. This avoids the ambiguity of `getFullYear()` which returns local time and can produce wrong results when the caller's timezone differs from the target location.
|
|
|
|
```javascript
|
|
// Always pass UTC date + explicit timezone
|
|
const date = new Date('2025-06-21T00:00:00Z');
|
|
getSpa(date, lat, lng, -4); // -4 for EDT
|
|
```
|
|
|
|
## Polar Conditions
|
|
|
|
When the sun does not rise or set (polar day/polar night), `lib/spa.js` sets sunrise, sunset, and suntransit to `-99999`. The wrapper propagates these raw values from `getSpa()`. `calcSpa()` passes them through `formatTime()`, which returns `"N/A"` for negative values.
|
|
|
|
Custom angle calculations (`adjustForCustomAngle`) return `NaN` sunrise/sunset when `cosH0 < -1 || cosH0 > 1`, which indicates no crossing of that zenith angle.
|
|
|
|
## Validation
|
|
|
|
Input validation runs before any SPA calculation:
|
|
|
|
- `date` must be a valid `Date` (not `Invalid Date`)
|
|
- `latitude` and `longitude` must be finite numbers in their valid ranges
|
|
- Invalid values throw before the SPA data structure is populated
|
|
|
|
The internal `spa_calculate()` function also validates its inputs and returns non-zero error codes for out-of-range values. The wrapper throws `Error` on any non-zero return.
|
|
|
|
---
|
|
|
|
[Home](Home) . [API Reference](API-Reference) . [Twilight Calculations](Twilight-Calculations) . [NREL SPA Algorithm](NREL-SPA-Algorithm)
|