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

920 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 日食与月食
[English](en/eclipse.md) | [返回 README](../../README.md)
> 本手册的完整示例以仓库根目录为工作目录执行,生成的图片写入 `doc/img/`。
## 目录
- [简单示例:2009长江大日食,上海某地见食情况](#简单示例2009长江大日食上海某地见食情况)
- [API 参考](#api-参考)
- [常用场景](#常用场景)
- [本地有没有日食或月食](#本地有没有日食或月食)
- [全球见食图与月食图](#全球见食图与月食图)
- [中心线与偏食足迹](#中心线与偏食足迹)
- [贝塞尔根数与沙罗](#贝塞尔根数与沙罗)
- [日食](#日食)
- [与 NASA 资料的时间对照](#与-nasa-资料的时间对照)
- [2009 年长江大日食:上海洋山附近](#2009-年长江大日食上海洋山附近)
- [2012 年日环食:厦门示例](#2012-年日环食厦门示例)
- [生成日食 SVG](#生成日食-svg)
- [月食](#月食)
- [代码示例](#代码示例)
- [与 NASA 数据对照](#与-nasa-数据对照)
- [月食 SVG](#月食-svg)
- [参考资料](#参考资料)
- [全球见食图与月食出图](#全球见食图与月食出图)
- [全球见食图](#全球见食图)
- [月食出图](#月食出图)
- [时标口径与 UT1](#时标口径与-ut1)
## 简单示例:2009长江大日食,上海某地见食情况
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2009, 7, 22, 0, 0, 0, 0, cst)
info, ok := eclipse.LocalSolarEclipseOnDate(date, 121.9850, 30.6167, 0)
if !ok {
fmt.Println("no local solar eclipse")
return
}
fmt.Println(info.Type)
fmt.Printf("%+v\n", info)
}
```
`ok=false` 表示该日期或地点没有命中所查询的日食,此时不应读取结果字段。全局事件、地方接触时刻和全球路径分别由下面的接口计算。
## API 参考
SVG 片段使用导入别名 `eclipsesvg "b612.me/astro/eclipse/svg"`;月掩图使用 `moonsvg "b612.me/astro/moon/svg"`。日期和时区沿用首例。
| 名称 | 用途 | 备注 |
| --- | --- | --- |
| `LocalSolarEclipseOnDate` / `LocalSolarEclipseOnDateNASABulletinSplitK` | 站心日食查询 | 返回 `(info, bool)`;默认 NASA Split-K 口径 |
| `LunarEclipseOnDate` / `LunarEclipseOnDateDanjon` / `LunarEclipseOnDateChauvenet` | 月食查询 | 同上;后缀强制影半径模型 |
| `SearchLocalCentralSolarEclipse` / `SolarEclipseCandidates` | 跨年搜索中心食 / 候选时刻表 | 前者带 `status.Exhausted` |
| `SolarEclipseCentralPath` / `SolarEclipsePartialFootprints` | 中心线和偏食足迹几何 | 选项见 `SolarEclipsePathOptions` / `SolarEclipsePartialFootprintOptions` |
| `SolarEclipseBesselianElements` / `SolarEclipseBesselianMuForPublishedTable` | 贝塞尔根数与已发布表换算 | 用来对表 |
| `eclipsesvg.SolarEclipseMapSVG` / `LunarEclipseMapSVG` | 全球见食图 / 月食可见区图 | 返回 `(string, bool)` |
| `eclipsesvg.LocalSolarEclipseSVG` / `LunarEclipseSVG` / `LunarEclipseDetailedSVG` | 站心视圆图 / 穿影图 / 详细版式 | 同上 |
| `eclipsesvg.SolarEclipseMapSVGOptions` / `LunarEclipseDetailedSVGOptions` | 出图选项(投影、图层、画布) | 图层开关见「全球见食图与月食出图」 |
| `astro.TimeScaleUT1` | UT1 口径出图 | 此时 `Location` 必须为 UTC |
| `SolarEclipseInfoInUT1` 等 `...InUT1` 系列 | 把结果里的民用时刻换成同一物理时刻的 UT1 读数 | 零值时刻原样保留 |
## 常用场景
### 本地有没有日食或月食
```go
solar, ok := eclipse.LocalSolarEclipseOnDate(date, 121.9850, 30.6167, 0)
fmt.Println(ok, solar.Type)
lunar, ok2 := eclipse.LunarEclipseOnDate(time.Date(2029, 1, 1, 0, 0, 0, 0, cst))
fmt.Println(ok2, lunar.Type)
```
```text
true total
true total
```
- `LocalSolarEclipseOnDate` 一次给出食型、各阶段时刻、食分、遮掩比例与食甚太阳高度;只关心"有没有"时看第二个返回值。
- 要跨年份找"下一次中心食",用 `SearchLocalCentralSolarEclipse`(返回 `status.Exhausted` 区分"跨度内确实没有");只要候选时刻表用 `SolarEclipseCandidates`。
- 月食侧对应 `LunarEclipseOnDate` 与带 `Danjon` / `Chauvenet` 后缀的强制模型入口。
### 全球见食图与月食图
```go
solar, ok := eclipsesvg.SolarEclipseMapSVG(date, eclipsesvg.SolarEclipseMapSVGOptions{Width: 1200, Height: 800, Location: cst})
local, ok2 := eclipsesvg.LocalSolarEclipseSVG(date, 121.9850, 30.6167, 0, eclipsesvg.LocalSolarEclipseSVGOptions{Width: 920, Height: 720, Step: 5 * time.Minute, Location: cst})
lunar, ok3 := eclipsesvg.LunarEclipseSVG(time.Date(2029, 1, 1, 0, 0, 0, 0, cst), eclipsesvg.LunarEclipseSVGOptions{Width: 960, Height: 620, Step: 10 * time.Minute, Location: cst})
detailed, ok4 := eclipsesvg.LunarEclipseDetailedSVG(time.Date(2029, 1, 1, 0, 0, 0, 0, cst), eclipsesvg.LunarEclipseDetailedSVGOptions{Location: cst})
fmt.Println(ok, len(solar), ok2, len(local), ok3, len(lunar), ok4, len(detailed))
```
```text
true 265827 true 13625 true 19831 true 319721
```
投影切换、图层开关(半影/本影轮廓、食分等值线、等时线)与画布下限都在[全球见食图与月食出图](#全球见食图与月食出图)里;正射球面与极区版式的取舍见[地图投影](map-geojson.md#地图投影)。
### 中心线与偏食足迹
```go
partial, ok := eclipse.SolarEclipsePartialFootprints(date,
eclipse.SolarEclipsePartialFootprintOptions{Step: 10 * time.Minute, BoundaryPoints: 180})
central, hasCentral := eclipse.SolarEclipseCentralPath(date,
eclipse.SolarEclipsePathOptions{Step: time.Minute, TargetSpacingKM: 20})
fmt.Println(ok, hasCentral, len(partial.CentralBandFootprints), len(central.CenterLine))
```
```text
true true 42 1117
```
- 只要几何、不要图(例如自己写数据管线或传给 `geojson`)时用这两个入口;`TargetSpacingKM` 控制中心线加密上限。
- 偏食足迹是瞬时足迹的并集,`Step` 小于两分钟会夹到两分钟;结果里的 `data-source` 会标注实际几何来源。
### 贝塞尔根数与沙罗
```go
elements, ok := eclipse.SolarEclipseBesselianElements(2460409.262835,
eclipse.SolarEclipseBesselianElementsOptions{DeltaTSeconds: 70.6, ReferenceJDE: 2460409.25})
t := 0.5
fmt.Printf("X=%.6f Y=%.6f D=%.6f L1=%.6f L2=%.6f\n",
elements.X.At(t), elements.Y.At(t), elements.D.At(t), elements.L1.At(t), elements.L2.At(t))
fmt.Println(info.HasSaros, info.Saros)
```
```text
X=-0.062351 Y=0.355150 D=7.593613 L1=0.535842 L2=-0.010245
true {136 37 71 true}
```
- 显式给 `DeltaTSeconds` 与 `ReferenceJDE` 时可直接与已发布的贝塞尔根数表逐项对表;已发布表用的是 T0 本身的恒星时,需要先用 `SolarEclipseBesselianMuForPublishedTable` 换算。
- 与 NASA 目录/图页的 UT 时刻比较前要先扣掉它们出版时采用的 ΔT 口径,方法见[与 NASA 资料的时间对照](#与-nasa-资料的时间对照);TD 层(不含 ΔT)才是能单独检验星历与几何的一层。
## 日食
> 图内与图注的时标声明见[时标声明](map-geojson.md#时标声明)。
日食计算统一放在 `eclipse` 包;SVG 生成功能放在 `eclipse/svg` 包。月亮半径默认采用 `NASA bulletin Split-K` 口径(半影与偏食 `k = 0.2724880`,本影与反本影 `k = 0.2722810`);需要 IAU 单一 `k = 0.2725076` 时,调用同名的 `...IAUSingleK` 接口。
太阳半径有两个口径:**标准档**(1 AU 处 `959.639″`,与已发布星历表、目录一致,默认)与**边缘档**(1 AU 处 `959.95″`,由全食边缘的光变曲线实测得到的食半径),两者相差 `0.31″`。以 2024-04-08 全食为例,改用边缘档会让全食带每侧收窄约 0.6 千米、中心食时长缩短约 1.5 秒;2023-10-14 环食则带变宽约 1.3 千米、时长延长约 2.1 秒;偏食食分变化约 `1e-4`。
可据此评估食限与中心时长对太阳半径的敏感性。
两个口径都可以选:`eclipse.SolarEclipseOptions{SunRadiusModel: ...}` 配合 `SolarEclipseOnDateWithOptions`、`LocalSolarEclipseOnDateWithOptions`,以及搜索与面板的 `LastSolarEclipseWithOptions` / `NextSolarEclipseWithOptions` / `ClosestSolarEclipseWithOptions` / `SolarEclipseGeocentricPanelWithOptions`,或 `basic` 侧的 `SolarEclipseWithOptions` / `LocalSolarEclipseWithOptions` 与各 `...Options` 结构里的 `SunRadiusModel` 字段;结果里的 `SunRadiusModel` 记录实际使用的口径,`basic.SolarEclipseSunSemidiameter` 与面板里的“视半径 S.D.”都按同一口径计算。
常用接口:
- `SolarEclipseOnDate`:判断某个当地日期附近是否有全局日食
- `LastSolarEclipse` / `NextSolarEclipse` / `ClosestSolarEclipse`:搜索全局日食
- `LocalSolarEclipseOnDate`:判断某地当天是否能看到站心日食
- `LastLocalSolarEclipse` / `NextLocalSolarEclipse` / `ClosestLocalSolarEclipse`:搜索某地可见的站心日食
- `LastLocalTotalSolarEclipse` / `NextLocalTotalSolarEclipse` / `ClosestLocalTotalSolarEclipse`:搜索某地可见的日全食,返回 `(info, ok)`
- `LastLocalAnnularSolarEclipse` / `NextLocalAnnularSolarEclipse` / `ClosestLocalAnnularSolarEclipse`:搜索某地可见的日环食,返回 `(info, ok)`
- `SolarEclipseCentralPath`:计算中心线、南北界和食甚点
- `SolarEclipsePartialFootprints`:计算偏食半影在地球表面的足迹;可选采样本影/反本影瞬时轮廓
- `SolarEclipseBesselianElements`:计算一次日食的多项式贝塞尔根数(`X`/`Y`/`D`/`L1`/`L2`/`Mu` 的三次系数与 `TanF1`/`TanF2`),窗口内无日食时返回 `(零值, false)`
- `eclipse/svg.LocalSolarEclipseSVG`:生成某地的日面视圆 SVG
`SolarEclipseBesselianElements` 给出一张与已发布根数表同形的多项式表:`T0JDE` 是参考时刻,`t = (jde - T0JDE) * 24` 是自它起算的 TT 小时数,`X`/`Y`/`D`/`L1`/`L2`/`Mu` 都是 `t` 的三次多项式(`D` 与 `Mu` 用度,其余用地球赤道半径),`TanF1`/`TanF2` 在本次日食内为常数。
`L1`/`L2` 用 Explanatory Supplement 的含 `1/cos f` 形式,本影 `L2` 为负表示月心尚未越过本影锥顶点。默认 `T0` 取食甚所在 TT 小时的下整、窗口半径 3 小时、窗口内 5 个等距时刻做三次最小二乘,与已发布表的拟合口径一致。`Model`、`SunRadiusModel`、`PenumbralK`、`UmbralK`、`DeltaTSeconds` 五个字段记录决定这些数值的口径,随结果一起返回。
**`Mu` 的时间自变量与已发布表不同,必须换算后才能对表。** 已发布表用 `T0` 本身的恒星时,本库用 `UT = TT - ΔT`,两者相差 `ΔT × 15.041067/3600` 度;`Mu` 在窗口内保持连续、不折回 `[0,360)`,`SolarEclipseBesselianMuForPublishedTable` 只平移常数项。本库取真实格林时角,是为了让 `Mu` 与本库自己的食甚经度、中心线和接触时刻自洽;直接拿已发布表的 `Mu` 配上正确的恒星时,会得到约 0.3° 的经度偏差(2024-04-08 的食甚纬度上约 30 km)。
```go
// 贝塞尔根数表,T0 与 ΔT 显式指定时可直接与已发布表对表。
elements, ok := eclipse.SolarEclipseBesselianElements(
2460409.262835, // 2024-04-08 日全食附近的 TT 儒略日
eclipse.SolarEclipseBesselianElementsOptions{DeltaTSeconds: 70.6, ReferenceJDE: 2460409.25},
)
if ok {
t := 0.5 // 自 T0 起 0.5 TT 小时
fmt.Println(elements.X.At(t), elements.Y.At(t), elements.D.At(t)) // 基本面坐标与影轴赤纬
fmt.Println(elements.L1.At(t), elements.L2.At(t)) // 半影、本影半径
// 已发布表用 T0 本身的恒星时,换算后才能对表。
fmt.Println(eclipse.SolarEclipseBesselianMuForPublishedTable(elements.Mu, elements.DeltaTSeconds).At(t))
}
```
`SolarEclipsePartialFootprintsInfo` 还给出影锥与地球的全球接触:`P1/P4` 是半影外切,`P2/P3` 是半影内切;`U1/U4` 是本影或反本影外切,`U2/U3` 是内切。某次日食不存在的接触保持 `time.Time` 零值。`CentralBeginOnEarth` / `CentralEndOnEarth` 仍表示影轴进入和离开地球,不等同于 `U1/U4`。
需要结构化的瞬时中心影轮廓时,可在 `SolarEclipsePartialFootprintOptions` 中设置 `CentralShadowStep`;结果写入 `CentralShadowFootprints`。零值关闭该额外计算;SVG 入口同样只在正值时采样(小于一分钟按一分钟),零值或负值都不会构造。
需要在数据层直接取等时线时,可在同一个 `SolarEclipsePartialFootprintOptions` 中设置 `GreatestTimeValues` 或 `GreatestTimeStep`。`GreatestTimeValues []time.Time` 是**食甚时刻取值**,按绝对时刻使用(其 `Location` 不参与换算),最多保留 64 条:重复的时刻取值与偏食可见窗口之外的时刻取值会被跳过,其余按时间先后排序,超出时保留最早的 64 条;没有可用支路的时刻取值不会出现在结果里。它为空时改用 `GreatestTimeStep` 按间隔生成,间隔只在为正值时生效,且对齐到 UTC 整刻度;要按展示时区对齐,请自行生成时刻后传给 `GreatestTimeValues`。
结果写入 `SolarEclipsePartialFootprintsInfo.GreatestTimeContours`:`JDE` 是对应的力学时儒略日,`Time` 是该时刻取值在输入时区下的时刻(显式传入的时刻取值原样回显,按步长生成时由 `JDE` 换算并抹到毫秒,避免往返把整分截断成前一分钟),`Segments` 是该时刻的等时线支路。等时线只出现在日月盘面确有重叠且太阳在几何地平以上(不含蒙气差与半径修正)的地方,两端止于地平线或偏食可见域边界;纬度 ±88° 以上不再延拓,同一时刻可能有多条互不相连的支路。不请求时既有输出完全不变。
日食结果 `SolarEclipseInfo`、`LocalSolarEclipseInfo`,以及 `SolarEclipsePath` / `SolarEclipsePartialFootprintsInfo` 里的 `Eclipse` 字段还会附带沙罗序列信息:
- `HasSaros`:是否成功匹配到沙罗序列
- `Saros.Series`:`Verified=true` 时为 NASA 沙罗系列号,否则为推算的暂定系列号
- `Saros.Member`:这次日食在该系列中的第几个成员,从 `1` 开始
- `Saros.Count`:该沙罗系列的总成员数
- `Saros.Verified`:是否已与内置权威目录锚点核验;扩展表或范围外推算结果为 `false`
说明:
- 沙罗周期约为 `6585.321` 天,也就是 `223` 个朔望月、约 `18 年 11 天 8 小时`;系列成员按此周期排列。
- 沙罗系列是一组按沙罗周期连续排列的日食事件;`Series` 标识该组,`Member` / `Count` 表示当前事件在该组中的序号和总数。
- 沙罗序列属于整场日食事件,不随观测地点改变,所以全局日食、站心日食、中心路径和偏食足迹中的对应值应当一致。
- 内置 NASA 锚点优先使用正式编号;天文年份 `-3000` 至 `+6000` 年内(含首尾年,`0` 年为公元前 1 年)未被锚点覆盖的事件使用预计算扩展表,范围外才实时演算外推。预计算与实时推算结果的 `Verified` 都是 `false`,不应视为实际已发布编号。
- 扩展编号沿用 NASA 的 [Saros/Inex 编号关系](https://eclipse.gsfc.nasa.gov/SEsaros/SEperiodicity.html),成员按 Split-K 模型计算,计数覆盖完整系列,不在预计算年份边界截断。`3288-11-15` 的推算结果为系列 `202`、第 `1/71` 个成员。
- 例如 `2024-04-08` 北美日全食属于 `日食沙罗序列139` 的第 `30/71` 个成员。
### 与 NASA 资料的时间对照
日食时间分两类:
- **全局日食**:关注整次日食的食甚 UT、食分、Gamma、食甚点经纬度和食带宽度。当前用 NASA GSFC 的日食搜索/贝塞尔根数资料对照了 `2023-04-20` 全环食、`2024-04-08` 日全食、`2024-10-02` 日环食、`2025-03-29` 日偏食。
- **站心日食**:关注某个观测点看到的初亏、食甚、复圆和中心食持续时间。当前用 NASA GSFC 的 local circumstances / Google map 日食资料对照了芝加哥偏食、2024 日全食食甚点、2024 日环食食甚点。
| 时标类型 | 比较项 | 作用 | 已知量级 |
| --- | --- | --- | --- |
| **TT / TD 层**(力学时,去掉 ΔT) | 几何与星历:贝塞尔根数、影轴位置、接触时刻的 TD 值 | 星历与影子几何本身 | 4 个回归样例中位差约 `0.2 s`;1901–2100 与 NASA 配对 195 次的中位差 `0.40 s`、p99 `2.37 s`、max `2.51 s` |
| **UT 层**(民用时刻,含 ΔT 口径) | 上面再叠一段地球自转换算 | 还额外含出版方选的 ΔT | 系统差 `4.8–5.9 s`,是口径差不是几何误差 |
**UT 层的差异来自 ΔT 口径,不是几何**:NASA 目录/图页的 UT 时刻用其出版时采用的 ΔT 从 TD 换算(2024 年 `74 s`、2026 年 `75 s`),而本库实测 ΔT 约 `69.1–69.2 s`,两者相差 `4.8–5.9 s`,会整体进入任何 UT 层比较。
换算成地面量级用 `basic.DeltaTGroundShiftKM(ΔΔT, 纬度)`(即 `≈0.4651·|ΔΔT|·cos(纬度)` km,ΔT 只改自转相位、不动 TT 几何):实测 `DeltaTGroundShiftKM(5, 36) = 1.881 km`、`DeltaTGroundShiftKM(5.9, 24) = 2.507 km`。
**分类对比**:
| 对照类型 | 样例 | 时间项 | 时标层级 | 对照结果 |
| --- | --- | --- | --- | --- |
| 全局日食 | 4 次现代日食 | 食甚 UT(包含ΔT 口径) | UT 层,`8 s` 阈值**已含**口径差;扣掉后见下一行 | 秒级对齐,在 `8 s` 阈值内(其中 `4.8–5.9 s` 是 ΔT 口径) |
| 全局日食 | 同上扣掉口径后 | 食甚 TD(本库 TT 对 NASA TD) | TT 层 | 中位差约 `0.2 s` |
| 站心日食 | 3 个本地观测点 | 食甚、初亏、复圆 | UT 层,但公开值多为整分钟 | 与公开分钟值一致(分辨率限制,不按秒级解读) |
| 站心日食 | 2 个中心食点 | 全食/环食持续时间 | **TT 层**:两端时刻相减,ΔT 自动抵消 | 秒级对齐,在 `5 s` 阈值内 |
- **日食持续时间反应模型层面精度**:它是两个接触时刻之差,ΔT 口径自动抵消,所以 `5 s` 阈值反映的是几何与星历(对食带边缘最敏感),与 ΔT 无关。
- **UT 世界时时刻的 `8 s` 差异主要是口径差异**:不能当成几何精度,它的构成是 `4.8–5.9 s`(ΔT 口径)+ 不到 `1 s`(TD 层几何残差)+ NASA 整秒打印的舍入,所以是个宽松上界而不是精度指标。
- **更大范围的一致性对比结果**:1901–2100 与 NASA 五千年目录配对 195 次(NASA 页面缺 1986–2000、2088–2100 两段),类型普查 `A145/T139/H13/P155` 与 NASA 逐一相等、类型不一致 `0`;食甚 TD 差中位 `0.40 s`、p99 `2.37 s`、max `2.51 s`;gamma 差中位 `3.2e-5`、食分差中位 `4.2e-5`、带宽差中位 `0.5 km`(max `8.3 km`)、中心食时长差中位 `0.25 s`(max `0.59 s`)。
- **复现NASA的 UT 值**:用 `astro.SetDeltaT` 注入NASA采用的 ΔT(例如 2024 年 `74 s`)后取 UT,就能把ΔT差值从比较里去掉;注入只影响换算,不改变 TT 几何。
- 此外,全局日食资料通常给到秒,适合直接做秒级对照;很多站心日食页面的初亏、复圆和本地食甚只公开到整分钟,因此这类资料只按分钟级核对,公开资料舍入造成的残差不按秒级误差解读。
- 下面的 2009 上海洋山港和 2012 厦门示例只展示接口调用与 SVG 输出,未承诺地方接触时刻的发布级精度;需要逐项核对时可与 NASA/IMCCE local circumstances 比对。
### 2009 年长江大日食:上海洋山附近
2009-07-22 “长江大日食”。下面示例选用上海东南方长江口洋山附近的观测点,接近中心线,全食持续约 5 分 57 秒。
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2009, 7, 22, 12, 0, 0, 0, cst)
// 上海洋山附近,东经为正,北纬为正,椭球高取 0 米。
info, ok := eclipse.LocalSolarEclipseOnDate(date, 121.9850, 30.6167, 0)
fmt.Println(ok, info.Type) // 是否命中本地日食;食型
fmt.Println(info.HasSaros, info.Saros) // 是否匹配沙罗序列;系列号、系列内序号、总成员数
fmt.Println(info.PartialStart) // 初亏
fmt.Println(info.CentralStart) // 全食开始
fmt.Println(info.GreatestEclipse) // 食甚
fmt.Println(info.CentralEnd) // 全食结束
fmt.Println(info.PartialEnd) // 复圆
fmt.Println(info.CentralEnd.Sub(info.CentralStart)) // 全食阶段持续时间
fmt.Printf("magnitude=%.6f obscuration=%.6f altitude=%.3f\n", info.Magnitude, info.Obscuration, info.SunAltitude) // 食分、遮掩比例、食甚太阳高度
// 同一天的中心路径,包含食甚点、中心线和南北界。
path, _ := eclipse.SolarEclipseCentralPath(
date,
eclipse.SolarEclipsePathOptions{Step: time.Minute, TargetSpacingKM: 100},
)
fmt.Printf("greatest lon=%.4f lat=%.4f width=%.1fkm center=%d\n",
path.Greatest.Longitude,
path.Greatest.Latitude,
path.Greatest.WidthKM,
len(path.CenterLine),
)
}
```
输出结果:
```text
true total // 洋山站点当天命中日食,食型为日全食
true {136 37 71 true} // Solar Saros 136,第 37/71 个成员,已核验
2009-07-22 08:23:55.092397034 +0800 CST // 初亏
2009-07-22 09:37:23.088684976 +0800 CST // 全食开始
2009-07-22 09:40:20.87983489 +0800 CST // 食甚
2009-07-22 09:43:19.723805487 +0800 CST // 全食结束
2009-07-22 11:03:13.914820253 +0800 CST // 复圆
5m56.635120511s // 全食持续时间
magnitude=1.076997 obscuration=1.000000 altitude=57.293 // 食分、遮掩比例、食甚太阳高度
greatest lon=144.1167 lat=24.2193 width=258.3km center=289 // 全局食甚点经纬度、食带宽度、中心线采样点数
```
### 2012 年日环食:厦门示例
2012-05-21 日环食在我国东南沿海可见。下面用厦门做一个本地日环食示例,食甚时太阳高度约 9.6 度,环食阶段持续约 4 分 19 秒。
```go
package main
import (
"fmt"
"time"
"b612.me/astro/eclipse"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2012, 5, 21, 12, 0, 0, 0, cst)
info, ok := eclipse.LocalSolarEclipseOnDate(date, 118.0894, 24.4798, 0)
fmt.Println(ok, info.Type) // 是否命中本地日食;食型
fmt.Println(info.HasSaros, info.Saros) // 是否匹配沙罗序列;系列号、系列内序号、总成员数
fmt.Println(info.PartialStart) // 初亏
fmt.Println(info.CentralStart) // 环食开始
fmt.Println(info.GreatestEclipse) // 食甚
fmt.Println(info.CentralEnd) // 环食结束
fmt.Println(info.PartialEnd) // 复圆
fmt.Println(info.CentralEnd.Sub(info.CentralStart)) // 环食阶段持续时间
fmt.Printf("magnitude=%.6f obscuration=%.6f altitude=%.3f\n", info.Magnitude, info.Obscuration, info.SunAltitude) // 食分、遮掩比例、食甚太阳高度
}
```
输出结果:
```text
true annular // 厦门站点当天命中日食,食型为日环食
true {128 58 73 true} // Solar Saros 128,第 58/73 个成员,已核验
2012-05-21 05:08:12.878718674 +0800 CST // 初亏
2012-05-21 06:08:15.561088621 +0800 CST // 环食开始
2012-05-21 06:10:25.180663168 +0800 CST // 食甚
2012-05-21 06:12:34.80941087 +0800 CST // 环食结束
2012-05-21 07:20:54.806806147 +0800 CST // 复圆
4m19.248322249s // 环食持续时间
magnitude=0.933289 obscuration=0.872354 altitude=9.565 // 食分、遮掩比例、食甚太阳高度
```
### 生成日食 SVG
现代城市观测示例可以使用 `2035-09-02` 北京日全食。按北京市区近似坐标(东经 `116.4074`,北纬 `39.9042`)计算,这次事件属于 `Solar Saros 145` 的第 `23/77` 个成员,全食阶段持续约 `1m33s`。
默认日食 SVG 头部会自动带上沙罗序列和全食/环食历时;更多文案可通过 `LocalSolarEclipseSVGOptions` 覆写:
- `Title`:主标题
- `SummaryText` / `GreatestText` / `MetaText`:标题下三行摘要
- `OverviewTitle` / `PhasePanelsTitle` / `ContactsTitle`:总览、阶段视圆、接触时刻三个分区标题
- `DirectionText` / `FooterNote`:底部方向说明和补充说明
```go
package main
import (
"fmt"
"os"
"time"
"b612.me/astro/eclipse"
eclipsesvg "b612.me/astro/eclipse/svg"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
// 2009 长江口洋山附近日全食图。
totalSVG, ok := eclipsesvg.LocalSolarEclipseSVG(
time.Date(2009, 7, 22, 12, 0, 0, 0, cst),
121.9850, 30.6167, 0,
eclipsesvg.LocalSolarEclipseSVGOptions{
Width: 920,
Height: 720,
Step: 5 * time.Minute,
Location: cst,
},
)
fmt.Println(ok, len(totalSVG)) // 是否生成成功;SVG 字节长度
if ok {
_ = os.WriteFile("doc/img/solar-eclipse-yangshan-2009.svg", []byte(totalSVG), 0o644)
}
// 2012 厦门日环食图。
annularSVG, ok := eclipsesvg.LocalSolarEclipseSVG(
time.Date(2012, 5, 21, 12, 0, 0, 0, cst),
118.0894, 24.4798, 0,
eclipsesvg.LocalSolarEclipseSVGOptions{
Width: 920,
Height: 720,
Step: 5 * time.Minute,
Location: cst,
},
)
fmt.Println(ok, len(annularSVG)) // 是否生成成功;SVG 字节长度
if ok {
_ = os.WriteFile("doc/img/solar-eclipse-xiamen-2012.svg", []byte(annularSVG), 0o644)
}
// 2035 北京日全食图,同时打印沙罗序列号和全食持续时间。
beijingDate := time.Date(2035, 9, 2, 12, 0, 0, 0, cst)
beijingInfo, ok := eclipse.LocalSolarEclipseOnDate(beijingDate, 116.4074, 39.9042, 0)
fmt.Println(ok, beijingInfo.Type) // 是否命中本地日食;食型
fmt.Println(beijingInfo.HasSaros, beijingInfo.Saros) // 是否匹配沙罗序列;系列号、系列内序号、总成员数
fmt.Println(beijingInfo.CentralEnd.Sub(beijingInfo.CentralStart)) // 全食持续时间
beijingSVG, ok := eclipsesvg.LocalSolarEclipseSVG(
beijingDate,
116.4074, 39.9042, 0,
eclipsesvg.LocalSolarEclipseSVGOptions{
Width: 920,
Height: 720,
Step: 5 * time.Minute,
Location: cst,
},
)
fmt.Println(ok, len(beijingSVG)) // 是否生成成功;SVG 字节长度
if ok {
_ = os.WriteFile("doc/img/solar-eclipse-beijing-2035.svg", []byte(beijingSVG), 0o644)
}
}
```
输出结果:
```text
true 13625 // 洋山日全食 SVG 生成成功,长度 13625 字节
true 13542 // 厦门日环食 SVG 生成成功,长度 13542 字节
true total // 北京站点当天命中日食,食型为日全食
true {145 23 77 true} // Solar Saros 145,第 23/77 个成员,已核验
1m33.329527974s // 北京市区近似坐标下的全食持续时间
true 13589 // 北京日全食 SVG 生成成功,长度 13589 字节
```
生成效果:
![2009 长江口洋山日全食](../img/solar-eclipse-yangshan-2009.svg)
![2012 厦门日环食](../img/solar-eclipse-xiamen-2012.svg)
![2035 北京日全食](../img/solar-eclipse-beijing-2035.svg)
## 月食
本库的月食判断与搜索能力统一放在 `eclipse` 包,返回结果会保持传入 `time.Time` 的时区。
常用接口:
- `LunarEclipseOnDate`:判断某个当地日期是否有月食
- `LastLunarEclipse` / `NextLunarEclipse` / `ClosestLunarEclipse`:搜索全局月食
- `LocalLunarEclipseOnDate`:判断某地当天是否能看到可见月食
- `LastLocalLunarEclipse` / `NextLocalLunarEclipse` / `ClosestLocalLunarEclipse`:搜索某地可见月食
- `LastLocalTotalLunarEclipse` / `NextLocalTotalLunarEclipse` / `ClosestLocalTotalLunarEclipse`:搜索某地可见月全食,返回 `(info, ok)`
- `GeometricLocalLunarEclipseOnDate`:判断某地当天是否发生几何月食,不做“月亮在地平线上方”的可见性过滤
- `eclipse/svg.LunarEclipseSVG`:生成月食穿影图 SVG
`LocalLunarEclipseInfo.Visibility` 按当地中天情况分为八类:`full`、`moonrise`、`moonset`、`rise-and-set`、`interrupted`、`penumbra-moonrise`、`penumbra-moonset` 与 `invisible`。两种 `penumbra-*` 状态要求本影阶段始终在地平线下,判定检查整个本影区间的月球高度极值,以覆盖接触时刻之间的极区掠射窗口;纯半影月食没有本影接触,不会返回这两类。`interrupted` 表示半影首尾可见而中途落到地平线下,即使本影阶段始终不可见,也仍属于此类。分类使用观测点当地中天,不使用全球食甚时刻。
`MarshalLunarEclipse` 导出 `visible-at-p1`、`visible-at-p4` 两个瞬时可见半球,以及 `visible-during-eclipse`(P1 到 P4 任意时刻可见)和 `visible-throughout-eclipse`(P1 到 P4 全程可见)两个时间包络。包络分别使用 `aggregation=union` 和 `aggregation=intersection`;没有全程可见区域时,后者为空 `MultiPolygon`。包络按经度列采样,列数由 `boundary_points` 推导并限制在 360..720,实际值写入 `longitude_points`。
同一接口还导出 `penumbra-moonset` 与 `penumbra-moonrise` 两条仅见半影带,分别对应 P1 可见/P4 不可见与 P4 可见/P1 不可见、且本影阶段始终不可见的区域;每条带包含 `phase=penumbral-only` 和自己的接触时刻。纯半影月食没有本影接触,不导出这两条带。
`MarshalLunarEclipseWithOptions` 通过 `LunarEclipseOptions` 控制输出内容和采样精度。`SkipRoles` 可跳过时间包络与仅见半影带;跳过两个时间包络时不会执行包络采样。`EnvelopeSweepSamples` 默认 48,显式值限制在 `[2, 192]`;`EnvelopeLongitudePoints` 默认 `max(360, boundaryPoints)`,显式值限制在 `[12, 720]`。
`MoonHorizon` 和 `MoonStateAt` 接收 UTC 儒略日;`HMoonHeight(jd, lon, lat, tz)` 接收该时区的民用墙上时间儒略日,并使用小时单位的时区偏移换算。仅当 `tz=0` 时,`jd` 才是 UTC;时标换算由库内部完成。
返回结果 `LunarEclipseInfo` 包含:
- 月食类型 `Type`
- 沙罗序列信息 `HasSaros` / `Saros`
- 半影食分 `PenumbralMagnitude`
- 本影食分 `UmbralMagnitude`
- 半影始、初亏、食既、食甚、生光、复圆、半影终等时刻
其中 `Saros` 的含义与日食部分相同:
- `Saros.Series`:`Verified=true` 时为 NASA 月食沙罗系列号,否则为推算的暂定系列号
- `Saros.Member`:这次月食在该系列中的第几个成员,从 `1` 开始
- `Saros.Count`:该沙罗系列的总成员数
- `Saros.Verified`:是否已与内置权威目录锚点核验;扩展表或范围外推算结果为 `false`
月食同样优先使用 NASA 锚点,天文年份 `-3000` 至 `+6000` 年内查扩展表,范围外才实时演算。推算成员按 Danjon 与 Chauvenet 检出的事件并集计数,因此不随调用的月食模型或观测地点改变;极浅成员可能与 NASA 目录不同,`Verified` 保持 `false`。
例如 `2028-12-31 / 2029-01-01` 这次跨年月全食属于 `月食沙罗序列125` 的第 `49/72` 个成员。
当前同时保留两套地影放大口径:
- **Danjon(默认)**:只对月球水平视差项乘 `1.01`,再与太阳视半径、太阳视差组合求影半径。NASA GSFC 当前月食目录与图页采用的也是这一路线,本库默认的 `LunarEclipseOnDate`、`LastLunarEclipse`、`NextLunarEclipse`、`ClosestLunarEclipse` 都使用它。
- **Chauvenet(兼容口径)**:先取 `0.99834 × 地球赤道半径`,再把整组影半径统一乘 `51/50`。这与传统旧历表口径更接近,适合做兼容性回归和旧结果对照。
两者的直接差异通常表现为:
- `Chauvenet` 给出的半影和本影都更大,半影食分通常比 `Danjon` 多约 `0.025`,本影食分通常多约 `0.005`
- 对边界月食而言,`Chauvenet` 更容易把结果推向“更深”的食型
- 与 NASA 目录、现代星历软件或当前主流月食资料对照时,对应的是默认的 `Danjon`
- 兼容既有历史基线时,对应的是显式调用的 `Chauvenet`
### 代码示例
```go
package main
import (
"b612.me/astro/eclipse"
"fmt"
"time"
)
func main() {
date := time.Date(2029, 1, 1, 0, 0, 0, 0, time.UTC)
// 默认使用 Danjon,更接近 NASA
info := eclipse.ClosestLunarEclipse(date)
fmt.Println(info.Type)
fmt.Println(info.HasSaros, info.Saros)
fmt.Println(info.Maximum)
fmt.Println(info.PenumbralMagnitude, info.UmbralMagnitude)
fmt.Println(info.PenumbralStart)
fmt.Println(info.PartialStart)
fmt.Println(info.TotalStart)
fmt.Println(info.TotalEnd)
fmt.Println(info.PartialEnd)
fmt.Println(info.PenumbralEnd)
// 如需兼容旧口径,可显式使用 Chauvenet
legacy := eclipse.ClosestLunarEclipseChauvenet(date)
fmt.Println(legacy.PenumbralMagnitude, legacy.UmbralMagnitude)
// 判断某个本地自然日是否发生月食,返回时区与输入保持一致
local := time.Date(2029, 1, 1, 12, 0, 0, 0, time.FixedZone("CST", 8*3600))
today, ok := eclipse.LunarEclipseOnDate(local)
fmt.Println(ok)
fmt.Println(today.Type)
fmt.Println(today.Maximum)
}
```
输出结果:
```text
total
true {125 49 72 true}
2028-12-31 16:52:05.603753328 +0000 UTC
2.2739938633996872 1.2461094682708755
2028-12-31 14:03:54.239418804 +0000 UTC
2028-12-31 15:07:42.171904742 +0000 UTC
2028-12-31 16:16:27.306801974 +0000 UTC
2028-12-31 17:27:46.228030622 +0000 UTC
2028-12-31 18:36:32.270547151 +0000 UTC
2028-12-31 19:40:11.575520038 +0000 UTC
2.299608256177245 1.2511661731458574
true
total
2029-01-01 00:52:05.603753328 +0800 CST
```
### 与 NASA 数据对照
对照值取自 NASA GSFC 的月食目录(食甚印为 **TD**、食分为目录值)。**UT 层的比较必须先扣掉时标口径**:目录与单次图页的 UT 时刻是用其出版时采用的 ΔT 从 TD 换算的(2026 年取 `75 s`、2024 年取 `74 s`),而实测 ΔT 只有约 `69.1–69.2 s`,两者相差 `4.8–5.9 s`。所以 UT 层的逐接触比较会整体带上这个时标差,它不是月球几何误差;TD 层(不含 ΔT)才是能单独检验星历与几何的一层。
| 样例 | 模型 | 半影食分误差 | 本影食分误差 | 食甚 TD 差(不含 ΔT) | 食甚 UT 差(含 ΔT 口径) |
|------|------|--------------|--------------|----------------------|--------------------------|
| 2026-03-03 月全食 | Danjon | -0.000067208 | -0.000069993 | +0.114 s | +5.99 s |
| 2026-03-03 月全食 | Chauvenet | +0.025599846 | +0.004935007 | +0.114 s | +5.99 s |
| 2026-08-28 月偏食 | Danjon | -0.000113694 | -0.000033624 | +0.090 s | +5.93 s |
| 2026-08-28 月偏食 | Chauvenet | +0.025567662 | +0.004957333 | +0.090 s | +5.93 s |
| 2024-03-25 半影月食 | Danjon | -0.000176555 | 见下说明 | +1.012 s | +5.81 s |
| 2024-03-25 半影月食 | Chauvenet | +0.026044973 | 见下说明 | +1.012 s | +5.81 s |
以 `2026-03-03` 月全食为例,当前默认 `Danjon` 与 NASA 的逐项差异为:
- 食型:一致,都是 `total`
- 食甚:本库 TD `11:34:52.113` vs NASA `11:34:52`,差 `+0.114 s`;换算到 UT 后本库比 NASA 图页晚 `5.99 s`,其中 `5.88 s` 来自 NASA 采用 `ΔT = 75 s`、本库实测 `ΔT = 69.12 s`
- 阶段历时:半影 `338.67 min` vs NASA `338.6`、本影 `207.17 min` vs `207.2`、全食 `58.31 min` vs `58.3`,都落在目录 0.1 分钟的印刷精度内
- 半影食分:`2.183732792` vs NASA `2.1838`,误差 `-0.000067208`
- 本影食分:`1.150630007` vs NASA `1.1507`,误差 `-0.000069993`
同一例中,`Chauvenet` 的结果为:
- 食型:一致,都是 `total`
- 半影食分:`2.209399846` vs NASA `2.1838`,误差 `+0.025599846`
- 本影食分:`1.155635007` vs NASA `1.1507`,误差 `+0.004935007`
`Chauvenet` 是保留给旧历表/旧口径兼容的影半径模型,半影和本影都会比默认 `Danjon` 更大;与 NASA 当前目录对照时,食分与接触时刻都会出现分钟/百分之一量级的偏移。这是模型口径差异,不代表默认月食接口的时间精度。
> 说明:NASA 目录的 TD 只印到整秒,因此 ±0.5 s 以内都属于印刷精度;上表 `2024-03-25` 的 `+1.012 s` 已经略超这一精度,是半影食甚这种极浅几何下两条链的差异。
> 说明:纯半影月食时,NASA 会给出负的 `umbral magnitude`,表示月面中心距本影边界还有余量;本库也保留这个负值,因此纯半影月食与 NASA 的本影食分已经属于同口径比较。
### 月食 SVG
`LunarEclipseSVG`、`LunarEclipseDetailedSVG` 与 `LunarEclipseMapSVG` 的默认模型与后缀入口口径见下文[月食出图](#月食出图)。
默认月食 SVG 头部会自动带上沙罗序列;如果需要自定义更多文字,可以通过 `LunarEclipseSVGOptions` 覆写:
- `Title`:主标题
- `SummaryText` / `MaximumText` / `CoordinatesText` / `DurationText` / `MetaText`:标题下五行信息
- `ContactsTitle`:接触时刻区标题
- `DirectionText` / `FooterNote`:底部方向说明和补充说明
```go
package main
import (
"fmt"
"os"
"time"
eclipsesvg "b612.me/astro/eclipse/svg"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
// 生成 2029-01-01 这次跨年月全食的穿影图。
svg, ok := eclipsesvg.LunarEclipseSVG(
time.Date(2029, 1, 1, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseSVGOptions{
Width: 960,
Height: 620,
Step: 10 * time.Minute,
Location: cst,
},
)
fmt.Println(ok, len(svg))
if ok {
_ = os.WriteFile("doc/img/lunar-eclipse-2029-01-01.svg", []byte(svg), 0o644)
}
}
```
输出结果:
```text
true 19831
```
生成效果:
![2029 跨年月全食穿影图](../img/lunar-eclipse-2029-01-01.svg)
### 参考资料
- NASA 月食 decade 目录:<https://eclipse.gsfc.nasa.gov/LEdecade/LEdecade2021.html?pubDate=20250222>
- NASA 2026-03-03 月全食图页:<https://eclipse.gsfc.nasa.gov/LEplot/LEplot2001/LE2026Mar03T.pdf>
- NASA 2026-08-28 月偏食图页:<https://eclipse.gsfc.nasa.gov/LEplot/LEplot2001/LE2026Aug28P.pdf>
- NASA 2024-03-25 半影月食图页:<https://eclipse.gsfc.nasa.gov/LEplot/LEplot2001/LE2024Mar25N.pdf>
- NASA 月食算法与历史说明:<https://eclipse.gsfc.nasa.gov/LEhistory/LEhistory.html>
## 全球见食图与月食出图
`eclipse/svg` 的五个入口都返回 `(string, bool)`:第二个返回值是 `false` 表示这张图按当前参数画不出来(当天没有该事件、画布低于下限、或 UT1 口径配了非 UTC 时区),不是渲染错误。
| 入口 | 画面 | 画布建议 |
| --- | --- | --- |
| `LocalSolarEclipseSVG` | 站心日面视圆图(见前文“生成日食 SVG”) | 920×720 起 |
| `SolarEclipseMapSVG` | 全球见食图,可切四种投影 | 1200×800;正射球面 1000×1414 |
| `LunarEclipseSVG` | 月食穿影图 | 960×620 起 |
| `LunarEclipseMapSVG` | 月食世界可见区图 | 1200×800 |
| `LunarEclipseDetailedSVG` | 月食详细版式(示意与底图合成一页) | 1000×1414 或 1414×1000 |
日食全球图包含偏食可见区、中心带与路径、阶段信息、日升日落线、太阳直射点、影轴进出地球点和接触点;定时半影及本影/反本影轮廓可按需打开。月食图绘制 `P1/P4` 可见半球和月出/月落过渡区,以 `P1`、食甚、`P4` 三刻地平圈近似整场可见区;它不包含 GeoJSON 的时间包络,因此两条地平线之间可能留有窄缝。
### 全球见食图
#### 等经纬投影
最小可用调用只给事件日期与展示时区,其余走默认(`TimeLabelStep` 默认 30 分钟):
```go
solar, ok := eclipsesvg.SolarEclipseMapSVG(
time.Date(2009, 7, 22, 12, 0, 0, 0, cst),
eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1414, Height: 1000, Location: cst,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
},
)
fmt.Println(ok, len(solar))
```
![2009 长江大日食全球见食图](../img/solar-eclipse-yangshan-2009-global.svg)
另外两场等经纬示例是 `2012-05-21` 厦门日环食与 `2035-09-02` 北京日全食,两张图与长江口图同口径:不画瞬时半影/本影轮廓、画 30 分钟食甚等时线,并用空切片关闭食分等值线:
```go
options := eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
MagnitudeValues: []float64{},
}
annular, ok := eclipsesvg.SolarEclipseMapSVG(time.Date(2012, 5, 21, 12, 0, 0, 0, cst), options)
total, ok := eclipsesvg.SolarEclipseMapSVG(time.Date(2035, 9, 2, 12, 0, 0, 0, cst), options)
fmt.Println(ok, len(annular), len(total))
```
![2012 厦门日环食全球见食图](../img/solar-eclipse-xiamen-2012-global.svg)
![2035 北京日全食全球见食图](../img/solar-eclipse-beijing-2035-global.svg)
#### 正射球面图
`EclipseMapProjectionOrthographic` 给出 NASA 版式的**正射球面图**:视点取食甚点,只画朝向视点的半个地球,建议使用 `1000×1414` 画布。投影不改变底层地理结果。
```go
globe, ok := eclipsesvg.SolarEclipseMapSVG(
time.Date(2009, 7, 22, 12, 0, 0, 0, cst),
eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1000, Height: 1414, Location: cst,
Projection: eclipsesvg.EclipseMapProjectionOrthographic,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
},
)
```
![2009 长江大日食正射球面图](../img/solar-eclipse-yangshan-2009-globe.svg)
正射球面图在竖版(如 `1000×1414`)下最舒展;横版(`1200×800`)同样能出图,只是球面会明显缩小。
#### 南北极方位等距投影
`EclipseMapProjectionNorthPolar` 与 `EclipseMapProjectionSouthPolar` 把极点放在画布中心,适合食带整体落在高纬的事件。`EclipseMapProjectionAuto`(零值)会在适合时自动选极图,需要固定版式时才显式指定;投影只影响 SVG 表达,不改变底层 WGS84 地理结果。
`2012-05-21` 日环食的偏食可见区覆盖北极点,强制北极投影便于查看跨反经线的见食范围:
```go
arctic, ok := eclipsesvg.SolarEclipseMapSVG(
time.Date(2012, 5, 21, 12, 0, 0, 0, cst),
eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst,
Projection: eclipsesvg.EclipseMapProjectionNorthPolar,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
},
)
```
![2012 日环食北极区全球见食图](../img/solar-eclipse-arctic-2012-global.svg)
`2021-12-04` 日全食的食带整条落在南极区,南极投影是最自然的版式;下面这张与横版全球图同一口径,只画 30 分钟食甚等时线:
```go
south, ok := eclipsesvg.SolarEclipseMapSVG(
time.Date(2021, 12, 4, 12, 0, 0, 0, cst),
eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst,
Projection: eclipsesvg.EclipseMapProjectionSouthPolar,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
MagnitudeValues: []float64{},
},
)
```
![2021 南极日全食南极区全球见食图](../img/solar-eclipse-southpolar-2021-12-04.svg)
#### 同一事件批量出四种投影
四个投影共用一份选项,只是 `Projection` 不同,适合一次生成后挑版式:
```go
date := time.Date(2009, 7, 22, 12, 0, 0, 0, cst)
for _, spec := range []struct {
name string
p eclipsesvg.EclipseMapProjection
}{
{"equirectangular", eclipsesvg.EclipseMapProjectionEquirectangular},
{"north-polar", eclipsesvg.EclipseMapProjectionNorthPolar},
{"south-polar", eclipsesvg.EclipseMapProjectionSouthPolar},
{"orthographic", eclipsesvg.EclipseMapProjectionOrthographic},
} {
options := eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst, Projection: spec.p,
}
svg, ok := eclipsesvg.SolarEclipseMapSVG(date, options)
if !ok {
continue
}
_ = os.WriteFile("solar-eclipse-"+spec.name+".svg", []byte(svg), 0o644)
}
```
#### 图层与采样开关
| 选项 | 默认 | 语义 |
| --- | --- | --- |
| `TimeLabelStep` | 30 分钟 | 中心线时刻标记间隔;负值关闭 |
| `GreatestTimeStep` | 关闭 | 食甚时刻等时线;必须显式给正值,按展示时区对齐,小于一分钟按一分钟,单次最多 64 条 |
| `MagnitudeValues` | `0.2/0.4/0.6/0.8`(nil 时) | 食分等值线电平;显式空切片关闭,非空切片按给定电平绘制 |
| `PenumbralOutlineStep` | 关闭 | 瞬时半影轮廓采样间隔;零值或负值不画,正值小于一分钟按一分钟 |
| `CentralShadowStep` | 关闭 | 瞬时本影/反本影轮廓采样间隔;同上 |
| `PartialStep` | 2 分钟 | 偏食足迹时间步长;非正值与小于两分钟的正值都夹到两分钟 |
| `BoundaryPoints` | 180 | 每个瞬时偏食足迹的角向采样数;非正值用 180,正值限制在 12..1440 |
| `CentralStep` | 2 分钟 | 中心路径时间步长;非正值用两分钟,正值小于一秒按一秒,长事件会自动放大以保持基础路径不超过 30000 个采样点 |
| `TargetSpacingKM` | 150 km | 中心线地面间距上限;非正值用 150 km,NaN 与 +Inf 禁用加密 |
半影/本影轮廓默认关闭。食分等值线在 `MagnitudeValues == nil` 时使用表中的默认电平;显式传入空切片可关闭。厦门、北京、北极区与南极点四张图不画瞬时轮廓,只画 30 分钟食甚等时线,并显式关闭食分等值线:
```go
options := eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst,
TimeLabelStep: 30 * time.Minute, GreatestTimeStep: 30 * time.Minute,
MagnitudeValues: []float64{},
}
```
等时线不是逐点求食甚再描等值线,而是固定时刻后解 `∂(日月中心角距²)/∂t = 0` 的零集再沿曲线延拓,成本正比于曲线长度;每条支路止于地平线或偏食可见域边界,纬度 ±88° 以上不再延拓,同一时刻可能有多条互不相连的支路。
可降级的图层用 `data-source` 标注实际几何来源,取值词表见 [地图投影](map-geojson.md#地图投影)。
#### 画布下限与回落
- 日食图下限 **800×560**:宽度小于 800 或高度小于 560 时回落到 960×640;更窄的横版画布会让地图框与右栏数据网格水平重叠、面板行距压到 1 px 以下。
- 偏食区填充由瞬时足迹的并集生成,因此 `PartialStep` 小于两分钟时会夹到两分钟;更密的请求不会提高结果精度。
- 月食详细版式按 `Height` 推导版面:`640×420` 与 `800×600` 容不下示意图与底图的下限而返回 `false`,`1000×1414` 与 `1414×1000` 正常出图。
### 月食出图
#### 三个入口与影半径模型
`LunarEclipseSVG`(穿影图)、`LunarEclipseMapSVG`(世界可见区)与 `LunarEclipseDetailedSVG`(详细版式)默认共用同一套模型:以 Danjon 为主,极浅半影按核心默认口径回退 Chauvenet,与 `LunarEclipseOnDate` 一致;带 `Danjon` / `Chauvenet` 后缀的入口强制指定模型,`...Chauvenet` 适合与采用经典影半径的旧表对表。
```go
diagram, ok := eclipsesvg.LunarEclipseSVG(
time.Date(2029, 1, 1, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseSVGOptions{
Width: 960, Height: 620, Step: 10 * time.Minute, Location: cst,
},
)
fmt.Println(ok, len(diagram))
```
![2029 跨年月全食穿影图](../img/lunar-eclipse-2029-01-01.svg)
#### 世界可见区图
底图区分全程可见、带食月出、带食月落与不可见四类区域:全程可见要求 `P1`、食甚、`P4` 三刻月球都在地平上(两端可见推不出中途可见,高纬下中天会让月亮在食甚前后落到地平下,这段归带食月落);带食月落覆盖 `P1` 可见而 `P4` 不可见的地点,以及只在食甚前后露出地平、两端都在地平下的极区透镜;带食月出覆盖 `P4` 可见而 `P1` 不可见的地点;三刻都不见才算不可见。`Projection` 可切极区与正射版式;下面这张图是默认输出,半影阶段已纳入分区(口径见下):
```go
visible, ok := eclipsesvg.LunarEclipseMapSVG(
time.Date(2029, 1, 1, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseMapSVGOptions{Width: 1200, Height: 800, Location: cst},
)
```
![2029 跨年月全食全球可见图](../img/lunar-eclipse-2029-01-01-global.svg)
半影阶段默认纳入分区,画法同 NASA 月食世界图:补画 `U1`、`U2`、`U3`、`U4` 四条地平边界线,把只看得见半影的月出、月落带单独着色,图例里分列「半影月出」与「半影月落」(月出偏蓝、月落偏紫红),并在图上方的时刻行里补一行本影接触时刻。两条带排除**整个本影阶段的可见区**:本影区间内只要有任何时刻月亮在地平上,该地点就归带食月出/带食月落。掩膜按 15 分钟档位取样整球可见半球(圆盘分辨率约 0.035°),残余的掠射窗口深度约 0.05°(约 0.15 像素);深度更浅、只持续几分钟的窗口由站点 API 与 GeoJSON 精确判定。纯半影月食没有本影接触,开关两侧输出一致。`DisablePenumbralPhase: true` 退回只按三刻地平状态分的四类可见区,不画 `U1–U4` 与这两条半影带:
```go
penumbral, ok := eclipsesvg.LunarEclipseMapSVG(
time.Date(2026, 3, 3, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: cst, DisablePenumbralPhase: true,
},
)
```
`LunarEclipseDetailedSVGOptions` 有同名字段,控制详细版式下方那张底图,默认同样画半影阶段。
#### 详细版式
详细版式把两类月食图合成一页:居中摘要(食甚、半影/本影食分、伽马、半影/本影半径、月距、沙罗序列)、左右两侧的日月地心坐标块、穿影示意图、历时/弧分比例尺/接触时刻三栏,以及下方的世界可见性底图与图例。地影几何由 `basic.LunarEclipseShadowGeometryAt` 给出,其中 **Gamma 用地球赤道半径、半影/本影半径用度**,换算成地球半径要乘以月球处的地球视差。
```go
detailed, ok := eclipsesvg.LunarEclipseDetailedSVG(
time.Date(2029, 1, 1, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseDetailedSVGOptions{Location: cst},
)
```
![2029 跨年月全食详细版式](../img/lunar-eclipse-2029-01-01-detailed.svg)
第二个事件 `2026-03-03` 月全食用同一批入口,穿影图与详细版式各一张:
```go
diagram2026, ok := eclipsesvg.LunarEclipseSVG(
time.Date(2026, 3, 3, 0, 0, 0, 0, cst),
eclipsesvg.LunarEclipseSVGOptions{Width: 960, Height: 620, Step: 10 * time.Minute, Location: cst},
)
detailed2026, ok := eclipsesvg.LunarEclipseDetailedSVG(
time.Date(2026, 3, 3, 12, 0, 0, 0, cst),
eclipsesvg.LunarEclipseDetailedSVGOptions{Location: cst},
)
```
![2026 月全食穿影图](../img/lunar-eclipse-2026-03-03.svg)
![2026 月全食详细版式](../img/lunar-eclipse-2026-03-03-detailed.svg)
版面由 `Height` 推导:横版把数据块排在地图右侧两栏三行,竖版把数据块排在下方三栏两行。
### 时标口径与 UT1
四族图默认按 UTC 口径出图,并在图上或图注写明时标;`TimeScale: astro.TimeScaleUT1` 改成 UT1 读数并补上 `DUT1 = UT1−UTC` 差值,此时 `Location` 必须是 UTC,否则返回 `false`。几何一律按民用时刻算完再换口径,换算不会平移等时线。
要在程序里直接取 UT1 读数,用 `eclipse` 的 `...InUT1` 系列(`SolarEclipseInfoInUT1`、`LocalSolarEclipseInfoInUT1`、`LunarEclipseInfoInUT1`、`SolarEclipsePathInUT1`、`SolarEclipsePartialFootprintsInUT1`、`SolarEclipseGeocentricPanelInUT1`、`TimeLabelsInUT1`),它们只改时刻字段,零值原样保留。
完整约定见[时标声明](map-geojson.md#时标声明)。
```go
ut1, ok := eclipsesvg.SolarEclipseMapSVG(
time.Date(2009, 7, 22, 12, 0, 0, 0, cst),
eclipsesvg.SolarEclipseMapSVGOptions{
Width: 1200, Height: 800, Location: time.UTC,
TimeScale: astro.TimeScaleUT1, TimeLabelStep: 30 * time.Minute,
},
)
```
![2009 长江大日食全球见食图(UT1 口径)](../img/solar-eclipse-yangshan-2009-global-ut1.svg)