2026-09-17 12:27:40 +08:00
|
|
|
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 {
|
2026-10-15 23:54:15 +08:00
|
|
|
return geodata.WebMercatorDistanceKM(
|
|
|
|
|
geodata.GeoPoint{Longitude: first.Longitude, Latitude: first.Latitude},
|
|
|
|
|
geodata.GeoPoint{Longitude: second.Longitude, Latitude: second.Latitude},
|
|
|
|
|
)
|
2026-09-17 12:27:40 +08:00
|
|
|
}
|