2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
156 lines
5.8 KiB
Go
156 lines
5.8 KiB
Go
package occultationgeo
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
)
|
|
|
|
// ValidateRiseSetCurves 检查 SVG 和 GeoJSON 渲染器消费的公开地平曲线数据。
|
|
// ValidateRiseSetCurves checks the public horizon-curve data consumed by SVG and GeoJSON renderers.
|
|
func ValidateRiseSetCurves(curves []basic.OccultationRiseSetCurve, start, end time.Time) error {
|
|
seen := make(map[[2]string]bool, len(curves))
|
|
for curveIndex, curve := range curves {
|
|
if !validRiseSetPhase(curve.Phase) {
|
|
return fmt.Errorf("curve[%d] has unsupported phase %q", curveIndex, curve.Phase)
|
|
}
|
|
if !validRiseSetDirection(curve.Direction) {
|
|
return fmt.Errorf("curve[%d] has unsupported direction %q", curveIndex, curve.Direction)
|
|
}
|
|
key := [2]string{string(curve.Phase), string(curve.Direction)}
|
|
if seen[key] {
|
|
return fmt.Errorf("curve[%d] duplicates phase %q and direction %q", curveIndex, curve.Phase, curve.Direction)
|
|
}
|
|
seen[key] = true
|
|
if len(curve.Segments) == 0 {
|
|
return fmt.Errorf("curve[%d] must contain at least one segment", curveIndex)
|
|
}
|
|
for segmentIndex, segment := range curve.Segments {
|
|
if len(segment) < 2 {
|
|
return fmt.Errorf("curve[%d].segment[%d] must contain at least two points", curveIndex, segmentIndex)
|
|
}
|
|
for pointIndex, point := range segment {
|
|
if err := validatePathPoint(point, start, end); err != nil {
|
|
return fmt.Errorf("curve[%d].segment[%d].point[%d]: %w", curveIndex, segmentIndex, pointIndex, err)
|
|
}
|
|
if pointIndex > 0 && !point.Time.After(segment[pointIndex-1].Time) {
|
|
return fmt.Errorf("curve[%d].segment[%d] times must be strictly increasing", curveIndex, segmentIndex)
|
|
}
|
|
if pointIndex > 0 && riseSetCurveBranchChanged(segment[pointIndex-1], point) {
|
|
return fmt.Errorf("curve[%d].segment[%d] contains a discontinuous branch jump between points %d and %d", curveIndex, segmentIndex, pointIndex-1, pointIndex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func riseSetCurveBranchChanged(first, second basic.OccultationPathPoint) bool {
|
|
distance := DistanceKM(first, second)
|
|
if !finite(distance) {
|
|
return true
|
|
}
|
|
if distance <= 500 {
|
|
return false
|
|
}
|
|
duration := math.Abs(second.Time.Sub(first.Time).Seconds())
|
|
return duration <= 0 || duration < 1 && distance/duration > 10
|
|
}
|
|
|
|
func validRiseSetPhase(phase basic.RiseSetPhase) bool {
|
|
switch phase {
|
|
case basic.RiseSetPhaseStart, basic.RiseSetPhaseGreatest, basic.RiseSetPhaseEnd:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validRiseSetDirection(direction basic.RiseSetDirection) bool {
|
|
switch direction {
|
|
case basic.RiseSetDirectionRise, basic.RiseSetDirectionSet:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ValidateFootprints 检查 SVG 和 GeoJSON 渲染器消费的瞬时月掩区域。
|
|
// ValidateFootprints checks instantaneous occultation regions consumed by SVG and GeoJSON renderers.
|
|
func ValidateFootprints(footprints []basic.OccultationFootprint, start, end time.Time) error {
|
|
previous := time.Time{}
|
|
for footprintIndex, footprint := range footprints {
|
|
if footprint.Time.IsZero() {
|
|
return fmt.Errorf("footprint[%d] time is required", footprintIndex)
|
|
}
|
|
if footprint.Time.Before(start) || footprint.Time.After(end) {
|
|
return fmt.Errorf("footprint[%d] time must be inside its contact interval", footprintIndex)
|
|
}
|
|
if !previous.IsZero() && !footprint.Time.After(previous) {
|
|
return fmt.Errorf("footprint times must be strictly increasing")
|
|
}
|
|
if len(footprint.Polygons) == 0 {
|
|
return fmt.Errorf("footprint[%d] must contain a polygon", footprintIndex)
|
|
}
|
|
for polygonIndex, polygon := range footprint.Polygons {
|
|
if len(polygon) < 3 {
|
|
return fmt.Errorf("footprint[%d].polygon[%d] must contain at least three points", footprintIndex, polygonIndex)
|
|
}
|
|
for pointIndex, point := range polygon {
|
|
if err := validatePathPoint(point, start, end); err != nil {
|
|
return fmt.Errorf("footprint[%d].polygon[%d].point[%d]: %w", footprintIndex, polygonIndex, pointIndex, err)
|
|
}
|
|
if !point.Time.Equal(footprint.Time) {
|
|
return fmt.Errorf("footprint[%d].polygon[%d].point[%d] time must match its footprint", footprintIndex, polygonIndex, pointIndex)
|
|
}
|
|
}
|
|
}
|
|
if footprint.Closed && len(footprint.Boundaries) != 1 {
|
|
return fmt.Errorf("footprint[%d] closed boundary must contain exactly one segment", footprintIndex)
|
|
}
|
|
for boundaryIndex, boundary := range footprint.Boundaries {
|
|
if len(boundary) < 2 {
|
|
return fmt.Errorf("footprint[%d].boundary[%d] must contain at least two points", footprintIndex, boundaryIndex)
|
|
}
|
|
for pointIndex, point := range boundary {
|
|
if err := validatePathPoint(point, start, end); err != nil {
|
|
return fmt.Errorf("footprint[%d].boundary[%d].point[%d]: %w", footprintIndex, boundaryIndex, pointIndex, err)
|
|
}
|
|
if !point.Time.Equal(footprint.Time) {
|
|
return fmt.Errorf("footprint[%d].boundary[%d].point[%d] time must match its footprint", footprintIndex, boundaryIndex, pointIndex)
|
|
}
|
|
}
|
|
}
|
|
previous = footprint.Time
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePathPoint(point basic.OccultationPathPoint, start, end time.Time) error {
|
|
if point.Time.IsZero() {
|
|
return fmt.Errorf("time is required")
|
|
}
|
|
if point.Time.Before(start) || point.Time.After(end) {
|
|
return fmt.Errorf("time must be inside the occultation interval")
|
|
}
|
|
if !finite(point.Longitude) || point.Longitude < -180 || point.Longitude > 180 {
|
|
return fmt.Errorf("longitude must be finite and within [-180, 180]")
|
|
}
|
|
if !finite(point.Latitude) || point.Latitude < -90 || point.Latitude > 90 {
|
|
return fmt.Errorf("latitude must be finite and within [-90, 90]")
|
|
}
|
|
if !finite(point.MoonAltitude) || point.MoonAltitude < -90 || point.MoonAltitude > 90 {
|
|
return fmt.Errorf("Moon altitude must be finite and within [-90, 90]")
|
|
}
|
|
if !finite(point.WidthKM) || point.WidthKM < 0 {
|
|
return fmt.Errorf("width must be finite and non-negative")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func finite(value float64) bool {
|
|
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
|
}
|