feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -86,7 +86,7 @@ func smoothOccultationOrdinaryWobbles(points []geodata.GeoPoint) []geodata.GeoPo
|
||||
y += weights[offset+2] * projected[1]
|
||||
}
|
||||
candidate := occultationUnprojectedPoint(x/35, y/35)
|
||||
move := geoDistanceKM(original[index], candidate)
|
||||
move := geodata.EquatorialDistanceKM(original[index], candidate)
|
||||
if move > maximumMoveKM {
|
||||
continue
|
||||
}
|
||||
@@ -109,20 +109,15 @@ func smoothOccultationOrdinaryWobbles(points []geodata.GeoPoint) []geodata.GeoPo
|
||||
func occultationProjectedPoint(point geodata.GeoPoint) [2]float64 {
|
||||
latitude := math.Max(-85.05112878, math.Min(85.05112878, point.Latitude)) * math.Pi / 180
|
||||
return [2]float64{
|
||||
EarthRadiusKM * point.Longitude * math.Pi / 180,
|
||||
EarthRadiusKM * math.Log(math.Tan(math.Pi/4+latitude/2)),
|
||||
geodata.EarthEquatorialRadiusKM * point.Longitude * math.Pi / 180,
|
||||
geodata.EarthEquatorialRadiusKM * math.Log(math.Tan(math.Pi/4+latitude/2)),
|
||||
}
|
||||
}
|
||||
|
||||
func occultationUnprojectedPoint(pointX, pointY float64) geodata.GeoPoint {
|
||||
longitude := pointX / EarthRadiusKM * 180 / math.Pi
|
||||
longitude = math.Mod(longitude+180, 360)
|
||||
if longitude < 0 {
|
||||
longitude += 360
|
||||
}
|
||||
return geodata.GeoPoint{
|
||||
Longitude: longitude - 180,
|
||||
Latitude: (2*math.Atan(math.Exp(pointY/EarthRadiusKM)) - math.Pi/2) * 180 / math.Pi,
|
||||
Longitude: geodata.NormalizeLongitude(pointX / geodata.EarthEquatorialRadiusKM * 180 / math.Pi),
|
||||
Latitude: (2*math.Atan(math.Exp(pointY/geodata.EarthEquatorialRadiusKM)) - math.Pi/2) * 180 / math.Pi,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,7 +528,7 @@ func roundOccultationBandJunctionRing(
|
||||
continue
|
||||
}
|
||||
previous, next := result[index-1], result[index+1]
|
||||
if occultationProjectedEdgeDistanceKM(previous, next) > maximumChordKM {
|
||||
if geodata.WebMercatorDistanceKM(previous, next) > maximumChordKM {
|
||||
continue
|
||||
}
|
||||
turn := occultationTurnAngleDegrees(previous, point, next)
|
||||
@@ -716,9 +711,9 @@ func removeOccultationPolarKinks(points []geodata.GeoPoint) []geodata.GeoPoint {
|
||||
for index := 1; index+1 < len(result); index++ {
|
||||
first, middle, last := result[index-1], result[index], result[index+1]
|
||||
if math.Abs(middle.Latitude) < minimumPolarLatitudeDegrees ||
|
||||
occultationProjectedEdgeDistanceKM(first, middle) > maximumAdjacentEdgeKM ||
|
||||
occultationProjectedEdgeDistanceKM(middle, last) > maximumAdjacentEdgeKM ||
|
||||
occultationProjectedEdgeDistanceKM(first, last) > maximumEndpointChordKM {
|
||||
geodata.WebMercatorDistanceKM(first, middle) > maximumAdjacentEdgeKM ||
|
||||
geodata.WebMercatorDistanceKM(middle, last) > maximumAdjacentEdgeKM ||
|
||||
geodata.WebMercatorDistanceKM(first, last) > maximumEndpointChordKM {
|
||||
continue
|
||||
}
|
||||
// A reversal in either projected axis is required. A merely curved
|
||||
@@ -896,20 +891,20 @@ func occultationCurveCoverageProbes(
|
||||
next = len(segment) - 1
|
||||
}
|
||||
first, second := segment[index], segment[next]
|
||||
deltaLongitude := normalizeGeoLongitude(second.Longitude - first.Longitude)
|
||||
deltaLongitude := geodata.NormalizeLongitude(second.Longitude - first.Longitude)
|
||||
deltaLatitude := second.Latitude - first.Latitude
|
||||
length := math.Hypot(deltaLongitude, deltaLatitude)
|
||||
if length <= 1e-12 {
|
||||
continue
|
||||
}
|
||||
midpoint := geodata.GeoPoint{
|
||||
Longitude: normalizeGeoLongitude(first.Longitude + deltaLongitude/2),
|
||||
Longitude: geodata.NormalizeLongitude(first.Longitude + deltaLongitude/2),
|
||||
Latitude: first.Latitude + deltaLatitude/2,
|
||||
}
|
||||
for _, offset := range []float64{0.00005, 0.0005, 0.002} {
|
||||
for _, side := range []float64{-1, 1} {
|
||||
candidates = append(candidates, geodata.GeoPoint{
|
||||
Longitude: normalizeGeoLongitude(midpoint.Longitude - side*deltaLatitude*offset/length),
|
||||
Longitude: geodata.NormalizeLongitude(midpoint.Longitude - side*deltaLatitude*offset/length),
|
||||
Latitude: midpoint.Latitude + side*deltaLongitude*offset/length,
|
||||
})
|
||||
}
|
||||
@@ -925,7 +920,7 @@ func occultationCurveCoverageProbes(
|
||||
}
|
||||
duplicate := false
|
||||
for _, existing := range probes {
|
||||
if len(existing) > 0 && geoDistanceKM(existing[0], probe) < 1 {
|
||||
if len(existing) > 0 && geodata.EquatorialDistanceKM(existing[0], probe) < 1 {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ func occultationPhaseBoundaryCyclesForEdges(
|
||||
validEdges := make([]bool, len(edges))
|
||||
nodeFor := func(point geodata.GeoPoint) int {
|
||||
for index := range nodes {
|
||||
if geoDistanceKM(nodes[index].point, point) <= curveBoundaryJoinDistanceKM {
|
||||
if geodata.EquatorialDistanceKM(nodes[index].point, point) <= curveBoundaryJoinDistanceKM {
|
||||
return index
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func occultationPhaseBoundaryCyclesForEdges(
|
||||
reverseGeoPointRing(next)
|
||||
}
|
||||
nextRing := append([]geodata.GeoPoint(nil), ring...)
|
||||
if len(nextRing) > 0 && geoDistanceKM(nextRing[len(nextRing)-1], next[0]) <= curveBoundaryJoinDistanceKM {
|
||||
if len(nextRing) > 0 && geodata.EquatorialDistanceKM(nextRing[len(nextRing)-1], next[0]) <= curveBoundaryJoinDistanceKM {
|
||||
next[0] = nextRing[len(nextRing)-1]
|
||||
}
|
||||
nextRing = append(nextRing, next[1:]...)
|
||||
@@ -500,7 +500,7 @@ func curveBoundaryEndpointsMatch(
|
||||
}
|
||||
|
||||
func curveBoundaryEndpointMatch(first geodata.GeoPoint, firstTime time.Time, second geodata.GeoPoint, secondTime time.Time) bool {
|
||||
return geoDistanceKM(first, second) <= curveBoundaryJoinDistanceKM &&
|
||||
return geodata.EquatorialDistanceKM(first, second) <= curveBoundaryJoinDistanceKM &&
|
||||
!firstTime.IsZero() && !secondTime.IsZero() && absDuration(firstTime.Sub(secondTime)) <= curveBoundaryJoinTime
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@ func densifyOccultationPolygons(
|
||||
continue
|
||||
}
|
||||
next := polygon[index+1]
|
||||
distance := occultationProjectedEdgeDistanceKM(point, next)
|
||||
distance := geodata.WebMercatorDistanceKM(point, next)
|
||||
steps := int(math.Ceil(distance / maximumEdgeKM))
|
||||
if steps < 2 {
|
||||
continue
|
||||
@@ -610,7 +610,7 @@ func densifyOccultationPolygons(
|
||||
// fallback can still render one long closing chord.
|
||||
if len(polygon) > 2 && !geodata.SameGeoPoint(polygon[0], polygon[len(polygon)-1]) {
|
||||
first, last := polygon[0], polygon[len(polygon)-1]
|
||||
distance := occultationProjectedEdgeDistanceKM(last, first)
|
||||
distance := geodata.WebMercatorDistanceKM(last, first)
|
||||
steps := int(math.Ceil(distance / maximumEdgeKM))
|
||||
if steps >= 2 {
|
||||
for step := 1; step < steps; step++ {
|
||||
@@ -623,24 +623,6 @@ func densifyOccultationPolygons(
|
||||
return result
|
||||
}
|
||||
|
||||
// occultationProjectedEdgeDistanceKM measures an edge in the Web Mercator
|
||||
// chart used by the GeoJSON/OpenLayers consumer, while retaining the shortest
|
||||
// wrapped longitude. At high latitude a small spherical edge expands strongly
|
||||
// in this chart; using only the great-circle distance leaves visible polygon
|
||||
// chords even though the source contact contour is densely sampled.
|
||||
func occultationProjectedEdgeDistanceKM(first, second geodata.GeoPoint) float64 {
|
||||
const maxLatitude = 85.05112878
|
||||
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 EarthRadiusKM * math.Hypot(longitude, secondY-firstY)
|
||||
}
|
||||
|
||||
func interpolateOccultationGeoPoint(
|
||||
first, second geodata.GeoPoint,
|
||||
fraction float64,
|
||||
@@ -747,7 +729,7 @@ func interpolateOccultationHorizonPoint(
|
||||
}
|
||||
point := first
|
||||
point.Time = first.Time.Add(time.Duration(float64(second.Time.Sub(first.Time)) * fraction))
|
||||
point.Longitude = normalizeGeoLongitude(first.Longitude + fraction*deltaLongitude)
|
||||
point.Longitude = geodata.NormalizeLongitude(first.Longitude + fraction*deltaLongitude)
|
||||
point.Latitude = first.Latitude + fraction*(second.Latitude-first.Latitude)
|
||||
point.MoonAltitude = 0
|
||||
point.WidthKM = first.WidthKM + fraction*(second.WidthKM-first.WidthKM)
|
||||
@@ -837,13 +819,13 @@ func occultationVisibleFootprintProbes(
|
||||
continue
|
||||
}
|
||||
midpoint := geodata.GeoPoint{
|
||||
Longitude: normalizeGeoLongitude(first.Longitude + deltaLongitude/2),
|
||||
Longitude: geodata.NormalizeLongitude(first.Longitude + deltaLongitude/2),
|
||||
Latitude: first.Latitude + deltaLatitude/2,
|
||||
}
|
||||
for _, offset := range []float64{0.0002, 0.002, 0.01, 0.03} {
|
||||
for _, side := range []float64{-1, 1} {
|
||||
edgeCandidates = append(edgeCandidates, geodata.GeoPoint{
|
||||
Longitude: normalizeGeoLongitude(midpoint.Longitude - side*deltaLatitude*offset/length),
|
||||
Longitude: geodata.NormalizeLongitude(midpoint.Longitude - side*deltaLatitude*offset/length),
|
||||
Latitude: midpoint.Latitude + side*deltaLongitude*offset/length,
|
||||
})
|
||||
}
|
||||
@@ -922,15 +904,7 @@ func limitOccultationCoveragePaths(
|
||||
|
||||
func sameOccultationPoint(first, second basic.OccultationPathPoint) bool {
|
||||
return math.Abs(first.Latitude-second.Latitude) <= 1e-10 &&
|
||||
math.Abs(normalizeGeoLongitude(first.Longitude-second.Longitude)) <= 1e-10
|
||||
}
|
||||
|
||||
func normalizeGeoLongitude(value float64) float64 {
|
||||
value = math.Mod(value+180, 360)
|
||||
if value < 0 {
|
||||
value += 360
|
||||
}
|
||||
return value - 180
|
||||
math.Abs(geodata.NormalizeLongitude(first.Longitude-second.Longitude)) <= 1e-10
|
||||
}
|
||||
|
||||
func occultationPolarFootprintProbes(
|
||||
@@ -1007,11 +981,11 @@ func removeOccultationHairpins(points []geodata.GeoPoint, maximumClosureKM, mini
|
||||
arcLength := 0.0
|
||||
best := -1
|
||||
for end := start + 1; end <= limit; end++ {
|
||||
arcLength += geoDistanceKM(result[end-1], result[end])
|
||||
arcLength += geodata.EquatorialDistanceKM(result[end-1], result[end])
|
||||
if end < start+3 {
|
||||
continue
|
||||
}
|
||||
closure := geoDistanceKM(result[start], result[end])
|
||||
closure := geodata.EquatorialDistanceKM(result[start], result[end])
|
||||
if closure <= maximumClosureKM && arcLength-closure >= minimumDetourKM {
|
||||
best = end
|
||||
}
|
||||
@@ -1053,11 +1027,11 @@ func smoothOccultationHairpins(
|
||||
best := -1
|
||||
bestDetour := minimumDetourKM
|
||||
for end := start + 1; end <= limit; end++ {
|
||||
arcLength += geoDistanceKM(result[end-1], result[end])
|
||||
arcLength += geodata.EquatorialDistanceKM(result[end-1], result[end])
|
||||
if end < start+3 {
|
||||
continue
|
||||
}
|
||||
closure := geoDistanceKM(result[start], result[end])
|
||||
closure := geodata.EquatorialDistanceKM(result[start], result[end])
|
||||
detour := arcLength - closure
|
||||
if closure <= maximumClosureKM && detour >= bestDetour {
|
||||
best = end
|
||||
@@ -1068,7 +1042,7 @@ func smoothOccultationHairpins(
|
||||
continue
|
||||
}
|
||||
first, last := result[start], result[best]
|
||||
steps := int(math.Ceil(geoDistanceKM(first, last) / maximumEdgeKM))
|
||||
steps := int(math.Ceil(geodata.EquatorialDistanceKM(first, last) / maximumEdgeKM))
|
||||
if steps < 1 {
|
||||
steps = 1
|
||||
}
|
||||
@@ -1108,7 +1082,7 @@ func removeOccultationSharpCorners(
|
||||
changed := false
|
||||
for index := 1; index+1 < len(result); index++ {
|
||||
first, middle, last := result[index-1], result[index], result[index+1]
|
||||
if geoDistanceKM(first, last) > maximumChordKM {
|
||||
if geodata.EquatorialDistanceKM(first, last) > maximumChordKM {
|
||||
continue
|
||||
}
|
||||
if occultationTurnAngleDegrees(first, middle, last) >= minimumAngleDegrees {
|
||||
@@ -1142,7 +1116,7 @@ func removeOccultationPolarSharpCorners(
|
||||
for index := 1; index+1 < len(result); index++ {
|
||||
first, middle, last := result[index-1], result[index], result[index+1]
|
||||
if math.Abs(middle.Latitude) < minimumLatitudeDegrees ||
|
||||
geoDistanceKM(first, last) > maximumChordKM ||
|
||||
geodata.EquatorialDistanceKM(first, last) > maximumChordKM ||
|
||||
occultationTurnAngleDegrees(first, middle, last) >= minimumAngleDegrees {
|
||||
continue
|
||||
}
|
||||
@@ -1170,7 +1144,7 @@ func smoothOccultationPolarCorners(points []geodata.GeoPoint) []geodata.GeoPoint
|
||||
for index := 1; index+1 < len(result); index++ {
|
||||
first, middle, last := result[index-1], result[index], result[index+1]
|
||||
if math.Abs(middle.Latitude) < 70 ||
|
||||
occultationProjectedEdgeDistanceKM(first, last) > 140 ||
|
||||
geodata.WebMercatorDistanceKM(first, last) > 140 ||
|
||||
occultationTurnAngleDegrees(first, middle, last) >= 165 {
|
||||
continue
|
||||
}
|
||||
@@ -1180,7 +1154,7 @@ func smoothOccultationPolarCorners(points []geodata.GeoPoint) []geodata.GeoPoint
|
||||
if !longitudeReversal && !latitudeReversal {
|
||||
continue
|
||||
}
|
||||
steps := int(math.Ceil(occultationProjectedEdgeDistanceKM(first, last) / 40))
|
||||
steps := int(math.Ceil(geodata.WebMercatorDistanceKM(first, last) / 40))
|
||||
if steps < 2 {
|
||||
steps = 2
|
||||
}
|
||||
@@ -1233,7 +1207,7 @@ func smoothOccultationPolarWobbles(points []geodata.GeoPoint) []geodata.GeoPoint
|
||||
first, last := result[start], result[end]
|
||||
if math.Abs(first.Latitude) < minimumPolarLatitudeDegrees ||
|
||||
math.Abs(last.Latitude) < minimumPolarLatitudeDegrees ||
|
||||
occultationProjectedEdgeDistanceKM(first, last) > maximumEndpointSpanKM {
|
||||
geodata.WebMercatorDistanceKM(first, last) > maximumEndpointSpanKM {
|
||||
continue
|
||||
}
|
||||
longitudeDirection := 0.0
|
||||
@@ -1271,7 +1245,7 @@ func smoothOccultationPolarWobbles(points []geodata.GeoPoint) []geodata.GeoPoint
|
||||
for index := start + 1; index < end; index++ {
|
||||
fraction := float64(index-start) / float64(windowPoints)
|
||||
baseline := interpolateOccultationGeoPoint(first, last, fraction)
|
||||
if geoDistanceKM(result[index], baseline) > maximumDeviationKM {
|
||||
if geodata.EquatorialDistanceKM(result[index], baseline) > maximumDeviationKM {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
// 掩星边界连续性判定的几何门限 / geometric thresholds for occultation-boundary continuity checks.
|
||||
const (
|
||||
EarthRadiusKM = 6378.1366
|
||||
BoundaryBranchJumpKM = 750.0
|
||||
// BoundaryBranchSpeedKMPerSecond 是判定"物理上不可能的支路跳变"的地面速度门限:
|
||||
// 掩星边界随月球影子移动,地面速度上限约 1.1 km/s,取 2 km/s 留约两倍余量。
|
||||
@@ -287,7 +286,7 @@ func nearestPolygonVertex(
|
||||
distance := math.Inf(1)
|
||||
for _, polygon := range polygons {
|
||||
for _, candidate := range polygon {
|
||||
value := geoDistanceKM(point, candidate)
|
||||
value := geodata.EquatorialDistanceKM(point, candidate)
|
||||
if value < distance {
|
||||
nearest, distance = candidate, value
|
||||
}
|
||||
@@ -327,7 +326,7 @@ func nearestPolygonBoundaryPoint(
|
||||
))
|
||||
}
|
||||
candidate := interpolateOccultationGeoPoint(start, end, fraction)
|
||||
value := geoDistanceKM(point, candidate)
|
||||
value := geodata.EquatorialDistanceKM(point, candidate)
|
||||
if value < distance {
|
||||
nearest, distance = candidate, value
|
||||
}
|
||||
@@ -742,12 +741,21 @@ func visibleBandPolygonsFromAnalyticContours(
|
||||
}
|
||||
return footprintFill
|
||||
}
|
||||
var selectionFill [][]geodata.GeoPoint
|
||||
var selectionFill, footprintSelection [][]geodata.GeoPoint
|
||||
if pointSource || len(visibilityContours) == 0 {
|
||||
// Geocentric limit strips can extend beyond the station-corrected
|
||||
// stellar envelope or omit a finite-disk horizon extremum. Use visible
|
||||
// footprints as witnesses; the analytic linework supplies every edge.
|
||||
selectionFill = getFootprintFill()
|
||||
if pointSource && len(visibilityContours) == 0 {
|
||||
// 密集恒星时间轴含大量重叠环;连续界限用于选面,逐环仍用于覆盖见证;界限条带选面失败时退回逐环。
|
||||
footprintSelection = selectionFill
|
||||
if limit := occultationLimitVisibleFillPolygons(northern, southern); len(limit) > 0 {
|
||||
selectionFill = limit
|
||||
} else if union := footprintVisibleUnionPolygons(footprints); len(union) > 0 {
|
||||
selectionFill = union
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(selectionFill) == 0 {
|
||||
selectionFill = occultationLimitVisibleFillPolygons(northern, southern)
|
||||
@@ -779,13 +787,21 @@ func visibleBandPolygonsFromAnalyticContours(
|
||||
if pointSource && len(visibilityContours) == 0 {
|
||||
initialCoverage = phaseLines
|
||||
}
|
||||
polygons, err := polygonize(initialCoverage)
|
||||
if err != nil && len(phaseLines) > 0 {
|
||||
// A multi-branch rise/set network can contain several closed faces with
|
||||
// identical physical junctions. If fill-only selection chooses the
|
||||
// adjacent face, require every exported phase line as a coverage witness
|
||||
// and retry without changing the boundary network or snap tolerance.
|
||||
polygons, err = polygonize(phaseLines)
|
||||
selectFaces := func() ([][]geodata.GeoPoint, error) {
|
||||
polygons, err := polygonize(initialCoverage)
|
||||
if err != nil && len(phaseLines) > 0 {
|
||||
// A multi-branch rise/set network can contain several closed faces with
|
||||
// identical physical junctions. If fill-only selection chooses the
|
||||
// adjacent face, require every exported phase line as a coverage witness
|
||||
// and retry without changing the boundary network or snap tolerance.
|
||||
polygons, err = polygonize(phaseLines)
|
||||
}
|
||||
return polygons, err
|
||||
}
|
||||
polygons, err := selectFaces()
|
||||
if err != nil && len(footprintSelection) > 0 {
|
||||
selectionFill = footprintSelection
|
||||
polygons, err = selectFaces()
|
||||
}
|
||||
if err != nil {
|
||||
if fallback, ok := fallbackVisible(); ok {
|
||||
|
||||
@@ -238,13 +238,13 @@ func TestSmoothOccultationHairpinsKeepsShortDisplayEdges(t *testing.T) {
|
||||
maximumEdge := 0.0
|
||||
maximumDetour := 0.0
|
||||
for index := 1; index < len(smoothed); index++ {
|
||||
maximumEdge = math.Max(maximumEdge, geoDistanceKM(smoothed[index-1], smoothed[index]))
|
||||
maximumEdge = math.Max(maximumEdge, geodata.EquatorialDistanceKM(smoothed[index-1], smoothed[index]))
|
||||
}
|
||||
for index := 1; index+1 < len(smoothed); index++ {
|
||||
maximumDetour = math.Max(maximumDetour,
|
||||
geoDistanceKM(smoothed[index-1], smoothed[index])+
|
||||
geoDistanceKM(smoothed[index], smoothed[index+1])-
|
||||
geoDistanceKM(smoothed[index-1], smoothed[index+1]))
|
||||
geodata.EquatorialDistanceKM(smoothed[index-1], smoothed[index])+
|
||||
geodata.EquatorialDistanceKM(smoothed[index], smoothed[index+1])-
|
||||
geodata.EquatorialDistanceKM(smoothed[index-1], smoothed[index+1]))
|
||||
}
|
||||
if maximumEdge > 50.01 {
|
||||
t.Fatalf("smoothed maximum edge=%.1f km, want <=50 km", maximumEdge)
|
||||
@@ -300,10 +300,10 @@ func TestSmoothOccultationOrdinaryWobblesHandlesHighLatitudeArc(t *testing.T) {
|
||||
}
|
||||
changed := false
|
||||
for index := 1; index+1 < len(points); index++ {
|
||||
if geoDistanceKM(points[index], smoothed[index]) > 0.001 {
|
||||
if geodata.EquatorialDistanceKM(points[index], smoothed[index]) > 0.001 {
|
||||
changed = true
|
||||
if geoDistanceKM(points[index], smoothed[index]) > 20 {
|
||||
t.Fatalf("high-latitude smoothing moved point %d by %.2f km", index, geoDistanceKM(points[index], smoothed[index]))
|
||||
if geodata.EquatorialDistanceKM(points[index], smoothed[index]) > 20 {
|
||||
t.Fatalf("high-latitude smoothing moved point %d by %.2f km", index, geodata.EquatorialDistanceKM(points[index], smoothed[index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func TestDensifyOccultationPolygonsIncludesClosingEdge(t *testing.T) {
|
||||
}
|
||||
densified := densifyOccultationPolygons([][]geodata.GeoPoint{points}, 50)
|
||||
ring := densified[0]
|
||||
maximumEdge := geoDistanceKM(ring[len(ring)-1], ring[0])
|
||||
maximumEdge := geodata.EquatorialDistanceKM(ring[len(ring)-1], ring[0])
|
||||
if maximumEdge > 50.01 {
|
||||
t.Fatalf("closing edge=%.1f km, want <=50 km", maximumEdge)
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func TestVisibleBandPolygons20250105SaturnEnvelopeCandidates(t *testing.T) {
|
||||
}
|
||||
for _, polygon := range final {
|
||||
for index := 1; index < len(polygon); index++ {
|
||||
if distance := geoDistanceKM(polygon[index-1], polygon[index]); distance > BoundaryBranchJumpKM {
|
||||
if distance := geodata.EquatorialDistanceKM(polygon[index-1], polygon[index]); distance > BoundaryBranchJumpKM {
|
||||
t.Fatalf("band ring spans a %.1f km edge", distance)
|
||||
}
|
||||
}
|
||||
@@ -519,7 +519,7 @@ func TestSmoothOccultationOrdinaryWobblesPreservesEndpointsAndBoundedMotion(t *t
|
||||
t.Fatalf("ordinary smoothing changed open endpoints: %#v", cleaned)
|
||||
}
|
||||
for index, point := range cleaned {
|
||||
if move := geoDistanceKM(point, ring[index]); move > 20.001 {
|
||||
if move := geodata.EquatorialDistanceKM(point, ring[index]); move > 20.001 {
|
||||
t.Fatalf("point %d moved %.2f km, want <=20 km", index, move)
|
||||
}
|
||||
}
|
||||
@@ -585,7 +585,7 @@ func occultationTestWindowTouchesHorizon(
|
||||
}
|
||||
for offset := -2; offset <= 2; offset++ {
|
||||
for _, horizon := range horizons {
|
||||
if geoDistanceKM(polygon[index+offset], horizon) <= windowKM {
|
||||
if geodata.EquatorialDistanceKM(polygon[index+offset], horizon) <= windowKM {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -716,7 +716,7 @@ func TestVisibleBandPolygons20250729MarsStitchesEndRiseFold(t *testing.T) {
|
||||
minimumTurn := 180.0
|
||||
for index := 1; index+1 < len(polygon); index++ {
|
||||
first, middle, last := polygon[index-1], polygon[index], polygon[index+1]
|
||||
detour := geoDistanceKM(first, middle) + geoDistanceKM(middle, last) - geoDistanceKM(first, last)
|
||||
detour := geodata.EquatorialDistanceKM(first, middle) + geodata.EquatorialDistanceKM(middle, last) - geodata.EquatorialDistanceKM(first, last)
|
||||
if detour > maximumDetour {
|
||||
maximumDetour = detour
|
||||
}
|
||||
@@ -1027,7 +1027,7 @@ func TestOccultationPhaseBoundaryCycleExcludesGreatestInteriorLine(t *testing.T)
|
||||
minimum := math.Inf(1)
|
||||
for _, line := range phaseLines {
|
||||
for _, candidate := range line {
|
||||
minimum = math.Min(minimum, geoDistanceKM(point, candidate))
|
||||
minimum = math.Min(minimum, geodata.EquatorialDistanceKM(point, candidate))
|
||||
}
|
||||
}
|
||||
if minimum > 1 {
|
||||
@@ -1135,7 +1135,7 @@ func TestVisibleBandPolygons20250729MarsTotalRiseSetRemovesPolarBacktrack(t *tes
|
||||
limit = len(polygon) - 1
|
||||
}
|
||||
for endIndex := startIndex + 1; endIndex <= limit; endIndex++ {
|
||||
edge := geoDistanceKM(polygon[endIndex-1], polygon[endIndex])
|
||||
edge := geodata.EquatorialDistanceKM(polygon[endIndex-1], polygon[endIndex])
|
||||
arcLength += edge
|
||||
if edge > maximumEdge {
|
||||
maximumEdge = edge
|
||||
@@ -1143,7 +1143,7 @@ func TestVisibleBandPolygons20250729MarsTotalRiseSetRemovesPolarBacktrack(t *tes
|
||||
if endIndex < startIndex+3 {
|
||||
continue
|
||||
}
|
||||
closure := geoDistanceKM(polygon[startIndex], polygon[endIndex])
|
||||
closure := geodata.EquatorialDistanceKM(polygon[startIndex], polygon[endIndex])
|
||||
if closure <= 180 && arcLength-closure >= 35 {
|
||||
t.Fatalf("total polygon %d retains a large polar backtrack: start=%d end=%d closure=%.1f km detour=%.1f km",
|
||||
polygonIndex, startIndex, endIndex, closure, arcLength-closure)
|
||||
|
||||
@@ -91,15 +91,8 @@ func densifyRiseSetSegment(
|
||||
}
|
||||
|
||||
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)
|
||||
return geodata.WebMercatorDistanceKM(
|
||||
geodata.GeoPoint{Longitude: first.Longitude, Latitude: first.Latitude},
|
||||
geodata.GeoPoint{Longitude: second.Longitude, Latitude: second.Latitude},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestSamePhaseHorizonConnectorAllowedMatchesReferenceRule(t *testing.T) {
|
||||
}
|
||||
last := horizonConnectorEndpoint{
|
||||
point: basic.OccultationPathPoint{
|
||||
Longitude: distanceKM / (EarthRadiusKM * math.Pi / 180),
|
||||
Longitude: distanceKM / (geodata.EarthEquatorialRadiusKM * math.Pi / 180),
|
||||
},
|
||||
curveIndex: 3, segmentIndex: 1, atStart: true,
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ func DirectVisibleBandPolygons(
|
||||
}
|
||||
if len(samples) > 0 {
|
||||
previous := samples[len(samples)-1]
|
||||
direct := geoDistanceKM(previous.a, boundary[0]) + geoDistanceKM(previous.b, boundary[len(boundary)-1])
|
||||
swapped := geoDistanceKM(previous.a, boundary[len(boundary)-1]) + geoDistanceKM(previous.b, boundary[0])
|
||||
direct := geodata.EquatorialDistanceKM(previous.a, boundary[0]) + geodata.EquatorialDistanceKM(previous.b, boundary[len(boundary)-1])
|
||||
swapped := geodata.EquatorialDistanceKM(previous.a, boundary[len(boundary)-1]) + geodata.EquatorialDistanceKM(previous.b, boundary[0])
|
||||
if swapped < direct {
|
||||
reverseGeoPoints(boundary)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func removeDirectProjectedSharpCorners(
|
||||
changed := false
|
||||
for index := 1; index+1 < len(result); index++ {
|
||||
first, middle, last := result[index-1], result[index], result[index+1]
|
||||
if geoDistanceKM(first, last) > maximumChordKM {
|
||||
if geodata.EquatorialDistanceKM(first, last) > maximumChordKM {
|
||||
continue
|
||||
}
|
||||
scale := math.Cos(middle.Latitude * math.Pi / 180)
|
||||
@@ -281,7 +281,7 @@ func reverseGeoPoints(points []geodata.GeoPoint) {
|
||||
}
|
||||
|
||||
func appendDirectRingPoint(ring *[]geodata.GeoPoint, point geodata.GeoPoint) {
|
||||
if len(*ring) > 0 && geoDistanceKM((*ring)[len(*ring)-1], point) <= 1e-6 {
|
||||
if len(*ring) > 0 && geodata.EquatorialDistanceKM((*ring)[len(*ring)-1], point) <= 1e-6 {
|
||||
return
|
||||
}
|
||||
*ring = append(*ring, point)
|
||||
@@ -299,7 +299,7 @@ func densifyDirectRing(ring []geodata.GeoPoint, maximumEdgeKM float64) []geodata
|
||||
}
|
||||
next := ring[index+1]
|
||||
steps := int(math.Ceil(math.Max(
|
||||
geoDistanceKM(point, next), occultationProjectedEdgeDistanceKM(point, next),
|
||||
geodata.EquatorialDistanceKM(point, next), geodata.WebMercatorDistanceKM(point, next),
|
||||
) / maximumEdgeKM))
|
||||
if steps < 2 {
|
||||
continue
|
||||
|
||||
@@ -128,7 +128,7 @@ func closedFootprintRingAlignmentScore(
|
||||
score := 0.0
|
||||
for sample := 0; sample < samples; sample++ {
|
||||
index := sample * len(previous) / samples
|
||||
score += geoDistanceKM(previous[index], current[(index+shift)%len(current)])
|
||||
score += geodata.EquatorialDistanceKM(previous[index], current[(index+shift)%len(current)])
|
||||
}
|
||||
return score
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func footprintClosedSweepMaximumStep(
|
||||
}
|
||||
maximum := 0.0
|
||||
for index := 0; index < count; index++ {
|
||||
maximum = math.Max(maximum, geoDistanceKM(first[index], second[index]))
|
||||
maximum = math.Max(maximum, geodata.EquatorialDistanceKM(first[index], second[index]))
|
||||
}
|
||||
return maximum
|
||||
}
|
||||
@@ -337,7 +337,7 @@ func occultationUnitBoundsNear(first, second occultationUnitBounds, limitKM floa
|
||||
dy := math.Max(0, math.Max(first.minY-second.maxY, second.minY-first.maxY))
|
||||
dz := math.Max(0, math.Max(first.minZ-second.maxZ, second.minZ-first.maxZ))
|
||||
// 大圆距离不小于弦长,因此弦距下界可以作为 60 km 近邻判定的必要条件。
|
||||
return EarthRadiusKM*math.Sqrt(dx*dx+dy*dy+dz*dz) <= limitKM
|
||||
return geodata.EarthEquatorialRadiusKM*math.Sqrt(dx*dx+dy*dy+dz*dz) <= limitKM
|
||||
}
|
||||
|
||||
func mergeTouchingVisiblePolygons(polygons [][]geodata.GeoPoint) [][]geodata.GeoPoint {
|
||||
@@ -367,7 +367,7 @@ func mergeTouchingVisiblePolygons(polygons [][]geodata.GeoPoint) [][]geodata.Geo
|
||||
}
|
||||
for _, left := range polygons[first] {
|
||||
for _, right := range polygons[second] {
|
||||
if geoDistanceKM(left, right) > touchingDistanceKM {
|
||||
if geodata.EquatorialDistanceKM(left, right) > touchingDistanceKM {
|
||||
continue
|
||||
}
|
||||
firstIndex, secondIndex = first, second
|
||||
@@ -388,7 +388,7 @@ func mergeTouchingVisiblePolygons(polygons [][]geodata.GeoPoint) [][]geodata.Geo
|
||||
break
|
||||
}
|
||||
bridgeHalfDeg := minimumBridgeHalfDeg
|
||||
if gap := geoDistanceKM(leftTouch, rightTouch) / EarthRadiusKM * 180 / math.Pi; gap/2+0.002 > bridgeHalfDeg {
|
||||
if gap := geodata.EquatorialDistanceKM(leftTouch, rightTouch) / geodata.EarthEquatorialRadiusKM * 180 / math.Pi; gap/2+0.002 > bridgeHalfDeg {
|
||||
bridgeHalfDeg = gap/2 + 0.002
|
||||
}
|
||||
bridge := []geodata.GeoPoint{
|
||||
@@ -629,7 +629,7 @@ func nearestFootprintPointIndex(points []geodata.GeoPoint, target geodata.GeoPoi
|
||||
nearest := 0
|
||||
distance := math.Inf(1)
|
||||
for index, point := range points {
|
||||
candidate := geoDistanceKM(point, target)
|
||||
candidate := geodata.EquatorialDistanceKM(point, target)
|
||||
if candidate < distance {
|
||||
nearest, distance = index, candidate
|
||||
}
|
||||
@@ -672,7 +672,7 @@ func footprintArcMatchScore(candidate, reference []geodata.GeoPoint) float64 {
|
||||
for index := 0; index < samples; index++ {
|
||||
candidateIndex := index * (len(candidate) - 1) / (samples - 1)
|
||||
referenceIndex := index * (len(reference) - 1) / (samples - 1)
|
||||
score += geoDistanceKM(candidate[candidateIndex], reference[referenceIndex])
|
||||
score += geodata.EquatorialDistanceKM(candidate[candidateIndex], reference[referenceIndex])
|
||||
}
|
||||
return score
|
||||
}
|
||||
@@ -721,7 +721,7 @@ func footprintStaticInteriorPolygons(footprints []basic.OccultationFootprint) []
|
||||
}
|
||||
centerCap := geodata.SphericalCircle(
|
||||
center,
|
||||
staticCenterCapRadiusKM/EarthRadiusKM*180/math.Pi,
|
||||
staticCenterCapRadiusKM/geodata.EarthEquatorialRadiusKM*180/math.Pi,
|
||||
staticCenterCapPoints,
|
||||
)
|
||||
if len(centerCap) >= 3 {
|
||||
@@ -782,8 +782,8 @@ func footprintPolygons(footprints []basic.OccultationFootprint) [][]geodata.GeoP
|
||||
|
||||
func geoPointSegmentDistanceKM(point, start, end geodata.GeoPoint) float64 {
|
||||
latitude := point.Latitude * math.Pi / 180
|
||||
scaleX := math.Cos(latitude) * EarthRadiusKM * math.Pi / 180
|
||||
scaleY := EarthRadiusKM * math.Pi / 180
|
||||
scaleX := math.Cos(latitude) * geodata.EarthEquatorialRadiusKM * math.Pi / 180
|
||||
scaleY := geodata.EarthEquatorialRadiusKM * math.Pi / 180
|
||||
x := func(value geodata.GeoPoint) float64 {
|
||||
return math.Remainder(value.Longitude-point.Longitude, 360) * scaleX
|
||||
}
|
||||
@@ -855,18 +855,8 @@ func BoundaryBranchChanged(first, second basic.OccultationPathPoint) bool {
|
||||
// DistanceKM 返回两个边界样本之间的最短球面距离。
|
||||
// DistanceKM returns the shortest spherical surface distance between two boundary samples.
|
||||
func DistanceKM(first, second basic.OccultationPathPoint) float64 {
|
||||
return geoDistanceKM(
|
||||
return geodata.EquatorialDistanceKM(
|
||||
geodata.GeoPoint{Longitude: first.Longitude, Latitude: first.Latitude},
|
||||
geodata.GeoPoint{Longitude: second.Longitude, Latitude: second.Latitude},
|
||||
)
|
||||
}
|
||||
|
||||
func geoDistanceKM(first, second geodata.GeoPoint) float64 {
|
||||
firstLatitude := first.Latitude * math.Pi / 180
|
||||
secondLatitude := second.Latitude * math.Pi / 180
|
||||
deltaLatitude := secondLatitude - firstLatitude
|
||||
deltaLongitude := math.Remainder((second.Longitude-first.Longitude)*math.Pi/180, 2*math.Pi)
|
||||
haversine := math.Sin(deltaLatitude/2)*math.Sin(deltaLatitude/2) +
|
||||
math.Cos(firstLatitude)*math.Cos(secondLatitude)*math.Sin(deltaLongitude/2)*math.Sin(deltaLongitude/2)
|
||||
return 2 * EarthRadiusKM * math.Asin(math.Sqrt(math.Min(1, haversine)))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func mergeTouchingVisiblePolygonsReference(polygons [][]geodata.GeoPoint) [][]ge
|
||||
for second := first + 1; second < len(polygons) && firstIndex < 0; second++ {
|
||||
for _, left := range polygons[first] {
|
||||
for _, right := range polygons[second] {
|
||||
if geoDistanceKM(left, right) > touchingDistanceKM {
|
||||
if geodata.EquatorialDistanceKM(left, right) > touchingDistanceKM {
|
||||
continue
|
||||
}
|
||||
firstIndex, secondIndex = first, second
|
||||
@@ -43,7 +43,7 @@ func mergeTouchingVisiblePolygonsReference(polygons [][]geodata.GeoPoint) [][]ge
|
||||
break
|
||||
}
|
||||
bridgeHalfDeg := minimumBridgeHalfDeg
|
||||
if gap := geoDistanceKM(leftTouch, rightTouch) / EarthRadiusKM * 180 / math.Pi; gap/2+0.002 > bridgeHalfDeg {
|
||||
if gap := geodata.EquatorialDistanceKM(leftTouch, rightTouch) / geodata.EarthEquatorialRadiusKM * 180 / math.Pi; gap/2+0.002 > bridgeHalfDeg {
|
||||
bridgeHalfDeg = gap/2 + 0.002
|
||||
}
|
||||
bridge := []geodata.GeoPoint{
|
||||
|
||||
@@ -1274,7 +1274,7 @@ func occultationCoverageProbeExists(
|
||||
probe geodata.GeoPoint,
|
||||
) bool {
|
||||
for _, existing := range coveragePaths {
|
||||
if geoDistanceKM(existing[0], probe) < 10 {
|
||||
if geodata.EquatorialDistanceKM(existing[0], probe) < 10 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user