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,132 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Observer (geodetic site) for topocentric computations.
5
+
6
+ use napi_derive::napi;
7
+
8
+ use qtty::*;
9
+ use siderust::coordinates::centers::Geodetic;
10
+ use siderust::coordinates::frames::ECEF;
11
+ use siderust::observatories;
12
+
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+ // Observer class
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ /// A geodetic observer location on the Earth's surface (WGS84 ellipsoid).
18
+ ///
19
+ /// Used as the reference site for all topocentric computations: altitude,
20
+ /// azimuth, crossings, culminations, and coordinate transforms to the
21
+ /// Horizontal frame.
22
+ ///
23
+ /// ```js
24
+ /// const { Observer } = require('@siderust/siderust');
25
+ ///
26
+ /// // Custom site
27
+ /// const obs = new Observer(-17.8925, 28.7543, 2396);
28
+ ///
29
+ /// // Preset observatory
30
+ /// const orm = Observer.roqueDeLasMuchachos();
31
+ /// ```
32
+ #[napi(js_name = "Observer")]
33
+ pub struct JsObserver {
34
+ pub(crate) inner: Geodetic<ECEF>,
35
+ }
36
+
37
+ #[napi]
38
+ impl JsObserver {
39
+ /// Create an observer at a geodetic position.
40
+ ///
41
+ /// @param lonDeg — Longitude in degrees (east positive).
42
+ /// @param latDeg — Latitude in degrees (north positive).
43
+ /// @param heightM — Height above WGS84 ellipsoid in metres.
44
+ #[napi(constructor)]
45
+ pub fn new(lon_deg: f64, lat_deg: f64, height_m: f64) -> napi::Result<Self> {
46
+ if !lon_deg.is_finite() || !lat_deg.is_finite() || !height_m.is_finite() {
47
+ return Err(napi::Error::from_reason(
48
+ "Observer coordinates must be finite (not NaN or ±infinity)",
49
+ ));
50
+ }
51
+ Ok(Self {
52
+ inner: Geodetic::<ECEF>::new(
53
+ Degrees::new(lon_deg),
54
+ Degrees::new(lat_deg),
55
+ Meters::new(height_m),
56
+ ),
57
+ })
58
+ }
59
+
60
+ // ── preset observatories ─────────────────────────────────────────
61
+
62
+ /// Roque de los Muchachos Observatory (La Palma, Spain).
63
+ ///
64
+ /// Longitude −17.8925°, Latitude +28.7543°, Altitude 2396 m.
65
+ #[napi(factory, js_name = "roqueDeLasMuchachos")]
66
+ pub fn roque_de_las_muchachos() -> Self {
67
+ Self {
68
+ inner: observatories::ROQUE_DE_LOS_MUCHACHOS,
69
+ }
70
+ }
71
+
72
+ /// Paranal Observatory (ESO, Chile).
73
+ ///
74
+ /// Longitude −70.4043°, Latitude −24.6272°, Altitude 2635 m.
75
+ #[napi(factory, js_name = "elParanal")]
76
+ pub fn el_paranal() -> Self {
77
+ Self {
78
+ inner: observatories::EL_PARANAL,
79
+ }
80
+ }
81
+
82
+ /// Mauna Kea Observatory (Hawaiʻi, USA).
83
+ ///
84
+ /// Longitude −155.4681°, Latitude +19.8207°, Altitude 4207 m.
85
+ #[napi(factory, js_name = "maunaKea")]
86
+ pub fn mauna_kea() -> Self {
87
+ Self {
88
+ inner: observatories::MAUNA_KEA,
89
+ }
90
+ }
91
+
92
+ /// La Silla Observatory (ESO, Chile).
93
+ ///
94
+ /// Longitude −70.7346°, Latitude −29.2584°, Altitude 2400 m.
95
+ #[napi(factory, js_name = "laSilla")]
96
+ pub fn la_silla() -> Self {
97
+ Self {
98
+ inner: observatories::LA_SILLA_OBSERVATORY,
99
+ }
100
+ }
101
+
102
+ // ── accessors ────────────────────────────────────────────────────
103
+
104
+ /// Longitude in degrees (east positive).
105
+ #[napi(getter, js_name = "lonDeg")]
106
+ pub fn lon_deg(&self) -> f64 {
107
+ self.inner.lon.value()
108
+ }
109
+
110
+ /// Latitude in degrees (north positive).
111
+ #[napi(getter, js_name = "latDeg")]
112
+ pub fn lat_deg(&self) -> f64 {
113
+ self.inner.lat.value()
114
+ }
115
+
116
+ /// Height above the WGS84 ellipsoid in metres.
117
+ #[napi(getter, js_name = "heightM")]
118
+ pub fn height_m(&self) -> f64 {
119
+ self.inner.height.value()
120
+ }
121
+
122
+ /// Human-readable string representation.
123
+ #[napi]
124
+ pub fn format(&self) -> String {
125
+ format!(
126
+ "Observer(lon={:.4}°, lat={:.4}°, h={:.1} m)",
127
+ self.inner.lon.value(),
128
+ self.inner.lat.value(),
129
+ self.inner.height.value(),
130
+ )
131
+ }
132
+ }
@@ -0,0 +1,218 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Lunar phase queries — geometry, events, and illumination periods.
5
+
6
+ use napi_derive::napi;
7
+
8
+ use crate::events::{make_window, MjdPeriod};
9
+ use crate::observer::JsObserver;
10
+
11
+ use qtty::*;
12
+ use siderust::calculus::ephemeris::Vsop87Ephemeris;
13
+ use siderust::calculus::lunar::phase::{
14
+ find_phase_events, illumination_above, illumination_below, illumination_range,
15
+ moon_phase_geocentric, moon_phase_topocentric, MoonPhaseLabel, PhaseSearchOpts,
16
+ };
17
+ use siderust::time::JulianDate;
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // Result types
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ /// Moon phase geometry at a single instant.
24
+ #[napi(object)]
25
+ pub struct MoonPhase {
26
+ /// Phase angle in degrees (Sun-Moon-Earth/observer angle).
27
+ pub phase_angle_deg: f64,
28
+ /// Fraction of the disk illuminated [0, 1].
29
+ pub illuminated_fraction: f64,
30
+ /// Elongation in degrees (eastward from the Sun).
31
+ pub elongation_deg: f64,
32
+ /// Whether the Moon is waxing.
33
+ pub waxing: bool,
34
+ /// Named phase label (e.g. `"FullMoon"`, `"WaxingCrescent"`).
35
+ pub label: String,
36
+ }
37
+
38
+ /// A principal lunar phase event (New Moon, First Quarter, Full Moon, Last Quarter).
39
+ #[napi(object)]
40
+ pub struct PhaseEvent {
41
+ /// Time of the event (Modified Julian Date).
42
+ pub mjd: f64,
43
+ /// Phase kind: `"NewMoon"`, `"FirstQuarter"`, `"FullMoon"`, `"LastQuarter"`.
44
+ pub kind: String,
45
+ }
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ // Phase geometry
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ /// Compute geocentric Moon phase geometry at a Julian Date.
52
+ ///
53
+ /// @param jd — Julian Date.
54
+ /// @returns Phase geometry including illumination, elongation, and named label.
55
+ ///
56
+ /// ```js
57
+ /// const { moonPhase } = require('@siderust/siderust');
58
+ /// const phase = moonPhase(2451545.0);
59
+ /// console.log(phase.label, phase.illuminatedFraction);
60
+ /// ```
61
+ #[napi(js_name = "moonPhase")]
62
+ pub fn moon_phase(jd: f64) -> napi::Result<MoonPhase> {
63
+ if !jd.is_finite() {
64
+ return Err(napi::Error::from_reason("jd must be finite"));
65
+ }
66
+ let geom = moon_phase_geocentric::<Vsop87Ephemeris>(JulianDate::new(jd));
67
+ let label = geom.label();
68
+ Ok(MoonPhase {
69
+ phase_angle_deg: geom.phase_angle.to::<Degree>().value(),
70
+ illuminated_fraction: geom.illuminated_fraction,
71
+ elongation_deg: geom.elongation.to::<Degree>().value(),
72
+ waxing: geom.waxing,
73
+ label: label_to_string(label),
74
+ })
75
+ }
76
+
77
+ /// Compute topocentric Moon phase geometry at a Julian Date for an observer.
78
+ ///
79
+ /// @param jd — Julian Date.
80
+ /// @param observer — Observer location on Earth.
81
+ #[napi(js_name = "moonPhaseTopocentric")]
82
+ pub fn moon_phase_topo(jd: f64, observer: &JsObserver) -> napi::Result<MoonPhase> {
83
+ if !jd.is_finite() {
84
+ return Err(napi::Error::from_reason("jd must be finite"));
85
+ }
86
+ let geom =
87
+ moon_phase_topocentric::<Vsop87Ephemeris>(JulianDate::new(jd), observer.inner.clone());
88
+ let label = geom.label();
89
+ Ok(MoonPhase {
90
+ phase_angle_deg: geom.phase_angle.to::<Degree>().value(),
91
+ illuminated_fraction: geom.illuminated_fraction,
92
+ elongation_deg: geom.elongation.to::<Degree>().value(),
93
+ waxing: geom.waxing,
94
+ label: label_to_string(label),
95
+ })
96
+ }
97
+
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+ // Phase event finding
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+
102
+ /// Find all principal lunar phase events in a time window.
103
+ ///
104
+ /// @param startMjd — Window start (MJD).
105
+ /// @param endMjd — Window end (MJD).
106
+ /// @returns Sorted array of `{ mjd, kind }` events.
107
+ ///
108
+ /// ```js
109
+ /// const { findPhaseEvents } = require('@siderust/siderust');
110
+ /// const events = findPhaseEvents(60000.0, 60030.0); // ~1 month
111
+ /// events.forEach(e => console.log(e.kind, e.mjd));
112
+ /// ```
113
+ #[napi(js_name = "findPhaseEvents")]
114
+ pub fn find_phase_events_js(start_mjd: f64, end_mjd: f64) -> napi::Result<Vec<PhaseEvent>> {
115
+ let window = make_window(start_mjd, end_mjd)?;
116
+ let events = find_phase_events::<Vsop87Ephemeris>(window, PhaseSearchOpts::default());
117
+ Ok(events
118
+ .into_iter()
119
+ .map(|e| PhaseEvent {
120
+ mjd: e.mjd.value(),
121
+ kind: phase_kind_to_string(e.kind),
122
+ })
123
+ .collect())
124
+ }
125
+
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // Illumination periods
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ /// Find periods where geocentric Moon illumination is above `kMin` ∈ [0, 1].
131
+ ///
132
+ /// @param startMjd — Window start (MJD).
133
+ /// @param endMjd — Window end (MJD).
134
+ /// @param kMin — Minimum illumination fraction.
135
+ /// @returns Array of MJD periods.
136
+ #[napi(js_name = "moonIlluminationAbove")]
137
+ pub fn moon_illumination_above(
138
+ start_mjd: f64,
139
+ end_mjd: f64,
140
+ k_min: f64,
141
+ ) -> napi::Result<Vec<MjdPeriod>> {
142
+ let window = make_window(start_mjd, end_mjd)?;
143
+ let periods = illumination_above::<Vsop87Ephemeris>(window, k_min, PhaseSearchOpts::default());
144
+ Ok(periods
145
+ .into_iter()
146
+ .map(|p| MjdPeriod {
147
+ start_mjd: p.start.value(),
148
+ end_mjd: p.end.value(),
149
+ })
150
+ .collect())
151
+ }
152
+
153
+ /// Find periods where geocentric Moon illumination is below `kMax` ∈ [0, 1].
154
+ #[napi(js_name = "moonIlluminationBelow")]
155
+ pub fn moon_illumination_below(
156
+ start_mjd: f64,
157
+ end_mjd: f64,
158
+ k_max: f64,
159
+ ) -> napi::Result<Vec<MjdPeriod>> {
160
+ let window = make_window(start_mjd, end_mjd)?;
161
+ let periods = illumination_below::<Vsop87Ephemeris>(window, k_max, PhaseSearchOpts::default());
162
+ Ok(periods
163
+ .into_iter()
164
+ .map(|p| MjdPeriod {
165
+ start_mjd: p.start.value(),
166
+ end_mjd: p.end.value(),
167
+ })
168
+ .collect())
169
+ }
170
+
171
+ /// Find periods where geocentric Moon illumination is in `[kMin, kMax]`.
172
+ #[napi(js_name = "moonIlluminationRange")]
173
+ pub fn moon_illumination_range(
174
+ start_mjd: f64,
175
+ end_mjd: f64,
176
+ k_min: f64,
177
+ k_max: f64,
178
+ ) -> napi::Result<Vec<MjdPeriod>> {
179
+ let window = make_window(start_mjd, end_mjd)?;
180
+ let periods =
181
+ illumination_range::<Vsop87Ephemeris>(window, k_min, k_max, PhaseSearchOpts::default());
182
+ Ok(periods
183
+ .into_iter()
184
+ .map(|p| MjdPeriod {
185
+ start_mjd: p.start.value(),
186
+ end_mjd: p.end.value(),
187
+ })
188
+ .collect())
189
+ }
190
+
191
+ // ─────────────────────────────────────────────────────────────────────────────
192
+ // Helpers
193
+ // ─────────────────────────────────────────────────────────────────────────────
194
+
195
+ fn label_to_string(label: MoonPhaseLabel) -> String {
196
+ match label {
197
+ MoonPhaseLabel::NewMoon => "NewMoon",
198
+ MoonPhaseLabel::WaxingCrescent => "WaxingCrescent",
199
+ MoonPhaseLabel::FirstQuarter => "FirstQuarter",
200
+ MoonPhaseLabel::WaxingGibbous => "WaxingGibbous",
201
+ MoonPhaseLabel::FullMoon => "FullMoon",
202
+ MoonPhaseLabel::WaningGibbous => "WaningGibbous",
203
+ MoonPhaseLabel::LastQuarter => "LastQuarter",
204
+ MoonPhaseLabel::WaningCrescent => "WaningCrescent",
205
+ }
206
+ .to_string()
207
+ }
208
+
209
+ fn phase_kind_to_string(kind: siderust::calculus::lunar::phase::PhaseKind) -> String {
210
+ use siderust::calculus::lunar::phase::PhaseKind;
211
+ match kind {
212
+ PhaseKind::NewMoon => "NewMoon",
213
+ PhaseKind::FirstQuarter => "FirstQuarter",
214
+ PhaseKind::FullMoon => "FullMoon",
215
+ PhaseKind::LastQuarter => "LastQuarter",
216
+ }
217
+ .to_string()
218
+ }
@@ -0,0 +1,292 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Vallés Puig, Ramon
3
+
4
+ //! Cartesian position type for Node.js bindings.
5
+
6
+ use napi_derive::napi;
7
+
8
+ // Frame and center string constants
9
+ pub const FRAME_ECL: &str = "EclipticMeanJ2000";
10
+ pub const FRAME_EQ: &str = "EquatorialMeanJ2000";
11
+ pub const FRAME_ICRS: &str = "ICRS";
12
+
13
+ pub const CENTER_BARY: &str = "Barycentric";
14
+ pub const CENTER_HELIO: &str = "Heliocentric";
15
+ pub const CENTER_GEO: &str = "Geocentric";
16
+
17
+ pub const UNIT_AU: &str = "au";
18
+ pub const UNIT_KM: &str = "km";
19
+
20
+ /// A 3D cartesian position with frame, center, and unit metadata.
21
+ ///
22
+ /// This mirrors the Python `Position` type for API parity.
23
+ #[napi]
24
+ #[derive(Clone)]
25
+ pub struct Position {
26
+ x: f64,
27
+ y: f64,
28
+ z: f64,
29
+ frame: String,
30
+ center: String,
31
+ unit: String,
32
+ }
33
+
34
+ #[napi]
35
+ impl Position {
36
+ /// Create a new position with explicit coordinates, frame, center, and unit.
37
+ #[napi(constructor)]
38
+ pub fn new(
39
+ x: f64,
40
+ y: f64,
41
+ z: f64,
42
+ frame: String,
43
+ center: String,
44
+ unit: String,
45
+ ) -> napi::Result<Self> {
46
+ validate_frame(&frame)?;
47
+ validate_center(&center)?;
48
+ validate_unit(&unit)?;
49
+ Ok(Self { x, y, z, frame, center, unit })
50
+ }
51
+
52
+ /// X coordinate.
53
+ #[napi(getter)]
54
+ pub fn x(&self) -> f64 {
55
+ self.x
56
+ }
57
+
58
+ /// Y coordinate.
59
+ #[napi(getter)]
60
+ pub fn y(&self) -> f64 {
61
+ self.y
62
+ }
63
+
64
+ /// Z coordinate.
65
+ #[napi(getter)]
66
+ pub fn z(&self) -> f64 {
67
+ self.z
68
+ }
69
+
70
+ /// Reference frame (e.g., "EclipticMeanJ2000", "ICRS").
71
+ #[napi(getter)]
72
+ pub fn frame(&self) -> String {
73
+ self.frame.clone()
74
+ }
75
+
76
+ /// Coordinate center (e.g., "Barycentric", "Heliocentric", "Geocentric").
77
+ #[napi(getter)]
78
+ pub fn center(&self) -> String {
79
+ self.center.clone()
80
+ }
81
+
82
+ /// Distance unit ("au" or "km").
83
+ #[napi(getter)]
84
+ pub fn unit(&self) -> String {
85
+ self.unit.clone()
86
+ }
87
+
88
+ /// Euclidean distance from origin.
89
+ #[napi]
90
+ pub fn magnitude(&self) -> f64 {
91
+ (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
92
+ }
93
+
94
+ /// Subtract another position to get a displacement vector.
95
+ #[napi]
96
+ pub fn subtract(&self, other: &Position) -> napi::Result<Displacement> {
97
+ if self.frame != other.frame {
98
+ return Err(napi::Error::from_reason(format!(
99
+ "Cannot subtract positions in different frames: '{}' vs '{}'",
100
+ self.frame, other.frame
101
+ )));
102
+ }
103
+ if self.center != other.center {
104
+ return Err(napi::Error::from_reason(format!(
105
+ "Cannot subtract positions with different centers: '{}' vs '{}'",
106
+ self.center, other.center
107
+ )));
108
+ }
109
+ if self.unit != other.unit {
110
+ return Err(napi::Error::from_reason(format!(
111
+ "Cannot subtract positions with different units: '{}' vs '{}'",
112
+ self.unit, other.unit
113
+ )));
114
+ }
115
+ Ok(Displacement {
116
+ dx: self.x - other.x,
117
+ dy: self.y - other.y,
118
+ dz: self.z - other.z,
119
+ frame: self.frame.clone(),
120
+ unit: self.unit.clone(),
121
+ })
122
+ }
123
+
124
+ /// Add a displacement to get a new position.
125
+ #[napi]
126
+ pub fn add_displacement(&self, d: &Displacement) -> napi::Result<Position> {
127
+ if self.frame != d.frame {
128
+ return Err(napi::Error::from_reason(format!(
129
+ "Cannot add displacement in different frame: '{}' vs '{}'",
130
+ self.frame, d.frame
131
+ )));
132
+ }
133
+ if self.unit != d.unit {
134
+ return Err(napi::Error::from_reason(format!(
135
+ "Cannot add displacement with different unit: '{}' vs '{}'",
136
+ self.unit, d.unit
137
+ )));
138
+ }
139
+ Ok(Position {
140
+ x: self.x + d.dx,
141
+ y: self.y + d.dy,
142
+ z: self.z + d.dz,
143
+ frame: self.frame.clone(),
144
+ center: self.center.clone(),
145
+ unit: self.unit.clone(),
146
+ })
147
+ }
148
+ }
149
+
150
+ impl Position {
151
+ /// Internal constructor without validation (for Rust callers).
152
+ pub(crate) fn new_internal(
153
+ x: f64,
154
+ y: f64,
155
+ z: f64,
156
+ frame: &str,
157
+ center: &str,
158
+ unit: &str,
159
+ ) -> Self {
160
+ Self {
161
+ x,
162
+ y,
163
+ z,
164
+ frame: frame.to_string(),
165
+ center: center.to_string(),
166
+ unit: unit.to_string(),
167
+ }
168
+ }
169
+ }
170
+
171
+ /// A displacement vector (difference between two positions).
172
+ #[napi]
173
+ #[derive(Clone)]
174
+ pub struct Displacement {
175
+ dx: f64,
176
+ dy: f64,
177
+ dz: f64,
178
+ frame: String,
179
+ unit: String,
180
+ }
181
+
182
+ #[napi]
183
+ impl Displacement {
184
+ /// Create a new displacement.
185
+ #[napi(constructor)]
186
+ pub fn new(
187
+ dx: f64,
188
+ dy: f64,
189
+ dz: f64,
190
+ frame: String,
191
+ unit: String,
192
+ ) -> napi::Result<Self> {
193
+ validate_frame(&frame)?;
194
+ validate_unit(&unit)?;
195
+ Ok(Self { dx, dy, dz, frame, unit })
196
+ }
197
+
198
+ /// X component of displacement.
199
+ #[napi(getter)]
200
+ pub fn dx(&self) -> f64 {
201
+ self.dx
202
+ }
203
+
204
+ /// Y component of displacement.
205
+ #[napi(getter)]
206
+ pub fn dy(&self) -> f64 {
207
+ self.dy
208
+ }
209
+
210
+ /// Z component of displacement.
211
+ #[napi(getter)]
212
+ pub fn dz(&self) -> f64 {
213
+ self.dz
214
+ }
215
+
216
+ /// Reference frame.
217
+ #[napi(getter)]
218
+ pub fn frame(&self) -> String {
219
+ self.frame.clone()
220
+ }
221
+
222
+ /// Distance unit.
223
+ #[napi(getter)]
224
+ pub fn unit(&self) -> String {
225
+ self.unit.clone()
226
+ }
227
+
228
+ /// Euclidean magnitude.
229
+ #[napi]
230
+ pub fn magnitude(&self) -> f64 {
231
+ (self.dx * self.dx + self.dy * self.dy + self.dz * self.dz).sqrt()
232
+ }
233
+
234
+ /// Scale displacement by a factor.
235
+ #[napi]
236
+ pub fn scale(&self, factor: f64) -> Displacement {
237
+ Displacement {
238
+ dx: self.dx * factor,
239
+ dy: self.dy * factor,
240
+ dz: self.dz * factor,
241
+ frame: self.frame.clone(),
242
+ unit: self.unit.clone(),
243
+ }
244
+ }
245
+
246
+ /// Add another displacement.
247
+ #[napi]
248
+ pub fn add(&self, other: &Displacement) -> napi::Result<Displacement> {
249
+ if self.frame != other.frame || self.unit != other.unit {
250
+ return Err(napi::Error::from_reason(
251
+ "Cannot add displacements with different frame/unit",
252
+ ));
253
+ }
254
+ Ok(Displacement {
255
+ dx: self.dx + other.dx,
256
+ dy: self.dy + other.dy,
257
+ dz: self.dz + other.dz,
258
+ frame: self.frame.clone(),
259
+ unit: self.unit.clone(),
260
+ })
261
+ }
262
+ }
263
+
264
+ fn validate_frame(f: &str) -> napi::Result<()> {
265
+ match f {
266
+ FRAME_ECL | FRAME_EQ | FRAME_ICRS | "ICRF" | "EquatorialMeanOfDate" | "EquatorialTrueOfDate" => Ok(()),
267
+ _ => Err(napi::Error::from_reason(format!(
268
+ "Unknown frame '{}'. Valid: EclipticMeanJ2000, EquatorialMeanJ2000, ICRS, ICRF",
269
+ f
270
+ ))),
271
+ }
272
+ }
273
+
274
+ fn validate_center(c: &str) -> napi::Result<()> {
275
+ match c {
276
+ CENTER_BARY | CENTER_HELIO | CENTER_GEO => Ok(()),
277
+ _ => Err(napi::Error::from_reason(format!(
278
+ "Unknown center '{}'. Valid: Barycentric, Heliocentric, Geocentric",
279
+ c
280
+ ))),
281
+ }
282
+ }
283
+
284
+ fn validate_unit(u: &str) -> napi::Result<()> {
285
+ match u {
286
+ UNIT_AU | UNIT_KM => Ok(()),
287
+ _ => Err(napi::Error::from_reason(format!(
288
+ "Unknown unit '{}'. Valid: au, km",
289
+ u
290
+ ))),
291
+ }
292
+ }