package svg import ( "fmt" "math" "regexp" "strconv" "testing" "time" eclipsecore "b612.me/astro/eclipse" "b612.me/astro/internal/svgmap" ) // 月食正射图的两条契约: // 1. 视点取食甚时刻的月下点("食甚可见半球"的边界就是圆盘边缘),否则视点固定在 (0°,0°), // 会把半个可见半球让给与本次月食无关的区域; // 2. 去掉圆盘边缘 2 px 内的固有亚像素带后,四类分区的着色必须与三刻地平几何一致。 func TestLunarEclipseMapSVGOrthographicCentering(t *testing.T) { for _, date := range []time.Time{ time.Date(2026, 3, 3, 0, 0, 0, 0, time.UTC), time.Date(2029, 1, 1, 0, 0, 0, 0, time.FixedZone("CST", 8*3600)), time.Date(2011, 6, 15, 12, 0, 0, 0, time.UTC), } { // 本用例核对投影几何与四类可见区的物理判据,半影带另有覆盖性用例。 doc, ok := LunarEclipseMapSVG(date, LunarEclipseMapSVGOptions{ Width: 960, Height: 640, Language: "zh", Location: time.UTC, Projection: EclipseMapProjectionOrthographic, DisablePenumbralPhase: true, }) if !ok { t.Fatalf("%s: missing orthographic map", date.Format("2006-01-02")) } centreX, centreY, radius := lunarOrthographicDisc(t, doc) partition := lunarMapPartitionOf(t, doc) // 食甚可见半球的边界是视界大圆:它的投影环应当以圆盘中心为圆心。 ringCentroidX, ringCentroidY, ok := lunarOrthographicRingCentroid(partition, "lunar-visible-maximum-shape") if !ok { t.Fatalf("%s: no maximum-visible ring", date.Format("2006-01-02")) } if offset := math.Hypot(ringCentroidX-centreX, ringCentroidY-centreY); offset > 1.5 { t.Errorf("%s: greatest-visible ring is %.3f px off the disc centre", date.Format("2006-01-02"), offset) } if err := lunarOrthographicInteriorMatchesPhysics(t, date, doc, partition, centreX, centreY, radius); err != nil { t.Errorf("%s: %v", date.Format("2006-01-02"), err) } } } // lunarOrthographicDisc 从图内读出圆盘圆心与半径。 func lunarOrthographicDisc(t *testing.T, doc string) (float64, float64, float64) { t.Helper() match := regexp.MustCompile(` radius-2 { continue } h1 := lunarPartitionAltitude(info.PenumbralStart, longitude, latitude) hm := lunarPartitionAltitude(info.Maximum, longitude, latitude) h4 := lunarPartitionAltitude(info.PenumbralEnd, longitude, latitude) if math.Abs(h1) < 0.05 || math.Abs(hm) < 0.05 || math.Abs(h4) < 0.05 { continue } expected := lunarOrthographicExpectedClass(h1, hm, h4) category, _ := partition.painted(x, y) counts[expected]++ if category != expected { mismatches++ if first == "" { first = fmt.Sprintf("lon=%s lat=%s h=(%s,%s,%s) expected=%s got=%s", lunarPartitionFormat(longitude), lunarPartitionFormat(latitude), lunarPartitionFormat(h1), lunarPartitionFormat(hm), lunarPartitionFormat(h4), expected, category) } } } } for _, class := range []string{lunarPartitionCategoryEntire, lunarPartitionCategoryMoonset, lunarPartitionCategoryMoonrise} { if counts[class] == 0 { t.Errorf("orthographic disc has no %s cell away from the rim", class) } } if mismatches != 0 { return fmt.Errorf("%d interior cells carry the wrong class, first %s", mismatches, first) } return nil } func lunarOrthographicExpectedClass(h1, hm, h4 float64) string { switch { case h1 > 0 && hm > 0 && h4 > 0: return lunarPartitionCategoryEntire case h1 > 0 && h4 > 0: return lunarPartitionCategoryMoonset case h1 > 0: return lunarPartitionCategoryMoonset case h4 > 0: return lunarPartitionCategoryMoonrise case hm > 0: return lunarPartitionCategoryMoonset } return lunarPartitionCategoryUnavailable }