feat: 完善时标与天象几何计算并扩展输出接口

- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
This commit is contained in:
2026-09-23 18:55:12 +08:00
parent 1f31a9b5b5
commit 16c62a97d5
503 changed files with 33290 additions and 9471 deletions
+337
View File
@@ -0,0 +1,337 @@
# 日晷与真太阳时
[English](en/sundial.md) | [返回 README](../../README.md)
> 本手册的完整示例以仓库根目录为工作目录执行。
`sundial` 把 `sun` 包的真太阳时、太阳时角与日晷绘制所需的几何量集中在一起,不引入另一套算法。日晷部分按经典平面日晷模型工作:一根指向天极的极轴晷针,其影子投在任意平面上;坐标约定由构造器给定,水平日晷是 **x 轴向东、y 轴向北**。
## 目录
- [计算水平日晷的影子位置](#计算水平日晷的影子位置)
- [API 参考](#api-参考)
- [真太阳时、平太阳时与均时差](#真太阳时平太阳时与均时差)
- [时角](#时角)
- [水平日晷时线角](#水平日晷时线角)
- [平面日晷核心](#平面日晷核心)
- [盘面受光区间](#盘面受光区间)
- [时间线与赤纬曲线](#时间线与赤纬曲线)
- [赤道、水平与垂直日晷特例](#赤道水平与垂直日晷特例)
- [返回结构](#返回结构)
- [完整示例](#完整示例)
- [综合示例:一天的可用时角与等时线](#综合示例一天的可用时角与等时线)
- [常见坑](#常见坑)
- [常用场景](#常用场景)
- [真太阳时与钟表时间的差](#真太阳时与钟表时间的差)
- [水平日晷的时线角与落影](#水平日晷的时线角与落影)
- [盘面受光区间与等时线](#盘面受光区间与等时线)
- [退化情形与各面日晷口径](#退化情形与各面日晷口径)
- [参数与返回值约定](#参数与返回值约定)
- [相关手册](#相关手册)
## 计算水平日晷的影子位置
```go
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`(纬度,度)。
### 真太阳时、平太阳时与均时差
```go
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` 是地方平太阳时,两者之差即均时差(真 − 平)。
### 时角
```go
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` 直接用绝对时刻求时角;后两者用于"给定钟面读数求落影方向",区别在于一个按地方平太阳时、一个按区时。
### 水平日晷时线角
```go
fmt.Println(sundial.HorizontalHourLineAngle(31.2304, -45))
fmt.Println(sundial.HorizontalHourLineAngleAt(date, lon, 31.2304))
```
前者给定纬度与带符号时角,后者直接用时刻与经纬度求当前时线角;返回值是时线相对午线的夹角。
### 平面日晷核心
```go
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` 则直接用时刻与经度。
### 盘面受光区间
```go
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`。
### 时间线与赤纬曲线
```go
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` 就是上面那条可用时角区间。
### 赤道、水平与垂直日晷特例
```go
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` | 该段的赤纬、可用时角区间与采样点列 |
### 完整示例
```go
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)
}
```
输出结果:
```text
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`),所以打印值本身就体现了地方真太阳时与钟表时间的差。
### 综合示例:一天的可用时角与等时线
```go
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):
```text
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` 也沿用同一符号。
- 以为时角区间一定是一段 → 跨零点的一天会被拆成多段,必须遍历返回的切片。
## 常用场景
### 真太阳时与钟表时间的差
```go
trueSolar := sundial.TrueSolarTime(date, lon)
mean := sundial.MeanSolarTime(date, lon)
fmt.Println(trueSolar, mean)
fmt.Println(trueSolar.Sub(mean)) // 均时差
```
```text
2026-06-21 09:34:10.438158222 +0805 LTZ 2026-06-21 09:35:53.532790863 +0805 LTZ
-1m43.094632641s
```
- `TrueSolarTime` 返回的是带合成时区的时刻,直接与自己钟表时间相减就得到"真太阳时快/慢多少"。
- 要把**钟面读数**换成时角(例如按区时排影子刻度),用 `MeanSolarHourAngle` / `ZoneTimeHourAngle`,不要手工加减均时差。
### 水平日晷的时线角与落影
```go
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)
```
```text
-27.40587112370779
-20.95918157094186
-6.511723246549 0.507600045956 true
```
- 水平日晷的坐标约定是 `x` 轴向东、`y` 轴向北;`Illuminated` 是"太阳在地平线上且盘面朝向太阳"的最终判据,只画有效影子时看它。
- 只有时角、没有具体时刻时用 `ShadowPointByHourAngleDeclination`,同一几何给 `-6.511402572556 0.506762879485 true`。
### 盘面受光区间与等时线
```go
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)
```
```text
decl=23.44 usable=-105.24..105.24 samples=211
2 -37.92998436772365
```
- `PlaneIlluminatedHourAngleIntervals` 只判断"盘面是否朝向太阳",`IlluminatedHourAngleIntervals` 再叠加太阳在地平线以上,回答"这盘一天能用多久"要用后者。
- 区间约定是 `[-180, 180]` 且 `Start <= End`,跨零点会拆成多段;赤纬曲线与时间线的完整接口见 [API 参考](#时间线与赤纬曲线)。
### 退化情形与各面日晷口径
```go
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)
```
```text
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` 的返回字段表示地方太阳时读数,不应直接作为新的观测时刻传入星历接口。换算约定见[时标手册](timescale.md)。
- **时角符号**:`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` 的区别**:前者只做几何受光判断,后者还要求太阳在地平线以上;做"这张日晷一天里能用多久"的结论时用后者。
## 相关手册
- 真太阳时、均时差与太阳位置:[太阳与月亮](sun-moon.md)
- 时角、恒星时与地平转换:[坐标工具](coord.md)
- 出图时标:[时标声明](map-geojson.md#时标声明)