Files
digital-psychology/apps/api/internal/star/ephemeris/ephemeris.go
T
jackyu66gitandCursor bd22d9dddd feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 11:37:53 +08:00

162 lines
3.7 KiB
Go

// Package ephemeris computes tropical ecliptic longitudes.
// Default backend: Swiss Ephemeris Moshier (no external ephe files).
// NOTE: go-swisseph is AGPL-3.0; commercial deployment needs Astrodienst SE license or AGPL compliance.
package ephemeris
import (
"fmt"
"math"
"os"
"sync"
"time"
swe "github.com/tejzpr/go-swisseph"
)
// Body keys match natal chart keys.
const (
BodySun = "sun"
BodyMoon = "moon"
BodyMercury = "mercury"
BodyVenus = "venus"
BodyMars = "mars"
BodyJupiter = "jupiter"
BodySaturn = "saturn"
BodyUranus = "uranus"
BodyNeptune = "neptune"
BodyPluto = "pluto"
)
var bodyID = map[string]int32{
BodySun: swe.Sun,
BodyMoon: swe.Moon,
BodyMercury: swe.Mercury,
BodyVenus: swe.Venus,
BodyMars: swe.Mars,
BodyJupiter: swe.Jupiter,
BodySaturn: swe.Saturn,
BodyUranus: swe.Uranus,
BodyNeptune: swe.Neptune,
BodyPluto: swe.Pluto,
}
// PlanetOrder is the standard body sequence (excluding ASC).
var PlanetOrder = []string{
BodySun, BodyMoon, BodyMercury, BodyVenus, BodyMars,
BodyJupiter, BodySaturn, BodyUranus, BodyNeptune, BodyPluto,
}
var (
once sync.Once
iflag int32 = swe.FlagMoseph
epheOK bool
)
func initSE() {
once.Do(func() {
path := os.Getenv("SE_EPHE_PATH")
if path == "" {
path = os.Getenv("EPHEMERIS_PATH")
}
if path != "" {
swe.SetEphePath(path)
iflag = swe.FlagSwieph
epheOK = true
} else {
// Moshier: no files required
iflag = swe.FlagMoseph
epheOK = true
}
})
}
// Backend returns "moshier" or "swiss" for diagnostics.
func Backend() string {
initSE()
if iflag == swe.FlagSwieph {
return "swiss"
}
return "moshier"
}
// JulianDayUT converts a UTC instant to Julian Day (UT).
func JulianDayUT(utc time.Time) float64 {
initSE()
utc = utc.UTC()
sec := float64(utc.Second()) + float64(utc.Nanosecond())/1e9
dret, err := swe.UtcToJd(
int32(utc.Year()), int32(utc.Month()), int32(utc.Day()),
int32(utc.Hour()), int32(utc.Minute()), sec, swe.GregCal,
)
if err != nil {
// Fallback Meeus-style JD
return julianDayFallback(utc)
}
return dret[1] // UT
}
// PlanetLon returns tropical ecliptic longitude in degrees [0,360).
func PlanetLon(jdUt float64, body string) (float64, error) {
initSE()
ipl, ok := bodyID[body]
if !ok {
return 0, fmt.Errorf("unknown body %q", body)
}
r := swe.CalcUT(jdUt, ipl, iflag)
if r.Flag < 0 {
return 0, fmt.Errorf("swe calc %s: %s", body, r.Error)
}
return norm360(r.Data[0]), nil
}
// Ascendant returns tropical ASC longitude degrees using Placidus cusps (Points[0]).
// Callers apply Whole Sign houses from this ASC.
func Ascendant(jdUt, lat, lng float64) (float64, error) {
initSE()
h := swe.Houses(jdUt, lat, lng, 'P')
if h.Flag < 0 || len(h.Points) < 1 {
return 0, fmt.Errorf("swe houses failed flag=%d", h.Flag)
}
return norm360(h.Points[0]), nil
}
// AllPlanetLons returns longitudes for PlanetOrder.
func AllPlanetLons(jdUt float64) (map[string]float64, error) {
out := make(map[string]float64, len(PlanetOrder))
for _, k := range PlanetOrder {
lon, err := PlanetLon(jdUt, k)
if err != nil {
return nil, err
}
out[k] = lon
}
return out, nil
}
func norm360(x float64) float64 {
x = math.Mod(x, 360)
if x < 0 {
x += 360
}
return x
}
func julianDayFallback(t time.Time) float64 {
y := t.Year()
m := int(t.Month())
d := float64(t.Day()) + float64(t.Hour())/24 + float64(t.Minute())/1440 + float64(t.Second())/86400
if m <= 2 {
y--
m += 12
}
A := y / 100
B := 2 - A + A/4
return math.Floor(365.25*float64(y+4716)) + math.Floor(30.6001*float64(m+1)) + d + float64(B) - 1524.5
}
// Ready reports whether ephemeris init succeeded (always true for Moshier).
func Ready() bool {
initSE()
return epheOK
}