244 lines
8.7 KiB
Go
244 lines
8.7 KiB
Go
|
|
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")
|
||
|
|
}
|
||
|
|
}
|