#include "nmWxChartWidget.h" #include "nmDataAnalyzeManager.h" #include #include #include #include // 常量定义 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(), QColor(240, 240, 240)); painter.fillRect(chartRect, QColor(248, 248, 248)); // 绘制网格、坐标轴、线条和点 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((dataRect.bottom() - yStart) / yStep) + 1; // 如果刻度太多,增加步长 while(tickCount > maxTicks && actualYStep > 0) { actualYStep *= 2; tickCount = static_cast((dataRect.bottom() - yStart) / actualYStep) + 1; } QSet 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::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::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 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 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& 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& 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& 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& 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()); // 从后往前删除选中的×标记点 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& 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& 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(); }