siderust-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.github/workflows/ci.yml +166 -0
  2. package/.gitmodules +9 -0
  3. package/CHANGELOG.md +26 -0
  4. package/LICENSE +661 -0
  5. package/README.md +138 -0
  6. package/package.json +12 -0
  7. package/qtty-js/.github/workflows/ci.yml +151 -0
  8. package/qtty-js/.gitmodules +3 -0
  9. package/qtty-js/CHANGELOG.md +31 -0
  10. package/qtty-js/LICENSE +661 -0
  11. package/qtty-js/README.md +132 -0
  12. package/qtty-js/package.json +20 -0
  13. package/qtty-js/qtty/.github/workflows/ci.yml +155 -0
  14. package/qtty-js/qtty/CHANGELOG.md +120 -0
  15. package/qtty-js/qtty/Cargo.lock +1462 -0
  16. package/qtty-js/qtty/Cargo.toml +12 -0
  17. package/qtty-js/qtty/LICENSE +661 -0
  18. package/qtty-js/qtty/README.md +9 -0
  19. package/qtty-js/qtty/qtty/Cargo.toml +41 -0
  20. package/qtty-js/qtty/qtty/README.md +8 -0
  21. package/qtty-js/qtty/qtty/examples/angles.rs +14 -0
  22. package/qtty-js/qtty/qtty/examples/astronomy.rs +17 -0
  23. package/qtty-js/qtty/qtty/examples/dimensional_arithmetic.rs +83 -0
  24. package/qtty-js/qtty/qtty/examples/python_integration.rs +61 -0
  25. package/qtty-js/qtty/qtty/examples/quickstart.rs +15 -0
  26. package/qtty-js/qtty/qtty/examples/ratios.rs +12 -0
  27. package/qtty-js/qtty/qtty/examples/serde_with_unit.rs +234 -0
  28. package/qtty-js/qtty/qtty/examples/serialization.rs +141 -0
  29. package/qtty-js/qtty/qtty/examples/serialization_advanced.rs +155 -0
  30. package/qtty-js/qtty/qtty/src/f32.rs +108 -0
  31. package/qtty-js/qtty/qtty/src/f64.rs +30 -0
  32. package/qtty-js/qtty/qtty/src/i128.rs +111 -0
  33. package/qtty-js/qtty/qtty/src/i16.rs +111 -0
  34. package/qtty-js/qtty/qtty/src/i32.rs +111 -0
  35. package/qtty-js/qtty/qtty/src/i64.rs +111 -0
  36. package/qtty-js/qtty/qtty/src/i8.rs +111 -0
  37. package/qtty-js/qtty/qtty/src/lib.rs +238 -0
  38. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.lock +83 -0
  39. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/Cargo.toml +10 -0
  40. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std/src/lib.rs +7 -0
  41. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.lock +83 -0
  42. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/Cargo.toml +10 -0
  43. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-no-std-alloc/src/lib.rs +7 -0
  44. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.lock +83 -0
  45. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/Cargo.toml +10 -0
  46. package/qtty-js/qtty/qtty/tests/fixtures/qtty-vec-std/src/lib.rs +5 -0
  47. package/qtty-js/qtty/qtty/tests/integration_tests.rs +529 -0
  48. package/qtty-js/qtty/qtty/tests/qtty_vec_feature_matrix.rs +58 -0
  49. package/qtty-js/qtty/qtty-core/Cargo.toml +41 -0
  50. package/qtty-js/qtty/qtty-core/README.md +8 -0
  51. package/qtty-js/qtty/qtty-core/examples/diesel_integration.rs +145 -0
  52. package/qtty-js/qtty/qtty-core/examples/quantity_db_serde.rs +215 -0
  53. package/qtty-js/qtty/qtty-core/src/dimension.rs +249 -0
  54. package/qtty-js/qtty/qtty-core/src/feature_diesel.rs +318 -0
  55. package/qtty-js/qtty/qtty-core/src/feature_pyo3.rs +27 -0
  56. package/qtty-js/qtty/qtty-core/src/feature_serde.rs +203 -0
  57. package/qtty-js/qtty/qtty-core/src/feature_tiberius.rs +28 -0
  58. package/qtty-js/qtty/qtty-core/src/lib.rs +744 -0
  59. package/qtty-js/qtty/qtty-core/src/macros.rs +93 -0
  60. package/qtty-js/qtty/qtty-core/src/quantity.rs +810 -0
  61. package/qtty-js/qtty/qtty-core/src/scalar.rs +1742 -0
  62. package/qtty-js/qtty/qtty-core/src/unit.rs +332 -0
  63. package/qtty-js/qtty/qtty-core/src/units/angular.rs +1228 -0
  64. package/qtty-js/qtty/qtty-core/src/units/area.rs +243 -0
  65. package/qtty-js/qtty/qtty-core/src/units/frequency.rs +179 -0
  66. package/qtty-js/qtty/qtty-core/src/units/length.rs +1270 -0
  67. package/qtty-js/qtty/qtty-core/src/units/mass.rs +488 -0
  68. package/qtty-js/qtty/qtty-core/src/units/mod.rs +26 -0
  69. package/qtty-js/qtty/qtty-core/src/units/power.rs +324 -0
  70. package/qtty-js/qtty/qtty-core/src/units/time.rs +667 -0
  71. package/qtty-js/qtty/qtty-core/src/units/unitless.rs +212 -0
  72. package/qtty-js/qtty/qtty-core/src/units/velocity.rs +210 -0
  73. package/qtty-js/qtty/qtty-core/src/units/volume.rs +269 -0
  74. package/qtty-js/qtty/qtty-core/tests/core.rs +628 -0
  75. package/qtty-js/qtty/qtty-core/tests/diesel.rs +461 -0
  76. package/qtty-js/qtty/qtty-core/tests/integers.rs +632 -0
  77. package/qtty-js/qtty/qtty-core/tests/no_cross_unit_ops.rs +35 -0
  78. package/qtty-js/qtty/qtty-core/tests/pyo3.rs +334 -0
  79. package/qtty-js/qtty/qtty-core/tests/quantity_f32.rs +276 -0
  80. package/qtty-js/qtty/qtty-core/tests/scalar_decimal.rs +258 -0
  81. package/qtty-js/qtty/qtty-core/tests/scalar_f32.rs +286 -0
  82. package/qtty-js/qtty/qtty-core/tests/scalar_f64_real.rs +287 -0
  83. package/qtty-js/qtty/qtty-core/tests/scalar_rational.rs +260 -0
  84. package/qtty-js/qtty/qtty-core/tests/serde.rs +256 -0
  85. package/qtty-js/qtty/qtty-core/tests/tiberius.rs +208 -0
  86. package/qtty-js/qtty/qtty-derive/Cargo.toml +23 -0
  87. package/qtty-js/qtty/qtty-derive/README.md +8 -0
  88. package/qtty-js/qtty/qtty-derive/src/lib.rs +340 -0
  89. package/qtty-js/qtty/qtty-ffi/ARCHITECTURE.md +3 -0
  90. package/qtty-js/qtty/qtty-ffi/Cargo.toml +31 -0
  91. package/qtty-js/qtty/qtty-ffi/README.md +9 -0
  92. package/qtty-js/qtty/qtty-ffi/build.rs +326 -0
  93. package/qtty-js/qtty/qtty-ffi/cbindgen.toml +105 -0
  94. package/qtty-js/qtty/qtty-ffi/include/qtty_ffi.h +1126 -0
  95. package/qtty-js/qtty/qtty-ffi/src/ffi.rs +1251 -0
  96. package/qtty-js/qtty/qtty-ffi/src/ffi_serde.rs +294 -0
  97. package/qtty-js/qtty/qtty-ffi/src/helpers.rs +310 -0
  98. package/qtty-js/qtty/qtty-ffi/src/lib.rs +229 -0
  99. package/qtty-js/qtty/qtty-ffi/src/macros.rs +121 -0
  100. package/qtty-js/qtty/qtty-ffi/src/registry.rs +274 -0
  101. package/qtty-js/qtty/qtty-ffi/src/types.rs +620 -0
  102. package/qtty-js/qtty/qtty-ffi/tests/integration_tests.rs +842 -0
  103. package/qtty-js/qtty/qtty-ffi/units.csv +156 -0
  104. package/qtty-js/qtty/qtty-ffi/units.csv.md +3 -0
  105. package/qtty-js/qtty-node/.prettierignore +6 -0
  106. package/qtty-js/qtty-node/.prettierrc.json +6 -0
  107. package/qtty-js/qtty-node/README.md +250 -0
  108. package/qtty-js/qtty-node/c8.config.json +11 -0
  109. package/qtty-js/qtty-node/eslint.config.js +31 -0
  110. package/qtty-js/qtty-node/examples/arithmetic.mjs +64 -0
  111. package/qtty-js/qtty-node/examples/astronomy.mjs +90 -0
  112. package/qtty-js/qtty-node/examples/quickstart.mjs +36 -0
  113. package/qtty-js/qtty-node/examples/serialization.mjs +125 -0
  114. package/qtty-js/qtty-node/examples/unit_factories.mjs +74 -0
  115. package/qtty-js/qtty-node/index.d.ts +219 -0
  116. package/qtty-js/qtty-node/index.js +323 -0
  117. package/qtty-js/qtty-node/lib/DerivedQuantity.js +122 -0
  118. package/qtty-js/qtty-node/lib/Quantity.js +151 -0
  119. package/qtty-js/qtty-node/lib/backend.js +25 -0
  120. package/qtty-js/qtty-node/native.cjs +306 -0
  121. package/qtty-js/qtty-node/package-lock.json +3223 -0
  122. package/qtty-js/qtty-node/package.json +70 -0
  123. package/qtty-js/qtty-node/units.d.ts +299 -0
  124. package/qtty-js/qtty-node/units.js +210 -0
  125. package/qtty-js/qtty-web/Cargo.lock +767 -0
  126. package/qtty-js/qtty-web/Cargo.toml +21 -0
  127. package/qtty-js/qtty-web/index.d.ts +140 -0
  128. package/qtty-js/qtty-web/index.js +20 -0
  129. package/qtty-js/qtty-web/lib/DerivedQuantity.js +58 -0
  130. package/qtty-js/qtty-web/lib/Quantity.js +75 -0
  131. package/qtty-js/qtty-web/lib/backend.js +80 -0
  132. package/qtty-js/qtty-web/package.json +45 -0
  133. package/qtty-js/qtty-web/src/lib.rs +111 -0
  134. package/qtty-js/scripts/ci.sh +73 -0
  135. package/scripts/ci.sh +123 -0
  136. package/siderust-core/Cargo.lock +787 -0
  137. package/siderust-core/Cargo.toml +18 -0
  138. package/siderust-core/DEDUPLICATION.md +124 -0
  139. package/siderust-core/src/body.rs +120 -0
  140. package/siderust-core/src/events.rs +184 -0
  141. package/siderust-core/src/lib.rs +20 -0
  142. package/siderust-core/src/observer.rs +55 -0
  143. package/siderust-core/src/position.rs +213 -0
  144. package/siderust-node/.prettierignore +7 -0
  145. package/siderust-node/.prettierrc.json +6 -0
  146. package/siderust-node/Cargo.lock +906 -0
  147. package/siderust-node/Cargo.toml +29 -0
  148. package/siderust-node/README.md +109 -0
  149. package/siderust-node/__test__/index.test.mjs +248 -0
  150. package/siderust-node/build.rs +5 -0
  151. package/siderust-node/c8.config.json +3 -0
  152. package/siderust-node/eslint.config.js +31 -0
  153. package/siderust-node/examples/01_basic_coordinates.mjs +24 -0
  154. package/siderust-node/examples/02_coordinate_transformations.mjs +25 -0
  155. package/siderust-node/examples/03_all_frames_conversions.mjs +26 -0
  156. package/siderust-node/examples/04_all_center_conversions.mjs +24 -0
  157. package/siderust-node/examples/05_target_tracking.mjs +22 -0
  158. package/siderust-node/examples/06_night_events.mjs +18 -0
  159. package/siderust-node/examples/07_moon_properties.mjs +21 -0
  160. package/siderust-node/examples/08_solar_system.mjs +19 -0
  161. package/siderust-node/examples/09_star_observability.mjs +22 -0
  162. package/siderust-node/examples/10_time_periods.mjs +9 -0
  163. package/siderust-node/examples/11_serialization.mjs +31 -0
  164. package/siderust-node/examples/12_runtime_ephemeris.mjs +27 -0
  165. package/siderust-node/examples/13_coordinate_operations.mjs +20 -0
  166. package/siderust-node/index.d.ts +623 -0
  167. package/siderust-node/index.js +79 -0
  168. package/siderust-node/lib/Observer.js +112 -0
  169. package/siderust-node/lib/Star.js +118 -0
  170. package/siderust-node/lib/backend.js +63 -0
  171. package/siderust-node/lib/wrappers.js +566 -0
  172. package/siderust-node/main.js +20 -0
  173. package/siderust-node/native.cjs +360 -0
  174. package/siderust-node/package-lock.json +3261 -0
  175. package/siderust-node/package.json +71 -0
  176. package/siderust-node/src/body.rs +74 -0
  177. package/siderust-node/src/coordinates.rs +372 -0
  178. package/siderust-node/src/ephemeris.rs +462 -0
  179. package/siderust-node/src/events.rs +577 -0
  180. package/siderust-node/src/lib.rs +43 -0
  181. package/siderust-node/src/observer.rs +132 -0
  182. package/siderust-node/src/phase.rs +218 -0
  183. package/siderust-node/src/position.rs +292 -0
  184. package/siderust-node/src/star.rs +200 -0
  185. package/siderust-web/Cargo.lock +855 -0
  186. package/siderust-web/Cargo.toml +34 -0
  187. package/siderust-web/README.md +100 -0
  188. package/siderust-web/__test__/index.test.mjs +118 -0
  189. package/siderust-web/examples/github-pages/README.md +31 -0
  190. package/siderust-web/examples/github-pages/index.html +135 -0
  191. package/siderust-web/index.d.ts +311 -0
  192. package/siderust-web/index.js +66 -0
  193. package/siderust-web/lib/Observer.js +103 -0
  194. package/siderust-web/lib/Star.js +116 -0
  195. package/siderust-web/lib/backend.js +400 -0
  196. package/siderust-web/lib/wrappers.js +512 -0
  197. package/siderust-web/package.json +55 -0
  198. package/siderust-web/src/body.rs +69 -0
  199. package/siderust-web/src/coordinates.rs +302 -0
  200. package/siderust-web/src/ephemeris.rs +456 -0
  201. package/siderust-web/src/events.rs +520 -0
  202. package/siderust-web/src/lib.rs +51 -0
  203. package/siderust-web/src/observer.rs +117 -0
  204. package/siderust-web/src/phase.rs +190 -0
  205. package/siderust-web/src/position.rs +291 -0
  206. package/siderust-web/src/star.rs +178 -0
  207. package/tempoch-js/.github/workflows/ci.yml +142 -0
  208. package/tempoch-js/.gitmodules +3 -0
  209. package/tempoch-js/CHANGELOG.md +25 -0
  210. package/tempoch-js/LICENSE +661 -0
  211. package/tempoch-js/README.md +126 -0
  212. package/tempoch-js/package.json +20 -0
  213. package/tempoch-js/scripts/ci.sh +73 -0
  214. package/tempoch-js/tempoch/.github/workflows/ci.yml +113 -0
  215. package/tempoch-js/tempoch/CHANGELOG.md +82 -0
  216. package/tempoch-js/tempoch/Cargo.lock +947 -0
  217. package/tempoch-js/tempoch/Cargo.toml +3 -0
  218. package/tempoch-js/tempoch/LICENSE +661 -0
  219. package/tempoch-js/tempoch/README.md +76 -0
  220. package/tempoch-js/tempoch/tempoch/Cargo.toml +27 -0
  221. package/tempoch-js/tempoch/tempoch/examples/periods.rs +45 -0
  222. package/tempoch-js/tempoch/tempoch/examples/quickstart.rs +13 -0
  223. package/tempoch-js/tempoch/tempoch/src/lib.rs +49 -0
  224. package/tempoch-js/tempoch/tempoch/tests/integration.rs +57 -0
  225. package/tempoch-js/tempoch/tempoch-core/Cargo.toml +24 -0
  226. package/tempoch-js/tempoch/tempoch-core/src/delta_t.rs +345 -0
  227. package/tempoch-js/tempoch/tempoch-core/src/instant.rs +811 -0
  228. package/tempoch-js/tempoch/tempoch-core/src/julian_date_ext.rs +142 -0
  229. package/tempoch-js/tempoch/tempoch-core/src/lib.rs +81 -0
  230. package/tempoch-js/tempoch/tempoch-core/src/period.rs +1168 -0
  231. package/tempoch-js/tempoch/tempoch-core/src/scales.rs +779 -0
  232. package/tempoch-js/tempoch/tempoch-ffi/Cargo.lock +889 -0
  233. package/tempoch-js/tempoch/tempoch-ffi/Cargo.toml +26 -0
  234. package/tempoch-js/tempoch/tempoch-ffi/build.rs +24 -0
  235. package/tempoch-js/tempoch/tempoch-ffi/cbindgen.toml +30 -0
  236. package/tempoch-js/tempoch/tempoch-ffi/src/error.rs +19 -0
  237. package/tempoch-js/tempoch/tempoch-ffi/src/lib.rs +82 -0
  238. package/tempoch-js/tempoch/tempoch-ffi/src/period.rs +101 -0
  239. package/tempoch-js/tempoch/tempoch-ffi/src/time.rs +711 -0
  240. package/tempoch-js/tempoch/tempoch-ffi/tests/ffi.rs +265 -0
  241. package/tempoch-js/tempoch-node/.prettierignore +6 -0
  242. package/tempoch-js/tempoch-node/.prettierrc.json +6 -0
  243. package/tempoch-js/tempoch-node/Cargo.lock +496 -0
  244. package/tempoch-js/tempoch-node/Cargo.toml +29 -0
  245. package/tempoch-js/tempoch-node/README.md +265 -0
  246. package/tempoch-js/tempoch-node/__test__/index.test.mjs +598 -0
  247. package/tempoch-js/tempoch-node/build.rs +5 -0
  248. package/tempoch-js/tempoch-node/c8.config.json +3 -0
  249. package/tempoch-js/tempoch-node/eslint.config.js +31 -0
  250. package/tempoch-js/tempoch-node/examples/periods.mjs +79 -0
  251. package/tempoch-js/tempoch-node/examples/quickstart.mjs +71 -0
  252. package/tempoch-js/tempoch-node/examples/timescales.mjs +92 -0
  253. package/tempoch-js/tempoch-node/index.d.ts +280 -0
  254. package/tempoch-js/tempoch-node/index.js +32 -0
  255. package/tempoch-js/tempoch-node/lib/JulianDate.js +176 -0
  256. package/tempoch-js/tempoch-node/lib/ModifiedJulianDate.js +156 -0
  257. package/tempoch-js/tempoch-node/lib/Period.js +133 -0
  258. package/tempoch-js/tempoch-node/lib/backend.js +38 -0
  259. package/tempoch-js/tempoch-node/lib/qttyCompat.js +92 -0
  260. package/tempoch-js/tempoch-node/native.cjs +317 -0
  261. package/tempoch-js/tempoch-node/package-lock.json +3223 -0
  262. package/tempoch-js/tempoch-node/package.json +56 -0
  263. package/tempoch-js/tempoch-node/src/lib.rs +573 -0
  264. package/tempoch-js/tempoch-web/Cargo.toml +23 -0
  265. package/tempoch-js/tempoch-web/index.d.ts +95 -0
  266. package/tempoch-js/tempoch-web/index.js +27 -0
  267. package/tempoch-js/tempoch-web/lib/JulianDate.js +170 -0
  268. package/tempoch-js/tempoch-web/lib/ModifiedJulianDate.js +145 -0
  269. package/tempoch-js/tempoch-web/lib/Period.js +121 -0
  270. package/tempoch-js/tempoch-web/lib/backend.js +118 -0
  271. package/tempoch-js/tempoch-web/package.json +46 -0
  272. package/tempoch-js/tempoch-web/src/lib.rs +184 -0
@@ -0,0 +1,577 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Altitude and azimuth event queries — the core observation-planning API.
5
+ //!
6
+ //! All functions accept an `Observer`, a body name (or `Star` handle), and
7
+ //! a time window, returning arrays of events or periods in a single call.
8
+
9
+ use napi_derive::napi;
10
+
11
+ use crate::body::{dispatch_body, parse_body};
12
+ use crate::observer::JsObserver;
13
+ use crate::star::JsStar;
14
+
15
+ use qtty::*;
16
+ use siderust::calculus::altitude::{self, SearchOpts};
17
+ use siderust::calculus::azimuth;
18
+ use siderust::AltitudePeriodsProvider;
19
+ use siderust::AzimuthProvider;
20
+ use tempoch::{ModifiedJulianDate, Period, MJD};
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ // Result types (plain objects)
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ /// A threshold-crossing event (rise or set).
27
+ #[napi(object)]
28
+ pub struct CrossingEvent {
29
+ /// Time of the crossing (Modified Julian Date).
30
+ pub mjd: f64,
31
+ /// Direction: `"rising"` or `"setting"`.
32
+ pub direction: String,
33
+ }
34
+
35
+ /// A culmination event (local altitude extremum).
36
+ #[napi(object)]
37
+ pub struct CulminationEvent {
38
+ /// Time of the culmination (Modified Julian Date).
39
+ pub mjd: f64,
40
+ /// Altitude at the extremum in degrees.
41
+ pub altitude_deg: f64,
42
+ /// Kind: `"max"` (upper culmination) or `"min"` (lower culmination).
43
+ pub kind: String,
44
+ }
45
+
46
+ /// A time period (MJD interval).
47
+ #[napi(object)]
48
+ pub struct MjdPeriod {
49
+ /// Start of the period (Modified Julian Date).
50
+ pub start_mjd: f64,
51
+ /// End of the period (Modified Julian Date).
52
+ pub end_mjd: f64,
53
+ }
54
+
55
+ /// An azimuth-crossing event.
56
+ #[napi(object)]
57
+ pub struct AzimuthCrossingEvent {
58
+ /// Time of the event (Modified Julian Date).
59
+ pub mjd: f64,
60
+ /// Crossing direction: `"rising"` or `"setting"`.
61
+ pub direction: String,
62
+ }
63
+
64
+ /// An azimuth extremum (local max or min bearing).
65
+ #[napi(object)]
66
+ pub struct AzimuthExtremum {
67
+ /// Time of the extremum (Modified Julian Date).
68
+ pub mjd: f64,
69
+ /// Azimuth at the extremum in degrees.
70
+ pub azimuth_deg: f64,
71
+ /// Kind: `"max"` or `"min"`.
72
+ pub kind: String,
73
+ }
74
+
75
+ // ═══════════════════════════════════════════════════════════════════════════
76
+ // Helpers
77
+ // ═══════════════════════════════════════════════════════════════════════════
78
+
79
+ pub(crate) fn make_window(start_mjd: f64, end_mjd: f64) -> napi::Result<Period<MJD>> {
80
+ if !start_mjd.is_finite() || !end_mjd.is_finite() {
81
+ return Err(napi::Error::from_reason(
82
+ "Window bounds (startMjd, endMjd) must be finite",
83
+ ));
84
+ }
85
+ if start_mjd >= end_mjd {
86
+ return Err(napi::Error::from_reason(
87
+ "Window start must be before end (startMjd < endMjd)",
88
+ ));
89
+ }
90
+ Ok(Period::new(
91
+ ModifiedJulianDate::new(start_mjd),
92
+ ModifiedJulianDate::new(end_mjd),
93
+ ))
94
+ }
95
+
96
+ fn convert_crossings(events: Vec<altitude::CrossingEvent>) -> Vec<CrossingEvent> {
97
+ events
98
+ .into_iter()
99
+ .map(|e| CrossingEvent {
100
+ mjd: e.mjd.value(),
101
+ direction: match e.direction {
102
+ altitude::CrossingDirection::Rising => "rising".to_string(),
103
+ altitude::CrossingDirection::Setting => "setting".to_string(),
104
+ },
105
+ })
106
+ .collect()
107
+ }
108
+
109
+ fn convert_culminations(events: Vec<altitude::CulminationEvent>) -> Vec<CulminationEvent> {
110
+ events
111
+ .into_iter()
112
+ .map(|e| CulminationEvent {
113
+ mjd: e.mjd.value(),
114
+ altitude_deg: e.altitude.value(),
115
+ kind: match e.kind {
116
+ altitude::CulminationKind::Max => "max".to_string(),
117
+ altitude::CulminationKind::Min => "min".to_string(),
118
+ },
119
+ })
120
+ .collect()
121
+ }
122
+
123
+ fn convert_periods(periods: Vec<Period<MJD>>) -> Vec<MjdPeriod> {
124
+ periods
125
+ .into_iter()
126
+ .map(|p| MjdPeriod {
127
+ start_mjd: p.start.value(),
128
+ end_mjd: p.end.value(),
129
+ })
130
+ .collect()
131
+ }
132
+
133
+ fn convert_az_crossings(
134
+ events: Vec<siderust::AzimuthCrossingEvent>,
135
+ ) -> Vec<AzimuthCrossingEvent> {
136
+ events
137
+ .into_iter()
138
+ .map(|e| AzimuthCrossingEvent {
139
+ mjd: e.mjd.value(),
140
+ direction: match e.direction {
141
+ siderust::AzimuthCrossingDirection::Rising => "rising".to_string(),
142
+ siderust::AzimuthCrossingDirection::Setting => "setting".to_string(),
143
+ },
144
+ })
145
+ .collect()
146
+ }
147
+
148
+ fn convert_az_extrema(events: Vec<siderust::AzimuthExtremum>) -> Vec<AzimuthExtremum> {
149
+ events
150
+ .into_iter()
151
+ .map(|e| AzimuthExtremum {
152
+ mjd: e.mjd.value(),
153
+ azimuth_deg: e.azimuth.value(),
154
+ kind: match e.kind {
155
+ siderust::AzimuthExtremumKind::Max => "max".to_string(),
156
+ siderust::AzimuthExtremumKind::Min => "min".to_string(),
157
+ },
158
+ })
159
+ .collect()
160
+ }
161
+
162
+ // ═══════════════════════════════════════════════════════════════════════════
163
+ // Body altitude — instantaneous
164
+ // ═══════════════════════════════════════════════════════════════════════════
165
+
166
+ /// Compute the altitude of a solar-system body at a single instant.
167
+ ///
168
+ /// @param body — Body name (e.g. `"Sun"`, `"Moon"`, `"Mars"`).
169
+ /// @param observer — Observer location.
170
+ /// @param mjd — Modified Julian Date of the instant.
171
+ /// @returns Altitude in degrees.
172
+ ///
173
+ /// ```js
174
+ /// const { Observer, bodyAltitudeAt } = require('@siderust/siderust');
175
+ /// const obs = Observer.roqueDeLasMuchachos();
176
+ /// const alt = bodyAltitudeAt('Sun', obs, 60000.0);
177
+ /// ```
178
+ #[napi(js_name = "bodyAltitudeAt")]
179
+ pub fn body_altitude_at(body: String, observer: &JsObserver, mjd: f64) -> napi::Result<f64> {
180
+ if !mjd.is_finite() {
181
+ return Err(napi::Error::from_reason("mjd must be finite"));
182
+ }
183
+ let kind = parse_body(&body)?;
184
+ let m = ModifiedJulianDate::new(mjd);
185
+ let result: f64 = dispatch_body!(kind, |b| {
186
+ b.altitude_at(&observer.inner, m).to::<Degree>().value()
187
+ });
188
+ Ok(result)
189
+ }
190
+
191
+ /// Compute the azimuth of a solar-system body at a single instant.
192
+ ///
193
+ /// @returns Azimuth in degrees (north = 0°, east = 90°).
194
+ #[napi(js_name = "bodyAzimuthAt")]
195
+ pub fn body_azimuth_at(body: String, observer: &JsObserver, mjd: f64) -> napi::Result<f64> {
196
+ if !mjd.is_finite() {
197
+ return Err(napi::Error::from_reason("mjd must be finite"));
198
+ }
199
+ let kind = parse_body(&body)?;
200
+ let m = ModifiedJulianDate::new(mjd);
201
+ let result: f64 = dispatch_body!(kind, |b| {
202
+ b.azimuth_at(&observer.inner, m).to::<Degree>().value()
203
+ });
204
+ Ok(result)
205
+ }
206
+
207
+ // ═══════════════════════════════════════════════════════════════════════════
208
+ // Body altitude — batch events
209
+ // ═══════════════════════════════════════════════════════════════════════════
210
+
211
+ /// Find threshold-crossing events (rise/set) for a solar-system body.
212
+ ///
213
+ /// @param body — Body name.
214
+ /// @param observer — Observer location.
215
+ /// @param startMjd — Window start (MJD).
216
+ /// @param endMjd — Window end (MJD).
217
+ /// @param thresholdDeg — Altitude threshold in degrees (e.g. 0 for horizon).
218
+ /// @returns Array of crossing events `{ mjd, direction }`.
219
+ #[napi(js_name = "bodyCrossings")]
220
+ pub fn body_crossings(
221
+ body: String,
222
+ observer: &JsObserver,
223
+ start_mjd: f64,
224
+ end_mjd: f64,
225
+ threshold_deg: f64,
226
+ ) -> napi::Result<Vec<CrossingEvent>> {
227
+ let kind = parse_body(&body)?;
228
+ let window = make_window(start_mjd, end_mjd)?;
229
+ let thr = Degrees::new(threshold_deg);
230
+ let opts = SearchOpts::default();
231
+ let result = dispatch_body!(kind, |b| {
232
+ convert_crossings(altitude::crossings(&b, &observer.inner, window, thr, opts))
233
+ });
234
+ Ok(result)
235
+ }
236
+
237
+ /// Find culmination events (altitude local extrema) for a solar-system body.
238
+ ///
239
+ /// @returns Array of culmination events `{ mjd, altitudeDeg, kind }`.
240
+ #[napi(js_name = "bodyCulminations")]
241
+ pub fn body_culminations(
242
+ body: String,
243
+ observer: &JsObserver,
244
+ start_mjd: f64,
245
+ end_mjd: f64,
246
+ ) -> napi::Result<Vec<CulminationEvent>> {
247
+ let kind = parse_body(&body)?;
248
+ let window = make_window(start_mjd, end_mjd)?;
249
+ let opts = SearchOpts::default();
250
+ let result = dispatch_body!(kind, |b| {
251
+ convert_culminations(altitude::culminations(&b, &observer.inner, window, opts))
252
+ });
253
+ Ok(result)
254
+ }
255
+
256
+ /// Find periods where a body's altitude is above a threshold.
257
+ ///
258
+ /// @returns Array of MJD periods `{ startMjd, endMjd }`.
259
+ #[napi(js_name = "bodyAboveThreshold")]
260
+ pub fn body_above_threshold(
261
+ body: String,
262
+ observer: &JsObserver,
263
+ start_mjd: f64,
264
+ end_mjd: f64,
265
+ threshold_deg: f64,
266
+ ) -> napi::Result<Vec<MjdPeriod>> {
267
+ let kind = parse_body(&body)?;
268
+ let window = make_window(start_mjd, end_mjd)?;
269
+ let thr = Degrees::new(threshold_deg);
270
+ let opts = SearchOpts::default();
271
+ let result = dispatch_body!(kind, |b| {
272
+ convert_periods(altitude::above_threshold(
273
+ &b,
274
+ &observer.inner,
275
+ window,
276
+ thr,
277
+ opts,
278
+ ))
279
+ });
280
+ Ok(result)
281
+ }
282
+
283
+ /// Find periods where a body's altitude is below a threshold.
284
+ ///
285
+ /// @returns Array of MJD periods `{ startMjd, endMjd }`.
286
+ #[napi(js_name = "bodyBelowThreshold")]
287
+ pub fn body_below_threshold(
288
+ body: String,
289
+ observer: &JsObserver,
290
+ start_mjd: f64,
291
+ end_mjd: f64,
292
+ threshold_deg: f64,
293
+ ) -> napi::Result<Vec<MjdPeriod>> {
294
+ let kind = parse_body(&body)?;
295
+ let window = make_window(start_mjd, end_mjd)?;
296
+ let thr = Degrees::new(threshold_deg);
297
+ let opts = SearchOpts::default();
298
+ let result = dispatch_body!(kind, |b| {
299
+ convert_periods(altitude::below_threshold(
300
+ &b,
301
+ &observer.inner,
302
+ window,
303
+ thr,
304
+ opts,
305
+ ))
306
+ });
307
+ Ok(result)
308
+ }
309
+
310
+ /// Find azimuth-crossing events for a body.
311
+ ///
312
+ /// @param bearingDeg — Target azimuth bearing in degrees.
313
+ /// @returns Array of azimuth crossing events `{ mjd, direction }`.
314
+ #[napi(js_name = "bodyAzimuthCrossings")]
315
+ pub fn body_azimuth_crossings(
316
+ body: String,
317
+ observer: &JsObserver,
318
+ start_mjd: f64,
319
+ end_mjd: f64,
320
+ bearing_deg: f64,
321
+ ) -> napi::Result<Vec<AzimuthCrossingEvent>> {
322
+ let kind = parse_body(&body)?;
323
+ let window = make_window(start_mjd, end_mjd)?;
324
+ let bearing = Degrees::new(bearing_deg);
325
+ let opts = SearchOpts::default();
326
+ let result = dispatch_body!(kind, |b| {
327
+ convert_az_crossings(azimuth::azimuth_crossings(
328
+ &b,
329
+ &observer.inner,
330
+ window,
331
+ bearing,
332
+ opts,
333
+ ))
334
+ });
335
+ Ok(result)
336
+ }
337
+
338
+ /// Find azimuth extrema (max/min bearing) for a body.
339
+ ///
340
+ /// @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`.
341
+ #[napi(js_name = "bodyAzimuthExtrema")]
342
+ pub fn body_azimuth_extrema(
343
+ body: String,
344
+ observer: &JsObserver,
345
+ start_mjd: f64,
346
+ end_mjd: f64,
347
+ ) -> napi::Result<Vec<AzimuthExtremum>> {
348
+ let kind = parse_body(&body)?;
349
+ let window = make_window(start_mjd, end_mjd)?;
350
+ let opts = SearchOpts::default();
351
+ let result = dispatch_body!(kind, |b| {
352
+ convert_az_extrema(azimuth::azimuth_extrema(
353
+ &b,
354
+ &observer.inner,
355
+ window,
356
+ opts,
357
+ ))
358
+ });
359
+ Ok(result)
360
+ }
361
+
362
+ // ═══════════════════════════════════════════════════════════════════════════
363
+ // Star altitude — instantaneous
364
+ // ═══════════════════════════════════════════════════════════════════════════
365
+
366
+ /// Compute the altitude of a catalog or custom star at a single instant.
367
+ ///
368
+ /// @returns Altitude in degrees.
369
+ #[napi(js_name = "starAltitudeAt")]
370
+ pub fn star_altitude_at(star: &JsStar, observer: &JsObserver, mjd: f64) -> napi::Result<f64> {
371
+ if !mjd.is_finite() {
372
+ return Err(napi::Error::from_reason("mjd must be finite"));
373
+ }
374
+ let m = ModifiedJulianDate::new(mjd);
375
+ Ok(star
376
+ .inner
377
+ .altitude_at(&observer.inner, m)
378
+ .to::<Degree>()
379
+ .value())
380
+ }
381
+
382
+ /// Compute the azimuth of a star at a single instant.
383
+ ///
384
+ /// @returns Azimuth in degrees.
385
+ #[napi(js_name = "starAzimuthAt")]
386
+ pub fn star_azimuth_at(star: &JsStar, observer: &JsObserver, mjd: f64) -> napi::Result<f64> {
387
+ if !mjd.is_finite() {
388
+ return Err(napi::Error::from_reason("mjd must be finite"));
389
+ }
390
+ let m = ModifiedJulianDate::new(mjd);
391
+ Ok(star
392
+ .inner
393
+ .azimuth_at(&observer.inner, m)
394
+ .to::<Degree>()
395
+ .value())
396
+ }
397
+
398
+ // ═══════════════════════════════════════════════════════════════════════════
399
+ // Star altitude — batch events
400
+ // ═══════════════════════════════════════════════════════════════════════════
401
+
402
+ /// Find threshold-crossing events for a star.
403
+ #[napi(js_name = "starCrossings")]
404
+ pub fn star_crossings(
405
+ star: &JsStar,
406
+ observer: &JsObserver,
407
+ start_mjd: f64,
408
+ end_mjd: f64,
409
+ threshold_deg: f64,
410
+ ) -> napi::Result<Vec<CrossingEvent>> {
411
+ let window = make_window(start_mjd, end_mjd)?;
412
+ let thr = Degrees::new(threshold_deg);
413
+ let opts = SearchOpts::default();
414
+ Ok(convert_crossings(altitude::crossings(
415
+ &star.inner,
416
+ &observer.inner,
417
+ window,
418
+ thr,
419
+ opts,
420
+ )))
421
+ }
422
+
423
+ /// Find culmination events for a star.
424
+ #[napi(js_name = "starCulminations")]
425
+ pub fn star_culminations(
426
+ star: &JsStar,
427
+ observer: &JsObserver,
428
+ start_mjd: f64,
429
+ end_mjd: f64,
430
+ ) -> napi::Result<Vec<CulminationEvent>> {
431
+ let window = make_window(start_mjd, end_mjd)?;
432
+ let opts = SearchOpts::default();
433
+ Ok(convert_culminations(altitude::culminations(
434
+ &star.inner,
435
+ &observer.inner,
436
+ window,
437
+ opts,
438
+ )))
439
+ }
440
+
441
+ /// Find periods where a star's altitude is above a threshold.
442
+ #[napi(js_name = "starAboveThreshold")]
443
+ pub fn star_above_threshold(
444
+ star: &JsStar,
445
+ observer: &JsObserver,
446
+ start_mjd: f64,
447
+ end_mjd: f64,
448
+ threshold_deg: f64,
449
+ ) -> napi::Result<Vec<MjdPeriod>> {
450
+ let window = make_window(start_mjd, end_mjd)?;
451
+ let thr = Degrees::new(threshold_deg);
452
+ let opts = SearchOpts::default();
453
+ Ok(convert_periods(altitude::above_threshold(
454
+ &star.inner,
455
+ &observer.inner,
456
+ window,
457
+ thr,
458
+ opts,
459
+ )))
460
+ }
461
+
462
+ /// Find periods where a star's altitude is below a threshold.
463
+ #[napi(js_name = "starBelowThreshold")]
464
+ pub fn star_below_threshold(
465
+ star: &JsStar,
466
+ observer: &JsObserver,
467
+ start_mjd: f64,
468
+ end_mjd: f64,
469
+ threshold_deg: f64,
470
+ ) -> napi::Result<Vec<MjdPeriod>> {
471
+ let window = make_window(start_mjd, end_mjd)?;
472
+ let thr = Degrees::new(threshold_deg);
473
+ let opts = SearchOpts::default();
474
+ Ok(convert_periods(altitude::below_threshold(
475
+ &star.inner,
476
+ &observer.inner,
477
+ window,
478
+ thr,
479
+ opts,
480
+ )))
481
+ }
482
+
483
+ // ═══════════════════════════════════════════════════════════════════════════
484
+ // Star azimuth — batch events
485
+ // ═══════════════════════════════════════════════════════════════════════════
486
+
487
+ /// Find azimuth-crossing events for a star.
488
+ ///
489
+ /// @param bearingDeg — Target azimuth bearing in degrees.
490
+ /// @returns Array of azimuth crossing events `{ mjd, direction }`.
491
+ #[napi(js_name = "starAzimuthCrossings")]
492
+ pub fn star_azimuth_crossings(
493
+ star: &JsStar,
494
+ observer: &JsObserver,
495
+ start_mjd: f64,
496
+ end_mjd: f64,
497
+ bearing_deg: f64,
498
+ ) -> napi::Result<Vec<AzimuthCrossingEvent>> {
499
+ let window = make_window(start_mjd, end_mjd)?;
500
+ let bearing = Degrees::new(bearing_deg);
501
+ let opts = SearchOpts::default();
502
+ Ok(convert_az_crossings(azimuth::azimuth_crossings(
503
+ &star.inner,
504
+ &observer.inner,
505
+ window,
506
+ bearing,
507
+ opts,
508
+ )))
509
+ }
510
+
511
+ /// Find azimuth extrema (max/min bearing) for a star.
512
+ ///
513
+ /// @returns Array of azimuth extrema `{ mjd, azimuthDeg, kind }`.
514
+ #[napi(js_name = "starAzimuthExtrema")]
515
+ pub fn star_azimuth_extrema(
516
+ star: &JsStar,
517
+ observer: &JsObserver,
518
+ start_mjd: f64,
519
+ end_mjd: f64,
520
+ ) -> napi::Result<Vec<AzimuthExtremum>> {
521
+ let window = make_window(start_mjd, end_mjd)?;
522
+ let opts = SearchOpts::default();
523
+ Ok(convert_az_extrema(azimuth::azimuth_extrema(
524
+ &star.inner,
525
+ &observer.inner,
526
+ window,
527
+ opts,
528
+ )))
529
+ }
530
+
531
+ // ═══════════════════════════════════════════════════════════════════════════
532
+ // Period utilities
533
+ // ═══════════════════════════════════════════════════════════════════════════
534
+
535
+ /// Intersect two lists of MJD periods, returning only overlapping intervals.
536
+ ///
537
+ /// This is useful for combining altitude and azimuth constraints, or
538
+ /// combining target visibility with astronomical night periods.
539
+ ///
540
+ /// @param periods1 — First list of MJD periods.
541
+ /// @param periods2 — Second list of MJD periods.
542
+ /// @returns Array of MJD periods representing the intersection.
543
+ ///
544
+ /// ```js
545
+ /// const altPeriods = starAboveThreshold(star, obs, mjd0, mjd1, 25);
546
+ /// const azPeriods = bodyAboveThreshold('Sun', obs, mjd0, mjd1, -18); // dark sky
547
+ /// const observable = intersectPeriods(altPeriods, azPeriods);
548
+ /// ```
549
+ #[napi(js_name = "intersectPeriods")]
550
+ pub fn intersect_periods_js(
551
+ periods1: Vec<MjdPeriod>,
552
+ periods2: Vec<MjdPeriod>,
553
+ ) -> Vec<MjdPeriod> {
554
+ let p1: Vec<Period<MJD>> = periods1
555
+ .iter()
556
+ .map(|p| Period::new(
557
+ ModifiedJulianDate::new(p.start_mjd),
558
+ ModifiedJulianDate::new(p.end_mjd),
559
+ ))
560
+ .collect();
561
+ let p2: Vec<Period<MJD>> = periods2
562
+ .iter()
563
+ .map(|p| Period::new(
564
+ ModifiedJulianDate::new(p.start_mjd),
565
+ ModifiedJulianDate::new(p.end_mjd),
566
+ ))
567
+ .collect();
568
+
569
+ let result = tempoch::intersect_periods(&p1, &p2);
570
+ result
571
+ .into_iter()
572
+ .map(|p| MjdPeriod {
573
+ start_mjd: p.start.value(),
574
+ end_mjd: p.end.value(),
575
+ })
576
+ .collect()
577
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Node.js native bindings for **siderust** — high-precision astronomy.
5
+ //!
6
+ //! This crate uses `napi-rs` to expose siderust's core workflows as a
7
+ //! JavaScript/TypeScript-friendly API. All numerical computation stays in
8
+ //! Rust; the JS layer only sees high-level domain objects.
9
+ //!
10
+ //! ## Design principles
11
+ //!
12
+ //! * **Batch over chatty** — altitude/azimuth queries return arrays of events
13
+ //! in a single crossing, avoiding per-value FFI overhead.
14
+ //! * **Domain objects** — `Observer`, `Star`, `Body`, `CartesianPosition`, etc.
15
+ //! rather than raw numeric tuples.
16
+ //! * **Reuse conventions** — camelCase naming, `napi::Error` for exceptions,
17
+ //! MJD/JD as `f64` — consistent with `@siderust/tempoch` and `@siderust/qtty`.
18
+
19
+ #![deny(clippy::all)]
20
+
21
+ mod body;
22
+ mod coordinates;
23
+ mod ephemeris;
24
+ mod events;
25
+ mod observer;
26
+ mod phase;
27
+ mod position;
28
+ mod star;
29
+
30
+ pub use body::*;
31
+ pub use coordinates::*;
32
+ pub use ephemeris::*;
33
+ pub use events::*;
34
+ pub use observer::*;
35
+ pub use phase::*;
36
+ pub use position::*;
37
+ pub use star::*;
38
+
39
+ /// Return the siderust-node version string.
40
+ #[napi_derive::napi(js_name = "version")]
41
+ pub fn version() -> String {
42
+ env!("CARGO_PKG_VERSION").to_string()
43
+ }