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

1390 lines
37 KiB
C++

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