Files
astro/geojson/eclipse_isochrone_test.go
T

244 lines
8.7 KiB
Go
Raw Normal View History

package geojson_test
// greatest-time-line 逐支路导出,time 与 jde 取自等时线本身,几何与支路逐点一致。
import (
"bytes"
"encoding/json"
"math"
"strings"
"testing"
"time"
"b612.me/astro/eclipse"
"b612.me/astro/geojson"
)
func cloneSolarGreatestTimeContours(
source []eclipse.SolarEclipseGreatestTimeContour,
) []eclipse.SolarEclipseGreatestTimeContour {
result := make([]eclipse.SolarEclipseGreatestTimeContour, len(source))
for index, contour := range source {
segments := make([][]eclipse.SolarEclipsePathPoint, len(contour.Segments))
for segmentIndex, segment := range contour.Segments {
segments[segmentIndex] = append([]eclipse.SolarEclipsePathPoint(nil), segment...)
}
contour.Segments = segments
result[index] = contour
}
return result
}
func TestMarshalSolarEclipseGreatestTimeLines(t *testing.T) {
location := time.FixedZone("UTC+8", 8*3600)
stepped := eclipse.SolarEclipsePartialFootprintOptions{
Step: 20 * time.Minute, BoundaryPoints: 24, DisableRiseSet: true,
GreatestTimeStep: 30 * time.Minute,
}
valued := stepped
valued.GreatestTimeStep = 0
valued.GreatestTimeValues = []time.Time{
time.Date(2009, time.July, 22, 9, 0, 0, 0, location),
time.Date(2009, time.July, 22, 10, 0, 0, 0, location),
time.Date(2009, time.July, 22, 11, 0, 0, 0, location),
}
for _, fixture := range []struct {
name string
date time.Time
options eclipse.SolarEclipsePartialFootprintOptions
central bool
wantMultiBranch bool
}{
{
name: "stepped-2021", date: time.Date(2021, time.June, 10, 0, 0, 0, 0, time.UTC),
options: stepped, wantMultiBranch: true,
},
{
name: "valued-2009-central", date: time.Date(2009, time.July, 22, 0, 0, 0, 0, location),
options: valued, central: true,
},
} {
t.Run(fixture.name, func(t *testing.T) {
partial, ok := eclipse.SolarEclipsePartialFootprintsNASABulletinSplitK(fixture.date, fixture.options)
if !ok || len(partial.GreatestTimeContours) == 0 {
t.Fatal("expected greatest-time contours")
}
multiBranch := false
for _, contour := range partial.GreatestTimeContours {
if len(contour.Segments) > 1 {
multiBranch = true
}
}
if multiBranch != fixture.wantMultiBranch {
t.Fatalf("fixture multi-branch=%v, want %v", multiBranch, fixture.wantMultiBranch)
}
var central *eclipse.SolarEclipsePath
if fixture.central {
path, pathOK := eclipse.SolarEclipseCentralPath(
fixture.date, eclipse.SolarEclipsePathOptions{Step: 10 * time.Minute},
)
if !pathOK {
t.Fatal("expected central path")
}
central = &path
}
data, err := geojson.MarshalSolarEclipse(partial, central)
if err != nil {
t.Fatalf("MarshalSolarEclipse: %v", err)
}
lines := featuresWithRole(decodeCollection(t, data), "greatest-time-line")
branches := 0
for _, contour := range partial.GreatestTimeContours {
branches += len(contour.Segments)
}
if len(lines) != branches {
t.Fatalf("greatest-time-line features=%d, branches=%d", len(lines), branches)
}
lineIndex := 0
for _, contour := range partial.GreatestTimeContours {
for _, segment := range contour.Segments {
line := lines[lineIndex]
lineIndex++
if line.Geometry.Type != "MultiLineString" {
t.Fatalf("line %d geometry=%q, want MultiLineString", lineIndex-1, line.Geometry.Type)
}
if got, want := line.Properties["time"], contour.Time.UTC().Format(time.RFC3339Nano); got != want {
t.Fatalf("line %d time=%v, want %v", lineIndex-1, got, want)
}
if got, ok := line.Properties["jde"].(float64); !ok || got != contour.JDE {
t.Fatalf("line %d jde=%v, want %v", lineIndex-1, line.Properties["jde"], contour.JDE)
}
assertTimedLineAligned(t, line)
var coordinates [][][]float64
if err := json.Unmarshal(line.Geometry.Coordinates, &coordinates); err != nil {
t.Fatalf("decode line %d: %v", lineIndex-1, err)
}
if len(coordinates) != 1 || len(coordinates[0]) != len(segment) {
t.Fatalf("line %d coordinate lines=%d points=%d, branch points=%d",
lineIndex-1, len(coordinates), coordinateCount(coordinates), len(segment))
}
for pointIndex, point := range segment {
if coordinates[0][pointIndex][0] != point.Longitude ||
coordinates[0][pointIndex][1] != point.Latitude {
t.Fatalf("line %d point %d=(%v, %v), branch point=(%v, %v)",
lineIndex-1, pointIndex, coordinates[0][pointIndex][0],
coordinates[0][pointIndex][1], point.Longitude, point.Latitude)
}
}
}
}
})
}
}
func coordinateCount(coordinates [][][]float64) int {
count := 0
for _, line := range coordinates {
count += len(line)
}
return count
}
func TestMarshalSolarEclipseRejectsInvalidGreatestTimeContours(t *testing.T) {
date := time.Date(2009, time.July, 22, 0, 0, 0, 0, time.UTC)
base, ok := eclipse.SolarEclipsePartialFootprintsNASABulletinSplitK(date, eclipse.SolarEclipsePartialFootprintOptions{
Step: 20 * time.Minute, BoundaryPoints: 24, DisableRiseSet: true,
GreatestTimeValues: []time.Time{
time.Date(2009, time.July, 22, 1, 0, 0, 0, time.UTC),
time.Date(2009, time.July, 22, 2, 0, 0, 0, time.UTC),
},
})
if !ok || len(base.GreatestTimeContours) == 0 || len(base.GreatestTimeContours[0].Segments) == 0 {
t.Fatal("expected greatest-time contours")
}
for _, testCase := range []struct {
name string
mutate func([]eclipse.SolarEclipseGreatestTimeContour)
}{
{name: "nan-longitude", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0][0].Longitude = math.NaN()
}},
{name: "infinite-latitude", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0][0].Latitude = math.Inf(1)
}},
{name: "longitude-out-of-range", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0][0].Longitude = 180.5
}},
{name: "latitude-out-of-range", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0][0].Latitude = -90.5
}},
{name: "single-point-branch", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0] = contours[0].Segments[0][:1]
}},
{name: "zero-jde", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].JDE = 0
}},
{name: "nan-jde", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].JDE = math.NaN()
}},
{name: "zero-time", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Time = time.Time{}
}},
{name: "zero-point-time", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments[0][0].Time = time.Time{}
}},
{name: "no-branches", mutate: func(contours []eclipse.SolarEclipseGreatestTimeContour) {
contours[0].Segments = nil
}},
} {
t.Run(testCase.name, func(t *testing.T) {
partial := base
partial.GreatestTimeContours = cloneSolarGreatestTimeContours(base.GreatestTimeContours)
testCase.mutate(partial.GreatestTimeContours)
if _, err := geojson.MarshalSolarEclipse(partial, nil); err == nil {
t.Fatal("invalid greatest-time contour was accepted")
} else if !strings.Contains(err.Error(), "greatest-time contour") {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestMarshalSolarEclipseWithoutGreatestTimeLinesIsUnchanged(t *testing.T) {
date := time.Date(2009, time.July, 22, 0, 0, 0, 0, time.UTC)
options := eclipse.SolarEclipsePartialFootprintOptions{
Step: 20 * time.Minute, BoundaryPoints: 24, DisableRiseSet: true,
}
plain, ok := eclipse.SolarEclipsePartialFootprintsNASABulletinSplitK(date, options)
if !ok {
t.Fatal("expected solar partial footprints")
}
first, err := geojson.MarshalSolarEclipse(plain, nil)
if err != nil {
t.Fatalf("MarshalSolarEclipse: %v", err)
}
second, err := geojson.MarshalSolarEclipse(plain, nil)
if err != nil {
t.Fatalf("MarshalSolarEclipse: %v", err)
}
if !bytes.Equal(first, second) {
t.Fatal("default solar eclipse export is not byte stable")
}
if bytes.Contains(first, []byte("greatest-time-line")) {
t.Fatal("default solar eclipse export contains greatest-time lines")
}
requested := options
requested.GreatestTimeValues = []time.Time{
time.Date(2009, time.July, 22, 1, 0, 0, 0, time.UTC),
time.Date(2009, time.July, 22, 2, 0, 0, 0, time.UTC),
}
withLines, ok := eclipse.SolarEclipsePartialFootprintsNASABulletinSplitK(date, requested)
if !ok || len(withLines.GreatestTimeContours) == 0 {
t.Fatal("expected greatest-time contours")
}
cleared := withLines
cleared.GreatestTimeContours = nil
third, err := geojson.MarshalSolarEclipse(cleared, nil)
if err != nil {
t.Fatalf("MarshalSolarEclipse: %v", err)
}
if !bytes.Equal(first, third) {
t.Fatal("clearing greatest-time contours changes the export")
}
}