// Package svgchart 提供日月食与月掩星详细版式共用的数据块与版面计算。 // Package svgchart holds the data blocks and page geometry shared by the detailed eclipse and occultation charts. package svgchart import ( "fmt" "html" "math" "strings" ) // PanelRow 是数据块里的一行;Detail 非空时在数值下方补一行灰色小字。 type PanelRow struct { Label string Value string Detail string } // PanelBlock 是一个带标题的数据块。 type PanelBlock struct { Title string Rows []PanelRow } // WritePanelBox 画一个数据块:标题加若干行“标签 + 数值”。 // 行高按行数等分,因此块高变化时不会溢出;Class 用于区分各族图的样式钩子。 func WritePanelBox( b *strings.Builder, class string, x, y, width, height float64, title string, rows []PanelRow, ) { if len(rows) == 0 || height <= 0 { return } fmt.Fprintf(b, ``, class) fmt.Fprintf(b, ``, x, y, width, height) fmt.Fprintf(b, `%s`, x+12, y+20, html.EscapeString(title)) rowHeight := (height - panelBoxHeaderHeight) / float64(len(rows)) for index, row := range rows { if row.Label == "" { continue } rowY := y + panelBoxHeaderHeight + float64(index)*rowHeight size := PanelRowFontSize(width, row) fmt.Fprintf(b, `%s`, x+panelBoxPadding, rowY+11, size, html.EscapeString(row.Label)) fmt.Fprintf(b, `%s`, x+width-panelBoxPadding, rowY+11, size, html.EscapeString(row.Value)) if row.Detail != "" { fmt.Fprintf(b, `%s`, x+panelBoxPadding, rowY+22, panelRowDetailSize, html.EscapeString(row.Detail)) } } b.WriteString(``) } const ( panelBoxHeaderHeight = 30 panelRowLabelSize = 10.5 panelRowDetailSize = 9 panelBoxPadding = 12 panelLabelValueGap = 8 panelMinimumFontSize = 8 ) // EstimatedTextWidth 按字号估算文本宽度:ASCII 取 0.62em,非 ASCII(含全角中文)取 1.05em。 // 没有字体度量可用,这里只服务于压叠判定,估值偏大以保证判定保守。 // EstimatedTextWidth estimates text width from the font size: 0.62em per ASCII rune and 1.05em // otherwise. It exists only for overlap checks, so it deliberately over-estimates. func EstimatedTextWidth(text string, fontSize float64) float64 { units := 0.0 for _, symbol := range text { if symbol < 128 { units += 0.62 continue } units += 1.05 } return units * fontSize } // EstimatedTextExtents 返回文本相对基线的上下留白,用于构造占位矩形。 // EstimatedTextExtents returns the space a text keeps above and below its baseline. func EstimatedTextExtents(fontSize float64) (above, below float64) { return 0.85 * fontSize, 0.30 * fontSize } // PanelRowFontSize 返回该行“标签 + 数值”放不下时等比缩小后的字号;缩小到下限仍放不下说明块太窄。 // PanelRowFontSize shrinks a row's font until its label and value fit side by side. func PanelRowFontSize(width float64, row PanelRow) float64 { content := width - 2*panelBoxPadding if content <= 0 { return panelMinimumFontSize } pair := EstimatedTextWidth(row.Label, panelRowLabelSize) + EstimatedTextWidth(row.Value, panelRowLabelSize) if pair+panelLabelValueGap <= content { return panelRowLabelSize } size := panelRowLabelSize * (content - panelLabelValueGap) / pair if size < panelMinimumFontSize { return panelMinimumFontSize } return size } func panelTextRect(x, baseline, fontSize float64, text string, anchorEnd bool) panelTextBox { width := EstimatedTextWidth(text, fontSize) left := x if anchorEnd { left = x - width } above, below := EstimatedTextExtents(fontSize) return panelTextBox{x: left, y: baseline - above, width: width, height: above + below} } // panelTextBox 是文本的保守包围盒。 type panelTextBox struct { x, y, width, height float64 } func panelTextBoxesOverlap(first, second panelTextBox) bool { left, right := math.Max(first.x, second.x), math.Min(first.x+first.width, second.x+second.width) top, bottom := math.Max(first.y, second.y), math.Min(first.y+first.height, second.y+second.height) return right-left > 0.5 && bottom-top > 0 } // PanelBoxOverlaps 报告该块在给定位次与尺寸下,块内文本是否会互相压叠。 // 行高按行数等分,所以块太矮时行距会小于文字高度,此时必须由调用方拒绝该画布而不是画出压叠的文字。 // PanelBoxOverlaps reports whether the block's own texts would collide at the given box; callers must // reject a canvas whose panels cannot keep the rows apart. func PanelBoxOverlaps(x, y, width, height float64, title string, rows []PanelRow) bool { if len(rows) == 0 || height <= 0 { return false } boxes := make([]panelTextBox, 0, 2*len(rows)+1) boxes = append(boxes, panelTextRect(x+12, y+20, 12, title, false)) rowHeight := (height - panelBoxHeaderHeight) / float64(len(rows)) for index, row := range rows { if row.Label == "" { continue } rowY := y + panelBoxHeaderHeight + float64(index)*rowHeight size := PanelRowFontSize(width, row) boxes = append(boxes, panelTextRect(x+panelBoxPadding, rowY+11, size, row.Label, false)) boxes = append(boxes, panelTextRect(x+width-panelBoxPadding, rowY+11, size, row.Value, true)) if row.Detail != "" { boxes = append(boxes, panelTextRect(x+panelBoxPadding, rowY+22, panelRowDetailSize, row.Detail, false)) } } for first := 0; first < len(boxes); first++ { for second := first + 1; second < len(boxes); second++ { if panelTextBoxesOverlap(boxes[first], boxes[second]) { return true } } } return false }