118 lines
5.7 KiB
Go
118 lines
5.7 KiB
Go
|
|
// Package lunarhorizon 在等距圆柱地图坐标里细化月食可见区使用的站心地平线。
|
|||
|
|
//
|
|||
|
|
// 极点附近一小段球面地平弧可以横跨近 180° 经度,把它的端点直接连成多边形会在极区
|
|||
|
|
// 切出地平线以外的假可见帽。这里按地图坐标误差自适应细分,并把新增顶点用同一个站心
|
|||
|
|
// 地平方程校正回零高度。
|
|||
|
|
//
|
|||
|
|
// Package lunarhorizon refines the topocentric horizon used by lunar-eclipse visibility
|
|||
|
|
// regions in equirectangular map coordinates.
|
|||
|
|
//
|
|||
|
|
// Near a pole a short spherical horizon arc can span almost 180 degrees of longitude, so
|
|||
|
|
// joining its endpoints directly cuts a false visible cap outside the horizon. This package
|
|||
|
|
// subdivides by error measured in map coordinates and corrects every inserted vertex onto the
|
|||
|
|
// same topocentric zero-altitude curve.
|
|||
|
|
package lunarhorizon
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"b612.me/astro/basic"
|
|||
|
|
"b612.me/astro/internal/geodata"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// normalizeLongitude 把经度归一化到 [-180, 180);internal/geodata 与 geojson 各有一份等价的未导出实现,无法跨包复用。
|
|||
|
|
func normalizeLongitude(value float64) float64 {
|
|||
|
|
value = math.Mod(value+180, 360)
|
|||
|
|
if value < 0 {
|
|||
|
|
value += 360
|
|||
|
|
}
|
|||
|
|
return value - 180
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// defaultToleranceDegrees 是 GeoJSON 导出使用的误差门限。GeoJSON 客户端可以无限放大,
|
|||
|
|
// 因此这里取远小于任何地图像素的角度。
|
|||
|
|
// defaultToleranceDegrees is the error tolerance used by the GeoJSON export. GeoJSON clients
|
|||
|
|
// can zoom without limit, so it stays far below any map pixel.
|
|||
|
|
const defaultToleranceDegrees = 0.002
|
|||
|
|
|
|||
|
|
// DefaultToleranceDegrees 暴露默认门限,供固定分辨率的调用方设置下限。
|
|||
|
|
// DefaultToleranceDegrees exposes the default tolerance so fixed-resolution callers can floor it.
|
|||
|
|
const DefaultToleranceDegrees = defaultToleranceDegrees
|
|||
|
|
|
|||
|
|
// Refine 用 GeoJSON 导出的默认门限细分一条站心地平线。
|
|||
|
|
// Refine subdivides one topocentric horizon with the GeoJSON export's default tolerance.
|
|||
|
|
//
|
|||
|
|
// Near a pole, a short spherical horizon arc can span almost 180 degrees of
|
|||
|
|
// longitude. Refine in map coordinates before clipping; extra vertices are
|
|||
|
|
// corrected to the same topocentric zero-altitude curve as the source ring.
|
|||
|
|
func Refine(points []geodata.GeoPoint, at time.Time) []geodata.GeoPoint {
|
|||
|
|
return RefineWithin(points, at, defaultToleranceDegrees)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RefineWithin 用给定误差门限(单位:度)细分一条站心地平线。插入的顶点被校正回零高度,
|
|||
|
|
// 因此细分只增加描述精度,不改变曲线本身;固定分辨率的目标(例如 SVG 地图)可以用
|
|||
|
|
// 与像素尺度相称的门限,避免为了显示不出来的精度生成成千上万个顶点。
|
|||
|
|
// RefineWithin subdivides one topocentric horizon with the given error tolerance in degrees.
|
|||
|
|
// Inserted vertices are corrected back onto zero altitude, so refinement adds description
|
|||
|
|
// accuracy without moving the curve. A fixed-resolution target such as an SVG map can pass a
|
|||
|
|
// tolerance matched to its pixel scale instead of emitting vertices no display can resolve.
|
|||
|
|
func RefineWithin(points []geodata.GeoPoint, at time.Time, toleranceDegrees float64) []geodata.GeoPoint {
|
|||
|
|
if !(toleranceDegrees > 0) {
|
|||
|
|
toleranceDegrees = defaultToleranceDegrees
|
|||
|
|
}
|
|||
|
|
if len(points) < 3 {
|
|||
|
|
return points
|
|||
|
|
}
|
|||
|
|
jd := basic.Date2JDE(at.UTC())
|
|||
|
|
tt := basic.TD2UT(jd, true)
|
|||
|
|
ra, dec := basic.HMoonTrueRaDec(tt)
|
|||
|
|
distanceAU := basic.HMoonAway(tt) / 149597870.7
|
|||
|
|
sidereal := basic.ApparentSiderealTime(jd) * 15
|
|||
|
|
center := geodata.GeoPoint{Longitude: normalizeLongitude(ra - sidereal), Latitude: dec}
|
|||
|
|
// Every correction is at the same instant. Reuse its full ephemeris while
|
|||
|
|
// retaining the ellipsoid and topocentric transform used by HMoonHeight.
|
|||
|
|
altitudeAt := func(point geodata.GeoPoint) float64 {
|
|||
|
|
ra, dec := basic.TopocentricRaDec(ra, dec, point.Latitude, point.Longitude, jd, distanceAU, 0)
|
|||
|
|
hourAngle := (sidereal + point.Longitude - ra) * math.Pi / 180
|
|||
|
|
latitude := point.Latitude * math.Pi / 180
|
|||
|
|
declination := dec * math.Pi / 180
|
|||
|
|
return math.Asin(math.Sin(latitude)*math.Sin(declination)+
|
|||
|
|
math.Cos(declination)*math.Cos(latitude)*math.Cos(hourAngle)) * 180 / math.Pi
|
|||
|
|
}
|
|||
|
|
result := make([]geodata.GeoPoint, 0, len(points))
|
|||
|
|
var refine func(geodata.GeoPoint, geodata.GeoPoint, int)
|
|||
|
|
refine = func(first, second geodata.GeoPoint, depth int) {
|
|||
|
|
middle := geodata.InterpolateGreatCircle(first, second, 0.5)
|
|||
|
|
linearLongitude := first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2
|
|||
|
|
errorDeg := math.Hypot(math.Remainder(middle.Longitude-linearLongitude, 360), middle.Latitude-(first.Latitude+second.Latitude)/2)
|
|||
|
|
if errorDeg <= toleranceDegrees || depth >= 20 {
|
|||
|
|
result = append(result, first)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for iteration := 0; iteration < 5; iteration++ {
|
|||
|
|
altitude := altitudeAt(middle)
|
|||
|
|
if math.Abs(altitude) < 1e-10 {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
angleDeg := basic.StarAngularSeparation(middle.Longitude, middle.Latitude, center.Longitude, center.Latitude)
|
|||
|
|
if !(angleDeg > 0) {
|
|||
|
|
// 只有传入的环并非地平线、中点与该瞬时月下点重合时才会走到这里;此时无法
|
|||
|
|
// 沿"朝向月下点"的方向修正,保留未修正的中点比产生 NaN 顶点安全。
|
|||
|
|
// Reached only when the input ring is not a horizon ring and the midpoint
|
|||
|
|
// coincides with that instant's sub-lunar point. There is no direction toward
|
|||
|
|
// the sub-lunar point to correct along, so keep the uncorrected midpoint rather
|
|||
|
|
// than emit a NaN vertex.
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
middle = geodata.InterpolateGreatCircle(middle, center, -altitude/angleDeg)
|
|||
|
|
}
|
|||
|
|
refine(first, middle, depth+1)
|
|||
|
|
refine(middle, second, depth+1)
|
|||
|
|
}
|
|||
|
|
for index, point := range points {
|
|||
|
|
refine(point, points[(index+1)%len(points)], 0)
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|