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.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxChartWidget.cpp

1389 lines
37 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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