feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
+85
-19
@@ -6,9 +6,13 @@
|
||||
// 每个要素都包含 event 和 role 属性。带时间的 MultiLineString 要素还包含与坐标段对齐的嵌套 times 数组。
|
||||
// Times 编码为 UTC RFC 3339 字符串;路径采样由上游日月食和月掩选项控制后再传入本包。
|
||||
// WithTimeMarkers 变体还会追加 role 为 time-marker 的 Point 要素,标签按请求地点格式化。
|
||||
// 月掩 fallback 掩带通常为 MultiPolygon;支路跳变留下的孤立零时长截面以 MultiLineString 表达,
|
||||
// 与连续扫掠同时存在时使用 GeometryCollection,避免用虚假多边形重新连接跳变。
|
||||
// Every feature has event and role properties. Timed MultiLineString features also contain a nested times array aligned with their coordinate segments.
|
||||
// Times are encoded as UTC RFC 3339 strings. Path sampling is controlled by the source eclipse and occultation options before values reach this package.
|
||||
// The WithTimeMarkers variants additionally append Point Features whose role is time-marker and whose label is formatted for the requested location.
|
||||
// Fallback occultation bands normally use MultiPolygon. Isolated zero-duration sections left by branch changes use MultiLineString,
|
||||
// combined with a continuous sweep in a GeometryCollection, so discontinuities are never reconnected by a false polygon.
|
||||
package geojson
|
||||
|
||||
import (
|
||||
@@ -39,7 +43,8 @@ type feature struct {
|
||||
|
||||
type geometry struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates interface{} `json:"coordinates"`
|
||||
Coordinates interface{} `json:"coordinates,omitempty"`
|
||||
Geometries []geometry `json:"geometries,omitempty"`
|
||||
}
|
||||
|
||||
type pathSample struct {
|
||||
@@ -69,6 +74,14 @@ func marshalFeatureCollection(features []feature) ([]byte, error) {
|
||||
if len(features) == 0 {
|
||||
return nil, fmt.Errorf("geojson: no geographic features")
|
||||
}
|
||||
return encodeFeatureCollection(features)
|
||||
}
|
||||
|
||||
func marshalEmptyFeatureCollection() ([]byte, error) {
|
||||
return encodeFeatureCollection(make([]feature, 0))
|
||||
}
|
||||
|
||||
func encodeFeatureCollection(features []feature) ([]byte, error) {
|
||||
value, err := json.Marshal(featureCollection{Type: featureCollectionType, Features: features})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geojson: encode feature collection: %w", err)
|
||||
@@ -277,21 +290,54 @@ func pointGeometry(longitude, latitude float64) (geometry, error) {
|
||||
}
|
||||
|
||||
func timedMultiLineGeometry(points []pathSample) (geometry, [][]string, error) {
|
||||
segments, err := splitTimedLine(points)
|
||||
if err != nil {
|
||||
return geometry{}, nil, err
|
||||
}
|
||||
coordinates := make([][][]float64, 0, len(segments))
|
||||
times := make([][]string, 0, len(segments))
|
||||
for _, segment := range segments {
|
||||
line := make([][]float64, len(segment))
|
||||
lineTimes := make([]string, len(segment))
|
||||
for index, point := range segment {
|
||||
line[index] = []float64{point.Longitude, point.Latitude}
|
||||
lineTimes[index] = formatTime(point.Time)
|
||||
return timedMultiLineGeometryFromSegments([][]pathSample{points})
|
||||
}
|
||||
|
||||
func timedMultiLineGeometryFromSegments(sourceSegments [][]pathSample) (geometry, [][]string, error) {
|
||||
return timedMultiLineGeometryFromSegmentsWithTimeOrder(sourceSegments, true)
|
||||
}
|
||||
|
||||
func timedMultiLineGeometryFromSegmentsWithTimeOrder(
|
||||
sourceSegments [][]pathSample,
|
||||
requireIncreasingTimes bool,
|
||||
) (geometry, [][]string, error) {
|
||||
coordinates := make([][][]float64, 0, len(sourceSegments))
|
||||
times := make([][]string, 0, len(sourceSegments))
|
||||
for _, source := range sourceSegments {
|
||||
if len(source) == 0 {
|
||||
continue
|
||||
}
|
||||
coordinates = append(coordinates, line)
|
||||
times = append(times, lineTimes)
|
||||
if len(source) == 1 {
|
||||
point := source[0]
|
||||
if point.Time.IsZero() {
|
||||
return geometry{}, nil, fmt.Errorf("geojson: timed line contains a zero time")
|
||||
}
|
||||
if err := validateCoordinate(point.Longitude, point.Latitude); err != nil {
|
||||
return geometry{}, nil, err
|
||||
}
|
||||
position := []float64{point.Longitude, point.Latitude}
|
||||
formattedTime := formatTime(point.Time)
|
||||
coordinates = append(coordinates, [][]float64{position, append([]float64(nil), position...)})
|
||||
times = append(times, []string{formattedTime, formattedTime})
|
||||
continue
|
||||
}
|
||||
segments, err := splitTimedLine(source, requireIncreasingTimes)
|
||||
if err != nil {
|
||||
return geometry{}, nil, err
|
||||
}
|
||||
for _, segment := range segments {
|
||||
line := make([][]float64, len(segment))
|
||||
lineTimes := make([]string, len(segment))
|
||||
for index, point := range segment {
|
||||
line[index] = []float64{point.Longitude, point.Latitude}
|
||||
lineTimes[index] = formatTime(point.Time)
|
||||
}
|
||||
coordinates = append(coordinates, line)
|
||||
times = append(times, lineTimes)
|
||||
}
|
||||
}
|
||||
if len(coordinates) == 0 {
|
||||
return geometry{}, nil, fmt.Errorf("geojson: line has no valid segments")
|
||||
}
|
||||
return geometry{Type: "MultiLineString", Coordinates: coordinates}, times, nil
|
||||
}
|
||||
@@ -309,7 +355,7 @@ func geoMultiLineGeometry(points []geodata.GeoPoint, closeLine bool) (geometry,
|
||||
if closeLine && !geodata.SameGeoPoint(geographic[0], geographic[len(geographic)-1]) {
|
||||
geographic = append(geographic, geographic[0])
|
||||
}
|
||||
segments := geodata.PolylineSegments(geographic, geodata.ProjectionEquirectangular)
|
||||
segments := geodata.PolylineSegments(geographic, geodata.ClipView{Projection: geodata.ProjectionEquirectangular})
|
||||
coordinates := make([][][]float64, 0, len(segments))
|
||||
for _, segment := range segments {
|
||||
if len(segment) < 2 {
|
||||
@@ -327,7 +373,24 @@ func geoMultiLineGeometry(points []geodata.GeoPoint, closeLine bool) (geometry,
|
||||
return geometry{Type: "MultiLineString", Coordinates: coordinates}, nil
|
||||
}
|
||||
|
||||
// multiPolygonFillGeometry tags fill-only polygons, which tolerate a coarser
|
||||
// map chord bound than the path strokes.
|
||||
func multiPolygonFillGeometry(polygons [][]geodata.GeoPoint) (geometry, error) {
|
||||
return multiPolygonGeometryWithin(
|
||||
polygons, sphericalFillChordLimitKM, sphericalFillChordErrorDegrees,
|
||||
)
|
||||
}
|
||||
|
||||
func multiPolygonGeometry(polygons [][]geodata.GeoPoint) (geometry, error) {
|
||||
return multiPolygonGeometryWithin(
|
||||
polygons, sphericalMapChordLimitKM, sphericalMapChordErrorDegrees,
|
||||
)
|
||||
}
|
||||
|
||||
func multiPolygonGeometryWithin(
|
||||
polygons [][]geodata.GeoPoint,
|
||||
chordLimitKM, chordErrorDegrees float64,
|
||||
) (geometry, error) {
|
||||
fragments := make([][]geodata.GeoPoint, 0, len(polygons))
|
||||
for index, polygon := range polygons {
|
||||
polygon = openRing(polygon)
|
||||
@@ -340,7 +403,10 @@ func multiPolygonGeometry(polygons [][]geodata.GeoPoint) (geometry, error) {
|
||||
}
|
||||
}
|
||||
fragments = append(fragments,
|
||||
geodata.PolygonFragments(polygon, geodata.ProjectionEquirectangular)...)
|
||||
geodata.PolygonFragments(
|
||||
sampleSphericalMapRingWithin(polygon, chordLimitKM, chordErrorDegrees),
|
||||
geodata.ClipView{Projection: geodata.ProjectionEquirectangular},
|
||||
)...)
|
||||
}
|
||||
return multiPolygonGeometryFromFragments(fragments)
|
||||
}
|
||||
@@ -403,7 +469,7 @@ func polygonArea(points []geodata.GeoPoint) float64 {
|
||||
return area / 2
|
||||
}
|
||||
|
||||
func splitTimedLine(points []pathSample) ([][]pathSample, error) {
|
||||
func splitTimedLine(points []pathSample, requireIncreasingTimes bool) ([][]pathSample, error) {
|
||||
if len(points) < 2 {
|
||||
return nil, fmt.Errorf("geojson: line requires at least two points")
|
||||
}
|
||||
@@ -414,7 +480,7 @@ func splitTimedLine(points []pathSample) ([][]pathSample, error) {
|
||||
if err := validateCoordinate(point.Longitude, point.Latitude); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index > 0 && !point.Time.After(points[index-1].Time) {
|
||||
if requireIncreasingTimes && index > 0 && !point.Time.After(points[index-1].Time) {
|
||||
return nil, fmt.Errorf("geojson: timed line times must be strictly increasing")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user