落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
3.7 KiB
Go
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
|
|
}
|