---
README.org | 8 +-
man/thp.1.scd | 2 +
man/thpd.5.scd | 31 +++++++
mods/star+star.ha | 213 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 250 insertions(+), 4 deletions(-)
create mode 100644 mods/star+star.ha
diff --git a/README.org b/README.org
index 809b97d..488ae9a 100644
--- a/README.org
+++ b/README.org
@@ -32,16 +32,16 @@ one patche applied (as of now):
To build thp with all modules included, you need libgit2 and your platforms libgit2-dev equivalent installed, then issued
#+BEGIN_SRC sh :exports code
-make EXTRA_MODS="project git clock timer shind uptime moon sun"
-make EXTRA_MODS="project git clock timer shind uptime moon sun" test
+make EXTRA_MODS="project git clock timer shind uptime moon sun star"
+make EXTRA_MODS="project git clock timer shind uptime moon sun star" test
make PREFIX="${HOME}" install
#+END_SRC
To build with just the modules you use in your spec, use ~MODS~:
#+BEGIN_SRC sh :exports code
-make MODS="path host project timer uptime moon sun"
-make MODS="path host project timer uptime moon sun" test
+make MODS="path host project timer uptime moon sun star"
+make MODS="path host project timer uptime moon sun star" test
make PREFIX="${HOME}" install
#+END_SRC
diff --git a/man/thp.1.scd b/man/thp.1.scd
index cd6a11f..f070fe2 100644
--- a/man/thp.1.scd
+++ b/man/thp.1.scd
@@ -110,6 +110,8 @@ All modules are optionally compiled into the server executable.
*sunrise* and *sunset* display the time of sunrise and sunset, respectively.
The location and time format can be configured. See thpd(5).
+*star* displays the position of a star as seen by an observer on Earth.
+
# STYLES
Available colors are: black, red, green, yellow, blue, magenta, cyan, white,
diff --git a/man/thpd.5.scd b/man/thpd.5.scd
index 1673a01..6808162 100644
--- a/man/thpd.5.scd
+++ b/man/thpd.5.scd
@@ -204,6 +204,37 @@ sunrise.layout=%H:%M|
sunset.layout=%H:%M|
```
+## star
+
+Display the position of a star as seen by an observer on Earth.
+
+- name[=Antares]
+ Star name.
+
+- ra[=16h29m24.0s]
+ Right ascension coordinate.
+
+- dec[=-26d25m55.0s]
+ Declination coordinate.
+
+- ao[=N]
+ Azimuth origin. Azimuth is usually measured from the north direction.
+To measure azimuth from the south, define ao=S.
+
+- layout[=${NAME}(${AZIMUTH}°,${ALTITUDE}°)]
+ Format string.
+
+Note: The observer's coordinates on Earth are defined in the _sun_ section.
+
+*example*:
+
+```
+[star]
+name=Betelgeuse
+ra=5h55m10.3s
+dec=+7d24m25.4s
+```
+
# SEE ALSO
*thpd*(1) *thp*(1) *thp*(5)
diff --git a/mods/star+star.ha b/mods/star+star.ha
new file mode 100644
index 0000000..7ced726
--- /dev/null
+++ b/mods/star+star.ha
@@ -0,0 +1,213 @@
+// Author: Carlos Une <une@fastmail.fm>
+// Maintainer: Carlos Une <une@fastmail.fm>
+// SPDX-FileCopyrightText: 2024 Carlos Une <une@fastmail.fm>
+// SPDX-License-Identifier: GPL-3.0-or-later
+// Compute star position. The error in the calculation is approximately ±1 arcminute
+// relative to the true value.
+use config;
+use env;
+use fmt;
+use io;
+use math;
+use memio;
+use regex;
+use strconv;
+use strings;
+use strings::template;
+use time::date;
+
+// Default observer coordinates. (São Paulo/BR)
+def DEFAULT_LATITUDE = "-23.5898764";
+def DEFAULT_LONGITUDE = "-46.6589231";
+// Default star coordinates. (Antares/Alpha Scorpii)
+def DEFAULT_STAR_NAME = "Antares";
+def DEFAULT_STAR_RA = "16h29m24.0s";
+def DEFAULT_STAR_DEC = "-26d25m55.0s";
+def DEFAULT_STAR_LAYOUT = "${NAME}(${AZIMUTH}°,${ALTITUDE}°)";
+// Standard epoch
+def J2000: f64 = 2451545.0;
+type azimuth_origin = enum { north, south };
+type invalid = !void;
+type coordinate_error = (regex::error | strconv::error | invalid);
+
+type starconfig = struct {
+ lat: f64,
+ lon: f64,
+ name: str,
+ ra: f64,
+ dec: f64,
+ ao: azimuth_origin,
+ template: template::template,
+};
+
+// Parse right ascension coordinate.
+// Example Input: "5h55m10.3053s"; Output: 88.79293875 [decimal degrees]
+fn parse_ra(s: str) (f64 | coordinate_error) = {
+ let re = regex::compile(`([[:digit:]]{1,2})h([[:digit:]]{1,2})m([[:digit:]]{1,2}\.[[:digit:]]+)s`)?;
+ defer regex::finish(&re);
+ let cap = regex::findall(&re, s);
+ defer regex::result_freeall(cap);
+
+ if (len(cap) == 1) {
+ let ra = 15.0*strconv::stof64(cap[0][1].content)?
+ + 15.0*strconv::stof64(cap[0][2].content)?/60.0
+ + 15.0*strconv::stof64(cap[0][3].content)?/3600.0;
+ if (ra <= 360.0) {
+ return ra;
+ };
+ };
+ return invalid;
+};
+
+// Parse declination coordinate.
+// Example Input: "+7d24m25.426s"; Output: 7.407062777777778 [decimal degrees]
+fn parse_dec(s: str) (f64 | coordinate_error) = {
+ let re = regex::compile(`([-+]?[[:digit:]]{1,2})d([[:digit:]]{1,2})m([[:digit:]]{1,2}\.[[:digit:]]+)s`)?;
+ defer regex::finish(&re);
+ let cap = regex::findall(&re, s);
+ defer regex::result_freeall(cap);
+
+ if (len(cap) == 1) {
+ let degree = strconv::stof64(cap[0][1].content)?;
+ let dec = math::signf64(degree): f64 *(math::absf64(degree)
+ + strconv::stof64(cap[0][2].content)?/60.0
+ + strconv::stof64(cap[0][3].content)?/3600.0);
+ if (dec >= -90.0 && dec <= 90.0) {
+ return dec;
+ };
+ };
+ return invalid;
+};
+
+fn read_starconfig() (starconfig | error) = {
+ let lat = match (strconv::stof64(config::setting("sun.latitude", DEFAULT_LATITUDE))) {
+ case let x: f64 => yield x;
+ case => return "mod_star:invalid latitude";
+ };
+ let lon = match (strconv::stof64(config::setting("sun.longitude", DEFAULT_LONGITUDE))) {
+ case let x: f64 => yield x;
+ case => return "mod_star:invalid longitude";
+ };
+ let ra = match (parse_ra(config::setting("star.ra", DEFAULT_STAR_RA))) {
+ case let r: f64 => yield r;
+ case => return "mod_star:invalid right ascension";
+ };
+ let dec = match (parse_dec(config::setting("star.dec", DEFAULT_STAR_DEC))) {
+ case let d: f64 => yield d;
+ case => return "mod_star:invalid declination";
+ };
+ let ao = switch (config::setting("star.ao", "N")) {
+ case "N" => yield azimuth_origin::north;
+ case "S" => yield azimuth_origin::south;
+ case => return "mod_star:invalid azimuth_origin";
+ };
+ return starconfig {
+ lat = lat,
+ lon = lon,
+ name = config::setting("star.name", DEFAULT_STAR_NAME),
+ ra = ra,
+ dec = dec,
+ ao = ao,
+ template = template::compile(config::setting("star.layout", DEFAULT_STAR_LAYOUT))! };
+};
+
+@init fn register_star() void = {
+ register("star", &mod_star);
+};
+
+export fn mod_star(pe: *env::env) str = {
+ let cfg = match (read_starconfig()) {
+ case let c: starconfig => yield c;
+ case let e: error => return strings::dup(e);
+ };
+ let JD = julian_day(date::now());
+ let (alpha, delta) = precess(cfg.ra, cfg.dec, JD);
+ let (az, alt) = to_horizontal(JD, cfg.lat, cfg.lon, alpha, delta, cfg.ao);
+ let sink = memio::dynamic();
+ match (template::execute(&cfg.template, &sink, ("NAME", cfg.name),
+ ("AZIMUTH", az: int), ("ALTITUDE", alt: int))) {
+ case size =>
+ return memio::string(&sink)!;
+ case io::error =>
+ return strings::dup("mod_star:template::execute() failed");
+ };
+};
+
+// Compute the effects of precession on equatorial coordinates (α, δ).
+// Precession Model: IAU 1976.
+// Equinox J2000, FK5 reference frame.
+// Reference: Meeus, J. — 'Astronomical Algorithms, 1991'
+fn precess(alpha: f64, delta: f64, JD: f64) (f64, f64) = {
+ let t = (JD - J2000)/36525.0;
+ let tt = t*t;
+ let ttt = tt*t;
+ // Euler angles (ζ, z, θ).
+ let zeta = 2306.2181*t + 0.30188*tt + 0.017998*ttt;
+ let z = 2306.2181*t + 1.09468*tt + 0.018203*ttt;
+ let theta = 2004.3109*t - 0.42665*tt - 0.041833*ttt;
+ let arcsecond = 1.0/3600.0;
+ z *= arcsecond;
+ alpha = rad(alpha);
+ delta = rad(delta);
+ zeta = rad(zeta*arcsecond);
+ theta = rad(theta*arcsecond);
+ let A = math::cosf64(delta)*math::sinf64(alpha + zeta);
+ let B = math::cosf64(theta)*math::cosf64(delta)*math::cosf64(alpha + zeta) - math::sinf64(theta)*math::sinf64(delta);
+ let C = math::sinf64(theta)*math::cosf64(delta)*math::cosf64(alpha + zeta) + math::cosf64(theta)*math::sinf64(delta);
+ let alpha2 = deg(math::atan2f64(A, B)) + z;
+ if (alpha2 < 0.0) { alpha2 += 360.0; };
+ return (alpha2, deg(math::asinf64(C)));
+};
+
+// Compute horizontal coordinates (azimuth, altitude).
+// Reference: Meeus, J. — 'Astronomical Algorithms, 1991'
+fn to_horizontal(JD: f64, lat: f64, lon: f64, alpha: f64, delta: f64, ao: azimuth_origin = azimuth_origin::north) (f64, f64) = {
+ let mst = mean_sidereal_time_gmst(JD);
+ let H = rad(mst + lon - alpha);
+ lat = rad(lat);
+ delta = rad(delta);
+ let A = deg(math::atan2f64(math::sinf64(H), math::cosf64(H)*math::sinf64(lat) - math::tanf64(delta)*math::cosf64(lat)));
+ let sinh = math::sinf64(lat)*math::sinf64(delta) + math::cosf64(lat)*math::cosf64(delta)*math::cosf64(H);
+ let h = deg(math::asinf64(sinh));
+ if (ao == azimuth_origin::north) {
+ A += 180.0; // azimuth measured from the north
+ };
+ if (A > 360.0) { A -= 360.0; };
+ return (A, h);
+};
+
+// Compute Julian Day
+fn julian_day(d: date::date) f64 = {
+ let Y = date::year(&d);
+ let M = date::month(&d);
+ let D = date::day(&d);
+ let H = date::hour(&d);
+ let MIN = date::minute(&d);
+ let S = date::second(&d): f64 + date::nanosecond(&d): f64/1.0e9;
+ return julian_day_ymd(Y, M, D) + ((H*3600 + MIN*60): f64 + S)/86400.0;
+};
+
+// Compute Julian Day
+// Reference: Meeus, J. — 'Astronomical Algorithms, 1991'
+fn julian_day_ymd(Y: int, M: int, D: int) f64 = {
+ if (M <= 2) {
+ Y -= 1;
+ M += 12;
+ };
+ let A = Y/100;
+ let B = if (Y > 1582 || (Y == 1582 && (M > 10 || (M == 10 && D > 4)))) {
+ yield 2 - A + A/4;
+ } else {
+ yield 0;
+ };
+ return (365.25 * (Y + 4716): f64): int: f64 + (30.6001 * (M + 1): f64): int: f64 + D: f64 + B: f64 - 1524.5;
+};
+
+// Mean sidereal time at Greenwich — GMST
+// Reference: Meeus, J. — 'Astronomical Algorithms, 1991'
+fn mean_sidereal_time_gmst(JD: f64) f64 = {
+ let T = (JD - J2000)/36525.0;
+ let r = math::modf64(280.46061837 + 360.98564736629 * (JD - J2000) + (0.000387933*T*T) - (T*T*T)/38710000.0, 360.0);
+ if (r < 0.0) r += 360.0;
+ return r;
+};
--
2.39.2