feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Altitude 月亮高度角,date 的时区参与地方时计算 / lunar altitude; the zone of date enters local time.
|
||||
func Altitude(date time.Time, lon, lat float64) float64 {
|
||||
return defaultModule.Altitude(date, lon, lat)
|
||||
}
|
||||
|
||||
// ApogeesInMonth 指定年月内的所有月球远地点 / all lunar apogees in the given Gregorian month.
|
||||
func ApogeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return defaultModule.ApogeesInMonth(year, month)
|
||||
}
|
||||
|
||||
// ApparentAltitude 月亮视高度角 / apparent lunar altitude.
|
||||
func ApparentAltitude(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return defaultModule.ApparentAltitude(date, lon, lat, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// ApparentDec 月亮站心视赤纬 / apparent topocentric declination.
|
||||
func ApparentDec(date time.Time, lon, lat float64) float64 {
|
||||
return defaultModule.ApparentDec(date, lon, lat)
|
||||
}
|
||||
|
||||
// ApparentLo 月亮地心视黄经 / apparent geocentric ecliptic longitude.
|
||||
func ApparentLo(date time.Time) float64 { return defaultModule.ApparentLo(date) }
|
||||
|
||||
// ApparentRa 月亮站心视赤经,date 的时区参与地方时计算 / apparent topocentric right ascension; the zone of date enters local time.
|
||||
func ApparentRa(date time.Time, lon, lat float64) float64 {
|
||||
return defaultModule.ApparentRa(date, lon, lat)
|
||||
}
|
||||
|
||||
// ApparentRaDec 月亮站心视赤经、视赤纬 / apparent topocentric right ascension and declination.
|
||||
func ApparentRaDec(date time.Time, lon, lat float64) (float64, float64) {
|
||||
return defaultModule.ApparentRaDec(date, lon, lat)
|
||||
}
|
||||
|
||||
// ApparentZenith 月亮视天顶距 / apparent lunar zenith distance.
|
||||
func ApparentZenith(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return defaultModule.ApparentZenith(date, lon, lat, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// AscendingNode 月球升交点黄经 / ascending node longitude of the Moon.
|
||||
func AscendingNode(date time.Time) float64 { return defaultModule.AscendingNode(date) }
|
||||
|
||||
// AscendingNodeN 月球升交点黄经(n 变体) / n-parameterised ascending node longitude of the Moon.
|
||||
func AscendingNodeN(date time.Time, n int) float64 { return defaultModule.AscendingNodeN(date, n) }
|
||||
|
||||
// Azimuth 月亮方位角,正北为 0° 向东增加 / lunar azimuth from north through east.
|
||||
func Azimuth(date time.Time, lon, lat float64) float64 { return defaultModule.Azimuth(date, lon, lat) }
|
||||
|
||||
// BrightLimbPositionAngle 月亮明亮边缘位置角,单位度 / position angle of the Moon's bright limb in degrees.
|
||||
func BrightLimbPositionAngle(date time.Time) float64 {
|
||||
return defaultModule.BrightLimbPositionAngle(date)
|
||||
}
|
||||
|
||||
// BrightLimbPositionAngleN 月亮明亮边缘位置角(n 变体),单位度 / n-parameterised position angle of the Moon's bright limb in degrees.
|
||||
func BrightLimbPositionAngleN(date time.Time, n int) float64 {
|
||||
return defaultModule.BrightLimbPositionAngleN(date, n)
|
||||
}
|
||||
|
||||
// ClosestConjunctionWithPlanet 最近一次行星合月(赤经合) / closest Moon-planet conjunction.
|
||||
func ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
return defaultModule.ClosestConjunctionWithPlanet(date, planet)
|
||||
}
|
||||
|
||||
// ClosestFirstQuarter 最近上弦英文别名 / English alias for ClosestShangXianYue.
|
||||
func ClosestFirstQuarter(date time.Time) time.Time { return defaultModule.ClosestFirstQuarter(date) }
|
||||
|
||||
// ClosestFullMoon 最近望月英文别名 / English alias for ClosestWangYue.
|
||||
func ClosestFullMoon(date time.Time) time.Time { return defaultModule.ClosestFullMoon(date) }
|
||||
|
||||
// ClosestLastQuarter 最近下弦英文别名 / English alias for ClosestXiaXianYue.
|
||||
func ClosestLastQuarter(date time.Time) time.Time { return defaultModule.ClosestLastQuarter(date) }
|
||||
|
||||
// ClosestMaximumNorthDeclination 离指定时刻最近一次月球最大北赤纬 / closest maximum northern lunar declination to date.
|
||||
func ClosestMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.ClosestMaximumNorthDeclination(date)
|
||||
}
|
||||
|
||||
// ClosestMaximumSouthDeclination 离指定时刻最近一次月球最大南赤纬 / closest maximum southern lunar declination to date.
|
||||
func ClosestMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.ClosestMaximumSouthDeclination(date)
|
||||
}
|
||||
|
||||
// ClosestNewMoon 最近朔月英文别名 / English alias for ClosestShuoYue.
|
||||
func ClosestNewMoon(date time.Time) time.Time { return defaultModule.ClosestNewMoon(date) }
|
||||
|
||||
// ClosestShangXianYue 最近上弦,结果保持 date 的时区 / closest first quarter in date's zone.
|
||||
func ClosestShangXianYue(date time.Time) time.Time { return defaultModule.ClosestShangXianYue(date) }
|
||||
|
||||
// ClosestShuoYue 最近朔月,结果保持 date 的时区 / closest new moon in date's zone.
|
||||
func ClosestShuoYue(date time.Time) time.Time { return defaultModule.ClosestShuoYue(date) }
|
||||
|
||||
// ClosestWangYue 最近望月,结果保持 date 的时区 / closest full moon in date's zone.
|
||||
func ClosestWangYue(date time.Time) time.Time { return defaultModule.ClosestWangYue(date) }
|
||||
|
||||
// ClosestXiaXianYue 最近下弦,结果保持 date 的时区 / closest last quarter in date's zone.
|
||||
func ClosestXiaXianYue(date time.Time) time.Time { return defaultModule.ClosestXiaXianYue(date) }
|
||||
|
||||
// CulminationTime 月亮中天时刻,取 date 时区的当地日期 / culmination time on date's local day.
|
||||
func CulminationTime(date time.Time, lon, lat float64) time.Time {
|
||||
return defaultModule.CulminationTime(date, lon, lat)
|
||||
}
|
||||
|
||||
// DescendingNode 月球降交点黄经 / descending node longitude of the Moon.
|
||||
func DescendingNode(date time.Time) float64 { return defaultModule.DescendingNode(date) }
|
||||
|
||||
// DescendingNodeN 月球降交点黄经(n 变体) / n-parameterised descending node longitude of the Moon.
|
||||
func DescendingNodeN(date time.Time, n int) float64 { return defaultModule.DescendingNodeN(date, n) }
|
||||
|
||||
// Diameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
|
||||
func Diameter(date time.Time) float64 { return defaultModule.Diameter(date) }
|
||||
|
||||
// DiameterN 月亮视直径(n 变体),单位角秒 / n-parameterised apparent lunar diameter in arcseconds.
|
||||
func DiameterN(date time.Time, n int) float64 { return defaultModule.DiameterN(date, n) }
|
||||
|
||||
// DownTime 月落时刻别名,同 SetTime / deprecated alias of SetTime.
|
||||
func DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return defaultModule.DownTime(date, lon, lat, height, aero)
|
||||
}
|
||||
|
||||
// EarthDistance 地月距离(千米) / Earth-Moon distance in kilometers.
|
||||
func EarthDistance(date time.Time) float64 {
|
||||
return defaultModule.EarthDistance(date)
|
||||
}
|
||||
|
||||
// FindBestPlanetOccultations 窗口内每次行星月掩的全球海平面几何掩甚点,可见性不参与选点 / global sea-level geometric greatest point of each planetary occultation.
|
||||
func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
||||
return defaultModule.FindBestPlanetOccultations(start, end, planet, options)
|
||||
}
|
||||
|
||||
// FindBestStarOccultations 窗口内每次恒星月掩的全球海平面几何掩甚点,可见性不参与选点 / global sea-level geometric greatest point of each stellar occultation.
|
||||
func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error) {
|
||||
return defaultModule.FindBestStarOccultations(start, end, star, options)
|
||||
}
|
||||
|
||||
// FindPlanetOccultationPaths 窗口内每次行星月掩的全球外接触掩带 / global outer-contact footprint of each planetary occultation.
|
||||
func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationPathOptions) ([]PlanetOccultationPath, error) {
|
||||
return defaultModule.FindPlanetOccultationPaths(start, end, planet, options)
|
||||
}
|
||||
|
||||
// FindPlanetOccultations 固定地点的有限盘面行星月掩,忽略行星环 / planetary occultations at a fixed site, rings excluded.
|
||||
func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
||||
return defaultModule.FindPlanetOccultations(start, end, planet, longitude, latitude, height, options)
|
||||
}
|
||||
|
||||
// FindStarOccultationPaths 点源恒星月掩的全球掩带,不加载内嵌星表 / global footprint of a stellar occultation; no embedded catalog is loaded.
|
||||
func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
|
||||
return defaultModule.FindStarOccultationPaths(start, end, star, options)
|
||||
}
|
||||
|
||||
// FindStarOccultations 固定地点的点源恒星月掩,不加载内嵌星表 / stellar occultations at a fixed site; no embedded catalog is loaded.
|
||||
func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]StarOccultationInfo, error) {
|
||||
return defaultModule.FindStarOccultations(start, end, star, longitude, latitude, height, options)
|
||||
}
|
||||
|
||||
// FirstQuarter 上弦英文别名 / English alias for ShangXianYue.
|
||||
func FirstQuarter(year float64) time.Time { return defaultModule.FirstQuarter(year) }
|
||||
|
||||
// FullMoon 望月英文别名 / English alias for WangYue.
|
||||
func FullMoon(year float64) time.Time { return defaultModule.FullMoon(year) }
|
||||
|
||||
// GeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination.
|
||||
func GeocentricApparentDec(date time.Time) float64 { return defaultModule.GeocentricApparentDec(date) }
|
||||
|
||||
// GeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension.
|
||||
func GeocentricApparentRa(date time.Time) float64 { return defaultModule.GeocentricApparentRa(date) }
|
||||
|
||||
// GeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination.
|
||||
func GeocentricApparentRaDec(date time.Time) (float64, float64) {
|
||||
return defaultModule.GeocentricApparentRaDec(date)
|
||||
}
|
||||
|
||||
// HourAngle 月亮时角,date 的时区参与地方时计算 / lunar hour angle; the zone of date enters local time.
|
||||
func HourAngle(date time.Time, lon, lat float64) float64 {
|
||||
return defaultModule.HourAngle(date, lon, lat)
|
||||
}
|
||||
|
||||
// LastConjunctionWithPlanet 上一次行星合月(赤经合) / previous Moon-planet conjunction.
|
||||
func LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
return defaultModule.LastConjunctionWithPlanet(date, planet)
|
||||
}
|
||||
|
||||
// LastFirstQuarter 上一次上弦英文别名 / English alias for LastShangXianYue.
|
||||
func LastFirstQuarter(date time.Time) time.Time { return defaultModule.LastFirstQuarter(date) }
|
||||
|
||||
// LastFullMoon 上一次望月英文别名 / English alias for LastWangYue.
|
||||
func LastFullMoon(date time.Time) time.Time { return defaultModule.LastFullMoon(date) }
|
||||
|
||||
// LastLastQuarter 上一次下弦英文别名 / English alias for LastXiaXianYue.
|
||||
func LastLastQuarter(date time.Time) time.Time { return defaultModule.LastLastQuarter(date) }
|
||||
|
||||
// LastMaximumNorthDeclination 指定时刻之前最近一次月球最大北赤纬 / last maximum northern lunar declination at or before date.
|
||||
func LastMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.LastMaximumNorthDeclination(date)
|
||||
}
|
||||
|
||||
// LastMaximumSouthDeclination 指定时刻之前最近一次月球最大南赤纬 / last maximum southern lunar declination at or before date.
|
||||
func LastMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.LastMaximumSouthDeclination(date)
|
||||
}
|
||||
|
||||
// LastNewMoon 上一次朔月英文别名 / English alias for LastShuoYue.
|
||||
func LastNewMoon(date time.Time) time.Time { return defaultModule.LastNewMoon(date) }
|
||||
|
||||
// LastQuarter 下弦英文别名 / English alias for XiaXianYue.
|
||||
func LastQuarter(year float64) time.Time { return defaultModule.LastQuarter(year) }
|
||||
|
||||
// LastShangXianYue 上一次上弦,结果保持 date 的时区 / previous first quarter in date's zone.
|
||||
func LastShangXianYue(date time.Time) time.Time { return defaultModule.LastShangXianYue(date) }
|
||||
|
||||
// LastShuoYue 上一次朔月,结果保持 date 的时区 / previous new moon in date's zone.
|
||||
func LastShuoYue(date time.Time) time.Time { return defaultModule.LastShuoYue(date) }
|
||||
|
||||
// LastWangYue 上一次望月,结果保持 date 的时区 / previous full moon in date's zone.
|
||||
func LastWangYue(date time.Time) time.Time { return defaultModule.LastWangYue(date) }
|
||||
|
||||
// LastXiaXianYue 上一次下弦,结果保持 date 的时区 / previous last quarter in date's zone.
|
||||
func LastXiaXianYue(date time.Time) time.Time { return defaultModule.LastXiaXianYue(date) }
|
||||
|
||||
// MaximumNorthDeclinationsInMonth 指定年月内的所有月球最大北赤纬事件 / all maximum northern lunar declination events in the given Gregorian month.
|
||||
func MaximumNorthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return defaultModule.MaximumNorthDeclinationsInMonth(year, month)
|
||||
}
|
||||
|
||||
// MaximumSouthDeclinationsInMonth 指定年月内的所有月球最大南赤纬事件 / all maximum southern lunar declination events in the given Gregorian month.
|
||||
func MaximumSouthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return defaultModule.MaximumSouthDeclinationsInMonth(year, month)
|
||||
}
|
||||
|
||||
// NewMoon 朔月英文别名 / English alias for ShuoYue.
|
||||
func NewMoon(year float64) time.Time { return defaultModule.NewMoon(year) }
|
||||
|
||||
// NextConjunctionWithPlanet 下一次行星合月(赤经合) / next Moon-planet conjunction.
|
||||
func NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
return defaultModule.NextConjunctionWithPlanet(date, planet)
|
||||
}
|
||||
|
||||
// NextFirstQuarter 下一次上弦英文别名 / English alias for NextShangXianYue.
|
||||
func NextFirstQuarter(date time.Time) time.Time { return defaultModule.NextFirstQuarter(date) }
|
||||
|
||||
// NextFullMoon 下一次望月英文别名 / English alias for NextWangYue.
|
||||
func NextFullMoon(date time.Time) time.Time { return defaultModule.NextFullMoon(date) }
|
||||
|
||||
// NextLastQuarter 下一次下弦英文别名 / English alias for NextXiaXianYue.
|
||||
func NextLastQuarter(date time.Time) time.Time { return defaultModule.NextLastQuarter(date) }
|
||||
|
||||
// NextMaximumNorthDeclination 指定时刻之后最近一次月球最大北赤纬 / next maximum northern lunar declination after date.
|
||||
func NextMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.NextMaximumNorthDeclination(date)
|
||||
}
|
||||
|
||||
// NextMaximumSouthDeclination 指定时刻之后最近一次月球最大南赤纬 / next maximum southern lunar declination after date.
|
||||
func NextMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return defaultModule.NextMaximumSouthDeclination(date)
|
||||
}
|
||||
|
||||
// NextNewMoon 下一次朔月英文别名 / English alias for NextShuoYue.
|
||||
func NextNewMoon(date time.Time) time.Time { return defaultModule.NextNewMoon(date) }
|
||||
|
||||
// NextShangXianYue 下一次上弦,结果保持 date 的时区 / next first quarter in date's zone.
|
||||
func NextShangXianYue(date time.Time) time.Time { return defaultModule.NextShangXianYue(date) }
|
||||
|
||||
// NextShuoYue 下一次朔月,结果保持 date 的时区 / next new moon in date's zone.
|
||||
func NextShuoYue(date time.Time) time.Time { return defaultModule.NextShuoYue(date) }
|
||||
|
||||
// NextWangYue 下一次望月,结果保持 date 的时区 / next full moon in date's zone.
|
||||
func NextWangYue(date time.Time) time.Time { return defaultModule.NextWangYue(date) }
|
||||
|
||||
// NextXiaXianYue 下一次下弦,结果保持 date 的时区 / next last quarter in date's zone.
|
||||
func NextXiaXianYue(date time.Time) time.Time { return defaultModule.NextXiaXianYue(date) }
|
||||
|
||||
// ParallacticAngle 月亮站心视差角(天顶方向角) / topocentric lunar parallactic angle.
|
||||
func ParallacticAngle(date time.Time, lon, lat float64) float64 {
|
||||
return defaultModule.ParallacticAngle(date, lon, lat)
|
||||
}
|
||||
|
||||
// PerigeesInMonth 指定年月内的所有月球近地点 / all lunar perigees in the given Gregorian month.
|
||||
func PerigeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return defaultModule.PerigeesInMonth(year, month)
|
||||
}
|
||||
|
||||
// Phase 月面受照比例,取值 [0, 1] / illuminated fraction in [0, 1].
|
||||
func Phase(date time.Time) float64 { return defaultModule.Phase(date) }
|
||||
|
||||
// PhaseDesc 中文月相名称 / Chinese lunar phase name.
|
||||
func PhaseDesc(date time.Time) string { return defaultModule.PhaseDesc(date) }
|
||||
|
||||
// Physical 月球物理观测参数 / physical observing parameters of the Moon.
|
||||
func Physical(date time.Time) PhysicalInfo { return defaultModule.Physical(date) }
|
||||
|
||||
// PhysicalN 月球物理观测参数(n 变体) / n-parameterised physical observing parameters of the Moon.
|
||||
func PhysicalN(date time.Time, n int) PhysicalInfo { return defaultModule.PhysicalN(date, n) }
|
||||
|
||||
// PlanetOccultationDiagram 已求解固定地点行星月掩的图形几何 / diagram geometry of a solved fixed-site planetary occultation.
|
||||
func PlanetOccultationDiagram(
|
||||
info PlanetOccultationInfo,
|
||||
options PlanetOccultationDiagramOptions,
|
||||
) PlanetOccultationDiagramResult {
|
||||
return defaultModule.PlanetOccultationDiagram(info, options)
|
||||
}
|
||||
|
||||
// PlanetOccultationFootprintsAt 指定时刻行星偏掩、全掩可见足迹 / visible partial and total planetary footprints at one instant.
|
||||
func PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (PlanetOccultationInstant, error) {
|
||||
return defaultModule.PlanetOccultationFootprintsAt(at, planet)
|
||||
}
|
||||
|
||||
// RiseTime 月出时刻,取 date 时区的当地日期;height 为椭球高(米),aero 为真时按大气折射与实时视半径算上缘 / moonrise on date's local day.
|
||||
func RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return defaultModule.RiseTime(date, lon, lat, height, aero)
|
||||
}
|
||||
|
||||
// Semidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
|
||||
func Semidiameter(date time.Time) float64 { return defaultModule.Semidiameter(date) }
|
||||
|
||||
// SemidiameterN 月亮视半径(n 变体),单位角秒 / n-parameterised apparent lunar semidiameter in arcseconds.
|
||||
func SemidiameterN(date time.Time, n int) float64 { return defaultModule.SemidiameterN(date, n) }
|
||||
|
||||
// SetTime 月落时刻,参数同 RiseTime / moonset on date's local day.
|
||||
func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return defaultModule.SetTime(date, lon, lat, height, aero)
|
||||
}
|
||||
|
||||
// ShangXianYue 上弦锚点解,year 为公历小数年,结果为 UTC / first quarter near a decimal-year anchor, in UTC.
|
||||
func ShangXianYue(year float64) time.Time { return defaultModule.ShangXianYue(year) }
|
||||
|
||||
// ShuoYue 朔月锚点解,year 为公历小数年,结果为 UTC / new moon near a decimal-year anchor, in UTC.
|
||||
func ShuoYue(year float64) time.Time { return defaultModule.ShuoYue(year) }
|
||||
|
||||
// StarOccultationLimbProfile 精密恒星月掩:给出全部遮挡区间与带内采样,limb 为 nil 时用模块月缘 / precise stellar occultation; nil limb uses the module profile.
|
||||
func StarOccultationLimbProfile(info StarOccultationInfo, star StarCoordinate, limb LunarLimb, options StarOccultationLimbProfileOptions) (StarOccultationLimbProfileResult, error) {
|
||||
return defaultModule.StarOccultationLimbProfile(info, star, limb, options)
|
||||
}
|
||||
|
||||
// StarOccultationDiagram 已求解固定地点恒星月掩的图形几何 / diagram geometry of a solved fixed-site stellar occultation.
|
||||
func StarOccultationDiagram(
|
||||
info StarOccultationInfo,
|
||||
star StarCoordinate,
|
||||
options StarOccultationDiagramOptions,
|
||||
) StarOccultationDiagramResult {
|
||||
return defaultModule.StarOccultationDiagram(info, star, options)
|
||||
}
|
||||
|
||||
// StarOccultationFootprintAt 指定时刻点源恒星月掩可见足迹 / visible stellar-occultation footprint at one instant.
|
||||
func StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultationInstant, error) {
|
||||
return defaultModule.StarOccultationFootprintAt(at, star)
|
||||
}
|
||||
|
||||
// SunMoonLoDiff 月日视黄经差,取 [0, 360),朔近 0°、望近 180° / Moon minus Sun apparent longitude in [0, 360).
|
||||
func SunMoonLoDiff(date time.Time) float64 { return defaultModule.SunMoonLoDiff(date) }
|
||||
|
||||
// TopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角(度),height 为椭球高(米) / topocentric bright-limb position angle in degrees.
|
||||
func TopocentricBrightLimbPositionAngle(date time.Time, observerLon, observerLat, height float64) float64 {
|
||||
return defaultModule.TopocentricBrightLimbPositionAngle(date, observerLon, observerLat, height)
|
||||
}
|
||||
|
||||
// TopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(n 变体),单位度 / n-parameterised topocentric position angle of the Moon's bright limb in degrees.
|
||||
func TopocentricBrightLimbPositionAngleN(date time.Time, observerLon, observerLat, height float64, n int) float64 {
|
||||
return defaultModule.TopocentricBrightLimbPositionAngleN(date, observerLon, observerLat, height, n)
|
||||
}
|
||||
|
||||
// TopocentricPhysical 月球站心物理观测参数,height 为椭球高(米) / topocentric physical observing parameters of the Moon.
|
||||
func TopocentricPhysical(date time.Time, observerLon, observerLat, height float64) PhysicalInfo {
|
||||
return defaultModule.TopocentricPhysical(date, observerLon, observerLat, height)
|
||||
}
|
||||
|
||||
// TopocentricPhysicalN 月球站心物理观测参数(n 变体) / n-parameterised topocentric physical observing parameters of the Moon.
|
||||
func TopocentricPhysicalN(date time.Time, observerLon, observerLat, height float64, n int) PhysicalInfo {
|
||||
return defaultModule.TopocentricPhysicalN(date, observerLon, observerLat, height, n)
|
||||
}
|
||||
|
||||
// TrueBo 月亮地心真黄纬 / geocentric true ecliptic latitude.
|
||||
func TrueBo(date time.Time) float64 { return defaultModule.TrueBo(date) }
|
||||
|
||||
// TrueBoN 月亮地心真黄纬,n<0 取完整级数,n>=0 时半解析模型用快速表、Legacy 按 n 截断 / geocentric true ecliptic latitude with series selector n.
|
||||
func TrueBoN(date time.Time, n int) float64 { return defaultModule.TrueBoN(date, n) }
|
||||
|
||||
// TrueDec 月亮地心真赤纬 / geocentric true declination.
|
||||
func TrueDec(date time.Time) float64 { return defaultModule.TrueDec(date) }
|
||||
|
||||
// TrueLo 月亮地心真黄经 / geocentric true ecliptic longitude.
|
||||
func TrueLo(date time.Time) float64 { return defaultModule.TrueLo(date) }
|
||||
|
||||
// TrueLoN 月亮地心真黄经,n<0 取完整级数,n>=0 时半解析模型用快速表、Legacy 按 n 截断 / geocentric true ecliptic longitude with series selector n.
|
||||
func TrueLoN(date time.Time, n int) float64 { return defaultModule.TrueLoN(date, n) }
|
||||
|
||||
// TrueRa 月亮地心真赤经 / geocentric true right ascension.
|
||||
func TrueRa(date time.Time) float64 { return defaultModule.TrueRa(date) }
|
||||
|
||||
// TrueRaDec 月亮地心真赤经、真赤纬 / geocentric true right ascension and declination.
|
||||
func TrueRaDec(date time.Time) (float64, float64) { return defaultModule.TrueRaDec(date) }
|
||||
|
||||
// WangYue 望月锚点解,year 为公历小数年,结果为 UTC / full moon near a decimal-year anchor, in UTC.
|
||||
func WangYue(year float64) time.Time { return defaultModule.WangYue(year) }
|
||||
|
||||
// XiaXianYue 下弦锚点解,year 为公历小数年,结果为 UTC / last quarter near a decimal-year anchor, in UTC.
|
||||
func XiaXianYue(year float64) time.Time { return defaultModule.XiaXianYue(year) }
|
||||
|
||||
// Zenith 月亮天顶距 / lunar zenith distance.
|
||||
func Zenith(date time.Time, lon, lat float64) float64 { return defaultModule.Zenith(date, lon, lat) }
|
||||
|
||||
// RiseTimeWithAtmosphere 月出时刻,按给定气压(hPa)与气温(°C)计算大气折射 / moonrise time with explicit pressure and temperature.
|
||||
func RiseTimeWithAtmosphere(date time.Time, lon, lat, height, pressureHPa, temperatureC float64) (time.Time, error) {
|
||||
return defaultModule.RiseTimeWithAtmosphere(date, lon, lat, height, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// SetTimeWithAtmosphere 月落时刻,按给定气压(hPa)与气温(°C)计算大气折射 / moonset time with explicit pressure and temperature.
|
||||
func SetTimeWithAtmosphere(date time.Time, lon, lat, height, pressureHPa, temperatureC float64) (time.Time, error) {
|
||||
return defaultModule.SetTimeWithAtmosphere(date, lon, lat, height, pressureHPa, temperatureC)
|
||||
}
|
||||
+4
-4
@@ -15,13 +15,13 @@ type ApsisInfo struct {
|
||||
}
|
||||
|
||||
// PerigeesInMonth 指定年月内的所有月球近地点 / all lunar perigees in the given Gregorian month.
|
||||
func PerigeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return convertMoonApsisInfos(basic.MoonPerigees(year, month))
|
||||
func (module *Moon) PerigeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return convertMoonApsisInfos(basic.MoonPerigeesWithInstance(year, month, module.inst))
|
||||
}
|
||||
|
||||
// ApogeesInMonth 指定年月内的所有月球远地点 / all lunar apogees in the given Gregorian month.
|
||||
func ApogeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return convertMoonApsisInfos(basic.MoonApogees(year, month))
|
||||
func (module *Moon) ApogeesInMonth(year int, month time.Month) []ApsisInfo {
|
||||
return convertMoonApsisInfos(basic.MoonApogeesWithInstance(year, month, module.inst))
|
||||
}
|
||||
|
||||
func convertMoonApsisInfos(events []basic.ApsisEvent) []ApsisInfo {
|
||||
|
||||
+6
-6
@@ -45,28 +45,28 @@ func validConjunctionPlanet(planet ConjunctionPlanet) bool {
|
||||
}
|
||||
|
||||
// LastConjunctionWithPlanet 上一次行星合月(赤经合) / previous Moon-planet conjunction.
|
||||
func LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
func (module *Moon) LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
if !validConjunctionPlanet(planet) {
|
||||
return time.Time{}
|
||||
}
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
return basic.JD2DateByZone(basic.LastMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.LastMoonPlanetConjunctionWithInstance(jde, conjunctionPlanetToBasic(planet), module.inst), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextConjunctionWithPlanet 下一次行星合月(赤经合) / next Moon-planet conjunction.
|
||||
func NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
func (module *Moon) NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
if !validConjunctionPlanet(planet) {
|
||||
return time.Time{}
|
||||
}
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
return basic.JD2DateByZone(basic.NextMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.NextMoonPlanetConjunctionWithInstance(jde, conjunctionPlanetToBasic(planet), module.inst), date.Location(), false)
|
||||
}
|
||||
|
||||
// ClosestConjunctionWithPlanet 最近一次行星合月(赤经合) / closest Moon-planet conjunction.
|
||||
func ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
func (module *Moon) ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time {
|
||||
if !validConjunctionPlanet(planet) {
|
||||
return time.Time{}
|
||||
}
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
return basic.JD2DateByZone(basic.ClosestMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.ClosestMoonPlanetConjunctionWithInstance(jde, conjunctionPlanetToBasic(planet), module.inst), date.Location(), false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/ephem"
|
||||
)
|
||||
|
||||
// Moon 指定星历实例的计算模块 / computation module bound to an ephemeris instance.
|
||||
type Moon struct {
|
||||
inst *ephem.Instance
|
||||
// lunarLimb 是月掩搜索的默认月缘剖面,nil 为圆月面。
|
||||
lunarLimb basic.LunarLimb
|
||||
}
|
||||
|
||||
// NewWithInstance 创建指定星历实例的模块,nil 使用全局模型 / create a module; nil uses the global model.
|
||||
func NewWithInstance(inst *ephem.Instance) *Moon { return &Moon{inst: inst} }
|
||||
|
||||
// WithLunarLimb 返回使用指定月缘剖面的副本,选项中未设置月缘时采用它 / copy with a default lunar limb for occultations.
|
||||
func (module *Moon) WithLunarLimb(limb basic.LunarLimb) *Moon {
|
||||
copied := *module
|
||||
copied.lunarLimb = limb
|
||||
return &copied
|
||||
}
|
||||
|
||||
// occultationOptions 在选项未设置月缘时补上模块默认值。
|
||||
func (module *Moon) occultationOptions(options OccultationSearchOptions) OccultationSearchOptions {
|
||||
if options.LunarLimb == nil {
|
||||
options.LunarLimb = module.lunarLimb
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
var defaultModule Moon
|
||||
|
||||
// EarthDistance 地月距离 / Earth-Moon distance.
|
||||
func (module *Moon) EarthDistance(date time.Time) float64 {
|
||||
jde := basic.UTC2TT(basic.Date2JD(date))
|
||||
model := ephem.Active()
|
||||
if module.inst != nil {
|
||||
model = module.inst.Model()
|
||||
}
|
||||
if model == ephem.Legacy {
|
||||
return basic.MoonAway(jde)
|
||||
}
|
||||
return basic.HMoonAwayWithInstance(jde, module.inst)
|
||||
}
|
||||
|
||||
// PlanetOccultationInfoInUT1 返回固定地点行星月掩的 UT1 时刻副本 / copy in UT1 of a local planetary result.
|
||||
func (module *Moon) PlanetOccultationInfoInUT1(info basic.PlanetOccultationInfo) basic.PlanetOccultationInfo {
|
||||
return PlanetOccultationInfoInUT1(info)
|
||||
}
|
||||
|
||||
// PlanetOccultationPathInUT1 返回行星月掩全球掩带的 UT1 时刻副本 / copy in UT1 of a planetary path.
|
||||
func (module *Moon) PlanetOccultationPathInUT1(path basic.PlanetOccultationPath) basic.PlanetOccultationPath {
|
||||
return PlanetOccultationPathInUT1(path)
|
||||
}
|
||||
|
||||
// StarCoordinateFromStarData 将星表记录转换为恒星月掩搜索接受的 J2000 输入;不会加载星表 / convert a catalog record to a J2000 occultation input.
|
||||
func (module *Moon) StarCoordinateFromStarData(star StarData) (StarCoordinate, error) {
|
||||
return StarCoordinateFromStarData(star)
|
||||
}
|
||||
|
||||
// StarOccultationInfoInUT1 返回固定地点恒星月掩的 UT1 时刻副本 / copy in UT1 of a local stellar result.
|
||||
func (module *Moon) StarOccultationInfoInUT1(info basic.StarOccultationInfo) basic.StarOccultationInfo {
|
||||
return StarOccultationInfoInUT1(info)
|
||||
}
|
||||
|
||||
// StarOccultationPathInUT1 返回恒星月掩全球路径的 UT1 时刻副本 / copy in UT1 of a stellar path.
|
||||
func (module *Moon) StarOccultationPathInUT1(path basic.StarOccultationPath) basic.StarOccultationPath {
|
||||
return StarOccultationPathInUT1(path)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/ephem"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type contextAPI interface {
|
||||
Altitude(date time.Time, lon, lat float64) float64
|
||||
ApogeesInMonth(year int, month time.Month) []ApsisInfo
|
||||
ApparentAltitude(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64
|
||||
ApparentDec(date time.Time, lon, lat float64) float64
|
||||
ApparentLo(date time.Time) float64
|
||||
ApparentRa(date time.Time, lon, lat float64) float64
|
||||
ApparentRaDec(date time.Time, lon, lat float64) (float64, float64)
|
||||
ApparentZenith(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64
|
||||
AscendingNode(date time.Time) float64
|
||||
AscendingNodeN(date time.Time, n int) float64
|
||||
Azimuth(date time.Time, lon, lat float64) float64
|
||||
BrightLimbPositionAngle(date time.Time) float64
|
||||
BrightLimbPositionAngleN(date time.Time, n int) float64
|
||||
ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time
|
||||
ClosestFirstQuarter(date time.Time) time.Time
|
||||
ClosestFullMoon(date time.Time) time.Time
|
||||
ClosestLastQuarter(date time.Time) time.Time
|
||||
ClosestMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
ClosestMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
ClosestNewMoon(date time.Time) time.Time
|
||||
ClosestShangXianYue(date time.Time) time.Time
|
||||
ClosestShuoYue(date time.Time) time.Time
|
||||
ClosestWangYue(date time.Time) time.Time
|
||||
ClosestXiaXianYue(date time.Time) time.Time
|
||||
CulminationTime(date time.Time, lon, lat float64) time.Time
|
||||
DescendingNode(date time.Time) float64
|
||||
DescendingNodeN(date time.Time, n int) float64
|
||||
Diameter(date time.Time) float64
|
||||
DiameterN(date time.Time, n int) float64
|
||||
DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error)
|
||||
EarthDistance(date time.Time) float64
|
||||
FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error)
|
||||
FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error)
|
||||
FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationPathOptions) ([]PlanetOccultationPath, error)
|
||||
FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error)
|
||||
FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error)
|
||||
FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]StarOccultationInfo, error)
|
||||
FirstQuarter(year float64) time.Time
|
||||
FullMoon(year float64) time.Time
|
||||
GeocentricApparentDec(date time.Time) float64
|
||||
GeocentricApparentRa(date time.Time) float64
|
||||
GeocentricApparentRaDec(date time.Time) (float64, float64)
|
||||
HourAngle(date time.Time, lon, lat float64) float64
|
||||
LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time
|
||||
LastFirstQuarter(date time.Time) time.Time
|
||||
LastFullMoon(date time.Time) time.Time
|
||||
LastLastQuarter(date time.Time) time.Time
|
||||
LastMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
LastMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
LastNewMoon(date time.Time) time.Time
|
||||
LastQuarter(year float64) time.Time
|
||||
LastShangXianYue(date time.Time) time.Time
|
||||
LastShuoYue(date time.Time) time.Time
|
||||
LastWangYue(date time.Time) time.Time
|
||||
LastXiaXianYue(date time.Time) time.Time
|
||||
MaximumNorthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo
|
||||
MaximumSouthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo
|
||||
NewMoon(year float64) time.Time
|
||||
NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time
|
||||
NextFirstQuarter(date time.Time) time.Time
|
||||
NextFullMoon(date time.Time) time.Time
|
||||
NextLastQuarter(date time.Time) time.Time
|
||||
NextMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
NextMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo
|
||||
NextNewMoon(date time.Time) time.Time
|
||||
NextShangXianYue(date time.Time) time.Time
|
||||
NextShuoYue(date time.Time) time.Time
|
||||
NextWangYue(date time.Time) time.Time
|
||||
NextXiaXianYue(date time.Time) time.Time
|
||||
ParallacticAngle(date time.Time, lon, lat float64) float64
|
||||
PerigeesInMonth(year int, month time.Month) []ApsisInfo
|
||||
Phase(date time.Time) float64
|
||||
PhaseDesc(date time.Time) string
|
||||
Physical(date time.Time) PhysicalInfo
|
||||
PhysicalN(date time.Time, n int) PhysicalInfo
|
||||
PlanetOccultationDiagram(
|
||||
info PlanetOccultationInfo,
|
||||
options PlanetOccultationDiagramOptions,
|
||||
) PlanetOccultationDiagramResult
|
||||
PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (PlanetOccultationInstant, error)
|
||||
PlanetOccultationInfoInUT1(info basic.PlanetOccultationInfo) basic.PlanetOccultationInfo
|
||||
PlanetOccultationPathInUT1(path basic.PlanetOccultationPath) basic.PlanetOccultationPath
|
||||
RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error)
|
||||
Semidiameter(date time.Time) float64
|
||||
SemidiameterN(date time.Time, n int) float64
|
||||
SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error)
|
||||
ShangXianYue(year float64) time.Time
|
||||
ShuoYue(year float64) time.Time
|
||||
StarCoordinateFromStarData(star StarData) (StarCoordinate, error)
|
||||
StarOccultationDiagram(
|
||||
info StarOccultationInfo,
|
||||
star StarCoordinate,
|
||||
options StarOccultationDiagramOptions,
|
||||
) StarOccultationDiagramResult
|
||||
StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultationInstant, error)
|
||||
StarOccultationInfoInUT1(info basic.StarOccultationInfo) basic.StarOccultationInfo
|
||||
StarOccultationPathInUT1(path basic.StarOccultationPath) basic.StarOccultationPath
|
||||
SunMoonLoDiff(date time.Time) float64
|
||||
TopocentricBrightLimbPositionAngle(date time.Time, observerLon, observerLat, height float64) float64
|
||||
TopocentricBrightLimbPositionAngleN(date time.Time, observerLon, observerLat, height float64, n int) float64
|
||||
TopocentricPhysical(date time.Time, observerLon, observerLat, height float64) PhysicalInfo
|
||||
TopocentricPhysicalN(date time.Time, observerLon, observerLat, height float64, n int) PhysicalInfo
|
||||
TrueBo(date time.Time) float64
|
||||
TrueBoN(date time.Time, n int) float64
|
||||
TrueDec(date time.Time) float64
|
||||
TrueLo(date time.Time) float64
|
||||
TrueLoN(date time.Time, n int) float64
|
||||
TrueRa(date time.Time) float64
|
||||
TrueRaDec(date time.Time) (float64, float64)
|
||||
WangYue(year float64) time.Time
|
||||
XiaXianYue(year float64) time.Time
|
||||
Zenith(date time.Time, lon, lat float64) float64
|
||||
}
|
||||
|
||||
var _ contextAPI = (*Moon)(nil)
|
||||
|
||||
func TestContextModuleIsolation(t *testing.T) {
|
||||
previous := basic.GetEphemerisModel()
|
||||
t.Cleanup(func() { basic.SetEphemerisModel(previous) })
|
||||
date := time.Date(2024, 10, 6, 12, 0, 0, 0, time.UTC)
|
||||
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
||||
basic.SetEphemerisModel(basic.EphemerisModel(model))
|
||||
want := ApparentLo(date)
|
||||
module := NewWithInstance(ephem.NewModelInstance(model))
|
||||
basic.SetEphemerisModel(basic.EphemerisModel(1 - int(model)))
|
||||
if got := module.ApparentLo(date); got != want {
|
||||
t.Fatalf("model %d: longitude %.12f, want %.12f", model, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -7,23 +7,23 @@ import (
|
||||
)
|
||||
|
||||
// Semidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
|
||||
func Semidiameter(date time.Time) float64 {
|
||||
return SemidiameterN(date, -1)
|
||||
func (module *Moon) Semidiameter(date time.Time) float64 {
|
||||
return module.SemidiameterN(date, -1)
|
||||
}
|
||||
|
||||
// SemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
|
||||
func SemidiameterN(date time.Time, n int) float64 {
|
||||
// SemidiameterN 月亮视半径(n 变体),单位角秒 / n-parameterised apparent lunar semidiameter in arcseconds.
|
||||
func (module *Moon) SemidiameterN(date time.Time, n int) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.MoonSemidiameterN(basic.UTC2TT(jd), n)
|
||||
return basic.MoonSemidiameterNWithInstance(basic.UTC2TT(jd), n, module.inst)
|
||||
}
|
||||
|
||||
// Diameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
|
||||
func Diameter(date time.Time) float64 {
|
||||
return DiameterN(date, -1)
|
||||
func (module *Moon) Diameter(date time.Time) float64 {
|
||||
return module.DiameterN(date, -1)
|
||||
}
|
||||
|
||||
// DiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
|
||||
func DiameterN(date time.Time, n int) float64 {
|
||||
// DiameterN 月亮视直径(n 变体),单位角秒 / n-parameterised apparent lunar diameter in arcseconds.
|
||||
func (module *Moon) DiameterN(date time.Time, n int) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.MoonDiameterN(basic.UTC2TT(jd), n)
|
||||
return basic.MoonDiameterNWithInstance(basic.UTC2TT(jd), n, module.inst)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package moon 月球视位置、升落、月相、天平动、视直径与物理观测参数;角度单位为度。
|
||||
// Package moon covers lunar apparent place, rise/set, phase, libration, apparent diameter and physical observing parameters; angles are degrees.
|
||||
// 求值接口的 N 后缀是 n 变体:n < 0 取完整级数;半解析模型下所有 n >= 0 使用同一张快速表;Legacy 按 n 截断。
|
||||
// The N suffix marks an n-variant: n < 0 uses the full series, every n >= 0 uses one fast table under the semianalytic model, and Legacy truncates by n.
|
||||
package moon
|
||||
@@ -0,0 +1,28 @@
|
||||
本包内嵌的月缘剖面数据不适用本仓库的 Apache-2.0,遵循其来源条款。完整声明见仓库根目录 /NOTICE(第 1 节)。
|
||||
The lunar limb profile embedded in this package is NOT covered by this repository's Apache-2.0 licence; it follows the terms of its source. The full notice is section 1 of /NOTICE in the repository root.
|
||||
|
||||
SELENE (KAGUYA) lunar limb profile — kaguya_lalt.bin
|
||||
Source data: SELENE Global Grid Topographic Data of the Moon (LALT_GGT_NUM),
|
||||
dataset sln-l-lalt-5-topo-ggt-num-v2.0, version 2.0, published 2017.
|
||||
Origin of the data: ISAS/JAXA.
|
||||
Provided by the SELENE (KAGUYA) Data Archive (DARTS), ISAS/JAXA:
|
||||
https://darts.isas.jaxa.jp/datasets/darts:sln-l-lalt-5-topo-ggt-num-v2.0
|
||||
|
||||
Acknowledgement, as requested by the SELENE Data Archive:
|
||||
"We thank the SELENE(KAGUYA) LALT team and the SELENE Data Archive for
|
||||
providing the SELENE(KAGUYA) data. SELENE is a Japanese mission developed
|
||||
and operated by JAXA."
|
||||
|
||||
Modifications: the original PDS3 fixed-width table (1/16 degree global grid,
|
||||
2880 x 5760 samples) was converted to a float32 elevation grid and resampled
|
||||
into a limb-radius profile table on a 1 degree grid of selenographic
|
||||
(libration) longitude and latitude and a 0.1 degree grid of position angle,
|
||||
taking max (R + h) cos(alpha) over a +/-9 degree arc per direction; the
|
||||
result is stored in this project's own zlib-compressed LLB1 container.
|
||||
|
||||
Licence: ISAS Data Policy,
|
||||
https://www.isas.jaxa.jp/en/researchers/data-policy/
|
||||
which states that its rules are compatible with Creative Commons Attribution
|
||||
4.0 International, https://creativecommons.org/licenses/by/4.0/
|
||||
Provided "as is", without warranties of any kind. ISAS/JAXA is not
|
||||
responsible for the actions of data users and does not endorse this project.
|
||||
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
// Package limb provides the embedded Kaguya LALT lunar limb profile.
|
||||
package limb
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sync"
|
||||
|
||||
"b612.me/astro/moon/limbtable"
|
||||
)
|
||||
|
||||
//go:embed kaguya_lalt.bin
|
||||
var kaguyaTable []byte
|
||||
|
||||
// Profile is the decoded Kaguya lunar limb profile.
|
||||
type Profile = limbtable.Profile
|
||||
|
||||
var (
|
||||
defaultOnce sync.Once
|
||||
defaultProfile *Profile
|
||||
)
|
||||
|
||||
// Default returns the embedded Kaguya LALT profile, decoded on first use.
|
||||
func Default() *Profile {
|
||||
defaultOnce.Do(func() {
|
||||
profile, err := Parse(kaguyaTable)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defaultProfile = profile
|
||||
})
|
||||
return defaultProfile
|
||||
}
|
||||
|
||||
// Parse decodes an LLB1 profile table.
|
||||
func Parse(data []byte) (*Profile, error) { return limbtable.Parse(data) }
|
||||
|
||||
const headerSize = limbtable.HeaderSize
|
||||
@@ -0,0 +1,110 @@
|
||||
package limb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 内置表的 sha256;换表时须同步更新。
|
||||
const kaguyaTableSHA256 = "7d49dc75497c404b78b5b259129d0f5ab17b56522128478591ea33c8cb97d072"
|
||||
|
||||
func TestEmbeddedTableChecksum(t *testing.T) {
|
||||
sum := sha256.Sum256(kaguyaTable)
|
||||
if got := hex.EncodeToString(sum[:]); got != kaguyaTableSHA256 {
|
||||
t.Fatalf("embedded table sha256 = %s, want %s", got, kaguyaTableSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimbRadiusMatchesGenerator(t *testing.T) {
|
||||
cases := []struct{ lon, lat, angle, radiusKM float64 }{
|
||||
{0, 0, 0, 1737.432000000},
|
||||
{3.3, -2.7, 123.45, 1735.688760000},
|
||||
{-8.6, 7.9, 359.97, 1738.466256000},
|
||||
{5.5, 1.25, 270.05, 1738.604000000},
|
||||
{-1.0, -6.0, 45.0, 1737.796000000},
|
||||
}
|
||||
profile := Default()
|
||||
for _, c := range cases {
|
||||
got, ok := profile.LimbRadiusKM(c.lon, c.lat, c.angle)
|
||||
if !ok {
|
||||
t.Fatalf("LimbRadiusKM(%v, %v, %v) not ok", c.lon, c.lat, c.angle)
|
||||
}
|
||||
if math.Abs(got-c.radiusKM) > 1e-6 {
|
||||
t.Errorf("LimbRadiusKM(%v, %v, %v) = %.9f, want %.9f", c.lon, c.lat, c.angle, got, c.radiusKM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimbRadiusWrapsAndClamps(t *testing.T) {
|
||||
profile := Default()
|
||||
base, _ := profile.LimbRadiusKM(2, 3, 10)
|
||||
for _, angle := range []float64{370, -350} {
|
||||
if got, _ := profile.LimbRadiusKM(2, 3, angle); math.Abs(got-base) > 1e-9 {
|
||||
t.Errorf("angle %v = %.9f, want %.9f", angle, got, base)
|
||||
}
|
||||
}
|
||||
edge, _ := profile.LimbRadiusKM(9, -8, 10)
|
||||
if got, _ := profile.LimbRadiusKM(12, -11, 10); got != edge {
|
||||
t.Errorf("out-of-range libration = %.9f, want clamped %.9f", got, edge)
|
||||
}
|
||||
if _, ok := profile.LimbRadiusKM(math.NaN(), 0, 0); ok {
|
||||
t.Error("NaN libration must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsBadInput(t *testing.T) {
|
||||
if _, err := Parse([]byte("nope")); err == nil {
|
||||
t.Error("short input must fail")
|
||||
}
|
||||
broken := append([]byte(nil), kaguyaTable[:headerSize+16]...)
|
||||
if _, err := Parse(broken); err == nil {
|
||||
t.Error("truncated payload must fail")
|
||||
}
|
||||
huge := append([]byte(nil), kaguyaTable...)
|
||||
binary.LittleEndian.PutUint32(huge[4:], 1<<30)
|
||||
binary.LittleEndian.PutUint32(huge[8:], 1<<30)
|
||||
if _, err := Parse(huge); err == nil {
|
||||
t.Error("oversized header must fail before allocating")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsNonFiniteHeader(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
angleStep, ref float64
|
||||
}{
|
||||
{name: "NaN angle step", angleStep: math.NaN(), ref: 1737.4},
|
||||
{name: "Inf angle step", angleStep: math.Inf(1), ref: 1737.4},
|
||||
{name: "NaN reference radius", angleStep: 180, ref: math.NaN()},
|
||||
{name: "zero reference radius", angleStep: 180, ref: 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := Parse(syntheticTable(c.angleStep, c.ref)); err == nil {
|
||||
t.Errorf("%s: Parse err = nil, want an error", c.name)
|
||||
}
|
||||
}
|
||||
if _, err := Parse(syntheticTable(180, 1737.4)); err != nil {
|
||||
t.Fatalf("valid header: Parse err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// syntheticTable 最小合法 2×2×2 LLB1 表,仅表头两项可变。
|
||||
func syntheticTable(angleStep, referenceRadiusKM float64) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("LLB1")
|
||||
for _, count := range []uint32{2, 2, 2} {
|
||||
binary.Write(&buf, binary.LittleEndian, count)
|
||||
}
|
||||
for _, value := range []float64{0, 1, 0, 1, angleStep, referenceRadiusKM, 1} {
|
||||
binary.Write(&buf, binary.LittleEndian, math.Float64bits(value))
|
||||
}
|
||||
writer := zlib.NewWriter(&buf)
|
||||
writer.Write(make([]byte, 2*8))
|
||||
writer.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Package limbtable decodes lunar limb profile tables without embedding a dataset.
|
||||
package limbtable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
const HeaderSize = 4 + 3*4 + 7*8
|
||||
const maxCells = 1 << 24
|
||||
|
||||
// Profile is a lunar limb profile table.
|
||||
type Profile struct {
|
||||
lonCount, latCount, angleCount int
|
||||
lonStart, lonStep float64
|
||||
latStart, latStep float64
|
||||
angleStep float64
|
||||
referenceRadiusKM float64
|
||||
minHeight, maxHeight float64
|
||||
heights []float32
|
||||
}
|
||||
|
||||
// Parse decodes an LLB1 profile table.
|
||||
func Parse(data []byte) (*Profile, error) {
|
||||
if len(data) < HeaderSize || string(data[:4]) != "LLB1" {
|
||||
return nil, errors.New("limbtable: not an LLB1 table")
|
||||
}
|
||||
le := binary.LittleEndian
|
||||
counts := [3]int{}
|
||||
for index := range counts {
|
||||
counts[index] = int(le.Uint32(data[4+4*index:]))
|
||||
}
|
||||
values := [7]float64{}
|
||||
for index := range values {
|
||||
values[index] = math.Float64frombits(le.Uint64(data[16+8*index:]))
|
||||
}
|
||||
profile := &Profile{
|
||||
lonCount: counts[0], latCount: counts[1], angleCount: counts[2],
|
||||
lonStart: values[0], lonStep: values[1],
|
||||
latStart: values[2], latStep: values[3],
|
||||
angleStep: values[4], referenceRadiusKM: values[5],
|
||||
}
|
||||
quantum := values[6]
|
||||
finite := true
|
||||
for _, value := range values {
|
||||
finite = finite && isFinite(value)
|
||||
}
|
||||
if !finite || profile.lonCount < 2 || profile.latCount < 2 || profile.angleCount < 2 ||
|
||||
!(profile.lonStep > 0) || !(profile.latStep > 0) || !(quantum > 0) || !(profile.referenceRadiusKM > 0) ||
|
||||
math.Abs(profile.angleStep*float64(profile.angleCount)-360) > 1e-9 ||
|
||||
profile.lonCount > maxCells/profile.latCount/profile.angleCount {
|
||||
return nil, errors.New("limbtable: invalid LLB1 header")
|
||||
}
|
||||
|
||||
reader, err := zlib.NewReader(bytes.NewReader(data[HeaderSize:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
total := profile.lonCount * profile.latCount * profile.angleCount
|
||||
raw := make([]byte, 2*total)
|
||||
if _, err := io.ReadFull(reader, raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := profile.lonCount * profile.angleCount
|
||||
levels := make([]int32, total)
|
||||
for index := range levels {
|
||||
levels[index] = int32(int16(le.Uint16(raw[2*index:])))
|
||||
}
|
||||
for index := row; index < total; index++ {
|
||||
levels[index] += levels[index-row]
|
||||
}
|
||||
for lat := 0; lat < profile.latCount; lat++ {
|
||||
base := lat * row
|
||||
for index := base + profile.angleCount; index < base+row; index++ {
|
||||
levels[index] += levels[index-profile.angleCount]
|
||||
}
|
||||
}
|
||||
profile.heights = make([]float32, total)
|
||||
profile.minHeight, profile.maxHeight = math.Inf(1), math.Inf(-1)
|
||||
for index, level := range levels {
|
||||
height := float32(float64(level) * quantum)
|
||||
profile.heights[index] = height
|
||||
profile.minHeight = math.Min(profile.minHeight, float64(height))
|
||||
profile.maxHeight = math.Max(profile.maxHeight, float64(height))
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// LimbRadiusKM returns the Moon-centre to limb-point distance in kilometres.
|
||||
func (profile *Profile) LimbRadiusKM(librationLongitude, librationLatitude, positionAngle float64) (float64, bool) {
|
||||
if profile == nil || len(profile.heights) == 0 ||
|
||||
!isFinite(librationLongitude) || !isFinite(librationLatitude) || !isFinite(positionAngle) {
|
||||
return 0, false
|
||||
}
|
||||
lon, lonWeight := profile.cell(librationLongitude, profile.lonStart, profile.lonStep, profile.lonCount)
|
||||
lat, latWeight := profile.cell(librationLatitude, profile.latStart, profile.latStep, profile.latCount)
|
||||
angle := math.Mod(positionAngle/profile.angleStep, float64(profile.angleCount))
|
||||
if angle < 0 {
|
||||
angle += float64(profile.angleCount)
|
||||
}
|
||||
angle0 := int(angle)
|
||||
if angle0 >= profile.angleCount {
|
||||
angle0 = profile.angleCount - 1
|
||||
}
|
||||
angleWeight := angle - float64(angle0)
|
||||
angle1 := (angle0 + 1) % profile.angleCount
|
||||
height := 0.0
|
||||
for dLat, wLat := range [2]float64{1 - latWeight, latWeight} {
|
||||
for dLon, wLon := range [2]float64{1 - lonWeight, lonWeight} {
|
||||
base := ((lat+dLat)*profile.lonCount + lon + dLon) * profile.angleCount
|
||||
height += wLat * wLon * ((1-angleWeight)*float64(profile.heights[base+angle0]) +
|
||||
angleWeight*float64(profile.heights[base+angle1]))
|
||||
}
|
||||
}
|
||||
return profile.referenceRadiusKM + height/1000, true
|
||||
}
|
||||
|
||||
// AngleStepDeg 位置角网格步长(度) / position-angle grid step in degrees.
|
||||
func (profile *Profile) AngleStepDeg() float64 {
|
||||
if profile == nil {
|
||||
return 0
|
||||
}
|
||||
return profile.angleStep
|
||||
}
|
||||
|
||||
// RadiusRangeKM 表内可能取到的最小、最大月缘半径(km) / smallest and largest limb radius.
|
||||
func (profile *Profile) RadiusRangeKM() (float64, float64) {
|
||||
if profile == nil || len(profile.heights) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return profile.referenceRadiusKM + profile.minHeight/1000, profile.referenceRadiusKM + profile.maxHeight/1000
|
||||
}
|
||||
|
||||
func (profile *Profile) cell(value, start, step float64, count int) (int, float64) {
|
||||
position := (value - start) / step
|
||||
if position <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
if position >= float64(count-1) {
|
||||
return count - 2, 1
|
||||
}
|
||||
index := int(position)
|
||||
return index, position - float64(index)
|
||||
}
|
||||
|
||||
func isFinite(value float64) bool {
|
||||
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
+16
-16
@@ -15,43 +15,43 @@ type MaximumDeclinationInfo struct {
|
||||
}
|
||||
|
||||
// MaximumNorthDeclinationsInMonth 指定年月内的所有月球最大北赤纬事件 / all maximum northern lunar declination events in the given Gregorian month.
|
||||
func MaximumNorthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfos(basic.MoonMaximumNorthDeclinations(year, month))
|
||||
func (module *Moon) MaximumNorthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfos(basic.MoonMaximumNorthDeclinationsWithInstance(year, month, module.inst))
|
||||
}
|
||||
|
||||
// MaximumSouthDeclinationsInMonth 指定年月内的所有月球最大南赤纬事件 / all maximum southern lunar declination events in the given Gregorian month.
|
||||
func MaximumSouthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfos(basic.MoonMaximumSouthDeclinations(year, month))
|
||||
func (module *Moon) MaximumSouthDeclinationsInMonth(year int, month time.Month) []MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfos(basic.MoonMaximumSouthDeclinationsWithInstance(year, month, module.inst))
|
||||
}
|
||||
|
||||
// LastMaximumNorthDeclination 指定时刻之前最近一次月球最大北赤纬 / last maximum northern lunar declination at or before date.
|
||||
func LastMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.LastMoonMaximumNorthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) LastMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.LastMoonMaximumNorthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
// NextMaximumNorthDeclination 指定时刻之后最近一次月球最大北赤纬 / next maximum northern lunar declination after date.
|
||||
func NextMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.NextMoonMaximumNorthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) NextMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.NextMoonMaximumNorthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
// ClosestMaximumNorthDeclination 离指定时刻最近一次月球最大北赤纬 / closest maximum northern lunar declination to date.
|
||||
func ClosestMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.ClosestMoonMaximumNorthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) ClosestMaximumNorthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.ClosestMoonMaximumNorthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
// LastMaximumSouthDeclination 指定时刻之前最近一次月球最大南赤纬 / last maximum southern lunar declination at or before date.
|
||||
func LastMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.LastMoonMaximumSouthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) LastMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.LastMoonMaximumSouthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
// NextMaximumSouthDeclination 指定时刻之后最近一次月球最大南赤纬 / next maximum southern lunar declination after date.
|
||||
func NextMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.NextMoonMaximumSouthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) NextMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.NextMoonMaximumSouthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
// ClosestMaximumSouthDeclination 离指定时刻最近一次月球最大南赤纬 / closest maximum southern lunar declination to date.
|
||||
func ClosestMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.ClosestMoonMaximumSouthDeclination(timeToUTJDE(date)))
|
||||
func (module *Moon) ClosestMaximumSouthDeclination(date time.Time) MaximumDeclinationInfo {
|
||||
return convertMaximumDeclinationInfo(date, basic.ClosestMoonMaximumSouthDeclinationWithInstance(timeToUTJDE(date), module.inst))
|
||||
}
|
||||
|
||||
func convertMaximumDeclinationInfos(events []basic.DeclinationEvent) []MaximumDeclinationInfo {
|
||||
|
||||
+173
-268
@@ -1,13 +1,13 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"b612.me/astro/internal/civiltime"
|
||||
"b612.me/astro/tools"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/civiltime"
|
||||
"b612.me/astro/tools"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,194 +36,131 @@ func riseSetResult(date time.Time, jd float64, err error) (time.Time, error) {
|
||||
}
|
||||
|
||||
// TrueLo 月亮真黄经 / true ecliptic longitude.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心真黄经,单位度。
|
||||
// Returns the Moon's geocentric true ecliptic longitude at the instant represented by date, in degrees.
|
||||
func TrueLo(date time.Time) float64 {
|
||||
func (module *Moon) TrueLo(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonTrueLo(basic.UTC2TT(jd))
|
||||
return basic.HMoonTrueLoWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// TrueLoN 截断项月亮真黄经 / truncated true ecliptic longitude.
|
||||
//
|
||||
// 参数与 TrueLo 相同;n<0 使用当前仓库内嵌的全部 ELP 项,其余值用于截断月球级数。
|
||||
// Uses the same inputs as TrueLo. n<0 keeps all embedded ELP terms in this repository; other values truncate the lunar series.
|
||||
func TrueLoN(date time.Time, n int) float64 {
|
||||
// TrueLoN 月亮真黄经(n 变体) / n-parameterised true ecliptic longitude.
|
||||
func (module *Moon) TrueLoN(date time.Time, n int) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonTrueLoN(basic.UTC2TT(jd), n)
|
||||
return basic.HMoonTrueLoNWithInstance(basic.UTC2TT(jd), n, module.inst)
|
||||
}
|
||||
|
||||
// TrueBo 月亮真黄纬 / true ecliptic latitude.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心真黄纬,单位度。
|
||||
// Returns the Moon's geocentric true ecliptic latitude at the instant represented by date, in degrees.
|
||||
func TrueBo(date time.Time) float64 {
|
||||
func (module *Moon) TrueBo(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonTrueBo(basic.UTC2TT(jd))
|
||||
return basic.HMoonTrueBoWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// TrueBoN 截断项月亮真黄纬 / truncated true ecliptic latitude.
|
||||
//
|
||||
// 参数与 TrueBo 相同;n<0 使用当前仓库内嵌的全部 ELP 项,其余值用于截断月球级数。
|
||||
// Uses the same inputs as TrueBo. n<0 keeps all embedded ELP terms in this repository; other values truncate the lunar series.
|
||||
func TrueBoN(date time.Time, n int) float64 {
|
||||
// TrueBoN 月亮真黄纬(n 变体) / n-parameterised true ecliptic latitude.
|
||||
func (module *Moon) TrueBoN(date time.Time, n int) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonTrueBoN(basic.UTC2TT(jd), n)
|
||||
return basic.HMoonTrueBoNWithInstance(basic.UTC2TT(jd), n, module.inst)
|
||||
}
|
||||
|
||||
// ApparentLo 月亮地心视黄经 / apparent geocentric ecliptic longitude.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心视黄经,单位度。
|
||||
// Returns the Moon's apparent geocentric ecliptic longitude at the instant represented by date, in degrees.
|
||||
func ApparentLo(date time.Time) float64 {
|
||||
func (module *Moon) ApparentLo(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonApparentLo(basic.UTC2TT(jd))
|
||||
return basic.HMoonApparentLoWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// TrueRa 月亮地心真赤经 / true geocentric right ascension.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心真赤经,单位度。
|
||||
// Returns the Moon's geocentric true right ascension at the instant represented by date, in degrees.
|
||||
func TrueRa(date time.Time) float64 {
|
||||
func (module *Moon) TrueRa(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricTrueRa(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricTrueRaWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// TrueDec 月亮地心真赤纬 / true geocentric declination.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心真赤纬,单位度。
|
||||
// Returns the Moon's geocentric true declination at the instant represented by date, in degrees.
|
||||
func TrueDec(date time.Time) float64 {
|
||||
func (module *Moon) TrueDec(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricTrueDec(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricTrueDecWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// TrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心真赤经与真赤纬,单位度。
|
||||
// Returns the Moon's geocentric true right ascension and declination at the instant represented by date, in degrees.
|
||||
func TrueRaDec(date time.Time) (float64, float64) {
|
||||
func (module *Moon) TrueRaDec(date time.Time) (float64, float64) {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricTrueRaDec(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricTrueRaDecWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// GeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心视赤经,单位度。
|
||||
// Returns the Moon's apparent geocentric right ascension at the instant represented by date, in degrees.
|
||||
func GeocentricApparentRa(date time.Time) float64 {
|
||||
func (module *Moon) GeocentricApparentRa(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricApparentRa(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricApparentRaWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// GeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心视赤纬,单位度。
|
||||
// Returns the Moon's apparent geocentric declination at the instant represented by date, in degrees.
|
||||
func GeocentricApparentDec(date time.Time) float64 {
|
||||
func (module *Moon) GeocentricApparentDec(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricApparentDec(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricApparentDecWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// GeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的地心视赤经与视赤纬,单位度。
|
||||
// Returns the Moon's apparent geocentric right ascension and declination at the instant represented by date, in degrees.
|
||||
func GeocentricApparentRaDec(date time.Time) (float64, float64) {
|
||||
func (module *Moon) GeocentricApparentRaDec(date time.Time) (float64, float64) {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.HMoonGeocentricApparentRaDec(basic.UTC2TT(jd))
|
||||
return basic.HMoonGeocentricApparentRaDecWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// ApparentRa 月亮站心视赤经 / apparent topocentric right ascension.
|
||||
//
|
||||
// date 为观测时刻,会读取其时区参与地方时计算;lon/lat 为观测者经纬度,东正西负、北正南负;返回值单位度。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon/lat are east-positive and north-positive; the result is in degrees.
|
||||
func ApparentRa(date time.Time, lon, lat float64) float64 {
|
||||
func (module *Moon) ApparentRa(date time.Time, lon, lat float64) float64 {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.HMoonApparentRa(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.HMoonApparentRaWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// ApparentDec 月亮站心视赤纬 / apparent topocentric declination.
|
||||
//
|
||||
// 参数与 ApparentRa 相同,返回月亮站心视赤纬,单位度。
|
||||
// Uses the same inputs as ApparentRa and returns the Moon's apparent topocentric declination in degrees.
|
||||
func ApparentDec(date time.Time, lon, lat float64) float64 {
|
||||
func (module *Moon) ApparentDec(date time.Time, lon, lat float64) float64 {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.HMoonApparentDec(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.HMoonApparentDecWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// ApparentRaDec 月亮站心视赤经、视赤纬 / apparent topocentric right ascension and declination.
|
||||
//
|
||||
// 参数与 ApparentRa 相同,返回月亮站心视赤经与视赤纬,单位度。
|
||||
// Uses the same inputs as ApparentRa and returns the Moon's apparent topocentric right ascension and declination in degrees.
|
||||
func ApparentRaDec(date time.Time, lon, lat float64) (float64, float64) {
|
||||
func (module *Moon) ApparentRaDec(date time.Time, lon, lat float64) (float64, float64) {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.HMoonApparentRaDec(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.HMoonApparentRaDecWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// HourAngle 月亮时角 / hour angle.
|
||||
//
|
||||
// date 为观测时刻,会读取其时区参与地方时计算;lon/lat 为观测者经纬度,东正西负、北正南负;返回值单位度。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon/lat are east-positive and north-positive; the result is in degrees.
|
||||
func HourAngle(date time.Time, lon, lat float64) float64 {
|
||||
func (module *Moon) HourAngle(date time.Time, lon, lat float64) float64 {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.MoonTimeAngle(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.MoonTimeAngleWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// Azimuth 月亮方位角 / azimuth.
|
||||
//
|
||||
// date 为观测时刻,会读取其时区参与地方时计算;lon/lat 为观测者经纬度,东正西负、北正南负;返回值按正北为 0°、向东增加。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon/lat are east-positive and north-positive; azimuth is measured from north toward east.
|
||||
func Azimuth(date time.Time, lon, lat float64) float64 {
|
||||
func (module *Moon) Azimuth(date time.Time, lon, lat float64) float64 {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.HMoonAzimuth(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.HMoonAzimuthWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// Altitude 月亮高度角 / lunar altitude.
|
||||
//
|
||||
// date 为观测时刻,会读取其时区参与地方时计算;lon/lat 为观测者经纬度,东正西负、北正南负;返回值单位度。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon/lat are east-positive and north-positive; the result is in degrees.
|
||||
func Altitude(date time.Time, lon, lat float64) float64 {
|
||||
func (module *Moon) Altitude(date time.Time, lon, lat float64) float64 {
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
return basic.HMoonHeight(localJD, lon, lat, float64(loc)/3600.0)
|
||||
return basic.HMoonHeightWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst)
|
||||
}
|
||||
|
||||
// Zenith 月亮天顶距 / lunar zenith distance.
|
||||
//
|
||||
// 参数与 Altitude 相同,返回值为对应时刻的天顶距,单位度。
|
||||
// Uses the same inputs as Altitude and returns the zenith distance in degrees.
|
||||
func Zenith(date time.Time, lon, lat float64) float64 {
|
||||
return 90 - Altitude(date, lon, lat)
|
||||
func (module *Moon) Zenith(date time.Time, lon, lat float64) float64 {
|
||||
return 90 - module.Altitude(date, lon, lat)
|
||||
}
|
||||
|
||||
// CulminationTime 月亮中天时刻 / culmination time.
|
||||
//
|
||||
// date 取其所在时区的当地日期,返回值保持相同时区;lon/lat 为观测者经纬度,东正西负、北正南负。
|
||||
// date is interpreted on its local civil day and the result keeps the same time zone. lon/lat are east-positive and north-positive.
|
||||
func CulminationTime(date time.Time, lon, lat float64) time.Time {
|
||||
func (module *Moon) CulminationTime(date time.Time, lon, lat float64) time.Time {
|
||||
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
calcJD := basic.MoonCulminationTime(localJD, lon, lat, float64(loc)/3600.0) - float64(loc)/86400
|
||||
calcJD := basic.MoonCulminationTimeWithInstance(localJD, lon, lat, float64(loc)/3600.0, module.inst) - float64(loc)/86400
|
||||
return basic.JD2DateByZone(calcJD, date.Location(), false)
|
||||
}
|
||||
|
||||
// RiseTime 月出时刻 / moonrise time.
|
||||
//
|
||||
// date 取其所在时区的当地日期,返回值保持相同时区;lon/lat 为观测者经纬度,东正西负、北正南负;
|
||||
// height 为椭球高(大地高),单位米;aero 为 true 时按动态标准大气折射和实时月球视半径计算上缘过地平线。
|
||||
// date is interpreted on its local civil day and the result keeps the same time zone. lon/lat are east-positive and north-positive;
|
||||
// height 为观测者椭球高(大地高),单位米;aero 使用动态标准折射和实时月球视半径计算上缘过地平线。
|
||||
// height is observer elevation in meters; aero uses dynamic standard refraction and the instantaneous lunar semidiameter for an upper-limb crossing.
|
||||
func RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
func (module *Moon) RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
if result, err, handled := civiltime.Event(date, ERR_NOT_TODAY, func(d time.Time) (time.Time, error) {
|
||||
return RiseTime(d, lon, lat, height, aero)
|
||||
return module.RiseTime(d, lon, lat, height, aero)
|
||||
}); handled {
|
||||
return result, err
|
||||
}
|
||||
@@ -235,27 +172,19 @@ func RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, e
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
}
|
||||
riseJD, err := basic.GetMoonRiseTime(localJD, lon, lat, timezone, aeroFloat, height)
|
||||
riseJD, err := basic.GetMoonRiseTimeWithInstance(localJD, lon, lat, timezone, aeroFloat, height, module.inst)
|
||||
return riseSetResult(date, riseJD, err)
|
||||
}
|
||||
|
||||
// DownTime 月落时刻别名 / deprecated moonset alias.
|
||||
//
|
||||
// Deprecated: use SetTime instead.
|
||||
//
|
||||
// 参数与 SetTime 相同,仅为兼容旧接口保留。
|
||||
// Same as SetTime and kept only for backward compatibility.
|
||||
func DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return SetTime(date, lon, lat, height, aero)
|
||||
func (module *Moon) DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return module.SetTime(date, lon, lat, height, aero)
|
||||
}
|
||||
|
||||
// SetTime 月落时刻 / moonset time.
|
||||
//
|
||||
// 参数与 RiseTime 相同,返回给定当地日期内的月落时刻。
|
||||
// Uses the same inputs as RiseTime and returns the moonset time on the corresponding local civil day.
|
||||
func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
func (module *Moon) SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
if result, err, handled := civiltime.Event(date, ERR_NOT_TODAY, func(d time.Time) (time.Time, error) {
|
||||
return SetTime(d, lon, lat, height, aero)
|
||||
return module.SetTime(d, lon, lat, height, aero)
|
||||
}); handled {
|
||||
return result, err
|
||||
}
|
||||
@@ -267,27 +196,21 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
}
|
||||
downJD, err := basic.GetMoonSetTime(localJD, lon, lat, timezone, aeroFloat, height)
|
||||
downJD, err := basic.GetMoonSetTimeWithInstance(localJD, lon, lat, timezone, aeroFloat, height, module.inst)
|
||||
return riseSetResult(date, downJD, err)
|
||||
}
|
||||
|
||||
// SunMoonLoDiff 日月黄经差 / Moon-Sun longitude difference.
|
||||
//
|
||||
// 返回月亮视黄经减去太阳视黄经的结果,单位度,取值范围 [0, 360);新月附近接近 0°,满月附近接近 180°。
|
||||
// Returns apparent lunar longitude minus apparent solar longitude in degrees, normalized to [0, 360). It is near 0° at new moon and near 180° at full moon.
|
||||
func SunMoonLoDiff(date time.Time) float64 {
|
||||
func (module *Moon) SunMoonLoDiff(date time.Time) float64 {
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
sunLo := basic.HSunApparentLo(jde)
|
||||
moonLo := basic.HMoonApparentLo(jde)
|
||||
sunLo := basic.HSunApparentLoWithInstance(jde, module.inst)
|
||||
moonLo := basic.HMoonApparentLoWithInstance(jde, module.inst)
|
||||
return tools.Limit360(moonLo - sunLo)
|
||||
}
|
||||
|
||||
// PhaseDesc 月相文字描述 / textual lunar phase description.
|
||||
//
|
||||
// 基于 SunMoonLoDiff 的分段结果返回中文月相名称。
|
||||
// Returns a Chinese phase name derived from the segmented Moon-Sun longitude difference.
|
||||
func PhaseDesc(date time.Time) string {
|
||||
moonSunLoDiff := SunMoonLoDiff(date)
|
||||
func (module *Moon) PhaseDesc(date time.Time) string {
|
||||
moonSunLoDiff := module.SunMoonLoDiff(date)
|
||||
if moonSunLoDiff >= 0 && moonSunLoDiff <= 30 {
|
||||
return "新月"
|
||||
} else if moonSunLoDiff > 30 && moonSunLoDiff <= 75 {
|
||||
@@ -310,77 +233,72 @@ func PhaseDesc(date time.Time) string {
|
||||
}
|
||||
|
||||
// Phase 月面受照比例 / illuminated fraction.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻的受照比例,范围 [0, 1]。
|
||||
// Returns the Moon's illuminated fraction at the instant represented by date, in the range [0, 1].
|
||||
func Phase(date time.Time) float64 {
|
||||
func (module *Moon) Phase(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
return basic.MoonPhase(basic.UTC2TT(jd))
|
||||
return basic.MoonPhaseWithInstance(basic.UTC2TT(jd), module.inst)
|
||||
}
|
||||
|
||||
// ShuoYue 朔月锚点解 / new-moon solution near a decimal year anchor.
|
||||
//
|
||||
// year 为公历小数年锚点,例如 2025.0 或 2025.5;返回以该锚点求得的一次朔月时刻,结果为 UTC。
|
||||
// year is a decimal Gregorian-year anchor such as 2025.0 or 2025.5. The returned time is one new moon solved near that anchor, in UTC.
|
||||
func ShuoYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonSH(year, 0))
|
||||
func (module *Moon) ShuoYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonSHWithInstance(year, 0, module.inst))
|
||||
return basic.JD2DateByZone(jd, time.UTC, false)
|
||||
}
|
||||
|
||||
// NextShuoYue 下一次朔月 / next new moon.
|
||||
//
|
||||
// 返回 date 之后最近一次朔月时刻,结果保持 date 的时区。
|
||||
// Returns the next new moon after date, keeping date's time zone.
|
||||
func NextShuoYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 0)
|
||||
func (module *Moon) NextShuoYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 0, module)
|
||||
}
|
||||
|
||||
// LastShuoYue 上一次朔月 / previous new moon.
|
||||
//
|
||||
// 返回 date 之前最近一次朔月时刻,结果保持 date 的时区。
|
||||
// Returns the previous new moon before date, keeping date's time zone.
|
||||
func LastShuoYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 0)
|
||||
func (module *Moon) LastShuoYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 0, module)
|
||||
}
|
||||
|
||||
// ClosestShuoYue 最近朔月 / closest new moon.
|
||||
//
|
||||
// 返回离 date 最近的朔月时刻,结果保持 date 的时区。
|
||||
// Returns the new moon nearest to date, keeping date's time zone.
|
||||
func ClosestShuoYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 0)
|
||||
func (module *Moon) ClosestShuoYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 0, module)
|
||||
}
|
||||
|
||||
// NewMoon 朔月英文别名 / English alias for ShuoYue.
|
||||
func NewMoon(year float64) time.Time {
|
||||
return ShuoYue(year)
|
||||
func (module *Moon) NewMoon(year float64) time.Time {
|
||||
return module.ShuoYue(year)
|
||||
}
|
||||
|
||||
// NextNewMoon 下一次朔月英文别名 / English alias for NextShuoYue.
|
||||
func NextNewMoon(date time.Time) time.Time {
|
||||
return NextShuoYue(date)
|
||||
func (module *Moon) NextNewMoon(date time.Time) time.Time {
|
||||
return module.NextShuoYue(date)
|
||||
}
|
||||
|
||||
// LastNewMoon 上一次朔月英文别名 / English alias for LastShuoYue.
|
||||
func LastNewMoon(date time.Time) time.Time {
|
||||
return LastShuoYue(date)
|
||||
func (module *Moon) LastNewMoon(date time.Time) time.Time {
|
||||
return module.LastShuoYue(date)
|
||||
}
|
||||
|
||||
// ClosestNewMoon 最近朔月英文别名 / English alias for ClosestShuoYue.
|
||||
func ClosestNewMoon(date time.Time) time.Time {
|
||||
return ClosestShuoYue(date)
|
||||
func (module *Moon) ClosestNewMoon(date time.Time) time.Time {
|
||||
return module.ClosestShuoYue(date)
|
||||
}
|
||||
|
||||
func closestMoonPhase(date time.Time, typed int) time.Time {
|
||||
func closestMoonPhase(date time.Time, typed int, modules ...*Moon) time.Time {
|
||||
module := &defaultModule
|
||||
if len(modules) > 0 {
|
||||
module = modules[0]
|
||||
}
|
||||
|
||||
//0=shuo 1=wang 2=shangxian 3=xiaxian
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
if typed < 2 {
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDE(jde, typed)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDEWithInstance(jde, typed, module.inst)), date.Location(), false)
|
||||
}
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDE(jde, typed-2)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDEWithInstance(jde, typed-2, module.inst)), date.Location(), false)
|
||||
}
|
||||
|
||||
func nextMoonPhase(date time.Time, typed int) time.Time {
|
||||
func nextMoonPhase(date time.Time, typed int, modules ...*Moon) time.Time {
|
||||
module := &defaultModule
|
||||
if len(modules) > 0 {
|
||||
module = modules[0]
|
||||
}
|
||||
|
||||
//0=shuo 1=wang 2=shangxian 3=xiaxian
|
||||
diffCode := 0.00
|
||||
switch typed {
|
||||
@@ -392,7 +310,7 @@ func nextMoonPhase(date time.Time, typed int) time.Time {
|
||||
diffCode = 270
|
||||
}
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
cost := basic.HMoonApparentLo(jde) - basic.HSunApparentLo(jde) - float64(diffCode)
|
||||
cost := basic.HMoonApparentLoWithInstance(jde, module.inst) - basic.HSunApparentLoWithInstance(jde, module.inst) - float64(diffCode)
|
||||
for cost < 0 {
|
||||
cost += 360
|
||||
}
|
||||
@@ -403,12 +321,17 @@ func nextMoonPhase(date time.Time, typed int) time.Time {
|
||||
jde += (240 - cost) / 11.19
|
||||
}
|
||||
if typed < 2 {
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDE(jde, typed)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDEWithInstance(jde, typed, module.inst)), date.Location(), false)
|
||||
}
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDE(jde, typed-2)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDEWithInstance(jde, typed-2, module.inst)), date.Location(), false)
|
||||
}
|
||||
|
||||
func lastMoonPhase(date time.Time, typed int) time.Time {
|
||||
func lastMoonPhase(date time.Time, typed int, modules ...*Moon) time.Time {
|
||||
module := &defaultModule
|
||||
if len(modules) > 0 {
|
||||
module = modules[0]
|
||||
}
|
||||
|
||||
//0=shuo 1=wang 2=shangxian 3=xiaxian
|
||||
diffCode := 0.00
|
||||
switch typed {
|
||||
@@ -420,7 +343,7 @@ func lastMoonPhase(date time.Time, typed int) time.Time {
|
||||
diffCode = 270
|
||||
}
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
cost := basic.HMoonApparentLo(jde) - basic.HSunApparentLo(jde) - float64(diffCode)
|
||||
cost := basic.HMoonApparentLoWithInstance(jde, module.inst) - basic.HSunApparentLoWithInstance(jde, module.inst) - float64(diffCode)
|
||||
for cost < 0 {
|
||||
cost += 360
|
||||
}
|
||||
@@ -431,176 +354,158 @@ func lastMoonPhase(date time.Time, typed int) time.Time {
|
||||
jde -= (cost - 120) / 11.19
|
||||
}
|
||||
if typed < 2 {
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDE(jde, typed)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonSHByJDEWithInstance(jde, typed, module.inst)), date.Location(), false)
|
||||
}
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDE(jde, typed-2)), date.Location(), false)
|
||||
return basic.JD2DateByZone(basic.TT2UTC(basic.CalcMoonXHByJDEWithInstance(jde, typed-2, module.inst)), date.Location(), false)
|
||||
}
|
||||
|
||||
// WangYue 望月锚点解 / full-moon solution near a decimal year anchor.
|
||||
//
|
||||
// year 为公历小数年锚点,例如 2025.0 或 2025.5;返回以该锚点求得的一次望月时刻,结果为 UTC。
|
||||
// year is a decimal Gregorian-year anchor such as 2025.0 or 2025.5. The returned time is one full moon solved near that anchor, in UTC.
|
||||
func WangYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonSH(year, 1))
|
||||
func (module *Moon) WangYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonSHWithInstance(year, 1, module.inst))
|
||||
return basic.JD2DateByZone(jd, time.UTC, false)
|
||||
}
|
||||
|
||||
// NextWangYue 下一次望月 / next full moon.
|
||||
//
|
||||
// 返回 date 之后最近一次望月时刻,结果保持 date 的时区。
|
||||
// Returns the next full moon after date, keeping date's time zone.
|
||||
func NextWangYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 1)
|
||||
func (module *Moon) NextWangYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 1, module)
|
||||
}
|
||||
|
||||
// LastWangYue 上一次望月 / previous full moon.
|
||||
//
|
||||
// 返回 date 之前最近一次望月时刻,结果保持 date 的时区。
|
||||
// Returns the previous full moon before date, keeping date's time zone.
|
||||
func LastWangYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 1)
|
||||
func (module *Moon) LastWangYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 1, module)
|
||||
}
|
||||
|
||||
// ClosestWangYue 最近望月 / closest full moon.
|
||||
//
|
||||
// 返回离 date 最近的望月时刻,结果保持 date 的时区。
|
||||
// Returns the full moon nearest to date, keeping date's time zone.
|
||||
func ClosestWangYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 1)
|
||||
func (module *Moon) ClosestWangYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 1, module)
|
||||
}
|
||||
|
||||
// FullMoon 望月英文别名 / English alias for WangYue.
|
||||
func FullMoon(year float64) time.Time {
|
||||
return WangYue(year)
|
||||
func (module *Moon) FullMoon(year float64) time.Time {
|
||||
return module.WangYue(year)
|
||||
}
|
||||
|
||||
// NextFullMoon 下一次望月英文别名 / English alias for NextWangYue.
|
||||
func NextFullMoon(date time.Time) time.Time {
|
||||
return NextWangYue(date)
|
||||
func (module *Moon) NextFullMoon(date time.Time) time.Time {
|
||||
return module.NextWangYue(date)
|
||||
}
|
||||
|
||||
// LastFullMoon 上一次望月英文别名 / English alias for LastWangYue.
|
||||
func LastFullMoon(date time.Time) time.Time {
|
||||
return LastWangYue(date)
|
||||
func (module *Moon) LastFullMoon(date time.Time) time.Time {
|
||||
return module.LastWangYue(date)
|
||||
}
|
||||
|
||||
// ClosestFullMoon 最近望月英文别名 / English alias for ClosestWangYue.
|
||||
func ClosestFullMoon(date time.Time) time.Time {
|
||||
return ClosestWangYue(date)
|
||||
func (module *Moon) ClosestFullMoon(date time.Time) time.Time {
|
||||
return module.ClosestWangYue(date)
|
||||
}
|
||||
|
||||
// ShangXianYue 上弦锚点解 / first-quarter solution near a decimal year anchor.
|
||||
//
|
||||
// year 为公历小数年锚点,例如 2025.0 或 2025.5;返回以该锚点求得的一次上弦时刻,结果为 UTC。
|
||||
// year is a decimal Gregorian-year anchor such as 2025.0 or 2025.5. The returned time is one first-quarter solution near that anchor, in UTC.
|
||||
func ShangXianYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonXH(year, 0))
|
||||
func (module *Moon) ShangXianYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonXHWithInstance(year, 0, module.inst))
|
||||
return basic.JD2DateByZone(jd, time.UTC, false)
|
||||
}
|
||||
|
||||
// NextShangXianYue 下一次上弦 / next first quarter.
|
||||
//
|
||||
// 返回 date 之后最近一次上弦时刻,结果保持 date 的时区。
|
||||
// Returns the next first quarter after date, keeping date's time zone.
|
||||
func NextShangXianYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 2)
|
||||
func (module *Moon) NextShangXianYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 2, module)
|
||||
}
|
||||
|
||||
// LastShangXianYue 上一次上弦 / previous first quarter.
|
||||
//
|
||||
// 返回 date 之前最近一次上弦时刻,结果保持 date 的时区。
|
||||
// Returns the previous first quarter before date, keeping date's time zone.
|
||||
func LastShangXianYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 2)
|
||||
func (module *Moon) LastShangXianYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 2, module)
|
||||
}
|
||||
|
||||
// ClosestShangXianYue 最近上弦 / closest first quarter.
|
||||
//
|
||||
// 返回离 date 最近的上弦时刻,结果保持 date 的时区。
|
||||
// Returns the first quarter nearest to date, keeping date's time zone.
|
||||
func ClosestShangXianYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 2)
|
||||
func (module *Moon) ClosestShangXianYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 2, module)
|
||||
}
|
||||
|
||||
// FirstQuarter 上弦英文别名 / English alias for ShangXianYue.
|
||||
func FirstQuarter(year float64) time.Time {
|
||||
return ShangXianYue(year)
|
||||
func (module *Moon) FirstQuarter(year float64) time.Time {
|
||||
return module.ShangXianYue(year)
|
||||
}
|
||||
|
||||
// NextFirstQuarter 下一次上弦英文别名 / English alias for NextShangXianYue.
|
||||
func NextFirstQuarter(date time.Time) time.Time {
|
||||
return NextShangXianYue(date)
|
||||
func (module *Moon) NextFirstQuarter(date time.Time) time.Time {
|
||||
return module.NextShangXianYue(date)
|
||||
}
|
||||
|
||||
// LastFirstQuarter 上一次上弦英文别名 / English alias for LastShangXianYue.
|
||||
func LastFirstQuarter(date time.Time) time.Time {
|
||||
return LastShangXianYue(date)
|
||||
func (module *Moon) LastFirstQuarter(date time.Time) time.Time {
|
||||
return module.LastShangXianYue(date)
|
||||
}
|
||||
|
||||
// ClosestFirstQuarter 最近上弦英文别名 / English alias for ClosestShangXianYue.
|
||||
func ClosestFirstQuarter(date time.Time) time.Time {
|
||||
return ClosestShangXianYue(date)
|
||||
func (module *Moon) ClosestFirstQuarter(date time.Time) time.Time {
|
||||
return module.ClosestShangXianYue(date)
|
||||
}
|
||||
|
||||
// XiaXianYue 下弦锚点解 / last-quarter solution near a decimal year anchor.
|
||||
//
|
||||
// year 为公历小数年锚点,例如 2025.0 或 2025.5;返回以该锚点求得的一次下弦时刻,结果为 UTC。
|
||||
// year is a decimal Gregorian-year anchor such as 2025.0 or 2025.5. The returned time is one last-quarter solution near that anchor, in UTC.
|
||||
func XiaXianYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonXH(year, 1))
|
||||
func (module *Moon) XiaXianYue(year float64) time.Time {
|
||||
jd := basic.TT2UTC(basic.CalcMoonXHWithInstance(year, 1, module.inst))
|
||||
return basic.JD2DateByZone(jd, time.UTC, false)
|
||||
}
|
||||
|
||||
// NextXiaXianYue 下一次下弦 / next last quarter.
|
||||
//
|
||||
// 返回 date 之后最近一次下弦时刻,结果保持 date 的时区。
|
||||
// Returns the next last quarter after date, keeping date's time zone.
|
||||
func NextXiaXianYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 3)
|
||||
func (module *Moon) NextXiaXianYue(date time.Time) time.Time {
|
||||
return nextMoonPhase(date, 3, module)
|
||||
}
|
||||
|
||||
// LastXiaXianYue 上一次下弦 / previous last quarter.
|
||||
//
|
||||
// 返回 date 之前最近一次下弦时刻,结果保持 date 的时区。
|
||||
// Returns the previous last quarter before date, keeping date's time zone.
|
||||
func LastXiaXianYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 3)
|
||||
func (module *Moon) LastXiaXianYue(date time.Time) time.Time {
|
||||
return lastMoonPhase(date, 3, module)
|
||||
}
|
||||
|
||||
// ClosestXiaXianYue 最近下弦 / closest last quarter.
|
||||
//
|
||||
// 返回离 date 最近的下弦时刻,结果保持 date 的时区。
|
||||
// Returns the last quarter nearest to date, keeping date's time zone.
|
||||
func ClosestXiaXianYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 3)
|
||||
func (module *Moon) ClosestXiaXianYue(date time.Time) time.Time {
|
||||
return closestMoonPhase(date, 3, module)
|
||||
}
|
||||
|
||||
// LastQuarter 下弦英文别名 / English alias for XiaXianYue.
|
||||
func LastQuarter(year float64) time.Time {
|
||||
return XiaXianYue(year)
|
||||
func (module *Moon) LastQuarter(year float64) time.Time {
|
||||
return module.XiaXianYue(year)
|
||||
}
|
||||
|
||||
// NextLastQuarter 下一次下弦英文别名 / English alias for NextXiaXianYue.
|
||||
func NextLastQuarter(date time.Time) time.Time {
|
||||
return NextXiaXianYue(date)
|
||||
func (module *Moon) NextLastQuarter(date time.Time) time.Time {
|
||||
return module.NextXiaXianYue(date)
|
||||
}
|
||||
|
||||
// LastLastQuarter 上一次下弦英文别名 / English alias for LastXiaXianYue.
|
||||
func LastLastQuarter(date time.Time) time.Time {
|
||||
return LastXiaXianYue(date)
|
||||
func (module *Moon) LastLastQuarter(date time.Time) time.Time {
|
||||
return module.LastXiaXianYue(date)
|
||||
}
|
||||
|
||||
// ClosestLastQuarter 最近下弦英文别名 / English alias for ClosestXiaXianYue.
|
||||
func ClosestLastQuarter(date time.Time) time.Time {
|
||||
return ClosestXiaXianYue(date)
|
||||
func (module *Moon) ClosestLastQuarter(date time.Time) time.Time {
|
||||
return module.ClosestXiaXianYue(date)
|
||||
}
|
||||
|
||||
// EarthDistance 地月距离 / Earth-Moon distance.
|
||||
//
|
||||
// 返回月亮在 date 对应绝对时刻到地球质心的距离,单位千米。
|
||||
// Returns the distance from the Moon to Earth's center at the instant represented by date, in kilometers.
|
||||
func EarthDistance(date time.Time) float64 {
|
||||
jd := basic.Date2JD(date)
|
||||
jde := basic.UTC2TT(jd)
|
||||
return basic.MoonAway(jde)
|
||||
// RiseTimeWithAtmosphere 月出时刻,按给定气压(hPa)与气温(°C)计算大气折射 / moonrise time with explicit pressure and temperature.
|
||||
func (module *Moon) RiseTimeWithAtmosphere(date time.Time, lon, lat, height, pressureHPa, temperatureC float64) (time.Time, error) {
|
||||
if result, err, handled := civiltime.Event(date, ERR_NOT_TODAY, func(d time.Time) (time.Time, error) {
|
||||
return module.RiseTimeWithAtmosphere(d, lon, lat, height, pressureHPa, temperatureC)
|
||||
}); handled {
|
||||
return result, err
|
||||
}
|
||||
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
jd, err := basic.GetMoonRiseTimeWithAtmosphere(localJD, lon, lat, float64(loc)/3600.0, height, pressureHPa, temperatureC, module.inst)
|
||||
return riseSetResult(date, jd, err)
|
||||
}
|
||||
|
||||
// SetTimeWithAtmosphere 月落时刻,按给定气压(hPa)与气温(°C)计算大气折射 / moonset time with explicit pressure and temperature.
|
||||
func (module *Moon) SetTimeWithAtmosphere(date time.Time, lon, lat, height, pressureHPa, temperatureC float64) (time.Time, error) {
|
||||
if result, err, handled := civiltime.Event(date, ERR_NOT_TODAY, func(d time.Time) (time.Time, error) {
|
||||
return module.SetTimeWithAtmosphere(d, lon, lat, height, pressureHPa, temperatureC)
|
||||
}); handled {
|
||||
return result, err
|
||||
}
|
||||
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
localJD := basic.Date2JD(date)
|
||||
_, loc := date.Zone()
|
||||
jd, err := basic.GetMoonSetTimeWithAtmosphere(localJD, lon, lat, float64(loc)/3600.0, height, pressureHPa, temperatureC, module.inst)
|
||||
return riseSetResult(date, jd, err)
|
||||
}
|
||||
|
||||
+10
-10
@@ -7,23 +7,23 @@ import (
|
||||
)
|
||||
|
||||
// AscendingNode 月球升交点黄经 / ascending node longitude of the Moon.
|
||||
func AscendingNode(date time.Time) float64 {
|
||||
return AscendingNodeN(date, -1)
|
||||
func (module *Moon) AscendingNode(date time.Time) float64 {
|
||||
return module.AscendingNodeN(date, -1)
|
||||
}
|
||||
|
||||
// AscendingNodeN 月球升交点黄经(截断版) / truncated ascending node longitude of the Moon.
|
||||
func AscendingNodeN(date time.Time, n int) float64 {
|
||||
// AscendingNodeN 月球升交点黄经(n 变体) / n-parameterised ascending node longitude of the Moon.
|
||||
func (module *Moon) AscendingNodeN(date time.Time, n int) float64 {
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
return basic.MoonAscendingNodeN(jde, n)
|
||||
return basic.MoonAscendingNodeNWithInstance(jde, n, module.inst)
|
||||
}
|
||||
|
||||
// DescendingNode 月球降交点黄经 / descending node longitude of the Moon.
|
||||
func DescendingNode(date time.Time) float64 {
|
||||
return DescendingNodeN(date, -1)
|
||||
func (module *Moon) DescendingNode(date time.Time) float64 {
|
||||
return module.DescendingNodeN(date, -1)
|
||||
}
|
||||
|
||||
// DescendingNodeN 月球降交点黄经(截断版) / truncated descending node longitude of the Moon.
|
||||
func DescendingNodeN(date time.Time, n int) float64 {
|
||||
// DescendingNodeN 月球降交点黄经(n 变体) / n-parameterised descending node longitude of the Moon.
|
||||
func (module *Moon) DescendingNodeN(date time.Time, n int) float64 {
|
||||
jde := basic.UTC2TT(basic.Date2JD(date.UTC()))
|
||||
return basic.MoonDescendingNodeN(jde, n)
|
||||
return basic.MoonDescendingNodeNWithInstance(jde, n, module.inst)
|
||||
}
|
||||
|
||||
+11
-10
@@ -41,9 +41,12 @@ const (
|
||||
// OccultationPartial 表示掩甚时有限目标盘面只有部分被覆盖。
|
||||
// OccultationPartial means only part of a finite target disk is covered at greatest occultation.
|
||||
OccultationPartial = basic.OccultationPartial
|
||||
// OccultationGrazing 表示两边缘相切,且没有正持续时间的重叠。
|
||||
// OccultationGrazing means the limbs are tangent without a positive-duration overlap.
|
||||
// OccultationGrazing 表示掠掩:圆月面下两边缘相切;按月缘剖面求解时星迹始终在月缘起伏带内,可有多段遮挡。
|
||||
// OccultationGrazing is a graze: tangent limbs with a circular Moon, or a track inside the limb relief band with a limb profile.
|
||||
OccultationGrazing = basic.OccultationGrazing
|
||||
// OccultationNone 表示月缘剖面下恒星未被遮挡,只出现在精密剖面结果中。
|
||||
// OccultationNone means the star is never hidden by the limb profile; it only appears in precise profile results.
|
||||
OccultationNone = basic.OccultationNone
|
||||
)
|
||||
|
||||
// OccultationPlanet 标识有限盘面行星目标。
|
||||
@@ -171,14 +174,12 @@ type StarOccultationInstant = basic.StarOccultationInstant
|
||||
// PlanetOccultationInstant contains the visible partial and total planetary footprints at one requested instant.
|
||||
type PlanetOccultationInstant = basic.PlanetOccultationInstant
|
||||
|
||||
// StarOccultationFootprintAt 计算指定时刻的精确恒星月掩可见足迹。
|
||||
// StarOccultationFootprintAt calculates the exact visible stellar-occultation footprint at one instant.
|
||||
func StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultationInstant, error) {
|
||||
return basic.StarOccultationFootprintAt(at, star)
|
||||
// StarOccultationFootprintAt 计算指定时刻的精确恒星月掩可见足迹 / exact stellar occultation footprint at an instant.
|
||||
func (module *Moon) StarOccultationFootprintAt(at time.Time, star StarCoordinate) (StarOccultationInstant, error) {
|
||||
return basic.StarOccultationFootprintAtWithInstance(at, star, module.inst)
|
||||
}
|
||||
|
||||
// PlanetOccultationFootprintsAt 计算指定时刻的精确行星偏掩和全掩可见足迹。
|
||||
// PlanetOccultationFootprintsAt calculates the exact visible partial and total planetary footprints at one instant.
|
||||
func PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (PlanetOccultationInstant, error) {
|
||||
return basic.PlanetOccultationFootprintsAt(at, planet)
|
||||
// PlanetOccultationFootprintsAt 计算指定时刻的精确行星偏掩和全掩可见足迹 / exact planetary occultation footprints at an instant.
|
||||
func (module *Moon) PlanetOccultationFootprintsAt(at time.Time, planet OccultationPlanet) (PlanetOccultationInstant, error) {
|
||||
return basic.PlanetOccultationFootprintsAtWithInstance(at, planet, module.inst)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package moon
|
||||
|
||||
// 外部基准:2017-09-18 轩辕十四掠掩(IOTA 实测 5 段)。内置剖面不保证段数,只钉住主遮挡段与实测最后一段相差不超过 1 s。
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/moon/limb"
|
||||
)
|
||||
|
||||
func TestRegulusGraze20170918MatchesMainObservedInterval(t *testing.T) {
|
||||
regulus := StarCoordinate{
|
||||
ID: "HIP 49669", RA: 152.09358042, Dec: 11.96719519,
|
||||
Epoch: time.Date(1991, time.April, 2, 13, 30, 0, 0, time.UTC), Frame: CoordinateFrameICRS,
|
||||
ProperMotionRACosDecMasPerYear: -248.73, ProperMotionDecMasPerYear: 5.59,
|
||||
ParallaxMas: 41.13, RadialVelocityKmPerSecond: 6,
|
||||
}
|
||||
const lon, lat, height = 22.966666666666667, 38.98208333333333, 85
|
||||
start := time.Date(2017, time.September, 18, 3, 0, 0, 0, time.UTC)
|
||||
events, err := FindStarOccultations(start, start.Add(time.Hour), regulus, lon, lat, height,
|
||||
OccultationSearchOptions{LunarLimb: limb.Default()})
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindStarOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
profile, err := StarOccultationLimbProfile(events[0], regulus, limb.Default(), StarOccultationLimbProfileOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(profile.Hidden) == 0 {
|
||||
t.Fatal("graze reports no hidden interval")
|
||||
}
|
||||
main := profile.Hidden[len(profile.Hidden)-1]
|
||||
for _, pair := range []struct {
|
||||
name string
|
||||
got, observed time.Time
|
||||
}{
|
||||
{"disappearance", main.Start, time.Date(2017, time.September, 18, 3, 29, 48, 0, time.UTC)},
|
||||
{"reappearance", main.End, time.Date(2017, time.September, 18, 3, 30, 2, 950_000_000, time.UTC)},
|
||||
} {
|
||||
if d := pair.got.Sub(pair.observed).Seconds(); math.Abs(d) > 1 {
|
||||
t.Errorf("%s %v is %.2f s from the observed %v", pair.name, pair.got, d, pair.observed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/moon/limb"
|
||||
)
|
||||
|
||||
// 月缘只应让接触时刻移动几秒:既要确实移动,又不能超过 15 s;掩甚与食型不变,模块副本不影响原模块。
|
||||
func TestOccultationLunarLimbShiftsContacts(t *testing.T) {
|
||||
location := time.FixedZone("CST", 8*3600)
|
||||
start := time.Date(2024, time.August, 21, 9, 30, 0, 0, location)
|
||||
end := time.Date(2024, time.August, 21, 12, 0, 0, 0, location)
|
||||
plain := NewWithInstance(nil)
|
||||
limbed := plain.WithLunarLimb(limb.Default())
|
||||
a, err := plain.FindPlanetOccultations(start, end, OccultationSaturn, -30.072, 16.21, 0, OccultationSearchOptions{})
|
||||
if err != nil || len(a) != 1 {
|
||||
t.Fatalf("circular: %v, %d events", err, len(a))
|
||||
}
|
||||
b, err := limbed.FindPlanetOccultations(start, end, OccultationSaturn, -30.072, 16.21, 0, OccultationSearchOptions{})
|
||||
if err != nil || len(b) != 1 {
|
||||
t.Fatalf("limb: %v, %d events", err, len(b))
|
||||
}
|
||||
if !a[0].Greatest.Equal(b[0].Greatest) || a[0].Type != b[0].Type {
|
||||
t.Fatal("greatest or type changed")
|
||||
}
|
||||
moved := false
|
||||
for i, pair := range [][2]time.Time{
|
||||
{a[0].ExternalImmersion, b[0].ExternalImmersion}, {a[0].InternalImmersion, b[0].InternalImmersion},
|
||||
{a[0].InternalEmersion, b[0].InternalEmersion}, {a[0].ExternalEmersion, b[0].ExternalEmersion},
|
||||
} {
|
||||
shift := pair[1].Sub(pair[0]).Seconds()
|
||||
t.Logf("Saturn contact %d shift %+.3f s", i, shift)
|
||||
if math.Abs(shift) > 15 {
|
||||
t.Errorf("contact %d shift %.3f s too large", i, shift)
|
||||
}
|
||||
if math.Abs(shift) > 0.05 {
|
||||
moved = true
|
||||
}
|
||||
}
|
||||
if !moved {
|
||||
t.Error("lunar limb did not move any contact")
|
||||
}
|
||||
viaOptions, err := plain.FindPlanetOccultations(start, end, OccultationSaturn, -30.072, 16.21, 0,
|
||||
OccultationSearchOptions{LunarLimb: limb.Default()})
|
||||
if err != nil || len(viaOptions) != 1 || viaOptions[0] != b[0] {
|
||||
t.Error("options limb differs from module limb")
|
||||
}
|
||||
again, _ := plain.FindPlanetOccultations(start, end, OccultationSaturn, -30.072, 16.21, 0, OccultationSearchOptions{})
|
||||
if len(again) != 1 || again[0] != a[0] {
|
||||
t.Error("WithLunarLimb changed the original module")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLunarLimbShiftsContacts(t *testing.T) {
|
||||
observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
|
||||
eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
ra, dec := occultationTestMoonTopocentricRaDec(occultationTestTimeToTT(eventTime), observer, -1)
|
||||
star := StarCoordinate{ID: "synthetic", RA: ra + 0.1, Dec: dec + 0.1, Epoch: eventTime, Frame: CoordinateFrameApparentOfDate}
|
||||
start, end := eventTime.Add(-3*time.Hour), eventTime.Add(3*time.Hour)
|
||||
plain := NewWithInstance(nil)
|
||||
a, err := plain.FindStarOccultations(start, end, star, observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{MaxEvents: 1})
|
||||
if err != nil || len(a) != 1 {
|
||||
t.Fatalf("circular: %v, %d events", err, len(a))
|
||||
}
|
||||
b, err := plain.WithLunarLimb(limb.Default()).FindStarOccultations(start, end, star, observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{MaxEvents: 1})
|
||||
if err != nil || len(b) != 1 {
|
||||
t.Fatalf("limb: %v, %d events", err, len(b))
|
||||
}
|
||||
if !a[0].Greatest.Equal(b[0].Greatest) {
|
||||
t.Fatal("greatest changed")
|
||||
}
|
||||
for i, pair := range [][2]time.Time{{a[0].Immersion, b[0].Immersion}, {a[0].Emersion, b[0].Emersion}} {
|
||||
shift := pair[1].Sub(pair[0]).Seconds()
|
||||
t.Logf("star contact %d shift %+.3f s", i, shift)
|
||||
if math.Abs(shift) > 15 {
|
||||
t.Errorf("contact %d shift %.3f s too large", i, shift)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationPathLunarLimbShiftsGrazeLines(t *testing.T) {
|
||||
star := StarCoordinate{
|
||||
ID: "HR 4799", RA: 189.1975, Dec: -5.831944444444,
|
||||
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
|
||||
ProperMotionRACosDecMasPerYear: -28, ProperMotionDecMasPerYear: -18,
|
||||
}
|
||||
start := time.Date(2025, 6, 4, 16, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, 6, 5, 16, 0, 0, 0, time.UTC)
|
||||
options := OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200, DisableFootprints: true, DisableRiseSet: true}
|
||||
plain := NewWithInstance(nil)
|
||||
circular, err := plain.FindStarOccultationPaths(start, end, star, options)
|
||||
if err != nil || len(circular) != 1 {
|
||||
t.Fatalf("circular: %v %d", err, len(circular))
|
||||
}
|
||||
limbed, err := plain.WithLunarLimb(limb.Default()).FindStarOccultationPaths(start, end, star, options)
|
||||
if err != nil || len(limbed) != 1 {
|
||||
t.Fatalf("limbed: %v %d", err, len(limbed))
|
||||
}
|
||||
for side, pair := range [][2][]OccultationPathPoint{{circular[0].NorthernLimit, limbed[0].NorthernLimit}, {circular[0].SouthernLimit, limbed[0].SouthernLimit}} {
|
||||
worst, moved := 0.0, 0
|
||||
for i := range pair[0] {
|
||||
a, b := pair[0][i], pair[1][i]
|
||||
d := math.Hypot((a.Longitude-b.Longitude)*math.Cos(a.Latitude*math.Pi/180), a.Latitude-b.Latitude) * 111.2
|
||||
if d > 0 {
|
||||
moved++
|
||||
}
|
||||
worst = math.Max(worst, d)
|
||||
}
|
||||
t.Logf("side %d: %d/%d moved, max %.3f km", side, moved, len(pair[0]), worst)
|
||||
if worst > 15 || moved == 0 {
|
||||
t.Errorf("side %d: implausible graze shift (max %.3f km, moved %d)", side, worst, moved)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。
|
||||
// 调用者显式提供坐标;函数不会加载内嵌星表。查询端点使用核心求解器的 10 ms 数值选择容差。
|
||||
// FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star.
|
||||
// Callers provide the coordinate explicitly; this function does not load the embedded star catalog. Query endpoints use the core solver's 10 ms numerical selection tolerance.
|
||||
func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
|
||||
return basic.FindStarOccultationPaths(start, end, star, options)
|
||||
// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带 / global paths of a point-source stellar occultation.
|
||||
func (module *Moon) FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
|
||||
if options.LunarLimb == nil {
|
||||
options.LunarLimb = module.lunarLimb
|
||||
}
|
||||
return basic.FindStarOccultationPathsWithInstance(start, end, star, options, module.inst)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,14 @@ import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// FindPlanetOccultations 搜索固定地点的有限盘面行星月掩。
|
||||
// 经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。圆形目标使用行星赤道半径,忽略行星环。
|
||||
// FindPlanetOccultations searches finite-disk lunar occultations of one planet at a fixed observing site.
|
||||
// Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is above mean sea level in meters. The circular target uses the planet's equatorial body radius; rings are excluded.
|
||||
func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
|
||||
// FindPlanetOccultations 搜索固定地点的有限盘面行星月掩 / finite-disk planetary occultations at a fixed site.
|
||||
func (module *Moon) FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
||||
return basic.FindPlanetOccultations(start, end, planet, longitude, latitude, height, options)
|
||||
return basic.FindPlanetOccultationsWithInstance(start, end, planet, longitude, latitude, height, module.occultationOptions(options), module.inst)
|
||||
}
|
||||
|
||||
// FindBestPlanetOccultations 返回窗口内每次有限盘面行星月掩的全球海平面几何掩甚点。
|
||||
// 地平线可见性只报告、不参与点选择;查询端点使用核心求解器的 10 ms 数值选择容差。
|
||||
// FindBestPlanetOccultations returns the global geometric greatest sea-level point for every finite-disk occultation of one planet in the window.
|
||||
// Horizon visibility is reported but does not affect point selection. Query endpoints use the core solver's 10 ms numerical selection tolerance.
|
||||
func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
|
||||
// FindBestPlanetOccultations 返回窗口内每次有限盘面行星月掩的全球海平面几何掩甚点 / global greatest points of planetary occultations in a window.
|
||||
func (module *Moon) FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
||||
return basic.FindBestPlanetOccultations(start, end, planet, options)
|
||||
return basic.FindBestPlanetOccultationsWithInstance(start, end, planet, module.occultationOptions(options), module.inst)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package moon
|
||||
|
||||
import "b612.me/astro/basic"
|
||||
import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// PlanetOccultationDiagramOptions 控制本地行星月掩轨迹采样。
|
||||
// PlanetOccultationDiagramOptions controls local planetary-occultation track sampling.
|
||||
@@ -14,11 +16,10 @@ type PlanetOccultationDiagramFrame = basic.PlanetOccultationDiagramFrame
|
||||
// PlanetOccultationDiagramResult contains a fixed-site Moon-centered planetary track.
|
||||
type PlanetOccultationDiagramResult = basic.PlanetOccultationDiagramResult
|
||||
|
||||
// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算图形几何数据。
|
||||
// PlanetOccultationDiagram computes diagram geometry for an already solved fixed-site planetary occultation.
|
||||
func PlanetOccultationDiagram(
|
||||
// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算图形几何数据 / diagram geometry for a solved local planetary occultation.
|
||||
func (module *Moon) PlanetOccultationDiagram(
|
||||
info PlanetOccultationInfo,
|
||||
options PlanetOccultationDiagramOptions,
|
||||
) PlanetOccultationDiagramResult {
|
||||
return basic.PlanetOccultationDiagram(info, options)
|
||||
return basic.PlanetOccultationDiagramWithInstance(info, options, module.inst)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// FindPlanetOccultationPaths 搜索窗口内每次有限盘面行星月掩的完整全球外接触掩带。
|
||||
// 查询端点使用核心求解器的 10 ms 数值选择容差。
|
||||
// FindPlanetOccultationPaths searches the complete global outer-contact footprint of every finite-disk planetary occultation selected by the window.
|
||||
// Query endpoints use the core solver's 10 ms numerical selection tolerance.
|
||||
func FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
|
||||
// FindPlanetOccultationPaths 搜索窗口内每次有限盘面行星月掩的完整全球外接触掩带 / global paths of finite-disk planetary occultations in a window.
|
||||
func (module *Moon) FindPlanetOccultationPaths(start, end time.Time, planet OccultationPlanet,
|
||||
options OccultationPathOptions) ([]PlanetOccultationPath, error) {
|
||||
return basic.FindPlanetOccultationPaths(start, end, planet, options)
|
||||
if options.LunarLimb == nil {
|
||||
options.LunarLimb = module.lunarLimb
|
||||
}
|
||||
return basic.FindPlanetOccultationPathsWithInstance(start, end, planet, options, module.inst)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,13 @@ import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// FindStarOccultations 搜索单颗点源恒星的月掩星。
|
||||
//
|
||||
// 输入坐标从历元传播并转换到当日视坐标系。此函数不会加载内嵌星表;需要星表搜索时,调用者必须显式加载并选择恒星。经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。
|
||||
// FindStarOccultations searches for lunar occultations of one point-source star.
|
||||
// The input coordinate is propagated from its epoch and converted to the apparent frame of date. This function never loads the embedded star catalog; callers that need catalog searches must load and select stars explicitly. Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters.
|
||||
func FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
|
||||
// FindStarOccultations 搜索单颗点源恒星的月掩星 / occultations of a point-source star at a fixed site.
|
||||
func (module *Moon) FindStarOccultations(start, end time.Time, star StarCoordinate, longitude, latitude, height float64,
|
||||
options OccultationSearchOptions) ([]StarOccultationInfo, error) {
|
||||
return basic.FindStarOccultations(start, end, star, longitude, latitude, height, options)
|
||||
return basic.FindStarOccultationsWithInstance(start, end, star, longitude, latitude, height, module.occultationOptions(options), module.inst)
|
||||
}
|
||||
|
||||
// FindBestStarOccultations 返回窗口内每次恒星月掩的全球海平面几何掩甚点。
|
||||
// 点选择不使用地平线或可见性评分;查询端点使用核心求解器的 10 ms 数值选择容差。
|
||||
// FindBestStarOccultations returns the global geometric greatest sea-level point for every stellar occultation whose greatest instant is in the window.
|
||||
// The point is selected without a horizon or visibility score. Query endpoints use the core solver's 10 ms numerical selection tolerance.
|
||||
func FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error) {
|
||||
return basic.FindBestStarOccultations(start, end, star, options)
|
||||
// FindBestStarOccultations 返回窗口内每次恒星月掩的全球海平面几何掩甚点 / global greatest points of stellar occultations in a window.
|
||||
func (module *Moon) FindBestStarOccultations(start, end time.Time, star StarCoordinate, options OccultationSearchOptions) ([]StarOccultationInfo, error) {
|
||||
return basic.FindBestStarOccultationsWithInstance(start, end, star, module.occultationOptions(options), module.inst)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package moon
|
||||
|
||||
import "b612.me/astro/basic"
|
||||
import (
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// StarOccultationDiagramOptions 控制本地恒星月掩轨迹采样。
|
||||
// StarOccultationDiagramOptions controls local stellar-occultation track sampling.
|
||||
@@ -14,12 +16,39 @@ type StarOccultationDiagramFrame = basic.StarOccultationDiagramFrame
|
||||
// StarOccultationDiagramResult contains a fixed-site Moon-centered stellar track.
|
||||
type StarOccultationDiagramResult = basic.StarOccultationDiagramResult
|
||||
|
||||
// StarOccultationDiagram 为已求解的固定地点恒星月掩计算图形几何数据。
|
||||
// StarOccultationDiagram computes diagram geometry for an already solved fixed-site stellar occultation.
|
||||
func StarOccultationDiagram(
|
||||
// StarOccultationDiagram 为已求解的固定地点恒星月掩计算图形几何数据 / diagram geometry for a solved local stellar occultation.
|
||||
func (module *Moon) StarOccultationDiagram(
|
||||
info StarOccultationInfo,
|
||||
star StarCoordinate,
|
||||
options StarOccultationDiagramOptions,
|
||||
) StarOccultationDiagramResult {
|
||||
return basic.StarOccultationDiagram(info, star, options)
|
||||
return basic.StarOccultationDiagramWithInstance(info, star, options, module.inst)
|
||||
}
|
||||
|
||||
// LunarLimb 月缘剖面,nil 为圆月面,内置数据表在 moon/limb / lunar limb profile; nil means a circular limb.
|
||||
type LunarLimb = basic.LunarLimb
|
||||
|
||||
// StarOccultationLimbProfileOptions 控制精密恒星月掩的星迹采样 / sampling of a precise stellar occultation.
|
||||
type StarOccultationLimbProfileOptions = basic.StarOccultationLimbProfileOptions
|
||||
|
||||
// StarOccultationLimbSample 星迹上一个时刻的月缘几何 / limb geometry at one instant on the star track.
|
||||
type StarOccultationLimbSample = basic.StarOccultationLimbSample
|
||||
|
||||
// OccultationInterval 一段连续遮挡 / one continuous hidden interval.
|
||||
type OccultationInterval = basic.OccultationInterval
|
||||
|
||||
// StarOccultationLimbProfileResult 一次恒星月掩沿星迹的月缘剖面与全部遮挡区间 / limb profile and every hidden interval of a stellar occultation.
|
||||
type StarOccultationLimbProfileResult = basic.StarOccultationLimbProfileResult
|
||||
|
||||
// StarOccultationLimbProfile 精密恒星月掩;limb 为 nil 时用模块月缘 / precise stellar occultation; nil limb uses the module profile.
|
||||
func (module *Moon) StarOccultationLimbProfile(info StarOccultationInfo, star StarCoordinate, limb LunarLimb, options StarOccultationLimbProfileOptions) (StarOccultationLimbProfileResult, error) {
|
||||
if limb == nil {
|
||||
limb = module.lunarLimb
|
||||
}
|
||||
return basic.StarOccultationLimbProfileWithInstance(info, star, limb, options, module.inst)
|
||||
}
|
||||
|
||||
// StarOccultationLimbProfileInUT1 返回精密恒星月掩的 UT1 时刻副本 / copy in UT1 of a precise stellar occultation.
|
||||
func (module *Moon) StarOccultationLimbProfileInUT1(profile StarOccultationLimbProfileResult) StarOccultationLimbProfileResult {
|
||||
return StarOccultationLimbProfileInUT1(profile)
|
||||
}
|
||||
|
||||
+21
-2
@@ -3,7 +3,6 @@ package moon
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro"
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
@@ -14,7 +13,7 @@ func occultationUT1Label(t time.Time) time.Time {
|
||||
if t.IsZero() {
|
||||
return t
|
||||
}
|
||||
return astro.LabelIn(astro.TimeScaleUT1, t)
|
||||
return basic.LabelIn(basic.TimeScaleUT1, t)
|
||||
}
|
||||
|
||||
func occultationUT1PathPoint(p basic.OccultationPathPoint) basic.OccultationPathPoint {
|
||||
@@ -129,3 +128,23 @@ func PlanetOccultationInfoInUT1(info basic.PlanetOccultationInfo) basic.PlanetOc
|
||||
info.ExternalEmersion = occultationUT1Label(info.ExternalEmersion)
|
||||
return info
|
||||
}
|
||||
|
||||
// StarOccultationLimbProfileInUT1 返回精密恒星月掩的 UT1 时刻副本 / copy in UT1 of a precise stellar occultation.
|
||||
func StarOccultationLimbProfileInUT1(profile basic.StarOccultationLimbProfileResult) basic.StarOccultationLimbProfileResult {
|
||||
profile.Occultation = StarOccultationInfoInUT1(profile.Occultation)
|
||||
segments := make([][]basic.StarOccultationLimbSample, len(profile.Segments))
|
||||
for i, segment := range profile.Segments {
|
||||
segments[i] = append([]basic.StarOccultationLimbSample(nil), segment...)
|
||||
for j := range segments[i] {
|
||||
segments[i][j].Time = occultationUT1Label(segments[i][j].Time)
|
||||
}
|
||||
}
|
||||
profile.Segments = segments
|
||||
hidden := append([]basic.OccultationInterval(nil), profile.Hidden...)
|
||||
for i := range hidden {
|
||||
hidden[i].Start = occultationUT1Label(hidden[i].Start)
|
||||
hidden[i].End = occultationUT1Label(hidden[i].End)
|
||||
}
|
||||
profile.Hidden = hidden
|
||||
return profile
|
||||
}
|
||||
|
||||
+2
-4
@@ -7,8 +7,6 @@ import (
|
||||
)
|
||||
|
||||
// ParallacticAngle 月亮视差角(天顶方向角) / lunar parallactic angle.
|
||||
//
|
||||
// 对月亮使用现有站心视时角和站心视赤纬链路,因此会显式依赖观测者经纬度。
|
||||
func ParallacticAngle(date time.Time, lon, lat float64) float64 {
|
||||
return basic.ParallacticAngleByHourAngle(HourAngle(date, lon, lat), ApparentDec(date, lon, lat), lat)
|
||||
func (module *Moon) ParallacticAngle(date time.Time, lon, lat float64) float64 {
|
||||
return basic.ParallacticAngleByHourAngle(module.HourAngle(date, lon, lat), module.ApparentDec(date, lon, lat), lat)
|
||||
}
|
||||
|
||||
+10
-13
@@ -7,26 +7,23 @@ import (
|
||||
)
|
||||
|
||||
// BrightLimbPositionAngle 月亮明亮边缘位置角,单位度 / position angle of the Moon's bright limb in degrees.
|
||||
func BrightLimbPositionAngle(date time.Time) float64 {
|
||||
return BrightLimbPositionAngleN(date, -1)
|
||||
func (module *Moon) BrightLimbPositionAngle(date time.Time) float64 {
|
||||
return module.BrightLimbPositionAngleN(date, -1)
|
||||
}
|
||||
|
||||
// BrightLimbPositionAngleN 月亮明亮边缘位置角(截断版),单位度 / truncated position angle of the Moon's bright limb in degrees.
|
||||
func BrightLimbPositionAngleN(date time.Time, n int) float64 {
|
||||
return basic.MoonBrightLimbPositionAngleN(observationTT(date), n)
|
||||
// BrightLimbPositionAngleN 月亮明亮边缘位置角(n 变体),单位度 / n-parameterised position angle of the Moon's bright limb in degrees.
|
||||
func (module *Moon) BrightLimbPositionAngleN(date time.Time, n int) float64 {
|
||||
return basic.MoonBrightLimbPositionAngleNWithInstance(observationTT(date), n, module.inst)
|
||||
}
|
||||
|
||||
// TopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角,单位度 / topocentric position angle of the Moon's bright limb in degrees.
|
||||
//
|
||||
// date 为观测时刻;observerLon/observerLat 为观测者经纬度,东正西负、北正南负;height 为椭球高(大地高),单位米。
|
||||
// date is the observing instant; observerLon/observerLat are east-positive and north-positive; height is observer elevation in meters.
|
||||
func TopocentricBrightLimbPositionAngle(date time.Time, observerLon, observerLat, height float64) float64 {
|
||||
return TopocentricBrightLimbPositionAngleN(date, observerLon, observerLat, height, -1)
|
||||
func (module *Moon) TopocentricBrightLimbPositionAngle(date time.Time, observerLon, observerLat, height float64) float64 {
|
||||
return module.TopocentricBrightLimbPositionAngleN(date, observerLon, observerLat, height, -1)
|
||||
}
|
||||
|
||||
// TopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版),单位度 / truncated topocentric position angle of the Moon's bright limb in degrees.
|
||||
func TopocentricBrightLimbPositionAngleN(date time.Time, observerLon, observerLat, height float64, n int) float64 {
|
||||
return basic.MoonTopocentricBrightLimbPositionAngleN(observationTT(date), observerLon, observerLat, height, n)
|
||||
// TopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(n 变体),单位度 / n-parameterised topocentric position angle of the Moon's bright limb in degrees.
|
||||
func (module *Moon) TopocentricBrightLimbPositionAngleN(date time.Time, observerLon, observerLat, height float64, n int) float64 {
|
||||
return basic.MoonTopocentricBrightLimbPositionAngleNWithInstance(observationTT(date), observerLon, observerLat, height, n, module.inst)
|
||||
}
|
||||
|
||||
func observationTT(date time.Time) float64 {
|
||||
|
||||
+5
-5
@@ -25,14 +25,14 @@ type PhysicalInfo struct {
|
||||
}
|
||||
|
||||
// Physical 月球物理观测参数 / physical observing parameters of the Moon.
|
||||
func Physical(date time.Time) PhysicalInfo {
|
||||
return PhysicalN(date, -1)
|
||||
func (module *Moon) Physical(date time.Time) PhysicalInfo {
|
||||
return module.PhysicalN(date, -1)
|
||||
}
|
||||
|
||||
// PhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
|
||||
func PhysicalN(date time.Time, n int) PhysicalInfo {
|
||||
// PhysicalN 月球物理观测参数(n 变体) / n-parameterised physical observing parameters of the Moon.
|
||||
func (module *Moon) PhysicalN(date time.Time, n int) PhysicalInfo {
|
||||
jd := basic.Date2JD(date.UTC())
|
||||
info := basic.MoonPhysicalN(basic.UTC2TT(jd), n)
|
||||
info := basic.MoonPhysicalNWithInstance(basic.UTC2TT(jd), n, module.inst)
|
||||
return PhysicalInfo{
|
||||
OpticalLongitude: info.OpticalLongitude,
|
||||
OpticalLatitude: info.OpticalLatitude,
|
||||
|
||||
@@ -7,16 +7,13 @@ import (
|
||||
)
|
||||
|
||||
// TopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
|
||||
//
|
||||
// date 为观测时刻;observerLon/observerLat 为观测者经纬度,东正西负、北正南负;height 为椭球高(大地高),单位米。
|
||||
// date is the observing instant; observerLon/observerLat are east-positive and north-positive; height is observer elevation in meters.
|
||||
func TopocentricPhysical(date time.Time, observerLon, observerLat, height float64) PhysicalInfo {
|
||||
return TopocentricPhysicalN(date, observerLon, observerLat, height, -1)
|
||||
func (module *Moon) TopocentricPhysical(date time.Time, observerLon, observerLat, height float64) PhysicalInfo {
|
||||
return module.TopocentricPhysicalN(date, observerLon, observerLat, height, -1)
|
||||
}
|
||||
|
||||
// TopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
|
||||
func TopocentricPhysicalN(date time.Time, observerLon, observerLat, height float64, n int) PhysicalInfo {
|
||||
info := basic.MoonTopocentricPhysicalN(observationTT(date), observerLon, observerLat, height, n)
|
||||
// TopocentricPhysicalN 月球站心物理观测参数(n 变体) / n-parameterised topocentric physical observing parameters of the Moon.
|
||||
func (module *Moon) TopocentricPhysicalN(date time.Time, observerLon, observerLat, height float64, n int) PhysicalInfo {
|
||||
info := basic.MoonTopocentricPhysicalNWithInstance(observationTT(date), observerLon, observerLat, height, n, module.inst)
|
||||
return PhysicalInfo{
|
||||
OpticalLongitude: info.OpticalLongitude,
|
||||
OpticalLatitude: info.OpticalLatitude,
|
||||
|
||||
+4
-4
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
// ApparentAltitude 月亮视高度角 / apparent lunar altitude.
|
||||
func ApparentAltitude(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.ApparentAltitude(Altitude(date, lon, lat), pressureHPa, temperatureC)
|
||||
func (module *Moon) ApparentAltitude(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return basic.ApparentAltitude(module.Altitude(date, lon, lat), pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
// ApparentZenith 月亮视天顶距 / apparent lunar zenith distance.
|
||||
func ApparentZenith(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return 90 - ApparentAltitude(date, lon, lat, pressureHPa, temperatureC)
|
||||
func (module *Moon) ApparentZenith(date time.Time, lon, lat, pressureHPa, temperatureC float64) float64 {
|
||||
return 90 - module.ApparentAltitude(date, lon, lat, pressureHPa, temperatureC)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// AltitudeLimb 取高度的位置(上缘、中心、下缘)/ limb used for the altitude.
|
||||
type AltitudeLimb = basic.AltitudeLimb
|
||||
|
||||
const (
|
||||
LimbUpper = basic.AltitudeLimbUpper
|
||||
LimbCenter = basic.AltitudeLimbCenter
|
||||
LimbLower = basic.AltitudeLimbLower
|
||||
)
|
||||
|
||||
// AltitudeOptions 升落选项;零值为上缘、标准气象、计入折射 / altitude crossing options.
|
||||
type AltitudeOptions = basic.AltitudeCrossingOptions
|
||||
|
||||
// AltitudeState 一个民用日内的穿越状态 / crossing state over one civil day.
|
||||
type AltitudeState = basic.AltitudeCrossingState
|
||||
|
||||
const (
|
||||
AltitudeCrosses = basic.AltitudeCrosses
|
||||
AltitudeAlwaysAbove = basic.AltitudeAlwaysAbove
|
||||
AltitudeAlwaysBelow = basic.AltitudeAlwaysBelow
|
||||
AltitudeTangent = basic.AltitudeTangent
|
||||
)
|
||||
|
||||
// AltitudeCrossingResult date 所在民用日内首次升、落与残差范围(度)/ first rise and set on the civil day.
|
||||
type AltitudeCrossingResult = basic.AltitudeCrossingResult
|
||||
|
||||
// RiseSet 月球在 date 所在民用日内穿过观测者地平线(含 height 带来的地平俯角)的升落;options 控制月缘与大气 / lunar rise and set on the civil day.
|
||||
func (module *Moon) RiseSet(date time.Time, lon, lat, height float64, options AltitudeOptions) (AltitudeCrossingResult, error) {
|
||||
return basic.SkyAltitudeCrossingOnDate(basic.SkyMoon, date, lon, lat, height, -basic.HeightDegreeByLat(height, lat), options, module.inst)
|
||||
}
|
||||
|
||||
// RiseSet 月球在 date 所在民用日内穿过观测者地平线(含 height 带来的地平俯角)的升落;options 控制月缘与大气 / lunar rise and set on the civil day.
|
||||
func RiseSet(date time.Time, lon, lat, height float64, options AltitudeOptions) (AltitudeCrossingResult, error) {
|
||||
return defaultModule.RiseSet(date, lon, lat, height, options)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 被测契约:默认选项与 RiseTime/SetTime 一致;夏令时切换日按真实日长取当天事件;实例方法使用绑定实例。
|
||||
package moon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/ephem"
|
||||
)
|
||||
|
||||
func TestRiseSetMatchesRiseTimeSetTime(t *testing.T) {
|
||||
loc := time.FixedZone("CST", 8*3600)
|
||||
for i := 0; i < 30; i++ {
|
||||
date := time.Date(2025, 1, 1, 12, 0, 0, 0, loc).AddDate(0, 0, i*12)
|
||||
got, err := RiseSet(date, 116.4, 39.9, 50, basic.AltitudeCrossingOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkEvent(t, date, "rise", got.Rise, got.HasRise, func() (time.Time, error) { return RiseTime(date, 116.4, 39.9, 50, true) })
|
||||
checkEvent(t, date, "set", got.Set, got.HasSet, func() (time.Time, error) { return SetTime(date, 116.4, 39.9, 50, true) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestRiseSetDaylightSavingDays(t *testing.T) {
|
||||
loc, err := time.LoadLocation("America/New_York")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
for _, date := range []time.Time{
|
||||
time.Date(2023, 3, 12, 12, 0, 0, 0, loc),
|
||||
time.Date(2023, 11, 5, 12, 0, 0, 0, loc),
|
||||
} {
|
||||
got, err := RiseSet(date, -74, 40.7, 0, basic.AltitudeCrossingOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkEvent(t, date, "rise", got.Rise, got.HasRise, func() (time.Time, error) { return RiseTime(date, -74, 40.7, 0, true) })
|
||||
checkEvent(t, date, "set", got.Set, got.HasSet, func() (time.Time, error) { return SetTime(date, -74, 40.7, 0, true) })
|
||||
for _, ev := range []time.Time{got.Rise, got.Set} {
|
||||
if !ev.IsZero() && (ev.Location() != loc || ev.YearDay() != date.YearDay()) {
|
||||
t.Errorf("%s: event %v outside civil day", date.Format("2006-01-02"), ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRiseSetUsesModuleInstance(t *testing.T) {
|
||||
date := time.Date(2025, 3, 20, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
got, err := NewWithInstance(ephem.NewModelInstance(ephem.Legacy)).RiseSet(date, 116.4, 39.9, 0, basic.AltitudeCrossingOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, err := basic.SkyAltitudeCrossingOnDate(basic.SkyMoon, date, 116.4, 39.9, 0, 0, basic.AltitudeCrossingOptions{}, ephem.NewModelInstance(ephem.Legacy))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.Rise.Equal(want.Rise) || !got.Set.Equal(want.Set) {
|
||||
t.Errorf("instance result %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func checkEvent(t *testing.T, date time.Time, kind string, got time.Time, has bool, ref func() (time.Time, error)) {
|
||||
t.Helper()
|
||||
want, err := ref()
|
||||
if has != (err == nil) {
|
||||
t.Errorf("%s %s: has=%v, reference err=%v", date.Format("2006-01-02"), kind, has, err)
|
||||
return
|
||||
}
|
||||
if has {
|
||||
if d := got.Sub(want); d > 100*time.Millisecond || d < -100*time.Millisecond {
|
||||
t.Errorf("%s %s: %v, reference %v", date.Format("2006-01-02"), kind, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -3,6 +3,7 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"b612.me/astro/internal/limbnote"
|
||||
"b612.me/astro/internal/timenote"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -253,7 +254,8 @@ func renderOccultationPathSVG(
|
||||
}
|
||||
writePlanetOccultationEventsPanel(&b, *planetPath, layout, options)
|
||||
}
|
||||
writeStarOccultationFooter(&b, layout, options, flags, occultationScaleInstant(path, planetPath))
|
||||
writeStarOccultationFooter(&b, layout, options, flags, occultationScaleInstant(path, planetPath),
|
||||
occultationLimbApplied(path, planetPath))
|
||||
b.WriteString(`</svg>`)
|
||||
return b.String(), nil
|
||||
}
|
||||
@@ -757,16 +759,26 @@ func occultationScaleInstant(path moon.StarOccultationPath, planetPath *moon.Pla
|
||||
return path.Greatest.Time
|
||||
}
|
||||
|
||||
// occultationLimbApplied 报告图中界线是否按月缘剖面修正。
|
||||
func occultationLimbApplied(path moon.StarOccultationPath, planetPath *moon.PlanetOccultationPath) bool {
|
||||
if planetPath != nil {
|
||||
return planetPath.LunarLimbApplied
|
||||
}
|
||||
return path.LunarLimbApplied
|
||||
}
|
||||
|
||||
func writeStarOccultationFooter(
|
||||
b *strings.Builder,
|
||||
layout starOccultationSVGLayout,
|
||||
options StarOccultationSVGOptions,
|
||||
flags occultationSVGTextFlags,
|
||||
instant time.Time,
|
||||
limbApplied bool,
|
||||
) {
|
||||
maxWidth := layout.width - 80
|
||||
lineHeight := svgchart.FooterLineHeight(11)
|
||||
scaleNote := timenote.Scale(options.TimeScale, instant, options.Location, options.Language)
|
||||
scaleNote := limbnote.Join(timenote.Scale(options.TimeScale, instant, options.Location, options.Language),
|
||||
limbnote.Limits(limbApplied, options.Language), options.Language)
|
||||
// 页脚默认排在画布底边之内(与既有版面一致),说明行放得下时声明单独占最后一行;
|
||||
// 自定义说明太长时改用按字号留白的行数上限:截断说明也要保住时标声明和底边留白。
|
||||
lines := occultationWrappedTextLines(starOccultationSVGFooter(options), flags.footerNote, maxWidth, 11)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"b612.me/astro/internal/limbnote"
|
||||
"b612.me/astro/internal/timenote"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"b612.me/astro"
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/internal/geodata"
|
||||
"b612.me/astro/internal/svgchart"
|
||||
"b612.me/astro/internal/svgmap"
|
||||
"b612.me/astro/moon"
|
||||
@@ -217,7 +219,7 @@ func occultationDetailedMoonRows(at time.Time, language string) []svgchart.Panel
|
||||
jde := basic.UTC2TT(basic.Date2JD(at.UTC()))
|
||||
ra, dec := basic.HMoonTrueRaDec(jde)
|
||||
sd := basic.MoonSemidiameter(jde)
|
||||
hp := math.Asin(math.Sin(sd/3600*math.Pi/180)*6378.137/1737.4) * 180 / math.Pi * 3600
|
||||
hp := math.Asin(math.Sin(sd/3600*math.Pi/180)*geodata.EarthWGS84RadiusKM/1737.4) * 180 / math.Pi * 3600
|
||||
rows := []svgchart.PanelRow{
|
||||
{Label: "赤经 R.A.", Value: occultationDetailedRA(ra)},
|
||||
{Label: "赤纬 Dec.", Value: occultationDetailedDec(dec)},
|
||||
@@ -363,7 +365,8 @@ func renderOccultationDetailedSVG(
|
||||
return "", err
|
||||
}
|
||||
writeOccultationDetailedGrid(&b, view, blocks)
|
||||
writeOccultationDetailedFooter(&b, view, options, occultationScaleInstant(path, planetPath))
|
||||
writeOccultationDetailedFooter(&b, view, options, occultationScaleInstant(path, planetPath),
|
||||
occultationLimbApplied(path, planetPath))
|
||||
b.WriteString(`</svg>`)
|
||||
return b.String(), nil
|
||||
}
|
||||
@@ -433,7 +436,7 @@ func writeOccultationDetailedSummary(
|
||||
}
|
||||
}
|
||||
|
||||
func writeOccultationDetailedFooter(b *strings.Builder, view occultationDetailedView, options OccultationDetailedSVGOptions, instant time.Time) {
|
||||
func writeOccultationDetailedFooter(b *strings.Builder, view occultationDetailedView, options OccultationDetailedSVGOptions, instant time.Time, limbApplied bool) {
|
||||
text := options.FooterNote
|
||||
if text == "" {
|
||||
if options.Language == starOccultationSVGLanguageEnglish {
|
||||
@@ -448,7 +451,8 @@ func writeOccultationDetailedFooter(b *strings.Builder, view occultationDetailed
|
||||
if options.FooterNote != "" {
|
||||
lines = svgchart.WrapText(text, maxWidth, 11)
|
||||
}
|
||||
lines = svgchart.FooterLinesWithScale(lines, timenote.Scale(options.TimeScale, instant, options.Location, options.Language),
|
||||
lines = svgchart.FooterLinesWithScale(lines, limbnote.Join(timenote.Scale(options.TimeScale, instant, options.Location, options.Language),
|
||||
limbnote.Limits(limbApplied, options.Language), options.Language),
|
||||
maxWidth, 11,
|
||||
svgchart.BaselineLineLimit(11, svgchart.FooterLineHeight(11), view.footerY,
|
||||
view.height-svgchart.FooterBottomPadding(11)))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/internal/ephemtest"
|
||||
"b612.me/astro/moon"
|
||||
)
|
||||
|
||||
@@ -155,6 +156,7 @@ func TestOccultationDetailedSVGPanelValuesGolden(t *testing.T) {
|
||||
t.Fatalf("StarOccultationDetailedSVG() error = %v", err)
|
||||
}
|
||||
star := occultationSVGDetailedPanelRows(t, starDiagram)
|
||||
semiValues := ephemtest.NewValues(t, "occultation_detailed_panel")
|
||||
for _, expected := range []struct {
|
||||
title, label, value string
|
||||
}{
|
||||
@@ -172,7 +174,12 @@ func TestOccultationDetailedSVGPanelValuesGolden(t *testing.T) {
|
||||
{"天平动", "纬天平动 b", "-2.79°"},
|
||||
{"天平动", "自转轴位置角 c", "-21.92°"},
|
||||
} {
|
||||
if got := occultationSVGPanelValue(t, star, expected.title, expected.label); got != expected.value {
|
||||
got := occultationSVGPanelValue(t, star, expected.title, expected.label)
|
||||
// 月心地心坐标随模型变化,与行星坐标行一样按模型取期望值。
|
||||
if expected.title == "月亮(地心坐标)" && strings.HasPrefix(expected.label, "赤") {
|
||||
expected.value = semiValues.WantString("moon."+expected.label, expected.value, got)
|
||||
}
|
||||
if got != expected.value {
|
||||
t.Fatalf("stellar panel %q row %q = %q, want %q", expected.title, expected.label, got, expected.value)
|
||||
}
|
||||
}
|
||||
@@ -195,7 +202,12 @@ func TestOccultationDetailedSVGPanelValuesGolden(t *testing.T) {
|
||||
{"接触时刻", "全掩始", "10:30:00"},
|
||||
{"接触时刻", "全掩终", "12:30:00"},
|
||||
} {
|
||||
if got := occultationSVGPanelValue(t, planet, expected.title, expected.label); got != expected.value {
|
||||
got := occultationSVGPanelValue(t, planet, expected.title, expected.label)
|
||||
// 新模型视位置更准,坐标行按模型取期望值。
|
||||
if strings.HasPrefix(expected.label, "赤") {
|
||||
expected.value = semiValues.WantString("planet."+expected.label, expected.value, got)
|
||||
}
|
||||
if got != expected.value {
|
||||
t.Fatalf("planetary panel %q row %q = %q, want %q", expected.title, expected.label, got, expected.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/moon"
|
||||
)
|
||||
|
||||
type constantLimb float64
|
||||
|
||||
func (r constantLimb) LimbRadiusKM(_, _, _ float64) (float64, bool) { return float64(r), true }
|
||||
|
||||
type failingLimb struct{}
|
||||
|
||||
func (failingLimb) LimbRadiusKM(_, _, _ float64) (float64, bool) { return 0, false }
|
||||
|
||||
func TestLocalOccultationSVGAcceptsCustomLimbAndRejectsFailedLimbFlag(t *testing.T) {
|
||||
start := time.Date(2034, time.November, 21, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2034, time.November, 23, 0, 0, 0, 0, time.UTC)
|
||||
find := func(profile basic.LunarLimb) moon.PlanetOccultationInfo {
|
||||
t.Helper()
|
||||
events, err := moon.FindPlanetOccultations(start, end, moon.OccultationJupiter,
|
||||
-42.36358358336173, 21.762064240049245, 0, moon.OccultationSearchOptions{LunarLimb: profile})
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindPlanetOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
return events[0]
|
||||
}
|
||||
circular := find(nil)
|
||||
|
||||
custom := find(constantLimb(1750))
|
||||
if !custom.LunarLimbApplied {
|
||||
t.Fatal("constant 1750 km limb: LunarLimbApplied = false, want true")
|
||||
}
|
||||
if custom.ExternalImmersion.Equal(circular.ExternalImmersion) {
|
||||
t.Fatal("constant 1750 km limb did not move the contact")
|
||||
}
|
||||
diagram, err := LocalPlanetOccultationSVG(custom, LocalPlanetOccultationSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatalf("LocalPlanetOccultationSVG(custom limb) error = %v", err)
|
||||
}
|
||||
if !strings.Contains(diagram, "月缘剖面") {
|
||||
t.Fatal("custom limb chart does not declare the limb profile")
|
||||
}
|
||||
|
||||
failed := find(failingLimb{})
|
||||
if failed.LunarLimbApplied {
|
||||
t.Fatal("failing limb: LunarLimbApplied = true, want false")
|
||||
}
|
||||
if !failed.ExternalImmersion.Equal(circular.ExternalImmersion) || !failed.InternalEmersion.Equal(circular.InternalEmersion) {
|
||||
t.Fatal("failing limb changed the circular contacts")
|
||||
}
|
||||
diagram, err = LocalPlanetOccultationSVG(failed, LocalPlanetOccultationSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatalf("LocalPlanetOccultationSVG(failed limb) error = %v", err)
|
||||
}
|
||||
if strings.Contains(diagram, "月缘剖面") || !strings.Contains(diagram, "圆月面") {
|
||||
t.Fatal("failed limb chart must declare the circular limb")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationPathLimbFlagRequiresCorrectedLimit(t *testing.T) {
|
||||
start := time.Date(2034, time.November, 21, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2034, time.November, 23, 0, 0, 0, 0, time.UTC)
|
||||
paths, err := moon.FindPlanetOccultationPaths(start, end, moon.OccultationJupiter,
|
||||
moon.OccultationPathOptions{Step: 10 * time.Minute, TargetSpacingKM: 400, LunarLimb: failingLimb{}})
|
||||
if err != nil || len(paths) != 1 {
|
||||
t.Fatalf("FindPlanetOccultationPaths() = %d paths, %v; want one", len(paths), err)
|
||||
}
|
||||
if paths[0].LunarLimbApplied {
|
||||
t.Fatal("failing limb: path LunarLimbApplied = true, want false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package svg
|
||||
|
||||
// 月缘剖面精修过的接触时刻不落在圆月面上,站心图仍须能出图。
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/moon"
|
||||
"b612.me/astro/moon/limb"
|
||||
)
|
||||
|
||||
func TestLocalPlanetOccultationSVGAcceptsLunarLimbContacts(t *testing.T) {
|
||||
events, err := moon.FindPlanetOccultations(
|
||||
time.Date(2034, time.November, 21, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2034, time.November, 23, 0, 0, 0, 0, time.UTC),
|
||||
moon.OccultationJupiter, -42.36358358336173, 21.762064240049245, 0,
|
||||
moon.OccultationSearchOptions{LunarLimb: limb.Default()},
|
||||
)
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindPlanetOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
if !events[0].LunarLimbApplied {
|
||||
t.Fatal("LunarLimbApplied = false, want true")
|
||||
}
|
||||
if _, err := LocalPlanetOccultationSVG(events[0], LocalPlanetOccultationSVGOptions{}); err != nil {
|
||||
t.Fatalf("LocalPlanetOccultationSVG() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStarOccultationSVGAcceptsLunarLimbContacts(t *testing.T) {
|
||||
star := moon.StarCoordinate{
|
||||
ID: "Antares", RA: 247.3516666666667, Dec: -26.431944444444444,
|
||||
Epoch: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC), Frame: moon.CoordinateFrameJ2000,
|
||||
ProperMotionRACosDecMasPerYear: -10, ProperMotionDecMasPerYear: -20, ParallaxMas: 24,
|
||||
}
|
||||
date := time.Date(2026, time.February, 11, 0, 0, 0, 0, time.UTC)
|
||||
events, err := moon.FindStarOccultations(date, date.Add(24*time.Hour), star,
|
||||
24.493397847811025, -74.18160307712625, 0,
|
||||
moon.OccultationSearchOptions{LunarLimb: limb.Default()})
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindStarOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
if _, err := LocalStarOccultationSVG(events[0], star, LocalStarOccultationSVGOptions{}); err != nil {
|
||||
t.Fatalf("LocalStarOccultationSVG() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationSVGsDeclareLunarLimbConvention(t *testing.T) {
|
||||
observer := [2]float64{-42.36358358336173, 21.762064240049245}
|
||||
start := time.Date(2034, time.November, 21, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2034, time.November, 23, 0, 0, 0, 0, time.UTC)
|
||||
draw := func(applied bool) string {
|
||||
t.Helper()
|
||||
options := moon.OccultationSearchOptions{}
|
||||
if applied {
|
||||
options.LunarLimb = limb.Default()
|
||||
}
|
||||
events, err := moon.FindPlanetOccultations(start, end, moon.OccultationJupiter,
|
||||
observer[0], observer[1], 0, options)
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindPlanetOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
if events[0].LunarLimbApplied != applied {
|
||||
t.Fatalf("LunarLimbApplied = %v, want %v", events[0].LunarLimbApplied, applied)
|
||||
}
|
||||
diagram, err := LocalPlanetOccultationSVG(events[0], LocalPlanetOccultationSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatalf("LocalPlanetOccultationSVG() error = %v", err)
|
||||
}
|
||||
return diagram
|
||||
}
|
||||
if circular := draw(false); !strings.Contains(circular, "圆月面") {
|
||||
t.Fatal("circular chart does not declare the circular limb")
|
||||
}
|
||||
if corrected := draw(true); !strings.Contains(corrected, "月缘剖面") {
|
||||
t.Fatal("limb chart does not declare the limb profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationPathSVGsDeclareLunarLimbConvention(t *testing.T) {
|
||||
start := time.Date(2034, time.November, 21, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2034, time.November, 23, 0, 0, 0, 0, time.UTC)
|
||||
for _, applied := range []bool{false, true} {
|
||||
options := moon.OccultationPathOptions{Step: 10 * time.Minute, TargetSpacingKM: 400}
|
||||
if applied {
|
||||
options.LunarLimb = limb.Default()
|
||||
}
|
||||
paths, err := moon.FindPlanetOccultationPaths(start, end, moon.OccultationJupiter, options)
|
||||
if err != nil || len(paths) != 1 {
|
||||
t.Fatalf("FindPlanetOccultationPaths() = %d paths, %v; want one", len(paths), err)
|
||||
}
|
||||
if paths[0].LunarLimbApplied != applied {
|
||||
t.Fatalf("LunarLimbApplied = %v, want %v", paths[0].LunarLimbApplied, applied)
|
||||
}
|
||||
want := "月面按圆月面计算。"
|
||||
if applied {
|
||||
want = "界线按月缘剖面修正"
|
||||
}
|
||||
global, err := PlanetOccultationPathSVG(paths[0], PlanetOccultationSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanetOccultationPathSVG() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(global, want) {
|
||||
t.Fatalf("global path chart (limb=%v) missing %q", applied, want)
|
||||
}
|
||||
detailed, err := PlanetOccultationDetailedSVG(paths[0], OccultationDetailedSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanetOccultationDetailedSVG() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(detailed, want) {
|
||||
t.Fatalf("detailed chart (limb=%v) missing %q", applied, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"b612.me/astro"
|
||||
"b612.me/astro/internal/svgasset"
|
||||
"b612.me/astro/internal/svgchart"
|
||||
"b612.me/astro/internal/timenote"
|
||||
"b612.me/astro/moon"
|
||||
)
|
||||
|
||||
const (
|
||||
limbProfileSVGDefaultWidth = 920
|
||||
limbProfileSVGDefaultHeight = 620
|
||||
limbProfileSVGMinimumWidth = 640
|
||||
limbProfileSVGMinimumHeight = 480
|
||||
limbProfileSVGMaxRows = 12
|
||||
// limbProfilePanelGap 栏间距须容下上栏的时刻、位置角两行刻度与下栏上沿的两行接触编号。
|
||||
limbProfilePanelGap = 56.0
|
||||
// limbProfileInsetMinHeight 缩略图(含标题)的最小高度,接触表按它限行。
|
||||
limbProfileInsetMinHeight = 120.0
|
||||
)
|
||||
|
||||
// ErrInvalidStarOccultationLimbProfileSVGOptions 表示精密月掩剖面图选项无效 / invalid precise occultation profile chart options.
|
||||
var ErrInvalidStarOccultationLimbProfileSVGOptions = errors.New("invalid stellar occultation limb profile SVG options")
|
||||
|
||||
// ErrEmptyStarOccultationLimbProfile 表示剖面没有采样,恒星全程在月缘起伏带外 / the profile has no samples; the star stays outside the relief band.
|
||||
var ErrEmptyStarOccultationLimbProfile = errors.New("stellar occultation limb profile has no samples")
|
||||
|
||||
// ErrInvalidStarOccultationLimbProfile 表示剖面数据不是 StarOccultationLimbProfile 能给出的形态 / malformed limb profile data.
|
||||
var ErrInvalidStarOccultationLimbProfile = errors.New("invalid stellar occultation limb profile")
|
||||
|
||||
// StarOccultationLimbProfileSVGOptions 控制精密恒星月掩的月缘剖面图 / options for the precise stellar occultation profile chart.
|
||||
type StarOccultationLimbProfileSVGOptions struct {
|
||||
// Width、Height 为画布尺寸,非正值用默认 / canvas size; non-positive uses defaults.
|
||||
Width int
|
||||
Height int
|
||||
// Title 为空时自动生成 / empty uses an automatic title.
|
||||
Title string
|
||||
// Language 为 "en" 时英文,否则中文 / "en" for English, otherwise Chinese.
|
||||
Language string
|
||||
// Location 控制显示时刻,nil 为 UTC+8 / display zone; nil uses UTC+8.
|
||||
Location *time.Location
|
||||
// TimeScale 为 TimeScaleUT1 时改用 UT1 时刻,此时 Location 须为 nil 或 UTC / label time scale.
|
||||
TimeScale astro.TimeScale
|
||||
}
|
||||
|
||||
// StarOccultationLimbProfileSVG 渲染精密恒星月掩剖面:横轴时刻、纵轴相对参考圆月面高度(km),给出遮挡区间与全部掩入复现 / precise stellar occultation limb profile chart.
|
||||
func StarOccultationLimbProfileSVG(profile moon.StarOccultationLimbProfileResult, options StarOccultationLimbProfileSVGOptions) (string, error) {
|
||||
if options.Width > 0 && options.Width < limbProfileSVGMinimumWidth {
|
||||
return "", fmt.Errorf("%w: width must be zero or at least %d", ErrInvalidStarOccultationLimbProfileSVGOptions, limbProfileSVGMinimumWidth)
|
||||
}
|
||||
if options.Height > 0 && options.Height < limbProfileSVGMinimumHeight {
|
||||
return "", fmt.Errorf("%w: height must be zero or at least %d", ErrInvalidStarOccultationLimbProfileSVGOptions, limbProfileSVGMinimumHeight)
|
||||
}
|
||||
if options.TimeScale == astro.TimeScaleUT1 && options.Location != nil && options.Location != time.UTC {
|
||||
return "", fmt.Errorf("occultation SVG: UT1 labels do not take a non-UTC location")
|
||||
}
|
||||
samples := 0
|
||||
for _, segment := range profile.Segments {
|
||||
samples += len(segment)
|
||||
}
|
||||
if samples < 2 || profile.Occultation.Greatest.IsZero() {
|
||||
return "", ErrEmptyStarOccultationLimbProfile
|
||||
}
|
||||
if err := validateStarOccultationLimbProfile(profile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if options.Width <= 0 {
|
||||
options.Width = limbProfileSVGDefaultWidth
|
||||
}
|
||||
if options.Height <= 0 {
|
||||
options.Height = limbProfileSVGDefaultHeight
|
||||
}
|
||||
if options.TimeScale == astro.TimeScaleUT1 {
|
||||
options.Location = time.UTC
|
||||
profile = moon.StarOccultationLimbProfileInUT1(profile)
|
||||
}
|
||||
if options.Location == nil {
|
||||
options.Location = time.FixedZone("UTC+8", starOccultationSVGDefaultZone)
|
||||
}
|
||||
if strings.EqualFold(options.Language, starOccultationSVGLanguageEnglish) {
|
||||
options.Language = starOccultationSVGLanguageEnglish
|
||||
} else {
|
||||
options.Language = starOccultationSVGLanguageChinese
|
||||
}
|
||||
return renderStarOccultationLimbProfileSVG(profile, options), nil
|
||||
}
|
||||
|
||||
type limbProfilePoint struct {
|
||||
t time.Time
|
||||
limbKM, starKM float64
|
||||
positionAngle float64
|
||||
// skyX、skyY 为恒星相对月心的天空平面坐标(km,向东、向北为正)。
|
||||
skyX, skyY float64
|
||||
}
|
||||
|
||||
// limbProfileHeights 把角量换算成月面上相对参考球的高度(km),恒星与月缘用同一比例。
|
||||
func limbProfileHeights(segment []moon.StarOccultationLimbSample, referenceKM float64) []limbProfilePoint {
|
||||
points := make([]limbProfilePoint, 0, len(segment))
|
||||
for _, sample := range segment {
|
||||
scale := referenceKM / sample.MeanRadiusArcsec
|
||||
points = append(points, limbProfilePoint{
|
||||
t: sample.Time,
|
||||
limbKM: (sample.LimbRadiusArcsec - sample.MeanRadiusArcsec) * scale,
|
||||
starKM: (sample.SeparationArcsec - sample.MeanRadiusArcsec) * scale,
|
||||
positionAngle: sample.PositionAngleDeg,
|
||||
skyX: sample.SeparationArcsec * scale * math.Sin(sample.PositionAngleDeg*math.Pi/180),
|
||||
skyY: sample.SeparationArcsec * scale * math.Cos(sample.PositionAngleDeg*math.Pi/180),
|
||||
})
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
func limbProfileText(language, zh, en string) string {
|
||||
if language == starOccultationSVGLanguageEnglish {
|
||||
return en
|
||||
}
|
||||
return zh
|
||||
}
|
||||
|
||||
func renderStarOccultationLimbProfileSVG(profile moon.StarOccultationLimbProfileResult, options StarOccultationLimbProfileSVGOptions) string {
|
||||
info := profile.Occultation
|
||||
width, height := float64(options.Width), float64(options.Height)
|
||||
language := options.Language
|
||||
segments := make([][]limbProfilePoint, 0, len(profile.Segments))
|
||||
minKM, maxKM := math.Inf(1), math.Inf(-1)
|
||||
for _, segment := range profile.Segments {
|
||||
points := limbProfileHeights(segment, limbProfileReferenceKM(profile))
|
||||
if len(points) < 2 {
|
||||
continue
|
||||
}
|
||||
for _, point := range points {
|
||||
minKM = math.Min(minKM, math.Min(point.limbKM, point.starKM))
|
||||
maxKM = math.Max(maxKM, math.Max(point.limbKM, point.starKM))
|
||||
}
|
||||
segments = append(segments, points)
|
||||
}
|
||||
if !(maxKM > minKM) {
|
||||
minKM, maxKM = -1, 1
|
||||
}
|
||||
padKM := (maxKM - minKM) * 0.08
|
||||
minKM, maxKM = minKM-padKM, maxKM+padKM
|
||||
|
||||
title := options.Title
|
||||
if title == "" {
|
||||
target := info.TargetID
|
||||
if target == "" {
|
||||
target = limbProfileText(language, "恒星", "star")
|
||||
}
|
||||
date := info.Greatest.In(options.Location).Format("2006-01-02")
|
||||
title = date + limbProfileText(language, " 精密月掩月缘剖面 · "+target, " Limb Profile · Occultation of "+target)
|
||||
}
|
||||
site := starOccultationFormatCoordinates(info.Observer.Longitude, info.Observer.Latitude)
|
||||
summary := fmt.Sprintf(limbProfileText(language, "观测点 %s | 椭球高 %.0f 米 | 遮挡 %d 段 | 采样 %s", "Site %s | elevation %.0f m | %d hidden interval(s) | step %s"),
|
||||
site, info.Observer.Height, len(profile.Hidden), profile.Step)
|
||||
direction := limbProfileDirectionText(segments, profile.Hidden, language)
|
||||
|
||||
const top = 136.0
|
||||
tableWidth := 260.0
|
||||
left, right := 70.0, width-tableWidth-48
|
||||
chartTop, chartBottom := top, height-122
|
||||
panels := len(segments)
|
||||
if panels < 1 {
|
||||
panels = 1
|
||||
}
|
||||
panelHeight := (chartBottom - chartTop - limbProfilePanelGap*float64(panels-1)) / float64(panels)
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img" aria-label="%s">`,
|
||||
options.Width, options.Height, options.Width, options.Height, html.EscapeString(title))
|
||||
b.WriteString(`<defs>`)
|
||||
b.WriteString(svgasset.MoonFaceSymbol())
|
||||
b.WriteString(`<marker id="limb-profile-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#d0473b"/></marker>`)
|
||||
b.WriteString(`</defs>`)
|
||||
b.WriteString(`<rect width="100%" height="100%" fill="#efefed"/>`)
|
||||
fmt.Fprintf(&b, `<rect x="22" y="18" width="%.3f" height="%.3f" fill="#ffffff" stroke="#c9c9c6" stroke-width="1.2"/>`, width-44, height-36)
|
||||
fmt.Fprintf(&b, `<text x="%.3f" y="46" fill="#111111" font-family="Georgia, 'Times New Roman', serif" font-size="%d" font-weight="700" text-anchor="middle">%s</text>`,
|
||||
width/2, starOccultationTitleFontSize(title, width), html.EscapeString(svgchart.EllipsizeText(title, width-80, float64(starOccultationTitleFontSize(title, width)))))
|
||||
fmt.Fprintf(&b, `<text x="%.3f" y="76" fill="#222222" font-family="Georgia, 'Times New Roman', serif" font-size="13" text-anchor="middle">%s</text>`,
|
||||
width/2, html.EscapeString(svgchart.EllipsizeText(summary, width-80, 13)))
|
||||
fmt.Fprintf(&b, `<text x="%.3f" y="98" fill="#3b4143" font-family="Georgia, 'Times New Roman', serif" font-size="12" text-anchor="middle">%s</text>`,
|
||||
width/2, html.EscapeString(svgchart.EllipsizeText(direction, width-80, 12)))
|
||||
|
||||
for index, points := range segments {
|
||||
y0 := chartTop + float64(index)*(panelHeight+limbProfilePanelGap)
|
||||
writeLimbProfilePanel(&b, points, profile.Hidden, left, right, y0, y0+panelHeight, minKM, maxKM, options)
|
||||
}
|
||||
// 接触表与图例让出缩略图的最小高度:图例占 84 px,表头 28 px,每行 19 px。
|
||||
maxRows := int((chartBottom + 30 - limbProfileInsetMinHeight - 18 - top - 28 - 84) / 19)
|
||||
if maxRows < 2 {
|
||||
maxRows = 2
|
||||
}
|
||||
if maxRows > limbProfileSVGMaxRows {
|
||||
maxRows = limbProfileSVGMaxRows
|
||||
}
|
||||
legendBottom := writeLimbProfileContacts(&b, profile, right+28, top, width-48, maxRows, options)
|
||||
writeLimbProfileInset(&b, segments, info, limbProfileReferenceKM(profile), right+28, legendBottom+18, width-48, chartBottom+30, language)
|
||||
writeLimbProfileFooter(&b, info, limbProfileReferenceKM(profile), left, height-66, width-left-40, options)
|
||||
b.WriteString(`</svg>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writeLimbProfilePanel(b *strings.Builder, points []limbProfilePoint, hidden []moon.OccultationInterval,
|
||||
left, right, top, bottom, minKM, maxKM float64, options StarOccultationLimbProfileSVGOptions) {
|
||||
start, end := points[0].t, points[len(points)-1].t
|
||||
span := end.Sub(start).Seconds()
|
||||
if span <= 0 {
|
||||
span = 1
|
||||
}
|
||||
x := func(t time.Time) float64 { return left + (right-left)*t.Sub(start).Seconds()/span }
|
||||
y := func(km float64) float64 { return bottom - (bottom-top)*(km-minKM)/(maxKM-minKM) }
|
||||
fmt.Fprintf(b, `<rect x="%.3f" y="%.3f" width="%.3f" height="%.3f" fill="#fafaf8" stroke="#9a9a96" stroke-width="0.8"/>`, left, top, right-left, bottom-top)
|
||||
for _, interval := range hidden {
|
||||
if interval.End.Before(start) || interval.Start.After(end) {
|
||||
continue
|
||||
}
|
||||
x0, x1 := x(laterTime(interval.Start, start)), x(earlierTime(interval.End, end))
|
||||
fmt.Fprintf(b, `<rect x="%.3f" y="%.3f" width="%.3f" height="%.3f" fill="#3b4a5a" fill-opacity="0.16"/>`, x0, top, math.Max(x1-x0, 0.6), bottom-top)
|
||||
}
|
||||
if minKM < 0 && maxKM > 0 {
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#b8b8b4" stroke-dasharray="4 3" stroke-width="0.8"/>`, left, y(0), right, y(0))
|
||||
}
|
||||
var limb, star strings.Builder
|
||||
for index, point := range points {
|
||||
command := "L"
|
||||
if index == 0 {
|
||||
command = "M"
|
||||
}
|
||||
fmt.Fprintf(&limb, "%s%.2f %.2f ", command, x(point.t), y(point.limbKM))
|
||||
fmt.Fprintf(&star, "%s%.2f %.2f ", command, x(point.t), y(point.starKM))
|
||||
}
|
||||
fmt.Fprintf(b, `<path d="%s" fill="none" stroke="#6f5a3a" stroke-width="1.3"/>`, strings.TrimSpace(limb.String()))
|
||||
fmt.Fprintf(b, `<path d="%s" fill="none" stroke="#9b3f36" stroke-width="1.1" stroke-dasharray="5 2"/>`, strings.TrimSpace(star.String()))
|
||||
font := `font-family="Georgia, 'Times New Roman', serif"`
|
||||
// 横轴按整刻度标时刻,下方配该时刻的位置角;刻度线贯穿剖面框作网格。
|
||||
step := limbProfileTickStep(span, right-left)
|
||||
majors := limbProfileTicks(start, end, step, options.Location)
|
||||
for _, tick := range majors {
|
||||
tx := x(tick)
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#e2e2de" stroke-width="0.6"/>`, tx, top, tx, bottom)
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#9a9a96" stroke-width="0.8"/>`, tx, bottom, tx, bottom+3)
|
||||
// 标签夹进剖面框,靠近两端时不越出框线,也不压住框外的 PA 轴标题。
|
||||
label := tick.In(options.Location).Format(limbProfileTickLayout(step))
|
||||
labelX := limbProfileClampLabel(tx, svgchart.EstimatedTextWidth(label, 10), left, right)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" %s font-size="10" text-anchor="middle">%s</text>`, labelX, bottom+14, font, label)
|
||||
angle := limbProfileFormatAngle(limbProfilePositionAngleAt(points, tick))
|
||||
angleX := limbProfileClampLabel(tx, svgchart.EstimatedTextWidth(angle, 9), left, right)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#6f5a3a" %s font-size="9" text-anchor="middle">%s</text>`, angleX, bottom+26, font, angle)
|
||||
}
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#6f5a3a" %s font-size="9" text-anchor="end">PA</text>`, left-4, bottom+26, font)
|
||||
minor := step / 6
|
||||
if step%time.Minute != 0 {
|
||||
minor = step / 5
|
||||
}
|
||||
major := make(map[int64]bool, len(majors))
|
||||
for _, tick := range majors {
|
||||
major[tick.UnixNano()] = true
|
||||
}
|
||||
for _, tick := range limbProfileTicks(start, end, minor, options.Location) {
|
||||
if major[tick.UnixNano()] {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#b8b8b4" stroke-width="0.6"/>`, x(tick), bottom, x(tick), bottom+2)
|
||||
}
|
||||
for _, label := range limbProfileContactLabels(hidden, start, end, x, left, right) {
|
||||
labelY := top - 4 - float64(label.row)*11
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#3b4a5a" stroke-width="0.8"/>`, label.x, labelY+2, label.contactX, top+6)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#3b4a5a" %s font-size="10" font-weight="700" text-anchor="middle">%d</text>`, label.x, labelY, font, label.number)
|
||||
}
|
||||
if ratio := limbProfileExaggeration(points, right-left, bottom-top, maxKM-minKM); ratio > 1.5 {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#747c7d" %s font-size="10" text-anchor="end">%s</text>`, right-6, top+14, font,
|
||||
html.EscapeString(fmt.Sprintf(limbProfileText(options.Language, "纵向放大约 %.0f 倍", "vertical ×%.0f"), ratio)))
|
||||
} else if ratio > 0 && ratio < 1/1.5 {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#747c7d" %s font-size="10" text-anchor="end">%s</text>`, right-6, top+14, font,
|
||||
html.EscapeString(fmt.Sprintf(limbProfileText(options.Language, "横向放大约 %.0f 倍", "horizontal ×%.0f"), 1/ratio)))
|
||||
}
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" %s font-size="10" text-anchor="end">%+.1f</text>`, left-4, top+9, font, maxKM)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" %s font-size="10" text-anchor="end">%+.1f</text>`, left-4, bottom, font, minKM)
|
||||
}
|
||||
|
||||
const limbProfileContactLabelGap = 14.0
|
||||
|
||||
type limbProfileContactLabel struct {
|
||||
number int
|
||||
row int
|
||||
x, contactX float64
|
||||
}
|
||||
|
||||
// limbProfileContactLabels 接触编号两行排位后逐行自右向左回推,使其落在 [left, right] 内且同行间距不小于 14 px。
|
||||
func limbProfileContactLabels(hidden []moon.OccultationInterval, start, end time.Time, x func(time.Time) float64, left, right float64) []limbProfileContactLabel {
|
||||
var labels []limbProfileContactLabel
|
||||
rows := [2][]int{}
|
||||
rowEnd := [2]float64{math.Inf(-1), math.Inf(-1)}
|
||||
number := 0
|
||||
for _, interval := range hidden {
|
||||
for _, contact := range []time.Time{interval.Start, interval.End} {
|
||||
number++
|
||||
if contact.Before(start) || contact.After(end) {
|
||||
continue
|
||||
}
|
||||
cx := x(contact)
|
||||
row := 0
|
||||
if rowEnd[1] < rowEnd[0] {
|
||||
row = 1
|
||||
}
|
||||
if cx-rowEnd[0] >= limbProfileContactLabelGap {
|
||||
row = 0
|
||||
}
|
||||
labelX := math.Max(cx, rowEnd[row]+limbProfileContactLabelGap)
|
||||
rowEnd[row] = labelX
|
||||
rows[row] = append(rows[row], len(labels))
|
||||
labels = append(labels, limbProfileContactLabel{number: number, row: row, x: labelX, contactX: cx})
|
||||
}
|
||||
}
|
||||
halfWidth := func(label limbProfileContactLabel) float64 {
|
||||
return svgchart.EstimatedTextWidth(fmt.Sprint(label.number), 10) / 2
|
||||
}
|
||||
for _, indices := range rows {
|
||||
if len(indices) == 0 {
|
||||
continue
|
||||
}
|
||||
limit := math.Inf(1)
|
||||
for k := len(indices) - 1; k >= 0; k-- {
|
||||
label := &labels[indices[k]]
|
||||
label.x = math.Min(label.x, math.Min(limit, right-halfWidth(*label)))
|
||||
limit = label.x - limbProfileContactLabelGap
|
||||
}
|
||||
// 一行塞不下时均分整行,间距小于 14 px 也不越框。
|
||||
first, last := labels[indices[0]], labels[indices[len(indices)-1]]
|
||||
if low, high := left+halfWidth(first), right-halfWidth(last); first.x < low && len(indices) > 1 {
|
||||
for k, index := range indices {
|
||||
labels[index].x = low + (high-low)*float64(k)/float64(len(indices)-1)
|
||||
}
|
||||
} else if first.x < low {
|
||||
labels[indices[0]].x = low
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func laterTime(a, b time.Time) time.Time {
|
||||
if a.After(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func earlierTime(a, b time.Time) time.Time {
|
||||
if a.Before(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// writeLimbProfileContacts 返回图例底边,供缩略图接在下方。
|
||||
func writeLimbProfileContacts(b *strings.Builder, profile moon.StarOccultationLimbProfileResult, left, top, right float64, maxRows int, options StarOccultationLimbProfileSVGOptions) float64 {
|
||||
language := options.Language
|
||||
font := `font-family="Georgia, 'Times New Roman', serif"`
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111111" %s font-size="13" font-weight="700">%s</text>`, left, top+4, font,
|
||||
html.EscapeString(limbProfileText(language, "掩入 / 复现", "Disappearances / reappearances")))
|
||||
rows := make([][2]string, 0, 2*len(profile.Hidden))
|
||||
for _, interval := range profile.Hidden {
|
||||
rows = append(rows,
|
||||
[2]string{fmt.Sprintf("%d %s", len(rows)+1, limbProfileText(language, "掩入", "Disappears")), interval.Start.In(options.Location).Format("15:04:05.00")},
|
||||
[2]string{fmt.Sprintf("%d %s", len(rows)+2, limbProfileText(language, "复现", "Reappears")), interval.End.In(options.Location).Format("15:04:05.00")})
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
rows = append(rows, [2]string{limbProfileText(language, "未发生遮挡", "Not occulted"), ""})
|
||||
}
|
||||
overflow := 0
|
||||
if len(rows) > maxRows {
|
||||
overflow = len(rows) - (maxRows - 1)
|
||||
rows = rows[:maxRows-1]
|
||||
}
|
||||
for index, row := range rows {
|
||||
baseline := top + 28 + float64(index)*19
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#3b4143" %s font-size="12">%s</text>`, left, baseline, font, html.EscapeString(row[0]))
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#222222" %s font-size="12" text-anchor="end">%s</text>`, right, baseline, font, row[1])
|
||||
}
|
||||
if overflow > 0 {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#747c7d" %s font-size="11">%s</text>`, left, top+28+float64(len(rows))*19, font,
|
||||
html.EscapeString(fmt.Sprintf(limbProfileText(language, "另有 %d 个接触未列出", "%d more contacts not listed"), overflow)))
|
||||
}
|
||||
listed := len(rows)
|
||||
if overflow > 0 {
|
||||
listed++
|
||||
}
|
||||
legendTop := top + 40 + float64(listed)*19
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#6f5a3a" stroke-width="1.3"/>`, left, legendTop, left+22, legendTop)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#3b4143" %s font-size="11">%s</text>`, left+28, legendTop+4, font,
|
||||
html.EscapeString(limbProfileText(language, "恒星方向月缘高度", "Limb height toward the star")))
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#9b3f36" stroke-width="1.1" stroke-dasharray="5 2"/>`, left, legendTop+18, left+22, legendTop+18)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#3b4143" %s font-size="11">%s</text>`, left+28, legendTop+22, font,
|
||||
html.EscapeString(limbProfileText(language, "恒星距参考圆月面(km)", "Star above the mean limb (km)")))
|
||||
fmt.Fprintf(b, `<rect x="%.3f" y="%.3f" width="22" height="10" fill="#3b4a5a" fill-opacity="0.16"/>`, left, legendTop+31)
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#3b4143" %s font-size="11">%s</text>`, left+28, legendTop+40, font,
|
||||
html.EscapeString(limbProfileText(language, "恒星被遮挡", "Star hidden")))
|
||||
return legendTop + 44
|
||||
}
|
||||
|
||||
func writeLimbProfileFooter(b *strings.Builder, info moon.StarOccultationInfo, referenceKM, left, baseline, maxWidth float64, options StarOccultationLimbProfileSVGOptions) {
|
||||
note := limbProfileText(options.Language,
|
||||
"纵轴为月面上相对参考球 %.1f km 的高度;月缘按剖面表插值,短于采样步长的遮挡可能漏检。",
|
||||
"Heights are relative to the %.1f km reference sphere; the limb is interpolated from the profile table and events shorter than the step may be missed.")
|
||||
note = fmt.Sprintf(note, referenceKM)
|
||||
lines := svgchart.TruncateTextLines(svgchart.WrapTextBalanced(note, maxWidth, 10), maxWidth, 10, 2)
|
||||
lines = append(lines, svgchart.EllipsizeText(timenote.Scale(options.TimeScale, info.Greatest, options.Location, options.Language), maxWidth, 10))
|
||||
font := `font-family="Georgia, 'Times New Roman', serif"`
|
||||
for index, line := range lines {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#747c7d" %s font-size="10">%s</text>`, left, baseline+float64(index)*14, font, html.EscapeString(line))
|
||||
}
|
||||
}
|
||||
|
||||
// limbProfileTrackKM 返回恒星在天空平面上走过的弦长(km,按月面尺度)。
|
||||
func limbProfileTrackKM(points []limbProfilePoint) float64 {
|
||||
first, last := points[0], points[len(points)-1]
|
||||
return math.Hypot(last.skyX-first.skyX, last.skyY-first.skyY)
|
||||
}
|
||||
|
||||
// limbProfileExaggeration 纵向放大倍数:横向每像素代表的月面距离与纵向之比。
|
||||
func limbProfileExaggeration(points []limbProfilePoint, widthPX, heightPX, spanKM float64) float64 {
|
||||
if widthPX <= 0 || heightPX <= 0 || spanKM <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (limbProfileTrackKM(points) / widthPX) / (spanKM / heightPX)
|
||||
}
|
||||
|
||||
// limbProfileCompass 把位置角换成八方位名。
|
||||
func limbProfileCompass(positionAngle float64, language string) string {
|
||||
zh := []string{"北", "东北", "东", "东南", "南", "西南", "西", "西北"}
|
||||
en := []string{"N", "NE", "E", "SE", "S", "SW", "W", "NW"}
|
||||
index := int(math.Floor(math.Mod(positionAngle+22.5+720, 360) / 45))
|
||||
if language == starOccultationSVGLanguageEnglish {
|
||||
return en[index]
|
||||
}
|
||||
return zh[index]
|
||||
}
|
||||
|
||||
func limbProfileDirectionText(segments [][]limbProfilePoint, hidden []moon.OccultationInterval, language string) string {
|
||||
if len(segments) == 0 {
|
||||
return ""
|
||||
}
|
||||
first := segments[0][0]
|
||||
lastSegment := segments[len(segments)-1]
|
||||
last := lastSegment[len(lastSegment)-1]
|
||||
if len(segments) == 1 {
|
||||
return fmt.Sprintf(limbProfileText(language,
|
||||
"恒星沿月缘自 PA %s(%s)移向 PA %s(%s),横向约 %.0f km;PA 自天北向东量",
|
||||
"The star runs along the limb from PA %s (%s) to PA %s (%s), about %.0f km; PA measured from north through east"),
|
||||
limbProfileFormatAngle(first.positionAngle), limbProfileCompass(first.positionAngle, language),
|
||||
limbProfileFormatAngle(last.positionAngle), limbProfileCompass(last.positionAngle, language), limbProfileTrackKM(segments[0]))
|
||||
}
|
||||
// 深掩时取首次掩入与最后复现时刻的位置角,而非两栏端点。
|
||||
immersion, emersion := first.positionAngle, last.positionAngle
|
||||
if len(hidden) > 0 {
|
||||
immersion = limbProfilePositionAngleAt(segments[0], hidden[0].Start)
|
||||
emersion = limbProfilePositionAngleAt(lastSegment, hidden[len(hidden)-1].End)
|
||||
}
|
||||
return fmt.Sprintf(limbProfileText(language,
|
||||
"恒星于 PA %s(%s)掩入、PA %s(%s)复现;上下两栏分别为掩入侧与复现侧,PA 自天北向东量",
|
||||
"The star disappears at PA %s (%s) and reappears at PA %s (%s); panels show the two sides; PA measured from north through east"),
|
||||
limbProfileFormatAngle(immersion), limbProfileCompass(immersion, language),
|
||||
limbProfileFormatAngle(emersion), limbProfileCompass(emersion, language))
|
||||
}
|
||||
|
||||
// limbProfileFormatAngle 按一位小数格式化位置角,舍入到 360.0° 时归一为 0.0°。
|
||||
func limbProfileFormatAngle(positionAngle float64) string {
|
||||
rounded := math.Round(positionAngle*10) / 10
|
||||
if rounded >= 360 {
|
||||
rounded -= 360
|
||||
}
|
||||
return fmt.Sprintf("%.1f°", rounded)
|
||||
}
|
||||
|
||||
// writeLimbProfileInset 画掩甚时刻的站心月相缩略图(上北左东),加粗放大的月缘段并用箭头标出恒星运动方向。
|
||||
func writeLimbProfileInset(b *strings.Builder, segments [][]limbProfilePoint, info moon.StarOccultationInfo, referenceKM, left, top, right, bottom float64, language string) {
|
||||
size := math.Min(right-left, bottom-top-26)
|
||||
if size < 60 || len(segments) == 0 {
|
||||
return
|
||||
}
|
||||
font := `font-family="Georgia, 'Times New Roman', serif"`
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111111" %s font-size="12" font-weight="700">%s</text>`, left, top+10, font,
|
||||
html.EscapeString(svgchart.EllipsizeText(limbProfileText(language, "月缘位置(上北左东)", "On the limb (N up, E left)"), right-left, 12)))
|
||||
radius := size/2 - 16
|
||||
cx, cy := left+(right-left)/2, top+26+size/2
|
||||
writeLocalOccultationMoon(b, cx, cy, radius, "limb-profile-moon",
|
||||
localOccultationMoonAppearanceAt(info.Greatest, info.Observer), "limb-profile-moon-phase")
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" %s font-size="10" text-anchor="middle">%s</text>`, cx, cy-radius-4, font, limbProfileText(language, "北", "N"))
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" %s font-size="10" text-anchor="end">%s</text>`, cx-radius-4, cy+4, font, limbProfileText(language, "东", "E"))
|
||||
limbPoint := func(positionAngle, scale float64) (float64, float64) {
|
||||
angle := positionAngle * math.Pi / 180
|
||||
return cx - scale*math.Sin(angle), cy - scale*math.Cos(angle)
|
||||
}
|
||||
for _, points := range segments {
|
||||
first, last := points[0], points[len(points)-1]
|
||||
x0, y0 := limbPoint(first.positionAngle, radius)
|
||||
x1, y1 := limbPoint(last.positionAngle, radius)
|
||||
sweep := 0
|
||||
if signedPositionAngleStep(first.positionAngle, last.positionAngle) < 0 {
|
||||
sweep = 1
|
||||
}
|
||||
fmt.Fprintf(b, `<path d="M%.2f %.2f A%.2f %.2f 0 0 %d %.2f %.2f" fill="none" stroke="#e0a32e" stroke-width="3.6"/>`, x0, y0, radius, radius, sweep, x1, y1)
|
||||
}
|
||||
// 恒星轨迹按真实比例画在月盘坐标里,深掩时两段之间连成穿过月面的弦。
|
||||
kmToPX := radius / referenceKM
|
||||
firstPoint := segments[0][0]
|
||||
lastSegment := segments[len(segments)-1]
|
||||
lastPoint := lastSegment[len(lastSegment)-1]
|
||||
fmt.Fprintf(b, `<line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" stroke="#d0473b" stroke-width="1.6" marker-end="url(#limb-profile-arrow)"/>`,
|
||||
cx-firstPoint.skyX*kmToPX, cy-firstPoint.skyY*kmToPX, cx-lastPoint.skyX*kmToPX, cy-lastPoint.skyY*kmToPX)
|
||||
}
|
||||
|
||||
// signedPositionAngleStep 返回自 from 到 to 的位置角变化(度),取 (−180, 180];负值为顺时针(向西)。
|
||||
func signedPositionAngleStep(from, to float64) float64 {
|
||||
delta := math.Mod(to-from+540, 360) - 180
|
||||
return delta
|
||||
}
|
||||
|
||||
// limbProfileTickStep 选使相邻时刻标签不重叠的最小整刻度步长。
|
||||
func limbProfileTickStep(spanSeconds, widthPX float64) time.Duration {
|
||||
for _, step := range []time.Duration{5 * time.Second, 10 * time.Second, 15 * time.Second, 20 * time.Second, 30 * time.Second,
|
||||
time.Minute, 2 * time.Minute, 5 * time.Minute, 10 * time.Minute, 15 * time.Minute, 30 * time.Minute, time.Hour} {
|
||||
if widthPX*step.Seconds()/spanSeconds >= 56 {
|
||||
return step
|
||||
}
|
||||
}
|
||||
return 2 * time.Hour
|
||||
}
|
||||
|
||||
func limbProfileTickLayout(step time.Duration) string {
|
||||
if step%time.Minute == 0 {
|
||||
return "15:04"
|
||||
}
|
||||
return "15:04:05"
|
||||
}
|
||||
|
||||
// limbProfilePositionAngleAt 在相邻样本间线性插值位置角,跨 0° 时按最短方向。
|
||||
func limbProfilePositionAngleAt(points []limbProfilePoint, at time.Time) float64 {
|
||||
for index := 1; index < len(points); index++ {
|
||||
if points[index].t.Before(at) {
|
||||
continue
|
||||
}
|
||||
previous, next := points[index-1], points[index]
|
||||
span := next.t.Sub(previous.t).Seconds()
|
||||
if span <= 0 {
|
||||
return next.positionAngle
|
||||
}
|
||||
fraction := at.Sub(previous.t).Seconds() / span
|
||||
return math.Mod(previous.positionAngle+fraction*signedPositionAngleStep(previous.positionAngle, next.positionAngle)+360, 360)
|
||||
}
|
||||
return points[len(points)-1].positionAngle
|
||||
}
|
||||
|
||||
// limbProfileTicks 返回 [start, end] 内按显示时区本地时钟对齐 step 的全部刻度;每个刻度按它自身所在时刻的时区偏移判断,跨夏令时切换也落在当地整刻度上。
|
||||
func limbProfileTicks(start, end time.Time, step time.Duration, location *time.Location) []time.Time {
|
||||
// ceil 返回不早于 at、在当地时钟上是 step 整数倍的第一个时刻;结果若落到另一偏移下,按结果的偏移重算一次。
|
||||
ceil := func(at time.Time) time.Time {
|
||||
withOffset := func(at time.Time, offset int) time.Time {
|
||||
shift := time.Duration(offset) * time.Second
|
||||
tick := at.Add(shift).Truncate(step).Add(-shift)
|
||||
if tick.Before(at) {
|
||||
tick = tick.Add(step)
|
||||
}
|
||||
return tick
|
||||
}
|
||||
_, offset := at.In(location).Zone()
|
||||
tick := withOffset(at, offset)
|
||||
if _, actual := tick.In(location).Zone(); actual != offset {
|
||||
tick = withOffset(at, actual)
|
||||
}
|
||||
return tick
|
||||
}
|
||||
var ticks []time.Time
|
||||
for tick := ceil(start); !tick.After(end) && len(ticks) < 1000; tick = ceil(tick.Add(step)) {
|
||||
ticks = append(ticks, tick)
|
||||
}
|
||||
return ticks
|
||||
}
|
||||
|
||||
// limbProfileClampLabel 返回中锚标签的 x,使宽 width 的标签整体落在 [left, right] 内。
|
||||
func limbProfileClampLabel(x, width, left, right float64) float64 {
|
||||
return math.Max(left+width/2, math.Min(right-width/2, x))
|
||||
}
|
||||
|
||||
// limbProfileReferenceKM 剖面结果声明的参考月球半径,旧结果未填时用 1737.4 km。
|
||||
func limbProfileReferenceKM(profile moon.StarOccultationLimbProfileResult) float64 {
|
||||
if profile.ReferenceRadiusKM > 0 && !math.IsInf(profile.ReferenceRadiusKM, 0) {
|
||||
return profile.ReferenceRadiusKM
|
||||
}
|
||||
return 1737.4
|
||||
}
|
||||
|
||||
// validateStarOccultationLimbProfile 拒绝 StarOccultationLimbProfile 给不出的数据:至多两栏、每栏至少两个样本且时间递增、角量有限、遮挡区间有序。
|
||||
func validateStarOccultationLimbProfile(profile moon.StarOccultationLimbProfileResult) error {
|
||||
if len(profile.Segments) > 2 {
|
||||
return fmt.Errorf("%w: at most two segments, got %d", ErrInvalidStarOccultationLimbProfile, len(profile.Segments))
|
||||
}
|
||||
finiteValue := func(value float64) bool { return !math.IsNaN(value) && !math.IsInf(value, 0) }
|
||||
if !finiteValue(profile.ReferenceRadiusKM) || profile.ReferenceRadiusKM < 0 {
|
||||
return fmt.Errorf("%w: reference radius must be finite and non-negative", ErrInvalidStarOccultationLimbProfile)
|
||||
}
|
||||
for index, segment := range profile.Segments {
|
||||
if len(segment) < 2 {
|
||||
return fmt.Errorf("%w: segment %d has fewer than two samples", ErrInvalidStarOccultationLimbProfile, index)
|
||||
}
|
||||
for sampleIndex, sample := range segment {
|
||||
if !finiteValue(sample.SeparationArcsec) || !finiteValue(sample.LimbRadiusArcsec) ||
|
||||
!finiteValue(sample.MeanRadiusArcsec) || sample.MeanRadiusArcsec <= 0 || !finiteValue(sample.PositionAngleDeg) {
|
||||
return fmt.Errorf("%w: segment %d sample %d has non-finite geometry", ErrInvalidStarOccultationLimbProfile, index, sampleIndex)
|
||||
}
|
||||
if sampleIndex > 0 && !sample.Time.After(segment[sampleIndex-1].Time) {
|
||||
return fmt.Errorf("%w: segment %d samples must increase in time", ErrInvalidStarOccultationLimbProfile, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
for index, interval := range profile.Hidden {
|
||||
if !interval.End.After(interval.Start) || (index > 0 && interval.Start.Before(profile.Hidden[index-1].End)) {
|
||||
return fmt.Errorf("%w: hidden intervals must be ordered", ErrInvalidStarOccultationLimbProfile)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package svg
|
||||
|
||||
// 精密月掩剖面图:每段遮挡都要出现在接触表里,无样本或非法选项时返回错误。
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro"
|
||||
"b612.me/astro/internal/svgchart"
|
||||
"b612.me/astro/moon"
|
||||
"b612.me/astro/moon/limb"
|
||||
)
|
||||
|
||||
var limbProfileTestStar = moon.StarCoordinate{
|
||||
ID: "Antares", RA: 247.3516666666667, Dec: -26.431944444444444,
|
||||
Epoch: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC), Frame: moon.CoordinateFrameJ2000,
|
||||
ProperMotionRACosDecMasPerYear: -10, ProperMotionDecMasPerYear: -20, ParallaxMas: 24,
|
||||
}
|
||||
|
||||
func limbProfileTestGraze(t *testing.T) moon.StarOccultationLimbProfileResult {
|
||||
t.Helper()
|
||||
start := time.Date(2026, time.February, 10, 12, 0, 0, 0, time.UTC)
|
||||
events, err := moon.FindStarOccultations(start, start.Add(36*time.Hour), limbProfileTestStar,
|
||||
41.5907, -55.07036, 0, moon.OccultationSearchOptions{LunarLimb: limb.Default()})
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindStarOccultations() = %d events, %v; want one", len(events), err)
|
||||
}
|
||||
profile, err := moon.StarOccultationLimbProfile(events[0], limbProfileTestStar, limb.Default(), moon.StarOccultationLimbProfileOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(profile.Hidden) < 2 {
|
||||
t.Fatalf("hidden intervals = %d, want a multi-contact graze", len(profile.Hidden))
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGListsEveryContact(t *testing.T) {
|
||||
profile := limbProfileTestGraze(t)
|
||||
for _, language := range []string{"", "en"} {
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Language: language, Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(diagram, "<svg") || !strings.HasSuffix(diagram, "</svg>") {
|
||||
t.Fatal("output is not a complete SVG document")
|
||||
}
|
||||
for _, interval := range profile.Hidden {
|
||||
for _, instant := range []time.Time{interval.Start, interval.End} {
|
||||
if label := instant.UTC().Format("15:04:05.00"); !strings.Contains(diagram, label) {
|
||||
t.Errorf("language %q: contact %s missing", language, label)
|
||||
}
|
||||
}
|
||||
}
|
||||
if got := strings.Count(diagram, `fill-opacity="0.16"`); got < len(profile.Hidden) {
|
||||
t.Errorf("language %q: %d shaded rects, want at least %d", language, got, len(profile.Hidden))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGDeclaresUT1(t *testing.T) {
|
||||
profile := limbProfileTestGraze(t)
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{TimeScale: astro.TimeScaleUT1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(diagram, "UT1") {
|
||||
t.Error("UT1 chart does not declare the time scale")
|
||||
}
|
||||
if _, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{TimeScale: astro.TimeScaleUT1, Location: time.FixedZone("x", 3600)}); err == nil {
|
||||
t.Error("UT1 with a non-UTC location accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGRejectsBadInput(t *testing.T) {
|
||||
if _, err := StarOccultationLimbProfileSVG(moon.StarOccultationLimbProfileResult{}, StarOccultationLimbProfileSVGOptions{}); err == nil {
|
||||
t.Error("empty profile accepted")
|
||||
}
|
||||
profile := limbProfileTestGraze(t)
|
||||
if _, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Width: 100}); err == nil {
|
||||
t.Error("tiny width accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGTextStaysInsideFrame(t *testing.T) {
|
||||
profile := limbProfileTestGraze(t)
|
||||
text := regexp.MustCompile(`<text x="([0-9.]+)"[^>]*font-size="([0-9]+)"([^>]*)>([^<]*)</text>`)
|
||||
for _, language := range []string{"zh", "en"} {
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Language: language, Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, match := range text.FindAllStringSubmatch(diagram, -1) {
|
||||
x, _ := strconv.ParseFloat(match[1], 64)
|
||||
size, _ := strconv.ParseFloat(match[2], 64)
|
||||
width := svgchart.EstimatedTextWidth(match[4], size)
|
||||
low, high := x, x+width
|
||||
if strings.Contains(match[3], "middle") {
|
||||
low, high = x-width/2, x+width/2
|
||||
} else if strings.Contains(match[3], `"end"`) {
|
||||
low, high = x-width, x
|
||||
}
|
||||
if low < 22 || high > limbProfileSVGDefaultWidth-22 {
|
||||
t.Errorf("language %q: text [%.0f, %.0f] leaves the frame: %s", language, low, high, match[4])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGDrawsPhasedMoonAndNumberedContacts(t *testing.T) {
|
||||
profile := limbProfileTestGraze(t)
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(diagram, `href="#le-moon"`) || !strings.Contains(diagram, `class="limb-profile-moon"`) {
|
||||
t.Fatal("inset does not use the textured, phased Moon")
|
||||
}
|
||||
if strings.Contains(diagram, `data-illumination="1.000000"`) {
|
||||
t.Error("inset ignores the lunar phase")
|
||||
}
|
||||
for number := 1; number <= 2*len(profile.Hidden); number++ {
|
||||
if !strings.Contains(diagram, `font-weight="700" text-anchor="middle">`+strconv.Itoa(number)+`</text>`) {
|
||||
t.Errorf("contact %d is not numbered on the profile", number)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func limbProfileTestAt(t *testing.T, latitude float64) moon.StarOccultationLimbProfileResult {
|
||||
t.Helper()
|
||||
info := moon.StarOccultationInfo{Greatest: time.Date(2026, time.February, 11, 3, 47, 15, 0, time.UTC),
|
||||
Observer: moon.Observer{Longitude: 41.5907, Latitude: latitude}}
|
||||
if latitude < -55.5 {
|
||||
start := time.Date(2026, time.February, 10, 12, 0, 0, 0, time.UTC)
|
||||
events, err := moon.FindStarOccultations(start, start.Add(36*time.Hour), limbProfileTestStar, 41.5907, latitude, 0,
|
||||
moon.OccultationSearchOptions{LunarLimb: limb.Default()})
|
||||
if err != nil || len(events) != 1 {
|
||||
t.Fatalf("FindStarOccultations() = %d events, %v", len(events), err)
|
||||
}
|
||||
info = events[0]
|
||||
}
|
||||
profile, err := moon.StarOccultationLimbProfile(info, limbProfileTestStar, limb.Default(), moon.StarOccultationLimbProfileOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
type limbProfileTestText struct {
|
||||
x, y, width float64
|
||||
text string
|
||||
}
|
||||
|
||||
func limbProfileTestTexts(diagram string) []limbProfileTestText {
|
||||
pattern := regexp.MustCompile(`<text x="([0-9.]+)" y="([0-9.]+)"[^>]*font-size="([0-9]+)"([^>]*)>([^<]*)</text>`)
|
||||
var texts []limbProfileTestText
|
||||
for _, match := range pattern.FindAllStringSubmatch(diagram, -1) {
|
||||
x, _ := strconv.ParseFloat(match[1], 64)
|
||||
y, _ := strconv.ParseFloat(match[2], 64)
|
||||
size, _ := strconv.ParseFloat(match[3], 64)
|
||||
width := svgchart.EstimatedTextWidth(match[5], size)
|
||||
if strings.Contains(match[4], "middle") {
|
||||
x -= width / 2
|
||||
} else if strings.Contains(match[4], `"end"`) {
|
||||
x -= width
|
||||
}
|
||||
texts = append(texts, limbProfileTestText{x, y, width, match[5]})
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGDeepCase(t *testing.T) {
|
||||
profile := limbProfileTestAt(t, -57.0)
|
||||
if len(profile.Segments) != 2 || len(profile.Hidden) != 1 || profile.Occultation.Type != moon.OccultationTotal {
|
||||
t.Fatalf("deep profile: %d segments, %d intervals, type %v", len(profile.Segments), len(profile.Hidden), profile.Occultation.Type)
|
||||
}
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
texts := limbProfileTestTexts(diagram)
|
||||
for i, a := range texts {
|
||||
for _, b := range texts[i+1:] {
|
||||
if math.Abs(a.y-b.y) < 9 && a.x < b.x+b.width && b.x < a.x+a.width {
|
||||
t.Errorf("texts overlap: %q at (%.0f,%.0f) and %q at (%.0f,%.0f)", a.text, a.x, a.y, b.text, b.x, b.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
immersion := profile.Hidden[0].Start
|
||||
want := ""
|
||||
for _, sample := range profile.Segments[0] {
|
||||
if !sample.Time.Before(immersion) {
|
||||
want = fmt.Sprintf("PA %.1f°", sample.PositionAngleDeg)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !strings.Contains(diagram, want) {
|
||||
t.Errorf("direction text does not use the immersion position angle %s", want)
|
||||
}
|
||||
if !strings.Contains(diagram, "横向放大约") {
|
||||
t.Error("compressed vertical scale is not labelled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGNotOcculted(t *testing.T) {
|
||||
profile := limbProfileTestAt(t, -55.07036+0.8/111.2)
|
||||
if len(profile.Hidden) != 0 || profile.Occultation.Type != moon.OccultationNone {
|
||||
t.Fatalf("near-miss profile: %d intervals, type %v", len(profile.Hidden), profile.Occultation.Type)
|
||||
}
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(diagram, "未发生遮挡") {
|
||||
t.Error("not-occulted chart does not say so")
|
||||
}
|
||||
if _, err := StarOccultationLimbProfileSVG(moon.StarOccultationLimbProfileResult{}, StarOccultationLimbProfileSVGOptions{}); !errors.Is(err, ErrEmptyStarOccultationLimbProfile) {
|
||||
t.Errorf("empty profile error = %v, want ErrEmptyStarOccultationLimbProfile", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimbProfileHelpers(t *testing.T) {
|
||||
kathmandu := time.FixedZone("UTC+5:45", 5*3600+45*60)
|
||||
start := time.Date(2026, 2, 11, 3, 3, 7, 0, time.UTC)
|
||||
for _, step := range []time.Duration{10 * time.Minute, time.Hour} {
|
||||
ticks := limbProfileTicks(start, start.Add(3*time.Hour), step, kathmandu)
|
||||
if len(ticks) == 0 || ticks[0].Before(start) {
|
||||
t.Fatalf("step %v: ticks %v", step, ticks)
|
||||
}
|
||||
for _, tick := range ticks {
|
||||
local := tick.In(kathmandu)
|
||||
if local.Sub(time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, kathmandu))%step != 0 {
|
||||
t.Errorf("step %v: tick %v not aligned in the display zone", step, local)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 豪勋爵岛夏令时只拨 30 分钟:切换后 20 分钟刻度仍须落在当地整 20 分上。
|
||||
lordHowe, err := time.LoadLocation("Australia/Lord_Howe")
|
||||
if err == nil {
|
||||
switchStart := time.Date(2026, 4, 4, 14, 0, 0, 0, time.UTC)
|
||||
for _, tick := range limbProfileTicks(switchStart, switchStart.Add(2*time.Hour), 20*time.Minute, lordHowe) {
|
||||
if local := tick.In(lordHowe); local.Minute()%20 != 0 || local.Second() != 0 {
|
||||
t.Errorf("Lord Howe tick %v is not on a local 20-minute mark", local)
|
||||
}
|
||||
}
|
||||
}
|
||||
if got := limbProfileFormatAngle(359.96); got != "0.0°" {
|
||||
t.Errorf("359.96 formats as %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGRejectsMalformedProfiles(t *testing.T) {
|
||||
good := limbProfileTestGraze(t)
|
||||
clone := func() moon.StarOccultationLimbProfileResult {
|
||||
profile := good
|
||||
profile.Segments = make([][]moon.StarOccultationLimbSample, len(good.Segments))
|
||||
for index, segment := range good.Segments {
|
||||
profile.Segments[index] = append([]moon.StarOccultationLimbSample(nil), segment...)
|
||||
}
|
||||
profile.Hidden = append([]moon.OccultationInterval(nil), good.Hidden...)
|
||||
return profile
|
||||
}
|
||||
cases := map[string]func(*moon.StarOccultationLimbProfileResult){
|
||||
"NaN position angle": func(p *moon.StarOccultationLimbProfileResult) { p.Segments[0][3].PositionAngleDeg = math.NaN() },
|
||||
"NaN mean radius": func(p *moon.StarOccultationLimbProfileResult) { p.Segments[0][3].MeanRadiusArcsec = math.NaN() },
|
||||
"Inf separation": func(p *moon.StarOccultationLimbProfileResult) { p.Segments[0][3].SeparationArcsec = math.Inf(1) },
|
||||
"five segments": func(p *moon.StarOccultationLimbProfileResult) {
|
||||
for len(p.Segments) < 5 {
|
||||
p.Segments = append(p.Segments, p.Segments[0])
|
||||
}
|
||||
},
|
||||
"time going back": func(p *moon.StarOccultationLimbProfileResult) {
|
||||
p.Segments[0][4].Time = p.Segments[0][2].Time
|
||||
},
|
||||
"reversed interval": func(p *moon.StarOccultationLimbProfileResult) {
|
||||
p.Hidden[0].Start, p.Hidden[0].End = p.Hidden[0].End, p.Hidden[0].Start
|
||||
},
|
||||
}
|
||||
for name, mutate := range cases {
|
||||
profile := clone()
|
||||
mutate(&profile)
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if !errors.Is(err, ErrInvalidStarOccultationLimbProfile) {
|
||||
t.Errorf("%s: err = %v, want ErrInvalidStarOccultationLimbProfile", name, err)
|
||||
}
|
||||
if diagram != "" {
|
||||
t.Errorf("%s: produced output", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGLabelsNeverOverlap(t *testing.T) {
|
||||
base := limbProfileTestGraze(t)
|
||||
start := base.Segments[0][0].Time
|
||||
end := base.Segments[0][len(base.Segments[0])-1].Time
|
||||
// 8 段各 8 s 的密集遮挡,加上不同起点相位,覆盖编号与首刻度的碰撞。
|
||||
for phase := 0; phase < 40; phase++ {
|
||||
profile := base
|
||||
shift := time.Duration(phase) * 1700 * time.Millisecond
|
||||
profile.Segments = [][]moon.StarOccultationLimbSample{append([]moon.StarOccultationLimbSample(nil), base.Segments[0]...)}
|
||||
for index := range profile.Segments[0] {
|
||||
profile.Segments[0][index].Time = profile.Segments[0][index].Time.Add(shift)
|
||||
}
|
||||
profile.Hidden = nil
|
||||
for k := 0; k < 8; k++ {
|
||||
at := start.Add(shift).Add(end.Sub(start)/3 + time.Duration(k)*12*time.Second)
|
||||
profile.Hidden = append(profile.Hidden, moon.OccultationInterval{Start: at, End: at.Add(8 * time.Second)})
|
||||
}
|
||||
profile.Occultation.Greatest = profile.Occultation.Greatest.Add(shift)
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
texts := limbProfileTestTexts(diagram)
|
||||
for i, a := range texts {
|
||||
for _, b := range texts[i+1:] {
|
||||
if math.Abs(a.y-b.y) < 8 && a.x < b.x+b.width-0.5 && b.x < a.x+a.width-0.5 {
|
||||
t.Fatalf("phase %d: %q at (%.1f,%.0f) overlaps %q at (%.1f,%.0f)", phase, a.text, a.x, a.y, b.text, b.x, b.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationLimbProfileSVGContactNumbersStayInsideFrame(t *testing.T) {
|
||||
base := limbProfileTestGraze(t)
|
||||
start := base.Segments[0][0].Time
|
||||
end := base.Segments[0][len(base.Segments[0])-1].Time
|
||||
const frameLeft, frameRight = 70.0, limbProfileSVGDefaultWidth - 260.0 - 48
|
||||
for _, count := range []int{3, 6, 8, 12} {
|
||||
for _, gap := range []time.Duration{2 * time.Second, 8 * time.Second} {
|
||||
profile := base
|
||||
profile.Hidden = nil
|
||||
for k := count - 1; k >= 0; k-- {
|
||||
at := end.Add(-time.Duration(k+1) * 2 * gap)
|
||||
profile.Hidden = append(profile.Hidden, moon.OccultationInterval{Start: at, End: at.Add(gap)})
|
||||
}
|
||||
if profile.Hidden[0].Start.Before(start) {
|
||||
t.Fatalf("%d intervals do not fit the profile span", count)
|
||||
}
|
||||
diagram, err := StarOccultationLimbProfileSVG(profile, StarOccultationLimbProfileSVGOptions{Location: time.UTC})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var numbers []limbProfileTestText
|
||||
for _, text := range limbProfileTestTexts(diagram) {
|
||||
if _, err := strconv.Atoi(text.text); err == nil && text.y > 110 && text.y < 136 {
|
||||
numbers = append(numbers, text)
|
||||
}
|
||||
}
|
||||
if len(numbers) != 2*count {
|
||||
t.Fatalf("%d intervals, gap %v: %d contact numbers, want %d", count, gap, len(numbers), 2*count)
|
||||
}
|
||||
for i, a := range numbers {
|
||||
if a.x < frameLeft-0.5 || a.x+a.width > frameRight+0.5 {
|
||||
t.Errorf("%d intervals, gap %v: number %s spans %.1f–%.1f, frame %.0f–%.0f", count, gap, a.text, a.x, a.x+a.width, frameLeft, frameRight)
|
||||
}
|
||||
for _, b := range numbers[i+1:] {
|
||||
if a.y == b.y && a.x < b.x+b.width-0.5 && b.x < a.x+a.width-0.5 {
|
||||
t.Errorf("%d intervals, gap %v: numbers %s and %s overlap", count, gap, a.text, b.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"b612.me/astro/internal/limbnote"
|
||||
"b612.me/astro/internal/timenote"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -254,7 +255,7 @@ func renderLocalStarOccultationSVG(
|
||||
writeLocalStarOccultationOverview(&b, diagram, events, layout, options)
|
||||
writeLocalStarOccultationContacts(&b, events, layout, options)
|
||||
writeLocalStarOccultationStages(&b, events, layout, options)
|
||||
writeLocalStarOccultationFooter(&b, layout, options, info.Greatest)
|
||||
writeLocalStarOccultationFooter(&b, layout, options, info.Greatest, info.LunarLimbApplied)
|
||||
b.WriteString(`</svg>`)
|
||||
return b.String()
|
||||
}
|
||||
@@ -374,6 +375,8 @@ func writeLocalStarOccultationTrack(
|
||||
}
|
||||
fmt.Fprintf(&path, "%s %.3f %.3f ", command, mapX(frame.StarXArcsec), mapY(frame.StarYArcsec))
|
||||
}
|
||||
// 白色衬底保证轨迹叠在暗月面与亮月面上都可见。
|
||||
fmt.Fprintf(b, `<path class="local-star-track-halo" d="%s" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linecap="round" opacity="0.7"/>`, strings.TrimSpace(path.String()))
|
||||
fmt.Fprintf(b, `<path class="local-star-track" d="%s" fill="none" stroke="#9b3f36" stroke-width="1.7" stroke-dasharray="5 4" opacity="0.82" marker-end="url(#local-occultation-arrow)"/>`, strings.TrimSpace(path.String()))
|
||||
}
|
||||
|
||||
@@ -469,12 +472,22 @@ func writeLocalStarOccultationAxes(b *strings.Builder, cx, cy, radius float64, l
|
||||
}
|
||||
}
|
||||
|
||||
// 被掩目标轮廓用浅金色,配暗色衬底,暗月面与亮月面上都能分辨。
|
||||
const localOccultationHiddenOutlineColor = "#f3d48e"
|
||||
|
||||
func writeLocalOccultationHiddenOutlineHalo(b *strings.Builder, x, y, radius float64) {
|
||||
fmt.Fprintf(b, `<circle class="hidden-outline-halo" cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#1a1c1e" stroke-width="3" opacity="0.55"/>`, x, y, radius)
|
||||
}
|
||||
|
||||
func writeLocalStarOccultationStar(b *strings.Builder, x, y float64, hidden bool, class, label string) {
|
||||
fill, stroke, dash := "#fff7d6", "#9b3028", ""
|
||||
if hidden {
|
||||
fill, stroke, dash = "none", "#7d3430", ` stroke-dasharray="2 2"`
|
||||
fill, stroke, dash = "none", localOccultationHiddenOutlineColor, ` stroke-dasharray="3 2"`
|
||||
}
|
||||
fmt.Fprintf(b, `<g class="%s" data-label="%s">`, html.EscapeString(class), html.EscapeString(label))
|
||||
if hidden {
|
||||
writeLocalOccultationHiddenOutlineHalo(b, x, y, 4.2)
|
||||
}
|
||||
fmt.Fprintf(b, `<circle cx="%.3f" cy="%.3f" r="4.2" fill="%s" stroke="%s" stroke-width="1.4"%s/>`, x, y, fill, stroke, dash)
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="%s" stroke-width="1"/>`, x-6, y, x+6, y, stroke)
|
||||
fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="%s" stroke-width="1"/>`, x, y-6, x, y+6, stroke)
|
||||
@@ -558,11 +571,13 @@ func writeLocalStarOccultationFooter(
|
||||
layout localStarOccultationSVGLayout,
|
||||
options LocalStarOccultationSVGOptions,
|
||||
instant time.Time,
|
||||
limbApplied bool,
|
||||
) {
|
||||
directionLines, footerLines := localOccultationFooterLines(
|
||||
localStarOccultationSVGDirectionText(options),
|
||||
localStarOccultationSVGFooterNote(options),
|
||||
timenote.Scale(options.TimeScale, instant, options.Location, options.Language),
|
||||
limbnote.Join(timenote.Scale(options.TimeScale, instant, options.Location, options.Language),
|
||||
limbnote.Contacts(limbApplied, options.Language), options.Language),
|
||||
layout.footerY, layout.width, layout.height)
|
||||
for index, line := range directionLines {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" font-family="Georgia, 'Times New Roman', serif" font-size="10">%s</text>`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"b612.me/astro/internal/limbnote"
|
||||
"b612.me/astro/internal/timenote"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -223,7 +224,7 @@ func renderLocalPlanetOccultationSVG(
|
||||
options.Width, options.Height, options.Width, options.Height, html.EscapeString(title))
|
||||
b.WriteString(`<defs>`)
|
||||
b.WriteString(svgasset.MoonFaceSymbol())
|
||||
b.WriteString(`<marker id="local-planet-occultation-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#9b3f36"/></marker>`)
|
||||
b.WriteString(`<marker id="local-planet-occultation-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#9b3f36"/></marker>`)
|
||||
b.WriteString(`</defs>`)
|
||||
b.WriteString(`<rect width="100%" height="100%" fill="#efefed"/>`)
|
||||
fmt.Fprintf(&b, `<rect x="22" y="18" width="%.3f" height="%.3f" fill="#ffffff" stroke="#c9c9c6" stroke-width="1.2"/>`, layout.width-44, layout.height-36)
|
||||
@@ -319,8 +320,6 @@ func writeLocalPlanetOccultationOverview(
|
||||
table.Reserve(mapX(event.frame.PlanetXArcsec)-radius, mapY(event.frame.PlanetYArcsec)-radius, 2*radius, 2*radius)
|
||||
}
|
||||
writeLocalStarOccultationAxes(b, cx, cy, moonRadius, options.Language, table)
|
||||
writeLocalPlanetOccultationLunarPath(b, diagram.Frames, mapX, mapY, extent, options.Language)
|
||||
writeLocalPlanetOccultationTrack(b, diagram.Frames, mapX, mapY)
|
||||
for _, event := range events {
|
||||
writeLocalPlanetOccultationDisk(b,
|
||||
mapX(event.frame.PlanetXArcsec), mapY(event.frame.PlanetYArcsec),
|
||||
@@ -330,6 +329,8 @@ func writeLocalPlanetOccultationOverview(
|
||||
writeLocalOccultationMoon(b, cx, cy, moonRadius, "overview-moon",
|
||||
localOccultationMoonAppearanceAt(diagram.Occultation.Greatest, diagram.Occultation.Observer),
|
||||
"local-planet-occultation-moon-overview")
|
||||
writeLocalPlanetOccultationLunarPath(b, diagram.Frames, mapX, mapY, extent, options.Language)
|
||||
writeLocalPlanetOccultationTrack(b, diagram.Frames, mapX, mapY)
|
||||
overviewLabels := make([]localOccultationOverviewLabel, 0, len(events))
|
||||
for _, event := range events {
|
||||
x := mapX(event.frame.PlanetXArcsec)
|
||||
@@ -378,7 +379,9 @@ func writeLocalPlanetOccultationTrack(
|
||||
}
|
||||
fmt.Fprintf(&path, "%s %.3f %.3f ", command, mapX(frame.PlanetXArcsec), mapY(frame.PlanetYArcsec))
|
||||
}
|
||||
fmt.Fprintf(b, `<path class="local-planet-track" d="%s" fill="none" stroke="#9b3f36" stroke-width="1.7" stroke-dasharray="5 4" opacity="0.82" marker-end="url(#local-planet-occultation-arrow)"/>`, strings.TrimSpace(path.String()))
|
||||
// 白色衬底保证轨迹叠在暗月面与亮月面上都可见。
|
||||
fmt.Fprintf(b, `<path class="local-planet-track-halo" d="%s" fill="none" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" opacity="0.7"/>`, strings.TrimSpace(path.String()))
|
||||
fmt.Fprintf(b, `<path class="local-planet-track" d="%s" fill="none" stroke="#9b3f36" stroke-width="1" stroke-dasharray="4 3" opacity="0.85" marker-end="url(#local-planet-occultation-arrow)"/>`, strings.TrimSpace(path.String()))
|
||||
}
|
||||
|
||||
func writeLocalPlanetOccultationLunarPath(
|
||||
@@ -447,7 +450,8 @@ func writeLocalPlanetOccultationDisk(
|
||||
) {
|
||||
fill, stroke, dash := "#d6ad69", "#704b2d", ""
|
||||
if hidden {
|
||||
fill, stroke, dash = "none", "#7d3430", ` stroke-dasharray="2 2"`
|
||||
writeLocalOccultationHiddenOutlineHalo(b, x, y, math.Max(radius, 0.15))
|
||||
fill, stroke, dash = "none", localOccultationHiddenOutlineColor, ` stroke-dasharray="3 2"`
|
||||
}
|
||||
fmt.Fprintf(b, `<circle class="%s" data-label="%s" cx="%.3f" cy="%.3f" r="%.3f" fill="%s" stroke="%s" stroke-width="1.2"%s/>`,
|
||||
html.EscapeString(class), html.EscapeString(label), x, y, math.Max(radius, 0.15), fill, stroke, dash)
|
||||
@@ -516,8 +520,9 @@ func writeLocalPlanetOccultationStages(
|
||||
writeLocalOccultationMoon(b, cx, cy, event.frame.MoonRadiusArcsec*scale, "stage-moon",
|
||||
event.appearance, fmt.Sprintf("local-planet-occultation-moon-stage-%d", index))
|
||||
if event.hidden {
|
||||
fmt.Fprintf(b, `<circle class="stage-planet-hidden-outline" cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#7d3430" stroke-width="1.1" stroke-dasharray="2 2"/>`,
|
||||
x, y, displayRadius)
|
||||
writeLocalOccultationHiddenOutlineHalo(b, x, y, displayRadius)
|
||||
fmt.Fprintf(b, `<circle class="stage-planet-hidden-outline" cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="%s" stroke-width="1.4" stroke-dasharray="3 2"/>`,
|
||||
x, y, displayRadius, localOccultationHiddenOutlineColor)
|
||||
}
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#1c2528" font-family="Arial, sans-serif" font-size="10.5" font-weight="700" text-anchor="middle">%s</text>`,
|
||||
cx, cy+radius+18, html.EscapeString(event.name))
|
||||
@@ -560,7 +565,8 @@ func writeLocalPlanetOccultationFooter(
|
||||
directionLines, footerLines := localOccultationFooterLines(
|
||||
localPlanetOccultationSVGDirectionText(options),
|
||||
localPlanetOccultationSVGFooterNote(info, options),
|
||||
timenote.Scale(options.TimeScale, info.Greatest, options.Location, options.Language),
|
||||
limbnote.Join(timenote.Scale(options.TimeScale, info.Greatest, options.Location, options.Language),
|
||||
limbnote.Contacts(info.LunarLimbApplied, options.Language), options.Language),
|
||||
layout.footerY, layout.width, layout.height)
|
||||
for index, line := range directionLines {
|
||||
fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#4f595b" font-family="Georgia, 'Times New Roman', serif" font-size="10">%s</text>`,
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestOccultationSVGTimeScaleUT1(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// assertOccultationUT1Declaration 钉住 UT1 声明是可解析的 <text> 元素并写出 DUT1:曾经的裸文本写入不会渲染。
|
||||
// assertOccultationUT1Declaration UT1 声明须是可解析的 <text> 元素并写出 DUT1。
|
||||
func assertOccultationUT1Declaration(t *testing.T, name, diagram string) {
|
||||
t.Helper()
|
||||
for _, text := range occultationSVGTexts(t, diagram) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"moon.赤纬 Dec.": "-00°01'34.4\"",
|
||||
"moon.赤经 R.A.": "23h37m22.4s",
|
||||
"planet.赤纬 Dec.": "+03°27'41.3\"",
|
||||
"planet.赤经 R.A.": "00h58m06.2s"
|
||||
}
|
||||
Reference in New Issue
Block a user