|
|
#include "nmWxChartWidget.h"
|
|
|
#include "nmDataAnalyzeManager.h"
|
|
|
|
|
|
#include <QPainter>
|
|
|
#include <QMouseEvent>
|
|
|
#include <QApplication>
|
|
|
#include <QtCore/qmath.h>
|
|
|
|
|
|
// 常量定义
|
|
|
const double nmWxChartWidget::POINT_RADIUS = 4.0;
|
|
|
const double nmWxChartWidget::POINT_CLICK_RADIUS = 8.0;
|
|
|
const double nmWxChartWidget::LINE_CLICK_DISTANCE = 5.0;
|
|
|
const double nmWxChartWidget::CROSS_MARK_SIZE = 4.0;
|
|
|
|
|
|
nmWxChartWidget::nmWxChartWidget(QWidget *parent)
|
|
|
: QWidget(parent)
|
|
|
, isDragging(false)
|
|
|
, isDraggingLine(false)
|
|
|
, dragPointIndex(-1)
|
|
|
, showAverageLine(false)
|
|
|
, averageValue(0.0)
|
|
|
, dSkinDqValue(0.0)
|
|
|
, selectionRangeX1(0.0)
|
|
|
, selectionRangeX2(0.0)
|
|
|
, m_snapToRateChanges(true)
|
|
|
, m_lassoMode(false)
|
|
|
, m_lassoDrawing(false)
|
|
|
, m_hasPerformedInitialFit(false)
|
|
|
{
|
|
|
setMouseTracking(true);
|
|
|
setMinimumSize(400, 300);
|
|
|
|
|
|
// 初始化数据范围
|
|
|
dataRect = QRectF(80, -0.5, 140, 1.0); // 默认值
|
|
|
|
|
|
// 初始化选择范围
|
|
|
selectionRangeX1 = 0.0;
|
|
|
selectionRangeX2 = 0.0;
|
|
|
}
|
|
|
|
|
|
nmWxChartWidget::~nmWxChartWidget()
|
|
|
{
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::paintEvent(QPaintEvent *event)
|
|
|
{
|
|
|
Q_UNUSED(event);
|
|
|
|
|
|
QPainter painter(this);
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
int marginLeft = 55;
|
|
|
int marginRight = 10;
|
|
|
int marginTop = 4;
|
|
|
int marginBottom = 45;
|
|
|
|
|
|
chartRect = QRectF(marginLeft, marginTop,
|
|
|
width() - marginLeft - marginRight,
|
|
|
height() - marginTop - marginBottom);
|
|
|
|
|
|
painter.fillRect(rect(), Qt::white);
|
|
|
painter.fillRect(chartRect, Qt::white);
|
|
|
|
|
|
// 绘制网格、坐标轴、线条和点
|
|
|
drawGrid(painter);
|
|
|
drawAxes(painter);
|
|
|
drawCrossMarks(painter); // 绘制×标记
|
|
|
drawLine(painter);
|
|
|
drawAverageLine(painter); // 绘制skin0线
|
|
|
drawPoints(painter);
|
|
|
// 在最后绘制套索(这样它会在所有其他元素之上)
|
|
|
if (m_lassoMode) {
|
|
|
drawLasso(painter);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawGrid(QPainter &painter)
|
|
|
{
|
|
|
painter.setPen(QPen(QColor(220, 220, 220), 1, Qt::DotLine));
|
|
|
|
|
|
// 动态计算网格线间隔
|
|
|
double xRange = dataRect.width();
|
|
|
double yRange = dataRect.height();
|
|
|
|
|
|
double xStep = xRange / 8.0; // 大约8条垂直线
|
|
|
double yStep = yRange / 8.0; // 大约8条水平线
|
|
|
|
|
|
// 规范化步长
|
|
|
xStep = pow(10, floor(log10(xStep))) * (xStep / pow(10, floor(log10(xStep))) >= 5 ? 5 :
|
|
|
(xStep / pow(10, floor(log10(xStep))) >= 2 ? 2 : 1));
|
|
|
yStep = pow(10, floor(log10(yStep))) * (yStep / pow(10, floor(log10(yStep))) >= 5 ? 5 :
|
|
|
(yStep / pow(10, floor(log10(yStep))) >= 2 ? 2 : 1));
|
|
|
|
|
|
// 垂直网格线
|
|
|
double xStart = ceil(dataRect.left() / xStep) * xStep;
|
|
|
|
|
|
for(double x = xStart; x <= dataRect.right(); x += xStep) {
|
|
|
QPointF screenPos = dataToScreen(QPointF(x, dataRect.top()));
|
|
|
painter.drawLine(screenPos.x(), chartRect.top(),
|
|
|
screenPos.x(), chartRect.bottom());
|
|
|
}
|
|
|
|
|
|
// 水平网格线
|
|
|
double yStart = ceil(dataRect.top() / yStep) * yStep;
|
|
|
|
|
|
for(double y = yStart; y <= dataRect.bottom(); y += yStep) {
|
|
|
QPointF screenPos = dataToScreen(QPointF(dataRect.left(), y));
|
|
|
painter.drawLine(chartRect.left(), screenPos.y(),
|
|
|
chartRect.right(), screenPos.y());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawAxes(QPainter& painter)
|
|
|
{
|
|
|
painter.setPen(QPen(Qt::black, 1));
|
|
|
|
|
|
// X轴 - 在图表底部
|
|
|
painter.drawLine(chartRect.left(), chartRect.bottom(),
|
|
|
chartRect.right(), chartRect.bottom());
|
|
|
|
|
|
// Y轴 - 在图表左边
|
|
|
painter.drawLine(chartRect.left(), chartRect.top(),
|
|
|
chartRect.left(), chartRect.bottom());
|
|
|
|
|
|
// 动态计算刻度
|
|
|
double xRange = dataRect.width();
|
|
|
double yRange = dataRect.height();
|
|
|
|
|
|
double xStep = xRange / 5.0;
|
|
|
double yStep = yRange / 5.0;
|
|
|
|
|
|
// 规范化步长
|
|
|
xStep = pow(10, floor(log10(xStep))) * (xStep / pow(10, floor(log10(xStep))) >= 5 ? 5 :
|
|
|
(xStep / pow(10, floor(log10(xStep))) >= 2 ? 2 : 1));
|
|
|
yStep = pow(10, floor(log10(yStep))) * (yStep / pow(10, floor(log10(yStep))) >= 5 ? 5 :
|
|
|
(yStep / pow(10, floor(log10(yStep))) >= 2 ? 2 : 1));
|
|
|
|
|
|
// 针对小范围数据的特殊处理
|
|
|
//if(yRange < 1.0) {
|
|
|
// // 数据范围很小时,使用固定的合理步长
|
|
|
// if(yRange <= 0.5) {
|
|
|
// yStep = 0.1;
|
|
|
// } else {
|
|
|
// yStep = 0.2;
|
|
|
// }
|
|
|
//}
|
|
|
|
|
|
if(yRange < 1.0) {
|
|
|
yStep = 0.1;
|
|
|
}
|
|
|
|
|
|
// 绘制主要刻度线
|
|
|
painter.setPen(QPen(Qt::black, 1));
|
|
|
|
|
|
// X轴主要刻度线和标签
|
|
|
double xStart = ceil(dataRect.left() / xStep) * xStep;
|
|
|
|
|
|
for(double x = xStart; x <= dataRect.right(); x += xStep) {
|
|
|
double xPos = chartRect.left() + (x - dataRect.left()) * chartRect.width() / dataRect.width();
|
|
|
|
|
|
// 绘制大刻度线
|
|
|
painter.drawLine(xPos, chartRect.bottom(), xPos, chartRect.bottom() + 8);
|
|
|
|
|
|
// 绘制刻度标签
|
|
|
int precision = (xStep < 1.0) ? 1 : 0;
|
|
|
QString xLabel = QString::number(x, 'f', precision);
|
|
|
QFontMetrics fm(painter.font());
|
|
|
int textWidth = fm.width(xLabel);
|
|
|
painter.drawText(xPos - textWidth / 2, chartRect.bottom() + 25, xLabel);
|
|
|
}
|
|
|
|
|
|
// 绘制X轴小刻度线(在大刻度之间,不显示数字)
|
|
|
double halfXStep = xStep / 2.0;
|
|
|
for(double x = xStart + halfXStep; x < dataRect.right(); x += xStep) {
|
|
|
double xPos = chartRect.left() + (x - dataRect.left()) * chartRect.width() / dataRect.width();
|
|
|
|
|
|
// 确保小刻度线在图表范围内
|
|
|
if(xPos > chartRect.left() && xPos < chartRect.right()) {
|
|
|
// 绘制小刻度线(长度为4像素,比大刻度线的8像素短)
|
|
|
painter.drawLine(xPos, chartRect.bottom(), xPos, chartRect.bottom() + 4);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Y轴主要刻度线 - 限制刻度数量
|
|
|
double yStart = ceil(dataRect.top() / yStep) * yStep;
|
|
|
|
|
|
// 计算可能的刻度数量,避免过多
|
|
|
int maxTicks = 10; // 最多显示10个刻度
|
|
|
double actualYStep = yStep;
|
|
|
int tickCount = static_cast<int>((dataRect.bottom() - yStart) / yStep) + 1;
|
|
|
|
|
|
// 如果刻度太多,增加步长
|
|
|
while(tickCount > maxTicks && actualYStep > 0) {
|
|
|
actualYStep *= 2;
|
|
|
tickCount = static_cast<int>((dataRect.bottom() - yStart) / actualYStep) + 1;
|
|
|
}
|
|
|
|
|
|
QSet<int> drawnYPixels;
|
|
|
|
|
|
// 绘制Y轴大刻度线和标签
|
|
|
for(double y = yStart; y <= dataRect.bottom(); y += actualYStep) {
|
|
|
QPointF pos = dataToScreen(QPointF(dataRect.left(), y));
|
|
|
int yPixel = qRound(pos.y());
|
|
|
|
|
|
bool alreadyDrawn = false;
|
|
|
QSet<int>::const_iterator it;
|
|
|
|
|
|
for(it = drawnYPixels.constBegin(); it != drawnYPixels.constEnd(); ++it) {
|
|
|
if(qAbs(*it - yPixel) <= 3) { // 增加容差到3像素
|
|
|
alreadyDrawn = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if(!alreadyDrawn) {
|
|
|
drawnYPixels.insert(yPixel);
|
|
|
// 绘制大刻度线
|
|
|
painter.drawLine(chartRect.left() - 8, pos.y(), chartRect.left(), pos.y());
|
|
|
|
|
|
// 智能精度显示
|
|
|
int precision = (actualYStep < 0.1) ? 2 : (actualYStep < 1.0) ? 1 : 0;
|
|
|
painter.drawText(pos.x() - 35, pos.y() + 5, QString::number(y, 'f', precision));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 绘制Y轴小刻度线(在大刻度之间,不显示数字)
|
|
|
double halfYStep = actualYStep / 2.0;
|
|
|
for(double y = yStart + halfYStep; y < dataRect.bottom(); y += actualYStep) {
|
|
|
QPointF pos = dataToScreen(QPointF(dataRect.left(), y));
|
|
|
|
|
|
// 确保小刻度线在图表范围内
|
|
|
if(pos.y() > chartRect.top() && pos.y() < chartRect.bottom()) {
|
|
|
// 检查是否与已绘制的大刻度线位置冲突(避免重叠)
|
|
|
int yPixel = qRound(pos.y());
|
|
|
bool conflictsWithMajor = false;
|
|
|
|
|
|
QSet<int>::const_iterator it;
|
|
|
for(it = drawnYPixels.constBegin(); it != drawnYPixels.constEnd(); ++it) {
|
|
|
if(qAbs(*it - yPixel) <= 2) { // 2像素容差避免与大刻度线重叠
|
|
|
conflictsWithMajor = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if(!conflictsWithMajor) {
|
|
|
// 绘制小刻度线(长度为4像素,比大刻度线的8像素短)
|
|
|
painter.drawLine(chartRect.left() - 4, pos.y(), chartRect.left(), pos.y());
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 轴标题
|
|
|
painter.setFont(QFont("Arial", 10));
|
|
|
painter.drawText(chartRect.center().x() - 50, chartRect.bottom() + 35,
|
|
|
"Liquid rate [STB/D]");
|
|
|
|
|
|
painter.save();
|
|
|
painter.translate(15, chartRect.center().y());
|
|
|
painter.rotate(-90);
|
|
|
painter.drawText(-15, 0, "Skin");
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawLine(QPainter &painter)
|
|
|
{
|
|
|
if(linePoints.size() < 2) return;
|
|
|
|
|
|
// 绘制黑色辅助线(沿着红线方向延伸,但限制在图表区域内)
|
|
|
painter.setPen(QPen(Qt::black, 1));
|
|
|
|
|
|
// 计算红线的斜率和方向
|
|
|
QPointF p1 = linePoints[0];
|
|
|
QPointF p2 = linePoints[1];
|
|
|
|
|
|
// 避免除零错误
|
|
|
if(qAbs(p2.x() - p1.x()) < 0.001) {
|
|
|
// 如果是垂直线,直接绘制垂直的黑线
|
|
|
QPointF topPoint = dataToScreen(QPointF(p1.x(), dataRect.top()));
|
|
|
QPointF bottomPoint = dataToScreen(QPointF(p1.x(), dataRect.bottom()));
|
|
|
painter.drawLine(topPoint, bottomPoint);
|
|
|
} else {
|
|
|
// 计算斜率
|
|
|
double slope = (p2.y() - p1.y()) / (p2.x() - p1.x());
|
|
|
|
|
|
// 使用红线中点作为参考点
|
|
|
double midX = (p1.x() + p2.x()) / 2.0;
|
|
|
double midY = (p1.y() + p2.y()) / 2.0;
|
|
|
|
|
|
// 计算黑线与图表边界的交点
|
|
|
QVector<QPointF> intersections;
|
|
|
|
|
|
// 与左边界的交点
|
|
|
double leftX = dataRect.left();
|
|
|
double leftY = midY + slope * (leftX - midX);
|
|
|
|
|
|
if(leftY >= dataRect.top() && leftY <= dataRect.bottom()) {
|
|
|
intersections.append(QPointF(leftX, leftY));
|
|
|
}
|
|
|
|
|
|
// 与右边界的交点
|
|
|
double rightX = dataRect.right();
|
|
|
double rightY = midY + slope * (rightX - midX);
|
|
|
|
|
|
if(rightY >= dataRect.top() && rightY <= dataRect.bottom()) {
|
|
|
intersections.append(QPointF(rightX, rightY));
|
|
|
}
|
|
|
|
|
|
// 与上边界的交点
|
|
|
double topY = dataRect.top();
|
|
|
double topX = midX + (topY - midY) / slope;
|
|
|
|
|
|
if(topX >= dataRect.left() && topX <= dataRect.right()) {
|
|
|
intersections.append(QPointF(topX, topY));
|
|
|
}
|
|
|
|
|
|
// 与下边界的交点
|
|
|
double bottomY = dataRect.bottom();
|
|
|
double bottomX = midX + (bottomY - midY) / slope;
|
|
|
|
|
|
if(bottomX >= dataRect.left() && bottomX <= dataRect.right()) {
|
|
|
intersections.append(QPointF(bottomX, bottomY));
|
|
|
}
|
|
|
|
|
|
// 如果找到了两个交点,绘制黑线
|
|
|
if(intersections.size() >= 2) {
|
|
|
QPointF point1 = dataToScreen(intersections[0]);
|
|
|
QPointF point2 = dataToScreen(intersections[1]);
|
|
|
painter.drawLine(point1, point2);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 绘制红色主线
|
|
|
painter.setPen(QPen(Qt::red, 3));
|
|
|
|
|
|
for(int i = 0; i < linePoints.size() - 1; ++i) {
|
|
|
QPointF start = dataToScreen(linePoints[i]);
|
|
|
QPointF end = dataToScreen(linePoints[i + 1]);
|
|
|
painter.drawLine(start, end);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawAverageLine(QPainter &painter)
|
|
|
{
|
|
|
if(!showAverageLine) return;
|
|
|
|
|
|
// 绘制水平的红色skin0线
|
|
|
painter.setPen(QPen(Qt::red, 2));
|
|
|
|
|
|
// 计算skin0线的屏幕坐标
|
|
|
QPointF leftPoint = dataToScreen(QPointF(dataRect.left(), averageValue));
|
|
|
QPointF rightPoint = dataToScreen(QPointF(dataRect.right(), averageValue));
|
|
|
|
|
|
// 绘制水平线,跨越整个图表宽度
|
|
|
painter.drawLine(leftPoint, rightPoint);
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawPoints(QPainter& painter)
|
|
|
{
|
|
|
// 绘制红色圆形端点
|
|
|
painter.setPen(QPen(Qt::red, 2));
|
|
|
painter.setBrush(Qt::red);
|
|
|
|
|
|
for(int i = 0; i < linePoints.size(); ++i) {
|
|
|
QPointF screenPos = dataToScreen(linePoints[i]);
|
|
|
painter.drawEllipse(screenPos, POINT_RADIUS, POINT_RADIUS);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawCrossMarks(QPainter& painter)
|
|
|
{
|
|
|
// 绘制×标记
|
|
|
painter.setPen(QPen(Qt::black, 1));
|
|
|
painter.setBrush(Qt::NoBrush);
|
|
|
|
|
|
for(int i = 0; i < crossMarkPoints.size(); ++i) {
|
|
|
QPointF screenPos = dataToScreen(crossMarkPoints[i]);
|
|
|
|
|
|
// 绘制×标记 - 两条对角线
|
|
|
painter.drawLine(screenPos.x() - CROSS_MARK_SIZE, screenPos.y() - CROSS_MARK_SIZE,
|
|
|
screenPos.x() + CROSS_MARK_SIZE, screenPos.y() + CROSS_MARK_SIZE);
|
|
|
painter.drawLine(screenPos.x() - CROSS_MARK_SIZE, screenPos.y() + CROSS_MARK_SIZE,
|
|
|
screenPos.x() + CROSS_MARK_SIZE, screenPos.y() - CROSS_MARK_SIZE);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::calculateAverageValue()
|
|
|
{
|
|
|
double oldSkin0Value = averageValue;
|
|
|
double oldDSkinDqValue = dSkinDqValue;
|
|
|
|
|
|
if(linePoints.size() < 2) {
|
|
|
showAverageLine = false;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 获取当前选择范围的x坐标
|
|
|
selectionRangeX1 = qMin(linePoints[0].x(), linePoints[1].x());
|
|
|
selectionRangeX2 = qMax(linePoints[0].x(), linePoints[1].x());
|
|
|
|
|
|
// 计算拟合直线的斜率和截距
|
|
|
QPointF p1 = linePoints[0];
|
|
|
QPointF p2 = linePoints[1];
|
|
|
|
|
|
// 避免除零错误
|
|
|
if(qAbs(p2.x() - p1.x()) < 0.001) {
|
|
|
// 垂直线或接近垂直线的情况
|
|
|
dSkinDqValue = 0.0; // 设置为0,表示无斜率
|
|
|
averageValue = (p1.y() + p2.y()) / 2.0; // 使用y坐标的平均值
|
|
|
showAverageLine = true;
|
|
|
} else {
|
|
|
// 计算斜率 m = (y2 - y1) / (x2 - x1) - 这就是 dSkin/dq 值
|
|
|
dSkinDqValue = (p2.y() - p1.y()) / (p2.x() - p1.x());
|
|
|
|
|
|
// 计算y轴截距(skin0值): b = y1 - m * x1
|
|
|
averageValue = p1.y() - dSkinDqValue * p1.x();
|
|
|
|
|
|
// 判断是否显示平均线
|
|
|
showAverageLine = shouldShowAverageLine();
|
|
|
}
|
|
|
|
|
|
// 如果值发生变化,发出信号
|
|
|
bool skin0Changed = qAbs(oldSkin0Value - averageValue) > 0.0001;
|
|
|
bool dSkinDqChanged = qAbs(oldDSkinDqValue - dSkinDqValue) > 0.0001;
|
|
|
|
|
|
if(skin0Changed || dSkinDqChanged) {
|
|
|
emit skinValuesChanged(averageValue, dSkinDqValue);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
bool nmWxChartWidget::shouldShowAverageLine()
|
|
|
{
|
|
|
// 当线条移动且范围合理时显示skin0线
|
|
|
double rangeWidth = selectionRangeX2 - selectionRangeX1;
|
|
|
|
|
|
if(rangeWidth < 5.0) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// 检查skin0值是否在合理范围内(在图表y轴范围内)
|
|
|
if(averageValue < dataRect.top() || averageValue > dataRect.bottom()) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::calculateDataRange()
|
|
|
{
|
|
|
if(crossMarkPoints.isEmpty() && linePoints.isEmpty()) {
|
|
|
dataRect = QRectF(80, -0.5, 140, 1.0); // 默认范围
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
double minX = 1e10, maxX = -1e10;
|
|
|
double minY = 1e10, maxY = -1e10;
|
|
|
|
|
|
// 如果有×标记点,优先基于×标记点确定范围
|
|
|
if(!crossMarkPoints.isEmpty()) {
|
|
|
// 首先基于×标记点计算基本范围
|
|
|
for(int i = 0; i < crossMarkPoints.size(); ++i) {
|
|
|
const QPointF& point = crossMarkPoints[i];
|
|
|
minX = qMin(minX, point.x());
|
|
|
maxX = qMax(maxX, point.x());
|
|
|
minY = qMin(minY, point.y());
|
|
|
maxY = qMax(maxY, point.y());
|
|
|
}
|
|
|
|
|
|
// 计算×标记点的范围
|
|
|
double xRange = maxX - minX;
|
|
|
double yRange = maxY - minY;
|
|
|
|
|
|
// 为×标记点添加紧凑的边距
|
|
|
double xMargin, yMargin;
|
|
|
|
|
|
if(xRange < 1e-6) {
|
|
|
// 如果×标记点在同一x位置,创建合理的默认x范围
|
|
|
xMargin = 25.0; // 固定边距
|
|
|
minX = minX - xMargin;
|
|
|
maxX = minX + 2 * xMargin;
|
|
|
} else {
|
|
|
// 添加紧凑边距,约10%,但限制最大值
|
|
|
xMargin = xRange * 0.08; // 减小到8%
|
|
|
xMargin = qMax(xMargin, 5.0); // 最小边距
|
|
|
xMargin = qMin(xMargin, 15.0); // 最大边距限制
|
|
|
minX -= xMargin;
|
|
|
maxX += xMargin;
|
|
|
}
|
|
|
|
|
|
if(yRange < 1e-6) {
|
|
|
// 如果×标记点在同一y位置,创建合理的默认y范围
|
|
|
yMargin = 0.3; // 固定边距
|
|
|
minY = minY - yMargin;
|
|
|
maxY = minY + 2 * yMargin;
|
|
|
} else {
|
|
|
// y轴使用稍大的边距,约12%
|
|
|
yMargin = yRange * 0.12;
|
|
|
yMargin = qMax(yMargin, 0.1);
|
|
|
yMargin = qMin(yMargin, 0.4); // 限制最大边距
|
|
|
minY -= yMargin;
|
|
|
maxY += yMargin;
|
|
|
}
|
|
|
|
|
|
// 现在检查红线端点,但只在它们不会过度扩展范围时才考虑
|
|
|
for(int i = 0; i < linePoints.size(); ++i) {
|
|
|
const QPointF& point = linePoints[i];
|
|
|
|
|
|
// 对于x坐标,如果红线端点超出当前范围,但扩展量合理,则适度扩展
|
|
|
if(point.x() < minX) {
|
|
|
double extension = minX - point.x();
|
|
|
double currentXRange = maxX - minX;
|
|
|
if(extension <= currentXRange * 0.15) { // 不超过当前范围15%的扩展
|
|
|
minX = point.x() - 3.0; // 小边距
|
|
|
}
|
|
|
}
|
|
|
if(point.x() > maxX) {
|
|
|
double extension = point.x() - maxX;
|
|
|
double currentXRange = maxX - minX;
|
|
|
if(extension <= currentXRange * 0.15) {
|
|
|
maxX = point.x() + 3.0; // 小边距
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 对y坐标类似处理
|
|
|
if(point.y() < minY) {
|
|
|
double extension = minY - point.y();
|
|
|
double currentYRange = maxY - minY;
|
|
|
if(extension <= currentYRange * 0.2) {
|
|
|
minY = point.y() - 0.05;
|
|
|
}
|
|
|
}
|
|
|
if(point.y() > maxY) {
|
|
|
double extension = point.y() - maxY;
|
|
|
double currentYRange = maxY - minY;
|
|
|
if(extension <= currentYRange * 0.2) {
|
|
|
maxY = point.y() + 0.05;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
// 如果没有×标记点,只基于红线端点(保持原有逻辑但减小边距)
|
|
|
for(int i = 0; i < linePoints.size(); ++i) {
|
|
|
const QPointF& point = linePoints[i];
|
|
|
minX = qMin(minX, point.x());
|
|
|
maxX = qMax(maxX, point.x());
|
|
|
minY = qMin(minY, point.y());
|
|
|
maxY = qMax(maxY, point.y());
|
|
|
}
|
|
|
|
|
|
// 添加适当但不过大的边距
|
|
|
double xMargin = (maxX - minX) * 0.08; // 减小边距
|
|
|
double yMargin = (maxY - minY) * 0.12;
|
|
|
xMargin = qMax(xMargin, 8.0); // 减小最小边距
|
|
|
yMargin = qMax(yMargin, 0.15);
|
|
|
|
|
|
minX -= xMargin;
|
|
|
maxX += xMargin;
|
|
|
minY -= yMargin;
|
|
|
maxY += yMargin;
|
|
|
}
|
|
|
|
|
|
// 设置最终的数据范围
|
|
|
dataRect = QRectF(minX, minY, maxX - minX, maxY - minY);
|
|
|
|
|
|
// 确保范围不为零
|
|
|
if(dataRect.width() < 1e-6) {
|
|
|
dataRect.setWidth(30.0); // 减小默认宽度
|
|
|
dataRect.moveLeft(dataRect.center().x() - 15.0);
|
|
|
}
|
|
|
|
|
|
if(dataRect.height() < 1e-6) {
|
|
|
dataRect.setHeight(0.8); // 减小默认高度
|
|
|
dataRect.moveTop(dataRect.center().y() - 0.4);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
QPointF nmWxChartWidget::dataToScreen(const QPointF &dataPoint)
|
|
|
{
|
|
|
double x = chartRect.left() +
|
|
|
(dataPoint.x() - dataRect.left()) * chartRect.width() / dataRect.width();
|
|
|
double y = chartRect.bottom() -
|
|
|
(dataPoint.y() - dataRect.top()) * chartRect.height() / dataRect.height();
|
|
|
return QPointF(x, y);
|
|
|
}
|
|
|
|
|
|
QPointF nmWxChartWidget::screenToData(const QPointF &screenPoint)
|
|
|
{
|
|
|
double x = dataRect.left() +
|
|
|
(screenPoint.x() - chartRect.left()) * dataRect.width() / chartRect.width();
|
|
|
double y = dataRect.top() +
|
|
|
(chartRect.bottom() - screenPoint.y()) * dataRect.height() / chartRect.height();
|
|
|
return QPointF(x, y);
|
|
|
}
|
|
|
|
|
|
int nmWxChartWidget::findNearestPoint(const QPointF &screenPos)
|
|
|
{
|
|
|
for(int i = 0; i < linePoints.size(); ++i) {
|
|
|
QPointF pointScreen = dataToScreen(linePoints[i]);
|
|
|
double distance = qSqrt(qPow(pointScreen.x() - screenPos.x(), 2) +
|
|
|
qPow(pointScreen.y() - screenPos.y(), 2));
|
|
|
|
|
|
if(distance <= POINT_CLICK_RADIUS) {
|
|
|
return i;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
bool nmWxChartWidget::isNearLine(const QPointF &screenPos)
|
|
|
{
|
|
|
for(int i = 0; i < linePoints.size() - 1; ++i) {
|
|
|
QPointF p1 = dataToScreen(linePoints[i]);
|
|
|
QPointF p2 = dataToScreen(linePoints[i + 1]);
|
|
|
|
|
|
// 计算点到线段的距离
|
|
|
double A = screenPos.x() - p1.x();
|
|
|
double B = screenPos.y() - p1.y();
|
|
|
double C = p2.x() - p1.x();
|
|
|
double D = p2.y() - p1.y();
|
|
|
|
|
|
double dot = A * C + B * D;
|
|
|
double lenSq = C * C + D * D;
|
|
|
|
|
|
if(lenSq == 0) continue;
|
|
|
|
|
|
double param = dot / lenSq;
|
|
|
|
|
|
double xx, yy;
|
|
|
|
|
|
if(param < 0) {
|
|
|
xx = p1.x();
|
|
|
yy = p1.y();
|
|
|
} else if(param > 1) {
|
|
|
xx = p2.x();
|
|
|
yy = p2.y();
|
|
|
} else {
|
|
|
xx = p1.x() + param * C;
|
|
|
yy = p1.y() + param * D;
|
|
|
}
|
|
|
|
|
|
double distance = qSqrt(qPow(screenPos.x() - xx, 2) + qPow(screenPos.y() - yy, 2));
|
|
|
|
|
|
if(distance <= LINE_CLICK_DISTANCE) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::mousePressEvent(QMouseEvent *event)
|
|
|
{
|
|
|
if (event->button() == Qt::LeftButton) {
|
|
|
lastMousePos = event->pos();
|
|
|
|
|
|
// 如果处于套索模式
|
|
|
if (m_lassoMode) {
|
|
|
m_lassoDrawing = true;
|
|
|
m_lassoPath.clear();
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
m_lassoPath.append(event->pos());
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 原有的点选择和线条拖动逻辑
|
|
|
dragPointIndex = findNearestPoint(event->pos());
|
|
|
|
|
|
if (dragPointIndex != -1) {
|
|
|
// 点击了某个点
|
|
|
isDragging = true;
|
|
|
isDraggingLine = false;
|
|
|
} else if (isNearLine(event->pos())) {
|
|
|
// 点击了线条
|
|
|
isDragging = false;
|
|
|
isDraggingLine = true;
|
|
|
dragOffset = QPointF(0, 0);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::mouseMoveEvent(QMouseEvent *event)
|
|
|
{
|
|
|
// 如果处于套索模式且正在绘制
|
|
|
if (m_lassoMode && m_lassoDrawing) {
|
|
|
m_lassoPath.append(event->pos());
|
|
|
|
|
|
// 实时检测套索内的×标记点
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
for (int i = 0; i < crossMarkPoints.size(); ++i) {
|
|
|
QPointF screenPos = dataToScreen(crossMarkPoints[i]);
|
|
|
if (isPointInPolygon(screenPos, m_lassoPath)) {
|
|
|
m_selectedCrossMarkIndices.append(i);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
update();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 原有的点拖动和线条拖动逻辑
|
|
|
bool needUpdate = false;
|
|
|
|
|
|
if (isDragging && dragPointIndex != -1) {
|
|
|
// 移动单个点
|
|
|
QPointF newDataPos = screenToData(event->pos());
|
|
|
|
|
|
// 限制在数据范围内
|
|
|
newDataPos.setX(qMax(dataRect.left(), qMin(dataRect.right(), newDataPos.x())));
|
|
|
newDataPos.setY(qMax(dataRect.top(), qMin(dataRect.bottom(), newDataPos.y())));
|
|
|
|
|
|
linePoints[dragPointIndex] = newDataPos;
|
|
|
needUpdate = true;
|
|
|
} else if (isDraggingLine) {
|
|
|
// 移动整条线
|
|
|
QPointF currentPos = event->pos();
|
|
|
QPointF delta = currentPos - lastMousePos;
|
|
|
|
|
|
// 计算移动后所有点的新位置
|
|
|
QVector<QPointF> newPoints;
|
|
|
bool canMove = true;
|
|
|
|
|
|
for (int i = 0; i < linePoints.size(); ++i) {
|
|
|
QPointF screenPos = dataToScreen(linePoints[i]);
|
|
|
screenPos += delta;
|
|
|
QPointF newDataPos = screenToData(screenPos);
|
|
|
|
|
|
// 检查是否超出边界
|
|
|
if (newDataPos.x() < dataRect.left() || newDataPos.x() > dataRect.right() ||
|
|
|
newDataPos.y() < dataRect.top() || newDataPos.y() > dataRect.bottom()) {
|
|
|
canMove = false;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
newPoints.append(newDataPos);
|
|
|
}
|
|
|
|
|
|
// 只有在所有点都在边界内时才移动
|
|
|
if (canMove) {
|
|
|
for (int i = 0; i < linePoints.size(); ++i) {
|
|
|
linePoints[i] = newPoints[i];
|
|
|
}
|
|
|
|
|
|
lastMousePos = currentPos;
|
|
|
needUpdate = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果红线位置发生变化,重新计算skin0值
|
|
|
if (needUpdate) {
|
|
|
calculateAverageValue();
|
|
|
update();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::mouseReleaseEvent(QMouseEvent *event)
|
|
|
{
|
|
|
Q_UNUSED(event);
|
|
|
|
|
|
// 如果处于套索模式且完成了绘制
|
|
|
if (m_lassoMode && m_lassoDrawing) {
|
|
|
m_lassoDrawing = false;
|
|
|
|
|
|
// 如果套索路径有足够的点,形成闭合多边形
|
|
|
if (m_lassoPath.size() > 2) {
|
|
|
// 确保多边形闭合
|
|
|
if (m_lassoPath.first() != m_lassoPath.last()) {
|
|
|
m_lassoPath.append(m_lassoPath.first());
|
|
|
}
|
|
|
|
|
|
// 最终确定选中的×标记点
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
for (int i = 0; i < crossMarkPoints.size(); ++i) {
|
|
|
QPointF screenPos = dataToScreen(crossMarkPoints[i]);
|
|
|
if (isPointInPolygon(screenPos, m_lassoPath)) {
|
|
|
m_selectedCrossMarkIndices.append(i);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果有选中的点,尝试删除(包含最少保留2个点的检查)
|
|
|
if (!m_selectedCrossMarkIndices.isEmpty()) {
|
|
|
deleteSelectedCrossMarks();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 清除套索路径
|
|
|
clearLassoSelection();
|
|
|
|
|
|
// 无论是否删除了点,都自动退出套索模式
|
|
|
exitLassoMode();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 原有的拖动结束逻辑
|
|
|
isDragging = false;
|
|
|
isDraggingLine = false;
|
|
|
dragPointIndex = -1;
|
|
|
|
|
|
// 释放鼠标时重新计算skin0值
|
|
|
calculateAverageValue();
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::exitLassoMode()
|
|
|
{
|
|
|
if (!m_lassoMode) {
|
|
|
return; // 如果不在套索模式,直接返回
|
|
|
}
|
|
|
|
|
|
// 退出套索模式
|
|
|
setLassoMode(false);
|
|
|
|
|
|
// 发出信号通知外部组件套索操作完成
|
|
|
emit lassoOperationCompleted();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setFlowSegmentData(const nmDataPerforation& perforationData)
|
|
|
{
|
|
|
m_perforationData = perforationData; // 复制数据
|
|
|
updateCrossMarkPoints();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setFlowData(const QVector<QPointF>& flowData)
|
|
|
{
|
|
|
m_flowData = flowData;
|
|
|
updateCrossMarkPoints();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::regenerateCrossMarkPoints()
|
|
|
{
|
|
|
crossMarkPoints.clear();
|
|
|
crossMarkSegmentIndices.clear();
|
|
|
initialPositions.clear();
|
|
|
|
|
|
if(m_rawFlowData.isEmpty()) {
|
|
|
if(linePoints.isEmpty()) {
|
|
|
linePoints.append(QPointF(100, 0.0));
|
|
|
linePoints.append(QPointF(200, 0.0));
|
|
|
calculateAverageValue();
|
|
|
}
|
|
|
calculateDataRange();
|
|
|
update();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const QVector<FlowSegmentData>& segments = m_perforationData.getFlowSegments();
|
|
|
|
|
|
// 构建×标记点
|
|
|
for(int i = 0; i < segments.size(); ++i) {
|
|
|
const FlowSegmentData& segment = segments[i];
|
|
|
double startTime = segment.segmentStart.getValue().toDouble();
|
|
|
double skinValue = segment.skinValue.getValue().toDouble();
|
|
|
|
|
|
// 确定当前流量段的结束时间
|
|
|
double endTime;
|
|
|
if(i < segments.size() - 1) {
|
|
|
endTime = segments[i + 1].segmentStart.getValue().toDouble();
|
|
|
} else {
|
|
|
double totalTime = 0.0;
|
|
|
for(int j = 0; j < m_rawFlowData.size(); ++j) {
|
|
|
totalTime += m_rawFlowData[j].x();
|
|
|
}
|
|
|
endTime = totalTime;
|
|
|
}
|
|
|
|
|
|
// 计算该流量段的平均流量值
|
|
|
double averageFlowRate = getAverageFlowRateForSegment(startTime, endTime);
|
|
|
// 创建×标记点
|
|
|
QPointF crossMarkPoint(averageFlowRate, skinValue);
|
|
|
crossMarkPoints.append(crossMarkPoint);
|
|
|
crossMarkSegmentIndices.append(i);
|
|
|
initialPositions.append(crossMarkPoint);
|
|
|
}
|
|
|
|
|
|
// 重新计算坐标范围
|
|
|
calculateDataRange();
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::updateCrossMarkPoints()
|
|
|
{
|
|
|
// 如果没有×标记点,说明需要重新生成
|
|
|
if(crossMarkPoints.isEmpty()) {
|
|
|
regenerateCrossMarkPoints();
|
|
|
} else {
|
|
|
// 如果已有×标记点,只更新数据
|
|
|
updateCrossMarkPointsData();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
double nmWxChartWidget::getAverageFlowRateForSegment(double segmentStartTime, double segmentEndTime) const
|
|
|
{
|
|
|
if(m_rawFlowData.isEmpty() || segmentEndTime <= segmentStartTime) {
|
|
|
return 100.0; // 默认值
|
|
|
}
|
|
|
|
|
|
double totalWeightedFlow = 0.0;
|
|
|
double totalDuration = 0.0;
|
|
|
double currentTime = 0.0;
|
|
|
|
|
|
// 遍历原始流量数据 (duration, flowRate)
|
|
|
for(int i = 0; i < m_rawFlowData.size(); ++i) {
|
|
|
const QPointF& segment = m_rawFlowData[i];
|
|
|
double segmentDuration = segment.x();
|
|
|
double flowRate = segment.y();
|
|
|
|
|
|
double flowSegmentStart = currentTime;
|
|
|
double flowSegmentEnd = currentTime + segmentDuration;
|
|
|
|
|
|
// 检查当前流量段是否与目标时间范围有重叠
|
|
|
double overlapStart = qMax(flowSegmentStart, segmentStartTime);
|
|
|
double overlapEnd = qMin(flowSegmentEnd, segmentEndTime);
|
|
|
|
|
|
if(overlapStart < overlapEnd) {
|
|
|
// 有重叠,计算重叠部分的时间加权
|
|
|
double overlapDuration = overlapEnd - overlapStart;
|
|
|
totalWeightedFlow += flowRate * overlapDuration;
|
|
|
totalDuration += overlapDuration;
|
|
|
}
|
|
|
|
|
|
currentTime += segmentDuration;
|
|
|
|
|
|
// 如果已经超出目标时间范围,可以提前退出
|
|
|
if(currentTime >= segmentEndTime) {
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if(totalDuration > 0.0) {
|
|
|
return totalWeightedFlow / totalDuration;
|
|
|
}
|
|
|
|
|
|
// 如果没有重叠或总时长为0,返回默认值
|
|
|
return 100.0;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::resetLinesToInitialPositions()
|
|
|
{
|
|
|
// 使用最小二乘法重新拟合×标记点
|
|
|
if(fitLineToPoints()) {
|
|
|
// 拟合成功,重新计算Skin0值
|
|
|
calculateAverageValue();
|
|
|
|
|
|
//calculateDataRange();
|
|
|
|
|
|
// 发出值改变信号
|
|
|
emit skinValuesChanged(averageValue, dSkinDqValue);
|
|
|
|
|
|
// 更新显示
|
|
|
update();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setSkinValues(double bValue, double kValue)
|
|
|
{
|
|
|
// 直接设置内部值
|
|
|
averageValue = bValue;
|
|
|
dSkinDqValue = kValue;
|
|
|
|
|
|
// 根据这些值计算并设置线条位置
|
|
|
setLinePositionsFromSkinValues(bValue, kValue);
|
|
|
|
|
|
// 显示平均线
|
|
|
showAverageLine = true;
|
|
|
|
|
|
// 更新显示
|
|
|
update();
|
|
|
|
|
|
// 发出信号通知值已改变
|
|
|
emit skinValuesChanged(averageValue, dSkinDqValue);
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setLinePositions(double x1, double y1, double x2, double y2)
|
|
|
{
|
|
|
// 确保坐标在有效范围内
|
|
|
x1 = qMax(dataRect.left(), qMin(dataRect.right(), x1));
|
|
|
y1 = qMax(dataRect.top(), qMin(dataRect.bottom(), y1));
|
|
|
x2 = qMax(dataRect.left(), qMin(dataRect.right(), x2));
|
|
|
y2 = qMax(dataRect.top(), qMin(dataRect.bottom(), y2));
|
|
|
|
|
|
// 设置线条点
|
|
|
linePoints.clear();
|
|
|
linePoints.append(QPointF(x1, y1));
|
|
|
linePoints.append(QPointF(x2, y2));
|
|
|
|
|
|
// 重新计算skin0和dSkin/dq值
|
|
|
calculateAverageValue();
|
|
|
|
|
|
// 更新显示
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::getLinePositions(double& x1, double& y1, double& x2, double& y2) const
|
|
|
{
|
|
|
if(linePoints.size() >= 2) {
|
|
|
x1 = linePoints[0].x();
|
|
|
y1 = linePoints[0].y();
|
|
|
x2 = linePoints[1].x();
|
|
|
y2 = linePoints[1].y();
|
|
|
} else {
|
|
|
// 默认位置
|
|
|
x1 = 100.0;
|
|
|
y1 = 0.0;
|
|
|
x2 = 200.0;
|
|
|
y2 = 0.0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setLinePositionsFromSkinValues(double skin0Value, double dSkinDqValue)
|
|
|
{
|
|
|
if(crossMarkPoints.size() >= 2) {
|
|
|
// 使用×标记点的x坐标范围
|
|
|
double x1 = crossMarkPoints.first().x();
|
|
|
double x2 = crossMarkPoints.last().x();
|
|
|
|
|
|
// 根据线性方程计算y坐标: y = skin0 + dSkinDq * x
|
|
|
double y1 = skin0Value + dSkinDqValue * x1;
|
|
|
double y2 = skin0Value + dSkinDqValue * x2;
|
|
|
|
|
|
// 确保y坐标在数据范围内
|
|
|
y1 = qMax(dataRect.top(), qMin(dataRect.bottom(), y1));
|
|
|
y2 = qMax(dataRect.top(), qMin(dataRect.bottom(), y2));
|
|
|
|
|
|
linePoints.clear();
|
|
|
linePoints.append(QPointF(x1, y1));
|
|
|
linePoints.append(QPointF(x2, y2));
|
|
|
} else {
|
|
|
// 使用默认x坐标范围
|
|
|
double x1 = 100.0;
|
|
|
double x2 = 200.0;
|
|
|
|
|
|
double y1 = skin0Value + dSkinDqValue * x1;
|
|
|
double y2 = skin0Value + dSkinDqValue * x2;
|
|
|
|
|
|
linePoints.clear();
|
|
|
linePoints.append(QPointF(x1, y1));
|
|
|
linePoints.append(QPointF(x2, y2));
|
|
|
}
|
|
|
|
|
|
// 更新选择范围
|
|
|
if(linePoints.size() >= 2) {
|
|
|
selectionRangeX1 = linePoints[0].x();
|
|
|
selectionRangeX2 = linePoints[1].x();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
bool nmWxChartWidget::fitLineToPoints()
|
|
|
{
|
|
|
// 检查是否有足够的×标记点进行拟合
|
|
|
if(crossMarkPoints.size() < 2) {
|
|
|
return false; // 至少需要2个点才能拟合直线
|
|
|
}
|
|
|
|
|
|
// 如果所有点的x坐标相同,无法进行线性拟合
|
|
|
double firstX = crossMarkPoints[0].x();
|
|
|
bool allSameX = true;
|
|
|
|
|
|
for(int i = 1; i < crossMarkPoints.size(); ++i) {
|
|
|
if(qAbs(crossMarkPoints[i].x() - firstX) > 1e-6) {
|
|
|
allSameX = false;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if(allSameX) {
|
|
|
// 所有点垂直分布,创建垂直线
|
|
|
// 设置垂直线的两个端点
|
|
|
linePoints.clear();
|
|
|
linePoints.append(QPointF(firstX, dataRect.top()));
|
|
|
linePoints.append(QPointF(firstX, dataRect.bottom()));
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// 执行最小二乘法拟合
|
|
|
double slope, intercept;
|
|
|
calculateLinearRegression(crossMarkPoints, slope, intercept);
|
|
|
|
|
|
// 设置拟合后的线条位置
|
|
|
setFittedLinePositions(slope, intercept);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::calculateLinearRegression(const QVector<QPointF>& points, double& slope, double& intercept)
|
|
|
{
|
|
|
if(points.size() < 2) {
|
|
|
slope = 0.0;
|
|
|
intercept = 0.0;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
int n = points.size();
|
|
|
double sumX = 0.0, sumY = 0.0, sumXY = 0.0, sumX2 = 0.0;
|
|
|
|
|
|
// 计算各项和值
|
|
|
for(int i = 0; i < n; ++i) {
|
|
|
double x = points[i].x();
|
|
|
double y = points[i].y();
|
|
|
|
|
|
sumX += x;
|
|
|
sumY += y;
|
|
|
sumXY += x * y;
|
|
|
sumX2 += x * x;
|
|
|
}
|
|
|
|
|
|
double denominator = n * sumX2 - sumX * sumX;
|
|
|
|
|
|
if(qAbs(denominator) < 1e-10) {
|
|
|
// 分母接近0,无法计算斜率
|
|
|
slope = 0.0;
|
|
|
intercept = sumY / n; // 使用y的平均值作为截距
|
|
|
} else {
|
|
|
slope = (n * sumXY - sumX * sumY) / denominator;
|
|
|
intercept = (sumY - slope * sumX) / n;
|
|
|
}
|
|
|
|
|
|
// 将计算结果保存到成员变量中
|
|
|
dSkinDqValue = slope; // dSkin/dq就是斜率
|
|
|
averageValue = intercept; // Skin0就是y轴截距
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setFittedLinePositions(double slope, double intercept)
|
|
|
{
|
|
|
// 确定线条的x坐标范围 - 使用×标记点的准确x坐标
|
|
|
double minX, maxX;
|
|
|
|
|
|
if(!crossMarkPoints.isEmpty()) {
|
|
|
// 找到最左边和最右边×标记点的x坐标
|
|
|
minX = crossMarkPoints[0].x();
|
|
|
maxX = crossMarkPoints[0].x();
|
|
|
|
|
|
for(int i = 1; i < crossMarkPoints.size(); ++i) {
|
|
|
double currentX = crossMarkPoints[i].x();
|
|
|
|
|
|
if(currentX < minX) {
|
|
|
minX = currentX;
|
|
|
}
|
|
|
|
|
|
if(currentX > maxX) {
|
|
|
maxX = currentX;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果只有一个×标记点或所有×标记点x坐标相同
|
|
|
if(qAbs(maxX - minX) < 1e-6) {
|
|
|
// 使用该x坐标,并稍微扩展范围以便显示
|
|
|
double centerX = minX;
|
|
|
minX = centerX - 10.0;
|
|
|
maxX = centerX + 10.0;
|
|
|
}
|
|
|
} else {
|
|
|
// 没有×标记点,使用数据范围
|
|
|
minX = dataRect.left();
|
|
|
maxX = dataRect.right();
|
|
|
}
|
|
|
|
|
|
// 根据直线方程 y = slope * x + intercept 计算对应的y坐标
|
|
|
double y1 = slope * minX + intercept;
|
|
|
double y2 = slope * maxX + intercept;
|
|
|
|
|
|
// 限制y坐标在数据范围内
|
|
|
y1 = qMax(dataRect.top(), qMin(dataRect.bottom(), y1));
|
|
|
y2 = qMax(dataRect.top(), qMin(dataRect.bottom(), y2));
|
|
|
|
|
|
// 设置线条端点 - x坐标精确对应最外侧×标记点
|
|
|
linePoints.clear();
|
|
|
linePoints.append(QPointF(minX, y1));
|
|
|
linePoints.append(QPointF(maxX, y2));
|
|
|
|
|
|
// 更新选择范围
|
|
|
selectionRangeX1 = minX;
|
|
|
selectionRangeX2 = maxX;
|
|
|
}
|
|
|
|
|
|
|
|
|
double nmWxChartWidget::getSkin0Value() const
|
|
|
{
|
|
|
return averageValue;
|
|
|
}
|
|
|
|
|
|
double nmWxChartWidget::getDSkinDqValue() const
|
|
|
{
|
|
|
return dSkinDqValue;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setRawFlowData(const QVector<QPointF>& rawFlowData)
|
|
|
{
|
|
|
m_rawFlowData = rawFlowData;
|
|
|
updateCrossMarkPoints();
|
|
|
|
|
|
// 在设置原始流量数据后,进行初始拟合
|
|
|
if (!m_hasPerformedInitialFit && !m_perforationData.getFlowSegments().isEmpty() && crossMarkPoints.size() >= 2) {
|
|
|
if (fitLineToPoints()) {
|
|
|
calculateAverageValue();
|
|
|
emit skinValuesChanged(averageValue, dSkinDqValue);
|
|
|
m_hasPerformedInitialFit = true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setSnapToRateChanges(bool snapToRateChanges)
|
|
|
{
|
|
|
m_snapToRateChanges = snapToRateChanges;
|
|
|
// 当状态改变时,重新更新×标记点
|
|
|
updateCrossMarkPoints();
|
|
|
}
|
|
|
|
|
|
bool nmWxChartWidget::getLassoMode() const {
|
|
|
return m_lassoMode;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::setLassoMode(bool enabled)
|
|
|
{
|
|
|
m_lassoMode = enabled;
|
|
|
m_lassoDrawing = false;
|
|
|
m_lassoPath.clear();
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
|
|
|
if (enabled) {
|
|
|
setCursor(Qt::CrossCursor); // 设置十字光标
|
|
|
} else {
|
|
|
setCursor(Qt::ArrowCursor); // 恢复默认光标
|
|
|
}
|
|
|
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
bool nmWxChartWidget::isPointInPolygon(const QPointF& point, const QPolygonF& polygon)
|
|
|
{
|
|
|
if (polygon.size() < 3) return false;
|
|
|
|
|
|
// 使用射线投射算法判断点是否在多边形内
|
|
|
bool inside = false;
|
|
|
int j = polygon.size() - 1;
|
|
|
|
|
|
for (int i = 0; i < polygon.size(); i++) {
|
|
|
if (((polygon[i].y() > point.y()) != (polygon[j].y() > point.y())) &&
|
|
|
(point.x() < (polygon[j].x() - polygon[i].x()) * (point.y() - polygon[i].y()) /
|
|
|
(polygon[j].y() - polygon[i].y()) + polygon[i].x())) {
|
|
|
inside = !inside;
|
|
|
}
|
|
|
j = i;
|
|
|
}
|
|
|
|
|
|
return inside;
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::deleteSelectedCrossMarks()
|
|
|
{
|
|
|
if (m_selectedCrossMarkIndices.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 检查删除后是否至少还能保留2个×标记点
|
|
|
int remainingPoints = crossMarkPoints.size() - m_selectedCrossMarkIndices.size();
|
|
|
if (remainingPoints < 2) {
|
|
|
// 如果删除后少于2个点,不执行删除操作
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 按索引从大到小排序,避免删除时索引变化的问题
|
|
|
std::sort(m_selectedCrossMarkIndices.begin(), m_selectedCrossMarkIndices.end(), std::greater<int>());
|
|
|
|
|
|
// 从后往前删除选中的×标记点
|
|
|
for (int i = 0; i < m_selectedCrossMarkIndices.size(); ++i) {
|
|
|
int index = m_selectedCrossMarkIndices[i];
|
|
|
if (index >= 0 && index < crossMarkPoints.size()) {
|
|
|
crossMarkPoints.remove(index);
|
|
|
if (index < crossMarkSegmentIndices.size()) {
|
|
|
crossMarkSegmentIndices.remove(index);
|
|
|
}
|
|
|
if (index < initialPositions.size()) {
|
|
|
initialPositions.remove(index);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
|
|
|
// 发出过滤完成信号
|
|
|
emit crossMarksFiltered();
|
|
|
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::drawLasso(QPainter& painter)
|
|
|
{
|
|
|
if (!m_lassoMode || m_lassoPath.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 绘制套索路径
|
|
|
painter.setPen(QPen(QColor(0, 150, 255), 1, Qt::DashLine)); // 蓝色虚线
|
|
|
painter.setBrush(QColor(0, 150, 255, 30)); // 半透明蓝色填充
|
|
|
|
|
|
if (m_lassoPath.size() > 2) {
|
|
|
painter.drawPolygon(m_lassoPath);
|
|
|
} else if (m_lassoPath.size() > 1) {
|
|
|
// 如果路径点数少于3个,只绘制线条
|
|
|
for (int i = 0; i < m_lassoPath.size() - 1; ++i) {
|
|
|
painter.drawLine(m_lassoPath[i], m_lassoPath[i + 1]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::clearLassoSelection()
|
|
|
{
|
|
|
m_lassoPath.clear();
|
|
|
m_selectedCrossMarkIndices.clear();
|
|
|
m_lassoDrawing = false;
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::resetAllCrossMarks()
|
|
|
{
|
|
|
crossMarkPoints.clear();
|
|
|
crossMarkSegmentIndices.clear();
|
|
|
initialPositions.clear();
|
|
|
|
|
|
if(m_rawFlowData.isEmpty()) {
|
|
|
// 如果没有流动段数据或原始流量数据,设置最基本的默认状态
|
|
|
if(linePoints.isEmpty()) {
|
|
|
// 设置一个最基本的默认线条,但不影响后续的拟合
|
|
|
linePoints.append(QPointF(100, 0.0));
|
|
|
linePoints.append(QPointF(200, 0.0));
|
|
|
calculateAverageValue();
|
|
|
}
|
|
|
|
|
|
update();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const QVector<FlowSegmentData>& segments = m_perforationData.getFlowSegments();
|
|
|
|
|
|
// 构建×标记点
|
|
|
for(int i = 0; i < segments.size(); ++i) {
|
|
|
const FlowSegmentData& segment = segments[i];
|
|
|
double startTime = segment.segmentStart.getValue().toDouble();
|
|
|
double skinValue = segment.skinValue.getValue().toDouble();
|
|
|
|
|
|
// 确定当前流量段的结束时间
|
|
|
double endTime;
|
|
|
if(i < segments.size() - 1) {
|
|
|
// 不是最后一个段,结束时间是下一个段的开始时间
|
|
|
endTime = segments[i + 1].segmentStart.getValue().toDouble();
|
|
|
} else {
|
|
|
// 是最后一个段,结束时间是总时间范围
|
|
|
// 计算总时间范围
|
|
|
double totalTime = 0.0;
|
|
|
for(int j = 0; j < m_rawFlowData.size(); ++j) {
|
|
|
totalTime += m_rawFlowData[j].x();
|
|
|
}
|
|
|
endTime = totalTime;
|
|
|
}
|
|
|
|
|
|
// 计算该流量段的平均流量值
|
|
|
double averageFlowRate = getAverageFlowRateForSegment(startTime, endTime);
|
|
|
|
|
|
// 创建×标记点,使用平均流量值作为横坐标
|
|
|
QPointF crossMarkPoint(averageFlowRate, skinValue);
|
|
|
crossMarkPoints.append(crossMarkPoint);
|
|
|
crossMarkSegmentIndices.append(i);
|
|
|
initialPositions.append(crossMarkPoint);
|
|
|
}
|
|
|
|
|
|
// 更新显示
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmWxChartWidget::updateCrossMarkPointsData()
|
|
|
{
|
|
|
if(crossMarkPoints.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const QVector<FlowSegmentData>& segments = m_perforationData.getFlowSegments();
|
|
|
|
|
|
// 只更新现有×标记点的y坐标(skin值),不改变x坐标和点的数量
|
|
|
for(int i = 0; i < crossMarkPoints.size() && i < crossMarkSegmentIndices.size(); ++i) {
|
|
|
int segmentIndex = crossMarkSegmentIndices[i];
|
|
|
if(segmentIndex >= 0 && segmentIndex < segments.size()) {
|
|
|
double newSkinValue = segments[segmentIndex].skinValue.getValue().toDouble();
|
|
|
crossMarkPoints[i].setY(newSkinValue);
|
|
|
}
|
|
|
}
|
|
|
update();
|
|
|
}
|