|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|