You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

707 lines
26 KiB
C#

using DevExpress.Data.Linq.Helpers;
using DevExpress.Utils;
using DevExpress.XtraCharts;
using DevExpress.XtraEditors;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace KepGridEditor
{
public delegate void CalibrationResultHandler(double k, double b, string formula);
public partial class FormFittingTool : XtraForm
{
// 三次多项式拟合算法辅助类(y = ax ^ 3 + bx ^ 2 + cx + d)
public class CubicPolynomialFit
{
public double A { get; private set; }
public double B { get; private set; }
public double C { get; private set; }
public double D { get; private set; }
public void Fit(List<double> x, List<double> y)
{
int n = x.Count;
if (n < 4) return;
// 构造正规方程组 (Least Squares)
double s0 = n;
double s1 = x.Sum();
double s2 = x.Sum(v => v * v);
double s3 = x.Sum(v => Math.Pow(v, 3));
double s4 = x.Sum(v => Math.Pow(v, 4));
double s5 = x.Sum(v => Math.Pow(v, 5));
double s6 = x.Sum(v => Math.Pow(v, 6));
double sy = y.Sum();
double sxy = x.Zip(y, (a, b) => a * b).Sum();
double sx2y = x.Zip(y, (a, b) => a * a * b).Sum();
double sx3y = x.Zip(y, (a, b) => Math.Pow(a, 3) * b).Sum();
// 4x4 矩阵
double[,] m = {
{ s0, s1, s2, s3 },
{ s1, s2, s3, s4 },
{ s2, s3, s4, s5 },
{ s3, s4, s5, s6 }
};
double[] r = { sy, sxy, sx2y, sx3y };
double[] res = SolveLinearSystem(m, r, 4);
// 结果对应 d, c, b, a
D = res[0];
C = res[1];
B = res[2];
A = res[3];
}
public double Eval(double v)
{
return A * Math.Pow(v, 3) + B * Math.Pow(v, 2) + C * v + D;
}
public string GetFormula(string varName)
{
return $"{A:F8}*{varName}*{varName}*{varName} + {B:F8}*{varName}*{varName} + {C:F6}*{varName} + {D:F6}";
}
private double[] SolveLinearSystem(double[,] matrix, double[] rightPart, int n)
{
double[] result = new double[n];
double[,] a = new double[n, n + 1];
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++) a[i, j] = matrix[i, j];
a[i, n] = rightPart[i];
}
for (int i = 0; i < n; i++)
{
int maxRow = i;
for (int k = i + 1; k < n; k++)
if (Math.Abs(a[k, i]) > Math.Abs(a[maxRow, i])) maxRow = k;
for (int k = i; k <= n; k++) { double tmp = a[maxRow, k]; a[maxRow, k] = a[i, k]; a[i, k] = tmp; }
for (int k = i + 1; k < n; k++)
{
double c = -a[k, i] / a[i, i];
for (int j = i; j <= n; j++)
{
if (i == j) a[k, j] = 0; else a[k, j] += c * a[i, j];
}
}
}
for (int i = n - 1; i >= 0; i--)
{
result[i] = a[i, n] / a[i, i];
for (int k = i - 1; k >= 0; k--) a[k, n] -= a[k, i] * result[i];
}
return result;
}
}
private BindingList<PointData> PointDatas = new BindingList<PointData>(); // 原始数据
private List<PointData> TrendKnobsPoint = new List<PointData>(); // 趋势控制点 (红色方块)
// 交互状态
private bool IsDragging = false;
private PointData EditingKnobPiont = null; // 当前拖拽的曲线控制点
private SeriesPoint DraggedVisualPoint = null; // 当前拖拽的线性手柄
// 相对位移计算 (解决出界问题)
private int StartMouseY;
private double StartDataY;
private double PixelsPerUnitY;
// 数据范围
private double meshZmin = 0, meshZmax = 0;
private double pointZmin = 0, pointZmax = 0;
// 拟合参数
private double LinearK = 1;
private double LinearB = 0;
private CubicPolynomialFit PolyFit = new CubicPolynomialFit();
public event CalibrationResultHandler OnCalibrationComplete;
public event EventHandler OnRestore;
public FormFittingTool()
{
InitializeComponent();
this.StartPosition = FormStartPosition.CenterParent;
if (cboFitType != null)
{
cboFitType.Properties.Items.Clear();
cboFitType.Properties.Items.AddRange(new object[] {
"直线拟合",
"曲线拟合"
});
cboFitType.SelectedIndex = 0;
}
InitializeChartConfiguration();
InitializeEvents();
}
private void InitializeChartConfiguration()
{
chartControl.Series.Clear();
chartControl.RuntimeHitTesting = true;
// 原始数据 (蓝色点)
Series sOrg = new Series("原始数据", ViewType.Point);
sOrg.DataSource = PointDatas;
sOrg.ArgumentDataMember = "X";
sOrg.ValueDataMembers.AddRange("Y");
sOrg.FilterString = "[IsVirtual] = false";
PointSeriesView vOrg = (PointSeriesView)sOrg.View;
vOrg.Color = Color.SteelBlue;
vOrg.PointMarkerOptions.Kind = MarkerKind.Circle;
vOrg.PointMarkerOptions.Size = 8;
// 虚拟点 (橙色点)
Series sVirt = new Series("虚拟点", ViewType.Point);
sVirt.DataSource = PointDatas;
sVirt.ArgumentDataMember = "X";
sVirt.ValueDataMembers.AddRange("Y");
sVirt.FilterString = "[IsVirtual] = true";
PointSeriesView vVirt = (PointSeriesView)sVirt.View;
vVirt.Color = Color.Orange;
vVirt.PointMarkerOptions.Kind = MarkerKind.Circle;
vVirt.PointMarkerOptions.Size = 8;
// 拟合线 (线本身)
Series sLine = new Series("拟合线", ViewType.Line);
sLine.ToolTipEnabled = DefaultBoolean.False;
sLine.CrosshairEnabled = DefaultBoolean.False;
LineSeriesView vLine = (LineSeriesView)sLine.View;
vLine.LineStyle.Thickness = 2;
vLine.MarkerVisibility = DefaultBoolean.False;
// 控制手柄 (红色方块 - 用于交互)
Series sKnobs = new Series("控制点", ViewType.Point);
sKnobs.ShowInLegend = false;
PointSeriesView vKnobs = (PointSeriesView)sKnobs.View;
vKnobs.Color = Color.Red;
vKnobs.PointMarkerOptions.Kind = MarkerKind.Square;
vKnobs.PointMarkerOptions.Size = 12;
vKnobs.PointMarkerOptions.BorderColor = Color.White;
chartControl.Series.AddRange(new Series[] { sOrg, sVirt, sLine, sKnobs });
// 标题坐标轴
chartControl.Titles.Clear();
ChartTitle title = new ChartTitle();
title.Text = "拟合校正";
title.Font = new Font("宋体", 18, FontStyle.Bold);
title.Dock = ChartTitleDockStyle.Top;
chartControl.Titles.Add(title);
if (chartControl.Diagram is XYDiagram diagram)
{
diagram.AxisX.Title.Text = "X图值";
diagram.AxisX.Title.Visibility = DefaultBoolean.True;
diagram.AxisY.Title.Text = "Y井值";
diagram.AxisY.Title.Visibility = DefaultBoolean.True;
diagram.EnableAxisXScrolling = true;
diagram.EnableAxisYScrolling = true;
}
chartControl.ToolTipEnabled = DefaultBoolean.True;
chartControl.Legend.VerticalIndent = 8;
gridControl.DataSource = PointDatas;
sOrg.ToolTipPointPattern = "{HINT}";
sVirt.ToolTipPointPattern = "{HINT}";
gridControl.ForceInitialize();
if (gridView.Columns["X"] != null)
{
gridView.Columns["X"].DisplayFormat.FormatType = DevExpress.Utils.FormatType.Numeric;
gridView.Columns["X"].DisplayFormat.FormatString = "F2"; // F2 表示保留2位小数
}
if (gridView.Columns["Y"] != null)
{
gridView.Columns["Y"].DisplayFormat.FormatType = DevExpress.Utils.FormatType.Numeric;
gridView.Columns["Y"].DisplayFormat.FormatString = "F2"; // F2 表示保留2位小数
}
gridView.OptionsSelection.MultiSelect = true;
gridView.OptionsSelection.MultiSelectMode = DevExpress.XtraGrid.Views.Grid.GridMultiSelectMode.CheckBoxRowSelect;
gridView.OptionsSelection.CheckBoxSelectorColumnWidth = 30;
gridView.OptionsSelection.ShowCheckBoxSelectorInColumnHeader = DevExpress.Utils.DefaultBoolean.True;
if (gridView.Columns["IsVirtual"] != null) gridView.Columns["IsVirtual"].Visible = false;
}
private void InitializeEvents()
{
PointDatas.ListChanged += (s, e) => chartControl.RefreshData();
btnCalibrate.Click += BtnCalibrate_Click;
if (btnAddPoint != null) btnAddPoint.Click += BtnAddPoint_Click;
if (btnRemovePoint != null) btnRemovePoint.Click += BtnRemovePoint_Click;
if (btnRestore != null) btnRestore.Click += BtnRestore_Click;
if (cboFitType != null)
cboFitType.SelectedIndexChanged += (s, e) => PerformFitting(false);
chartControl.MouseDown += ChartControl_MouseDown;
chartControl.MouseMove += ChartControl_MouseMove;
chartControl.MouseUp += ChartControl_MouseUp;
chartControl.CustomDrawCrosshair += ChartControl_CustomDrawCrosshair;
// 线性拖拽时手柄变红
chartControl.CustomDrawSeriesPoint += (s, e) => {
if (cboFitType != null && cboFitType.SelectedIndex == 0 && e.Series.Name == "拟合线")
e.SeriesDrawOptions.Color = Color.Red;
};
gridView.PopupMenuShowing += (s, e) => e.Allow = false;
}
private void PerformFitting(bool isUserClick)
{
if (PointDatas.Count < 2) return;
int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0;
// 基础线性回归 (获取趋势)
double n = PointDatas.Count;
double sumX = PointDatas.Sum(p => p.X);
double sumY = PointDatas.Sum(p => p.Y);
double sumXY = PointDatas.Sum(p => p.X * p.Y);
double sumXX = PointDatas.Sum(p => p.X * p.X);
double denom = (n * sumXX - sumX * sumX);
if (Math.Abs(denom) > 1e-9)
{
LinearK = (n * sumXY - sumX * sumY) / denom;
LinearB = (sumY - LinearK * sumX) / n;
}
else
{
LinearK = 0; LinearB = PointDatas.Average(p => p.Y);
}
// 模式处理
if (fitMode == 0)
{
// 线性模式:清空趋势点
TrendKnobsPoint.Clear();
}
else
{
// 曲线模式:若无控制点,根据线性趋势初始化
if (TrendKnobsPoint.Count == 0)
{
InitializeKnobsFromTrend();
}
// 计算三次多项式
var xList = TrendKnobsPoint.Select(p => p.X).ToList();
var yList = TrendKnobsPoint.Select(p => p.Y).ToList();
PolyFit.Fit(xList, yList);
}
UpdateLineSeries(fitMode);
UpdateFormulaLabel(fitMode);
if (isUserClick) SubmitResult(fitMode);
}
private void InitializeKnobsFromTrend()
{
TrendKnobsPoint.Clear();
if (PointDatas.Count == 0) return;
double minX = PointDatas.Min(p => p.X);
double maxX = PointDatas.Max(p => p.X);
double padding = (maxX - minX) * 0.1;
double startX = minX - padding;
double endX = maxX + padding;
int knobCount = 5;
double step = (endX - startX) / (knobCount - 1);
for (int i = 0; i < knobCount; i++)
{
double x = startX + i * step;
double y = LinearK * x + LinearB; // 落在回归线上
TrendKnobsPoint.Add(new PointData { X = x, Y = y });
}
}
private void UpdateLineSeries(int fitMode)
{
Series sLine = chartControl.Series["拟合线"];
Series sKnobs = chartControl.Series["控制点"];
if (sLine == null || PointDatas.Count == 0) return;
sLine.Points.Clear();
sKnobs.Points.Clear();
double minX = PointDatas.Min(p => p.X);
double maxX = PointDatas.Max(p => p.X);
double padding = (maxX - minX) > 0 ? (maxX - minX) * 0.1 : 1.0;
double startX = minX - padding;
double endX = maxX + padding;
LineSeriesView view = (LineSeriesView)sLine.View;
if (fitMode == 0)
{
// 线性
view.Color = Color.CornflowerBlue;
view.LineStyle.DashStyle = DashStyle.Dash;
double y1 = LinearK * startX + LinearB;
double y2 = LinearK * endX + LinearB;
sKnobs.Points.Add(new SeriesPoint(startX, y1));
sKnobs.Points.Add(new SeriesPoint(endX, y2));
sLine.Points.Add(new SeriesPoint(startX, y1));
sLine.Points.Add(new SeriesPoint(endX, y2));
}
else
{
// 曲线
view.Color = Color.Purple;
view.LineStyle.DashStyle = DashStyle.Solid;
foreach (var knob in TrendKnobsPoint)
sKnobs.Points.Add(new SeriesPoint(knob.X, knob.Y));
int segments = 100;
double step = (endX - startX) / segments;
for (int i = 0; i <= segments; i++)
{
double x = startX + i * step;
sLine.Points.Add(new SeriesPoint(x, PolyFit.Eval(x)));
}
}
}
private void UpdateFormulaLabel(int fitMode)
{
if (fitMode == 0)
{
lblFormula.Text = $"z = {LinearK:F4} * z + {LinearB:F4}";
}
else
{
// z = Az^3 + Bz^2 + Cz + D
lblFormula.Text = $"z = {PolyFit.A:F6}*z^3 + {PolyFit.B:F6}*z^2 + {PolyFit.C:F6}*z + {PolyFit.D:F4}";
}
}
private void SubmitResult(int fitMode)
{
string formulaZ;
double kOut = double.NaN;
double bOut = double.NaN;
if (fitMode == 0)
{
formulaZ = $"{LinearK:F4} * z + {LinearB:F4}";
kOut = LinearK;
bOut = LinearB;
}
else
{
// 曲线公式
formulaZ = PolyFit.GetFormula("z");
}
OnCalibrationComplete?.Invoke(kOut, bOut, formulaZ);
}
// 相对位移拖拽
private void ChartControl_MouseDown(object sender, MouseEventArgs e)
{
int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0;
ChartHitInfo hit = chartControl.CalcHitInfo(e.Location);
Series s = hit.Series as Series;
// 查找拖拽对象
if (s != null && s.Name == "控制点" && hit.SeriesPoint != null)
{
if (fitMode == 0)
{
DraggedVisualPoint = hit.SeriesPoint;
}
else
{
double mx = hit.SeriesPoint.NumericalArgument;
EditingKnobPiont = TrendKnobsPoint.OrderBy(k => Math.Abs(k.X - mx)).FirstOrDefault();
}
IsDragging = true;
}
else
{
// 模糊捕捉
XYDiagram diag = (XYDiagram)chartControl.Diagram;
DiagramCoordinates c = diag.PointToDiagram(e.Location);
if (c != null && !c.IsEmpty)
{
double mx = c.NumericalArgument;
if (fitMode == 1 && TrendKnobsPoint.Count > 0)
{
EditingKnobPiont = TrendKnobsPoint.OrderBy(k => Math.Abs(k.X - mx)).First();
IsDragging = true;
}
else if (fitMode == 0)
{
Series sKnobs = chartControl.Series["控制点"];
if (sKnobs != null && sKnobs.Points.Count >= 2)
{
SeriesPoint p1 = sKnobs.Points[0], p2 = sKnobs.Points[1];
DraggedVisualPoint = Math.Abs(p1.NumericalArgument - mx) < Math.Abs(p2.NumericalArgument - mx) ? p1 : p2;
IsDragging = true;
}
}
}
}
// 初始化相对拖拽参数
if (IsDragging)
{
XYDiagram diagram = (XYDiagram)chartControl.Diagram;
StartMouseY = e.Location.Y;
double refX = 0;
if (fitMode == 0 && DraggedVisualPoint != null)
{
StartDataY = DraggedVisualPoint.Values[0];
refX = DraggedVisualPoint.NumericalArgument;
}
else if (fitMode == 1 && EditingKnobPiont != null)
{
StartDataY = EditingKnobPiont.Y;
refX = EditingKnobPiont.X;
}
ControlCoordinates c1 = diagram.DiagramToPoint(refX, StartDataY);
ControlCoordinates c2 = diagram.DiagramToPoint(refX, StartDataY + 1.0);
PixelsPerUnitY = Math.Abs(c1.Point.Y - c2.Point.Y);
if (PixelsPerUnitY < 0.001) PixelsPerUnitY = 1.0;
// 禁用坐标轴
diagram.EnableAxisXScrolling = false;
diagram.EnableAxisYScrolling = false;
diagram.EnableAxisXZooming = false;
diagram.EnableAxisYZooming = false;
chartControl.Cursor = Cursors.Hand;
}
}
private void ChartControl_MouseMove(object sender, MouseEventArgs e)
{
if (!IsDragging)
{
ChartHitInfo hit = chartControl.CalcHitInfo(e.Location);
Series s = hit.Series as Series;
bool hover = s != null && s.Name == "控制点";
chartControl.Cursor = hover ? Cursors.Hand : Cursors.Default;
return;
}
int fitMode = cboFitType != null ? cboFitType.SelectedIndex : 0;
// 相对位移计算
double deltaPixels = e.Location.Y - StartMouseY;
double deltaValue = deltaPixels / PixelsPerUnitY;
double newYValue = StartDataY - deltaValue;
if (fitMode == 1 && EditingKnobPiont != null)
{
// 曲线
EditingKnobPiont.Y = newYValue;
PerformFitting(false);
chartControl.RefreshData();
}
else if (fitMode == 0 && DraggedVisualPoint != null)
{
// 线性
DraggedVisualPoint.Values[0] = newYValue;
// 反算 K, B
Series sKnobs = chartControl.Series["控制点"];
if (sKnobs != null && sKnobs.Points.Count >= 2)
{
SeriesPoint p1 = sKnobs.Points[0], p2 = sKnobs.Points[1];
double x1 = p1.NumericalArgument, y1 = p1.Values[0];
double x2 = p2.NumericalArgument, y2 = p2.Values[0];
if (Math.Abs(x2 - x1) > 1e-6)
{
LinearK = (y2 - y1) / (x2 - x1);
LinearB = y1 - LinearK * x1;
}
Series sLine = chartControl.Series["拟合线"];
if (sLine.Points.Count >= 2)
{
sLine.Points[0].Values[0] = y1;
sLine.Points[1].Values[0] = y2;
}
}
chartControl.RefreshData();
UpdateFormulaLabel(0);
}
}
private void ChartControl_MouseUp(object sender, MouseEventArgs e)
{
if (IsDragging)
{
IsDragging = false;
EditingKnobPiont = null;
DraggedVisualPoint = null;
chartControl.Cursor = Cursors.Default;
if (chartControl.Diagram is XYDiagram d)
{
d.EnableAxisXScrolling = true;
d.EnableAxisYScrolling = true;
d.EnableAxisXZooming = true;
d.EnableAxisYZooming = true;
}
}
}
public void LoadData(List<double> inputY, List<double> inputZ, List<string> nameList, double zmin, double zmax)
{
meshZmin = zmin; meshZmax = zmax;
PointDatas.Clear();
TrendKnobsPoint.Clear();
int count = Math.Min(inputY.Count, inputZ.Count);
for (int i = 0; i < count; i++)
{
double val = inputY[i];
if (val < pointZmin) pointZmin = val;
if (val > pointZmax) pointZmax = val;
string currentName = (nameList != null && i < nameList.Count)
? nameList[i]
: $"数据{i + 1}";
PointDatas.Add(new PointData { Name = currentName, X = inputY[i], Y = inputZ[i], IsVirtual = false });
}
if (PointDatas.Count <= 2)
{
PointDatas.Add(new PointData { Name = "虚拟点1", X = zmin, Y = pointZmin, IsVirtual = true });
PointDatas.Add(new PointData { Name = "虚拟点2", X = zmax, Y = pointZmax, IsVirtual = true });
}
PerformFitting(false);
}
private void BtnAddPoint_Click(object sender, EventArgs e)
{
double dx = 0, dy = 0;
if (Math.Abs(meshZmax - meshZmin) > 1e-6)
{
dx = (meshZmin + meshZmax) / 2.0;
dy = (pointZmin + pointZmax) / 2.0;
}
else if (PointDatas.Count > 0)
{
dx = PointDatas.Average(p => p.X);
dy = PointDatas.Average(p => p.Y);
}
PointDatas.Add(new PointData { Name = $"虚拟点{PointDatas.Count + 1}", X = Math.Round(dx, 2), Y = Math.Round(dy, 2), IsVirtual = true });
PerformFitting(false);
}
private void BtnRemovePoint_Click(object sender, EventArgs e)
{
//if (gridView.GetFocusedRow() is PointData row && XtraMessageBox.Show($"删除 {row.Name}?", "确认", MessageBoxButtons.YesNo) == DialogResult.Yes)
//{
// PointDatas.Remove(row);
// PerformFitting(false);
//}
int[] selectedRows = gridView.GetSelectedRows();
if (selectedRows.Length == 0)
{
XtraMessageBox.Show("请先勾选要删除的数据点。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (XtraMessageBox.Show($"确定删除选中的 {selectedRows.Length} 个点?", "确认", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
var itemsToDelete = new List<PointData>();
// 收集对象
foreach (int handle in selectedRows)
{
if (handle >= 0)
itemsToDelete.Add(gridView.GetRow(handle) as PointData);
}
// 执行删除
foreach (var item in itemsToDelete)
{
if (item != null) PointDatas.Remove(item);
}
PerformFitting(false);
}
}
private void ChartControl_CustomDrawCrosshair(object sender, CustomDrawCrosshairEventArgs e)
{
// 遍历所有被十字准线捕捉到的元素组
foreach (CrosshairElementGroup group in e.CrosshairElementGroups)
{
// 遍历组内的每个元素 (对应每个 Series 的点)
foreach (CrosshairElement element in group.CrosshairElements)
{
Series series = element.Series;
// 只处理 "原始数据" 和 "虚拟点"
if (series.Name == "原始数据" || series.Name == "虚拟点")
{
// 获取绑定的数据对象
PointData p = element.SeriesPoint.Tag as PointData;
if (p != null)
{
double zVal = p.Y;
double tzVal = p.X;
double diff = zVal - tzVal;
element.LabelElement.Text = $"{zVal:0.##}({diff:0.##})\n{p.Name}";
}
}
else if (series.Name == "拟合线" || series.Name == "控制点")
{
element.Visible = false;
}
}
}
}
private void BtnRestore_Click(object sender, EventArgs e)
{
TrendKnobsPoint.Clear();
PerformFitting(false);
OnRestore?.Invoke(this, EventArgs.Empty);
}
private void BtnCalibrate_Click(object sender, EventArgs e) => PerformFitting(true);
}
}