Files
astro/doc/manual/sundial.md
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

16 KiB
Raw Permalink Blame History

日晷与真太阳时

English | 返回 README

本手册的完整示例以仓库根目录为工作目录执行。

sundial 把 sun 包的真太阳时、太阳时角与日晷绘制所需的几何量集中在一起,不引入另一套算法。日晷部分按经典平面日晷模型工作:一根指向天极的极轴晷针,其影子投在任意平面上;坐标约定由构造器给定,水平日晷是 x 轴向东、y 轴向北。

目录

计算水平日晷的影子位置

package main

import (
	"fmt"
	"time"

	"b612.me/astro/sundial"
)

func main() {
	cst := time.FixedZone("CST", 8*3600)
	date := time.Date(2026, 6, 21, 9, 30, 0, 0, cst)
	lon, lat := 121.4737, 31.2304
	fmt.Println(sundial.TrueSolarTime(date, lon))
	fmt.Println(sundial.HourAngle(date, lon))
	dial := sundial.HorizontalDial(lat, 10)
	shadow := dial.ShadowPointAt(date, lon)
	if !shadow.Illuminated {
		fmt.Println("no illuminated shadow")
		return
	}
	fmt.Printf("x=%.6f y=%.6f\n", shadow.X, shadow.Y)
}

HorizontalDial 的长度参数与返回坐标使用同一单位;例如晷针长度填厘米,影子坐标也是厘米。x 轴向东、y 轴向北,Illuminated 为真时才有可用的受光落影。

API 参考

分组 入口 用途 单位与口径
真/平太阳时 TrueSolarTime / MeanSolarTime 该绝对时刻在指定经度上的地方真/平太阳时 返回地方太阳时读数;经度东正西负(度)
太阳时角 HourAngle 真太阳时角 度,上午为负、下午为正
时角换算 MeanSolarHourAngle / ZoneTimeHourAngle 把地方平太阳时或区时钟面读数换成视太阳时角 小时数与经度(度)
水平时线角 HorizontalHourLineAngle / HorizontalHourLineAngleAt 水平日晷时线相对午线的角度 度
平面日晷核心 PlanarDial(字段)+ Geometry / ShadowPointByHourAngleDeclination / ShadowPointAt 任意平面的几何量与落影点 坐标与晷针长度为同一长度单位
盘面受光 PlaneIlluminatedHourAngleIntervals / IlluminatedHourAngleIntervals 盘面受光时角区间与最终可用时角区间 度,[-180, 180]
时间线 MeanSolarTimePoint / ZoneTimePoint / MeanSolarTimeLine / ZoneTimeLine 把平太阳时线或区时线直接接到日晷几何 返回 PlanarShadowPoint / TimeLineSample
赤纬曲线 DeclinationCurve / DeclinationCurveAt 按赤纬或日期生成分段采样点列 时角步长单位为度
特例构造 EquatorialNorthDial / EquatorialSouthDial / HorizontalDial / VerticalDial 赤道(南北面)、水平、垂直日晷 纬度与法线方位角单位为度

以下片段省略公共前置变量:date(时刻,民用时标)、lon(经度,东正西负,度)、lat(纬度,度)。

真太阳时、平太阳时与均时差

fmt.Println(sundial.TrueSolarTime(date, lon))
fmt.Println(sundial.MeanSolarTime(date, lon))
fmt.Println(sundial.TrueSolarTime(date, lon).Sub(sundial.MeanSolarTime(date, lon))) // 均时差

三者共用 sun 包的口径:TrueSolarTime 是视太阳时,MeanSolarTime 是地方平太阳时,两者之差即均时差(真 − 平)。

时角

fmt.Println(sundial.HourAngle(date, lon))          // 真太阳时角,上午为负
fmt.Println(sundial.MeanSolarHourAngle(date, 9.5)) // 地方平太阳时 9:30 对应的时角
fmt.Println(sundial.ZoneTimeHourAngle(date, lon, 9.5)) // 区时钟面 9:30 对应的时角

HourAngle 直接用绝对时刻求时角;后两者用于"给定钟面读数求落影方向",区别在于一个按地方平太阳时、一个按区时。

水平日晷时线角

fmt.Println(sundial.HorizontalHourLineAngle(31.2304, -45))
fmt.Println(sundial.HorizontalHourLineAngleAt(date, lon, 31.2304))

前者给定纬度与带符号时角,后者直接用时刻与经纬度求当前时线角;返回值是时线相对午线的夹角。

平面日晷核心

dial := sundial.PlanarDial{
	Latitude: 31.2304, PlaneNormalAzimuth: 180, PlaneNormalZenithDistance: 90, StylusLength: 10,
}
g := dial.Geometry()
fmt.Println(g.HasFiniteCenter, g.PolarStylusLength, g.PolarStylusPlaneAngle)
p := dial.ShadowPointByHourAngleDeclination(-45, 23.44)
fmt.Println(p.X, p.Y, p.Illuminated)

PlanarDial 的四个字段分别是纬度、盘面法线方位角、法线天顶距与晷针长度。Geometry 给出日晷中心(极轴晷针固定点)、极轴晷针长度以及它与盘面的夹角;ShadowPointByHourAngleDeclination 给定带符号时角与太阳赤纬求落影点,ShadowPointAt 则直接用时刻与经度。

盘面受光区间

for _, iv := range dial.PlaneIlluminatedHourAngleIntervals(23.44) {
	fmt.Println(iv.Start, iv.End)
}
for _, iv := range dial.IlluminatedHourAngleIntervals(23.44) {
	fmt.Println(iv.Start, iv.End)
}

前者只回答"盘面是否朝向太阳"(几何受光),后者叠加太阳在地平线以上与盘面朝向两个条件,给出最终可用的时角区间;区间约定在 [-180, 180] 且 Start <= End。

时间线与赤纬曲线

dates := []time.Time{date, date.Add(30 * time.Minute), date.Add(time.Hour)}
fmt.Println(len(dial.MeanSolarTimeLine(dates, 9.5)))
segs := dial.DeclinationCurve(23.44, 1.0)
segsAt := dial.DeclinationCurveAt(date, 1.0)
fmt.Println(len(segs), len(segsAt))

时间线把"地方平太阳时线上的等时刻点"直接投影成 TimeLineSample;赤纬曲线按固定赤纬或当日赤纬分段采样,段内 Interval 就是上面那条可用时角区间。

赤道、水平与垂直日晷特例

h := sundial.HorizontalDial(31.2304, 10)
n := sundial.EquatorialNorthDial(31.2304, 10)
s := sundial.EquatorialSouthDial(31.2304, 10)
v := sundial.VerticalDial(31.2304, 180, 10)
fmt.Println(h.PlaneNormalZenithDistance, n.PlaneNormalAzimuth, s.PlaneNormalAzimuth)
fmt.Println(v.PlaneNormalAzimuth, v.PlaneNormalZenithDistance)

四个构造器给出的盘面法线口径不同,画图时按 PlanarDial 字段判断即可:

构造器 PlaneNormalAzimuth PlaneNormalZenithDistance
HorizontalDial 180° 0°(法线指向天顶;x 轴向东、y 轴向北)
EquatorialNorthDial 0° 90° - 纬度
EquatorialSouthDial 180° 90° + 纬度
VerticalDial 入参归一化到 [0°, 360°) 90°

北赤道日晷在北半球用于春夏半年(太阳赤纬为正),南赤道日晷用于秋冬半年;垂直日晷的 planeNormalAzimuth 按正北 0°、向东增加,朝南墙面取 180°、朝东墙面取 90°。

返回结构

类型 字段 含义
PlanarShadowPoint X / Y 落影点坐标,与 StylusLength 同单位
DenominatorQ 投影分母,趋近 0 表示影子趋于无穷远
SunAboveHorizon / PlaneIlluminated / Illuminated 太阳在地平线上、盘面受光、两者同时成立的最终判据
PlanarGeometry CenterX / CenterY 日晷中心(极轴晷针固定点)坐标
PolarStylusLength / PolarStylusPlaneAngle 极轴晷针长度、它与盘面的夹角
HasFiniteCenter 为 false 时中心退化到无穷远,相关量为 NaN
HourAngleInterval Start / End 有符号时角区间(度),保证 Start <= End
TimeLineSample Date / Declination / HourAngle / Point 时刻、太阳赤纬、真太阳时角与对应落影点
DeclinationCurveSegment Declination / Interval / Samples 该段的赤纬、可用时角区间与采样点列

完整示例

package main

import (
	"fmt"
	"time"

	"b612.me/astro/sundial"
)

func main() {
	date := time.Date(2026, 6, 21, 9, 30, 0, 0, time.FixedZone("CST", 8*3600))
	lon, lat := 121.4737, 31.2304

	trueSolar := sundial.TrueSolarTime(date, lon)
	hourAngle := sundial.HourAngle(date, lon)
	lineAngle := sundial.HorizontalHourLineAngle(lat, -45)
	lineAngleNow := sundial.HorizontalHourLineAngleAt(date, lon, lat)

	fmt.Println(trueSolar)
	fmt.Printf("hour angle=%.6f line@9am=%.6f line@now=%.6f\n", hourAngle, lineAngle, lineAngleNow)
}

输出结果:

2026-06-21 09:34:10.438158222 +0805 LTZ
hour angle=-36.456508 line@9am=-27.405871 line@now=-20.959182

第一行的时区是合成的当地真太阳时区(经度 121.4737° 对应 +08:05 的 LTZ),所以打印值本身就体现了地方真太阳时与钟表时间的差。

综合示例:一天的可用时角与等时线

dial := sundial.HorizontalDial(31.2304, 10)
for _, seg := range dial.DeclinationCurveAt(date, 1.0) {
	fmt.Printf("decl=%.2f usable=%.2f..%.2f samples=%d\n", seg.Declination, seg.Interval.Start, seg.Interval.End, len(seg.Samples))
}
mean := sundial.MeanSolarTime(date, 121.4737)
samples := dial.MeanSolarTimeLine([]time.Time{mean, mean.Add(30 * time.Minute)}, 9.5)
fmt.Println(len(samples), samples[0].HourAngle)

实测(date = 2026-06-21 09:30 CST、121.4737°E, 31.2304°N、晷针长 10):

decl=23.44 usable=-105.24..105.24 samples=211
2 -37.92998436772365

第一行说明当天在纬度 31.2304° 的水平日晷上,太阳赤纬 23.44° 时盘面能用的时角区间是 [-105.24°, +105.24°](约 14 小时),采样 211 点;第二行是"地方平太阳时 9:30 这条时线"的两个采样点与其时角。这两个量配合就能直接画出一张带可用范围的水平日晷。

常见坑

  • 把 ZoneTimePoint 的 date 当成目标地点的地方真太阳时 → 它只用年月日与时区,钟面时间来自 zoneTimeHours。
  • 用 PlaneIlluminated 判断"这张日晷一天里能不能用" → 应该看 Illuminated,它才叠加了太阳在地平线以上这个条件。
  • 垂直日晷传"墙面朝向" → planeNormalAzimuth 是法线方位角,朝南墙是 180°。
  • 时角符号写反 → HourAngle 上午为负、下午为正,HorizontalHourLineAngle 也沿用同一符号。
  • 以为时角区间一定是一段 → 跨零点的一天会被拆成多段,必须遍历返回的切片。

常用场景

真太阳时与钟表时间的差

trueSolar := sundial.TrueSolarTime(date, lon)
mean := sundial.MeanSolarTime(date, lon)
fmt.Println(trueSolar, mean)
fmt.Println(trueSolar.Sub(mean)) // 均时差
2026-06-21 09:34:10.438158222 +0805 LTZ 2026-06-21 09:35:53.532790863 +0805 LTZ
-1m43.094632641s
  • TrueSolarTime 返回的是带合成时区的时刻,直接与自己钟表时间相减就得到"真太阳时快/慢多少"。
  • 要把钟面读数换成时角(例如按区时排影子刻度),用 MeanSolarHourAngle / ZoneTimeHourAngle,不要手工加减均时差。

水平日晷的时线角与落影

dial := sundial.HorizontalDial(lat, 10)
fmt.Println(sundial.HorizontalHourLineAngle(lat, -45))        // 时角 -45° 的时线角
fmt.Println(sundial.HorizontalHourLineAngleAt(date, lon, lat)) // 当前时刻的时线角
p := dial.ShadowPointAt(date, lon)
fmt.Println(p.X, p.Y, p.Illuminated)
-27.40587112370779
-20.95918157094186
-6.511723246549 0.507600045956 true
  • 水平日晷的坐标约定是 x 轴向东、y 轴向北;Illuminated 是"太阳在地平线上且盘面朝向太阳"的最终判据,只画有效影子时看它。
  • 只有时角、没有具体时刻时用 ShadowPointByHourAngleDeclination,同一几何给 -6.511402572556 0.506762879485 true。

盘面受光区间与等时线

dial := sundial.HorizontalDial(31.2304, 10)
for _, seg := range dial.DeclinationCurveAt(date, 1.0) {
	fmt.Printf("decl=%.2f usable=%.2f..%.2f samples=%d\n", seg.Declination, seg.Interval.Start, seg.Interval.End, len(seg.Samples))
}
mean := sundial.MeanSolarTime(date, 121.4737)
samples := dial.MeanSolarTimeLine([]time.Time{mean, mean.Add(30 * time.Minute)}, 9.5)
fmt.Println(len(samples), samples[0].HourAngle)
decl=23.44 usable=-105.24..105.24 samples=211
2 -37.92998436772365
  • PlaneIlluminatedHourAngleIntervals 只判断"盘面是否朝向太阳",IlluminatedHourAngleIntervals 再叠加太阳在地平线以上,回答"这盘一天能用多久"要用后者。
  • 区间约定是 [-180, 180] 且 Start <= End,跨零点会拆成多段;赤纬曲线与时间线的完整接口见 API 参考。

退化情形与各面日晷口径

h := sundial.HorizontalDial(31.2304, 10)
n := sundial.EquatorialNorthDial(31.2304, 10)
s := sundial.EquatorialSouthDial(31.2304, 10)
v := sundial.VerticalDial(31.2304, 180, 10)
fmt.Println(h.PlaneNormalZenithDistance, n.PlaneNormalAzimuth, s.PlaneNormalAzimuth)
fmt.Println(v.PlaneNormalAzimuth, v.PlaneNormalZenithDistance)
0 0 180
180 90
  • 退化条件:盘面法线与极轴垂直(等价于极轴晷针与盘面平行)时,Geometry().HasFiniteCenter 为 false,CenterX/CenterY/PolarStylusLength 为 NaN、PolarStylusPlaneAngle 为 0——纬度 45°、法线方位 180°、法线天顶距 45° 正落在这个点上,此时以中心为基准的绘制不可用。
  • 各面口径:水平盘法线指向天顶(天顶距 0°);垂直盘法线天顶距 90°,其 planeNormalAzimuth 是法线方向(朝南墙 180°、朝东墙 90°);赤道南北面分别用 90-纬度 与 90+纬度。四个构造器的完整表见赤道、水平与垂直日晷特例。

参数与返回值约定

  • 单位:时角、时线角、赤纬、纬度、法线方位角与法线天顶距都是度;PlanarDial 的 StylusLength 与返回点 X/Y 共用同一长度单位,可以是任意自洽单位(毫米、米或画布坐标)。
  • 时标:观测输入为民用时刻;TrueSolarTime / MeanSolarTime 的返回字段表示地方太阳时读数,不应直接作为新的观测时刻传入星历接口。换算约定见时标手册。
  • 时角符号:HourAngle 上午为负、下午为正;HourAngleInterval 的取值范围是 [-180, 180] 且保证 Start <= End。跨零点的一天会被拆成多段。
  • date 的时区语义(易错):MeanSolarTimePoint / MeanSolarTimeLine 的 date 表示目标地点的地方平太阳时(通常是 MeanSolarTime(...) 的返回值);ZoneTimePoint / ZoneTimeLine 会忽略传入 date 的时分秒,只用它的年月日与时区,再用参数 zoneTimeHours 替换钟面时间。传错会把整条时线平移。
  • 落影有效性的三个布尔量:SunAboveHorizon 表示太阳在地平线以上,PlaneIlluminated 表示盘面朝向太阳,Illuminated 是两者同时成立后的最终判据;只画影子时必须看 Illuminated。
  • 退化情形:当盘面法线与极轴垂直(等价于极轴晷针与盘面平行)时,PlanarGeometry.HasFiniteCenter 为 false,CenterX/CenterY/PolarStylusLength 返回 NaN、PolarStylusPlaneAngle 为 0——此时中心在无穷远,所有以中心为基准的绘制都不可用。例如纬度 45°、法线方位 180°、法线天顶距 45° 就落在这个退化点上。
  • PlaneIlluminated 与 Illuminated 的区别:前者只做几何受光判断,后者还要求太阳在地平线以上;做"这张日晷一天里能用多久"的结论时用后者。

相关手册