using CRSim.Core.Abstractions; using CRSim.Core.Models; using CRSim.Core.Models.PlatformDiagram; using iText.IO.Font; using iText.Kernel.Colors; using iText.Kernel.Font; using iText.Kernel.Geom; using iText.Kernel.Pdf; using iText.Kernel.Pdf.Canvas; using iText.Layout; using iText.Layout.Element; using iText.Layout.Properties; using System.Reflection; namespace CRSim.PlatformDiagram { public class Generator { public static void Generate(Station station, string savePath, float pageWidth,List trainColors) { float Padding = 40; float HeaderHeight = 16; float PlatformTableVerticalMargin = 9; float PlatformTableHorizontalMargin = 10; float TimelineHeight = 10; float TimelineExtLength = 4; float PlatformHeight = 55; float InfoBarWidth = 50; PdfWriter writer = new(savePath); PdfDocument pdf = new(writer); PdfFont font = PdfFontFactory.CreateFont("C:/Windows/Fonts/simsun.ttc,1", PdfEncodings.IDENTITY_H); int platformCount = station.Platforms.Count; float pageHeight = (Padding + TimelineHeight + PlatformTableVerticalMargin) * 2 + platformCount * PlatformHeight + HeaderHeight; Document doc = new(pdf, new PageSize(pageWidth, pageHeight)); doc.SetMargins(Padding, Padding, Padding, Padding); var canvas = new PdfCanvas(pdf.AddNewPage()); TimeSpan startTime = TimeSpan.FromHours(0); TimeSpan endTime = TimeSpan.FromHours(24); #region 时间线 int intervalMin = 10; Color lineColor = new DeviceRgb(0xAA, 0xAA, 0x7E); canvas.SetStrokeColor(lineColor); // 绘制整条横线 canvas.SetLineWidth(1f); var xStart = Padding + InfoBarWidth + PlatformTableHorizontalMargin - TimelineExtLength; var xEnd = pageWidth - Padding; var yTop = pageHeight - Padding - HeaderHeight - TimelineHeight; var yBottom = Padding + TimelineHeight; canvas.MoveTo(xStart, yTop).LineTo(xEnd, yTop).Stroke(); canvas.MoveTo(xStart, yBottom).LineTo(xEnd, yBottom).Stroke(); // 画线和时间文字 var xBase = Padding + InfoBarWidth + PlatformTableHorizontalMargin; yTop = pageHeight - Padding - HeaderHeight - TimelineHeight - PlatformTableVerticalMargin; yBottom = Padding + TimelineHeight + PlatformTableVerticalMargin; var PlatformTableLength = pageWidth - Padding * 2 - InfoBarWidth - PlatformTableHorizontalMargin - TimelineExtLength; for (TimeSpan t = startTime; t <= endTime; t += TimeSpan.FromMinutes(intervalMin)) { float x = xBase + MapTime(t, startTime, endTime, PlatformTableLength); // 设置竖线样式 if (t.Minutes % 60 == 0) { canvas.SetLineWidth(1f); canvas.SetLineDash(0); } else if (t.Minutes % 30 == 0) { canvas.SetLineWidth(0.5f); canvas.SetLineDash(0); } else // 10分钟 { canvas.SetLineWidth(0.5f); canvas.SetLineDash(3, 3); } // 竖线从下横线到上横线 canvas.MoveTo(x, yBottom).LineTo(x, yTop).Stroke(); static void DrawLabel(PdfCanvas canvas, PdfFont font, string text, float x, float y, float width, float height, Color color, bool alignBottom) { // 获取文字度量信息 float fontSize = text != "30" ? 14 : 10; float textWidth = font.GetWidth(text, fontSize); float ascent = font.GetAscent(text, fontSize); float descent = font.GetDescent(text, fontSize); float textHeight = ascent - descent; // 水平居中 float textX = x + (width - textWidth) / 2; // 垂直位置 float textY; if (alignBottom) { textY = y - descent; } else { textY = y + height - ascent; } // 绘制文字 canvas.BeginText(); canvas.SetFontAndSize(font, fontSize); canvas.SetFillColor(color); canvas.MoveText(textX, textY); canvas.ShowText(text); canvas.EndText(); } if (t.Minutes % 60 == 0) // 整点显示小时 { string label = $"{t.Hours:D1}"; DrawLabel(canvas, font, label, x - 10, yTop + PlatformTableVerticalMargin + 2, 20, TimelineHeight, lineColor, alignBottom: true); DrawLabel(canvas, font, label, x - 10, yBottom - PlatformTableVerticalMargin - TimelineHeight - 2, 20, TimelineHeight, lineColor, alignBottom: false); } else if (t.Minutes % 30 == 0) // 半点显示“30” { DrawLabel(canvas, font, "30", x - 10, yTop + PlatformTableVerticalMargin + 2, 20, TimelineHeight, lineColor, alignBottom: true); DrawLabel(canvas, font, "30", x - 10, yBottom - PlatformTableVerticalMargin - TimelineHeight - 2, 20, TimelineHeight, lineColor, alignBottom: false); } } // 1. 初始化字典(保持不变) Dictionary platformHeights = station.TrainStops .Select(x => x.Platform) .Where(p => p != null) .Distinct() .ToDictionary(p => p!, p => 0f); // 2. 绘制底线(保持不变) canvas.MoveTo(xBase, yBottom).LineTo(xEnd - TimelineExtLength, yBottom).Stroke(); // 3. 遍历所有站台,按索引分配高度 for (int i = 0; i < platformCount; i++) { var platform = station.Platforms[i]; string name = platform.Name; // 计算当前站台的 Y 坐标高度 // 逻辑:i 越大(后面的站台),高度越高 float currentY = yBottom + (platformCount - i - 1) * PlatformHeight; // 直接存入字典,不再判断是否为数字 if (platformHeights.ContainsKey(name)) { platformHeights[name] = currentY; } // 绘制站台分隔线 canvas.SetLineWidth(1f); canvas.MoveTo(xBase, currentY) .LineTo(xEnd - TimelineExtLength, currentY) .Stroke(); } canvas.SetLineWidth(1f); canvas.MoveTo(xBase, yBottom + platformCount * PlatformHeight) .LineTo(xEnd - TimelineExtLength, yBottom + platformCount * PlatformHeight) .Stroke(); #endregion #region 列车 foreach (var train in station.TrainStops) { float barHeight = PlatformHeight / 2.5f - 4; TimeSpan fixedDuration = TimeSpan.FromMinutes(2); if (train.Platform is null || !platformHeights.TryGetValue(train.Platform, out var yBottom1)) continue; TimeSpan arrival = (TimeSpan)(train.ArrivalTime ?? train.DepartureTime - fixedDuration); TimeSpan departure = (TimeSpan)(train.DepartureTime ?? train.ArrivalTime + fixedDuration); var matchedColor = trainColors .Where(tc => train.Number.StartsWith(tc.Prefix)) .OrderByDescending(tc => tc.Prefix.Length) .FirstOrDefault(); var color = matchedColor?.Color ?? trainColors.FirstOrDefault(x => x.Prefix == "默认").Color; Color itextColor = new DeviceRgb(color.R, color.G, color.B); canvas.SetFillColor(itextColor); canvas.SetStrokeColor(itextColor); // 检查是否跨零点 if (departure < arrival) { float xStart1 = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(arrival, startTime, endTime, PlatformTableLength); float xEnd1 = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(TimeSpan.FromHours(24), startTime, endTime, PlatformTableLength); canvas.RoundRectangle(xStart1, yBottom1 + 4, xEnd1 - xStart1, barHeight, 3f); var rect = new Rectangle(xStart1, yBottom1 + 4 + barHeight, 150, PlatformHeight - barHeight - 8); var pdfCanvas = new Canvas(canvas, rect); string timeStr = train.ArrivalTime.HasValue && train.DepartureTime.HasValue ? $"{train.ArrivalTime.Value.Minutes} {train.DepartureTime.Value.Minutes}" : train.ArrivalTime?.Minutes.ToString() ?? train.DepartureTime?.Minutes.ToString() ?? ""; pdfCanvas.Add(new Paragraph($"{train.Number}\n{train.Origin}->{train.Terminal}\n{timeStr}") .SetFont(font) .SetFontSize(8) .SetMultipliedLeading(1f) .SetMargins(0, 0, 0, 0)); canvas.Fill(); float xStart2 = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(TimeSpan.Zero, startTime, endTime, PlatformTableLength); float xEnd2 = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(departure, startTime, endTime, PlatformTableLength); canvas.RoundRectangle(xStart2, yBottom1 + 4, xEnd2 - xStart2, barHeight, 3f); canvas.Fill(); } else { xStart = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(arrival, startTime, endTime, PlatformTableLength); xEnd = Padding + InfoBarWidth + PlatformTableHorizontalMargin + MapTime(departure, startTime, endTime, PlatformTableLength); canvas.RoundRectangle(xStart, yBottom1 + 4, xEnd - xStart, barHeight, 3f); var rect = new Rectangle(xStart, yBottom1 + 4 + barHeight, 150, PlatformHeight - barHeight - 8); var pdfCanvas = new Canvas(canvas, rect); string timeStr = train.ArrivalTime.HasValue && train.DepartureTime.HasValue ? $"{train.ArrivalTime.Value.Minutes} {train.DepartureTime.Value.Minutes}" : train.ArrivalTime?.Minutes.ToString() ?? train.DepartureTime?.Minutes.ToString() ?? ""; pdfCanvas.Add(new Paragraph($"{train.Number + (train.ArrivalTime.HasValue ? "" : "*") + (train.DepartureTime.HasValue ? "" : "#")}\n{train.Origin}->{train.Terminal}\n{timeStr}") .SetFont(font) .SetFontSize(8) .SetMultipliedLeading(1f) .SetMargins(0,0,0,0)); canvas.Fill(); } } #endregion #region 基本信息 Rectangle rect1 = new(Padding, pageHeight - Padding, 500, HeaderHeight); var layoutCanvas = new Canvas(canvas, rect1); layoutCanvas.Add(new Paragraph($"{station.Name}站台占用示意图") .SetFont(font) .SetFontSize(18) .SetFontColor(ColorConstants.BLUE) .SetMargins(0,0,0,0)); string text = $"使用 CRSim v{Assembly.GetExecutingAssembly().GetName().Version} 绘制"; float fontSize = 12f; var para = new Paragraph(text) .SetFont(font) .SetFontSize(fontSize) .SetFontColor(ColorConstants.BLUE) .SetMargins(0, 0, 0, 0); rect1 = new(pageWidth - Padding - font.GetWidth(text, fontSize), pageHeight - Padding, 500, HeaderHeight); layoutCanvas = new Canvas(canvas, rect1); layoutCanvas.Add(para); foreach (var kvp in platformHeights) { rect1 = new(Padding, kvp.Value + PlatformHeight / 2 - 10, InfoBarWidth, 20); layoutCanvas = new Canvas(canvas, rect1); layoutCanvas.Add(new Paragraph($"{kvp.Key}站台") .SetFont(font) .SetFontSize(14) .SetMargins(0, 0, 0, 0) .SetFontColor(ColorConstants.BLUE) .SetTextAlignment(TextAlignment.CENTER) .SetVerticalAlignment(VerticalAlignment.MIDDLE)); } #endregion doc.Close(); } static float MapTime(TimeSpan t, TimeSpan start, TimeSpan end, float width) { double totalMinutes = (end - start).TotalMinutes; double posMinutes = (t - start).TotalMinutes; return (float)(posMinutes / totalMinutes * width); } } }