2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
106 lines
4.1 KiB
Go
106 lines
4.1 KiB
Go
package occultationgeo
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
"b612.me/astro/internal/geodata"
|
|
)
|
|
|
|
const defaultRiseSetProjectedSpacingKM = 35.0
|
|
|
|
// DensifyRiseSetCurves 按投影距离插入大地线样本,且不改变端点和分支拓扑。
|
|
// DensifyRiseSetCurves inserts geodesic samples using the projected distance
|
|
// consumed by map renderers. Rise/set solvers intentionally use a coarser time
|
|
// step for performance; using those raw vertices as SVG/GeoJSON linework makes
|
|
// high-latitude curves visibly angular even when the physical path is smooth.
|
|
// Endpoints, phase, direction, and segment topology are preserved exactly.
|
|
func DensifyRiseSetCurves(
|
|
curves []basic.OccultationRiseSetCurve,
|
|
maximumProjectedSpacingKM float64,
|
|
) []basic.OccultationRiseSetCurve {
|
|
if len(curves) == 0 || maximumProjectedSpacingKM <= 0 {
|
|
return curves
|
|
}
|
|
result := make([]basic.OccultationRiseSetCurve, len(curves))
|
|
for curveIndex, curve := range curves {
|
|
result[curveIndex] = basic.OccultationRiseSetCurve{
|
|
Phase: curve.Phase, Direction: curve.Direction,
|
|
Segments: make([][]basic.OccultationPathPoint, len(curve.Segments)),
|
|
}
|
|
for segmentIndex, segment := range curve.Segments {
|
|
result[curveIndex].Segments[segmentIndex] = densifyRiseSetSegment(segment, maximumProjectedSpacingKM)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// DensifyOccultationPathPoints 将相同的投影感知间距应用于单条月掩路径。
|
|
// DensifyOccultationPathPoints applies the same projection-aware spacing to a
|
|
// standalone geographic line such as a horizon connector. It keeps both
|
|
// endpoints and does not attach phase metadata to the result.
|
|
func DensifyOccultationPathPoints(
|
|
points []basic.OccultationPathPoint,
|
|
maximumProjectedSpacingKM float64,
|
|
) []basic.OccultationPathPoint {
|
|
return densifyRiseSetSegment(points, maximumProjectedSpacingKM)
|
|
}
|
|
|
|
func densifyRiseSetSegment(
|
|
segment []basic.OccultationPathPoint,
|
|
maximumProjectedSpacingKM float64,
|
|
) []basic.OccultationPathPoint {
|
|
if len(segment) < 2 {
|
|
return append([]basic.OccultationPathPoint(nil), segment...)
|
|
}
|
|
result := make([]basic.OccultationPathPoint, 0, len(segment)*2)
|
|
// The interpolation is geodesic on the sphere, while the spacing check is
|
|
// made in Web Mercator. A chord that is exactly at the projected limit can
|
|
// grow slightly after interpolation because Mercator is nonlinear in
|
|
// latitude. Keep a small margin so the emitted linework stays below the
|
|
// requested limit after projection.
|
|
effectiveSpacingKM := maximumProjectedSpacingKM * 0.75
|
|
for index, point := range segment {
|
|
result = append(result, point)
|
|
if index+1 >= len(segment) {
|
|
continue
|
|
}
|
|
next := segment[index+1]
|
|
steps := int(math.Ceil(projectedGeoPointDistanceKM(point, next) / effectiveSpacingKM))
|
|
if steps < 2 {
|
|
continue
|
|
}
|
|
for step := 1; step < steps; step++ {
|
|
fraction := float64(step) / float64(steps)
|
|
middle := interpolateOccultationGeoPoint(
|
|
geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude},
|
|
geodata.GeoPoint{Longitude: next.Longitude, Latitude: next.Latitude},
|
|
fraction,
|
|
)
|
|
result = append(result, basic.OccultationPathPoint{
|
|
Time: point.Time.Add(time.Duration(float64(next.Time.Sub(point.Time)) * fraction)),
|
|
Longitude: middle.Longitude,
|
|
Latitude: middle.Latitude,
|
|
MoonAltitude: point.MoonAltitude + (next.MoonAltitude-point.MoonAltitude)*fraction,
|
|
WidthKM: point.WidthKM + (next.WidthKM-point.WidthKM)*fraction,
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func projectedGeoPointDistanceKM(first, second basic.OccultationPathPoint) float64 {
|
|
const maxLatitude = 85.05112878
|
|
const radiusKM = 6378.1366
|
|
clampLatitude := func(value float64) float64 {
|
|
return math.Max(-maxLatitude, math.Min(maxLatitude, value))
|
|
}
|
|
longitude := math.Remainder(second.Longitude-first.Longitude, 360) * math.Pi / 180
|
|
firstLatitude := clampLatitude(first.Latitude) * math.Pi / 180
|
|
secondLatitude := clampLatitude(second.Latitude) * math.Pi / 180
|
|
firstY := math.Log(math.Tan(math.Pi/4 + firstLatitude/2))
|
|
secondY := math.Log(math.Tan(math.Pi/4 + secondLatitude/2))
|
|
return radiusKM * math.Hypot(longitude, secondY-firstY)
|
|
}
|