Files
astro/internal/occultationgeo/reference_test.go
T

325 lines
9.9 KiB
Go
Raw Normal View History

package occultationgeo
// 本文件保存改动前的参考实现,仅用于差分对照:新实现必须在相同随机输入上给出逐点相同的输出。
import (
"fmt"
"math"
"b612.me/astro/basic"
"b612.me/astro/internal/geodata"
)
func mergeTouchingVisiblePolygonsReference(polygons [][]geodata.GeoPoint) [][]geodata.GeoPoint {
const (
touchingDistanceKM = 60.0
minimumBridgeHalfDeg = 0.01
)
for len(polygons) > 1 {
firstIndex, secondIndex := -1, -1
var touching, leftTouch, rightTouch geodata.GeoPoint
for first := 0; first < len(polygons) && firstIndex < 0; first++ {
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 {
continue
}
firstIndex, secondIndex = first, second
leftTouch, rightTouch = left, right
touching = geodata.GeoPoint{
Longitude: left.Longitude + math.Remainder(right.Longitude-left.Longitude, 360)/2,
Latitude: (left.Latitude + right.Latitude) / 2,
}
break
}
if firstIndex >= 0 {
break
}
}
}
}
if firstIndex < 0 || secondIndex < 0 {
break
}
bridgeHalfDeg := minimumBridgeHalfDeg
if gap := geoDistanceKM(leftTouch, rightTouch) / EarthRadiusKM * 180 / math.Pi; gap/2+0.002 > bridgeHalfDeg {
bridgeHalfDeg = gap/2 + 0.002
}
bridge := []geodata.GeoPoint{
{Longitude: touching.Longitude - bridgeHalfDeg, Latitude: touching.Latitude - bridgeHalfDeg},
{Longitude: touching.Longitude + bridgeHalfDeg, Latitude: touching.Latitude - bridgeHalfDeg},
{Longitude: touching.Longitude + bridgeHalfDeg, Latitude: touching.Latitude + bridgeHalfDeg},
{Longitude: touching.Longitude - bridgeHalfDeg, Latitude: touching.Latitude + bridgeHalfDeg},
}
leftPolygon := append([]geodata.GeoPoint(nil), polygons[firstIndex]...)
rightPolygon := append([]geodata.GeoPoint(nil), polygons[secondIndex]...)
for index, point := range leftPolygon {
if point == leftTouch {
leftPolygon[index] = touching
break
}
}
for index, point := range rightPolygon {
if point == rightTouch {
rightPolygon[index] = touching
break
}
}
pair, err := geodata.UnionPolygons([][]geodata.GeoPoint{
leftPolygon, rightPolygon, bridge,
})
if err != nil || len(pair) != 1 {
pair, err = geodata.UnionPolygons([][]geodata.GeoPoint{
leftPolygon, rightPolygon,
})
}
if err != nil || len(pair) != 1 {
break
}
next := make([][]geodata.GeoPoint, 0, len(polygons)-1)
for index, polygon := range polygons {
if index == firstIndex {
next = append(next, pair[0])
continue
}
if index == secondIndex {
continue
}
next = append(next, polygon)
}
polygons = next
}
return polygons
}
func removeTinyPolygonComponentsReference(polygons [][]geodata.GeoPoint) [][]geodata.GeoPoint {
if len(polygons) < 2 {
return polygons
}
areas := make([]float64, len(polygons))
maximum := 0.0
for index, polygon := range polygons {
areas[index] = math.Abs(geoRingArea(polygon))
if areas[index] > maximum {
maximum = areas[index]
}
}
if maximum <= 0 || !finiteGeo(maximum) {
return polygons
}
threshold := maximum * 1e-4
filtered := make([][]geodata.GeoPoint, 0, len(polygons))
for index, polygon := range polygons {
if areas[index] >= threshold {
filtered = append(filtered, polygon)
}
}
if len(filtered) == 0 {
return polygons[:1]
}
return filtered
}
func pairedBoundaryPolygonsReference(
first, second []basic.OccultationPathPoint,
) [][]geodata.GeoPoint {
count := len(first)
if len(second) < count {
count = len(second)
}
polygons := make([][]geodata.GeoPoint, 0, count)
pendingBranch := false
for index := 1; index < count; index++ {
firstChanged := BoundaryBranchChanged(first[index-1], first[index])
secondChanged := BoundaryBranchChanged(second[index-1], second[index])
if pendingBranch {
if firstChanged || secondChanged {
pendingBranch = false
}
if firstChanged || secondChanged || pendingBranch {
continue
}
}
if firstChanged != secondChanged {
pendingBranch = true
continue
}
if firstChanged { // both sides changed at the same transition
continue
}
previousFirst, currentFirst := first[index-1], first[index]
previousSecond, currentSecond := second[index-1], second[index]
polygons = append(polygons, []geodata.GeoPoint{
{Longitude: previousFirst.Longitude, Latitude: previousFirst.Latitude},
{Longitude: currentFirst.Longitude, Latitude: currentFirst.Latitude},
{Longitude: currentSecond.Longitude, Latitude: currentSecond.Latitude},
{Longitude: previousSecond.Longitude, Latitude: previousSecond.Latitude},
})
}
return polygons
}
func constrainPolygonsWithinReference(
parent, child [][]geodata.GeoPoint,
) [][]geodata.GeoPoint {
if len(parent) == 0 || len(child) == 0 {
return child
}
initialMissDistance := geodata.SphericalPolygonsPathMissDistanceKM(parent, child, true)
if initialMissDistance <= 0 {
return child
}
const maximumRepairDistanceKM = 100.0
const maximumResidualMissDistanceKM = 10.0
if initialMissDistance > maximumRepairDistanceKM {
return child
}
result := make([][]geodata.GeoPoint, len(child))
for index, source := range child {
if len(source) < 4 {
if len(openFootprintRing(source)) >= 3 && math.Abs(geoRingArea(source)) > 1e-12 {
result[index] = append([]geodata.GeoPoint(nil), source...)
}
continue
}
ring := append([]geodata.GeoPoint(nil), source...)
closed := geodata.SameGeoPoint(ring[0], ring[len(ring)-1])
limit := len(ring)
if closed {
limit--
}
containment := geodata.SphericalPolygonsContainPoints(parent, ring[:limit])
for pointIndex := 0; pointIndex < limit; pointIndex++ {
point := ring[pointIndex]
if containment[pointIndex] {
continue
}
nearest, distance := nearestPolygonBoundaryPoint(parent, point)
if distance <= maximumRepairDistanceKM {
ring[pointIndex] = nearest
}
}
if closed {
ring[len(ring)-1] = ring[0]
}
result[index] = ring
}
result = usableOccultationPolygons(result)
if geodata.SphericalPolygonsPathMissDistanceKM(parent, result, true) <= 0 {
return result
}
// Vertex-only repair cannot see a child edge whose endpoints are both
// inside the parent while its great-circle midpoint crosses outside. Split
// the repaired ring at the same projected spacing used by output geometry,
// then apply the local vertex snap to those newly exposed edge probes.
densified := densifyOccultationPolygons(result, 10)
for pass := 0; pass < 6; pass++ {
changed := false
for index, source := range densified {
ring := append([]geodata.GeoPoint(nil), source...)
closed := len(ring) > 1 && geodata.SameGeoPoint(ring[0], ring[len(ring)-1])
limit := len(ring)
if closed {
limit--
}
containment := geodata.SphericalPolygonsContainPoints(parent, ring[:limit])
for pointIndex := 0; pointIndex < limit; pointIndex++ {
point := ring[pointIndex]
if containment[pointIndex] {
continue
}
nearest, distance := nearestPolygonBoundaryPoint(parent, point)
if distance <= maximumRepairDistanceKM {
ring[pointIndex] = nearest
changed = true
}
}
if closed {
ring[len(ring)-1] = ring[0]
}
for {
cleaned := removeDirectProjectedSharpCorners(ring, 20, 30)
cleaned = removeOccultationSharpCorners(cleaned, 20, 30)
if len(cleaned) == len(ring) {
break
}
ring = cleaned
}
if closed && len(ring) > 1 && !geodata.SameGeoPoint(ring[0], ring[len(ring)-1]) {
ring = append(ring, ring[0])
}
densified[index] = ring
}
densified = usableOccultationPolygons(densified)
if geodata.SphericalPolygonsPathMissDistanceKM(parent, densified, true) <= maximumResidualMissDistanceKM {
return densified
}
if !changed {
break
}
}
return child
}
func footprintOpenSweepPolygonsWithTransitionsReference(
footprints []basic.OccultationFootprint,
includeTransitions bool,
) ([][]geodata.GeoPoint, error) {
polygons := make([][]geodata.GeoPoint, 0, 2)
for start := 0; start < len(footprints); {
for start < len(footprints) && footprints[start].Closed {
start++
}
if start == len(footprints) {
break
}
end := start
for end < len(footprints) && !footprints[end].Closed {
end++
}
samples := make([]geodata.OpenBoundarySweepSample, 0, end-start+2)
if includeTransitions && start > 0 {
boundary, ok := footprintTransitionBoundary(footprints[start-1], footprints[start])
if ok {
samples = append(samples, geodata.OpenBoundarySweepSample{Boundaries: [][]geodata.GeoPoint{boundary}})
}
}
for index := start; index < end; index++ {
samples = append(samples, geodata.OpenBoundarySweepSample{
Boundaries: footprintGeoBoundaries(footprints[index]),
})
}
if includeTransitions && end < len(footprints) {
boundary, ok := footprintTransitionBoundary(footprints[end], footprints[end-1])
if ok {
samples = append(samples, geodata.OpenBoundarySweepSample{Boundaries: [][]geodata.GeoPoint{boundary}})
}
}
group, err := geodata.OpenBoundarySweep(samples)
if err != nil && includeTransitions {
// At an open/closed transition the exact transition arc can be
// numerically coincident with the first ribbon edge. Retry the same
// physical run without that synthetic endpoint; the sampled open arcs
// still provide both endpoint tracks and avoid a false diagonal cap.
bareSamples := make([]geodata.OpenBoundarySweepSample, 0, end-start)
for index := start; index < end; index++ {
bareSamples = append(bareSamples, geodata.OpenBoundarySweepSample{
Boundaries: footprintGeoBoundaries(footprints[index]),
})
}
group, err = geodata.OpenBoundarySweep(bareSamples)
}
if err != nil {
return nil, err
}
polygons = append(polygons, group...)
start = end
}
if len(polygons) == 0 {
return nil, fmt.Errorf("open footprint samples contain no usable sweep")
}
return polygons, nil
}