2026-10-15 23:54:15 +08:00
package svg
import (
"fmt"
"html"
"math"
"strings"
"time"
"b612.me/astro"
"b612.me/astro/basic"
"b612.me/astro/internal/svgchart"
"b612.me/astro/internal/timenote"
)
const (
transitLocalDefaultWidth = 960
transitLocalDefaultHeight = 680
)
// TransitLocalSVGOptions 控制站心凌日日面图。
// TransitLocalSVGOptions controls a topocentric planetary-transit solar-disk chart.
type TransitLocalSVGOptions struct {
// Width 和 Height 是 SVG 画布尺寸;宽度小于 640 或高度小于 480 时使用 960x680 默认值。
// Width and Height are SVG canvas dimensions. Width values below 640 and height values below 480 use the 960x680 defaults.
Width int
Height int
// Step 是行星轨迹采样步长;非正值使用两分钟。
// Step is the planet-track sampling step. Non-positive values use two minutes.
Step time . Duration
// Language 为 "en"(不区分大小写)时使用英文,否则使用中文。
// Language uses English for "en" (case-insensitive) and Chinese otherwise.
Language string
// Location 控制显示的事件时刻;nil 使用 date.Location()。
// Location controls displayed event times. Nil uses date.Location().
Location * time . Location
// TimeScale 选择图中时刻的时标:零值 UTC; TimeScaleUT1 要求 Location 为 nil 或 UTC,否则返回 false。
// TimeScale selects the label scale: the zero value is UTC; TimeScaleUT1 requires Location to be nil or UTC, otherwise the renderer returns false.
TimeScale astro . TimeScale
// 空文本字段使用本地化的自动标签。
// Empty text fields use localized automatic labels.
Title string
FooterNote string
}
// TransitLocalSVG 绘制距 date 最近一次水星或金星凌日在观测地(经度 lon、纬度 lat,东经北纬为正,海拔 height 米)
// 的站心日面图:上北左东的天空视向,画出行星中心轨迹、各接触与凌甚时的行星圆面,以及接触时刻表。
// 轨迹上太阳中心在视地平下(计入标准大气折射)的部分画成灰色虚线。该地几何上看不到凌日、找不到凌日或选项无效时返回 false。
// TransitLocalSVG renders the topocentric solar-disk view of the Mercury or Venus transit closest to date, seen from
// longitude lon and latitude lat (east and north positive) at height metres: north up and east left as on the sky, with
// the planet-centre track, the planet disk at each contact and at greatest, and a contact table. Track parts where the
// Sun's centre is below the apparent horizon (standard refraction) are drawn grey and dashed. It returns false when the
// site sees no geometric transit, no transit is found or the options are invalid.
func TransitLocalSVG ( planet TransitPlanet , date time . Time , lon , lat , height float64 , options TransitLocalSVGOptions ) ( string , bool ) {
transit , ok := closestPlanetTransit ( planet , date )
if ! ok {
return "" , false
}
if options . TimeScale == astro . TimeScaleUT1 {
if options . Location != nil && options . Location != time . UTC {
return "" , false
}
options . Location = time . UTC
}
local := basic . PlanetTransitLocal ( transit , basic . Observer { Longitude : lon , Latitude : lat , Height : height })
if ! local . Valid {
return "" , false
}
if options . Step <= 0 {
options . Step = 2 * time . Minute
}
frames := basic . PlanetTransitLocalFrames ( transit , local , options . Step . Hours () / 24 )
if len ( frames ) < 2 {
return "" , false
}
options = normalizeTransitLocalSVGOptions ( date , options )
return renderTransitLocalSVG ( planet , local , frames , options ), true
}
func normalizeTransitLocalSVGOptions ( date time . Time , options TransitLocalSVGOptions ) TransitLocalSVGOptions {
if options . Width < 640 {
options . Width = transitLocalDefaultWidth
}
if options . Height < 480 {
options . Height = transitLocalDefaultHeight
}
if strings . EqualFold ( options . Language , "en" ) {
options . Language = "en"
} else {
options . Language = "zh"
}
if options . Location == nil {
options . Location = date . Location ()
}
return options
}
// transitLocalEvent 是日面上标注的一个接触或凌甚。
type transitLocalEvent struct {
label , name string
contact basic . PlanetTransitLocalContact
separation float64
}
func transitLocalEvents ( local basic . PlanetTransitLocalResult , language string ) [] transitLocalEvent {
names := [] string { "一触" , "二触" , "凌甚" , "三触" , "四触" }
if language == "en" {
names = [] string { "C1" , "C2" , "Greatest" , "C3" , "C4" }
}
external := local . SunSemidiameterArcsec + local . PlanetSemidiameterArcsec
internal := local . SunSemidiameterArcsec - local . PlanetSemidiameterArcsec
candidates := [] transitLocalEvent {
{ "C1" , names [ 0 ], local . ExternalIngress , external },
{ "C2" , names [ 1 ], local . InternalIngress , internal },
{ "G" , names [ 2 ], local . Greatest , local . MinimumSeparationArcsec },
{ "C3" , names [ 3 ], local . InternalEgress , internal },
{ "C4" , names [ 4 ], local . ExternalEgress , external },
}
events := candidates [: 0 ]
for _ , event := range candidates {
if event . contact . Valid {
events = append ( events , event )
}
}
return events
}
// transitLocalSunUp 判断太阳中心几何高度 altitude 是否在视地平上(标准大气折射)。
func transitLocalSunUp ( altitude float64 ) bool {
return altitude > - basic . RefractionFromApparentAltitude ( 0 , transitMapRefractionPressureHPa , transitMapRefractionTemperatureC )
}
func transitDisplayTime ( value time . Time , scale astro . TimeScale , location * time . Location ) time . Time {
if value . IsZero () {
return value
}
if scale == astro . TimeScaleUT1 {
return basic . UT1FromUTC ( value )
}
return value . In ( location )
}
func renderTransitLocalSVG (
planet TransitPlanet ,
local basic . PlanetTransitLocalResult ,
frames [] basic . PlanetTransitLocalFrame ,
options TransitLocalSVGOptions ,
) string {
width , height := float64 ( options . Width ), float64 ( options . Height )
at := func ( jd float64 ) time . Time {
return transitDisplayTime ( basic . JD2DateByZone ( jd , time . UTC , false ), options . TimeScale , options . Location )
}
title := options . Title
if title == "" {
date := at ( local . Greatest . JD ). Format ( "2006-01-02" )
if options . Language == "en" {
title = fmt . Sprintf ( "%s Transit of %s, Local Circumstances" , date , transitMapPlanetName ( planet , "en" ))
} else {
title = fmt . Sprintf ( "%s %s凌日站心图" , date , transitMapPlanetName ( planet , "zh" ))
}
}
site := transitLocalSiteText ( local . Observer , options . Language )
summary := transitLocalSummaryText ( local , options . Language )
var b strings . Builder
fmt . Fprintf ( & b , `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img" aria-label="%s">` ,
options . Width , options . Height , options . Width , options . Height , html . EscapeString ( title ))
b . WriteString ( `<defs><radialGradient id="transit-sun" cx="40%" cy="34%" r="64%"><stop offset="0%" stop-color="#fff8bb"/><stop offset="58%" stop-color="#ffd55f"/><stop offset="100%" stop-color="#f5a623"/></radialGradient></defs>` )
b . WriteString ( `<rect width="100%" height="100%" fill="#efefed"/>` )
fmt . Fprintf ( & b , `<rect x="22" y="18" width="%.3f" height="%.3f" fill="#ffffff" stroke="#c9c9c6" stroke-width="1.2"/>` , width - 44 , height - 36 )
titleText := title
if options . Title != "" {
titleText = svgchart . EllipsizeText ( title , width - 52 , 24 )
}
fmt . Fprintf ( & b , `<text x="%.3f" y="47" fill="#111111" font-family="Georgia, 'Times New Roman', serif" font-size="24" font-weight="700" text-anchor="middle">%s</text>` ,
width / 2 , html . EscapeString ( titleText ))
for index , line := range [] string { site , summary } {
2026-10-20 22:35:39 +08:00
line = svgchart . EllipsizeText ( line , width - 80 , 13 )
2026-10-15 23:54:15 +08:00
fmt . Fprintf ( & b , `<text x="%.3f" y="%.3f" fill="#333" font-family="Georgia, 'Times New Roman', serif" font-size="13" text-anchor="middle">%s</text>` ,
width / 2 , 72 + float64 ( index ) * 19 , html . EscapeString ( line ))
}
2026-10-20 22:35:39 +08:00
// 日面区占左侧,接触表占右侧;日面半径要容下盘外的东南西北轴标签,所以上下界由副标题与页脚算出。
2026-10-15 23:54:15 +08:00
panelWidth := 300.0
2026-10-20 22:35:39 +08:00
footerLines := transitLocalFooterLines ( local , options , width )
footerBaselines := transitLocalFooterBaselines ( len ( footerLines ), height )
footerTop := height - 30.0
if len ( footerBaselines ) > 0 {
footerTop = footerBaselines [ 0 ] - 10.2
}
const (
headerBottom = 72 + 19 + 10 // 经纬度行、摘要行与留白
axisLabelAbove = 33.0 // 盘顶到“北”字上沿
axisLabelBelow = 36.0 // 盘底到“南”字下沿
minimumDiskHeight = 150.0
)
top , bottom := headerBottom + axisLabelAbove , footerTop - axisLabelBelow
if bottom - top < minimumDiskHeight {
shrink := ( minimumDiskHeight - ( bottom - top )) / 2
top -= shrink
bottom += shrink
}
2026-10-15 23:54:15 +08:00
diskLeft , diskRight := 46.0 , width - panelWidth - 60
cx , cy := ( diskLeft + diskRight ) / 2 , ( top + bottom ) / 2
extent := local . SunSemidiameterArcsec + 2.2 * local . PlanetSemidiameterArcsec + 60
scale := math . Min (( diskRight - diskLeft ) / 2 , ( bottom - top ) / 2 ) / extent
mapX := func ( x float64 ) float64 { return cx - x * scale } // 东在左
mapY := func ( y float64 ) float64 { return cy - y * scale }
sunRadius := local . SunSemidiameterArcsec * scale
fmt . Fprintf ( & b , `<circle class="transit-sun" cx="%.3f" cy="%.3f" r="%.3f" fill="url(#transit-sun)" stroke="#c78211" stroke-width="1.4"/>` , cx , cy , sunRadius )
writeTransitLocalAxes ( & b , cx , cy , sunRadius , options . Language )
writeTransitLocalTrack ( & b , frames , mapX , mapY )
events := transitLocalEvents ( local , options . Language )
planetRadius := math . Max ( local . PlanetSemidiameterArcsec * scale , 2 )
table := & svgchart . LabelTable {}
2026-10-20 22:35:39 +08:00
table . Reserve ( 30 , footerTop - 4 , width - 60 , height - footerTop + 4 )
table . Reserve ( cx - 14 , cy + sunRadius + 16 , 28 , 18 )
table . Reserve ( cx - 14 , cy - sunRadius - 32 , 28 , 20 )
2026-10-15 23:54:15 +08:00
for _ , event := range events {
x , y := transitLocalEventPosition ( frames , event . contact . JD , mapX , mapY )
fill , stroke := "#1d1d1d" , "#000000"
if ! transitLocalSunUp ( event . contact . SunAltitudeDeg ) {
fill , stroke = "#9a9a9a" , "#6d6d6d"
}
fmt . Fprintf ( & b , `<circle class="transit-planet-%s" cx="%.3f" cy="%.3f" r="%.3f" fill="%s" fill-opacity="0.92" stroke="%s" stroke-width="0.8"/>` ,
strings . ToLower ( event . label ), x , y , planetRadius , fill , stroke )
table . Reserve ( x - planetRadius , y - planetRadius , 2 * planetRadius , 2 * planetRadius )
}
for _ , event := range events {
x , y := transitLocalEventPosition ( frames , event . contact . JD , mapX , mapY )
// 标签放在远离日心的一侧,避开日面内的轨迹与圆面。
dx , dy := x - cx , y - cy
norm := math . Hypot ( dx , dy )
if norm < 1e-6 {
dx , dy , norm = 0 , - 1 , 1
}
text := event . name + " " + at ( event . contact . JD ). Format ( "15:04:05" )
var placement svgchart . LabelPlacement
placed := false
// 相邻接触相隔仅数分钟时近处位次全被占,逐圈外推并画引线,不得丢标签。
for ring := 0 ; ring < 6 && ! placed ; ring ++ {
offset := planetRadius + 12 + float64 ( ring ) * 16
candidates := [] svgchart . LabelPlacement {}
for _ , turn := range [] float64 { 0 , 0.5 , - 0.5 , 1 , - 1 } {
ux := dx / norm * math . Cos ( turn ) - dy / norm * math . Sin ( turn )
uy := dx / norm * math . Sin ( turn ) + dy / norm * math . Cos ( turn )
anchor := "start"
if ux < - 0.3 {
anchor = "end"
} else if ux < 0.3 {
anchor = "middle"
}
candidate := svgchart . LabelPlacement { X : x + ux * offset , Y : y + uy * offset + 4 , Anchor : anchor }
// 标签平移进白色图框内、接触表左侧,不能伸出画面。
box := svgchart . LabelTextBox ( candidate . X , candidate . Y , 11 , text , anchor )
candidate . X += math . Max ( 0 , 30 - box . X ) - math . Max ( 0 , box . X + box . Width - ( width - panelWidth - 44 ))
2026-10-20 22:35:39 +08:00
candidate . Y += math . Max ( 0 , top - 8 - box . Y ) - math . Max ( 0 , box . Y + box . Height - ( footerTop - 4 ))
2026-10-15 23:54:15 +08:00
candidates = append ( candidates , candidate )
}
placement , placed = table . Place ( text , 11 , candidates )
if placed && transitLocalLabelGap ( placement , text , x , y ) > planetRadius + 16 {
// 引线连到标签框上离接触点最近的位置。
box := svgchart . LabelTextBox ( placement . X , placement . Y , 11 , text , placement . Anchor )
endX := math . Max ( box . X , math . Min ( x , box . X + box . Width ))
endY := math . Max ( box . Y , math . Min ( y , box . Y + box . Height ))
fmt . Fprintf ( & b , `<line class="transit-event-leader" x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#555" stroke-width="0.7" opacity="0.7"/>` ,
x , y , endX , endY )
}
}
if ! placed {
continue
}
fmt . Fprintf ( & b , `<text class="transit-event-label" x="%.3f" y="%.3f" fill="#222" stroke="#ffffff" stroke-width="2.6" paint-order="stroke" font-family="Arial, sans-serif" font-size="11" text-anchor="%s">%s</text>` ,
placement . X , placement . Y , placement . Anchor , html . EscapeString ( text ))
}
2026-10-20 22:35:39 +08:00
tableWidth := panelWidth
tableX := width - panelWidth - 36
if minimum := cx + sunRadius + 40 ; tableX < minimum {
tableX = minimum
tableWidth = width - tableX - 36
}
writeTransitLocalContactTable ( & b , local , events , at , tableX , top + 10 , tableWidth , options . Language )
writeTransitLocalFooter ( & b , footerLines , footerBaselines )
2026-10-15 23:54:15 +08:00
b . WriteString ( `</svg>` )
return b . String ()
}
// transitLocalLabelGap 是接触点到标签框的最近距离。
func transitLocalLabelGap ( placement svgchart . LabelPlacement , text string , x , y float64 ) float64 {
box := svgchart . LabelTextBox ( placement . X , placement . Y , 11 , text , placement . Anchor )
return math . Hypot ( x - math . Max ( box . X , math . Min ( x , box . X + box . Width )), y - math . Max ( box . Y , math . Min ( y , box . Y + box . Height )))
}
func transitLocalSiteText ( observer basic . Observer , language string ) string {
ew , ns := "E" , "N"
if language != "en" {
ew , ns = "东经" , "北纬"
}
lon , lat := observer . Longitude , observer . Latitude
if lon < 0 {
ew = map [ bool ] string { true : "W" , false : "西经" }[ language == "en" ]
lon = - lon
}
if lat < 0 {
ns = map [ bool ] string { true : "S" , false : "南纬" }[ language == "en" ]
lat = - lat
}
if language == "en" {
return fmt . Sprintf ( "Site %.4f°%s %.4f°%s, height %.0f m" , lon , ew , lat , ns , observer . Height )
}
return fmt . Sprintf ( "观测地 %s %.4f° %s %.4f°,海拔 %.0f 米" , ew , lon , ns , lat , observer . Height )
}
func transitLocalSummaryText ( local basic . PlanetTransitLocalResult , language string ) string {
kind := map [ bool ] string { true : "完整凌日" , false : "掠凌(无内切接触)" }[ local . HasInternal ]
visible := map [ bool ] string { true : "可见" , false : "全程太阳在地平下" }[ local . Visible ]
if language == "en" {
kind = map [ bool ] string { true : "full transit" , false : "grazing transit (no internal contacts)" }[ local . HasInternal ]
visible = map [ bool ] string { true : "visible" , false : "Sun below the horizon throughout" }[ local . Visible ]
return fmt . Sprintf ( "Topocentric %s, %s | minimum separation %.1f″ | Sun %.1f″ | planet %.2f″" ,
kind , visible , local . MinimumSeparationArcsec , local . SunSemidiameterArcsec , local . PlanetSemidiameterArcsec )
}
return fmt . Sprintf ( "站心%s, %s | 最小角距 %.1f″ | 太阳视半径 %.1f″ | 行星视半径 %.2f″" ,
kind , visible , local . MinimumSeparationArcsec , local . SunSemidiameterArcsec , local . PlanetSemidiameterArcsec )
}
// writeTransitLocalAxes 画过日心的南北、东西虚线与方向字。
func writeTransitLocalAxes ( b * strings . Builder , cx , cy , radius float64 , language string ) {
2026-10-20 22:35:39 +08:00
reach := radius + 12
2026-10-15 23:54:15 +08:00
fmt . Fprintf ( b , `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#8b6a2b" stroke-width="0.8" stroke-dasharray="3 4" stroke-opacity="0.7"/>` , cx , cy - reach , cx , cy + reach )
fmt . Fprintf ( b , `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#8b6a2b" stroke-width="0.8" stroke-dasharray="3 4" stroke-opacity="0.7"/>` , cx - reach , cy , cx + reach , cy )
labels := [ 4 ] string { "北" , "南" , "东" , "西" }
if language == "en" {
labels = [ 4 ] string { "N" , "S" , "E" , "W" }
}
for index , item := range [] struct { x , y float64 }{
{ cx , cy - reach - 6 }, { cx , cy + reach + 16 }, { cx - reach - 12 , cy + 5 }, { cx + reach + 12 , cy + 5 },
} {
fmt . Fprintf ( b , `<text x="%.3f" y="%.3f" fill="#6b5222" font-family="Georgia, 'Times New Roman', serif" font-size="13" text-anchor="middle">%s</text>` ,
item . x , item . y , labels [ index ])
}
}
// writeTransitLocalTrack 画行星中心轨迹:太阳在视地平上的段为实线,地平下为灰色虚线。
func writeTransitLocalTrack ( b * strings . Builder , frames [] basic . PlanetTransitLocalFrame , mapX , mapY func ( float64 ) float64 ) {
start := 0
for index := 1 ; index <= len ( frames ); index ++ {
if index < len ( frames ) && transitLocalSunUp ( frames [ index ]. SunAltitudeDeg ) == transitLocalSunUp ( frames [ start ]. SunAltitudeDeg ) {
continue
}
// 段尾取下一段首点,相邻两段首尾相接。
end := index
if end == len ( frames ) {
end --
}
class , color , dash := "transit-track" , "#24518a" , "none"
if ! transitLocalSunUp ( frames [ start ]. SunAltitudeDeg ) {
class , color , dash = "transit-track-below-horizon" , "#8a8a8a" , "5 4"
}
fmt . Fprintf ( b , `<path class="%s" d="` , class )
for position := start ; position <= end ; position ++ {
command := "L"
if position == start {
command = "M"
}
fmt . Fprintf ( b , `%s %.3f %.3f ` , command , mapX ( frames [ position ]. PlanetXArcsec ), mapY ( frames [ position ]. PlanetYArcsec ))
}
fmt . Fprintf ( b , `" fill="none" stroke="%s" stroke-width="1.4" stroke-dasharray="%s" stroke-linecap="round"/>` , color , dash )
start = index
}
}
// transitLocalEventPosition 取时刻最接近 jd 的采样帧位置;接触与凌甚时刻本身在采样里。
func transitLocalEventPosition ( frames [] basic . PlanetTransitLocalFrame , jd float64 , mapX , mapY func ( float64 ) float64 ) ( float64 , float64 ) {
best := 0
for index := range frames {
if math . Abs ( frames [ index ]. JD - jd ) < math . Abs ( frames [ best ]. JD - jd ) {
best = index
}
}
return mapX ( frames [ best ]. PlanetXArcsec ), mapY ( frames [ best ]. PlanetYArcsec )
}
func writeTransitLocalContactTable (
b * strings . Builder ,
local basic . PlanetTransitLocalResult ,
events [] transitLocalEvent ,
at func ( float64 ) time . Time ,
x , y , width float64 ,
language string ,
) {
heading := [ 4 ] string { "接触时刻" , "时刻" , "太阳高度" , "方位" }
if language == "en" {
heading = [ 4 ] string { "Contacts" , "Time" , "Sun alt." , "Az." }
}
fmt . Fprintf ( b , `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="15" font-weight="700">%s</text>` , x , y , heading [ 0 ])
columns := [] float64 { x , x + 78 , x + 176 , x + 252 }
rowY := y + 26
for index , text := range [] string { "" , heading [ 1 ], heading [ 2 ], heading [ 3 ]} {
fmt . Fprintf ( b , `<text x="%.3f" y="%.3f" fill="#555" font-family="Arial, sans-serif" font-size="11">%s</text>` , columns [ index ], rowY , text )
}
fmt . Fprintf ( b , `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#bbb" stroke-width="0.8"/>` , x , rowY + 6 , x + width - 8 , rowY + 6 )
for _ , event := range events {
rowY += 22
fill := "#222"
if ! transitLocalSunUp ( event . contact . SunAltitudeDeg ) {
fill = "#8a8a8a"
}
cells := [] string {
event . name ,
at ( event . contact . JD ). Format ( "15:04:05" ),
fmt . Sprintf ( "%+.1f°" , event . contact . SunAltitudeDeg ),
fmt . Sprintf ( "%.1f°" , event . contact . SunAzimuthDeg ),
}
for index , text := range cells {
fmt . Fprintf ( b , `<text class="transit-contact-row" x="%.3f" y="%.3f" fill="%s" font-family="Arial, sans-serif" font-size="12">%s</text>` ,
columns [ index ], rowY , fill , html . EscapeString ( text ))
}
}
rowY += 30
lines := [] string {}
duration := time . Duration (( local . ExternalEgress . JD - local . ExternalIngress . JD ) * 86400 * float64 ( time . Second )). Round ( time . Second )
if language == "en" {
lines = append ( lines , "Duration C1– C4 " + duration . String ())
} else {
lines = append ( lines , "一触至四触历时 " + duration . String ())
}
for _ , item := range [] struct {
jd float64
zh , en string
}{{ local . Sunrise , "凌日中日出 " , "Sunrise during transit " }, { local . Sunset , "凌日中日落 " , "Sunset during transit " }} {
if item . jd == 0 {
continue
}
label := item . zh
if language == "en" {
label = item . en
}
lines = append ( lines , label + at ( item . jd ). Format ( "15:04:05" ))
}
for _ , line := range lines {
fmt . Fprintf ( b , `<text x="%.3f" y="%.3f" fill="#333" font-family="Arial, sans-serif" font-size="12">%s</text>` , x , rowY , html . EscapeString ( line ))
rowY += 19
}
}
2026-10-20 22:35:39 +08:00
func transitLocalFooterLines ( local basic . PlanetTransitLocalResult , options TransitLocalSVGOptions , width float64 ) [] string {
2026-10-15 23:54:15 +08:00
note := options . FooterNote
if note == "" {
if options . Language == "en" {
note = "Sky view, north up and east left. Topocentric contacts; Sun altitudes are geometric centre altitudes, grey parts have the Sun's centre below the apparent horizon (standard refraction)."
} else {
2026-10-20 22:35:39 +08:00
note = "天空视向,上北左东;站心接触时刻,太阳高度为太阳中心几何高度,灰色部分太阳中心在视地平下(计入标准大气折射)。"
2026-10-15 23:54:15 +08:00
}
}
scaleNote := timenote . Scale ( options . TimeScale , basic . JD2DateByZone ( local . Greatest . JD , time . UTC , false ), options . Location , options . Language )
2026-10-20 22:35:39 +08:00
return svgchart . FooterLinesWithScale ( svgchart . WrapText ( note , width - 80 , 12 ), scaleNote , width - 80 , 12 , 3 )
}
func transitLocalFooterBaselines ( lineCount int , height float64 ) [] float64 {
baselines , _ := svgchart . FooterBlock ( lineCount , height , 12 , svgchart . FooterLineHeight ( 12 ), svgchart . FooterBottomPadding ( 12 ) + 18 )
return baselines
}
func writeTransitLocalFooter ( b * strings . Builder , lines [] string , baselines [] float64 ) {
2026-10-15 23:54:15 +08:00
for index , line := range lines {
fmt . Fprintf ( b , `<text x="40" y="%.3f" fill="#555" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>` ,
baselines [ index ], html . EscapeString ( line ))
}
}