533 lines
18 KiB
Go
533 lines
18 KiB
Go
|
|
package geojson
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"b612.me/astro/internal/geodata"
|
||
|
|
"b612.me/astro/moon"
|
||
|
|
)
|
||
|
|
|
||
|
|
const lunarOccultationEvent = "lunar-occultation"
|
||
|
|
|
||
|
|
// MarshalStarOccultation 将月掩恒星的全球掩带和中心线编码为 GeoJSON。
|
||
|
|
// MarshalStarOccultation encodes a global stellar occultation band and center line as GeoJSON.
|
||
|
|
func MarshalStarOccultation(path moon.StarOccultationPath) ([]byte, error) {
|
||
|
|
return marshalStarOccultation(path, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarshalStarOccultationWithTimeMarkers 编码恒星月掩,并沿中心线按固定间隔追加 Point 要素。
|
||
|
|
// MarshalStarOccultationWithTimeMarkers encodes a stellar occultation and adds Point Features at regular intervals along its center line.
|
||
|
|
func MarshalStarOccultationWithTimeMarkers(
|
||
|
|
path moon.StarOccultationPath,
|
||
|
|
options TimeMarkerOptions,
|
||
|
|
) ([]byte, error) {
|
||
|
|
return marshalStarOccultation(path, &options)
|
||
|
|
}
|
||
|
|
|
||
|
|
func marshalStarOccultation(path moon.StarOccultationPath, markerOptions *TimeMarkerOptions) ([]byte, error) {
|
||
|
|
if markerOptions != nil {
|
||
|
|
if err := validateTimeMarkerOptions(*markerOptions); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err := validateStarOccultationPathData(path); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
properties := map[string]interface{}{
|
||
|
|
"target_type": "star",
|
||
|
|
"target_id": path.TargetID,
|
||
|
|
"complete": path.Complete,
|
||
|
|
"step_seconds": path.Step.Seconds(),
|
||
|
|
"target_spacing_km": path.TargetSpacingKM,
|
||
|
|
}
|
||
|
|
band, err := occultationBandPolygon(path.NorthernLimit, path.SouthernLimit)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: stellar occultation band: %w", err)
|
||
|
|
}
|
||
|
|
value, err := multiPolygonGeometry([][]geodata.GeoPoint{band})
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: stellar occultation band: %w", err)
|
||
|
|
}
|
||
|
|
features := []feature{
|
||
|
|
newFeature(lunarOccultationEvent, "occultation-band", value, cloneProperties(properties)),
|
||
|
|
}
|
||
|
|
if len(path.CenterLine) > 0 {
|
||
|
|
features, err = appendOccultationPathLine(features, "center-line", path.CenterLine, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
features, err = appendOccultationPathLine(features, "north-limit", path.NorthernLimit, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
features, err = appendOccultationPathLine(features, "south-limit", path.SouthernLimit, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if markerOptions != nil && len(path.CenterLine) > 0 {
|
||
|
|
features, err = appendTimeMarkerFeatures(
|
||
|
|
features,
|
||
|
|
lunarOccultationEvent,
|
||
|
|
"center-line",
|
||
|
|
occultationPathSamples(path.CenterLine),
|
||
|
|
*markerOptions,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, marker := range []struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{
|
||
|
|
{role: "start", point: path.Start},
|
||
|
|
{role: "greatest", point: path.Greatest},
|
||
|
|
{role: "end", point: path.End},
|
||
|
|
} {
|
||
|
|
features, err = appendOccultationPoint(features, marker.role, marker.point, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return marshalFeatureCollection(features)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarshalPlanetOccultation 将月掩行星的部分掩、全掩和中心线编码为 GeoJSON。
|
||
|
|
// MarshalPlanetOccultation encodes partial, total, and center-line planetary occultation geometry as GeoJSON.
|
||
|
|
func MarshalPlanetOccultation(path moon.PlanetOccultationPath) ([]byte, error) {
|
||
|
|
return marshalPlanetOccultation(path, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarshalPlanetOccultationWithTimeMarkers 编码行星月掩,并沿中心线按固定间隔追加 Point 要素。
|
||
|
|
// MarshalPlanetOccultationWithTimeMarkers encodes a planetary occultation and adds Point Features at regular intervals along its center line.
|
||
|
|
func MarshalPlanetOccultationWithTimeMarkers(
|
||
|
|
path moon.PlanetOccultationPath,
|
||
|
|
options TimeMarkerOptions,
|
||
|
|
) ([]byte, error) {
|
||
|
|
return marshalPlanetOccultation(path, &options)
|
||
|
|
}
|
||
|
|
|
||
|
|
func marshalPlanetOccultation(path moon.PlanetOccultationPath, markerOptions *TimeMarkerOptions) ([]byte, error) {
|
||
|
|
if markerOptions != nil {
|
||
|
|
if err := validateTimeMarkerOptions(*markerOptions); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err := path.Planet.Validate(); err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: planetary occultation target: %w", err)
|
||
|
|
}
|
||
|
|
if err := validatePlanetOccultationPathData(path); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
properties := map[string]interface{}{
|
||
|
|
"target_type": "planet",
|
||
|
|
"target_id": path.TargetID,
|
||
|
|
"planet": string(path.Planet),
|
||
|
|
"complete": path.Complete,
|
||
|
|
"has_total_band": path.HasTotalBand,
|
||
|
|
"total_complete": path.TotalComplete,
|
||
|
|
"step_seconds": path.Step.Seconds(),
|
||
|
|
"target_spacing_km": path.TargetSpacingKM,
|
||
|
|
"greatest_total_width_km": path.GreatestTotalWidthKM,
|
||
|
|
}
|
||
|
|
features := make([]feature, 0, len(path.PartialFootprints)+len(path.TotalFootprints)+12)
|
||
|
|
var err error
|
||
|
|
if len(path.PartialFootprints) > 0 {
|
||
|
|
features, err = appendOccultationFootprints(
|
||
|
|
features, "partial-footprint", path.PartialFootprints, properties,
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
features, err = appendOccultationBand(
|
||
|
|
features, "partial-band", path.NorthernLimit, path.SouthernLimit, properties,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
if path.HasTotalBand {
|
||
|
|
if len(path.TotalFootprints) > 0 {
|
||
|
|
features, err = appendOccultationFootprints(
|
||
|
|
features, "total-footprint", path.TotalFootprints, properties,
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
features, err = appendOccultationBand(
|
||
|
|
features, "total-band", path.NorthernTotalLimit, path.SouthernTotalLimit, properties,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(path.CenterLine) > 0 {
|
||
|
|
features, err = appendOccultationPathLine(features, "center-line", path.CenterLine, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
features, err = appendOccultationPathLine(features, "north-limit", path.NorthernLimit, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
features, err = appendOccultationPathLine(features, "south-limit", path.SouthernLimit, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if path.HasTotalBand {
|
||
|
|
features, err = appendOccultationPathLine(
|
||
|
|
features, "north-total-limit", path.NorthernTotalLimit, properties,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
features, err = appendOccultationPathLine(
|
||
|
|
features, "south-total-limit", path.SouthernTotalLimit, properties,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if markerOptions != nil && len(path.CenterLine) > 0 {
|
||
|
|
features, err = appendTimeMarkerFeatures(
|
||
|
|
features,
|
||
|
|
lunarOccultationEvent,
|
||
|
|
"center-line",
|
||
|
|
occultationPathSamples(path.CenterLine),
|
||
|
|
*markerOptions,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
markers := []struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{
|
||
|
|
{role: "start", point: path.Start},
|
||
|
|
}
|
||
|
|
if path.HasTotalBand {
|
||
|
|
markers = append(markers, struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{role: "total-start", point: path.TotalStart})
|
||
|
|
}
|
||
|
|
markers = append(markers, struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{role: "greatest", point: path.Greatest})
|
||
|
|
if path.HasTotalBand {
|
||
|
|
markers = append(markers, struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{role: "total-end", point: path.TotalEnd})
|
||
|
|
}
|
||
|
|
markers = append(markers, struct {
|
||
|
|
role string
|
||
|
|
point moon.OccultationPathPoint
|
||
|
|
}{role: "end", point: path.End})
|
||
|
|
for _, marker := range markers {
|
||
|
|
features, err = appendOccultationPoint(features, marker.role, marker.point, properties)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return marshalFeatureCollection(features)
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationBand(
|
||
|
|
features []feature,
|
||
|
|
role string,
|
||
|
|
northern, southern []moon.OccultationPathPoint,
|
||
|
|
properties map[string]interface{},
|
||
|
|
) ([]feature, error) {
|
||
|
|
band, err := occultationBandPolygon(northern, southern)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: %s: %w", role, err)
|
||
|
|
}
|
||
|
|
value, err := multiPolygonGeometry([][]geodata.GeoPoint{band})
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: %s: %w", role, err)
|
||
|
|
}
|
||
|
|
return append(features, newFeature(
|
||
|
|
lunarOccultationEvent, role, value, cloneProperties(properties),
|
||
|
|
)), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationFootprints(
|
||
|
|
features []feature,
|
||
|
|
role string,
|
||
|
|
footprints []moon.PlanetOccultationFootprint,
|
||
|
|
properties map[string]interface{},
|
||
|
|
) ([]feature, error) {
|
||
|
|
appended := 0
|
||
|
|
for _, footprint := range footprints {
|
||
|
|
if footprint.Time.IsZero() {
|
||
|
|
return nil, fmt.Errorf("geojson: %s time is required", role)
|
||
|
|
}
|
||
|
|
polygons := make([][]geodata.GeoPoint, 0, len(footprint.Polygons))
|
||
|
|
for _, source := range footprint.Polygons {
|
||
|
|
polygon := make([]geodata.GeoPoint, len(source))
|
||
|
|
for index, point := range source {
|
||
|
|
polygon[index] = geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude}
|
||
|
|
}
|
||
|
|
polygons = append(polygons, polygon)
|
||
|
|
}
|
||
|
|
value, err := multiPolygonGeometry(polygons)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("geojson: %s at %s: %w", role, formatTime(footprint.Time), err)
|
||
|
|
}
|
||
|
|
footprintProperties := cloneProperties(properties)
|
||
|
|
footprintProperties["time"] = formatTime(footprint.Time)
|
||
|
|
features = append(features, newFeature(
|
||
|
|
lunarOccultationEvent, role, value, footprintProperties,
|
||
|
|
))
|
||
|
|
appended++
|
||
|
|
}
|
||
|
|
if appended == 0 {
|
||
|
|
return nil, fmt.Errorf("geojson: %s has no valid polygons", role)
|
||
|
|
}
|
||
|
|
return features, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationBandPolygon(
|
||
|
|
northern, southern []moon.OccultationPathPoint,
|
||
|
|
) ([]geodata.GeoPoint, error) {
|
||
|
|
if len(northern) != len(southern) {
|
||
|
|
return nil, fmt.Errorf("paired limits must have the same sample count")
|
||
|
|
}
|
||
|
|
count := len(northern)
|
||
|
|
if count < 2 {
|
||
|
|
return nil, fmt.Errorf("paired limits require at least two points per side")
|
||
|
|
}
|
||
|
|
for index := range northern {
|
||
|
|
if northern[index].Time.IsZero() || southern[index].Time.IsZero() {
|
||
|
|
return nil, fmt.Errorf("paired limit sample %d time is required", index)
|
||
|
|
}
|
||
|
|
if !northern[index].Time.Equal(southern[index].Time) {
|
||
|
|
return nil, fmt.Errorf("paired limit sample %d times must match", index)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
polygon := make([]geodata.GeoPoint, 0, 2*count)
|
||
|
|
for _, point := range northern[:count] {
|
||
|
|
polygon = append(polygon, geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
|
||
|
|
}
|
||
|
|
for index := count - 1; index >= 0; index-- {
|
||
|
|
point := southern[index]
|
||
|
|
polygon = append(polygon, geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
|
||
|
|
}
|
||
|
|
return polygon, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationPathLine(
|
||
|
|
features []feature,
|
||
|
|
role string,
|
||
|
|
points []moon.OccultationPathPoint,
|
||
|
|
properties map[string]interface{},
|
||
|
|
) ([]feature, error) {
|
||
|
|
samples := make([]pathSample, len(points))
|
||
|
|
for index, point := range points {
|
||
|
|
samples[index] = occultationPathSample(point)
|
||
|
|
}
|
||
|
|
return appendTimedLineFeature(features, lunarOccultationEvent, role, samples, properties)
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathSamples(points []moon.OccultationPathPoint) []pathSample {
|
||
|
|
samples := make([]pathSample, len(points))
|
||
|
|
for index, point := range points {
|
||
|
|
samples[index] = occultationPathSample(point)
|
||
|
|
}
|
||
|
|
return samples
|
||
|
|
}
|
||
|
|
|
||
|
|
func appendOccultationPoint(
|
||
|
|
features []feature,
|
||
|
|
role string,
|
||
|
|
point moon.OccultationPathPoint,
|
||
|
|
properties map[string]interface{},
|
||
|
|
) ([]feature, error) {
|
||
|
|
pointProperties := cloneProperties(properties)
|
||
|
|
pointProperties["moon_altitude_deg"] = point.MoonAltitude
|
||
|
|
pointProperties["width_km"] = point.WidthKM
|
||
|
|
return appendPointFeature(
|
||
|
|
features, lunarOccultationEvent, role, occultationPathSample(point), pointProperties,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationPathSample(point moon.OccultationPathPoint) pathSample {
|
||
|
|
return pathSample{Time: point.Time, Longitude: point.Longitude, Latitude: point.Latitude}
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateStarOccultationPathData(path moon.StarOccultationPath) error {
|
||
|
|
if !path.Complete {
|
||
|
|
return fmt.Errorf("geojson: occultation path is incomplete")
|
||
|
|
}
|
||
|
|
if err := (moon.OccultationPathOptions{Step: path.Step, TargetSpacingKM: path.TargetSpacingKM}).Validate(); err != nil {
|
||
|
|
return fmt.Errorf("geojson: invalid occultation path sampling metadata: %w", err)
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathPoint("start", path.Start); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathPoint("greatest", path.Greatest); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathPoint("end", path.End); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if path.Greatest.Time.Before(path.Start.Time) || path.End.Time.Before(path.Greatest.Time) {
|
||
|
|
return fmt.Errorf("geojson: occultation times must be ordered start, greatest, end")
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathSeries("center line", path.CenterLine, false); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathSeries("northern limit", path.NorthernLimit, true); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathSeries("southern limit", path.SouthernLimit, true); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if len(path.NorthernLimit) != len(path.SouthernLimit) {
|
||
|
|
return fmt.Errorf("geojson: occultation northern and southern limits must have the same sample count")
|
||
|
|
}
|
||
|
|
for index := range path.NorthernLimit {
|
||
|
|
if !path.NorthernLimit[index].Time.Equal(path.SouthernLimit[index].Time) {
|
||
|
|
return fmt.Errorf("geojson: occultation limit sample %d times must match", index)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
last := len(path.NorthernLimit) - 1
|
||
|
|
if !path.NorthernLimit[0].Time.Equal(path.Start.Time) || !path.SouthernLimit[0].Time.Equal(path.Start.Time) ||
|
||
|
|
!path.NorthernLimit[last].Time.Equal(path.End.Time) || !path.SouthernLimit[last].Time.Equal(path.End.Time) {
|
||
|
|
return fmt.Errorf("geojson: occultation limits must span start through end")
|
||
|
|
}
|
||
|
|
if len(path.CenterLine) > 0 {
|
||
|
|
if path.CenterLine[0].Time.Before(path.Start.Time) ||
|
||
|
|
path.CenterLine[len(path.CenterLine)-1].Time.After(path.End.Time) {
|
||
|
|
return fmt.Errorf("geojson: occultation center line must be inside start and end")
|
||
|
|
}
|
||
|
|
if path.Greatest.Time.Before(path.CenterLine[0].Time) ||
|
||
|
|
path.Greatest.Time.After(path.CenterLine[len(path.CenterLine)-1].Time) {
|
||
|
|
return fmt.Errorf("geojson: occultation greatest time is outside the center-line interval")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func validatePlanetOccultationPathData(path moon.PlanetOccultationPath) error {
|
||
|
|
starPath := moon.StarOccultationPath{
|
||
|
|
TargetID: path.TargetID, Start: path.Start, Greatest: path.Greatest, End: path.End,
|
||
|
|
Complete: path.Complete, CenterLine: path.CenterLine,
|
||
|
|
NorthernLimit: path.NorthernLimit, SouthernLimit: path.SouthernLimit,
|
||
|
|
Step: path.Step, TargetSpacingKM: path.TargetSpacingKM,
|
||
|
|
}
|
||
|
|
if err := validateStarOccultationPathData(starPath); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !path.HasTotalBand {
|
||
|
|
if path.TotalComplete || !path.TotalStart.Time.IsZero() || !path.TotalEnd.Time.IsZero() ||
|
||
|
|
len(path.NorthernTotalLimit) != 0 || len(path.SouthernTotalLimit) != 0 ||
|
||
|
|
len(path.TotalFootprints) != 0 || path.GreatestTotalWidthKM != 0 {
|
||
|
|
return fmt.Errorf("geojson: total-band fields require HasTotalBand")
|
||
|
|
}
|
||
|
|
return validateOccultationFootprints("partial", path.PartialFootprints, path.Start.Time, path.End.Time)
|
||
|
|
}
|
||
|
|
if !path.TotalComplete {
|
||
|
|
return fmt.Errorf("geojson: total-occultation band is incomplete")
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathPoint("total start", path.TotalStart); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathPoint("total end", path.TotalEnd); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !path.Start.Time.Before(path.TotalStart.Time) || !path.TotalStart.Time.Before(path.Greatest.Time) ||
|
||
|
|
!path.Greatest.Time.Before(path.TotalEnd.Time) || !path.TotalEnd.Time.Before(path.End.Time) {
|
||
|
|
return fmt.Errorf("geojson: total-band times must be inside outer start, greatest, and end")
|
||
|
|
}
|
||
|
|
if !finiteGeoJSON(path.GreatestTotalWidthKM) || path.GreatestTotalWidthKM <= 0 ||
|
||
|
|
!finiteGeoJSON(path.Greatest.WidthKM) || path.GreatestTotalWidthKM >= path.Greatest.WidthKM {
|
||
|
|
return fmt.Errorf("geojson: total-band width must be positive and narrower than the outer band")
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathSeries("northern total limit", path.NorthernTotalLimit, true); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := validateOccultationPathSeries("southern total limit", path.SouthernTotalLimit, true); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if len(path.NorthernTotalLimit) != len(path.SouthernTotalLimit) {
|
||
|
|
return fmt.Errorf("geojson: total northern and southern limits must have the same sample count")
|
||
|
|
}
|
||
|
|
for index := range path.NorthernTotalLimit {
|
||
|
|
if !path.NorthernTotalLimit[index].Time.Equal(path.SouthernTotalLimit[index].Time) {
|
||
|
|
return fmt.Errorf("geojson: total limit sample %d times must match", index)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
last := len(path.NorthernTotalLimit) - 1
|
||
|
|
if !path.NorthernTotalLimit[0].Time.Equal(path.TotalStart.Time) ||
|
||
|
|
!path.SouthernTotalLimit[0].Time.Equal(path.TotalStart.Time) ||
|
||
|
|
!path.NorthernTotalLimit[last].Time.Equal(path.TotalEnd.Time) ||
|
||
|
|
!path.SouthernTotalLimit[last].Time.Equal(path.TotalEnd.Time) {
|
||
|
|
return fmt.Errorf("geojson: total limits must span total start through total end")
|
||
|
|
}
|
||
|
|
if err := validateOccultationFootprints("partial", path.PartialFootprints, path.Start.Time, path.End.Time); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return validateOccultationFootprints("total", path.TotalFootprints, path.TotalStart.Time, path.TotalEnd.Time)
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateOccultationPathSeries(name string, points []moon.OccultationPathPoint, required bool) error {
|
||
|
|
if required && len(points) < 2 {
|
||
|
|
return fmt.Errorf("geojson: %s requires at least two points", name)
|
||
|
|
}
|
||
|
|
previous := time.Time{}
|
||
|
|
for index, point := range points {
|
||
|
|
if err := validateOccultationPathPoint(fmt.Sprintf("%s[%d]", name, index), point); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !previous.IsZero() && !point.Time.After(previous) {
|
||
|
|
return fmt.Errorf("geojson: %s times must be strictly increasing", name)
|
||
|
|
}
|
||
|
|
previous = point.Time
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateOccultationPathPoint(name string, point moon.OccultationPathPoint) error {
|
||
|
|
if point.Time.IsZero() {
|
||
|
|
return fmt.Errorf("geojson: %s time is required", name)
|
||
|
|
}
|
||
|
|
if err := validateCoordinate(point.Longitude, point.Latitude); err != nil {
|
||
|
|
return fmt.Errorf("geojson: %s: %w", name, err)
|
||
|
|
}
|
||
|
|
if !finiteGeoJSON(point.MoonAltitude) || point.MoonAltitude < -90 || point.MoonAltitude > 90 {
|
||
|
|
return fmt.Errorf("geojson: %s Moon altitude must be finite and within [-90, 90]", name)
|
||
|
|
}
|
||
|
|
if !finiteGeoJSON(point.WidthKM) || point.WidthKM < 0 {
|
||
|
|
return fmt.Errorf("geojson: %s width must be finite and non-negative", name)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateOccultationFootprints(
|
||
|
|
name string,
|
||
|
|
footprints []moon.PlanetOccultationFootprint,
|
||
|
|
start, end time.Time,
|
||
|
|
) error {
|
||
|
|
previous := time.Time{}
|
||
|
|
for index, footprint := range footprints {
|
||
|
|
if footprint.Time.IsZero() {
|
||
|
|
return fmt.Errorf("geojson: %s footprint[%d] time is required", name, index)
|
||
|
|
}
|
||
|
|
if footprint.Time.Before(start) || footprint.Time.After(end) {
|
||
|
|
return fmt.Errorf("geojson: %s footprint[%d] time must be inside its contact interval", name, index)
|
||
|
|
}
|
||
|
|
if !previous.IsZero() && !footprint.Time.After(previous) {
|
||
|
|
return fmt.Errorf("geojson: %s footprint times must be strictly increasing", name)
|
||
|
|
}
|
||
|
|
previous = footprint.Time
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|