feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user