Files
astro/internal/occultationgeo/curve_sampling.go
T

99 lines
3.7 KiB
Go
Raw Normal View History

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 {
return geodata.WebMercatorDistanceKM(
geodata.GeoPoint{Longitude: first.Longitude, Latitude: first.Latitude},
geodata.GeoPoint{Longitude: second.Longitude, Latitude: second.Latitude},
)
}