From 4f031e6cdc1a27ec5144659e55e1e6f2d9105e6d Mon Sep 17 00:00:00 2001
From: denglihong2007 <98096191+denglihong2007@users.noreply.github.com>
Date: Sun, 12 Oct 2025 00:32:33 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AB=99=E5=8F=B0=E5=8D=A0=E7=94=A8?=
=?UTF-8?q?=E7=A4=BA=E6=84=8F=E5=9B=BE=E7=94=9F=E6=88=90=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CRSim.PlatformDiagram.csproj | 19 ++
CRSim.PlatformDiagram/PlatformDiagram.cs | 246 ++++++++++++++++++
CRSim.sln | 6 +
CRSim/App.xaml.cs | 9 +-
CRSim/CRSim.csproj | 4 +
CRSim/MainWindow.xaml | 5 +
.../PlatformDiagramPageViewModel.cs | 65 +++++
CRSim/Views/PlatformDiagramPage.xaml | 42 +++
CRSim/Views/PlatformDiagramPage.xaml.cs | 17 ++
9 files changed, 410 insertions(+), 3 deletions(-)
create mode 100644 CRSim.PlatformDiagram/CRSim.PlatformDiagram.csproj
create mode 100644 CRSim.PlatformDiagram/PlatformDiagram.cs
create mode 100644 CRSim/ViewModels/PlatformDiagramPageViewModel.cs
create mode 100644 CRSim/Views/PlatformDiagramPage.xaml
create mode 100644 CRSim/Views/PlatformDiagramPage.xaml.cs
diff --git a/CRSim.PlatformDiagram/CRSim.PlatformDiagram.csproj b/CRSim.PlatformDiagram/CRSim.PlatformDiagram.csproj
new file mode 100644
index 0000000..6cdcf5d
--- /dev/null
+++ b/CRSim.PlatformDiagram/CRSim.PlatformDiagram.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net9.0-windows10.0.19041.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim.PlatformDiagram/PlatformDiagram.cs b/CRSim.PlatformDiagram/PlatformDiagram.cs
new file mode 100644
index 0000000..463c888
--- /dev/null
+++ b/CRSim.PlatformDiagram/PlatformDiagram.cs
@@ -0,0 +1,246 @@
+using CRSim.Core.Models;
+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;
+
+namespace CRSim.PlatformDiagram
+{
+ public class Generator
+ {
+ public static void Generate(Station station, string savePath)
+ {
+ float PageWidth = 5000;
+ 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);
+ }
+ }
+
+ Dictionary platformHeights = station.TrainStops
+ .Select(x => x.Platform)
+ .Where(p => p != null)
+ .Distinct()
+ .ToDictionary(p => p!, p => 0f);
+ canvas.MoveTo(xBase, yBottom).LineTo(xEnd - TimelineExtLength, yBottom).Stroke();
+ for (int i = 0; i < platformCount; i++)
+ {
+ string name = station.Platforms[i].Name;
+ if (double.TryParse(name, out double platformNumber))
+ {
+ platformHeights[name] = yBottom + (platformCount - i - 1f) * PlatformHeight;
+ }
+ canvas.SetLineWidth(1f);
+ canvas.MoveTo(xBase, yBottom + (platformCount - i) * PlatformHeight)
+ .LineTo(xEnd - TimelineExtLength, yBottom + (platformCount - i) * 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 trainColors = new Dictionary
+ {
+ { "G", "FF00BE" }, // pink
+ { "D", "004C99" }, // deep blue
+ { "C", "009999" }, // cyan
+ { "T", "0000FF" }, // blue
+ { "Z", "804000" }, // brown
+ { "K", "FF0000" }, // red
+ { "Y", "FF0000" }, // red
+ { "L", "008000" }, // dark green
+ { "default", "008000" } // dark green
+ };
+ string firstChar = train.Number[..1].ToUpper();
+ string colorHex = trainColors.TryGetValue(firstChar, out string? value) ? value : trainColors["default"];
+ var color = new DeviceRgb(
+ Convert.ToInt32(colorHex[..2], 16),
+ Convert.ToInt32(colorHex.Substring(2, 2), 16),
+ Convert.ToInt32(colorHex.Substring(4, 2), 16)
+ );
+ canvas.SetFillColor(color);
+ canvas.SetStrokeColor(color);
+
+ // 检查是否跨零点
+ 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));
+ 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);
+ }
+ }
+}
diff --git a/CRSim.sln b/CRSim.sln
index c807cdb..fe707a7 100644
--- a/CRSim.sln
+++ b/CRSim.sln
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSim", "CRSim\CRSim.csproj
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSim.ScreenSimulator", "CRSim.ScreenSimulator\CRSim.ScreenSimulator.csproj", "{94927020-7AD7-4F6F-A456-F34A8E0BC40A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSim.PlatformDiagram", "CRSim.PlatformDiagram\CRSim.PlatformDiagram.csproj", "{895EE8B3-1A15-4CA8-9AFE-246585732AA5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -29,6 +31,10 @@ Global
{94927020-7AD7-4F6F-A456-F34A8E0BC40A}.Debug|x64.Build.0 = Debug|x64
{94927020-7AD7-4F6F-A456-F34A8E0BC40A}.Release|x64.ActiveCfg = Release|x64
{94927020-7AD7-4F6F-A456-F34A8E0BC40A}.Release|x64.Build.0 = Release|x64
+ {895EE8B3-1A15-4CA8-9AFE-246585732AA5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {895EE8B3-1A15-4CA8-9AFE-246585732AA5}.Debug|x64.Build.0 = Debug|Any CPU
+ {895EE8B3-1A15-4CA8-9AFE-246585732AA5}.Release|x64.ActiveCfg = Release|Any CPU
+ {895EE8B3-1A15-4CA8-9AFE-246585732AA5}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CRSim/App.xaml.cs b/CRSim/App.xaml.cs
index 3d11e1e..5ef7982 100644
--- a/CRSim/App.xaml.cs
+++ b/CRSim/App.xaml.cs
@@ -1,4 +1,5 @@
-using CRSim.WebsiteSimulator;
+using CRSim.PlatformDiagram;
+using CRSim.WebsiteSimulator;
using System.Diagnostics;
namespace CRSim
@@ -43,14 +44,16 @@ namespace CRSim
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
services.AddTransient();
- services.AddTransient();
PluginService.InitializePlugins(context, services, parsedOptions.ExternalPluginPath);
})
.Build();
diff --git a/CRSim/CRSim.csproj b/CRSim/CRSim.csproj
index 0c5622f..eeee04d 100644
--- a/CRSim/CRSim.csproj
+++ b/CRSim/CRSim.csproj
@@ -56,6 +56,7 @@
+
@@ -77,6 +78,9 @@
Designer
+
+ Designer
+
Designer
diff --git a/CRSim/MainWindow.xaml b/CRSim/MainWindow.xaml
index d3f8001..99e7808 100644
--- a/CRSim/MainWindow.xaml
+++ b/CRSim/MainWindow.xaml
@@ -49,6 +49,11 @@
+
+
+
+
+
diff --git a/CRSim/ViewModels/PlatformDiagramPageViewModel.cs b/CRSim/ViewModels/PlatformDiagramPageViewModel.cs
new file mode 100644
index 0000000..bed237d
--- /dev/null
+++ b/CRSim/ViewModels/PlatformDiagramPageViewModel.cs
@@ -0,0 +1,65 @@
+using CRSim.PlatformDiagram;
+
+namespace CRSim.ViewModels;
+
+public partial class PlatformDiagramPageViewModel(IDialogService _dialogService, IDatabaseService _databaseService, Generator _generator) : ObservableObject
+{
+ public string PageTitle = "生成站台占用图";
+ public Station? SelectedStation;
+ public List Stations => _databaseService.GetAllStations();
+
+ [ObservableProperty]
+ public partial bool IsSelected { get; set; } = false;
+
+ [RelayCommand]
+ public void StationSelected(object s)
+ {
+ if(s is Station station)
+ {
+ SelectedStation = station;
+ IsSelected = true;
+ }
+ }
+
+ [RelayCommand]
+ public async Task Generate()
+ {
+ if(!CheckStation(SelectedStation,out string detail))
+ {
+ await _dialogService.ShowTextAsync("警告", detail);
+ }
+ var path = _dialogService.SaveFile(".pdf", $"{SelectedStation.Name}站台占用图");
+ if(string.IsNullOrEmpty(path) || SelectedStation == null) return;
+ await Task.Run(() => Generator.Generate(SelectedStation, path));
+ }
+
+ public static bool CheckStation(Station station,out string detail)
+ {
+ detail = string.Empty;
+ var trainStops = station.TrainStops;
+ var platformGroups = trainStops
+ .Where(ts => !string.IsNullOrEmpty(ts.Platform) && ts.DepartureTime != null && ts.ArrivalTime != null)
+ .GroupBy(ts => ts.Platform);
+ foreach (var group in platformGroups)
+ {
+ var stops = group.OrderBy(ts => ts.ArrivalTime ?? TimeSpan.Zero).ToList();
+ for (int i = 0; i < stops.Count - 1; i++)
+ {
+ var current = stops[i];
+ var next = stops[i + 1];
+ // 判断时间是否重叠
+ if (current.DepartureTime != null && next.ArrivalTime != null &&
+ current.DepartureTime > next.ArrivalTime)
+ {
+ detail += $"\n站台 {group.Key} 车次 {current.Number} ({current.ArrivalTime:hh\\:mm}-{current.DepartureTime:hh\\:mm}) 与车次 {next.Number} ({next.ArrivalTime:hh\\:mm}-{next.DepartureTime:hh\\:mm}) 时间重叠;";
+ }
+ }
+ }
+ if(!string.IsNullOrEmpty(detail))
+ {
+ detail = $"检查到冲突 {detail.Count('\n')} 个!{detail}\n程序将会继续尝试绘制图像。";
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/CRSim/Views/PlatformDiagramPage.xaml b/CRSim/Views/PlatformDiagramPage.xaml
new file mode 100644
index 0000000..a99b91b
--- /dev/null
+++ b/CRSim/Views/PlatformDiagramPage.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CRSim/Views/PlatformDiagramPage.xaml.cs b/CRSim/Views/PlatformDiagramPage.xaml.cs
new file mode 100644
index 0000000..2b48a20
--- /dev/null
+++ b/CRSim/Views/PlatformDiagramPage.xaml.cs
@@ -0,0 +1,17 @@
+namespace CRSim.Views;
+
+public sealed partial class PlatformDiagramPage : Page
+{
+ public PlatformDiagramPageViewModel ViewModel { get; } = App.AppHost.Services.GetService();
+
+ public PlatformDiagramPage()
+ {
+ InitializeComponent();
+ DataContext = ViewModel;
+ }
+
+ private void Page_Loaded(object sender, RoutedEventArgs e)
+ {
+ App.AppHost.Services.GetService().XamlRoot = this.XamlRoot;
+ }
+}