mirror of
https://github.com/acamarata/nrel-spa-dart.git
synced 2026-07-02 20:00:39 +00:00
Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11890cf5a9 | ||
|
|
0a75c4a8f8 | ||
|
|
e19e4bdf20 | ||
|
|
f7e31e0aaf | ||
|
|
d591412c34 | ||
|
|
86db6c6bae | ||
|
|
01a1c56cd0 |
15 changed files with 616 additions and 77 deletions
|
|
@ -1 +0,0 @@
|
|||
CLAUDE.md
|
||||
97
.github/wiki/API-Reference.md
vendored
97
.github/wiki/API-Reference.md
vendored
|
|
@ -11,49 +11,110 @@ SpaResult getSpa(
|
|||
double elevation = 0,
|
||||
double pressure = 1013,
|
||||
double temperature = 15,
|
||||
double deltaUt1 = 0,
|
||||
double deltaT = 67,
|
||||
double slope = 0,
|
||||
double azmRotation = 0,
|
||||
double atmosRefract = 0.5667,
|
||||
int functionCode = spaZaRts,
|
||||
List<double> customAngles = const [],
|
||||
})
|
||||
```
|
||||
|
||||
Computes solar position for a given location and moment.
|
||||
Computes solar position for a given location and moment. Returns raw fractional-hour values.
|
||||
|
||||
Throws `ArgumentError` if any input falls outside the NREL SPA valid range.
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `date` | `DateTime` | required | UTC date and time |
|
||||
| `latitude` | `double` | required | Degrees (-90 to 90) |
|
||||
| `longitude` | `double` | required | Degrees (-180 to 180) |
|
||||
| `timezone` | `double` | required | Hours from UTC |
|
||||
| `latitude` | `double` | required | Degrees (-90 to 90, south negative) |
|
||||
| `longitude` | `double` | required | Degrees (-180 to 180, west negative) |
|
||||
| `timezone` | `double` | required | Hours from UTC (e.g., -5 for EST) |
|
||||
| `elevation` | `double` | 0 | Meters above sea level |
|
||||
| `pressure` | `double` | 1013 | Atmospheric pressure (mbar) |
|
||||
| `temperature` | `double` | 15 | Temperature (Celsius) |
|
||||
| `deltaUt1` | `double` | 0 | Difference UT1 - UTC (seconds, typically small) |
|
||||
| `deltaT` | `double` | 67 | TT - UT1 (seconds) |
|
||||
| `customAngles` | `List<double>` | [] | Zenith angles for custom rise/set computation |
|
||||
| `slope` | `double` | 0 | Surface tilt angle (degrees, for incidence) |
|
||||
| `azmRotation` | `double` | 0 | Surface azimuth rotation (degrees, for incidence) |
|
||||
| `atmosRefract` | `double` | 0.5667 | Atmospheric refraction at horizon (degrees) |
|
||||
| `functionCode` | `int` | `spaZaRts` | What to calculate; one of `spaZa`, `spaZaInc`, `spaZaRts`, `spaAll` |
|
||||
| `customAngles` | `List<double>` | `[]` | Zenith angles for custom rise/set computation |
|
||||
|
||||
### Function codes
|
||||
|
||||
| Constant | Value | Computes |
|
||||
| --- | --- | --- |
|
||||
| `spaZa` | 0 | Zenith and azimuth only |
|
||||
| `spaZaInc` | 1 | Zenith, azimuth, and surface incidence |
|
||||
| `spaZaRts` | 2 | Zenith, azimuth, sunrise, sunset, solar noon (default) |
|
||||
| `spaAll` | 3 | All of the above |
|
||||
|
||||
### SpaResult fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `zenith` | `double` | Solar zenith angle (degrees) |
|
||||
| `azimuth` | `double` | Solar azimuth angle (degrees, clockwise from north) |
|
||||
| `sunrise` | `double` | Sunrise in fractional hours (local time) |
|
||||
| `solarNoon` | `double` | Solar noon in fractional hours (local time) |
|
||||
| `sunset` | `double` | Sunset in fractional hours (local time) |
|
||||
| `angles` | `List<SpaAngleResult>` | Rise/set pairs for each entry in `customAngles` |
|
||||
| `azimuth` | `double` | Solar azimuth angle, eastward from north (degrees) |
|
||||
| `sunrise` | `double` | Sunrise in fractional hours, local time (NaN if polar) |
|
||||
| `solarNoon` | `double` | Solar noon in fractional hours, local time (NaN if polar) |
|
||||
| `sunset` | `double` | Sunset in fractional hours, local time (NaN if polar) |
|
||||
| `incidence` | `double` | Surface incidence angle (degrees; `spaZaInc`/`spaAll` only) |
|
||||
| `angles` | `List<SpaAnglesResult>` | Rise/set pairs for each entry in `customAngles` |
|
||||
|
||||
### SpaAngleResult fields
|
||||
### SpaAnglesResult fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `zenith` | `double` | The requested zenith angle |
|
||||
| `sunrise` | `double` | Rise time in fractional hours for this zenith |
|
||||
| `sunset` | `double` | Set time in fractional hours for this zenith |
|
||||
| `sunrise` | `double` | Rise time in fractional hours for this zenith angle |
|
||||
| `sunset` | `double` | Set time in fractional hours for this zenith angle |
|
||||
|
||||
### Custom zenith angles
|
||||
---
|
||||
|
||||
Pass any solar zenith angles to get rise/set times at those angles. Standard civil/nautical/astronomical twilight angles are 96, 102, and 108 degrees respectively. Prayer time implementations use this to calculate Fajr and Isha.
|
||||
## calcSpa
|
||||
|
||||
```dart
|
||||
SpaFormattedResult calcSpa(
|
||||
DateTime date,
|
||||
double latitude,
|
||||
double longitude,
|
||||
double timezone, {
|
||||
// same optional parameters as getSpa
|
||||
})
|
||||
```
|
||||
|
||||
Same as `getSpa`, but formats `sunrise`, `solarNoon`, and `sunset` as `HH:MM:SS` strings. Returns `"N/A"` for those fields during polar day or polar night.
|
||||
|
||||
### SpaFormattedResult fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `zenith` | `double` | Solar zenith angle (degrees) |
|
||||
| `azimuth` | `double` | Solar azimuth angle (degrees) |
|
||||
| `sunrise` | `String` | Sunrise as `HH:MM:SS`, or `"N/A"` |
|
||||
| `solarNoon` | `String` | Solar noon as `HH:MM:SS`, or `"N/A"` |
|
||||
| `sunset` | `String` | Sunset as `HH:MM:SS`, or `"N/A"` |
|
||||
| `incidence` | `double` | Surface incidence angle (degrees) |
|
||||
| `angles` | `List<SpaFormattedAnglesResult>` | Rise/set pairs with formatted strings |
|
||||
|
||||
---
|
||||
|
||||
## formatTime
|
||||
|
||||
```dart
|
||||
String formatTime(double hours)
|
||||
```
|
||||
|
||||
Formats fractional hours to an `HH:MM:SS` string. Returns `"N/A"` for non-finite or negative values (polar scenarios).
|
||||
|
||||
---
|
||||
|
||||
## Custom zenith angles
|
||||
|
||||
Pass any solar zenith angles to `customAngles` to get rise/set times at those depression depths. Standard angles: civil twilight = 96°, nautical = 102°, astronomical = 108°. Prayer time implementations use this for Fajr and Isha.
|
||||
|
||||
```dart
|
||||
final result = getSpa(
|
||||
|
|
@ -62,8 +123,8 @@ final result = getSpa(
|
|||
customAngles: [96.0, 102.0, 108.0],
|
||||
);
|
||||
|
||||
for (final angle in result.angles) {
|
||||
print('${angle.zenith}: rise ${angle.sunrise}, set ${angle.sunset}');
|
||||
for (int i = 0; i < result.angles.length; i++) {
|
||||
print('Angle ${i}: rise ${result.angles[i].sunrise}, set ${result.angles[i].sunset}');
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
32
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
32
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as contributors and maintainers pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Accepting constructive criticism gracefully
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy toward other community members
|
||||
|
||||
Examples of unacceptable behavior:
|
||||
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior. They will take appropriate and fair corrective action in response to any behavior they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at alisalaah@gmail.com. All complaints will be reviewed and investigated. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
||||
46
.github/wiki/CONTRIBUTING.md
vendored
Normal file
46
.github/wiki/CONTRIBUTING.md
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Contributing to nrel-spa-dart
|
||||
|
||||
Thanks for taking the time to contribute.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Fork the repository and clone your fork.
|
||||
2. Install the Dart SDK (stable channel): [dart.dev/get-dart](https://dart.dev/get-dart)
|
||||
3. Install dependencies: `dart pub get`
|
||||
4. Run the tests: `dart test`
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Open a GitHub issue with:
|
||||
- A minimal reproducible example
|
||||
- The actual output and the expected output
|
||||
- Your Dart SDK version (`dart --version`)
|
||||
- Your operating system
|
||||
|
||||
## Suggesting enhancements
|
||||
|
||||
Open a GitHub issue before writing code. Describe the use case and the proposed API. Algorithm changes require a reference to the relevant paper or standard.
|
||||
|
||||
## Pull requests
|
||||
|
||||
1. Create a feature branch from `main`.
|
||||
2. Keep changes focused. One feature or bug fix per PR.
|
||||
3. Add or update tests for any changed behavior.
|
||||
4. Run `dart analyze` and confirm zero issues.
|
||||
5. Run `dart format lib/ test/` before committing.
|
||||
6. Update the CHANGELOG.md under `## [Unreleased]` with a brief description.
|
||||
7. Open the PR with a clear title and description.
|
||||
|
||||
## Code style
|
||||
|
||||
Follow standard Dart conventions. The project uses `dart format` with default settings. Lint rules are defined in `analysis_options.yaml` and inherit from the `lints` package.
|
||||
|
||||
Comments should explain *why*, not *what*. Algorithm steps should reference the equation number from Reda & Andreas (2004).
|
||||
|
||||
## Algorithm changes
|
||||
|
||||
This package implements the NREL Solar Position Algorithm exactly as specified in NREL/TP-560-34302. Proposed deviations from the paper require strong justification and a test against the validation dataset from the paper.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
33
.github/wiki/Home.md
vendored
33
.github/wiki/Home.md
vendored
|
|
@ -1,29 +1,34 @@
|
|||
# nrel_spa
|
||||
|
||||
NREL Solar Position Algorithm for Dart and Flutter. Calculates solar zenith, azimuth, sunrise, sunset, and solar noon for any location and time. Pure Dart, zero dependencies.
|
||||
NREL Solar Position Algorithm for Dart and Flutter. Calculates solar zenith, azimuth, sunrise, sunset, and solar noon for any location and time. Pure Dart, zero dependencies, accurate to ±0.0003 degrees.
|
||||
|
||||
Accurate to +/- 0.0003 degrees. Based on Reda & Andreas (2004), NREL/TP-560-34302.
|
||||
Based on Reda & Andreas (2004), NREL/TP-560-34302.
|
||||
|
||||
## Quick Start
|
||||
## Install
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
nrel_spa: ^1.0.1
|
||||
```
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
final result = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 17, 0, 0),
|
||||
40.7128, // latitude
|
||||
-74.0060, // longitude
|
||||
-5.0, // UTC offset (EST)
|
||||
40.7128, // latitude
|
||||
-74.0060, // longitude
|
||||
-5.0, // UTC offset (EST)
|
||||
);
|
||||
|
||||
print('Zenith: ${result.zenith.toStringAsFixed(4)}');
|
||||
print('Azimuth: ${result.azimuth.toStringAsFixed(4)}');
|
||||
print('Sunrise: ${result.sunrise.toStringAsFixed(4)} h');
|
||||
print('Solar Noon: ${result.solarNoon.toStringAsFixed(4)} h');
|
||||
print('Sunset: ${result.sunset.toStringAsFixed(4)} h');
|
||||
print('Zenith: ${result.zenith.toStringAsFixed(4)}°');
|
||||
print('Sunrise: ${result.sunrise.toStringAsFixed(4)} h');
|
||||
```
|
||||
|
||||
## Pages
|
||||
## Contents
|
||||
|
||||
- [API Reference](API-Reference): Full function and type reference
|
||||
- [Architecture](Architecture): Algorithm design and implementation notes
|
||||
- [Quickstart Guide](guides/quickstart) — install, first call, common patterns
|
||||
- [Advanced Usage](guides/advanced) — custom zenith angles, elevation, atmospheric correction
|
||||
- [API Reference](API-Reference) — full function and type reference
|
||||
- [Examples](examples/basic-usage) — real-world snippets
|
||||
- [Contributing](CONTRIBUTING)
|
||||
|
|
|
|||
27
.github/wiki/SECURITY.md
vendored
Normal file
27
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Security
|
||||
|
||||
## Scope
|
||||
|
||||
`nrel_spa` is a pure-computation library with no network access, no file I/O, and no external dependencies. The attack surface is limited to the mathematical functions themselves.
|
||||
|
||||
The main concern is input validation: `getSpa` and `calcSpa` throw `ArgumentError` when inputs fall outside the NREL SPA valid ranges (e.g., latitude outside -90..90, year outside -2000..6000). If you pass untrusted input to these functions, catch the error.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security issue (for example, a case where malformed input causes unexpected behavior beyond the documented `ArgumentError`), please report it privately before filing a public issue.
|
||||
|
||||
**Contact:** alisalaah@gmail.com
|
||||
|
||||
Include:
|
||||
|
||||
1. A description of the vulnerability
|
||||
2. Steps to reproduce it
|
||||
3. The version of `nrel_spa` where you observed the issue
|
||||
4. Any suggested fix if you have one
|
||||
|
||||
You can expect an acknowledgment within 48 hours and a resolution or status update within 7 days.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- The algorithm is valid for years -2000 to 6000 and is accurate to within ±0.0003° for solar zenith angle. Inputs outside this range produce `ArgumentError`.
|
||||
- This is a Dart port of the NREL SPA algorithm (Reda & Andreas, 2004, NREL/TP-560-34302). Numerical results match the reference implementation to the precision defined in that report.
|
||||
3
.github/wiki/_Footer.md
vendored
Normal file
3
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[](https://pub.dev/packages/nrel_spa) [](https://github.com/acamarata/nrel-spa-dart/blob/main/LICENSE)
|
||||
|
||||
[pub.dev](https://pub.dev/packages/nrel_spa) | [GitHub](https://github.com/acamarata/nrel-spa-dart) | [Issues](https://github.com/acamarata/nrel-spa-dart/issues)
|
||||
19
.github/wiki/_Sidebar.md
vendored
Normal file
19
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
## nrel_spa
|
||||
|
||||
- [Home](Home)
|
||||
- [API Reference](API-Reference)
|
||||
|
||||
**Guides**
|
||||
- [Quickstart](guides/quickstart)
|
||||
|
||||
**Examples**
|
||||
- [Basic Usage](examples/basic-usage)
|
||||
- [Prayer Times Integration](examples/prayer-times-integration)
|
||||
|
||||
**Project**
|
||||
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||
- [Security](SECURITY)
|
||||
|
||||
---
|
||||
|
||||
[pub.dev](https://pub.dev/packages/nrel_spa) | [GitHub](https://github.com/acamarata/nrel-spa-dart)
|
||||
74
.github/wiki/examples/basic-usage.md
vendored
Normal file
74
.github/wiki/examples/basic-usage.md
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Basic Usage Examples
|
||||
|
||||
## Sunrise and Sunset for a City
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
void printSolarDay(String city, double lat, double lng, double utcOffset) {
|
||||
final now = DateTime.now().toUtc();
|
||||
final result = getSpa(now, lat, lng, utcOffset);
|
||||
|
||||
String fmt(double h) {
|
||||
if (h.isNaN) return 'n/a';
|
||||
final hh = h.truncate();
|
||||
final mm = ((h - hh) * 60).round();
|
||||
return '${hh.toString().padLeft(2, '0')}:${mm.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
print('$city');
|
||||
print(' Sunrise: ${fmt(result.sunrise)}');
|
||||
print(' Solar noon: ${fmt(result.solarNoon)}');
|
||||
print(' Sunset: ${fmt(result.sunset)}');
|
||||
print(' Zenith now: ${result.zenith.toStringAsFixed(2)}°');
|
||||
}
|
||||
|
||||
void main() {
|
||||
printSolarDay('New York', 40.7128, -74.0060, -5.0);
|
||||
printSolarDay('London', 51.5074, -0.1278, 0.0);
|
||||
printSolarDay('Makkah', 21.3891, 39.8579, 3.0);
|
||||
printSolarDay('Melbourne', -37.8136, 144.9631, 10.0);
|
||||
}
|
||||
```
|
||||
|
||||
## Annual Solar Noon Curve
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
void main() {
|
||||
const lat = 40.7128;
|
||||
const lng = -74.0060;
|
||||
const utcOffset = -5.0;
|
||||
|
||||
print('Day,SolarNoon');
|
||||
for (int day = 1; day <= 365; day++) {
|
||||
final date = DateTime.utc(2024, 1, 1).add(Duration(days: day - 1));
|
||||
final result = getSpa(date.add(const Duration(hours: 12)), lat, lng, utcOffset);
|
||||
print('$day,${result.solarNoon.toStringAsFixed(6)}');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Twilight Times for Prayer Calculation
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
void main() {
|
||||
final date = DateTime.utc(2024, 3, 15, 12, 0, 0);
|
||||
const lat = 33.8938; // Jerusalem
|
||||
const lng = 35.5018;
|
||||
const utcOffset = 2.0; // EET
|
||||
|
||||
final result = getSpa(
|
||||
date, lat, lng, utcOffset,
|
||||
customAngles: [108.0, 107.0], // Fajr 18°, Isha 17°
|
||||
);
|
||||
|
||||
print('Fajr (18°): ${result.angles[0].sunrise.toStringAsFixed(4)} h');
|
||||
print('Isha (17°): ${result.angles[1].sunset.toStringAsFixed(4)} h');
|
||||
print('Sunrise: ${result.sunrise.toStringAsFixed(4)} h');
|
||||
print('Sunset: ${result.sunset.toStringAsFixed(4)} h');
|
||||
}
|
||||
```
|
||||
133
.github/wiki/examples/prayer-times-integration.md
vendored
Normal file
133
.github/wiki/examples/prayer-times-integration.md
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Prayer Times Integration
|
||||
|
||||
This example shows how to use `nrel_spa` inside a Flutter app to build a solar-position widget. It computes the current solar zenith and the times for sunrise, solar noon, and sunset, then displays them in a card.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Add to `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
nrel_spa: ^1.0.0
|
||||
geolocator: ^14.0.0
|
||||
```
|
||||
|
||||
## SolarDayCard Widget
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
class SolarDayCard extends StatefulWidget {
|
||||
const SolarDayCard({super.key});
|
||||
|
||||
@override
|
||||
State<SolarDayCard> createState() => _SolarDayCardState();
|
||||
}
|
||||
|
||||
class _SolarDayCardState extends State<SolarDayCard> {
|
||||
SpaFormattedResult? _result;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_compute();
|
||||
}
|
||||
|
||||
Future<void> _compute() async {
|
||||
try {
|
||||
final permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
setState(() => _error = 'Location permission denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
final pos = await Geolocator.getCurrentPosition();
|
||||
final now = DateTime.now().toUtc();
|
||||
final utcOffset = DateTime.now().timeZoneOffset.inMinutes / 60.0;
|
||||
|
||||
// calcSpa returns pre-formatted HH:MM:SS strings for the time fields.
|
||||
final result = calcSpa(now, pos.latitude, pos.longitude, utcOffset);
|
||||
setState(() => _result = result);
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error != null) {
|
||||
return Card(child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Error: $_error'),
|
||||
));
|
||||
}
|
||||
|
||||
if (_result == null) {
|
||||
return const Card(child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
));
|
||||
}
|
||||
|
||||
final r = _result!;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Solar Day', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
_row('Sunrise', r.sunrise),
|
||||
_row('Solar noon', r.solarNoon),
|
||||
_row('Sunset', r.sunset),
|
||||
_row('Zenith now', '${r.zenith.toStringAsFixed(2)}°'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(String label, String value) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text(label), Text(value)],
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Zenith Angles for Twilight
|
||||
|
||||
The `customAngles` parameter lets you request rise and set times at any zenith angle. This is useful for civil twilight (96°), nautical twilight (102°), and Islamic twilight used in prayer-time calculation (Fajr at 108°, Isha at 107°).
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
void printTwilight(double lat, double lng, double utcOffset) {
|
||||
final date = DateTime.now().toUtc();
|
||||
|
||||
// spaZaRts (default) computes rise/transit/set. Required for customAngles.
|
||||
final r = getSpa(
|
||||
date, lat, lng, utcOffset,
|
||||
functionCode: spaZaRts,
|
||||
customAngles: [96.0, 102.0, 108.0],
|
||||
);
|
||||
|
||||
print('Civil twilight begin: ${r.angles[0].sunrise.toStringAsFixed(4)} h');
|
||||
print('Nautical twilight begin: ${r.angles[1].sunrise.toStringAsFixed(4)} h');
|
||||
print('Fajr (18°) begin: ${r.angles[2].sunrise.toStringAsFixed(4)} h');
|
||||
print('Fajr (18°) end: ${r.angles[2].sunset.toStringAsFixed(4)} h');
|
||||
print('Nautical twilight end: ${r.angles[1].sunset.toStringAsFixed(4)} h');
|
||||
print('Civil twilight end: ${r.angles[0].sunset.toStringAsFixed(4)} h');
|
||||
}
|
||||
|
||||
void main() {
|
||||
printTwilight(40.7128, -74.006, -5.0); // New York, EST
|
||||
}
|
||||
```
|
||||
83
.github/wiki/guides/advanced.md
vendored
Normal file
83
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Advanced Usage
|
||||
|
||||
## Custom Zenith Angles
|
||||
|
||||
Calculate rise/set times for any solar depression angle. Useful for computing civil, nautical, and astronomical twilight, or Islamic prayer times.
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
final result = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
40.7128, -74.0060, -5.0,
|
||||
customAngles: [96.0, 102.0, 108.0], // civil, nautical, astronomical
|
||||
);
|
||||
|
||||
for (int i = 0; i < result.angles.length; i++) {
|
||||
final a = result.angles[i];
|
||||
print('Angle ${a.angle}°: rise=${a.sunrise.toStringAsFixed(4)} h, set=${a.sunset.toStringAsFixed(4)} h');
|
||||
}
|
||||
```
|
||||
|
||||
Standard zenith angles:
|
||||
|
||||
| Twilight type | Zenith angle |
|
||||
| --- | --- |
|
||||
| Civil | 96.0° |
|
||||
| Nautical | 102.0° |
|
||||
| Astronomical | 108.0° |
|
||||
| Fajr (18°) | 108.0° |
|
||||
| Isha (17°) | 107.0° |
|
||||
|
||||
## Elevation and Atmospheric Correction
|
||||
|
||||
For more accurate results at high elevation or in varying atmospheric conditions:
|
||||
|
||||
```dart
|
||||
final result = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
39.7392, // Denver latitude
|
||||
-104.9903, // Denver longitude
|
||||
-7.0, // UTC offset (MST)
|
||||
elevation: 1609.0, // meters above sea level
|
||||
pressure: 835.0, // mbar (lower at altitude)
|
||||
temperature: 10.0, // Celsius
|
||||
);
|
||||
```
|
||||
|
||||
## deltaT Parameter
|
||||
|
||||
`deltaT` is the difference between Terrestrial Time (TT) and Universal Time (UT1), in seconds. The default is 67 seconds, which is accurate for recent dates. For historical calculations or projections, supply the correct value from [IERS tables](https://www.iers.org/IERS/EN/Science/EarthRotation/EarthRotation.html).
|
||||
|
||||
```dart
|
||||
// Historical calculation (1990)
|
||||
final result = getSpa(
|
||||
DateTime.utc(1990, 6, 21, 12, 0, 0),
|
||||
40.7128, -74.0060, -5.0,
|
||||
deltaT: 57.2,
|
||||
);
|
||||
```
|
||||
|
||||
## Batch Calculations
|
||||
|
||||
For high-volume calculations (annual ephemeris, solar energy modeling), call `getSpa` in a loop. The function is synchronous and pure — safe to call from any isolate.
|
||||
|
||||
```dart
|
||||
final times = List.generate(365, (i) {
|
||||
final date = DateTime.utc(2024, 1, 1).add(Duration(days: i));
|
||||
return getSpa(date, lat, lng, utcOffset);
|
||||
});
|
||||
```
|
||||
|
||||
For parallel batch work in Flutter, dispatch to an isolate:
|
||||
|
||||
```dart
|
||||
import 'dart:isolate';
|
||||
|
||||
final results = await Isolate.run(() {
|
||||
return List.generate(365, (i) {
|
||||
final date = DateTime.utc(2024, 1, 1).add(Duration(days: i));
|
||||
return getSpa(date, lat, lng, utcOffset);
|
||||
});
|
||||
});
|
||||
```
|
||||
69
.github/wiki/guides/quickstart.md
vendored
Normal file
69
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Quickstart
|
||||
|
||||
## Install
|
||||
|
||||
Add to `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
nrel_spa: ^1.0.1
|
||||
```
|
||||
|
||||
Run `dart pub get`.
|
||||
|
||||
## First Call
|
||||
|
||||
```dart
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
|
||||
void main() {
|
||||
final result = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 17, 0, 0),
|
||||
40.7128, // latitude (New York)
|
||||
-74.0060, // longitude
|
||||
-5.0, // UTC offset (EST)
|
||||
);
|
||||
|
||||
print('Solar zenith: ${result.zenith.toStringAsFixed(4)}°');
|
||||
print('Solar azimuth: ${result.azimuth.toStringAsFixed(4)}°');
|
||||
print('Sunrise: ${result.sunrise.toStringAsFixed(4)} h');
|
||||
print('Solar noon: ${result.solarNoon.toStringAsFixed(4)} h');
|
||||
print('Sunset: ${result.sunset.toStringAsFixed(4)} h');
|
||||
}
|
||||
```
|
||||
|
||||
## Reading Results
|
||||
|
||||
`getSpa` returns an `SpaResult`:
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `zenith` | `double` | Solar zenith angle (degrees) |
|
||||
| `azimuth` | `double` | Solar azimuth angle (degrees from north) |
|
||||
| `sunrise` | `double` | Sunrise time (decimal hours, UTC) |
|
||||
| `solarNoon` | `double` | Solar noon time (decimal hours, UTC) |
|
||||
| `sunset` | `double` | Sunset time (decimal hours, UTC) |
|
||||
| `angles` | `List<AngleResult>` | Rise/set times for custom zenith angles |
|
||||
|
||||
## DateTime Input
|
||||
|
||||
Always pass UTC `DateTime` values. The `timezone` parameter shifts the output times to local time.
|
||||
|
||||
```dart
|
||||
// Convert local DateTime to UTC before passing
|
||||
final local = DateTime(2024, 3, 15, 12, 0, 0);
|
||||
final utc = local.toUtc();
|
||||
final result = getSpa(utc, lat, lng, utcOffset);
|
||||
```
|
||||
|
||||
## Null Results for Sunrise/Sunset
|
||||
|
||||
At polar latitudes or in summer/winter extremes, sunrise or sunset may not occur. In those cases, `result.sunrise` and `result.sunset` return `double.nan`. Always check before displaying:
|
||||
|
||||
```dart
|
||||
if (!result.sunrise.isNaN) {
|
||||
print('Sunrise: ${result.sunrise}');
|
||||
} else {
|
||||
print('No sunrise today.');
|
||||
}
|
||||
```
|
||||
21
README.md
21
README.md
|
|
@ -3,6 +3,7 @@
|
|||
[](https://pub.dev/packages/nrel_spa)
|
||||
[](https://github.com/acamarata/nrel-spa-dart/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/acamarata/nrel-spa-dart/wiki)
|
||||
|
||||
NREL Solar Position Algorithm for Dart and Flutter. Calculates solar zenith, azimuth, sunrise, sunset, and solar noon for any location and time. Pure Dart, zero dependencies.
|
||||
|
||||
|
|
@ -54,25 +55,9 @@ for (final angle in result.angles) {
|
|||
|
||||
## API
|
||||
|
||||
### `getSpa(date, latitude, longitude, timezone, {...})`
|
||||
`getSpa(date, latitude, longitude, timezone, {...})` returns `SpaResult` with `zenith`, `azimuth`, `sunrise`, `solarNoon`, `sunset`, and `angles`. Use `calcSpa` for pre-formatted HH:MM:SS strings.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `date` | `DateTime` | required | UTC date and time |
|
||||
| `latitude` | `double` | required | Degrees (-90 to 90) |
|
||||
| `longitude` | `double` | required | Degrees (-180 to 180) |
|
||||
| `timezone` | `double` | required | Hours from UTC |
|
||||
| `elevation` | `double` | 0 | Meters above sea level |
|
||||
| `pressure` | `double` | 1013 | Atmospheric pressure (mbar) |
|
||||
| `temperature` | `double` | 15 | Temperature (Celsius) |
|
||||
| `deltaT` | `double` | 67 | TT - UT1 (seconds) |
|
||||
| `customAngles` | `List<double>` | [] | Zenith angles for rise/set |
|
||||
|
||||
Returns `SpaResult` with `zenith`, `azimuth`, `sunrise`, `solarNoon`, `sunset`, and `angles`.
|
||||
|
||||
## Architecture
|
||||
|
||||
A direct port of the NREL Solar Position Algorithm (Reda & Andreas 2004) to Dart. The implementation follows the original algorithm's 142 intermediate calculations exactly. Custom zenith angles are computed in a single pass alongside the primary solar angles using the same ephemeris state.
|
||||
Full parameter reference and type definitions: [API Reference](https://github.com/acamarata/nrel-spa-dart/wiki/API-Reference).
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ description: >
|
|||
azimuth, sunrise, sunset, and solar noon for any location and time.
|
||||
Pure Dart, zero dependencies, ±0.0003° accuracy.
|
||||
version: 1.0.1
|
||||
homepage: https://github.com/acamarata/nrel-spa-dart
|
||||
repository: https://github.com/acamarata/nrel-spa-dart
|
||||
issue_tracker: https://github.com/acamarata/nrel-spa-dart/issues
|
||||
topics:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:nrel_spa/nrel_spa.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
|
@ -128,24 +126,26 @@ void main() {
|
|||
expect(result.incidence.isNaN, isTrue);
|
||||
});
|
||||
|
||||
test('matches spaZaRts zenith/azimuth to within floating point tolerance',
|
||||
() {
|
||||
final za = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
40.7128,
|
||||
-74.0060,
|
||||
-5.0,
|
||||
functionCode: spaZa,
|
||||
);
|
||||
final rts = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
40.7128,
|
||||
-74.0060,
|
||||
-5.0,
|
||||
);
|
||||
expect((za.zenith - rts.zenith).abs(), lessThan(1e-10));
|
||||
expect((za.azimuth - rts.azimuth).abs(), lessThan(1e-10));
|
||||
});
|
||||
test(
|
||||
'matches spaZaRts zenith/azimuth to within floating point tolerance',
|
||||
() {
|
||||
final za = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
40.7128,
|
||||
-74.0060,
|
||||
-5.0,
|
||||
functionCode: spaZa,
|
||||
);
|
||||
final rts = getSpa(
|
||||
DateTime.utc(2024, 3, 15, 12, 0, 0),
|
||||
40.7128,
|
||||
-74.0060,
|
||||
-5.0,
|
||||
);
|
||||
expect((za.zenith - rts.zenith).abs(), lessThan(1e-10));
|
||||
expect((za.azimuth - rts.azimuth).abs(), lessThan(1e-10));
|
||||
},
|
||||
);
|
||||
|
||||
test('throws when customAngles used with spaZa', () {
|
||||
expect(
|
||||
|
|
@ -368,8 +368,13 @@ void main() {
|
|||
|
||||
test('throws for invalid functionCode', () {
|
||||
expect(
|
||||
() => getSpa(DateTime.utc(2024, 1, 1), 40.0, -74.0, -5.0,
|
||||
functionCode: 4),
|
||||
() => getSpa(
|
||||
DateTime.utc(2024, 1, 1),
|
||||
40.0,
|
||||
-74.0,
|
||||
-5.0,
|
||||
functionCode: 4,
|
||||
),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
|
|
@ -422,10 +427,7 @@ void main() {
|
|||
);
|
||||
expect(result.angles.length, equals(1));
|
||||
expect(result.angles[0], isA<SpaFormattedAnglesResult>());
|
||||
expect(
|
||||
result.angles[0].sunrise,
|
||||
matches(RegExp(r'^\d{2}:\d{2}:\d{2}$')),
|
||||
);
|
||||
expect(result.angles[0].sunrise, matches(RegExp(r'^\d{2}:\d{2}:\d{2}$')));
|
||||
});
|
||||
|
||||
test('incidence populated for spaAll', () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue