587 lines
21 KiB
C#
587 lines
21 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Data;
|
||
using System.Windows.Media;
|
||
using System.Windows.Shapes;
|
||
using NavisworksTransport.Core;
|
||
using NavisworksTransport.Core.Models;
|
||
using NavisworksTransport.UI.WPF.ViewModels;
|
||
|
||
namespace NavisworksTransport.UI.WPF.Views
|
||
{
|
||
/// <summary>
|
||
/// PathAnalysisDialog.xaml 的交互逻辑
|
||
/// 路径规划分析对话框 - 支持多维度评分和同终点组分析(非模态窗口)
|
||
/// </summary>
|
||
public partial class PathAnalysisDialog : Window
|
||
{
|
||
#region 单例模式
|
||
|
||
private static PathAnalysisDialog _currentInstance = null;
|
||
private static readonly object _lock = new object();
|
||
|
||
#endregion
|
||
|
||
public PathAnalysisDialog()
|
||
{
|
||
try
|
||
{
|
||
InitializeComponent();
|
||
var viewModel = new ViewModels.PathAnalysisViewModel();
|
||
DataContext = viewModel;
|
||
|
||
// 订阅分析完成事件,绘制图表
|
||
viewModel.AnalysisCompleted += OnAnalysisCompleted;
|
||
|
||
LogManager.Info("PathAnalysisDialog初始化完成");
|
||
Title = $"路径规划分析 - 多路径对比 [{DateTime.Now:MM-dd HH:mm}]";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"初始化PathAnalysisDialog失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析完成时绘制图表
|
||
/// </summary>
|
||
private void OnAnalysisCompleted(object sender, AnalysisCompletedEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
Dispatcher.Invoke(() =>
|
||
{
|
||
DrawBarChart(e.EndpointGroups);
|
||
DrawRadarChart(e.SelectedGroup);
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"绘制图表失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制柱状图
|
||
/// </summary>
|
||
private void DrawBarChart(List<EndpointGroupViewModel> groups)
|
||
{
|
||
if (groups == null || groups.Count == 0) return;
|
||
|
||
var canvas = BarChartCanvas;
|
||
canvas.Children.Clear();
|
||
|
||
// 收集所有路径数据
|
||
var allPaths = new List<PathDetailedAnalysis>();
|
||
foreach (var group in groups)
|
||
{
|
||
allPaths.AddRange(group.PathAnalyses);
|
||
}
|
||
|
||
if (allPaths.Count == 0) return;
|
||
|
||
// 限制显示数量
|
||
var displayPaths = allPaths.Take(8).ToList();
|
||
|
||
double canvasWidth = canvas.Width;
|
||
double canvasHeight = canvas.Height;
|
||
double marginLeft = 60;
|
||
double marginRight = 20;
|
||
double marginTop = 30;
|
||
double marginBottom = 50;
|
||
double chartWidth = canvasWidth - marginLeft - marginRight;
|
||
double chartHeight = canvasHeight - marginTop - marginBottom;
|
||
|
||
// 绘制坐标轴
|
||
DrawAxis(canvas, marginLeft, marginTop, chartWidth, chartHeight);
|
||
|
||
// 四个维度的颜色
|
||
var colors = new[] {
|
||
Color.FromRgb(40, 167, 69), // 安全性 - 绿
|
||
Color.FromRgb(0, 123, 255), // 效率 - 蓝
|
||
Color.FromRgb(255, 193, 7), // 转弯 - 黄
|
||
Color.FromRgb(23, 162, 184) // 直达性 - 青
|
||
};
|
||
var dimensionNames = new[] { "安全", "效率", "转弯", "直达" };
|
||
|
||
int barGroupCount = displayPaths.Count;
|
||
double groupWidth = chartWidth / barGroupCount;
|
||
double barWidth = groupWidth * 0.7 / 4; // 4个柱子
|
||
|
||
for (int i = 0; i < displayPaths.Count; i++)
|
||
{
|
||
var analysis = displayPaths[i];
|
||
double groupX = marginLeft + i * groupWidth + groupWidth * 0.15;
|
||
double[] scores = new[] { analysis.SafetyScore, analysis.EfficiencyScore,
|
||
analysis.TurnDifficultyScore, analysis.TortuosityScore };
|
||
|
||
// 绘制每个维度的柱子
|
||
for (int j = 0; j < 4; j++)
|
||
{
|
||
double barHeight = scores[j] / 100.0 * chartHeight;
|
||
double barX = groupX + j * barWidth;
|
||
double barY = marginTop + chartHeight - barHeight;
|
||
|
||
var rect = new Rectangle
|
||
{
|
||
Width = barWidth - 2,
|
||
Height = barHeight,
|
||
Fill = new SolidColorBrush(colors[j]),
|
||
RadiusX = 2,
|
||
RadiusY = 2
|
||
};
|
||
Canvas.SetLeft(rect, barX);
|
||
Canvas.SetTop(rect, barY);
|
||
canvas.Children.Add(rect);
|
||
|
||
// 在柱子上方显示分数
|
||
var scoreText = new TextBlock
|
||
{
|
||
Text = scores[j].ToString("F0"),
|
||
FontSize = 8,
|
||
FontWeight = FontWeights.Bold,
|
||
Foreground = new SolidColorBrush(colors[j]),
|
||
TextAlignment = TextAlignment.Center,
|
||
Width = barWidth
|
||
};
|
||
Canvas.SetLeft(scoreText, barX);
|
||
Canvas.SetTop(scoreText, barY - 12);
|
||
canvas.Children.Add(scoreText);
|
||
}
|
||
|
||
// 路径名称(水平显示)
|
||
var label = new TextBlock
|
||
{
|
||
Text = analysis.RouteName,
|
||
FontSize = 9,
|
||
Foreground = new SolidColorBrush(Colors.Gray),
|
||
TextAlignment = TextAlignment.Center,
|
||
Width = groupWidth * 0.85,
|
||
TextWrapping = TextWrapping.Wrap
|
||
};
|
||
Canvas.SetLeft(label, groupX);
|
||
Canvas.SetTop(label, marginTop + chartHeight + 5);
|
||
canvas.Children.Add(label);
|
||
}
|
||
|
||
// 绘制图例
|
||
DrawLegend(canvas, dimensionNames, colors, canvasWidth - 150, 5);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制雷达图
|
||
/// </summary>
|
||
private void DrawRadarChart(EndpointGroupViewModel selectedGroup)
|
||
{
|
||
if (selectedGroup?.PathAnalyses == null || selectedGroup.PathAnalyses.Count == 0) return;
|
||
|
||
var canvas = RadarChartCanvas;
|
||
canvas.Children.Clear();
|
||
|
||
var analyses = selectedGroup.PathAnalyses.Take(5).ToList();
|
||
|
||
double canvasWidth = canvas.Width;
|
||
double canvasHeight = canvas.Height;
|
||
double centerX = canvasWidth / 2;
|
||
double centerY = canvasHeight / 2 + 10;
|
||
double radius = Math.Min(canvasWidth, canvasHeight) / 2 - 30;
|
||
|
||
var dimensions = new[] { "安全性", "效率", "转弯难度", "直达性" };
|
||
int dimensionCount = dimensions.Length;
|
||
|
||
// 绘制网格
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
double r = radius * i / 5;
|
||
var polygon = new Polygon
|
||
{
|
||
Stroke = new SolidColorBrush(Color.FromRgb(200, 200, 200)),
|
||
StrokeThickness = 0.5,
|
||
Fill = Brushes.Transparent
|
||
};
|
||
|
||
for (int j = 0; j < dimensionCount; j++)
|
||
{
|
||
double angle = j * 2 * Math.PI / dimensionCount - Math.PI / 2;
|
||
double x = centerX + r * Math.Cos(angle);
|
||
double y = centerY + r * Math.Sin(angle);
|
||
polygon.Points.Add(new Point(x, y));
|
||
}
|
||
canvas.Children.Add(polygon);
|
||
}
|
||
|
||
// 绘制轴线和标签
|
||
for (int i = 0; i < dimensionCount; i++)
|
||
{
|
||
double angle = i * 2 * Math.PI / dimensionCount - Math.PI / 2;
|
||
double x = centerX + radius * Math.Cos(angle);
|
||
double y = centerY + radius * Math.Sin(angle);
|
||
|
||
// 轴线
|
||
var line = new Line
|
||
{
|
||
X1 = centerX,
|
||
Y1 = centerY,
|
||
X2 = x,
|
||
Y2 = y,
|
||
Stroke = new SolidColorBrush(Color.FromRgb(200, 200, 200)),
|
||
StrokeThickness = 0.5
|
||
};
|
||
canvas.Children.Add(line);
|
||
|
||
// 标签
|
||
var labelX = centerX + (radius + 20) * Math.Cos(angle);
|
||
var labelY = centerY + (radius + 20) * Math.Sin(angle);
|
||
var label = new TextBlock
|
||
{
|
||
Text = dimensions[i],
|
||
FontSize = 10,
|
||
FontWeight = FontWeights.Bold,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(44, 90, 160))
|
||
};
|
||
Canvas.SetLeft(label, labelX - 15);
|
||
Canvas.SetTop(label, labelY - 8);
|
||
canvas.Children.Add(label);
|
||
}
|
||
|
||
// 绘制数据
|
||
var pathColors = new[] {
|
||
Colors.Red, Colors.Blue, Colors.Green, Colors.Orange, Colors.Purple
|
||
};
|
||
|
||
for (int i = 0; i < analyses.Count; i++)
|
||
{
|
||
var analysis = analyses[i];
|
||
double[] scores = new[] { analysis.SafetyScore, analysis.EfficiencyScore,
|
||
analysis.TurnDifficultyScore, analysis.TortuosityScore };
|
||
|
||
var polygon = new Polygon
|
||
{
|
||
Stroke = new SolidColorBrush(pathColors[i % pathColors.Length]),
|
||
StrokeThickness = analysis.IsBestInGroup ? 2.5 : 1.5,
|
||
Fill = new SolidColorBrush(pathColors[i % pathColors.Length]) { Opacity = 0.1 }
|
||
};
|
||
|
||
for (int j = 0; j < dimensionCount; j++)
|
||
{
|
||
double angle = j * 2 * Math.PI / dimensionCount - Math.PI / 2;
|
||
double r = radius * scores[j] / 100;
|
||
double x = centerX + r * Math.Cos(angle);
|
||
double y = centerY + r * Math.Sin(angle);
|
||
polygon.Points.Add(new Point(x, y));
|
||
}
|
||
|
||
canvas.Children.Add(polygon);
|
||
|
||
// 数据点
|
||
for (int j = 0; j < dimensionCount; j++)
|
||
{
|
||
double angle = j * 2 * Math.PI / dimensionCount - Math.PI / 2;
|
||
double r = radius * scores[j] / 100;
|
||
double x = centerX + r * Math.Cos(angle);
|
||
double y = centerY + r * Math.Sin(angle);
|
||
|
||
var ellipse = new Ellipse
|
||
{
|
||
Width = 4,
|
||
Height = 4,
|
||
Fill = new SolidColorBrush(pathColors[i % pathColors.Length])
|
||
};
|
||
Canvas.SetLeft(ellipse, x - 2);
|
||
Canvas.SetTop(ellipse, y - 2);
|
||
canvas.Children.Add(ellipse);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DrawAxis(Canvas canvas, double left, double top, double width, double height)
|
||
{
|
||
// Y轴
|
||
var yAxis = new Line
|
||
{
|
||
X1 = left,
|
||
Y1 = top,
|
||
X2 = left,
|
||
Y2 = top + height,
|
||
Stroke = new SolidColorBrush(Colors.Gray),
|
||
StrokeThickness = 1
|
||
};
|
||
canvas.Children.Add(yAxis);
|
||
|
||
// X轴
|
||
var xAxis = new Line
|
||
{
|
||
X1 = left,
|
||
Y1 = top + height,
|
||
X2 = left + width,
|
||
Y2 = top + height,
|
||
Stroke = new SolidColorBrush(Colors.Gray),
|
||
StrokeThickness = 1
|
||
};
|
||
canvas.Children.Add(xAxis);
|
||
|
||
// Y轴刻度和标签
|
||
for (int i = 0; i <= 5; i++)
|
||
{
|
||
double y = top + height - i * height / 5;
|
||
var tick = new Line
|
||
{
|
||
X1 = left - 3,
|
||
Y1 = y,
|
||
X2 = left,
|
||
Y2 = y,
|
||
Stroke = new SolidColorBrush(Colors.Gray),
|
||
StrokeThickness = 1
|
||
};
|
||
canvas.Children.Add(tick);
|
||
|
||
var label = new TextBlock
|
||
{
|
||
Text = (i * 20).ToString(),
|
||
FontSize = 9,
|
||
Foreground = new SolidColorBrush(Colors.Gray)
|
||
};
|
||
Canvas.SetLeft(label, left - 20);
|
||
Canvas.SetTop(label, y - 6);
|
||
canvas.Children.Add(label);
|
||
}
|
||
}
|
||
|
||
private void DrawLegend(Canvas canvas, string[] names, Color[] colors, double x, double y)
|
||
{
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
var rect = new Rectangle
|
||
{
|
||
Width = 10,
|
||
Height = 10,
|
||
Fill = new SolidColorBrush(colors[i]),
|
||
RadiusX = 2,
|
||
RadiusY = 2
|
||
};
|
||
Canvas.SetLeft(rect, x + i * 35);
|
||
Canvas.SetTop(rect, y);
|
||
canvas.Children.Add(rect);
|
||
|
||
var label = new TextBlock
|
||
{
|
||
Text = names[i],
|
||
FontSize = 9,
|
||
Foreground = new SolidColorBrush(Colors.Gray)
|
||
};
|
||
Canvas.SetLeft(label, x + i * 35 + 12);
|
||
Canvas.SetTop(label, y - 1);
|
||
canvas.Children.Add(label);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示路径分析窗口(单例模式,非模态)
|
||
/// </summary>
|
||
public static PathAnalysisDialog ShowAnalysis(Window owner = null)
|
||
{
|
||
try
|
||
{
|
||
lock (_lock)
|
||
{
|
||
// 如果当前已有实例且仍然打开,则激活窗口
|
||
if (_currentInstance != null)
|
||
{
|
||
try
|
||
{
|
||
if (_currentInstance.IsLoaded)
|
||
{
|
||
_currentInstance.Activate();
|
||
_currentInstance.Focus();
|
||
LogManager.Info("PathAnalysisDialog已激活现有窗口");
|
||
return _currentInstance;
|
||
}
|
||
}
|
||
catch (Exception)
|
||
{
|
||
// 如果访问失败,认为窗口已关闭,创建新实例
|
||
_currentInstance = null;
|
||
}
|
||
}
|
||
|
||
// 创建新实例
|
||
var dialog = new PathAnalysisDialog();
|
||
|
||
if (owner != null)
|
||
{
|
||
dialog.Owner = owner;
|
||
}
|
||
|
||
// 设置为当前实例
|
||
_currentInstance = dialog;
|
||
|
||
// 使用Show()非模态显示,允许与主窗口同时操作
|
||
dialog.Show();
|
||
LogManager.Info("PathAnalysisDialog已以非模态方式显示");
|
||
|
||
return dialog;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"显示路径分析窗口失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("用户关闭路径分析对话框");
|
||
this.Close();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"关闭PathAnalysisDialog时发生错误: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
protected override void OnClosed(EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
LogManager.Info("PathAnalysisDialog已关闭");
|
||
|
||
// 清理单例引用
|
||
lock (_lock)
|
||
{
|
||
if (_currentInstance == this)
|
||
{
|
||
_currentInstance = null;
|
||
}
|
||
}
|
||
|
||
base.OnClosed(e);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogManager.Error($"PathAnalysisDialog清理过程中发生错误: {ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
#region 值转换器
|
||
|
||
/// <summary>
|
||
/// 分数转颜色转换器
|
||
/// </summary>
|
||
public class ScoreToColorConverter : IValueConverter
|
||
{
|
||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
if (value is double score)
|
||
{
|
||
if (score >= 80) return new SolidColorBrush(Colors.Green);
|
||
if (score >= 60) return new SolidColorBrush(Colors.Orange);
|
||
return new SolidColorBrush(Colors.Red);
|
||
}
|
||
return new SolidColorBrush(Colors.Gray);
|
||
}
|
||
|
||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
return Binding.DoNothing;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 优先级转颜色转换器
|
||
/// </summary>
|
||
public class PriorityToColorConverter : IValueConverter
|
||
{
|
||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
if (value is int priority)
|
||
{
|
||
if (priority >= 4) return new SolidColorBrush(Color.FromRgb(220, 53, 69)); // 红色 - 高优先级
|
||
if (priority >= 3) return new SolidColorBrush(Color.FromRgb(255, 193, 7)); // 黄色 - 中优先级
|
||
return new SolidColorBrush(Color.FromRgb(40, 167, 69)); // 绿色 - 低优先级
|
||
}
|
||
return new SolidColorBrush(Colors.Gray);
|
||
}
|
||
|
||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
return Binding.DoNothing;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 策略描述转换器
|
||
/// </summary>
|
||
public class StrategyDescriptionConverter : IValueConverter
|
||
{
|
||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
if (value is string strategy)
|
||
{
|
||
switch (strategy)
|
||
{
|
||
case "安全优先":
|
||
return "优先选择无碰撞路径,适合精密组件运输(安全权重50%)";
|
||
case "效率优先":
|
||
return "优先选择最短路径,适合紧急任务(效率权重40%)";
|
||
case "平衡模式":
|
||
return "综合考虑安全与效率,通用场景推荐(默认)";
|
||
default:
|
||
return "请选择分析策略";
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
return Binding.DoNothing;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 布尔转背景色转换器
|
||
/// </summary>
|
||
public class BoolToBackgroundConverter : IValueConverter
|
||
{
|
||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
if (value is bool isSelected && isSelected)
|
||
{
|
||
return new SolidColorBrush(Color.FromRgb(230, 240, 255)); // 选中时的浅蓝色
|
||
}
|
||
return new SolidColorBrush(Colors.Transparent);
|
||
}
|
||
|
||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
return Binding.DoNothing;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Null转Visibility转换器
|
||
/// </summary>
|
||
public class NullToVisibilityConverter : IValueConverter
|
||
{
|
||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
return value == null ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|