117 lines
3.7 KiB
Go
117 lines
3.7 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestPlanetOccultationSaturnFootprintsContainCenterLine(t *testing.T) {
|
||
|
|
config, ok := planetOccultationConfigFor(OccultationSaturn)
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("Saturn occultation config is unavailable")
|
||
|
|
}
|
||
|
|
seedTT := occultationTimeToTT(time.Date(2025, time.February, 1, 4, 0, 48, 0, time.UTC))
|
||
|
|
for _, contact := range []struct {
|
||
|
|
name string
|
||
|
|
frameAt occultationPathFrameFunc
|
||
|
|
}{
|
||
|
|
{name: "partial", frameAt: func(tt float64) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationPathFrameAt(tt, config)
|
||
|
|
}},
|
||
|
|
{name: "total", frameAt: func(tt float64) (occultationPathFrame, bool) {
|
||
|
|
return planetOccultationTotalPathFrameAt(tt, config)
|
||
|
|
}},
|
||
|
|
} {
|
||
|
|
startTT, endTT, found := occultationPathWindowForFrame(
|
||
|
|
seedTT, seedTT-occultationPathSearchSpanDays, seedTT+occultationPathSearchSpanDays,
|
||
|
|
contact.frameAt, true,
|
||
|
|
)
|
||
|
|
if !found {
|
||
|
|
t.Fatalf("%s center interval is unavailable", contact.name)
|
||
|
|
}
|
||
|
|
checked := 0
|
||
|
|
for tt := startTT + 2.0/1440.0; tt < endTT-2.0/1440.0; tt += 5.0 / 1440.0 {
|
||
|
|
frame, frameOK := contact.frameAt(tt)
|
||
|
|
center, _, centerOK := occultationEarthLineIntersection(frame.moon, frame.axis)
|
||
|
|
footprint, footprintOK := planetOccultationFootprintAt(tt, contact.frameAt, time.UTC)
|
||
|
|
if !frameOK || !centerOK || !footprintOK {
|
||
|
|
t.Fatalf("%s center or footprint is unavailable at TT %.9f", contact.name, tt)
|
||
|
|
}
|
||
|
|
longitude, latitude := occultationPathGeodetic(tt, center)
|
||
|
|
if !planetOccultationFootprintContains(footprint, longitude, latitude) {
|
||
|
|
t.Fatalf("%s footprint does not contain center %.6f, %.6f at TT %.9f",
|
||
|
|
contact.name, longitude, latitude, tt)
|
||
|
|
}
|
||
|
|
checked++
|
||
|
|
}
|
||
|
|
if checked < 10 {
|
||
|
|
t.Fatalf("%s checked only %d center samples", contact.name, checked)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanetOccultationPathRejectsExcessiveAggregateSampling(t *testing.T) {
|
||
|
|
start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC)
|
||
|
|
_, err := FindPlanetOccultationPaths(
|
||
|
|
start, start.Add(24*time.Hour), OccultationSaturn,
|
||
|
|
OccultationPathOptions{Step: time.Second},
|
||
|
|
)
|
||
|
|
if !errors.Is(err, ErrOccultationPathSamplingLimit) {
|
||
|
|
t.Fatalf("FindPlanetOccultationPaths() error = %v, want ErrOccultationPathSamplingLimit", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanetOccultationFootprintsHaveIndependentSampleBudget(t *testing.T) {
|
||
|
|
times := occultationPathSampleTimesWithLimit(
|
||
|
|
0, 1, 0.500001, 1.0/86400.0, planetOccultationFootprintMaxSamples,
|
||
|
|
)
|
||
|
|
if len(times) > planetOccultationFootprintMaxSamples {
|
||
|
|
t.Fatalf("footprint sample count = %d, maximum %d", len(times), planetOccultationFootprintMaxSamples)
|
||
|
|
}
|
||
|
|
foundGreatest := false
|
||
|
|
for _, sample := range times {
|
||
|
|
if sample == 0.500001 {
|
||
|
|
foundGreatest = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !foundGreatest {
|
||
|
|
t.Fatal("bounded footprint samples omitted greatest")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func planetOccultationFootprintContains(
|
||
|
|
footprint PlanetOccultationFootprint,
|
||
|
|
longitude, latitude float64,
|
||
|
|
) bool {
|
||
|
|
for _, polygon := range footprint.Polygons {
|
||
|
|
inside := false
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
currentX := math.Remainder(polygon[current].Longitude-longitude, 360)
|
||
|
|
previousX := math.Remainder(polygon[previous].Longitude-longitude, 360)
|
||
|
|
currentY := polygon[current].Latitude
|
||
|
|
previousY := polygon[previous].Latitude
|
||
|
|
if math.Abs(currentX-previousX) > 180 {
|
||
|
|
if currentX < previousX {
|
||
|
|
currentX += 360
|
||
|
|
} else {
|
||
|
|
previousX += 360
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (currentY > latitude) == (previousY > latitude) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
intersectionX := previousX + (latitude-previousY)*(currentX-previousX)/(currentY-previousY)
|
||
|
|
if intersectionX > 0 {
|
||
|
|
inside = !inside
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if inside {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|